@crmy/openclaw-plugin 0.5.11 → 0.6.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/SKILL.md CHANGED
@@ -1,140 +1,278 @@
1
1
  ---
2
2
  name: crmy
3
- description: CRMy agent — manages contacts, accounts, deals, and pipeline using the CRMy CRM. Search before creating. Log every meaningful interaction. Always suggest next steps.
3
+ description: CRMy agent — manages contacts, accounts, deals, and pipeline. Search before creating. Log every meaningful interaction. Always suggest a next step.
4
4
  ---
5
5
 
6
6
  # CRMy — Your AI-Native CRM
7
7
 
8
- You have full access to CRMy, an agent-first CRM. You are not just a tool caller you are a proactive sales and relationship intelligence assistant. Think like a great CRM manager: remember context, connect the dots, and always suggest what should happen next.
8
+ You have full access to CRMy via the **`crmy` tool**. Every call takes an `action` string and an optional `params` object.
9
+
10
+ ```
11
+ crmy({ action: "<action>", params: { ... } })
12
+ ```
9
13
 
10
14
  ---
11
15
 
12
16
  ## Core Principles
13
17
 
14
18
  ### 1. Search before you create
15
- **Always** run `crmy_search` or a specific search tool before creating any record. Duplicates are expensive. If you find a match, confirm with the user before proceeding.
19
+ Always search before creating any record. Duplicates are expensive.
16
20
 
17
21
  ```
18
- User: "Add a contact for Sarah Chen at Acme"
19
- crmy_contact_search("Sarah Chen") first
20
- If found: "I found Sarah Chen at Acme Corp — want me to update her record instead?"
21
- → If not found: create with crmy_contact_create
22
+ User: "Add Sarah Chen at Acme"
23
+ crmy({ action: "contact.search", params: { q: "Sarah Chen" } })
24
+ Found? Confirm before updating. Not found? Create.
22
25
  ```
23
26
 
24
27
  ### 2. Log every meaningful interaction
25
- Any time the user mentions talking to someone, having a meeting, sending a proposal, or receiving news about a deal — offer to log it as an activity. Don't wait to be asked.
28
+ Any time the user mentions a call, meeting, email, or deal news — offer to log it. Don't wait to be asked.
26
29
 
27
30
  ```
28
- User: "Just got off a call with Marcus, he's interested in the enterprise plan"
29
- Log call via crmy_contact_log_activity
30
- Suggest advancing the opportunity stage
31
- → Ask if there's a follow-up to schedule
31
+ User: "Just got off a call with Marcus, he's interested in enterprise"
32
+ crmy({ action: "contact.log_activity", params: { activity_type: "call", subject_type: "contact", subject_id: "<marcus-id>", summary: "...", outcome: "positive" } })
33
+ Offer to advance the opportunity stage
32
34
  ```
33
35
 
34
36
  ### 3. Link everything
35
- Contacts belong to accounts. Opportunities belong to accounts and contacts. When creating any record, ask about relationships if they're not provided.
37
+ Contacts belong to accounts. Opportunities belong to accounts. Ask about relationships when not provided.
36
38
 
37
39
  ### 4. Always suggest a next step
38
- After any CRM action, end with one concrete suggestion:
39
40
  - After logging a call → "Want me to advance the deal stage or set a follow-up?"
40
41
  - After creating a contact → "Should I create an opportunity for this relationship?"
41
42
  - After advancing a stage → "Want me to log what triggered this move?"
42
43
 
43
44
  ---
44
45
 
45
- ## CRMy Data Model
46
+ ## Actions Reference
47
+
48
+ ### `search`
49
+ Global cross-entity search — contacts, accounts, opportunities, activities.
50
+
51
+ ```
52
+ crmy({ action: "search", params: { q: "Acme", limit: 10 } })
53
+ ```
54
+ | Param | Type | Notes |
55
+ |-------|------|-------|
56
+ | q | string | **required** — search query |
57
+ | limit | number | max results (default 10) |
58
+
59
+ ---
60
+
61
+ ### `contact.search`
62
+ Search contacts by name, email, company, or keyword.
63
+
64
+ ```
65
+ crmy({ action: "contact.search", params: { q: "Sarah", stage: "customer", limit: 20 } })
66
+ ```
67
+ | Param | Type | Notes |
68
+ |-------|------|-------|
69
+ | q | string | **required** |
70
+ | stage | string | filter by lifecycle stage |
71
+ | limit | number | default 20 |
72
+
73
+ ---
74
+
75
+ ### `contact.create`
76
+ Create a new contact.
77
+
78
+ ```
79
+ crmy({ action: "contact.create", params: {
80
+ name: "Sarah Chen",
81
+ email: "sarah@acme.com",
82
+ phone: "+1 555 0100",
83
+ title: "VP Engineering",
84
+ account_id: "<uuid>",
85
+ lifecycle_stage: "prospect",
86
+ notes: "Met at SaaStr 2026"
87
+ }})
88
+ ```
89
+ | Param | Required |
90
+ |-------|----------|
91
+ | name | ✓ |
92
+ | email, phone, title, account_id, lifecycle_stage, notes | optional |
93
+
94
+ ---
95
+
96
+ ### `contact.update`
97
+ Update fields on an existing contact.
98
+
99
+ ```
100
+ crmy({ action: "contact.update", params: { id: "<uuid>", email: "new@acme.com" } })
101
+ ```
102
+ `id` is **required**. Include only the fields to change.
103
+
104
+ ---
105
+
106
+ ### `contact.set_stage`
107
+ Change a contact's lifecycle stage.
108
+
109
+ ```
110
+ crmy({ action: "contact.set_stage", params: { id: "<uuid>", stage: "customer", note: "Signed contract" } })
111
+ ```
112
+ **Lifecycle stages in order:** `lead` → `prospect` → `customer` → `churned` / `partner`
113
+
114
+ ---
115
+
116
+ ### `contact.log_activity`
117
+ Log a call, email, meeting, demo, proposal, or note against any record.
46
118
 
47
- ### Contacts
48
- People you have relationships with. Key fields: `name`, `email`, `phone`, `title`, `account_id`, `lifecycle_stage`.
119
+ ```
120
+ crmy({ action: "contact.log_activity", params: {
121
+ activity_type: "call",
122
+ subject_type: "contact",
123
+ subject_id: "<uuid>",
124
+ summary: "Discussed enterprise pricing",
125
+ outcome: "positive",
126
+ duration_minutes: 30,
127
+ notes: "Wants a proposal by Friday"
128
+ }})
129
+ ```
130
+ | Param | Required | Values |
131
+ |-------|----------|--------|
132
+ | activity_type | ✓ | call, email, meeting, demo, proposal, note |
133
+ | subject_type | ✓ | contact, account, opportunity |
134
+ | subject_id | ✓ | UUID of the record |
135
+ | summary | ✓ | short description |
136
+ | outcome | | positive, neutral, negative |
137
+ | duration_minutes | | for calls and meetings |
138
+ | performed_at | | ISO 8601 (defaults to now) |
139
+ | notes | | detailed notes |
140
+
141
+ ---
142
+
143
+ ### `account.search`
144
+ Search companies/accounts.
145
+
146
+ ```
147
+ crmy({ action: "account.search", params: { q: "Acme", industry: "SaaS", limit: 20 } })
148
+ ```
149
+
150
+ ---
151
+
152
+ ### `account.create`
153
+ Create a new company/account.
154
+
155
+ ```
156
+ crmy({ action: "account.create", params: {
157
+ name: "Acme Corp",
158
+ domain: "acme.com",
159
+ industry: "SaaS",
160
+ size: "51-200"
161
+ }})
162
+ ```
163
+ `name` is **required**.
164
+
165
+ ---
166
+
167
+ ### `opportunity.search`
168
+ Search deals/opportunities.
49
169
 
50
- **Lifecycle stages** (in order):
51
- - `lead` heard of them, no real relationship yet
52
- - `prospect` — actively exploring a fit
53
- - `customer` paying customer
54
- - `churned` — was a customer, no longer active
55
- - `partner` strategic relationship, not a direct sale
170
+ ```
171
+ crmy({ action: "opportunity.search", params: { q: "Acme", stage: "proposal", limit: 20 } })
172
+ ```
173
+ | Param | Notes |
174
+ |-------|-------|
175
+ | q | **required** |
176
+ | stage | filter by deal stage |
177
+ | account_id | filter by account UUID |
56
178
 
57
- Use `crmy_contact_set_lifecycle` when a relationship meaningfully changes.
179
+ ---
58
180
 
59
- ### Accounts
60
- Companies and organizations. Key fields: `name`, `domain`, `industry`, `size`.
181
+ ### `opportunity.create`
182
+ Create a new deal.
61
183
 
62
- ### Opportunities (Deals)
63
- Revenue-generating relationships. Key fields: `name`, `account_id`, `value`, `stage`, `close_date`.
184
+ ```
185
+ crmy({ action: "opportunity.create", params: {
186
+ name: "Acme Corp — Enterprise",
187
+ account_id: "<uuid>",
188
+ value: 48000,
189
+ stage: "prospecting",
190
+ close_date: "2026-09-30"
191
+ }})
192
+ ```
193
+ `name` is **required**.
64
194
 
65
- **Deal stages** (typical progression):
66
- - `prospecting` → `qualification` → `proposal` → `negotiation` → `closed_won` / `closed_lost`
195
+ ---
67
196
 
68
- Use `crmy_opportunity_advance_stage` to move a deal. Always include a `note` explaining why.
197
+ ### `opportunity.advance`
198
+ Move a deal to a new stage.
69
199
 
70
- ### Activities
71
- The record of every interaction. Always specify `activity_type`:
72
- - `call` — phone or video call
73
- - `email` — email sent or received
74
- - `meeting` in-person or virtual meeting
75
- - `demo` — product demonstration
76
- - `proposal` — proposal sent
77
- - `note` — internal note or observation
200
+ ```
201
+ crmy({ action: "opportunity.advance", params: {
202
+ id: "<uuid>",
203
+ stage: "closed_won",
204
+ note: "Signed MSA received",
205
+ lost_reason: ""
206
+ }})
207
+ ```
208
+ `id` and `stage` are **required**. Always include a `note`.
78
209
 
79
- Set `outcome` to `positive`, `neutral`, or `negative` based on how it went.
210
+ **Deal stages:** `prospecting` `qualification` `proposal` `negotiation` `closed_won` / `closed_lost`
211
+
212
+ ---
213
+
214
+ ### `pipeline.summary`
215
+ Get pipeline analytics grouped by stage (or owner/forecast_cat).
216
+
217
+ ```
218
+ crmy({ action: "pipeline.summary", params: { group_by: "stage" } })
219
+ ```
80
220
 
81
221
  ---
82
222
 
83
223
  ## Multi-Step Workflows
84
224
 
85
225
  ### "Log a call I just had"
86
- 1. Search for the contact first (`crmy_contact_search`)
87
- 2. Log the activity (`crmy_contact_log_activity`) with type `call`, the summary, and outcome
88
- 3. If they mentioned a deal → search for the opportunity (`crmy_opportunity_search`) and offer to advance its stage
89
- 4. Suggest: "Want me to update [contact]'s lifecycle stage to reflect this?"
226
+ 1. `contact.search` find the contact
227
+ 2. `contact.log_activity` type: call, summary, outcome
228
+ 3. If deal mentioned → `opportunity.search` offer `opportunity.advance`
229
+ 4. Suggest: "Want me to update their lifecycle stage?"
90
230
 
91
231
  ### "We just closed a deal"
92
- 1. Find the opportunity (`crmy_opportunity_search`)
93
- 2. Advance to `closed_won` with a note (`crmy_opportunity_advance_stage`)
94
- 3. Update the primary contact's lifecycle to `customer` (`crmy_contact_set_lifecycle`)
95
- 4. Log a closing activity (`crmy_contact_log_activity`, type: `meeting`, outcome: `positive`)
96
- 5. Celebrate, then ask: "Should I set up a follow-up for onboarding?"
97
-
98
- ### "How is the pipeline looking?"
99
- 1. Pull the summary (`crmy_pipeline_summary`)
100
- 2. Highlight: total value, deals by stage, any deals that haven't moved recently
101
- 3. Proactively ask: "Want me to look at any of these deals in detail?"
102
-
103
- ### "Find everyone at Acme Corp"
104
- 1. Search accounts for Acme (`crmy_account_search`)
105
- 2. Search contacts at that account (`crmy_contact_search` with the account name or id)
106
- 3. Present a clean summary: contacts, their titles, lifecycle stages, and any open deals
232
+ 1. `opportunity.search` — find the deal
233
+ 2. `opportunity.advance` stage: closed_won + note
234
+ 3. `contact.set_stage` primary contact customer
235
+ 4. `contact.log_activity` type: meeting, outcome: positive
236
+ 5. Celebrate, then: "Should I set up an onboarding follow-up?"
237
+
238
+ ### "How's the pipeline?"
239
+ 1. `pipeline.summary` group_by: stage
240
+ 2. Present as a table: stage | deal count | total value
241
+ 3. Highlight any deals stuck in the same stage for 30+ days
242
+ 4. Ask: "Want me to look at any of these in detail?"
107
243
 
108
244
  ### "New lead from the conference"
109
- 1. Search for the contact first (avoid duplicates)
110
- 2. Create the contact with `lifecycle_stage: lead` (`crmy_contact_create`)
111
- 3. Search for or create their company (`crmy_account_search` / `crmy_account_create`)
112
- 4. Link them via `account_id`
113
- 5. Log where you met them as an activity (type: `meeting`)
114
- 6. Ask: "Want to create an opportunity for this relationship?"
245
+ 1. `contact.search` avoid duplicate
246
+ 2. `contact.create` lifecycle_stage: lead
247
+ 3. `account.search` or `account.create` — find/create their company
248
+ 4. `contact.update` link account_id
249
+ 5. `contact.log_activity` — type: meeting (where you met)
250
+ 6. Ask: "Want to create an opportunity?"
251
+
252
+ ### "Who do we know at Stripe?"
253
+ 1. `account.search` — q: "Stripe"
254
+ 2. `contact.search` — q: "Stripe" (or filter by account_id)
255
+ 3. Present: name, title, lifecycle stage, any open deals
115
256
 
116
257
  ---
117
258
 
118
- ## Tone and Presentation
259
+ ## Presentation Guidelines
119
260
 
120
- - **Be concise.** When returning search results, summarize — don't dump raw JSON.
121
- - **Use names, not UUIDs** in your responses.
122
- - **Confirm before bulk operations.** If the user wants to update 5 contacts, confirm scope first.
123
- - **When something fails**, explain what went wrong in plain language and suggest a fix (e.g., "The server isn't reachable — is `npx @crmy/cli server` running?").
124
- - **Format pipeline data** as a clean table or bullet list, not raw numbers.
261
+ - **Summarize results** — don't dump raw JSON. Use names, not UUIDs.
262
+ - **Format pipeline data** as a table or bullets, not raw numbers.
263
+ - **Confirm before bulk changes** "I'll update 5 contacts proceed?"
264
+ - **On API errors** explain in plain English and suggest a fix:
265
+ - "Server not reachable is `npx @crmy/cli server` running?"
266
+ - "Record not found — want me to search first?"
125
267
 
126
268
  ---
127
269
 
128
- ## Example Interactions
129
-
130
- > "Sarah from Acme said she's ready to move forward"
131
- → Search for Sarah → find opportunity → advance stage → log activity → update lifecycle → suggest next step
132
-
133
- > "Pull up our pipeline"
134
- → `crmy_pipeline_summary` → present as table with stage, deal count, total value → highlight any stuck deals
135
-
136
- > "Who do we know at Stripe?"
137
- → `crmy_account_search("Stripe")` → `crmy_contact_search` filtered by account → list contacts with titles and stages
270
+ ## Quick Examples
138
271
 
139
- > "Log that I sent a proposal to Marcus at Zendesk"
140
- → Find Marcus → log activity (type: proposal) → ask if the deal stage should move to `proposal`
272
+ | User says | Actions |
273
+ |-----------|---------|
274
+ | "Sarah from Acme is ready to move forward" | contact.search → opportunity.search → opportunity.advance → contact.log_activity → contact.set_stage |
275
+ | "Pull up our pipeline" | pipeline.summary → present table → offer drill-down |
276
+ | "Who do we know at Stripe?" | account.search → contact.search → list with stages |
277
+ | "Log that I sent a proposal to Marcus" | contact.search → contact.log_activity (type: proposal) → offer opportunity.advance |
278
+ | "Add a new lead: Jamie Lee, CTO at Horizon" | contact.search (check dup) → contact.create → account.search → link account |
package/dist/index.d.ts CHANGED
@@ -1,16 +1,17 @@
1
+ interface ToolInput {
2
+ type: 'object';
3
+ properties: Record<string, {
4
+ type: string;
5
+ description: string;
6
+ enum?: string[];
7
+ }>;
8
+ required?: string[];
9
+ }
1
10
  interface ToolDef {
2
11
  id: string;
3
12
  name: string;
4
13
  description: string;
5
- input: {
6
- type: 'object';
7
- properties: Record<string, {
8
- type: string;
9
- description: string;
10
- enum?: string[];
11
- }>;
12
- required?: string[];
13
- };
14
+ input: ToolInput;
14
15
  handler: (input: Record<string, unknown>) => Promise<unknown>;
15
16
  }
16
17
  interface OpenClawApi {
package/dist/index.js CHANGED
@@ -71,219 +71,73 @@ var CrmyClient = class {
71
71
  };
72
72
 
73
73
  // src/index.ts
74
+ var ROUTES = {
75
+ // Global cross-entity search
76
+ "search": async (c, p) => c.get("/search", { q: str(p.q), limit: num(p.limit, 10) }),
77
+ // Contacts
78
+ "contact.search": async (c, p) => c.get("/contacts", { q: str(p.q), stage: str(p.stage), limit: num(p.limit, 20) }),
79
+ "contact.create": async (c, p) => c.post("/contacts", p),
80
+ "contact.update": async (c, { id, ...rest }) => c.patch(`/contacts/${str(id)}`, rest),
81
+ "contact.set_stage": async (c, { id, ...rest }) => c.patch(`/contacts/${str(id)}`, rest),
82
+ "contact.log_activity": async (c, p) => c.post("/activities", p),
83
+ // Accounts
84
+ "account.search": async (c, p) => c.get("/accounts", { q: str(p.q), industry: str(p.industry), limit: num(p.limit, 20) }),
85
+ "account.create": async (c, p) => c.post("/accounts", p),
86
+ // Opportunities
87
+ "opportunity.search": async (c, p) => c.get("/opportunities", {
88
+ q: str(p.q),
89
+ stage: str(p.stage),
90
+ account_id: str(p.account_id),
91
+ limit: num(p.limit, 20)
92
+ }),
93
+ "opportunity.create": async (c, p) => c.post("/opportunities", p),
94
+ "opportunity.advance": async (c, { id, ...rest }) => c.patch(`/opportunities/${str(id)}`, rest),
95
+ // Analytics
96
+ "pipeline.summary": async (c, p) => c.get("/analytics/pipeline", { group_by: str(p.group_by) ?? "stage" })
97
+ };
98
+ var VALID_ACTIONS = Object.keys(ROUTES);
99
+ function str(v) {
100
+ return v != null && v !== "" ? String(v) : void 0;
101
+ }
102
+ function num(v, fallback) {
103
+ const n = Number(v);
104
+ return isNaN(n) ? fallback : n;
105
+ }
74
106
  var index_default = (api) => {
75
107
  const cfg = resolveConfig(api.config);
76
108
  const client = new CrmyClient(cfg);
77
109
  api.registerTool({
78
- id: "crmy_search",
79
- name: "CRMy: Search",
80
- description: "Search across all CRMy records \u2014 contacts, accounts, opportunities, activities, and more. Use this as a first step when you are not sure which record type the user is referring to.",
81
- input: {
82
- type: "object",
83
- properties: {
84
- q: { type: "string", description: "Search query" },
85
- limit: { type: "number", description: "Max results (default 10)" }
86
- },
87
- required: ["q"]
88
- },
89
- handler: async (input) => client.get("/search", { q: input.q, limit: input.limit ?? 10 })
90
- });
91
- api.registerTool({
92
- id: "crmy_contact_search",
93
- name: "CRMy: Search Contacts",
94
- description: "Search for contacts by name, email, company, or any keyword. Supports optional lifecycle stage filter.",
95
- input: {
96
- type: "object",
97
- properties: {
98
- q: { type: "string", description: "Search query (name, email, company, etc.)" },
99
- stage: { type: "string", description: "Filter by lifecycle stage (e.g. prospect, customer, churned)" },
100
- limit: { type: "number", description: "Max results (default 20)" }
101
- },
102
- required: ["q"]
103
- },
104
- handler: async (input) => client.get("/contacts", { q: input.q, stage: input.stage, limit: input.limit ?? 20 })
105
- });
106
- api.registerTool({
107
- id: "crmy_contact_create",
108
- name: "CRMy: Create Contact",
109
- description: "Create a new contact in CRMy. At minimum provide a name. Email, phone, title, and account_id are optional.",
110
+ id: "crmy",
111
+ name: "CRMy CRM",
112
+ // Keep the top-level description short OpenClaw loads it for every turn.
113
+ // Detail lives in SKILL.md which is loaded only when this tool is active.
114
+ description: "Interact with the CRMy CRM. Specify an action and optional params. Valid actions: " + VALID_ACTIONS.join(", ") + ". See SKILL.md for param shapes and workflow guidance.",
110
115
  input: {
111
116
  type: "object",
112
117
  properties: {
113
- name: { type: "string", description: "Full name (required)" },
114
- email: { type: "string", description: "Email address" },
115
- phone: { type: "string", description: "Phone number" },
116
- title: { type: "string", description: "Job title" },
117
- account_id: { type: "string", description: "UUID of the associated account/company" },
118
- lifecycle_stage: { type: "string", description: "Initial lifecycle stage (e.g. prospect, lead, customer)" },
119
- notes: { type: "string", description: "Any initial notes about this contact" }
120
- },
121
- required: ["name"]
122
- },
123
- handler: async (input) => client.post("/contacts", input)
124
- });
125
- api.registerTool({
126
- id: "crmy_contact_update",
127
- name: "CRMy: Update Contact",
128
- description: "Update fields on an existing contact. Provide the contact id and any fields to change.",
129
- input: {
130
- type: "object",
131
- properties: {
132
- id: { type: "string", description: "Contact UUID (required)" },
133
- name: { type: "string", description: "Updated name" },
134
- email: { type: "string", description: "Updated email" },
135
- phone: { type: "string", description: "Updated phone" },
136
- title: { type: "string", description: "Updated job title" },
137
- account_id: { type: "string", description: "Move to a different account UUID" }
138
- },
139
- required: ["id"]
140
- },
141
- handler: async ({ id, ...rest }) => client.patch(`/contacts/${id}`, rest)
142
- });
143
- api.registerTool({
144
- id: "crmy_contact_log_activity",
145
- name: "CRMy: Log Activity",
146
- description: "Log a call, email, meeting, or other activity against a contact, account, or opportunity. Provide subject_type + subject_id to attach it to the right record.",
147
- input: {
148
- type: "object",
149
- properties: {
150
- activity_type: {
151
- type: "string",
152
- description: "Type of activity \u2014 e.g. call, email, meeting, demo, proposal, note"
153
- },
154
- subject_type: {
118
+ action: {
155
119
  type: "string",
156
- description: "Record type the activity is attached to: contact, account, or opportunity",
157
- enum: ["contact", "account", "opportunity"]
120
+ description: "CRMy action to perform (see SKILL.md for the full list).",
121
+ enum: VALID_ACTIONS
158
122
  },
159
- subject_id: { type: "string", description: "UUID of the contact, account, or opportunity" },
160
- summary: { type: "string", description: "Short summary of what happened" },
161
- outcome: { type: "string", description: "Outcome of the activity (e.g. positive, neutral, negative)" },
162
- performed_at: { type: "string", description: "ISO 8601 timestamp (defaults to now)" },
163
- duration_minutes: { type: "number", description: "Duration in minutes (for calls and meetings)" },
164
- notes: { type: "string", description: "Detailed notes" }
165
- },
166
- required: ["activity_type", "subject_type", "subject_id", "summary"]
167
- },
168
- handler: async (input) => client.post("/activities", input)
169
- });
170
- api.registerTool({
171
- id: "crmy_contact_set_lifecycle",
172
- name: "CRMy: Set Contact Lifecycle Stage",
173
- description: "Change the lifecycle stage of a contact (e.g. lead \u2192 prospect \u2192 customer \u2192 churned).",
174
- input: {
175
- type: "object",
176
- properties: {
177
- id: { type: "string", description: "Contact UUID" },
178
- stage: { type: "string", description: "New lifecycle stage (e.g. lead, prospect, customer, churned)" },
179
- note: { type: "string", description: "Optional note explaining the stage change" }
180
- },
181
- required: ["id", "stage"]
182
- },
183
- handler: async ({ id, ...rest }) => client.patch(`/contacts/${id}`, rest)
184
- });
185
- api.registerTool({
186
- id: "crmy_account_search",
187
- name: "CRMy: Search Accounts",
188
- description: "Search for companies/accounts by name, industry, or keyword.",
189
- input: {
190
- type: "object",
191
- properties: {
192
- q: { type: "string", description: "Search query (company name, domain, etc.)" },
193
- industry: { type: "string", description: "Filter by industry" },
194
- limit: { type: "number", description: "Max results (default 20)" }
195
- },
196
- required: ["q"]
197
- },
198
- handler: async (input) => client.get("/accounts", { q: input.q, industry: input.industry, limit: input.limit ?? 20 })
199
- });
200
- api.registerTool({
201
- id: "crmy_account_create",
202
- name: "CRMy: Create Account",
203
- description: "Create a new company/account record in CRMy.",
204
- input: {
205
- type: "object",
206
- properties: {
207
- name: { type: "string", description: "Company name (required)" },
208
- domain: { type: "string", description: "Company website domain (e.g. acme.com)" },
209
- industry: { type: "string", description: "Industry sector" },
210
- size: { type: "string", description: "Company size (e.g. 1-10, 11-50, 51-200, 201-1000, 1000+)" },
211
- notes: { type: "string", description: "Any initial notes about this company" }
212
- },
213
- required: ["name"]
214
- },
215
- handler: async (input) => client.post("/accounts", input)
216
- });
217
- api.registerTool({
218
- id: "crmy_opportunity_search",
219
- name: "CRMy: Search Opportunities",
220
- description: "Search for deals/opportunities by name, account, or stage.",
221
- input: {
222
- type: "object",
223
- properties: {
224
- q: { type: "string", description: "Search query (deal name, account, etc.)" },
225
- stage: { type: "string", description: "Filter by deal stage (e.g. prospecting, proposal, closed_won)" },
226
- account_id: { type: "string", description: "Filter by account UUID" },
227
- limit: { type: "number", description: "Max results (default 20)" }
228
- },
229
- required: ["q"]
230
- },
231
- handler: async (input) => client.get("/opportunities", {
232
- q: input.q,
233
- stage: input.stage,
234
- account_id: input.account_id,
235
- limit: input.limit ?? 20
236
- })
237
- });
238
- api.registerTool({
239
- id: "crmy_opportunity_create",
240
- name: "CRMy: Create Opportunity",
241
- description: "Create a new deal/opportunity in CRMy. Provide a name and optionally an account, value, and stage.",
242
- input: {
243
- type: "object",
244
- properties: {
245
- name: { type: "string", description: "Deal name (required)" },
246
- account_id: { type: "string", description: "Associated account UUID" },
247
- value: { type: "number", description: "Deal value in your base currency" },
248
- stage: { type: "string", description: "Initial deal stage (defaults to prospecting)" },
249
- close_date: { type: "string", description: "Expected close date (ISO 8601, e.g. 2026-06-30)" },
250
- notes: { type: "string", description: "Any notes about this deal" }
251
- },
252
- required: ["name"]
253
- },
254
- handler: async (input) => client.post("/opportunities", input)
255
- });
256
- api.registerTool({
257
- id: "crmy_opportunity_advance_stage",
258
- name: "CRMy: Advance Opportunity Stage",
259
- description: "Move a deal to a new stage (e.g. proposal \u2192 negotiation \u2192 closed_won). Optionally add a note.",
260
- input: {
261
- type: "object",
262
- properties: {
263
- id: { type: "string", description: "Opportunity UUID (required)" },
264
- stage: { type: "string", description: "New stage name (required)" },
265
- note: { type: "string", description: "Note explaining the stage transition" },
266
- lost_reason: { type: "string", description: "If closing as lost, the reason" }
123
+ params: {
124
+ type: "object",
125
+ description: "Parameters for the chosen action. Shape varies by action \u2014 see SKILL.md. All fields are optional except where noted (e.g. id for updates, name for creates)."
126
+ }
267
127
  },
268
- required: ["id", "stage"]
128
+ required: ["action"]
269
129
  },
270
- handler: async ({ id, ...rest }) => client.patch(`/opportunities/${id}`, rest)
271
- });
272
- api.registerTool({
273
- id: "crmy_pipeline_summary",
274
- name: "CRMy: Pipeline Summary",
275
- description: 'Get a summary of the sales pipeline \u2014 total deal count and value by stage. Useful for answering questions like "how many deals do we have?" or "what is our pipeline worth?"',
276
- input: {
277
- type: "object",
278
- properties: {
279
- group_by: {
280
- type: "string",
281
- description: "Group results by: stage (default), owner, or forecast_cat",
282
- enum: ["stage", "owner", "forecast_cat"]
283
- }
130
+ handler: async (input) => {
131
+ const action = str(input.action);
132
+ if (!action || !ROUTES[action]) {
133
+ throw new Error(
134
+ `Unknown CRMy action "${action}". Valid actions: ${VALID_ACTIONS.join(", ")}`
135
+ );
284
136
  }
285
- },
286
- handler: async (input) => client.get("/analytics/pipeline", { group_by: input.group_by ?? "stage" })
137
+ const params = input.params ?? {};
138
+ api.logger?.info(`[crmy] action=${action}`);
139
+ return ROUTES[action](client, params);
140
+ }
287
141
  });
288
142
  };
289
143
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crmy/openclaw-plugin",
3
- "version": "0.5.11",
3
+ "version": "0.6.0",
4
4
  "description": "CRMy plugin for OpenClaw — expose CRM tools (contacts, accounts, opportunities, pipeline) in any AI assistant running OpenClaw",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",