mojulo 0.0.0 → 0.1.1
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 +54 -4
- package/lib/audit-logger-new.js +11 -0
- package/lib/auth/gate.js +25 -0
- package/lib/auth/service.js +17 -0
- package/lib/auth/session.js +63 -0
- package/lib/builder/chat-processor.js +607 -0
- package/lib/builder/composer-bridge.js +82 -0
- package/lib/builder/evaluator.js +159 -0
- package/lib/builder/executor.js +252 -0
- package/lib/builder/index.js +48 -0
- package/lib/builder/session.js +248 -0
- package/lib/builder/system-prompt.js +422 -0
- package/lib/builder/tone-presets.js +75 -0
- package/lib/builder/tool-executors.js +1527 -0
- package/lib/builder/tools.js +338 -0
- package/lib/builder/validators.js +239 -0
- package/lib/composer/composer.js +225 -0
- package/lib/composer/index.js +40 -0
- package/lib/composer/protocols/00_base.txt +19 -0
- package/lib/composer/protocols/01_knowledge.txt +9 -0
- package/lib/composer/protocols/02_form-gathering.txt +32 -0
- package/lib/composer/protocols/03_appointments.txt +16 -0
- package/lib/composer/protocols/04_triage.txt +15 -0
- package/lib/composer/protocols/05_optical-read.txt +22 -0
- package/lib/composer/response-builder.js +98 -0
- package/lib/config-builder.js +650 -0
- package/lib/db/ids.js +10 -0
- package/lib/db/index.js +179 -0
- package/lib/db/repositories/apiKeys.js +72 -0
- package/lib/db/repositories/auditLogs.js +12 -0
- package/lib/db/repositories/botSpaces.js +12 -0
- package/lib/db/repositories/builderSessions.js +312 -0
- package/lib/db/repositories/deploymentEvents.js +12 -0
- package/lib/db/repositories/deployments.js +385 -0
- package/lib/db/repositories/documents.js +68 -0
- package/lib/db/repositories/mcpJobs.js +84 -0
- package/lib/deployers/bot-fleet.js +110 -0
- package/lib/deployers/bot-proxy.js +72 -0
- package/lib/deployers/build.js +89 -0
- package/lib/deployers/cloud-deploy.js +310 -0
- package/lib/deployers/docker.js +439 -0
- package/lib/deployers/fly.js +432 -0
- package/lib/deployers/index.js +38 -0
- package/lib/deployment-auth.js +36 -0
- package/lib/document-parser.js +171 -0
- package/lib/embedder/chunker.js +93 -0
- package/lib/embedder/local.js +101 -0
- package/lib/embedder/preview-rag.js +93 -0
- package/lib/envelope-schema.js +54 -0
- package/lib/fleet/scoped-sql.js +342 -0
- package/lib/form-schema-config/base.js +135 -0
- package/lib/form-schema-config/index.js +286 -0
- package/lib/form-schema-config/locales/af-ZA.js +153 -0
- package/lib/form-schema-config/locales/ar-AE.js +142 -0
- package/lib/form-schema-config/locales/ar-SA.js +164 -0
- package/lib/form-schema-config/locales/de-DE.js +152 -0
- package/lib/form-schema-config/locales/en-AU.js +161 -0
- package/lib/form-schema-config/locales/en-CA.js +115 -0
- package/lib/form-schema-config/locales/en-GB.js +132 -0
- package/lib/form-schema-config/locales/en-IN.js +219 -0
- package/lib/form-schema-config/locales/en-MY.js +171 -0
- package/lib/form-schema-config/locales/en-NG.js +198 -0
- package/lib/form-schema-config/locales/en-PH.js +186 -0
- package/lib/form-schema-config/locales/en-SG.js +153 -0
- package/lib/form-schema-config/locales/en-US.js +138 -0
- package/lib/form-schema-config/locales/es-ES.js +171 -0
- package/lib/form-schema-config/locales/es-MX.js +193 -0
- package/lib/form-schema-config/locales/fr-CA.js +138 -0
- package/lib/form-schema-config/locales/fr-FR.js +155 -0
- package/lib/form-schema-config/locales/hi-IN.js +219 -0
- package/lib/form-schema-config/locales/it-IT.js +157 -0
- package/lib/form-schema-config/locales/ja-JP.js +169 -0
- package/lib/form-schema-config/locales/ko-KR.js +140 -0
- package/lib/form-schema-config/locales/nl-NL.js +149 -0
- package/lib/form-schema-config/locales/pt-BR.js +168 -0
- package/lib/form-schema-config/locales/zh-CN.js +172 -0
- package/lib/form-schema-config/locales/zh-HK.js +142 -0
- package/lib/form-structure-schema.js +191 -0
- package/lib/llm-providers.js +828 -0
- package/lib/markdown.js +197 -0
- package/lib/mcp/catalysts/appointment-to-calendar.md +84 -0
- package/lib/mcp/catalysts/conversations-to-channel-digest.md +104 -0
- package/lib/mcp/catalysts/document-extract-to-store.md +92 -0
- package/lib/mcp/catalysts/knowledge-gap-miner.md +96 -0
- package/lib/mcp/catalysts/loader.js +144 -0
- package/lib/mcp/catalysts/qualify-lead-to-crm.md +83 -0
- package/lib/mcp/catalysts/scan-conversations-for-signal.md +92 -0
- package/lib/mcp/catalysts/submission-to-ticket.md +83 -0
- package/lib/mcp/catalysts/submissions-to-warehouse.md +103 -0
- package/lib/mcp/catalysts/weekly-submissions-digest.md +82 -0
- package/lib/mcp/jobs.js +64 -0
- package/lib/mcp/server.js +184 -0
- package/lib/mcp/session-binding.js +130 -0
- package/lib/mcp/tools/build.js +123 -0
- package/lib/mcp/tools/catalysts.js +477 -0
- package/lib/mcp/tools/context.js +325 -0
- package/lib/mcp/tools/fleet.js +391 -0
- package/lib/mcp/tools/jobs-tools.js +240 -0
- package/lib/mcp/tools/operate.js +314 -0
- package/lib/preview/build-preview-config.js +136 -0
- package/lib/rate-limiter.js +11 -0
- package/lib/resolve-api-key.js +142 -0
- package/lib/storage/index.js +40 -0
- package/messages/de.json +2136 -0
- package/messages/en.json +2136 -0
- package/messages/es.json +2136 -0
- package/messages/fr.json +2136 -0
- package/messages/it.json +2136 -0
- package/messages/ja.json +2136 -0
- package/messages/ko.json +2136 -0
- package/messages/nl.json +2136 -0
- package/messages/pl.json +2136 -0
- package/messages/pt.json +2136 -0
- package/messages/ru.json +2136 -0
- package/messages/uk.json +2136 -0
- package/messages/zh.json +2136 -0
- package/package.json +68 -5
- package/scripts/mcp-config.mjs +162 -0
- package/scripts/mcp-stdio-loader.mjs +42 -0
- package/scripts/mcp-stdio.mjs +108 -0
- package/scripts/mojulo-paths.mjs +48 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Ring 3 — catalysts.
|
|
3
|
+
*
|
|
4
|
+
* Exposes the curated library of workflow patterns shipped with mojulo. The
|
|
5
|
+
* user's Claude calls `list_catalysts` to discover what patterns exist,
|
|
6
|
+
* `get_catalyst` to read the full prose body for a chosen pattern, then
|
|
7
|
+
* combines that with `get_deployment` (operate ring) and its own installed
|
|
8
|
+
* MCPs to synthesize a concrete skill into the user's `.claude/skills/`.
|
|
9
|
+
*
|
|
10
|
+
* The "catalyst" framing is literal: each pattern enables one phase transition
|
|
11
|
+
* from user intent + bot shape + destination MCP into a structured skill
|
|
12
|
+
* artifact. The catalyst itself is not consumed and does not appear in the
|
|
13
|
+
* resulting skill.
|
|
14
|
+
*
|
|
15
|
+
* The term is deliberately bare — not "skill catalyst" — so the concept stays
|
|
16
|
+
* conceptually distinct from the skill it produces. Catalysts catalyze skills;
|
|
17
|
+
* they are not themselves skills.
|
|
18
|
+
*
|
|
19
|
+
* Catalysts are read-only from MCP. Authoring lives in the repo
|
|
20
|
+
* ([control/lib/mcp/catalysts/](control/lib/mcp/catalysts/)) — see
|
|
21
|
+
* [docs/catalysts.md](docs/catalysts.md).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { getCatalyst, getCatalystCatalog, listCatalysts } from '@/lib/mcp/catalysts/loader';
|
|
25
|
+
import { DeploymentRepository } from '@/lib/db/repositories/deployments';
|
|
26
|
+
import { registerTool } from '@/lib/mcp/server';
|
|
27
|
+
|
|
28
|
+
// Prepended to every catalyst body returned by get_catalyst. Two jobs:
|
|
29
|
+
//
|
|
30
|
+
// 1. Posture preamble — explicitly authorize the model to treat the catalyst
|
|
31
|
+
// as a starting point and apply judgment. Without this, models tend to
|
|
32
|
+
// recite the recipe even when the user's situation doesn't fit. Strategic
|
|
33
|
+
// nudge: catalysts are inspiration + tested patterns, not contracts.
|
|
34
|
+
//
|
|
35
|
+
// 2. Vocabulary disambiguation — three terms in this space ("skill",
|
|
36
|
+
// "protocol", "catalyst") overlap enough that the model needs grounding
|
|
37
|
+
// on each read, not just once in a tool description it may not re-consult
|
|
38
|
+
// mid-task.
|
|
39
|
+
//
|
|
40
|
+
// Exported for tests.
|
|
41
|
+
export const SYNTHESIZER_BRIEFING = `# How to read this catalyst — posture first
|
|
42
|
+
|
|
43
|
+
This catalyst is a **starting point, not a contract.** The library is non-exhaustive. Treat it as a worked example to learn from, not a recipe to recite.
|
|
44
|
+
|
|
45
|
+
- **Adapt freely.** Combine elements across catalysts, skip sections that don't apply to the user's situation, add steps the catalyst didn't anticipate. The value is its *thinking* — mapping intent, idempotency strategy, pitfalls — not its literal prose.
|
|
46
|
+
- **No catalyst fits the user's intent? Write from scratch.** Don't force a mismatched pattern onto the user's request. Synthesize directly from their goal and the bot's shape, drawing on judgment absorbed from any catalysts you've read.
|
|
47
|
+
- **Pitfalls in the body still apply when you adapt.** The PII-through-the-LLM warnings, rate-limit notes, irreversible-write cautions, and calibration advice generalize across patterns — they're not catalyst-specific gotchas. Carry them forward even when you deviate from the catalyst's prescribed flow.
|
|
48
|
+
- **Safety defaults are standing posture, not negotiable.** Regardless of path: (1) default \`dryRun: true\` for any external write, requiring explicit per-run opt-in for live mode; (2) include mojulo trace (deployment id, conversation id, submission id, captured-at) in every destination payload so reviewers can walk back to the source.
|
|
49
|
+
- **Encode the dry-run as a skill step, not a conversational promise.** Mojulo synthesizes; it does not certify (see the verification posture in \`forward_context\`). The dry-run / inspect / promote sequence belongs *inside the SKILL.md*: write a concrete first-invocation pattern ("first run with \`dryRun: true\` on one real record; surface the destination payload; wait for explicit go-live") rather than relying on the user to remember they're supposed to dry-run. A skill that defaults \`dryRun: true\` but doesn't *demonstrate* dry-run as its first step gets skipped under deadline pressure.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
# Vocabulary — three concepts kept distinct
|
|
54
|
+
|
|
55
|
+
Three terms easy to confuse. The term **catalyst** is intentionally bare — not prefixed with "skill" — because catalysts produce skills; they are not themselves a sub-type of skill:
|
|
56
|
+
|
|
57
|
+
- **Mojulo protocols** are a *bot's* runtime capabilities (\`knowledge\`, \`formGathering\`, \`triage\`, \`appointments\`, \`opticalRead\`). A deployed mojulo bot has zero or more enabled — they determine what the bot does when it talks to end users. The \`requires.protocols\` field in this catalyst's metadata names which protocols the target bot must have for it to apply. You read enabled protocols off a deployment via \`get_deployment\`.
|
|
58
|
+
- **A Claude Code skill** is a *user-owned local file* (\`.claude/skills/<name>/SKILL.md\`) that you (Claude) read and execute when invoked in a Claude Code session. The skill is the artifact you are about to **synthesize and write to disk**. Mojulo does not host, execute, or store skills — once you write it, it belongs entirely to the user.
|
|
59
|
+
- **This document** is a *mojulo catalyst* — a workflow recipe mojulo ships through MCP. The name is literal: you read it once to **catalyze** the synthesis of a skill from the user's intent, the bot's shape, and the destination MCP. The catalyst itself doesn't end up in the resulting skill — it's the nucleation point that lets a structured skill crystallize out. After synthesis, the catalyst is no longer referenced. Catalysts are not a sub-type of skill; they're a separate concept that *produces* skills.
|
|
60
|
+
|
|
61
|
+
Your job: combine this catalyst's body (or your judgment-driven adaptation of it), the target bot's shape (from \`get_deployment\`), and the destination MCP the user has installed locally → write a concrete \`SKILL.md\` (plus any helper files) into \`.claude/skills/\`.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
export async function listCatalystsHandler(input, _ctx) {
|
|
68
|
+
const { category } = input || {};
|
|
69
|
+
const catalysts = listCatalysts({ category });
|
|
70
|
+
return { total: catalysts.length, catalysts };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function getCatalystHandler(input, _ctx) {
|
|
74
|
+
const { id } = input || {};
|
|
75
|
+
if (!id) throw new Error('id is required');
|
|
76
|
+
const catalyst = getCatalyst(id);
|
|
77
|
+
if (!catalyst) throw new Error(`Catalyst not found: ${id}`);
|
|
78
|
+
return { ...catalyst, body: SYNTHESIZER_BRIEFING + catalyst.body };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Returned by custom_catalyst. The audience here is the **opposite** of the
|
|
82
|
+
// in-repo /write-catalyst skill's audience: this body is for a Claude Code
|
|
83
|
+
// session connected to mojulo over MCP whose user wants to contribute a new
|
|
84
|
+
// catalyst back to the library. They do not have the mojulo repo, the spec
|
|
85
|
+
// doc, the loader, or the exemplar files on disk — so this body has to be
|
|
86
|
+
// self-contained. The exemplars are reachable via the existing `get_catalyst`
|
|
87
|
+
// tool; we point at them rather than inlining them.
|
|
88
|
+
//
|
|
89
|
+
// Exported for tests.
|
|
90
|
+
export const CUSTOM_CATALYST_GUIDE = `# Drafting a custom catalyst — author's guide
|
|
91
|
+
|
|
92
|
+
You are about to help the user draft a new mojulo catalyst — a curated workflow recipe that ships through this MCP. Catalysts you author here are **proposals** to the mojulo library. If a maintainer accepts the PR, your catalyst ships to every mojulo user as a peer of the canonical entries. That is the bar to write to.
|
|
93
|
+
|
|
94
|
+
A catalyst is *not* a Claude Code skill, *not* a mojulo bot capability ("protocol"), and *not* a one-off automation for this specific user. If you're unclear on the distinction, call \`forward_context\` first — it disambiguates all three terms.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Read these first
|
|
99
|
+
|
|
100
|
+
Before you write a single line, anchor on what the bar looks like. Call:
|
|
101
|
+
|
|
102
|
+
1. \`list_catalysts\` — see every shipped pattern (id, summary, category).
|
|
103
|
+
2. \`get_catalyst("qualify-lead-to-crm")\` — the canonical exemplar. Study its mapping section, idempotency section, and pitfalls section specifically. That is the density you have to match.
|
|
104
|
+
3. \`get_catalyst("<closest existing id>")\` — whichever catalyst is closest in shape to the user's intent. If the user wants a digest pattern, read \`weekly-submissions-digest\`. If extraction, read \`document-extract-to-store\`. Etc.
|
|
105
|
+
|
|
106
|
+
The body you draft is a **prompt that has to teach a future Claude how to synthesize a working skill on first try.** It is not documentation for a human reader. The exemplars show what that looks like. Don't skim them.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Step 1 — Posture-check (push back here, before gathering anything else)
|
|
111
|
+
|
|
112
|
+
A catalyst is the **wrong tool** in these cases. If any apply, stop and tell the user — don't try to force the request into a catalyst shape.
|
|
113
|
+
|
|
114
|
+
1. **The request changes what the bot *does* during a conversation.** That's a mojulo protocol, not a catalyst. Protocols change what the bot does *inside* a conversation; catalysts change what happens with the bot's data *afterward*. Protocols are a control-plane code change, not a contributor catalyst.
|
|
115
|
+
2. **The workflow writes back to the bot's corpus or config.** Forbidden by body principle 4 below. Catalysts read from mojulo and write to *destinations* only — never back into the bot.
|
|
116
|
+
3. **The request is bot-specific or one-off.** Catalysts are shipped library entries — reusable across bots and users. If it's bespoke, the user should have you synthesize a \`.claude/skills/\` skill directly with no catalyst — that's already a supported path.
|
|
117
|
+
4. **The destination is one specific MCP, not a category.** A catalyst's value is destination-agnostic mapping intent (\`crm-like\`, \`calendar-like\`, \`actuator-like\`, etc.). "Sync to my specific Notion database with this exact schema" is a skill, not a catalyst.
|
|
118
|
+
5. **The "mapping intent" is generic.** If the user can't articulate at least one non-obvious, opinionated decision the catalyst encodes — a specific field-mapping choice, a default behavior, a calibration heuristic — the catalyst won't pay rent.
|
|
119
|
+
- **Bad mapping insight:** "map the form fields to the CRM contact fields by name." (The synthesizer would already do this without a catalyst.)
|
|
120
|
+
- **Good mapping insight:** "HubSpot splits identity into \`firstname\`/\`lastname\` while Salesforce uses \`FirstName\`/\`LastName\` and Attio uses object/attribute pairs — synthesize the right shape from the destination MCP's surface, never assume a flat \`name\` field." (Specific, opinionated, would be guessed wrong by default.)
|
|
121
|
+
6. **No clear idempotency story.** Without a cursor field AND a dedupe key, the Idempotency section becomes hand-waving and the synthesized skill will double-write or skip records under real conditions.
|
|
122
|
+
- **Bad idempotency story:** "the skill should be idempotent." (Aspiration, not mechanism.)
|
|
123
|
+
- **Good idempotency story:** "cursor on submission \`captured_at\` via a \`since\` parameter; dedupe on the user-configured \`dedupeKey\` (typically email or phone) with a search-before-create against the destination — two layers because the cursor doesn't catch a user re-running an old window."
|
|
124
|
+
|
|
125
|
+
When pushing back, name the specific failure and suggest the right alternative (mojulo protocol PR, local-only skill, more specific request). Don't soften — the library is curated, and a thin catalyst dilutes it.
|
|
126
|
+
|
|
127
|
+
**Example pushback exchange (do this, don't fudge):**
|
|
128
|
+
|
|
129
|
+
> User: "I want a catalyst that automatically emails me a daily summary of conversations from my bot."
|
|
130
|
+
>
|
|
131
|
+
> You: That's not catalyst-shaped — it's closer to the existing \`conversations-to-channel-digest\` pattern, but as you described it, the destination is "email me" (one specific surface) and the mapping insight is "summarize the day's conversations" (generic). Two options: (a) call \`get_catalyst("conversations-to-channel-digest")\` and we synthesize a personal skill for you that emails the digest via Gmail — no PR needed; (b) if you want to *contribute* a digest variant, the value-add would need to be a specific decision the existing digest catalyst doesn't make, like "group by triage outcome" or "elevate any conversation with a low CSAT signal." Which fits?
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Step 2 — Gather context (one batched round)
|
|
136
|
+
|
|
137
|
+
If the posture-check passes, ask the user the following in one message. Don't drip questions out one at a time. Skip questions the user already answered in their intent.
|
|
138
|
+
|
|
139
|
+
1. **Workflow intent in one paragraph.** What mojulo data → what destination concept, and the user's motivation.
|
|
140
|
+
2. **Mojulo source surface.** Which existing mojulo MCP tools (\`query_submissions\`, \`query_conversations\`, \`get_deployment\`, \`get_conversation\`, etc.) does the synthesized skill call? Common shapes: form-side (\`query_submissions\` + \`get_deployment\`), conversation-side (\`query_conversations\` + \`get_conversation\` + \`get_deployment\`), or both.
|
|
141
|
+
3. **Required protocols.** Which mojulo bot capabilities does the target bot need enabled — \`formGathering\`, \`appointments\`, \`triage\`, \`opticalRead\`, \`knowledge\`, or none? Separate required from optional.
|
|
142
|
+
4. **Destination MCP category.** Pick from existing categories where possible: \`crm-like\`, \`calendar-like\`, \`ticketing-like\`, \`actuator-like\`, \`doc-or-channel-like\`, \`data-store-like\`. If proposing a new category, the user must justify why none fit — don't proliferate categories.
|
|
143
|
+
5. **Catalyst category (the \`category\` frontmatter field).** Existing: \`crm-sync\`, \`itsm\`, \`calendar\`, \`digest\`, \`analysis\`, \`rag-curation\`, \`extraction-pipeline\`. Same discipline — ask before adding a new one.
|
|
144
|
+
6. **Mapping insight — the value-add.** What's the specific, opinionated decision this catalyst encodes that a future Claude would otherwise have to guess at? Apply the bad-vs-good rubric from posture rule 5.
|
|
145
|
+
7. **Idempotency strategy.** Cursor field (usually a submission/conversation timestamp via a \`since\` input) AND dedupe key (usually a destination-side search-before-create on a stable id). Apply the bad-vs-good rubric from posture rule 6.
|
|
146
|
+
8. **Pitfalls.** PII exposure, irreversible writes, rate limits, calibration drift are universal — surface those automatically. Ask the user for any domain-specific pitfalls (timezone bugs, confidence thresholds, schema drift).
|
|
147
|
+
9. **Parameters to ask the user at synthesis time.** Each \`parameters[]\` entry the synthesized skill needs to be parameterized over (\`name\`, \`prompt\`, optional \`default\`). Typically 2-4. More than 5 usually means the catalyst is trying to do two things — push back.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Step 3 — Pick the id and slug
|
|
152
|
+
|
|
153
|
+
The \`id\` is the file slug and frontmatter \`id\`. Conventions:
|
|
154
|
+
|
|
155
|
+
- kebab-case, descriptive, ≤ ~40 chars
|
|
156
|
+
- shape: \`<source>-to-<destination>\` (e.g. \`qualify-lead-to-crm\`, \`appointment-to-calendar\`) or \`<verb>-<source>-<modifier>\` (e.g. \`scan-conversations-for-signal\`, \`knowledge-gap-miner\`)
|
|
157
|
+
- must not collide with an existing id — check \`list_catalysts\` output before committing
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Step 4 — Draft the file
|
|
162
|
+
|
|
163
|
+
Save as \`<id>.md\` in a working directory the user picks (e.g. \`./catalyst-proposals/<id>.md\`). The file has two parts: JSON frontmatter between \`---\` fences, then a markdown body.
|
|
164
|
+
|
|
165
|
+
### Frontmatter (JSON, between \`---\` fences)
|
|
166
|
+
|
|
167
|
+
**Required string fields:**
|
|
168
|
+
|
|
169
|
+
- \`id\` — kebab-case slug, matches the filename.
|
|
170
|
+
- \`name\` — human-readable title.
|
|
171
|
+
- \`summary\` — one line, implementation-shaped. Used in \`list_catalysts\`.
|
|
172
|
+
- \`valueHook\` — one sentence in **user-outcome** terms. Read aloud by \`recommend_catalysts\` to position the catalyst *before* the user has decided to read the body. Outcome-shaped ("CRM contacts overnight, deduped and scored"), not implementation-shaped — don't just restate the \`summary\`.
|
|
173
|
+
|
|
174
|
+
**Optional fields:**
|
|
175
|
+
|
|
176
|
+
- \`version\` (number, default 1)
|
|
177
|
+
- \`category\` (string — see Step 2.5)
|
|
178
|
+
- \`requires.protocols\` (array of protocol names the target bot must have)
|
|
179
|
+
- \`requires.optionalProtocols\` (array — nice to have but not required)
|
|
180
|
+
- \`requires.destinationMcpCategory\` (one of the categories from Step 2.4)
|
|
181
|
+
- \`requires.destinationExamples\` — **required if \`destinationMcpCategory\` is set.** Array of 3-5 named MCPs that satisfy the category (e.g., for \`crm-like\`: \`["HubSpot", "Salesforce", "Pipedrive", "Attio", "Close"]\`). The \`recommend_catalysts\` tool surfaces these as consultation suggestions ("you could install HubSpot to unlock this") — missing or empty is a hole in the consultation posture.
|
|
182
|
+
- \`parameters\` (array of \`{ name, prompt, default? }\`)
|
|
183
|
+
- \`mcpTools.mojulo\` (array of mojulo tool names the skill calls)
|
|
184
|
+
- \`mcpTools.destination.description\` — *abstract* prose describing the shape of MCP needed plus 2-4 example MCPs. Do not bind to a specific MCP.
|
|
185
|
+
|
|
186
|
+
### Body — the six-section template
|
|
187
|
+
|
|
188
|
+
Every shipped catalyst follows this. Don't deviate without reason.
|
|
189
|
+
|
|
190
|
+
1. **Opening paragraph** — what this catalyst does in plain English, ~2-3 sentences. Frame the source protocol or data shape it operates on.
|
|
191
|
+
2. **How to synthesize the skill** — numbered steps. First step is almost always \`get_deployment(deploymentId)\` to read the bot's shape. Then "ask the user the N \`parameters\` questions" (batched). Then "inspect the bound destination MCP" to discover its concrete surface. Last step: where to write the file (\`.claude/skills/<bot-slug>-<purpose>/SKILL.md\`) — name the slug pattern.
|
|
192
|
+
3. **Mapping intent** — the load-bearing section. Specific field-to-field guidance, what to do when a field doesn't fit, when to ask the user vs. when to assume. This is where the value-add lives. Be concrete — quote field names, name destination shapes.
|
|
193
|
+
4. **Idempotency** — cursor strategy AND dedupe key. Always pair them — the cursor is the primary defense, search-before-create is the safety net.
|
|
194
|
+
5. **Pitfalls** — bullets, each with a specific mitigation (not just the risk). At minimum touch on: PII exposure (especially anything where the LLM reads form/conversation content), irreversible writes (default \`dryRun: true\`, opt-in to live), rate limits, calibration drift. Add domain-specific pitfalls the user surfaced.
|
|
195
|
+
6. **Skill behavior contract** — bullets for \`Inputs:\`, \`Outputs:\`, \`Side effects (live mode):\`. Inputs always include \`deploymentId\` (required), \`since\` (optional ISO), \`dryRun\` (default true).
|
|
196
|
+
|
|
197
|
+
### Body principles to enforce
|
|
198
|
+
|
|
199
|
+
- Default \`dryRun: true\` in the contract. Live mode is per-run opt-in.
|
|
200
|
+
- Always require mojulo trace (submission id, conversation id, deployment id, captured-at) in destination payloads.
|
|
201
|
+
- Surface PII concerns explicitly when the synthesized skill will read form/conversation content through the LLM.
|
|
202
|
+
- Don't write back to the bot. Catalysts read from mojulo, write to destinations.
|
|
203
|
+
- Sample, don't sweep. Analytical catalysts default to bounded samples (typically 30) — the user graduates after calibration.
|
|
204
|
+
|
|
205
|
+
### What NOT to write in the body
|
|
206
|
+
|
|
207
|
+
- Don't restate vocabulary disambiguation (catalyst vs. skill vs. protocol). The synthesizer briefing prepended to every \`get_catalyst\` response already does that — you'd be duplicating.
|
|
208
|
+
- Don't restate the "adapt freely, posture is starting point not contract" preamble. Same reason.
|
|
209
|
+
- Don't pad sections that don't apply. If there's no meaningful trend-delta concern, skip it — don't fabricate.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Step 5 — Self-validate the draft
|
|
214
|
+
|
|
215
|
+
You can't run mojulo's test suite from here. Walk this checklist by hand before handing off:
|
|
216
|
+
|
|
217
|
+
- [ ] Frontmatter is valid JSON (parses without error).
|
|
218
|
+
- [ ] All four required string fields are present and non-empty: \`id\`, \`name\`, \`summary\`, \`valueHook\`.
|
|
219
|
+
- [ ] If \`requires.destinationMcpCategory\` is set, \`requires.destinationExamples\` is a non-empty array of strings.
|
|
220
|
+
- [ ] \`valueHook\` is outcome-shaped (what the *user* gets), not implementation-shaped (what the *skill* does).
|
|
221
|
+
- [ ] The body has all six sections in order. No section is fabricated padding.
|
|
222
|
+
- [ ] Mapping intent contains at least one specific, non-obvious decision (re-check posture rule 5).
|
|
223
|
+
- [ ] Idempotency section names both a cursor field and a dedupe key (re-check posture rule 6).
|
|
224
|
+
- [ ] Pitfalls section has a specific mitigation per bullet, not just a stated risk.
|
|
225
|
+
- [ ] Skill behavior contract names \`deploymentId\`, \`since\`, \`dryRun\` inputs.
|
|
226
|
+
|
|
227
|
+
If any check fails, fix before handing off. A maintainer's first review pass will run the same checks plus the loader's structural parse.
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Step 6 — Hand off to the user
|
|
232
|
+
|
|
233
|
+
Tell the user:
|
|
234
|
+
|
|
235
|
+
- Where you wrote the file in their working directory (e.g. \`./catalyst-proposals/<id>.md\`).
|
|
236
|
+
- That this is a **proposal** to the mojulo library, not a local skill. Accepted catalysts ship to every mojulo user.
|
|
237
|
+
- To contribute, open a PR against **https://github.com/zombico/mojulo** adding the file under \`control/lib/mcp/catalysts/\`. No other files need to change — the loader picks new \`.md\` files up automatically.
|
|
238
|
+
- The maintainers will review against the posture-check rules above and the loader's structural parse, and may push back on mapping density or value-add. If the catalyst is bot-specific or thin, expect the maintainers to suggest converting it to a local skill instead.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Final reminders
|
|
243
|
+
|
|
244
|
+
- **Push back early.** Once you've drafted a hollow catalyst with the user, it's hard to un-write. The posture-check in Step 1 is the most valuable thing you do here.
|
|
245
|
+
- **Anchor on exemplars.** Re-read the catalysts you pulled in Step 1 before drafting each section. The skill's quality scales with how closely you match the existing tone, density, and opinionatedness.
|
|
246
|
+
- **The body is a prompt, not documentation.** The reader is a future Claude trying to write a working skill in one pass. Optimize for their decisions, not the user's understanding of how the catalyst works.
|
|
247
|
+
`;
|
|
248
|
+
|
|
249
|
+
// Returned in every recommend_catalysts response so the agent re-encounters
|
|
250
|
+
// the consultation posture at the moment of acting on it. Mirrors the role
|
|
251
|
+
// SYNTHESIZER_BRIEFING plays for get_catalyst — the rules are easier to
|
|
252
|
+
// follow when they sit next to the data they apply to.
|
|
253
|
+
//
|
|
254
|
+
// Exported for tests.
|
|
255
|
+
export const CONSULTATION_POSTURE = `# How to use these recommendations — consultation, not gatekeeping
|
|
256
|
+
|
|
257
|
+
This tool returns catalysts whose shape fits the bot you named, each annotated with a \`destinationCategory\` (the kind of MCP that satisfies it) and \`destinationExamples\` (named MCPs that fit). Mojulo does **not** know which MCPs are installed in the user's Claude — only you do.
|
|
258
|
+
|
|
259
|
+
Cross-reference \`destinationExamples\` against the MCPs available in this session:
|
|
260
|
+
|
|
261
|
+
- **Example IS installed** → present as something the user can do now. Lead with the \`valueHook\`. Ask if they want to read the catalyst.
|
|
262
|
+
- **No example installed** → present as a soft suggestion, not a blocker. Lead with the \`valueHook\` and add: "you'd need a CRM MCP — HubSpot, Salesforce, Pipedrive, Attio — wired into Claude for this." Never gatekeep ("can't do this") — frame as an opt-in upgrade.
|
|
263
|
+
- **\`missingProtocols\` non-empty** → the bot's protocols don't currently support this catalyst. Mention it as a possibility unlocked by editing the bot, not by installing an MCP.
|
|
264
|
+
|
|
265
|
+
Lead with the user's outcome (\`valueHook\`), not the catalyst's name. The catalyst id is a handle to fetch the recipe with \`get_catalyst\`; it's not how you describe the value to the user.`;
|
|
266
|
+
|
|
267
|
+
export async function customCatalystHandler(_input, _ctx) {
|
|
268
|
+
// Plain text content (not JSON-stringified) so the agent reads it as prose.
|
|
269
|
+
return { content: [{ type: 'text', text: CUSTOM_CATALYST_GUIDE }] };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function enabledProtocolsOf(dep) {
|
|
273
|
+
const map = dep.config?.enabledProtocols || {};
|
|
274
|
+
return Object.entries(map)
|
|
275
|
+
.filter(([, on]) => on)
|
|
276
|
+
.map(([protocol]) => protocol);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function buildRecommendation(catalyst, applicableDeployments = []) {
|
|
280
|
+
return {
|
|
281
|
+
id: catalyst.id,
|
|
282
|
+
name: catalyst.name,
|
|
283
|
+
valueHook: catalyst.valueHook,
|
|
284
|
+
summary: catalyst.summary,
|
|
285
|
+
category: catalyst.category,
|
|
286
|
+
destinationCategory: catalyst.requires?.destinationMcpCategory || null,
|
|
287
|
+
destinationExamples: Array.isArray(catalyst.requires?.destinationExamples)
|
|
288
|
+
? catalyst.requires.destinationExamples
|
|
289
|
+
: [],
|
|
290
|
+
...(applicableDeployments.length > 0 ? { applicableDeployments } : {}),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function recommendForOneBot(deploymentId) {
|
|
295
|
+
const dep = await DeploymentRepository.findById(deploymentId);
|
|
296
|
+
if (!dep) throw new Error(`Deployment not found: ${deploymentId}`);
|
|
297
|
+
|
|
298
|
+
const enabledProtocols = enabledProtocolsOf(dep);
|
|
299
|
+
const applicable = [];
|
|
300
|
+
const requiresProtocolChange = [];
|
|
301
|
+
|
|
302
|
+
for (const catalyst of getCatalystCatalog().values()) {
|
|
303
|
+
const required = Array.isArray(catalyst.requires?.protocols)
|
|
304
|
+
? catalyst.requires.protocols
|
|
305
|
+
: [];
|
|
306
|
+
const missingProtocols = required.filter((p) => !enabledProtocols.includes(p));
|
|
307
|
+
const rec = { ...buildRecommendation(catalyst), missingProtocols };
|
|
308
|
+
if (missingProtocols.length === 0) applicable.push(rec);
|
|
309
|
+
else requiresProtocolChange.push(rec);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
consultationPosture: CONSULTATION_POSTURE,
|
|
314
|
+
deployment: { id: dep.id, botName: dep.botName, enabledProtocols },
|
|
315
|
+
applicable,
|
|
316
|
+
requiresProtocolChange,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function recommendForFleet(deploymentIds) {
|
|
321
|
+
let deployments = await DeploymentRepository.list();
|
|
322
|
+
if (Array.isArray(deploymentIds) && deploymentIds.length > 0) {
|
|
323
|
+
const wanted = new Set(deploymentIds);
|
|
324
|
+
deployments = deployments.filter((d) => wanted.has(d.id));
|
|
325
|
+
}
|
|
326
|
+
if (deployments.length === 0) {
|
|
327
|
+
throw new Error('No deployments matched fleet recommendation request');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// For each bot, compute the enabled-protocol set once.
|
|
331
|
+
const botEnabled = deployments.map((d) => ({
|
|
332
|
+
id: d.id,
|
|
333
|
+
botName: d.botName,
|
|
334
|
+
enabledProtocols: enabledProtocolsOf(d),
|
|
335
|
+
}));
|
|
336
|
+
|
|
337
|
+
const applicable = [];
|
|
338
|
+
const requiresProtocolChange = [];
|
|
339
|
+
|
|
340
|
+
for (const catalyst of getCatalystCatalog().values()) {
|
|
341
|
+
const required = Array.isArray(catalyst.requires?.protocols)
|
|
342
|
+
? catalyst.requires.protocols
|
|
343
|
+
: [];
|
|
344
|
+
|
|
345
|
+
const fitting = botEnabled.filter((b) =>
|
|
346
|
+
required.every((p) => b.enabledProtocols.includes(p)),
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
if (fitting.length > 0) {
|
|
350
|
+
// crossBot: catalyst applies to ≥2 bots → the value-add of fleet mode.
|
|
351
|
+
// A skill synthesized from this recommendation should iterate over
|
|
352
|
+
// applicableDeployments rather than binding to a single bot.
|
|
353
|
+
const rec = {
|
|
354
|
+
...buildRecommendation(
|
|
355
|
+
catalyst,
|
|
356
|
+
fitting.map((b) => ({ id: b.id, botName: b.botName })),
|
|
357
|
+
),
|
|
358
|
+
crossBot: fitting.length > 1,
|
|
359
|
+
};
|
|
360
|
+
applicable.push(rec);
|
|
361
|
+
} else {
|
|
362
|
+
// No bot in the requested set has the required protocols. Surface a
|
|
363
|
+
// per-bot missingProtocols hint anchored on the smallest gap so the
|
|
364
|
+
// user can see what'd need to change.
|
|
365
|
+
const gaps = botEnabled.map((b) => ({
|
|
366
|
+
botId: b.id,
|
|
367
|
+
botName: b.botName,
|
|
368
|
+
missingProtocols: required.filter((p) => !b.enabledProtocols.includes(p)),
|
|
369
|
+
}));
|
|
370
|
+
gaps.sort((a, b) => a.missingProtocols.length - b.missingProtocols.length);
|
|
371
|
+
requiresProtocolChange.push({
|
|
372
|
+
...buildRecommendation(catalyst),
|
|
373
|
+
smallestGap: gaps[0],
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Sort fleet-applicable by breadth (most bots first) — catalysts that span
|
|
379
|
+
// the fleet are the new category this surface enables.
|
|
380
|
+
applicable.sort(
|
|
381
|
+
(a, b) =>
|
|
382
|
+
(b.applicableDeployments?.length || 0) - (a.applicableDeployments?.length || 0),
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
consultationPosture: CONSULTATION_POSTURE,
|
|
387
|
+
fleet: {
|
|
388
|
+
totalBots: deployments.length,
|
|
389
|
+
bots: botEnabled,
|
|
390
|
+
},
|
|
391
|
+
applicable,
|
|
392
|
+
requiresProtocolChange,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export async function recommendCatalystsHandler(input, _ctx) {
|
|
397
|
+
const { deploymentId, scope, deploymentIds } = input || {};
|
|
398
|
+
|
|
399
|
+
if (deploymentId) {
|
|
400
|
+
return recommendForOneBot(deploymentId);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (scope === 'fleet' || Array.isArray(deploymentIds)) {
|
|
404
|
+
return recommendForFleet(deploymentIds);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
throw new Error(
|
|
408
|
+
"deploymentId is required, OR pass { scope: 'fleet' } / { deploymentIds: [...] } for cross-bot recommendations",
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export function registerCatalystTools() {
|
|
413
|
+
registerTool({
|
|
414
|
+
name: 'list_catalysts',
|
|
415
|
+
description:
|
|
416
|
+
"List curated workflow recipes (\"catalysts\") shipped with mojulo. A catalyst is NOT a Claude Code skill and NOT a mojulo bot capability — it is a recipe you read to catalyze the synthesis of a Claude Code skill (.claude/skills/<name>/SKILL.md) that operates on a mojulo bot's data via this MCP plus a destination MCP installed in Claude Code. The name is intentionally bare (not \"skill catalyst\") to keep the concept distinct from the skill it produces: catalysts produce skills, they are not skills. Each catalyst is consumed once per synthesis to crystallize a structured skill out of user intent + bot shape + destination. Returns id, name, summary, category, and requirements per catalyst (notably requires.protocols, which names the mojulo bot capabilities the target bot must have enabled). Call get_catalyst to read the full recipe.",
|
|
417
|
+
inputSchema: {
|
|
418
|
+
type: 'object',
|
|
419
|
+
properties: {
|
|
420
|
+
category: {
|
|
421
|
+
type: 'string',
|
|
422
|
+
description: 'Optional filter (e.g., crm-sync, itsm, calendar, digest, analysis, rag-curation).',
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
handler: listCatalystsHandler,
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
registerTool({
|
|
430
|
+
name: 'get_catalyst',
|
|
431
|
+
description:
|
|
432
|
+
"Get the full body of a catalyst by id. The returned body is a recipe written for you (Claude) to read at synthesis time — it tells you how to write a new Claude Code skill (.claude/skills/<name>/SKILL.md) that operates on a mojulo bot. The body starts with a synthesizer briefing that (a) explicitly licenses you to adapt, combine, or write from scratch when the catalyst doesn't fit, and (b) disambiguates three overlapping terms (mojulo protocols vs. Claude Code skills vs. catalysts). Read the briefing before the recipe.",
|
|
433
|
+
inputSchema: {
|
|
434
|
+
type: 'object',
|
|
435
|
+
properties: {
|
|
436
|
+
id: { type: 'string', description: 'Catalyst id from list_catalysts.' },
|
|
437
|
+
},
|
|
438
|
+
required: ['id'],
|
|
439
|
+
},
|
|
440
|
+
handler: getCatalystHandler,
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
registerTool({
|
|
444
|
+
name: 'custom_catalyst',
|
|
445
|
+
description:
|
|
446
|
+
"Return an author's guide for drafting a new catalyst to contribute back to the mojulo library. Use this when the user says they want to write, propose, or contribute a new catalyst — NOT when they want to automate something for themselves (that's a local skill, synthesized from get_catalyst or directly from intent). The returned body is self-contained: posture-check rules with worked examples (so you push back on requests that aren't catalyst-shaped before drafting), batched context-gathering questions, the JSON frontmatter spec, the six-section body template, body principles, a by-hand validation checklist, and PR hand-off instructions. The guide tells you to anchor on existing exemplars via list_catalysts + get_catalyst before drafting — do that. The output of the workflow is a single .md file saved in the user's working directory, ready for them to PR to github.com/zombico/mojulo under control/lib/mcp/catalysts/.",
|
|
447
|
+
inputSchema: { type: 'object', properties: {} },
|
|
448
|
+
handler: customCatalystHandler,
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
registerTool({
|
|
452
|
+
name: 'recommend_catalysts',
|
|
453
|
+
description:
|
|
454
|
+
"Recommend catalysts that fit either ONE deployment (single-bot mode) or every connected bot (fleet mode). Single-bot: pass `deploymentId`; returns catalysts annotated with `missingProtocols` per the bot's enabled capabilities. Fleet mode: pass `scope: 'fleet'` (every connected bot) or `deploymentIds: [...]` (an explicit subset). Each fleet recommendation is annotated with `applicableDeployments: [{ id, botName }]` so a synthesized skill can iterate across the matching bots, and `crossBot: true` whenever a catalyst applies to ≥2 bots — the new category fleet aggregation unlocks (e.g., 'weekly digest across every intake bot into one CRM'). Use this — not `list_catalysts` — whenever the user asks 'what can I do with this bot?' / 'what can I do across all my bots?' / 'what should I automate?'. CONSULTATION surface: catalysts whose `destinationExamples` aren't installed in the user's Claude should be surfaced as soft suggestions, never as blockers. The response includes a `consultationPosture` block with the exact framing rules — read it before composing your answer.",
|
|
455
|
+
inputSchema: {
|
|
456
|
+
type: 'object',
|
|
457
|
+
properties: {
|
|
458
|
+
deploymentId: {
|
|
459
|
+
type: 'string',
|
|
460
|
+
description:
|
|
461
|
+
'Single-bot mode. Deployment id from list_deployments. Mutually exclusive with scope/deploymentIds.',
|
|
462
|
+
},
|
|
463
|
+
scope: {
|
|
464
|
+
type: 'string',
|
|
465
|
+
enum: ['fleet'],
|
|
466
|
+
description: "Pass 'fleet' to recommend across every connected bot.",
|
|
467
|
+
},
|
|
468
|
+
deploymentIds: {
|
|
469
|
+
type: 'array',
|
|
470
|
+
items: { type: 'string' },
|
|
471
|
+
description: 'Explicit deployment-id subset for fleet mode. Overrides scope: fleet.',
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
handler: recommendCatalystsHandler,
|
|
476
|
+
});
|
|
477
|
+
}
|