clawdbot-pipedrive 1.1.0 → 2.0.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 CHANGED
@@ -2,15 +2,17 @@
2
2
 
3
3
  Pipedrive CRM integration plugin for [Clawdbot](https://clawd.bot).
4
4
 
5
+ **Now using Pipedrive API v2** - 50% lower token costs, better filtering, cursor-based pagination.
6
+
5
7
  ## Features
6
8
 
7
- - **Deals**: Search, list, create, update, delete deals
8
- - **Persons**: Search, get, create, update contacts
9
- - **Organizations**: Search, get, create companies
10
- - **Activities**: List, create, update, delete tasks/calls/meetings
11
- - **Pipelines & Stages**: List pipelines and stages
12
- - **Notes**: List and create notes on any entity
13
- - **Users**: List users, get current user
9
+ - **Deals**: Search, list, create, update, delete deals (v2)
10
+ - **Persons**: Search, list, create, update, delete contacts (v2)
11
+ - **Organizations**: Search, list, create, update, delete companies (v2)
12
+ - **Activities**: List, create, update, delete tasks/calls/meetings (v2)
13
+ - **Pipelines & Stages**: List pipelines and stages (v2)
14
+ - **Notes**: List, create, update, delete notes (v1 - no v2 yet)
15
+ - **Users**: List users, get current user (v1 - no v2 yet)
14
16
 
15
17
  ## Installation
16
18
 
package/index.ts CHANGED
@@ -1,4 +1,101 @@
1
1
  import { Type } from "@sinclair/typebox";
2
+ import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { homedir } from "node:os";
5
+
6
+ // Skill template - embedded so it works without network access
7
+ const SKILL_TEMPLATE = `# Pipedrive CRM Workflows
8
+
9
+ > Customize this file for your organization's Pipedrive workflows.
10
+ > This file will NOT be overwritten by plugin updates.
11
+
12
+ ## Deal Naming Convention
13
+
14
+ When creating deals, use this format:
15
+ - **Title**: \`[Company Name] - [Product/Plan] - [Value]\`
16
+ - Example: \`Acme Corp - Enterprise - $2,500/mo\`
17
+
18
+ ## Pipeline Stages
19
+
20
+ | Stage ID | Name | When to use |
21
+ |----------|------|-------------|
22
+ | 1 | Lead | Initial contact |
23
+ | 2 | Qualified | Confirmed interest |
24
+ | 3 | Proposal | Pricing sent |
25
+ | 4 | Negotiation | Active discussions |
26
+ | 5 | Closed Won | Deal signed |
27
+ | 6 | Closed Lost | Deal lost |
28
+
29
+ > **Note**: Replace stage IDs with your actual Pipedrive stage IDs.
30
+ > Find them via: \`pipedrive_list_stages\`
31
+
32
+ ## Required Fields
33
+
34
+ When creating deals, always include:
35
+ - \`title\` - Following naming convention above
36
+ - \`value\` - Deal value in your currency
37
+ - \`person_id\` or \`org_id\` - Link to contact/company
38
+
39
+ ## Activity Types
40
+
41
+ | Type | Use for | Subject format |
42
+ |------|---------|----------------|
43
+ | \`call\` | Phone calls | "Call: [topic]" |
44
+ | \`meeting\` | Demos, meetings | "Meeting: [purpose]" |
45
+ | \`task\` | Follow-ups, to-dos | "Task: [action]" |
46
+ | \`email\` | Email follow-ups | "Email: [subject]" |
47
+
48
+ ## Common Workflows
49
+
50
+ ### New Lead
51
+ 1. Search if contact exists: \`pipedrive_search_persons\`
52
+ 2. Create person if new: \`pipedrive_create_person\`
53
+ 3. Create deal: \`pipedrive_create_deal\`
54
+ 4. Schedule follow-up: \`pipedrive_create_activity\`
55
+
56
+ ### After Demo
57
+ 1. Update deal stage: \`pipedrive_update_deal\` with next stage_id
58
+ 2. Add notes: \`pipedrive_create_note\`
59
+ 3. Create follow-up task: \`pipedrive_create_activity\`
60
+
61
+ ### Close Won
62
+ 1. Update deal: \`pipedrive_update_deal\` with \`status: "won"\`
63
+ 2. Add closing note: \`pipedrive_create_note\`
64
+
65
+ ### Close Lost
66
+ 1. Update deal: \`pipedrive_update_deal\` with \`status: "lost"\` and \`lost_reason\`
67
+ `;
68
+
69
+ /**
70
+ * Sets up the skill template file
71
+ * - Creates skill if it doesn't exist
72
+ * - If skill exists, saves new template as .latest for comparison
73
+ */
74
+ function setupSkillTemplate(): void {
75
+ const skillDir = join(homedir(), ".clawdbot", "skills", "pipedrive");
76
+ const skillFile = join(skillDir, "SKILL.md");
77
+ const latestFile = join(skillDir, "SKILL.md.latest");
78
+
79
+ try {
80
+ mkdirSync(skillDir, { recursive: true });
81
+
82
+ if (!existsSync(skillFile)) {
83
+ writeFileSync(skillFile, SKILL_TEMPLATE);
84
+ console.log(`[pipedrive] Created skill template: ${skillFile}`);
85
+ console.log("[pipedrive] Customize this file with your organization's workflows.");
86
+ } else {
87
+ const existing = readFileSync(skillFile, "utf-8");
88
+ if (existing !== SKILL_TEMPLATE) {
89
+ writeFileSync(latestFile, SKILL_TEMPLATE);
90
+ console.log(`[pipedrive] Skill file exists: ${skillFile} (not modified)`);
91
+ console.log(`[pipedrive] New template available: ${latestFile}`);
92
+ console.log("[pipedrive] Compare with: diff ~/.clawdbot/skills/pipedrive/SKILL.md{,.latest}");
93
+ }
94
+ }
95
+ } catch (err) {
96
+ console.warn("[pipedrive] Could not set up skill template:", err);
97
+ }
98
+ }
2
99
 
3
100
  type PipedriveConfig = {
4
101
  apiKey?: string;
@@ -30,8 +127,8 @@ type ClawdbotPluginDefinition = {
30
127
  const plugin: ClawdbotPluginDefinition = {
31
128
  id: "pipedrive",
32
129
  name: "Pipedrive CRM",
33
- description: "Interact with Pipedrive deals, persons, organizations, and activities",
34
- version: "1.0.0",
130
+ description: "Interact with Pipedrive deals, persons, organizations, and activities (API v2)",
131
+ version: "2.0.0",
35
132
 
36
133
  configSchema: {
37
134
  parse: (v) => v as PipedriveConfig,
@@ -50,6 +147,8 @@ const plugin: ClawdbotPluginDefinition = {
50
147
  },
51
148
 
52
149
  register(api) {
150
+ setupSkillTemplate();
151
+
53
152
  const cfg = api.pluginConfig as PipedriveConfig;
54
153
 
55
154
  if (!cfg.apiKey || !cfg.domain) {
@@ -57,16 +156,20 @@ const plugin: ClawdbotPluginDefinition = {
57
156
  return;
58
157
  }
59
158
 
60
- const baseUrl = `https://${cfg.domain}.pipedrive.com/api/v1`;
159
+ const baseUrlV2 = `https://${cfg.domain}.pipedrive.com/api/v2`;
160
+ const baseUrlV1 = `https://${cfg.domain}.pipedrive.com/api/v1`; // For endpoints not yet in v2
61
161
 
62
- async function pipedriveRequest(endpoint: string, options?: RequestInit) {
162
+ async function pipedriveRequest(endpoint: string, options?: RequestInit & { useV1?: boolean }) {
163
+ const baseUrl = options?.useV1 ? baseUrlV1 : baseUrlV2;
63
164
  const url = new URL(`${baseUrl}${endpoint}`);
64
165
  url.searchParams.set("api_token", cfg.apiKey!);
166
+
167
+ const { useV1, ...fetchOptions } = options || {};
65
168
  const res = await fetch(url.toString(), {
66
- ...options,
169
+ ...fetchOptions,
67
170
  headers: {
68
171
  "Content-Type": "application/json",
69
- ...options?.headers,
172
+ ...fetchOptions?.headers,
70
173
  },
71
174
  });
72
175
  if (!res.ok) {
@@ -76,7 +179,7 @@ const plugin: ClawdbotPluginDefinition = {
76
179
  return res.json();
77
180
  }
78
181
 
79
- // ============ DEALS ============
182
+ // ============ DEALS (v2) ============
80
183
 
81
184
  api.registerTool({
82
185
  name: "pipedrive_search_deals",
@@ -84,14 +187,16 @@ const plugin: ClawdbotPluginDefinition = {
84
187
  parameters: Type.Object({
85
188
  term: Type.String({ description: "Search term" }),
86
189
  status: Type.Optional(
87
- Type.String({ description: "Filter by status: open, won, lost, deleted, all_not_deleted" })
190
+ Type.String({ description: "Filter by status: open, won, lost, deleted" })
88
191
  ),
192
+ limit: Type.Optional(Type.Number({ description: "Number of results (default 100)" })),
89
193
  }),
90
194
  async execute(_id, params) {
91
- const { term, status } = params as { term: string; status?: string };
92
- let endpoint = `/deals/search?term=${encodeURIComponent(term)}`;
93
- if (status) endpoint += `&status=${status}`;
94
- const data = await pipedriveRequest(endpoint);
195
+ const { term, status, limit } = params as { term: string; status?: string; limit?: number };
196
+ const query = new URLSearchParams({ term });
197
+ if (status) query.set("status", status);
198
+ if (limit) query.set("limit", String(limit));
199
+ const data = await pipedriveRequest(`/deals/search?${query}`);
95
200
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
96
201
  },
97
202
  });
@@ -113,26 +218,22 @@ const plugin: ClawdbotPluginDefinition = {
113
218
  name: "pipedrive_list_deals",
114
219
  description: "List deals with optional filters",
115
220
  parameters: Type.Object({
116
- status: Type.Optional(Type.String({ description: "Filter by status: open, won, lost, deleted, all_not_deleted" })),
221
+ status: Type.Optional(Type.String({ description: "Filter by status: open, won, lost, deleted" })),
117
222
  stage_id: Type.Optional(Type.Number({ description: "Filter by pipeline stage ID" })),
118
- user_id: Type.Optional(Type.Number({ description: "Filter by owner user ID" })),
223
+ owner_id: Type.Optional(Type.Number({ description: "Filter by owner user ID" })),
224
+ person_id: Type.Optional(Type.Number({ description: "Filter by person ID" })),
225
+ org_id: Type.Optional(Type.Number({ description: "Filter by organization ID" })),
226
+ pipeline_id: Type.Optional(Type.Number({ description: "Filter by pipeline ID" })),
119
227
  limit: Type.Optional(Type.Number({ description: "Number of results (default 100, max 500)" })),
120
- start: Type.Optional(Type.Number({ description: "Pagination start (default 0)" })),
228
+ cursor: Type.Optional(Type.String({ description: "Pagination cursor from previous response" })),
229
+ sort_by: Type.Optional(Type.String({ description: "Sort by: id, add_time, update_time" })),
230
+ sort_direction: Type.Optional(Type.String({ description: "Sort direction: asc, desc" })),
121
231
  }),
122
232
  async execute(_id, params) {
123
- const { status, stage_id, user_id, limit, start } = params as {
124
- status?: string;
125
- stage_id?: number;
126
- user_id?: number;
127
- limit?: number;
128
- start?: number;
129
- };
130
233
  const query = new URLSearchParams();
131
- if (status) query.set("status", status);
132
- if (stage_id) query.set("stage_id", String(stage_id));
133
- if (user_id) query.set("user_id", String(user_id));
134
- if (limit) query.set("limit", String(limit));
135
- if (start) query.set("start", String(start));
234
+ for (const [key, value] of Object.entries(params)) {
235
+ if (value !== undefined) query.set(key, String(value));
236
+ }
136
237
  const data = await pipedriveRequest(`/deals?${query}`);
137
238
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
138
239
  },
@@ -148,7 +249,8 @@ const plugin: ClawdbotPluginDefinition = {
148
249
  person_id: Type.Optional(Type.Number({ description: "Associated person/contact ID" })),
149
250
  org_id: Type.Optional(Type.Number({ description: "Associated organization ID" })),
150
251
  stage_id: Type.Optional(Type.Number({ description: "Pipeline stage ID" })),
151
- user_id: Type.Optional(Type.Number({ description: "Owner user ID" })),
252
+ owner_id: Type.Optional(Type.Number({ description: "Owner user ID" })),
253
+ pipeline_id: Type.Optional(Type.Number({ description: "Pipeline ID" })),
152
254
  expected_close_date: Type.Optional(Type.String({ description: "Expected close date (YYYY-MM-DD)" })),
153
255
  }),
154
256
  async execute(_id, params) {
@@ -170,14 +272,15 @@ const plugin: ClawdbotPluginDefinition = {
170
272
  currency: Type.Optional(Type.String({ description: "Currency code" })),
171
273
  status: Type.Optional(Type.String({ description: "Status: open, won, lost, deleted" })),
172
274
  stage_id: Type.Optional(Type.Number({ description: "Move to stage ID" })),
173
- user_id: Type.Optional(Type.Number({ description: "New owner user ID" })),
275
+ owner_id: Type.Optional(Type.Number({ description: "New owner user ID" })),
276
+ pipeline_id: Type.Optional(Type.Number({ description: "Move to pipeline ID" })),
174
277
  expected_close_date: Type.Optional(Type.String({ description: "Expected close date (YYYY-MM-DD)" })),
175
278
  lost_reason: Type.Optional(Type.String({ description: "Reason for losing (when status=lost)" })),
176
279
  }),
177
280
  async execute(_id, params) {
178
281
  const { id, ...updateParams } = params as { id: number } & Record<string, unknown>;
179
282
  const data = await pipedriveRequest(`/deals/${id}`, {
180
- method: "PUT",
283
+ method: "PATCH", // v2 uses PATCH instead of PUT
181
284
  body: JSON.stringify(updateParams),
182
285
  });
183
286
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
@@ -186,7 +289,7 @@ const plugin: ClawdbotPluginDefinition = {
186
289
 
187
290
  api.registerTool({
188
291
  name: "pipedrive_delete_deal",
189
- description: "Delete a deal",
292
+ description: "Delete a deal (marks as deleted, 30-day retention)",
190
293
  parameters: Type.Object({
191
294
  id: Type.Number({ description: "Deal ID to delete" }),
192
295
  }),
@@ -197,17 +300,20 @@ const plugin: ClawdbotPluginDefinition = {
197
300
  },
198
301
  });
199
302
 
200
- // ============ PERSONS (CONTACTS) ============
303
+ // ============ PERSONS (v2) ============
201
304
 
202
305
  api.registerTool({
203
306
  name: "pipedrive_search_persons",
204
- description: "Search for persons/contacts in Pipedrive",
307
+ description: "Search for persons/contacts by name, email, phone, or notes",
205
308
  parameters: Type.Object({
206
309
  term: Type.String({ description: "Search term (name, email, phone)" }),
310
+ limit: Type.Optional(Type.Number({ description: "Number of results" })),
207
311
  }),
208
312
  async execute(_id, params) {
209
- const { term } = params as { term: string };
210
- const data = await pipedriveRequest(`/persons/search?term=${encodeURIComponent(term)}`);
313
+ const { term, limit } = params as { term: string; limit?: number };
314
+ const query = new URLSearchParams({ term });
315
+ if (limit) query.set("limit", String(limit));
316
+ const data = await pipedriveRequest(`/persons/search?${query}`);
211
317
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
212
318
  },
213
319
  });
@@ -225,6 +331,27 @@ const plugin: ClawdbotPluginDefinition = {
225
331
  },
226
332
  });
227
333
 
334
+ api.registerTool({
335
+ name: "pipedrive_list_persons",
336
+ description: "List all persons with optional filters",
337
+ parameters: Type.Object({
338
+ owner_id: Type.Optional(Type.Number({ description: "Filter by owner user ID" })),
339
+ org_id: Type.Optional(Type.Number({ description: "Filter by organization ID" })),
340
+ limit: Type.Optional(Type.Number({ description: "Number of results (default 100)" })),
341
+ cursor: Type.Optional(Type.String({ description: "Pagination cursor" })),
342
+ sort_by: Type.Optional(Type.String({ description: "Sort by: id, add_time, update_time, name" })),
343
+ sort_direction: Type.Optional(Type.String({ description: "Sort direction: asc, desc" })),
344
+ }),
345
+ async execute(_id, params) {
346
+ const query = new URLSearchParams();
347
+ for (const [key, value] of Object.entries(params)) {
348
+ if (value !== undefined) query.set(key, String(value));
349
+ }
350
+ const data = await pipedriveRequest(`/persons?${query}`);
351
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
352
+ },
353
+ });
354
+
228
355
  api.registerTool({
229
356
  name: "pipedrive_create_person",
230
357
  description: "Create a new person/contact",
@@ -233,11 +360,18 @@ const plugin: ClawdbotPluginDefinition = {
233
360
  email: Type.Optional(Type.String({ description: "Email address" })),
234
361
  phone: Type.Optional(Type.String({ description: "Phone number" })),
235
362
  org_id: Type.Optional(Type.Number({ description: "Associated organization ID" })),
363
+ owner_id: Type.Optional(Type.Number({ description: "Owner user ID" })),
236
364
  }),
237
365
  async execute(_id, params) {
366
+ // v2 expects email/phone as arrays of objects
367
+ const { email, phone, ...rest } = params as { email?: string; phone?: string } & Record<string, unknown>;
368
+ const body: Record<string, unknown> = { ...rest };
369
+ if (email) body.emails = [{ value: email, primary: true, label: "work" }];
370
+ if (phone) body.phones = [{ value: phone, primary: true, label: "work" }];
371
+
238
372
  const data = await pipedriveRequest("/persons", {
239
373
  method: "POST",
240
- body: JSON.stringify(params),
374
+ body: JSON.stringify(body),
241
375
  });
242
376
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
243
377
  },
@@ -252,28 +386,49 @@ const plugin: ClawdbotPluginDefinition = {
252
386
  email: Type.Optional(Type.String({ description: "New email" })),
253
387
  phone: Type.Optional(Type.String({ description: "New phone" })),
254
388
  org_id: Type.Optional(Type.Number({ description: "New organization ID" })),
389
+ owner_id: Type.Optional(Type.Number({ description: "New owner user ID" })),
255
390
  }),
256
391
  async execute(_id, params) {
257
- const { id, ...updateParams } = params as { id: number } & Record<string, unknown>;
392
+ const { id, email, phone, ...rest } = params as { id: number; email?: string; phone?: string } & Record<string, unknown>;
393
+ const body: Record<string, unknown> = { ...rest };
394
+ if (email) body.emails = [{ value: email, primary: true, label: "work" }];
395
+ if (phone) body.phones = [{ value: phone, primary: true, label: "work" }];
396
+
258
397
  const data = await pipedriveRequest(`/persons/${id}`, {
259
- method: "PUT",
260
- body: JSON.stringify(updateParams),
398
+ method: "PATCH",
399
+ body: JSON.stringify(body),
261
400
  });
262
401
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
263
402
  },
264
403
  });
265
404
 
266
- // ============ ORGANIZATIONS ============
405
+ api.registerTool({
406
+ name: "pipedrive_delete_person",
407
+ description: "Delete a person (marks as deleted, 30-day retention)",
408
+ parameters: Type.Object({
409
+ id: Type.Number({ description: "Person ID to delete" }),
410
+ }),
411
+ async execute(_id, params) {
412
+ const { id } = params as { id: number };
413
+ const data = await pipedriveRequest(`/persons/${id}`, { method: "DELETE" });
414
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
415
+ },
416
+ });
417
+
418
+ // ============ ORGANIZATIONS (v2) ============
267
419
 
268
420
  api.registerTool({
269
421
  name: "pipedrive_search_organizations",
270
- description: "Search for organizations in Pipedrive",
422
+ description: "Search for organizations by name, address, or notes",
271
423
  parameters: Type.Object({
272
424
  term: Type.String({ description: "Search term (organization name)" }),
425
+ limit: Type.Optional(Type.Number({ description: "Number of results" })),
273
426
  }),
274
427
  async execute(_id, params) {
275
- const { term } = params as { term: string };
276
- const data = await pipedriveRequest(`/organizations/search?term=${encodeURIComponent(term)}`);
428
+ const { term, limit } = params as { term: string; limit?: number };
429
+ const query = new URLSearchParams({ term });
430
+ if (limit) query.set("limit", String(limit));
431
+ const data = await pipedriveRequest(`/organizations/search?${query}`);
277
432
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
278
433
  },
279
434
  });
@@ -291,12 +446,33 @@ const plugin: ClawdbotPluginDefinition = {
291
446
  },
292
447
  });
293
448
 
449
+ api.registerTool({
450
+ name: "pipedrive_list_organizations",
451
+ description: "List all organizations with optional filters",
452
+ parameters: Type.Object({
453
+ owner_id: Type.Optional(Type.Number({ description: "Filter by owner user ID" })),
454
+ limit: Type.Optional(Type.Number({ description: "Number of results (default 100)" })),
455
+ cursor: Type.Optional(Type.String({ description: "Pagination cursor" })),
456
+ sort_by: Type.Optional(Type.String({ description: "Sort by: id, add_time, update_time, name" })),
457
+ sort_direction: Type.Optional(Type.String({ description: "Sort direction: asc, desc" })),
458
+ }),
459
+ async execute(_id, params) {
460
+ const query = new URLSearchParams();
461
+ for (const [key, value] of Object.entries(params)) {
462
+ if (value !== undefined) query.set(key, String(value));
463
+ }
464
+ const data = await pipedriveRequest(`/organizations?${query}`);
465
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
466
+ },
467
+ });
468
+
294
469
  api.registerTool({
295
470
  name: "pipedrive_create_organization",
296
471
  description: "Create a new organization",
297
472
  parameters: Type.Object({
298
473
  name: Type.String({ description: "Organization name (required)" }),
299
474
  address: Type.Optional(Type.String({ description: "Address" })),
475
+ owner_id: Type.Optional(Type.Number({ description: "Owner user ID" })),
300
476
  }),
301
477
  async execute(_id, params) {
302
478
  const data = await pipedriveRequest("/organizations", {
@@ -307,7 +483,39 @@ const plugin: ClawdbotPluginDefinition = {
307
483
  },
308
484
  });
309
485
 
310
- // ============ ACTIVITIES ============
486
+ api.registerTool({
487
+ name: "pipedrive_update_organization",
488
+ description: "Update an existing organization",
489
+ parameters: Type.Object({
490
+ id: Type.Number({ description: "Organization ID to update (required)" }),
491
+ name: Type.Optional(Type.String({ description: "New name" })),
492
+ address: Type.Optional(Type.String({ description: "New address" })),
493
+ owner_id: Type.Optional(Type.Number({ description: "New owner user ID" })),
494
+ }),
495
+ async execute(_id, params) {
496
+ const { id, ...updateParams } = params as { id: number } & Record<string, unknown>;
497
+ const data = await pipedriveRequest(`/organizations/${id}`, {
498
+ method: "PATCH",
499
+ body: JSON.stringify(updateParams),
500
+ });
501
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
502
+ },
503
+ });
504
+
505
+ api.registerTool({
506
+ name: "pipedrive_delete_organization",
507
+ description: "Delete an organization (marks as deleted, 30-day retention)",
508
+ parameters: Type.Object({
509
+ id: Type.Number({ description: "Organization ID to delete" }),
510
+ }),
511
+ async execute(_id, params) {
512
+ const { id } = params as { id: number };
513
+ const data = await pipedriveRequest(`/organizations/${id}`, { method: "DELETE" });
514
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
515
+ },
516
+ });
517
+
518
+ // ============ ACTIVITIES (v2) ============
311
519
 
312
520
  api.registerTool({
313
521
  name: "pipedrive_list_activities",
@@ -316,10 +524,13 @@ const plugin: ClawdbotPluginDefinition = {
316
524
  deal_id: Type.Optional(Type.Number({ description: "Filter by deal ID" })),
317
525
  person_id: Type.Optional(Type.Number({ description: "Filter by person ID" })),
318
526
  org_id: Type.Optional(Type.Number({ description: "Filter by organization ID" })),
319
- done: Type.Optional(Type.Number({ description: "Filter by completion: 0 = not done, 1 = done" })),
527
+ owner_id: Type.Optional(Type.Number({ description: "Filter by owner user ID" })),
528
+ done: Type.Optional(Type.Boolean({ description: "Filter by completion: true = done, false = not done" })),
320
529
  type: Type.Optional(Type.String({ description: "Filter by type: call, meeting, task, deadline, email, lunch" })),
321
530
  limit: Type.Optional(Type.Number({ description: "Number of results (default 100)" })),
322
- start: Type.Optional(Type.Number({ description: "Pagination start" })),
531
+ cursor: Type.Optional(Type.String({ description: "Pagination cursor" })),
532
+ sort_by: Type.Optional(Type.String({ description: "Sort by: id, add_time, update_time, due_date" })),
533
+ sort_direction: Type.Optional(Type.String({ description: "Sort direction: asc, desc" })),
323
534
  }),
324
535
  async execute(_id, params) {
325
536
  const query = new URLSearchParams();
@@ -357,7 +568,8 @@ const plugin: ClawdbotPluginDefinition = {
357
568
  person_id: Type.Optional(Type.Number({ description: "Associated person ID" })),
358
569
  org_id: Type.Optional(Type.Number({ description: "Associated organization ID" })),
359
570
  note: Type.Optional(Type.String({ description: "Activity notes/description" })),
360
- done: Type.Optional(Type.Number({ description: "Mark as done: 0 = not done, 1 = done" })),
571
+ done: Type.Optional(Type.Boolean({ description: "Mark as done: true = done, false = not done" })),
572
+ owner_id: Type.Optional(Type.Number({ description: "Owner user ID" })),
361
573
  }),
362
574
  async execute(_id, params) {
363
575
  const data = await pipedriveRequest("/activities", {
@@ -377,13 +589,14 @@ const plugin: ClawdbotPluginDefinition = {
377
589
  type: Type.Optional(Type.String({ description: "New type" })),
378
590
  due_date: Type.Optional(Type.String({ description: "New due date (YYYY-MM-DD)" })),
379
591
  due_time: Type.Optional(Type.String({ description: "New due time (HH:MM)" })),
380
- done: Type.Optional(Type.Number({ description: "Mark as done: 0 = not done, 1 = done" })),
592
+ done: Type.Optional(Type.Boolean({ description: "Mark as done: true = done, false = not done" })),
381
593
  note: Type.Optional(Type.String({ description: "New notes" })),
594
+ owner_id: Type.Optional(Type.Number({ description: "New owner user ID" })),
382
595
  }),
383
596
  async execute(_id, params) {
384
597
  const { id, ...updateParams } = params as { id: number } & Record<string, unknown>;
385
598
  const data = await pipedriveRequest(`/activities/${id}`, {
386
- method: "PUT",
599
+ method: "PATCH",
387
600
  body: JSON.stringify(updateParams),
388
601
  });
389
602
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
@@ -392,7 +605,7 @@ const plugin: ClawdbotPluginDefinition = {
392
605
 
393
606
  api.registerTool({
394
607
  name: "pipedrive_delete_activity",
395
- description: "Delete an activity",
608
+ description: "Delete an activity (marks as deleted, 30-day retention)",
396
609
  parameters: Type.Object({
397
610
  id: Type.Number({ description: "Activity ID to delete" }),
398
611
  }),
@@ -403,34 +616,74 @@ const plugin: ClawdbotPluginDefinition = {
403
616
  },
404
617
  });
405
618
 
406
- // ============ PIPELINES & STAGES ============
619
+ // ============ PIPELINES (v2) ============
407
620
 
408
621
  api.registerTool({
409
622
  name: "pipedrive_list_pipelines",
410
623
  description: "List all pipelines",
411
- parameters: Type.Object({}),
412
- async execute() {
413
- const data = await pipedriveRequest("/pipelines");
624
+ parameters: Type.Object({
625
+ limit: Type.Optional(Type.Number({ description: "Number of results" })),
626
+ cursor: Type.Optional(Type.String({ description: "Pagination cursor" })),
627
+ }),
628
+ async execute(_id, params) {
629
+ const query = new URLSearchParams();
630
+ for (const [key, value] of Object.entries(params)) {
631
+ if (value !== undefined) query.set(key, String(value));
632
+ }
633
+ const endpoint = query.toString() ? `/pipelines?${query}` : "/pipelines";
634
+ const data = await pipedriveRequest(endpoint);
414
635
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
415
636
  },
416
637
  });
417
638
 
639
+ api.registerTool({
640
+ name: "pipedrive_get_pipeline",
641
+ description: "Get details of a specific pipeline by ID",
642
+ parameters: Type.Object({
643
+ id: Type.Number({ description: "Pipeline ID" }),
644
+ }),
645
+ async execute(_id, params) {
646
+ const { id } = params as { id: number };
647
+ const data = await pipedriveRequest(`/pipelines/${id}`);
648
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
649
+ },
650
+ });
651
+
652
+ // ============ STAGES (v2) ============
653
+
418
654
  api.registerTool({
419
655
  name: "pipedrive_list_stages",
420
656
  description: "List all stages, optionally filtered by pipeline",
421
657
  parameters: Type.Object({
422
658
  pipeline_id: Type.Optional(Type.Number({ description: "Filter by pipeline ID" })),
659
+ limit: Type.Optional(Type.Number({ description: "Number of results" })),
660
+ cursor: Type.Optional(Type.String({ description: "Pagination cursor" })),
423
661
  }),
424
662
  async execute(_id, params) {
425
- const { pipeline_id } = params as { pipeline_id?: number };
426
- let endpoint = "/stages";
427
- if (pipeline_id) endpoint += `?pipeline_id=${pipeline_id}`;
663
+ const query = new URLSearchParams();
664
+ for (const [key, value] of Object.entries(params)) {
665
+ if (value !== undefined) query.set(key, String(value));
666
+ }
667
+ const endpoint = query.toString() ? `/stages?${query}` : "/stages";
428
668
  const data = await pipedriveRequest(endpoint);
429
669
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
430
670
  },
431
671
  });
432
672
 
433
- // ============ NOTES ============
673
+ api.registerTool({
674
+ name: "pipedrive_get_stage",
675
+ description: "Get details of a specific stage by ID",
676
+ parameters: Type.Object({
677
+ id: Type.Number({ description: "Stage ID" }),
678
+ }),
679
+ async execute(_id, params) {
680
+ const { id } = params as { id: number };
681
+ const data = await pipedriveRequest(`/stages/${id}`);
682
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
683
+ },
684
+ });
685
+
686
+ // ============ NOTES (v1 - no v2 available yet) ============
434
687
 
435
688
  api.registerTool({
436
689
  name: "pipedrive_list_notes",
@@ -440,13 +693,27 @@ const plugin: ClawdbotPluginDefinition = {
440
693
  person_id: Type.Optional(Type.Number({ description: "Filter by person ID" })),
441
694
  org_id: Type.Optional(Type.Number({ description: "Filter by organization ID" })),
442
695
  limit: Type.Optional(Type.Number({ description: "Number of results" })),
696
+ start: Type.Optional(Type.Number({ description: "Pagination offset" })),
443
697
  }),
444
698
  async execute(_id, params) {
445
699
  const query = new URLSearchParams();
446
700
  for (const [key, value] of Object.entries(params)) {
447
701
  if (value !== undefined) query.set(key, String(value));
448
702
  }
449
- const data = await pipedriveRequest(`/notes?${query}`);
703
+ const data = await pipedriveRequest(`/notes?${query}`, { useV1: true });
704
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
705
+ },
706
+ });
707
+
708
+ api.registerTool({
709
+ name: "pipedrive_get_note",
710
+ description: "Get details of a specific note by ID",
711
+ parameters: Type.Object({
712
+ id: Type.Number({ description: "Note ID" }),
713
+ }),
714
+ async execute(_id, params) {
715
+ const { id } = params as { id: number };
716
+ const data = await pipedriveRequest(`/notes/${id}`, { useV1: true });
450
717
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
451
718
  },
452
719
  });
@@ -464,19 +731,51 @@ const plugin: ClawdbotPluginDefinition = {
464
731
  const data = await pipedriveRequest("/notes", {
465
732
  method: "POST",
466
733
  body: JSON.stringify(params),
734
+ useV1: true,
735
+ });
736
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
737
+ },
738
+ });
739
+
740
+ api.registerTool({
741
+ name: "pipedrive_update_note",
742
+ description: "Update an existing note",
743
+ parameters: Type.Object({
744
+ id: Type.Number({ description: "Note ID to update (required)" }),
745
+ content: Type.String({ description: "New content" }),
746
+ }),
747
+ async execute(_id, params) {
748
+ const { id, ...updateParams } = params as { id: number } & Record<string, unknown>;
749
+ const data = await pipedriveRequest(`/notes/${id}`, {
750
+ method: "PUT", // v1 uses PUT
751
+ body: JSON.stringify(updateParams),
752
+ useV1: true,
467
753
  });
468
754
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
469
755
  },
470
756
  });
471
757
 
472
- // ============ USERS ============
758
+ api.registerTool({
759
+ name: "pipedrive_delete_note",
760
+ description: "Delete a note",
761
+ parameters: Type.Object({
762
+ id: Type.Number({ description: "Note ID to delete" }),
763
+ }),
764
+ async execute(_id, params) {
765
+ const { id } = params as { id: number };
766
+ const data = await pipedriveRequest(`/notes/${id}`, { method: "DELETE", useV1: true });
767
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
768
+ },
769
+ });
770
+
771
+ // ============ USERS (v1 - mostly no v2 available) ============
473
772
 
474
773
  api.registerTool({
475
774
  name: "pipedrive_list_users",
476
775
  description: "List all users in the Pipedrive account",
477
776
  parameters: Type.Object({}),
478
777
  async execute() {
479
- const data = await pipedriveRequest("/users");
778
+ const data = await pipedriveRequest("/users", { useV1: true });
480
779
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
481
780
  },
482
781
  });
@@ -486,12 +785,27 @@ const plugin: ClawdbotPluginDefinition = {
486
785
  description: "Get the current authenticated user's details",
487
786
  parameters: Type.Object({}),
488
787
  async execute() {
489
- const data = await pipedriveRequest("/users/me");
788
+ const data = await pipedriveRequest("/users/me", { useV1: true });
789
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
790
+ },
791
+ });
792
+
793
+ api.registerTool({
794
+ name: "pipedrive_get_user",
795
+ description: "Get details of a specific user by ID",
796
+ parameters: Type.Object({
797
+ id: Type.Number({ description: "User ID" }),
798
+ }),
799
+ async execute(_id, params) {
800
+ const { id } = params as { id: number };
801
+ const data = await pipedriveRequest(`/users/${id}`, { useV1: true });
490
802
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
491
803
  },
492
804
  });
493
805
 
494
- console.log(`[pipedrive] Registered ${22} tools for ${cfg.domain}.pipedrive.com`);
806
+ const v2Tools = 22;
807
+ const v1Tools = 6;
808
+ console.log(`[pipedrive] Registered ${v2Tools + v1Tools} tools (${v2Tools} v2, ${v1Tools} v1) for ${cfg.domain}.pipedrive.com`);
495
809
  },
496
810
  };
497
811
 
package/install.sh CHANGED
@@ -28,14 +28,19 @@ echo
28
28
  echo "[2/3] Setting up skill template..."
29
29
  mkdir -p "$SKILL_DIR"
30
30
 
31
+ LATEST_FILE="$SKILL_DIR/SKILL.md.latest"
32
+
31
33
  if [ -f "$SKILL_FILE" ]; then
32
- echo " $SKILL_FILE already exists. Skipping (not overwriting)."
33
- echo " To update manually, see: https://github.com/graileanu/clawdbot-pipedrive/blob/master/examples/SKILL-TEMPLATE.md"
34
+ echo " $SKILL_FILE already exists (not overwriting your customizations)."
35
+ # Download latest template for comparison
36
+ curl -sL "https://raw.githubusercontent.com/graileanu/clawdbot-pipedrive/master/examples/SKILL-TEMPLATE.md" -o "$LATEST_FILE"
37
+ echo " Latest template saved to: $LATEST_FILE"
38
+ echo " Compare changes: diff $SKILL_FILE $LATEST_FILE"
34
39
  else
35
40
  # Download template from GitHub
36
41
  curl -sL "https://raw.githubusercontent.com/graileanu/clawdbot-pipedrive/master/examples/SKILL-TEMPLATE.md" -o "$SKILL_FILE"
37
42
  echo " Created $SKILL_FILE"
38
- echo " Edit this file to customize for your organization."
43
+ echo " Customize this file for your organization's workflows."
39
44
  fi
40
45
  echo
41
46
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawdbot-pipedrive",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "description": "Pipedrive CRM integration for Clawdbot",
6
6
  "author": "graileanu",