capsulemcp 1.8.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 +6 -6
- package/dist/http.js +861 -841
- package/dist/index.js +842 -779
- package/package.json +7 -8
package/dist/index.js
CHANGED
|
@@ -138,6 +138,26 @@ function invalidateByPrefix(pathPrefix, trigger) {
|
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
// src/capsule/normalize.ts
|
|
142
|
+
var KEY_RENAMES = {
|
|
143
|
+
kase: "project",
|
|
144
|
+
kases: "projects",
|
|
145
|
+
restrictedKases: "restrictedProjects"
|
|
146
|
+
};
|
|
147
|
+
function normalizeProjectKeys(value) {
|
|
148
|
+
if (Array.isArray(value)) {
|
|
149
|
+
return value.map(normalizeProjectKeys);
|
|
150
|
+
}
|
|
151
|
+
if (value !== null && typeof value === "object") {
|
|
152
|
+
const out = {};
|
|
153
|
+
for (const [key, v] of Object.entries(value)) {
|
|
154
|
+
out[KEY_RENAMES[key] ?? key] = normalizeProjectKeys(v);
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
return value;
|
|
159
|
+
}
|
|
160
|
+
|
|
141
161
|
// src/capsule/client.ts
|
|
142
162
|
var DEFAULT_BASE_URL = "https://api.capsulecrm.com/api/v2";
|
|
143
163
|
function baseUrl() {
|
|
@@ -253,38 +273,31 @@ async function parseErrorBody(res) {
|
|
|
253
273
|
}
|
|
254
274
|
}
|
|
255
275
|
var REQUEST_TIMEOUT_MS = 6e4;
|
|
256
|
-
function
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const controller = new AbortController();
|
|
262
|
-
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
263
|
-
timer.unref();
|
|
264
|
-
return {
|
|
265
|
-
options: { ...options ?? {}, signal: controller.signal },
|
|
266
|
-
cleanup: () => clearTimeout(timer)
|
|
267
|
-
};
|
|
276
|
+
function isTimeoutAbort(err) {
|
|
277
|
+
return err instanceof Error && // AbortSignal.timeout rejects with a DOMException named
|
|
278
|
+
// "TimeoutError"; plain aborts (and older undici paths) surface
|
|
279
|
+
// as "AbortError" or carry "aborted" in the message.
|
|
280
|
+
(err.name === "TimeoutError" || err.name === "AbortError" || /aborted/i.test(err.message));
|
|
268
281
|
}
|
|
269
282
|
async function mapAbort(p) {
|
|
270
283
|
try {
|
|
271
284
|
return await p;
|
|
272
285
|
} catch (err) {
|
|
273
|
-
if (
|
|
286
|
+
if (isTimeoutAbort(err)) {
|
|
274
287
|
throw new CapsuleTimeoutError();
|
|
275
288
|
}
|
|
276
289
|
throw err;
|
|
277
290
|
}
|
|
278
291
|
}
|
|
279
292
|
async function fetchWithTimeout(url, options) {
|
|
280
|
-
const { options: opts, cleanup } = withTimeout(options);
|
|
281
293
|
const startedAt = Date.now();
|
|
282
294
|
try {
|
|
283
|
-
|
|
284
|
-
|
|
295
|
+
return await fetch(url, {
|
|
296
|
+
...options ?? {},
|
|
297
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
298
|
+
});
|
|
285
299
|
} catch (err) {
|
|
286
|
-
|
|
287
|
-
const isAbort = err instanceof Error && (err.name === "AbortError" || /aborted/i.test(err.message));
|
|
300
|
+
const isAbort = isTimeoutAbort(err);
|
|
288
301
|
emitCapsuleFailure(
|
|
289
302
|
options?.method ?? "GET",
|
|
290
303
|
url,
|
|
@@ -308,24 +321,22 @@ async function doFetch(url, options) {
|
|
|
308
321
|
const startedAt = Date.now();
|
|
309
322
|
const method = options?.method ?? "GET";
|
|
310
323
|
const first = await fetchWithTimeout(url, options);
|
|
311
|
-
if (first.
|
|
312
|
-
const delay = parseRateLimitDelay(first
|
|
313
|
-
first
|
|
314
|
-
await drainBody(first.res);
|
|
324
|
+
if (first.status === 429) {
|
|
325
|
+
const delay = parseRateLimitDelay(first);
|
|
326
|
+
await drainBody(first);
|
|
315
327
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
316
328
|
const retried = await fetchWithTimeout(url, options);
|
|
317
|
-
if (retried.
|
|
318
|
-
retried
|
|
319
|
-
await drainBody(retried.res);
|
|
329
|
+
if (retried.status === 429) {
|
|
330
|
+
await drainBody(retried);
|
|
320
331
|
emitCapsuleRateLimited(method, url, Date.now() - startedAt);
|
|
321
332
|
throw new CapsuleApiError(
|
|
322
333
|
429,
|
|
323
334
|
"Rate limit exceeded after one retry. Please slow down your requests."
|
|
324
335
|
);
|
|
325
336
|
}
|
|
326
|
-
return {
|
|
337
|
+
return { res: retried, startedAt, method, url, retriedAfter429: true };
|
|
327
338
|
}
|
|
328
|
-
return {
|
|
339
|
+
return { res: first, startedAt, method, url, retriedAfter429: false };
|
|
329
340
|
}
|
|
330
341
|
async function consumeBody(start, body) {
|
|
331
342
|
try {
|
|
@@ -418,7 +429,8 @@ async function throwForStatus(res) {
|
|
|
418
429
|
}
|
|
419
430
|
async function handleResponse(res) {
|
|
420
431
|
await throwForStatus(res);
|
|
421
|
-
|
|
432
|
+
const body = await mapAbort(res.json());
|
|
433
|
+
return normalizeProjectKeys(body);
|
|
422
434
|
}
|
|
423
435
|
function buildUrl(path, params) {
|
|
424
436
|
const url = new URL(`${baseUrl()}${path}`);
|
|
@@ -435,15 +447,19 @@ async function capsuleGet(path, params) {
|
|
|
435
447
|
const token = getToken();
|
|
436
448
|
const url = buildUrl(path, params);
|
|
437
449
|
const start = await doFetch(url, { headers: baseHeaders(token) });
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
}
|
|
450
|
+
return consumeBody(start, async () => {
|
|
451
|
+
const data = await handleResponse(start.res);
|
|
452
|
+
const nextPage = parseNextPage(start.res.headers.get("Link"));
|
|
453
|
+
return { data, nextPage };
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
async function capsuleGetList(path, params) {
|
|
457
|
+
const { data, nextPage } = await capsuleGet(path, params);
|
|
458
|
+
return { ...data, nextPage };
|
|
459
|
+
}
|
|
460
|
+
async function capsuleGetCachedList(path, params) {
|
|
461
|
+
const { data, nextPage } = await capsuleGetCached(path, params);
|
|
462
|
+
return { ...data, nextPage };
|
|
447
463
|
}
|
|
448
464
|
async function capsuleGetCached(path, params) {
|
|
449
465
|
if (cacheDisabled()) return capsuleGet(path, params);
|
|
@@ -482,11 +498,7 @@ async function capsulePost(path, body) {
|
|
|
482
498
|
headers: { ...baseHeaders(token), "Content-Type": "application/json" },
|
|
483
499
|
body: JSON.stringify(body)
|
|
484
500
|
});
|
|
485
|
-
|
|
486
|
-
return await consumeBody(start, () => handleResponse(start.res));
|
|
487
|
-
} finally {
|
|
488
|
-
start.cleanup();
|
|
489
|
-
}
|
|
501
|
+
return consumeBody(start, () => handleResponse(start.res));
|
|
490
502
|
}
|
|
491
503
|
async function capsulePostNoContent(path) {
|
|
492
504
|
if (isReadOnly()) throw new CapsuleReadOnlyError("POST");
|
|
@@ -496,15 +508,11 @@ async function capsulePostNoContent(path) {
|
|
|
496
508
|
method: "POST",
|
|
497
509
|
headers: baseHeaders(token)
|
|
498
510
|
});
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
});
|
|
505
|
-
} finally {
|
|
506
|
-
start.cleanup();
|
|
507
|
-
}
|
|
511
|
+
await consumeBody(start, async () => {
|
|
512
|
+
if (start.res.status === 204) return;
|
|
513
|
+
await throwForStatus(start.res);
|
|
514
|
+
await mapAbort(start.res.text());
|
|
515
|
+
});
|
|
508
516
|
}
|
|
509
517
|
async function capsuleSearch(path, body, params) {
|
|
510
518
|
const token = getToken();
|
|
@@ -514,15 +522,11 @@ async function capsuleSearch(path, body, params) {
|
|
|
514
522
|
headers: { ...baseHeaders(token), "Content-Type": "application/json" },
|
|
515
523
|
body: JSON.stringify(body)
|
|
516
524
|
});
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
});
|
|
523
|
-
} finally {
|
|
524
|
-
start.cleanup();
|
|
525
|
-
}
|
|
525
|
+
return consumeBody(start, async () => {
|
|
526
|
+
const data = await handleResponse(start.res);
|
|
527
|
+
const nextPage = parseNextPage(start.res.headers.get("Link"));
|
|
528
|
+
return { data, nextPage };
|
|
529
|
+
});
|
|
526
530
|
}
|
|
527
531
|
async function capsulePut(path, body) {
|
|
528
532
|
if (isReadOnly()) throw new CapsuleReadOnlyError("PUT");
|
|
@@ -533,68 +537,60 @@ async function capsulePut(path, body) {
|
|
|
533
537
|
headers: { ...baseHeaders(token), "Content-Type": "application/json" },
|
|
534
538
|
body: JSON.stringify(body)
|
|
535
539
|
});
|
|
536
|
-
|
|
537
|
-
return await consumeBody(start, () => handleResponse(start.res));
|
|
538
|
-
} finally {
|
|
539
|
-
start.cleanup();
|
|
540
|
-
}
|
|
540
|
+
return consumeBody(start, () => handleResponse(start.res));
|
|
541
541
|
}
|
|
542
542
|
async function capsuleGetBinary(path, maxBytes) {
|
|
543
543
|
const token = getToken();
|
|
544
544
|
const url = buildUrl(path);
|
|
545
545
|
const start = await doFetch(url, { headers: baseHeaders(token) });
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
if (
|
|
554
|
-
|
|
555
|
-
|
|
546
|
+
return consumeBody(start, async () => {
|
|
547
|
+
const res = start.res;
|
|
548
|
+
await throwForStatus(res);
|
|
549
|
+
const contentType = res.headers.get("Content-Type") ?? "application/octet-stream";
|
|
550
|
+
const declared = res.headers.get("Content-Length");
|
|
551
|
+
const declaredBytes = declared ? Number(declared) : NaN;
|
|
552
|
+
if (maxBytes !== void 0 && Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {
|
|
553
|
+
if (res.body) await res.body.cancel().catch(() => {
|
|
554
|
+
});
|
|
555
|
+
return {
|
|
556
|
+
contentType,
|
|
557
|
+
buffer: Buffer.alloc(0),
|
|
558
|
+
truncated: true,
|
|
559
|
+
sizeBytes: declaredBytes
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
if (maxBytes !== void 0 && res.body) {
|
|
563
|
+
const reader = res.body.getReader();
|
|
564
|
+
const chunks = [];
|
|
565
|
+
let total = 0;
|
|
566
|
+
let truncated = false;
|
|
567
|
+
while (true) {
|
|
568
|
+
const { done, value } = await mapAbort(reader.read());
|
|
569
|
+
if (done) break;
|
|
570
|
+
total += value.byteLength;
|
|
571
|
+
if (total > maxBytes) {
|
|
572
|
+
truncated = true;
|
|
573
|
+
await reader.cancel().catch(() => {
|
|
574
|
+
});
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
chunks.push(value);
|
|
578
|
+
}
|
|
579
|
+
if (truncated) {
|
|
556
580
|
return {
|
|
557
581
|
contentType,
|
|
558
582
|
buffer: Buffer.alloc(0),
|
|
559
583
|
truncated: true,
|
|
560
|
-
sizeBytes:
|
|
584
|
+
sizeBytes: total
|
|
561
585
|
};
|
|
562
586
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
if (done) break;
|
|
571
|
-
total += value.byteLength;
|
|
572
|
-
if (total > maxBytes) {
|
|
573
|
-
truncated = true;
|
|
574
|
-
await reader.cancel().catch(() => {
|
|
575
|
-
});
|
|
576
|
-
break;
|
|
577
|
-
}
|
|
578
|
-
chunks.push(value);
|
|
579
|
-
}
|
|
580
|
-
if (truncated) {
|
|
581
|
-
return {
|
|
582
|
-
contentType,
|
|
583
|
-
buffer: Buffer.alloc(0),
|
|
584
|
-
truncated: true,
|
|
585
|
-
sizeBytes: total
|
|
586
|
-
};
|
|
587
|
-
}
|
|
588
|
-
const buffer2 = Buffer.concat(chunks.map((c) => Buffer.from(c)));
|
|
589
|
-
return { contentType, buffer: buffer2, sizeBytes: buffer2.length };
|
|
590
|
-
}
|
|
591
|
-
const arrayBuffer = await mapAbort(res.arrayBuffer());
|
|
592
|
-
const buffer = Buffer.from(arrayBuffer);
|
|
593
|
-
return { contentType, buffer, sizeBytes: buffer.length };
|
|
594
|
-
});
|
|
595
|
-
} finally {
|
|
596
|
-
start.cleanup();
|
|
597
|
-
}
|
|
587
|
+
const buffer2 = Buffer.concat(chunks.map((c) => Buffer.from(c)));
|
|
588
|
+
return { contentType, buffer: buffer2, sizeBytes: buffer2.length };
|
|
589
|
+
}
|
|
590
|
+
const arrayBuffer = await mapAbort(res.arrayBuffer());
|
|
591
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
592
|
+
return { contentType, buffer, sizeBytes: buffer.length };
|
|
593
|
+
});
|
|
598
594
|
}
|
|
599
595
|
async function capsulePostBinary(path, body, contentType, filename) {
|
|
600
596
|
if (isReadOnly()) throw new CapsuleReadOnlyError("POST");
|
|
@@ -610,11 +606,7 @@ async function capsulePostBinary(path, body, contentType, filename) {
|
|
|
610
606
|
},
|
|
611
607
|
body
|
|
612
608
|
});
|
|
613
|
-
|
|
614
|
-
return await consumeBody(start, () => handleResponse(start.res));
|
|
615
|
-
} finally {
|
|
616
|
-
start.cleanup();
|
|
617
|
-
}
|
|
609
|
+
return consumeBody(start, () => handleResponse(start.res));
|
|
618
610
|
}
|
|
619
611
|
async function capsuleDelete(path) {
|
|
620
612
|
if (isReadOnly()) throw new CapsuleReadOnlyError("DELETE");
|
|
@@ -624,15 +616,11 @@ async function capsuleDelete(path) {
|
|
|
624
616
|
method: "DELETE",
|
|
625
617
|
headers: baseHeaders(token)
|
|
626
618
|
});
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
});
|
|
633
|
-
} finally {
|
|
634
|
-
start.cleanup();
|
|
635
|
-
}
|
|
619
|
+
await consumeBody(start, async () => {
|
|
620
|
+
if (start.res.status === 204) return;
|
|
621
|
+
await throwForStatus(start.res);
|
|
622
|
+
await mapAbort(start.res.text());
|
|
623
|
+
});
|
|
636
624
|
}
|
|
637
625
|
|
|
638
626
|
// src/server.ts
|
|
@@ -668,6 +656,45 @@ var ICONS = [
|
|
|
668
656
|
}
|
|
669
657
|
];
|
|
670
658
|
|
|
659
|
+
// src/server/tier.ts
|
|
660
|
+
var CORE_TOOLS = /* @__PURE__ */ new Set([
|
|
661
|
+
// Parties
|
|
662
|
+
"search_parties",
|
|
663
|
+
"filter_parties",
|
|
664
|
+
"get_party",
|
|
665
|
+
"create_party",
|
|
666
|
+
"update_party",
|
|
667
|
+
"list_party_entries",
|
|
668
|
+
// Opportunities
|
|
669
|
+
"search_opportunities",
|
|
670
|
+
"filter_opportunities",
|
|
671
|
+
"get_opportunity",
|
|
672
|
+
"create_opportunity",
|
|
673
|
+
"update_opportunity",
|
|
674
|
+
// Projects
|
|
675
|
+
"search_projects",
|
|
676
|
+
"filter_projects",
|
|
677
|
+
"list_projects",
|
|
678
|
+
"get_project",
|
|
679
|
+
"create_project",
|
|
680
|
+
"update_project",
|
|
681
|
+
// Tasks
|
|
682
|
+
"list_tasks",
|
|
683
|
+
"get_task",
|
|
684
|
+
"create_task",
|
|
685
|
+
"update_task",
|
|
686
|
+
"complete_task",
|
|
687
|
+
// Timeline + tags + identity
|
|
688
|
+
"add_note",
|
|
689
|
+
"list_tags",
|
|
690
|
+
"add_tag",
|
|
691
|
+
"get_current_user"
|
|
692
|
+
]);
|
|
693
|
+
function shouldRegister(name) {
|
|
694
|
+
if (process.env["CAPSULE_MCP_TIER"] !== "core") return true;
|
|
695
|
+
return CORE_TOOLS.has(name);
|
|
696
|
+
}
|
|
697
|
+
|
|
671
698
|
// src/tasks/store.ts
|
|
672
699
|
import { InMemoryTaskStore } from "@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js";
|
|
673
700
|
import {
|
|
@@ -876,27 +903,25 @@ function wrapAsText(result) {
|
|
|
876
903
|
};
|
|
877
904
|
}
|
|
878
905
|
function registerTool(server2, name, description, schema, handler) {
|
|
906
|
+
if (!shouldRegister(name)) return;
|
|
879
907
|
const registerWithSchema = server2.registerTool.bind(server2);
|
|
880
908
|
const annotations = inferAnnotations(name);
|
|
881
|
-
registerWithSchema(
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
const
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
} catch (err) {
|
|
893
|
-
emitToolCall({ tool: name, clientId, argFields, startedAt, outcome: "error" });
|
|
894
|
-
throw err;
|
|
895
|
-
}
|
|
909
|
+
registerWithSchema(name, { description, inputSchema: schema, annotations }, async (input) => {
|
|
910
|
+
const startedAt = Date.now();
|
|
911
|
+
const argFields = argFieldNames(input);
|
|
912
|
+
const clientId = getRequestContext()?.clientId;
|
|
913
|
+
try {
|
|
914
|
+
const result = await handler(input);
|
|
915
|
+
emitToolCall({ tool: name, clientId, argFields, startedAt, outcome: "success" });
|
|
916
|
+
return wrapAsText(result);
|
|
917
|
+
} catch (err) {
|
|
918
|
+
emitToolCall({ tool: name, clientId, argFields, startedAt, outcome: "error" });
|
|
919
|
+
throw err;
|
|
896
920
|
}
|
|
897
|
-
);
|
|
921
|
+
});
|
|
898
922
|
}
|
|
899
923
|
function registerToolTask(server2, name, description, schema, handler) {
|
|
924
|
+
if (!shouldRegister(name)) return;
|
|
900
925
|
const registerWithSchema = server2.experimental.tasks.registerToolTask.bind(
|
|
901
926
|
server2.experimental.tasks
|
|
902
927
|
);
|
|
@@ -907,7 +932,7 @@ function registerToolTask(server2, name, description, schema, handler) {
|
|
|
907
932
|
description,
|
|
908
933
|
inputSchema: schema,
|
|
909
934
|
execution: { taskSupport: "optional" },
|
|
910
|
-
|
|
935
|
+
annotations
|
|
911
936
|
},
|
|
912
937
|
{
|
|
913
938
|
createTask: async (input, extra) => {
|
|
@@ -967,7 +992,7 @@ function registerToolTask(server2, name, description, schema, handler) {
|
|
|
967
992
|
}
|
|
968
993
|
|
|
969
994
|
// src/tools/parties.ts
|
|
970
|
-
import { z as
|
|
995
|
+
import { z as z7 } from "zod";
|
|
971
996
|
|
|
972
997
|
// src/tools/body-helpers.ts
|
|
973
998
|
function setRef(body, key, id) {
|
|
@@ -977,9 +1002,22 @@ function setNullableRef(body, key, id) {
|
|
|
977
1002
|
if (id === null) body[key] = null;
|
|
978
1003
|
else if (id !== void 0) body[key] = { id };
|
|
979
1004
|
}
|
|
1005
|
+
function assertSingleParentRef(toolName, refs, opts = {}) {
|
|
1006
|
+
const set = [refs.partyId, refs.opportunityId, refs.projectId].filter(
|
|
1007
|
+
(v) => typeof v === "number"
|
|
1008
|
+
).length;
|
|
1009
|
+
if (opts.required && set !== 1) {
|
|
1010
|
+
throw new Error(`${toolName}: provide exactly one of partyId, opportunityId, or projectId`);
|
|
1011
|
+
}
|
|
1012
|
+
if (set > 1) {
|
|
1013
|
+
throw new Error(
|
|
1014
|
+
`${toolName}: provide at most one of partyId, opportunityId, or projectId \u2014 Capsule allows a record to be related to at most one entity`
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
980
1018
|
|
|
981
1019
|
// src/tools/define-batch.ts
|
|
982
|
-
import { z } from "zod";
|
|
1020
|
+
import { z as z2 } from "zod";
|
|
983
1021
|
|
|
984
1022
|
// src/capsule/batch.ts
|
|
985
1023
|
function chunk(arr, size) {
|
|
@@ -998,37 +1036,43 @@ function getBatchConcurrency() {
|
|
|
998
1036
|
MAX_CONCURRENCY
|
|
999
1037
|
);
|
|
1000
1038
|
}
|
|
1001
|
-
async function
|
|
1002
|
-
const concurrency = getBatchConcurrency();
|
|
1039
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
1003
1040
|
const results = new Array(items.length);
|
|
1004
|
-
const startedAt = Date.now();
|
|
1005
|
-
const signal = options.signal;
|
|
1006
1041
|
let cursor = 0;
|
|
1007
1042
|
async function worker() {
|
|
1008
1043
|
while (true) {
|
|
1009
1044
|
const i = cursor;
|
|
1010
1045
|
cursor += 1;
|
|
1011
1046
|
if (i >= items.length) return;
|
|
1012
|
-
|
|
1013
|
-
results[i] = {
|
|
1014
|
-
ok: false,
|
|
1015
|
-
error: { message: "cancelled by tasks/cancel" }
|
|
1016
|
-
};
|
|
1017
|
-
continue;
|
|
1018
|
-
}
|
|
1019
|
-
try {
|
|
1020
|
-
const result = await action(items[i], i);
|
|
1021
|
-
results[i] = { ok: true, result };
|
|
1022
|
-
} catch (err) {
|
|
1023
|
-
results[i] = { ok: false, error: extractError(err) };
|
|
1024
|
-
}
|
|
1047
|
+
results[i] = await fn(items[i], i);
|
|
1025
1048
|
}
|
|
1026
1049
|
}
|
|
1027
1050
|
const workers = [];
|
|
1028
|
-
for (let w = 0; w < Math.min(
|
|
1051
|
+
for (let w = 0; w < Math.min(limit, items.length); w++) {
|
|
1029
1052
|
workers.push(worker());
|
|
1030
1053
|
}
|
|
1031
1054
|
await Promise.all(workers);
|
|
1055
|
+
return results;
|
|
1056
|
+
}
|
|
1057
|
+
async function batchExecute(tool, items, action, options = {}) {
|
|
1058
|
+
const concurrency = getBatchConcurrency();
|
|
1059
|
+
const startedAt = Date.now();
|
|
1060
|
+
const signal = options.signal;
|
|
1061
|
+
const results = await mapWithConcurrency(
|
|
1062
|
+
items,
|
|
1063
|
+
concurrency,
|
|
1064
|
+
async (item, i) => {
|
|
1065
|
+
if (signal?.aborted) {
|
|
1066
|
+
return { ok: false, error: { message: "cancelled by tasks/cancel" } };
|
|
1067
|
+
}
|
|
1068
|
+
try {
|
|
1069
|
+
const result = await action(item, i);
|
|
1070
|
+
return { ok: true, result };
|
|
1071
|
+
} catch (err) {
|
|
1072
|
+
return { ok: false, error: extractError(err) };
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
);
|
|
1032
1076
|
const succeeded = results.filter((r) => r.ok).length;
|
|
1033
1077
|
const failed = results.length - succeeded;
|
|
1034
1078
|
const summary = { total: results.length, succeeded, failed };
|
|
@@ -1073,10 +1117,52 @@ function topFailureReasons(results, n) {
|
|
|
1073
1117
|
return Array.from(counts.values()).sort((a, b) => b.count - a.count).slice(0, n);
|
|
1074
1118
|
}
|
|
1075
1119
|
|
|
1120
|
+
// src/tools/strip-descriptions.ts
|
|
1121
|
+
import { z } from "zod";
|
|
1122
|
+
function cloneWithDef(node, patch) {
|
|
1123
|
+
const def = node.def;
|
|
1124
|
+
return node.clone({ ...def, ...patch });
|
|
1125
|
+
}
|
|
1126
|
+
function stripDescriptions(schema) {
|
|
1127
|
+
let node = schema;
|
|
1128
|
+
if (node instanceof z.ZodObject) {
|
|
1129
|
+
const shape = node.def.shape;
|
|
1130
|
+
const next = {};
|
|
1131
|
+
let changed = false;
|
|
1132
|
+
for (const [key, child] of Object.entries(shape)) {
|
|
1133
|
+
next[key] = stripDescriptions(child);
|
|
1134
|
+
if (next[key] !== child) changed = true;
|
|
1135
|
+
}
|
|
1136
|
+
if (changed) node = cloneWithDef(node, { shape: next });
|
|
1137
|
+
} else if (node instanceof z.ZodArray) {
|
|
1138
|
+
const element = stripDescriptions(node.def.element);
|
|
1139
|
+
if (element !== node.def.element) node = cloneWithDef(node, { element });
|
|
1140
|
+
} else if (node instanceof z.ZodOptional || node instanceof z.ZodNullable || node instanceof z.ZodDefault || node instanceof z.ZodReadonly) {
|
|
1141
|
+
const innerType = stripDescriptions(node.def.innerType);
|
|
1142
|
+
if (innerType !== node.def.innerType) node = cloneWithDef(node, { innerType });
|
|
1143
|
+
} else if (node instanceof z.ZodUnion) {
|
|
1144
|
+
const options = node.def.options.map(stripDescriptions);
|
|
1145
|
+
if (options.some((o, i) => o !== node.def.options[i])) {
|
|
1146
|
+
node = cloneWithDef(node, { options });
|
|
1147
|
+
}
|
|
1148
|
+
} else if (node instanceof z.ZodPipe) {
|
|
1149
|
+
const inSchema = stripDescriptions(node.def.in);
|
|
1150
|
+
const outSchema = stripDescriptions(node.def.out);
|
|
1151
|
+
if (inSchema !== node.def.in || outSchema !== node.def.out) {
|
|
1152
|
+
node = cloneWithDef(node, { in: inSchema, out: outSchema });
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
if (node.description !== void 0) {
|
|
1156
|
+
node = node.meta({ description: void 0 });
|
|
1157
|
+
}
|
|
1158
|
+
return node;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1076
1161
|
// src/tools/define-batch.ts
|
|
1077
1162
|
function defineBatch(args) {
|
|
1078
|
-
const
|
|
1079
|
-
|
|
1163
|
+
const itemSchema = stripDescriptions(args.itemSchema);
|
|
1164
|
+
const schema = z2.object({
|
|
1165
|
+
items: z2.array(itemSchema).min(1).max(50).describe(args.itemDescription)
|
|
1080
1166
|
});
|
|
1081
1167
|
async function handler(input, opts = {}) {
|
|
1082
1168
|
return batchExecute(args.toolName, input.items, args.itemHandler, opts);
|
|
@@ -1084,27 +1170,51 @@ function defineBatch(args) {
|
|
|
1084
1170
|
return { schema, handler };
|
|
1085
1171
|
}
|
|
1086
1172
|
|
|
1087
|
-
// src/tools/descriptions.ts
|
|
1088
|
-
var EMBED_TAGS_FIELDS_DESCRIPTION = "Comma-separated embeds, e.g. 'tags,fields'";
|
|
1089
|
-
var EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION = "Comma-separated embeds, e.g. 'attachments,participants'";
|
|
1090
|
-
|
|
1091
1173
|
// src/tools/define-delete.ts
|
|
1092
|
-
import { z as
|
|
1174
|
+
import { z as z5 } from "zod";
|
|
1093
1175
|
|
|
1094
1176
|
// src/tools/confirm-flag.ts
|
|
1095
|
-
import { z as
|
|
1177
|
+
import { z as z3 } from "zod";
|
|
1096
1178
|
var CONFIRM_REQUIRED_MESSAGE = "confirm: true is required to perform this destructive operation (set the parameter explicitly to acknowledge the destructive intent)";
|
|
1097
1179
|
function confirmFlag() {
|
|
1098
|
-
return
|
|
1180
|
+
return z3.literal(true, { error: () => CONFIRM_REQUIRED_MESSAGE });
|
|
1099
1181
|
}
|
|
1100
1182
|
|
|
1101
1183
|
// src/tools/shared-schemas.ts
|
|
1102
|
-
import { z as
|
|
1103
|
-
var positiveId =
|
|
1184
|
+
import { z as z4 } from "zod";
|
|
1185
|
+
var positiveId = z4.preprocess((input) => {
|
|
1104
1186
|
if (typeof input !== "string") return input;
|
|
1105
1187
|
const trimmed = input.trim();
|
|
1106
1188
|
return /^\d+$/.test(trimmed) ? Number(trimmed) : input;
|
|
1107
|
-
},
|
|
1189
|
+
}, z4.number().int().positive());
|
|
1190
|
+
var paginationFields = {
|
|
1191
|
+
page: z4.number().int().positive().optional().default(1),
|
|
1192
|
+
perPage: z4.number().int().min(1).max(100).optional().default(25)
|
|
1193
|
+
};
|
|
1194
|
+
var paginationFieldsNoDefaults = {
|
|
1195
|
+
page: z4.number().int().positive().optional(),
|
|
1196
|
+
perPage: z4.number().int().min(1).max(100).optional()
|
|
1197
|
+
};
|
|
1198
|
+
var ENTITY_PATH = {
|
|
1199
|
+
parties: "parties",
|
|
1200
|
+
opportunities: "opportunities",
|
|
1201
|
+
projects: "kases"
|
|
1202
|
+
};
|
|
1203
|
+
function embedParam(allowed) {
|
|
1204
|
+
return z4.string().superRefine((value, ctx) => {
|
|
1205
|
+
const tokens = value.split(",").map((t) => t.trim());
|
|
1206
|
+
for (const token of tokens) {
|
|
1207
|
+
if (token === "" || !allowed.includes(token)) {
|
|
1208
|
+
ctx.addIssue({
|
|
1209
|
+
code: "custom",
|
|
1210
|
+
message: `Unknown embed token '${token}'. Valid tokens: ${allowed.join(", ")} (comma-separated). Capsule silently ignores unknown tokens, so this is rejected client-side to prevent silently-missing data.`
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
}).describe(`Comma-separated embeds. Valid tokens: ${allowed.join(", ")}.`).optional();
|
|
1215
|
+
}
|
|
1216
|
+
var RECORD_EMBEDS = ["tags", "fields", "missingImportantFields"];
|
|
1217
|
+
var ENTRY_EMBEDS = ["attachments", "participants"];
|
|
1108
1218
|
|
|
1109
1219
|
// src/capsule/idempotent.ts
|
|
1110
1220
|
var isCapsule404 = (err) => err instanceof CapsuleApiError && err.status === 404;
|
|
@@ -1131,7 +1241,7 @@ async function idempotentWithResult(op, success, alreadyDone, isAlreadyDoneError
|
|
|
1131
1241
|
// src/tools/define-delete.ts
|
|
1132
1242
|
function defineDelete(args) {
|
|
1133
1243
|
const { toolName, pathPrefix, confirmHint, idDescription } = args;
|
|
1134
|
-
const schema =
|
|
1244
|
+
const schema = z5.object({
|
|
1135
1245
|
id: idDescription ? positiveId.describe(idDescription) : positiveId,
|
|
1136
1246
|
confirm: confirmFlag().describe(confirmHint)
|
|
1137
1247
|
});
|
|
@@ -1179,12 +1289,12 @@ async function chunkedMultiGet(base, responseKey, ids, params) {
|
|
|
1179
1289
|
}
|
|
1180
1290
|
|
|
1181
1291
|
// src/tools/custom-field-helpers.ts
|
|
1182
|
-
import { z as
|
|
1183
|
-
var CustomFieldWriteSchema =
|
|
1292
|
+
import { z as z6 } from "zod";
|
|
1293
|
+
var CustomFieldWriteSchema = z6.object({
|
|
1184
1294
|
definitionId: positiveId.describe(
|
|
1185
1295
|
"The custom-field definition id from list_custom_fields. Identifies which field on the entity to set."
|
|
1186
1296
|
),
|
|
1187
|
-
value:
|
|
1297
|
+
value: z6.union([z6.string(), z6.number(), z6.boolean(), z6.null()]).describe(
|
|
1188
1298
|
"The new value. String for TEXT / DATE / LIST / LARGE_TEXT / LINK fields, number for NUMBER fields, boolean for BOOLEAN fields. Clearing: pass null for TEXT / NUMBER / DATE / LIST (Capsule removes the row). BOOLEAN does NOT accept null (Capsule returns 422 'invalid type for field'); use `value: false` instead. Note BOOLEAN fields are observably **two-state**: a row exists with `value: true`, or no row exists. Setting `value: false` removes the row entirely \u2014 readers should treat absent BOOLEAN rows as equivalent to false. Tri-state BOOLEAN semantics (true / false / unknown) are not achievable through Capsule's API. Audit-log noise: sending value=null on a field that's already empty/cleared is accepted by Capsule but still bumps the parent entity's `updatedAt`. Read the current value via embed='fields' first if `updatedAt` is being used as a 'last meaningful change' signal. NUMBER quirks: Capsule stores numerics correctly but the read-back via embed=fields returns them as STRINGS (e.g. value=3 reads as '3'); callers comparing values must coerce. TEXT quirks: value='' has the same observable effect as value=null (row removed); empty-string and never-set are indistinguishable."
|
|
1189
1299
|
)
|
|
1190
1300
|
});
|
|
@@ -1200,24 +1310,24 @@ function mapFieldsForBody(fields) {
|
|
|
1200
1310
|
}
|
|
1201
1311
|
|
|
1202
1312
|
// src/tools/parties.ts
|
|
1203
|
-
var EmailAddressSchema =
|
|
1204
|
-
address:
|
|
1205
|
-
type:
|
|
1313
|
+
var EmailAddressSchema = z7.object({
|
|
1314
|
+
address: z7.string().email(),
|
|
1315
|
+
type: z7.string().optional()
|
|
1206
1316
|
});
|
|
1207
|
-
var PhoneNumberSchema =
|
|
1317
|
+
var PhoneNumberSchema = z7.object({
|
|
1208
1318
|
// Capsule rejects empty strings with `phoneNumber.number: number is
|
|
1209
1319
|
// required`. Enforce at the schema layer to catch typos pre-call,
|
|
1210
1320
|
// matching how EmailAddressSchema's address field behaves.
|
|
1211
|
-
number:
|
|
1212
|
-
type:
|
|
1321
|
+
number: z7.string().min(1),
|
|
1322
|
+
type: z7.string().optional()
|
|
1213
1323
|
});
|
|
1214
1324
|
var CountryDescription = "Country name. Capsule validates this against a small canonical-English-name dictionary; inputs not in the dictionary are REJECTED with 422 'address.country: unknown country' (NOT silently passed through or normalised). Probed examples \u2014 accepted: `United States`, `United Kingdom`, `Czechia`, `Germany`. Aliased: `USA \u2192 United States`. Rejected: `United States of America`, `Czech Republic` (use `Czechia`), `UK`/`Britain` (use `United Kingdom`), `Deutschland` (use `Germany`). Empty string is accepted and stored as `null` \u2014 a de-facto 'clear' shape. To discover an accepted name, read an existing party that already has the country set.";
|
|
1215
|
-
var AddressSchema =
|
|
1216
|
-
street:
|
|
1217
|
-
city:
|
|
1218
|
-
state:
|
|
1219
|
-
country:
|
|
1220
|
-
zip:
|
|
1325
|
+
var AddressSchema = z7.object({
|
|
1326
|
+
street: z7.string().optional(),
|
|
1327
|
+
city: z7.string().optional(),
|
|
1328
|
+
state: z7.string().optional(),
|
|
1329
|
+
country: z7.string().optional().describe(CountryDescription),
|
|
1330
|
+
zip: z7.string().optional()
|
|
1221
1331
|
});
|
|
1222
1332
|
function validateWebsiteAddress(data, ctx) {
|
|
1223
1333
|
const isUrlService = data.service === void 0 || data.service === "URL";
|
|
@@ -1240,7 +1350,7 @@ function validateWebsiteAddress(data, ctx) {
|
|
|
1240
1350
|
});
|
|
1241
1351
|
}
|
|
1242
1352
|
}
|
|
1243
|
-
var WebsiteServiceEnum =
|
|
1353
|
+
var WebsiteServiceEnum = z7.enum([
|
|
1244
1354
|
"URL",
|
|
1245
1355
|
"SKYPE",
|
|
1246
1356
|
"TWITTER",
|
|
@@ -1259,33 +1369,31 @@ var WebsiteServiceEnum = z6.enum([
|
|
|
1259
1369
|
"BLUESKY",
|
|
1260
1370
|
"SNAPCHAT"
|
|
1261
1371
|
]);
|
|
1262
|
-
var WebsiteSchema =
|
|
1263
|
-
address:
|
|
1372
|
+
var WebsiteSchema = z7.object({
|
|
1373
|
+
address: z7.string().min(1).describe(
|
|
1264
1374
|
"The website address. A URL when service='URL', or a handle (e.g. '@acmeco') for social services like 'TWITTER', 'INSTAGRAM'. Capsule names this field `address` regardless of service type."
|
|
1265
1375
|
),
|
|
1266
1376
|
service: WebsiteServiceEnum.optional().describe(
|
|
1267
1377
|
"Service type. One of: URL, SKYPE, TWITTER, LINKED_IN, FACEBOOK, XING, FEED, GOOGLE_PLUS, FLICKR, GITHUB, YOUTUBE, INSTAGRAM, PINTEREST, TIKTOK, THREADS, BLUESKY, SNAPCHAT. Defaults to 'URL' if omitted."
|
|
1268
1378
|
)
|
|
1269
1379
|
}).superRefine(validateWebsiteAddress);
|
|
1270
|
-
var searchPartiesSchema =
|
|
1271
|
-
q:
|
|
1272
|
-
embed:
|
|
1273
|
-
|
|
1274
|
-
perPage: z6.number().int().min(1).max(100).optional().default(25)
|
|
1380
|
+
var searchPartiesSchema = z7.object({
|
|
1381
|
+
q: z7.string().optional().describe("Free-text search query"),
|
|
1382
|
+
embed: embedParam(RECORD_EMBEDS),
|
|
1383
|
+
...paginationFields
|
|
1275
1384
|
});
|
|
1276
1385
|
async function searchParties(input) {
|
|
1277
1386
|
const path = input.q ? "/parties/search" : "/parties";
|
|
1278
|
-
|
|
1387
|
+
return capsuleGetList(path, {
|
|
1279
1388
|
q: input.q,
|
|
1280
1389
|
embed: input.embed,
|
|
1281
1390
|
page: input.page,
|
|
1282
1391
|
perPage: input.perPage
|
|
1283
1392
|
});
|
|
1284
|
-
return { ...data, nextPage };
|
|
1285
1393
|
}
|
|
1286
|
-
var getPartySchema =
|
|
1394
|
+
var getPartySchema = z7.object({
|
|
1287
1395
|
id: positiveId.describe("Party ID"),
|
|
1288
|
-
embed:
|
|
1396
|
+
embed: embedParam(RECORD_EMBEDS)
|
|
1289
1397
|
});
|
|
1290
1398
|
async function getParty(input) {
|
|
1291
1399
|
const { data } = await capsuleGet(`/parties/${input.id}`, {
|
|
@@ -1293,51 +1401,47 @@ async function getParty(input) {
|
|
|
1293
1401
|
});
|
|
1294
1402
|
return data;
|
|
1295
1403
|
}
|
|
1296
|
-
var getPartiesSchema =
|
|
1297
|
-
ids:
|
|
1404
|
+
var getPartiesSchema = z7.object({
|
|
1405
|
+
ids: z7.array(positiveId).min(1).max(50).describe(
|
|
1298
1406
|
"Array of party IDs (1\u201350). Capsule's native batch-fetch endpoint caps at 10 per request; the connector transparently splits larger sets into 10-id chunks and fans out the Capsule calls in parallel. Result shape is identical regardless of input size."
|
|
1299
1407
|
),
|
|
1300
|
-
embed:
|
|
1408
|
+
embed: embedParam(RECORD_EMBEDS)
|
|
1301
1409
|
});
|
|
1302
1410
|
async function getParties(input) {
|
|
1303
1411
|
return chunkedMultiGet("/parties", "parties", input.ids, { embed: input.embed });
|
|
1304
1412
|
}
|
|
1305
|
-
var listPartyOpportunitiesSchema =
|
|
1413
|
+
var listPartyOpportunitiesSchema = z7.object({
|
|
1306
1414
|
partyId: positiveId,
|
|
1307
|
-
|
|
1308
|
-
perPage: z6.number().int().min(1).max(100).optional().default(25)
|
|
1415
|
+
...paginationFields
|
|
1309
1416
|
});
|
|
1310
1417
|
async function listPartyOpportunities(input) {
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
);
|
|
1315
|
-
return { ...data, nextPage };
|
|
1418
|
+
return capsuleGetList(`/parties/${input.partyId}/opportunities`, {
|
|
1419
|
+
page: input.page,
|
|
1420
|
+
perPage: input.perPage
|
|
1421
|
+
});
|
|
1316
1422
|
}
|
|
1317
|
-
var listPartyProjectsSchema =
|
|
1423
|
+
var listPartyProjectsSchema = z7.object({
|
|
1318
1424
|
partyId: positiveId,
|
|
1319
|
-
|
|
1320
|
-
perPage: z6.number().int().min(1).max(100).optional().default(25)
|
|
1425
|
+
...paginationFields
|
|
1321
1426
|
});
|
|
1322
1427
|
async function listPartyProjects(input) {
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
);
|
|
1327
|
-
return { ...data, nextPage };
|
|
1428
|
+
return capsuleGetList(`/parties/${input.partyId}/kases`, {
|
|
1429
|
+
page: input.page,
|
|
1430
|
+
perPage: input.perPage
|
|
1431
|
+
});
|
|
1328
1432
|
}
|
|
1329
1433
|
var PartyWriteBaseSchema = {
|
|
1330
|
-
about:
|
|
1331
|
-
emailAddresses:
|
|
1434
|
+
about: z7.string().optional(),
|
|
1435
|
+
emailAddresses: z7.array(EmailAddressSchema).optional().describe(
|
|
1332
1436
|
"APPEND-ONLY: items are merged into the existing list, never replaced. For atomic add/remove/replace use add_party_email_address and remove_party_email_address_by_id. Passing `[]` here is a silent no-op (does not clear the list and does not advance updatedAt)."
|
|
1333
1437
|
),
|
|
1334
|
-
phoneNumbers:
|
|
1438
|
+
phoneNumbers: z7.array(PhoneNumberSchema).optional().describe(
|
|
1335
1439
|
"APPEND-ONLY: items are merged into the existing list, never replaced. For atomic add/remove/replace use add_party_phone_number and remove_party_phone_number_by_id."
|
|
1336
1440
|
),
|
|
1337
|
-
addresses:
|
|
1441
|
+
addresses: z7.array(AddressSchema).optional().describe(
|
|
1338
1442
|
"APPEND-ONLY: items are merged into the existing list, never replaced. For atomic add/remove/replace use add_party_address and remove_party_address_by_id. The `country` field is mapped through Capsule's country dictionary \u2014 see `add_party_address.country` for the dictionary edges (small canonical-English-name list; inputs not in the dictionary are REJECTED with 422, not silently dropped)."
|
|
1339
1443
|
),
|
|
1340
|
-
websites:
|
|
1444
|
+
websites: z7.array(WebsiteSchema).optional().describe(
|
|
1341
1445
|
"APPEND-ONLY: items are merged into the existing list, never replaced. For atomic add/remove/replace use add_party_website and remove_party_website_by_id."
|
|
1342
1446
|
),
|
|
1343
1447
|
ownerId: positiveId.nullable().optional().describe(
|
|
@@ -1347,16 +1451,16 @@ var PartyWriteBaseSchema = {
|
|
|
1347
1451
|
"Assign to team ID (discover via list_teams). Pass a team ID to set, or `null` to unassign. Capsule enforces the owner\u2208team membership constraint \u2014 passing a team the current owner doesn't belong to returns 422 'owner is not a member of the team'. Combine `ownerId: null` + `teamId: <T>` in one call to transfer a party to team-ownership with no specific user (verified empirically in v1.6.4 wire-trace; the membership rule doesn't fire when owner is null)."
|
|
1348
1452
|
)
|
|
1349
1453
|
};
|
|
1350
|
-
var createPartySchema =
|
|
1351
|
-
type:
|
|
1454
|
+
var createPartySchema = z7.object({
|
|
1455
|
+
type: z7.enum(["person", "organisation"]),
|
|
1352
1456
|
// person
|
|
1353
|
-
firstName:
|
|
1354
|
-
lastName:
|
|
1355
|
-
title:
|
|
1356
|
-
jobTitle:
|
|
1457
|
+
firstName: z7.string().optional(),
|
|
1458
|
+
lastName: z7.string().optional(),
|
|
1459
|
+
title: z7.string().optional(),
|
|
1460
|
+
jobTitle: z7.string().optional(),
|
|
1357
1461
|
organisationId: positiveId.optional().describe("Link person to an existing organisation ID"),
|
|
1358
1462
|
// organisation
|
|
1359
|
-
name:
|
|
1463
|
+
name: z7.string().optional(),
|
|
1360
1464
|
...PartyWriteBaseSchema,
|
|
1361
1465
|
ownerId: positiveId.optional().describe(
|
|
1362
1466
|
"Assign to user ID. Defaults to the API-token owner when omitted. To create a team-owned party with no specific user, first create the party, then call update_party with `ownerId: null` and `teamId`."
|
|
@@ -1364,9 +1468,24 @@ var createPartySchema = z6.object({
|
|
|
1364
1468
|
teamId: positiveId.optional().describe(
|
|
1365
1469
|
"Assign to team ID (discover via list_teams). Omit to leave team unset on create. To clear an existing team or create a team-owned party with no specific owner, use update_party after creation."
|
|
1366
1470
|
),
|
|
1367
|
-
fields:
|
|
1471
|
+
fields: z7.array(CustomFieldWriteSchema).optional().describe(
|
|
1368
1472
|
fieldsArrayDescriptor("get_party") + " Verified empirically in v1.6.5 wire-trace: Capsule's POST /parties accepts the same `fields[]` shape as PUT, so callers can set custom field values on creation without a follow-up update."
|
|
1369
1473
|
)
|
|
1474
|
+
}).superRefine((data, ctx) => {
|
|
1475
|
+
if (data.type === "person" && !data.firstName && !data.lastName) {
|
|
1476
|
+
ctx.addIssue({
|
|
1477
|
+
code: "custom",
|
|
1478
|
+
path: ["firstName"],
|
|
1479
|
+
message: "create_party: a person requires firstName and/or lastName"
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
if (data.type === "organisation" && !data.name) {
|
|
1483
|
+
ctx.addIssue({
|
|
1484
|
+
code: "custom",
|
|
1485
|
+
path: ["name"],
|
|
1486
|
+
message: "create_party: an organisation requires name"
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1370
1489
|
});
|
|
1371
1490
|
async function createParty(input) {
|
|
1372
1491
|
const { ownerId, teamId, organisationId, fields, ...rest } = input;
|
|
@@ -1378,17 +1497,17 @@ async function createParty(input) {
|
|
|
1378
1497
|
if (mappedFields !== void 0) body["fields"] = mappedFields;
|
|
1379
1498
|
return capsulePost("/parties", { party: body });
|
|
1380
1499
|
}
|
|
1381
|
-
var updatePartySchema =
|
|
1500
|
+
var updatePartySchema = z7.object({
|
|
1382
1501
|
id: positiveId,
|
|
1383
|
-
firstName:
|
|
1384
|
-
lastName:
|
|
1385
|
-
title:
|
|
1386
|
-
jobTitle:
|
|
1387
|
-
name:
|
|
1502
|
+
firstName: z7.string().optional(),
|
|
1503
|
+
lastName: z7.string().optional(),
|
|
1504
|
+
title: z7.string().optional(),
|
|
1505
|
+
jobTitle: z7.string().optional(),
|
|
1506
|
+
name: z7.string().optional(),
|
|
1388
1507
|
organisationId: positiveId.nullable().optional().describe(
|
|
1389
1508
|
"For PERSON parties: link to an organisation by id, or `null` to unlink (the person becomes an orphan / standalone record). Discover org IDs via search_parties / filter_parties with type=organisation. For ORGANISATION parties: silently ignored by Capsule's API \u2014 organisations don't have a parent organisation in the data model. Empirically verified in v1.6.3 wire-trace; no client-side type guard since the no-op is harmless."
|
|
1390
1509
|
),
|
|
1391
|
-
fields:
|
|
1510
|
+
fields: z7.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_party")),
|
|
1392
1511
|
...PartyWriteBaseSchema
|
|
1393
1512
|
});
|
|
1394
1513
|
async function updateParty(input) {
|
|
@@ -1417,12 +1536,44 @@ var { schema: batchUpdatePartySchema, handler: batchUpdateParty } = defineBatch(
|
|
|
1417
1536
|
var { schema: deletePartySchema, handler: deleteParty } = defineDelete({
|
|
1418
1537
|
toolName: "delete_party",
|
|
1419
1538
|
pathPrefix: "/parties",
|
|
1420
|
-
confirmHint: "Must be set to true. Deletes the party AND all linked notes, tasks, opportunities, and projects
|
|
1539
|
+
confirmHint: "Must be set to true. Deletes the party AND all linked notes, tasks, opportunities, and projects. Deleting an ORGANISATION does NOT delete people linked to it via organisationId \u2014 their `organisation` field is silently cleared to null and they survive as standalone records. Irreversible."
|
|
1421
1540
|
});
|
|
1422
|
-
|
|
1541
|
+
function definePartySubResourceRemove(opts) {
|
|
1542
|
+
const shape = {
|
|
1543
|
+
partyId: positiveId,
|
|
1544
|
+
[opts.idField]: positiveId.describe(
|
|
1545
|
+
`Capsule's id for the ${opts.rowNoun} row. Read it from get_party (each entry in ${opts.arrayKey} carries an id).`
|
|
1546
|
+
)
|
|
1547
|
+
};
|
|
1548
|
+
const schema = z7.object(shape);
|
|
1549
|
+
async function handler(input) {
|
|
1550
|
+
const partyId = input["partyId"];
|
|
1551
|
+
const rowId = input[opts.idField];
|
|
1552
|
+
return idempotentWithResult(
|
|
1553
|
+
() => capsulePut(`/parties/${partyId}`, {
|
|
1554
|
+
party: { [opts.arrayKey]: [{ id: rowId, _delete: true }] }
|
|
1555
|
+
}),
|
|
1556
|
+
(result) => ({
|
|
1557
|
+
removed: true,
|
|
1558
|
+
alreadyRemoved: false,
|
|
1559
|
+
partyId,
|
|
1560
|
+
[opts.idField]: rowId,
|
|
1561
|
+
...result
|
|
1562
|
+
}),
|
|
1563
|
+
() => ({
|
|
1564
|
+
removed: true,
|
|
1565
|
+
alreadyRemoved: true,
|
|
1566
|
+
partyId,
|
|
1567
|
+
[opts.idField]: rowId
|
|
1568
|
+
})
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
return { schema, handler };
|
|
1572
|
+
}
|
|
1573
|
+
var addPartyEmailAddressSchema = z7.object({
|
|
1423
1574
|
partyId: positiveId,
|
|
1424
|
-
address:
|
|
1425
|
-
type:
|
|
1575
|
+
address: z7.string().email(),
|
|
1576
|
+
type: z7.string().optional().describe("Free-form label, e.g. 'Work', 'Home'.")
|
|
1426
1577
|
});
|
|
1427
1578
|
async function addPartyEmailAddress(input) {
|
|
1428
1579
|
const { partyId, address, type } = input;
|
|
@@ -1432,32 +1583,17 @@ async function addPartyEmailAddress(input) {
|
|
|
1432
1583
|
party: { emailAddresses: [item] }
|
|
1433
1584
|
});
|
|
1434
1585
|
}
|
|
1435
|
-
var
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
)
|
|
1586
|
+
var removePartyEmailAddress = definePartySubResourceRemove({
|
|
1587
|
+
arrayKey: "emailAddresses",
|
|
1588
|
+
idField: "emailAddressId",
|
|
1589
|
+
rowNoun: "email-address"
|
|
1440
1590
|
});
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
() => capsulePut(`/parties/${partyId}`, {
|
|
1445
|
-
party: { emailAddresses: [{ id: emailAddressId, _delete: true }] }
|
|
1446
|
-
}),
|
|
1447
|
-
(result) => ({
|
|
1448
|
-
removed: true,
|
|
1449
|
-
alreadyRemoved: false,
|
|
1450
|
-
partyId,
|
|
1451
|
-
emailAddressId,
|
|
1452
|
-
...result
|
|
1453
|
-
}),
|
|
1454
|
-
() => ({ removed: true, alreadyRemoved: true, partyId, emailAddressId })
|
|
1455
|
-
);
|
|
1456
|
-
}
|
|
1457
|
-
var addPartyPhoneNumberSchema = z6.object({
|
|
1591
|
+
var removePartyEmailAddressByIdSchema = removePartyEmailAddress.schema;
|
|
1592
|
+
var removePartyEmailAddressById = removePartyEmailAddress.handler;
|
|
1593
|
+
var addPartyPhoneNumberSchema = z7.object({
|
|
1458
1594
|
partyId: positiveId,
|
|
1459
|
-
number:
|
|
1460
|
-
type:
|
|
1595
|
+
number: z7.string().min(1),
|
|
1596
|
+
type: z7.string().optional().describe("Free-form label, e.g. 'Work', 'Mobile'.")
|
|
1461
1597
|
});
|
|
1462
1598
|
async function addPartyPhoneNumber(input) {
|
|
1463
1599
|
const { partyId, number, type } = input;
|
|
@@ -1467,36 +1603,21 @@ async function addPartyPhoneNumber(input) {
|
|
|
1467
1603
|
party: { phoneNumbers: [item] }
|
|
1468
1604
|
});
|
|
1469
1605
|
}
|
|
1470
|
-
var
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
)
|
|
1606
|
+
var removePartyPhoneNumber = definePartySubResourceRemove({
|
|
1607
|
+
arrayKey: "phoneNumbers",
|
|
1608
|
+
idField: "phoneNumberId",
|
|
1609
|
+
rowNoun: "phone-number"
|
|
1475
1610
|
});
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
() => capsulePut(`/parties/${partyId}`, {
|
|
1480
|
-
party: { phoneNumbers: [{ id: phoneNumberId, _delete: true }] }
|
|
1481
|
-
}),
|
|
1482
|
-
(result) => ({
|
|
1483
|
-
removed: true,
|
|
1484
|
-
alreadyRemoved: false,
|
|
1485
|
-
partyId,
|
|
1486
|
-
phoneNumberId,
|
|
1487
|
-
...result
|
|
1488
|
-
}),
|
|
1489
|
-
() => ({ removed: true, alreadyRemoved: true, partyId, phoneNumberId })
|
|
1490
|
-
);
|
|
1491
|
-
}
|
|
1492
|
-
var addPartyAddressSchema = z6.object({
|
|
1611
|
+
var removePartyPhoneNumberByIdSchema = removePartyPhoneNumber.schema;
|
|
1612
|
+
var removePartyPhoneNumberById = removePartyPhoneNumber.handler;
|
|
1613
|
+
var addPartyAddressSchema = z7.object({
|
|
1493
1614
|
partyId: positiveId,
|
|
1494
|
-
street:
|
|
1495
|
-
city:
|
|
1496
|
-
state:
|
|
1497
|
-
country:
|
|
1498
|
-
zip:
|
|
1499
|
-
type:
|
|
1615
|
+
street: z7.string().optional(),
|
|
1616
|
+
city: z7.string().optional(),
|
|
1617
|
+
state: z7.string().optional(),
|
|
1618
|
+
country: z7.string().optional().describe(CountryDescription),
|
|
1619
|
+
zip: z7.string().optional(),
|
|
1620
|
+
type: z7.string().optional().describe("Free-form label, e.g. 'Office', 'Home'.")
|
|
1500
1621
|
});
|
|
1501
1622
|
async function addPartyAddress(input) {
|
|
1502
1623
|
const { partyId, ...rest } = input;
|
|
@@ -1508,31 +1629,16 @@ async function addPartyAddress(input) {
|
|
|
1508
1629
|
party: { addresses: [item] }
|
|
1509
1630
|
});
|
|
1510
1631
|
}
|
|
1511
|
-
var
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
)
|
|
1632
|
+
var removePartyAddress = definePartySubResourceRemove({
|
|
1633
|
+
arrayKey: "addresses",
|
|
1634
|
+
idField: "addressId",
|
|
1635
|
+
rowNoun: "address"
|
|
1516
1636
|
});
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
() => capsulePut(`/parties/${partyId}`, {
|
|
1521
|
-
party: { addresses: [{ id: addressId, _delete: true }] }
|
|
1522
|
-
}),
|
|
1523
|
-
(result) => ({
|
|
1524
|
-
removed: true,
|
|
1525
|
-
alreadyRemoved: false,
|
|
1526
|
-
partyId,
|
|
1527
|
-
addressId,
|
|
1528
|
-
...result
|
|
1529
|
-
}),
|
|
1530
|
-
() => ({ removed: true, alreadyRemoved: true, partyId, addressId })
|
|
1531
|
-
);
|
|
1532
|
-
}
|
|
1533
|
-
var addPartyWebsiteSchema = z6.object({
|
|
1637
|
+
var removePartyAddressByIdSchema = removePartyAddress.schema;
|
|
1638
|
+
var removePartyAddressById = removePartyAddress.handler;
|
|
1639
|
+
var addPartyWebsiteSchema = z7.object({
|
|
1534
1640
|
partyId: positiveId,
|
|
1535
|
-
address:
|
|
1641
|
+
address: z7.string().min(1).describe(
|
|
1536
1642
|
"The website address. A URL when service='URL', or a handle (e.g. '@acmeco') for social services."
|
|
1537
1643
|
),
|
|
1538
1644
|
service: WebsiteServiceEnum.optional().describe("Defaults to 'URL' if omitted.")
|
|
@@ -1545,58 +1651,41 @@ async function addPartyWebsite(input) {
|
|
|
1545
1651
|
party: { websites: [item] }
|
|
1546
1652
|
});
|
|
1547
1653
|
}
|
|
1548
|
-
var
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
)
|
|
1654
|
+
var removePartyWebsite = definePartySubResourceRemove({
|
|
1655
|
+
arrayKey: "websites",
|
|
1656
|
+
idField: "websiteId",
|
|
1657
|
+
rowNoun: "website"
|
|
1553
1658
|
});
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
return idempotentWithResult(
|
|
1557
|
-
() => capsulePut(`/parties/${partyId}`, {
|
|
1558
|
-
party: { websites: [{ id: websiteId, _delete: true }] }
|
|
1559
|
-
}),
|
|
1560
|
-
(result) => ({
|
|
1561
|
-
removed: true,
|
|
1562
|
-
alreadyRemoved: false,
|
|
1563
|
-
partyId,
|
|
1564
|
-
websiteId,
|
|
1565
|
-
...result
|
|
1566
|
-
}),
|
|
1567
|
-
() => ({ removed: true, alreadyRemoved: true, partyId, websiteId })
|
|
1568
|
-
);
|
|
1569
|
-
}
|
|
1659
|
+
var removePartyWebsiteByIdSchema = removePartyWebsite.schema;
|
|
1660
|
+
var removePartyWebsiteById = removePartyWebsite.handler;
|
|
1570
1661
|
|
|
1571
1662
|
// src/tools/opportunities.ts
|
|
1572
|
-
import { z as
|
|
1573
|
-
var OpportunityValueSchema =
|
|
1574
|
-
amount:
|
|
1575
|
-
currency:
|
|
1663
|
+
import { z as z8 } from "zod";
|
|
1664
|
+
var OpportunityValueSchema = z8.object({
|
|
1665
|
+
amount: z8.number().nonnegative(),
|
|
1666
|
+
currency: z8.string({
|
|
1576
1667
|
error: (iss) => iss.code === "invalid_type" && iss.input === void 0 ? "currency is required when amount is set (3-letter ISO 4217 code, e.g. 'USD', 'EUR', 'GBP')" : void 0
|
|
1577
1668
|
}).length(3).describe(
|
|
1578
1669
|
"ISO 4217 currency code (3 letters), e.g. 'GBP', 'USD', 'EUR'. Required when amount is set."
|
|
1579
1670
|
)
|
|
1580
1671
|
});
|
|
1581
|
-
var searchOpportunitiesSchema =
|
|
1582
|
-
q:
|
|
1583
|
-
embed:
|
|
1584
|
-
|
|
1585
|
-
perPage: z7.number().int().min(1).max(100).optional().default(25)
|
|
1672
|
+
var searchOpportunitiesSchema = z8.object({
|
|
1673
|
+
q: z8.string().optional().describe("Free-text search query"),
|
|
1674
|
+
embed: embedParam(RECORD_EMBEDS),
|
|
1675
|
+
...paginationFields
|
|
1586
1676
|
});
|
|
1587
1677
|
async function searchOpportunities(input) {
|
|
1588
1678
|
const path = input.q ? "/opportunities/search" : "/opportunities";
|
|
1589
|
-
|
|
1679
|
+
return capsuleGetList(path, {
|
|
1590
1680
|
q: input.q,
|
|
1591
1681
|
embed: input.embed,
|
|
1592
1682
|
page: input.page,
|
|
1593
1683
|
perPage: input.perPage
|
|
1594
1684
|
});
|
|
1595
|
-
return { ...data, nextPage };
|
|
1596
1685
|
}
|
|
1597
|
-
var getOpportunitySchema =
|
|
1686
|
+
var getOpportunitySchema = z8.object({
|
|
1598
1687
|
id: positiveId,
|
|
1599
|
-
embed:
|
|
1688
|
+
embed: embedParam(RECORD_EMBEDS)
|
|
1600
1689
|
});
|
|
1601
1690
|
async function getOpportunity(input) {
|
|
1602
1691
|
const { data } = await capsuleGet(`/opportunities/${input.id}`, {
|
|
@@ -1604,32 +1693,32 @@ async function getOpportunity(input) {
|
|
|
1604
1693
|
});
|
|
1605
1694
|
return data;
|
|
1606
1695
|
}
|
|
1607
|
-
var getOpportunitiesSchema =
|
|
1608
|
-
ids:
|
|
1696
|
+
var getOpportunitiesSchema = z8.object({
|
|
1697
|
+
ids: z8.array(positiveId).min(1).max(50).describe(
|
|
1609
1698
|
"Array of opportunity IDs (1\u201350). Capsule's native batch-fetch endpoint caps at 10 per request; the connector transparently splits larger sets into 10-id chunks and fans out the Capsule calls in parallel."
|
|
1610
1699
|
),
|
|
1611
|
-
embed:
|
|
1700
|
+
embed: embedParam(RECORD_EMBEDS)
|
|
1612
1701
|
});
|
|
1613
1702
|
async function getOpportunities(input) {
|
|
1614
1703
|
return chunkedMultiGet("/opportunities", "opportunities", input.ids, { embed: input.embed });
|
|
1615
1704
|
}
|
|
1616
|
-
var createOpportunitySchema =
|
|
1617
|
-
name:
|
|
1705
|
+
var createOpportunitySchema = z8.object({
|
|
1706
|
+
name: z8.string().min(1),
|
|
1618
1707
|
partyId: positiveId.describe("ID of the party this opportunity belongs to"),
|
|
1619
1708
|
milestoneId: positiveId.describe(
|
|
1620
1709
|
"ID of the pipeline milestone to place this opportunity at. The milestone implicitly determines the pipeline \u2014 there is no separate pipelineId parameter. Discover via list_pipelines / list_milestones. NOTE: some Capsule tenants configure **pipeline / milestone-reached automation rules** that mutate `owner` and/or `team` immediately after creation \u2014 e.g. an 'Assign to a Team' action that fires on entry to a specific milestone and has been observed to clear `owner` as an automation side-effect. If you observe a newly-created opp landing with `owner: null` despite passing `ownerId`, the cause is almost certainly a milestone automation on the destination pipeline rather than the connector. Documented workaround: follow `create_opportunity` with an immediate `batch_update_opportunity({items: [{id, ownerId, teamId}]})` carrying both fields \u2014 PUT does not re-fire milestone-reached triggers, so the owner sticks."
|
|
1621
1710
|
),
|
|
1622
|
-
description:
|
|
1711
|
+
description: z8.string().optional(),
|
|
1623
1712
|
value: OpportunityValueSchema.optional(),
|
|
1624
|
-
expectedCloseOn:
|
|
1625
|
-
probability:
|
|
1713
|
+
expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
|
|
1714
|
+
probability: z8.number().int().min(0).max(100).optional(),
|
|
1626
1715
|
ownerId: positiveId.optional().describe(
|
|
1627
1716
|
"Assign to user ID. Defaults to the API-token owner when omitted \u2014 note that opportunities do NOT inherit owner from the linked party, even though one might expect it. To clear owner later, call update_opportunity with `ownerId: null`. Discover IDs via list_users. WARNING: tenant pipeline / milestone-reached automation can mutate this field post-create \u2014 see the `milestoneId` description for details and the chained-PUT workaround."
|
|
1628
1717
|
),
|
|
1629
1718
|
teamId: positiveId.optional().describe(
|
|
1630
1719
|
"Assign to team ID (discover via list_teams). Independent from `ownerId` \u2014 setting one does NOT clear the other on create. Three ownership shapes are valid: owner alone, team alone, or owner+team (the owner must be a member of the team; users can belong to multiple teams \u2014 422 'owner is not a member of the team' otherwise)."
|
|
1631
1720
|
),
|
|
1632
|
-
fields:
|
|
1721
|
+
fields: z8.array(CustomFieldWriteSchema).optional().describe(
|
|
1633
1722
|
fieldsArrayDescriptor("get_opportunity") + " Capsule's POST /opportunities accepts the same `fields[]` shape as PUT (inferred by symmetry with the v1.6.5 wire-trace findings on POST /parties and POST /kases \u2014 the tenant probed had no opportunity custom fields configured, so this is unverified empirically). Setting custom fields on creation removes the create-then-update ritual."
|
|
1634
1723
|
)
|
|
1635
1724
|
});
|
|
@@ -1646,23 +1735,23 @@ async function createOpportunity(input) {
|
|
|
1646
1735
|
if (mappedFields !== void 0) body["fields"] = mappedFields;
|
|
1647
1736
|
return capsulePost("/opportunities", { opportunity: body });
|
|
1648
1737
|
}
|
|
1649
|
-
var updateOpportunitySchema =
|
|
1738
|
+
var updateOpportunitySchema = z8.object({
|
|
1650
1739
|
id: positiveId,
|
|
1651
|
-
name:
|
|
1740
|
+
name: z8.string().min(1).optional(),
|
|
1652
1741
|
partyId: positiveId.optional().describe(
|
|
1653
1742
|
"Reassign the opportunity to a different primary party. Capsule requires every opportunity to have a party \u2014 passing `null` is rejected with 422 'party is required' (use Capsule's web UI if you need to dissolve the link entirely). Discover ids via search_parties / filter_parties. No defensive read-modify-write needed: this connector verified empirically (v1.6.3 wire-trace) that `party` is a standalone PUT field on /opportunities and does not interact with the asymmetric owner/team semantic from NOTES-ON-CAPSULE-API.md \xA727. NOTE: parent-ref nullability differs by entity \u2014 `update_task.partyId` IS nullable (orphan task), but opportunities and projects must always have a parent party. The same applies to `update_project.partyId`."
|
|
1654
1743
|
),
|
|
1655
1744
|
milestoneId: positiveId.optional().describe(
|
|
1656
1745
|
"Move the opportunity to this milestone. Side effects depend on the target: closing milestones (Won/Lost) auto-set `closedOn` to today and `probability` to the milestone default (100/0), preserving `lastOpenMilestone` as the previous open stage; moving back to an open milestone clears `closedOn` and re-applies the milestone's default probability (Won/Lost is reversible \u2014 no separate reopen tool). WARNING: Capsule does NOT validate that the new milestone belongs to the opportunity's current pipeline. Passing a milestoneId from a different pipeline silently relocates the opportunity across pipelines, and `lastOpenMilestone` may then reference a milestone in the previous pipeline. Verify against the opportunity's current pipeline (read the opp first, list its pipeline's milestones via list_milestones) before passing a cross-pipeline id. NOTE: changing `milestoneId` can fire **pipeline / milestone-reached automations** that mutate `owner` / `team` on the destination milestone (same shape as `create_opportunity` \u2014 see its `milestoneId` description for the owner-clearing automation caveat). If a milestone-change-and-owner-set in the same call lands with `owner: null`, follow up with a second `update_opportunity` (or `batch_update_opportunity`) carrying both `ownerId` and `teamId` \u2014 milestone-reached triggers only fire on the transition, so a subsequent PUT preserves your values."
|
|
1657
1746
|
),
|
|
1658
|
-
description:
|
|
1747
|
+
description: z8.string().optional(),
|
|
1659
1748
|
value: OpportunityValueSchema.optional(),
|
|
1660
|
-
expectedCloseOn:
|
|
1661
|
-
probability:
|
|
1749
|
+
expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
|
|
1750
|
+
probability: z8.number().int().min(0).max(100).optional().describe(
|
|
1662
1751
|
"Win probability 0\u2013100. On an open milestone this overrides the milestone's default probability. CANNOT be set in the same call as a closing milestone (Won/Lost) \u2014 Capsule processes the milestone change first, the opportunity becomes closed, then the probability update is rejected as edit-on-closed-opp with 422 'probability can be updated only for open opportunity'. To close an opportunity, leave probability out of the call: it auto-snaps to 100% (Won) or 0% (Lost)."
|
|
1663
1752
|
),
|
|
1664
1753
|
lostReasonId: positiveId.optional().describe(
|
|
1665
|
-
"Reason the opportunity was lost. Only meaningful when transitioning to a Lost milestone \u2014 Capsule silently drops it for other milestones. Without this set, a connector-driven Lost-close leaves `lostReason: null`. Discover IDs via
|
|
1754
|
+
"Reason the opportunity was lost. Only meaningful when transitioning to a Lost milestone \u2014 Capsule silently drops it for other milestones. Without this set, a connector-driven Lost-close leaves `lostReason: null`. Discover IDs via list_lost_reasons."
|
|
1666
1755
|
),
|
|
1667
1756
|
ownerId: positiveId.nullable().optional().describe(
|
|
1668
1757
|
"Reassign owner: pass a user ID to set, or `null` to unassign (verified empirically in v1.6.5 wire-trace \u2014 Capsule accepts `owner: null` on PUT /opportunities/:id, mirroring the v1.6.4 finding on /parties; brings update_opportunity into parity with update_party and update_project). When you supply `ownerId` and omit `teamId`, the connector fetches the opportunity's current team and includes it in the PUT body to preserve it across the owner change. Without this defensive read, Capsule's PUT would clear the existing team (see NOTES-ON-CAPSULE-API.md \xA727 \u2014 same asymmetric semantic as /kases). Supply `teamId` explicitly on the same call to change the team instead. Combine `ownerId: null` + `teamId: <T>` in one call to transfer an opportunity to team-ownership with no specific user (verified empirically in v1.6.5; the owner-clears-team semantic doesn't fire when owner is being cleared to null)."
|
|
@@ -1670,7 +1759,7 @@ var updateOpportunitySchema = z7.object({
|
|
|
1670
1759
|
teamId: positiveId.nullable().optional().describe(
|
|
1671
1760
|
"Reassign team: pass a team ID (discover via list_teams) to set, or `null` to unassign. Capsule preserves the existing owner across a team change (server-side), so `update_opportunity { teamId }` alone is safe \u2014 the owner is carried through. Owner must be a member of the new team or Capsule returns 422 'owner is not a member of the team'. Independent from `ownerId` \u2014 setting `teamId` does NOT clear the owner."
|
|
1672
1761
|
),
|
|
1673
|
-
fields:
|
|
1762
|
+
fields: z8.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_opportunity"))
|
|
1674
1763
|
});
|
|
1675
1764
|
async function updateOpportunity(input) {
|
|
1676
1765
|
const { id, partyId, milestoneId, ownerId, teamId, lostReasonId, fields, ...rest } = input;
|
|
@@ -1706,25 +1795,37 @@ var { schema: deleteOpportunitySchema, handler: deleteOpportunity } = defineDele
|
|
|
1706
1795
|
});
|
|
1707
1796
|
|
|
1708
1797
|
// src/tools/projects.ts
|
|
1709
|
-
import { z as
|
|
1710
|
-
var
|
|
1711
|
-
|
|
1712
|
-
embed:
|
|
1713
|
-
|
|
1714
|
-
|
|
1798
|
+
import { z as z9 } from "zod";
|
|
1799
|
+
var searchProjectsSchema = z9.object({
|
|
1800
|
+
q: z9.string().optional().describe("Free-text search query"),
|
|
1801
|
+
embed: embedParam(RECORD_EMBEDS),
|
|
1802
|
+
...paginationFields
|
|
1803
|
+
});
|
|
1804
|
+
async function searchProjects(input) {
|
|
1805
|
+
const path = input.q ? "/kases/search" : "/kases";
|
|
1806
|
+
return capsuleGetList(path, {
|
|
1807
|
+
q: input.q,
|
|
1808
|
+
embed: input.embed,
|
|
1809
|
+
page: input.page,
|
|
1810
|
+
perPage: input.perPage
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
var listProjectsSchema = z9.object({
|
|
1814
|
+
status: z9.enum(["OPEN", "CLOSED"]).optional(),
|
|
1815
|
+
embed: embedParam(RECORD_EMBEDS),
|
|
1816
|
+
...paginationFields
|
|
1715
1817
|
});
|
|
1716
1818
|
async function listProjects(input) {
|
|
1717
|
-
|
|
1819
|
+
return capsuleGetList("/kases", {
|
|
1718
1820
|
status: input.status,
|
|
1719
1821
|
embed: input.embed,
|
|
1720
1822
|
page: input.page,
|
|
1721
1823
|
perPage: input.perPage
|
|
1722
1824
|
});
|
|
1723
|
-
return { ...data, nextPage };
|
|
1724
1825
|
}
|
|
1725
|
-
var getProjectSchema =
|
|
1826
|
+
var getProjectSchema = z9.object({
|
|
1726
1827
|
id: positiveId,
|
|
1727
|
-
embed:
|
|
1828
|
+
embed: embedParam(RECORD_EMBEDS)
|
|
1728
1829
|
});
|
|
1729
1830
|
async function getProject(input) {
|
|
1730
1831
|
const { data } = await capsuleGet(`/kases/${input.id}`, {
|
|
@@ -1732,20 +1833,20 @@ async function getProject(input) {
|
|
|
1732
1833
|
});
|
|
1733
1834
|
return data;
|
|
1734
1835
|
}
|
|
1735
|
-
var getProjectsSchema =
|
|
1736
|
-
ids:
|
|
1836
|
+
var getProjectsSchema = z9.object({
|
|
1837
|
+
ids: z9.array(positiveId).min(1).max(50).describe(
|
|
1737
1838
|
"Array of project IDs (1\u201350). Capsule's native batch-fetch endpoint caps at 10 per request; the connector transparently splits larger sets into 10-id chunks and fans out the Capsule calls in parallel."
|
|
1738
1839
|
),
|
|
1739
|
-
embed:
|
|
1840
|
+
embed: embedParam(RECORD_EMBEDS)
|
|
1740
1841
|
});
|
|
1741
1842
|
async function getProjects(input) {
|
|
1742
|
-
return chunkedMultiGet("/kases", "
|
|
1843
|
+
return chunkedMultiGet("/kases", "projects", input.ids, { embed: input.embed });
|
|
1743
1844
|
}
|
|
1744
|
-
var createProjectSchema =
|
|
1745
|
-
name:
|
|
1845
|
+
var createProjectSchema = z9.object({
|
|
1846
|
+
name: z9.string().min(1),
|
|
1746
1847
|
partyId: positiveId.describe("ID of the party linked to this project"),
|
|
1747
|
-
description:
|
|
1748
|
-
status:
|
|
1848
|
+
description: z9.string().optional(),
|
|
1849
|
+
status: z9.enum(["OPEN", "CLOSED"]).optional().describe("Defaults to OPEN when omitted."),
|
|
1749
1850
|
ownerId: positiveId.optional().describe(
|
|
1750
1851
|
"Assign to user ID. Defaults to the API-token owner when omitted, same as create_party / create_opportunity / create_task. NOTE: some Capsule tenants configure board-level **automation rules** that mutate `owner` (and `team`) on project creation \u2014 e.g. an automation that clears `owner` when a project enters a particular board. If you observe a project landing with unexpected `owner: null` after a create_project with `ownerId`, check the target board's automation configuration. Capsule's API itself does not drop `ownerId` when `stageId` is also supplied."
|
|
1751
1852
|
),
|
|
@@ -1755,8 +1856,8 @@ var createProjectSchema = z8.object({
|
|
|
1755
1856
|
stageId: positiveId.optional().describe(
|
|
1756
1857
|
"Stage (board column) to place the project on. Discover IDs via list_stages \u2014 each stage belongs to one Board, so picking a stageId implicitly picks the board. If omitted, the project is created with no stage assignment (and won't appear on any board). NOTE: tenant-specific board automation rules may run on project creation and mutate `owner` / `team` fields. See `create_project.ownerId` / `create_project.teamId` for the automation caveat. Capsule's create endpoint itself preserves the `ownerId` / `teamId` you supply \u2014 any clearing you observe traces to board automations, not the API."
|
|
1757
1858
|
),
|
|
1758
|
-
expectedCloseOn:
|
|
1759
|
-
fields:
|
|
1859
|
+
expectedCloseOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
|
|
1860
|
+
fields: z9.array(CustomFieldWriteSchema).optional().describe(
|
|
1760
1861
|
fieldsArrayDescriptor("get_project") + " Verified empirically in v1.6.5 wire-trace: Capsule's POST /kases accepts the same `fields[]` shape as PUT, so callers can set custom field values on creation without a follow-up update. Project-specific: setting a field whose definition lives under a 'data tag' populates the row's internal tagId but does NOT auto-add the data tag to the project's tags array \u2014 use add_tag explicitly if you want it visible via embed=tags."
|
|
1761
1862
|
)
|
|
1762
1863
|
});
|
|
@@ -1774,11 +1875,11 @@ async function createProject(input) {
|
|
|
1774
1875
|
if (mappedFields !== void 0) body["fields"] = mappedFields;
|
|
1775
1876
|
return capsulePost("/kases", { kase: body });
|
|
1776
1877
|
}
|
|
1777
|
-
var updateProjectSchema =
|
|
1878
|
+
var updateProjectSchema = z9.object({
|
|
1778
1879
|
id: positiveId,
|
|
1779
|
-
name:
|
|
1780
|
-
description:
|
|
1781
|
-
status:
|
|
1880
|
+
name: z9.string().min(1).optional(),
|
|
1881
|
+
description: z9.string().optional(),
|
|
1882
|
+
status: z9.enum(["OPEN", "CLOSED"]).optional(),
|
|
1782
1883
|
partyId: positiveId.optional().describe(
|
|
1783
1884
|
"Reassign the project to a different primary party. Capsule requires every project to have a party \u2014 passing `null` is rejected with 422 'party is required' (verified empirically in v1.6.3 wire-trace). Discover ids via search_parties / filter_parties. NOTE: parent-ref nullability differs by entity \u2014 `update_task.partyId` IS nullable (orphan task), but opportunities and projects must always have a parent party. The same applies to `update_opportunity.partyId`."
|
|
1784
1885
|
),
|
|
@@ -1791,8 +1892,8 @@ var updateProjectSchema = z8.object({
|
|
|
1791
1892
|
stageId: positiveId.nullable().optional().describe(
|
|
1792
1893
|
"Move the project to this stage (board column), or `null` to remove from all stages (verified empirically in v1.6.5 wire-trace \u2014 Capsule accepts `stage: null` on PUT /kases/:id and the project no longer appears on any board). Discover IDs via list_stages. Owner and team are preserved across stage-only updates (Capsule's PUT semantic). WARNING (cross-board): Capsule does NOT validate that the new stage belongs to the project's current board \u2014 passing a stageId from a different board silently relocates the project across boards. Team and other board-derived defaults are NOT updated to match the new board. Verify against the project's current board (read the project first, list its board's stages) before passing a cross-board id."
|
|
1793
1894
|
),
|
|
1794
|
-
expectedCloseOn:
|
|
1795
|
-
fields:
|
|
1895
|
+
expectedCloseOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
|
|
1896
|
+
fields: z9.array(CustomFieldWriteSchema).optional().describe(
|
|
1796
1897
|
fieldsArrayDescriptor("get_project") + " Project-specific: setting a field whose definition lives under a 'data tag' populates the row's internal tagId but does NOT auto-add the data tag to the project's tags array \u2014 use add_tag explicitly if you want it visible via embed=tags."
|
|
1797
1898
|
)
|
|
1798
1899
|
});
|
|
@@ -1806,7 +1907,7 @@ async function updateProject(input) {
|
|
|
1806
1907
|
let resolvedTeamId = teamId;
|
|
1807
1908
|
let resolvedStageId = stageId;
|
|
1808
1909
|
if (ownerId !== void 0 && (teamId === void 0 || stageId === void 0)) {
|
|
1809
|
-
const current = await readEntityRefs(`/kases/${id}`, "
|
|
1910
|
+
const current = await readEntityRefs(`/kases/${id}`, "project");
|
|
1810
1911
|
if (teamId === void 0) resolvedTeamId = current.teamId;
|
|
1811
1912
|
if (stageId === void 0) resolvedStageId = current.stageId;
|
|
1812
1913
|
}
|
|
@@ -1831,23 +1932,22 @@ var { schema: deleteProjectSchema, handler: deleteProject } = defineDelete({
|
|
|
1831
1932
|
});
|
|
1832
1933
|
|
|
1833
1934
|
// src/tools/tasks.ts
|
|
1834
|
-
import { z as
|
|
1835
|
-
var listTasksSchema =
|
|
1935
|
+
import { z as z10 } from "zod";
|
|
1936
|
+
var listTasksSchema = z10.object({
|
|
1836
1937
|
// Note: Capsule has a third internal status `PENDING` (a task that's
|
|
1837
1938
|
// part of an active track but not yet "open"), but it can only be
|
|
1838
1939
|
// reached via track machinery — it is NOT directly settable by
|
|
1839
1940
|
// /tasks PUT, and a list filter for it returns the same as OPEN
|
|
1840
1941
|
// anyway. We expose only the two values that are actually filterable
|
|
1841
1942
|
// by the v2 API.
|
|
1842
|
-
status:
|
|
1943
|
+
status: z10.enum(["OPEN", "COMPLETED"]).optional().describe(
|
|
1843
1944
|
"Defaults to OPEN when omitted. Pass COMPLETED to filter to completed tasks, or 'OPEN' explicitly."
|
|
1844
1945
|
),
|
|
1845
1946
|
ownerId: positiveId.optional().describe("Filter to tasks owned by this user ID"),
|
|
1846
|
-
|
|
1847
|
-
perPage: z9.number().int().min(1).max(100).optional().default(25)
|
|
1947
|
+
...paginationFields
|
|
1848
1948
|
});
|
|
1849
1949
|
async function listTasks(input) {
|
|
1850
|
-
|
|
1950
|
+
return capsuleGetList("/tasks", {
|
|
1851
1951
|
// Default 'OPEN' applied here (not via zod .default()) so that
|
|
1852
1952
|
// z.infer keeps `status` optional for callers that omit it.
|
|
1853
1953
|
status: input.status ?? "OPEN",
|
|
@@ -1856,28 +1956,27 @@ async function listTasks(input) {
|
|
|
1856
1956
|
page: input.page,
|
|
1857
1957
|
perPage: input.perPage
|
|
1858
1958
|
});
|
|
1859
|
-
return { ...data, nextPage };
|
|
1860
1959
|
}
|
|
1861
|
-
var getTaskSchema =
|
|
1960
|
+
var getTaskSchema = z10.object({
|
|
1862
1961
|
id: positiveId.describe("Task ID")
|
|
1863
1962
|
});
|
|
1864
1963
|
async function getTask(input) {
|
|
1865
1964
|
const { data } = await capsuleGet(`/tasks/${input.id}`);
|
|
1866
1965
|
return data;
|
|
1867
1966
|
}
|
|
1868
|
-
var getTasksSchema =
|
|
1869
|
-
ids:
|
|
1967
|
+
var getTasksSchema = z10.object({
|
|
1968
|
+
ids: z10.array(positiveId).min(1).max(50).describe(
|
|
1870
1969
|
"Array of task IDs (1\u201350). Capsule's native batch-fetch endpoint caps at 10 per request; the connector transparently splits larger sets into 10-id chunks and fans out the Capsule calls in parallel."
|
|
1871
1970
|
)
|
|
1872
1971
|
});
|
|
1873
1972
|
async function getTasks(input) {
|
|
1874
1973
|
return chunkedMultiGet("/tasks", "tasks", input.ids);
|
|
1875
1974
|
}
|
|
1876
|
-
var createTaskSchema =
|
|
1877
|
-
description:
|
|
1878
|
-
dueOn:
|
|
1879
|
-
dueTime:
|
|
1880
|
-
detail:
|
|
1975
|
+
var createTaskSchema = z10.object({
|
|
1976
|
+
description: z10.string().min(1),
|
|
1977
|
+
dueOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("YYYY-MM-DD"),
|
|
1978
|
+
dueTime: z10.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
|
|
1979
|
+
detail: z10.string().optional(),
|
|
1881
1980
|
ownerId: positiveId.optional().describe(
|
|
1882
1981
|
"Assign to user ID. Defaults to the API-token owner when omitted. Once set, this connector cannot clear the owner back to null \u2014 use Capsule's web UI for that."
|
|
1883
1982
|
),
|
|
@@ -1886,10 +1985,7 @@ var createTaskSchema = z9.object({
|
|
|
1886
1985
|
projectId: positiveId.optional().describe("Link task to a project (mutually exclusive with partyId/opportunityId)")
|
|
1887
1986
|
});
|
|
1888
1987
|
async function createTask(input) {
|
|
1889
|
-
|
|
1890
|
-
if (linked.length > 1) {
|
|
1891
|
-
throw new Error("Provide at most one of partyId, opportunityId, or projectId");
|
|
1892
|
-
}
|
|
1988
|
+
assertSingleParentRef("create_task", input);
|
|
1893
1989
|
const { ownerId, partyId, opportunityId, projectId, ...rest } = input;
|
|
1894
1990
|
const body = { ...rest };
|
|
1895
1991
|
setRef(body, "owner", ownerId);
|
|
@@ -1898,16 +1994,16 @@ async function createTask(input) {
|
|
|
1898
1994
|
setRef(body, "kase", projectId);
|
|
1899
1995
|
return capsulePost("/tasks", { task: body });
|
|
1900
1996
|
}
|
|
1901
|
-
var updateTaskSchema =
|
|
1997
|
+
var updateTaskSchema = z10.object({
|
|
1902
1998
|
id: positiveId,
|
|
1903
|
-
description:
|
|
1904
|
-
dueOn:
|
|
1905
|
-
dueTime:
|
|
1906
|
-
detail:
|
|
1999
|
+
description: z10.string().min(1).optional(),
|
|
2000
|
+
dueOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
|
|
2001
|
+
dueTime: z10.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
|
|
2002
|
+
detail: z10.string().optional(),
|
|
1907
2003
|
// Capsule rejects direct sets of `PENDING` (which is a track-machinery
|
|
1908
2004
|
// internal state) with 422 "cannot set task status to PENDING".
|
|
1909
2005
|
// Only OPEN and COMPLETED are settable here.
|
|
1910
|
-
status:
|
|
2006
|
+
status: z10.enum(["OPEN", "COMPLETED"]).optional().describe(
|
|
1911
2007
|
"Set to OPEN or COMPLETED. (PENDING exists internally for track-driven tasks but cannot be set directly via this tool \u2014 Capsule rejects it.) Setting status: OPEN on an already-open task is a true no-op (does not advance updatedAt)."
|
|
1912
2008
|
),
|
|
1913
2009
|
ownerId: positiveId.optional().describe(
|
|
@@ -1925,12 +2021,7 @@ var updateTaskSchema = z9.object({
|
|
|
1925
2021
|
});
|
|
1926
2022
|
async function updateTask(input) {
|
|
1927
2023
|
const { id, ownerId, partyId, opportunityId, projectId, ...rest } = input;
|
|
1928
|
-
|
|
1929
|
-
if (setCount > 1) {
|
|
1930
|
-
throw new Error(
|
|
1931
|
-
"update_task: provide at most one of partyId, opportunityId, or projectId (Capsule rejects multi-parent tasks with 422 'task can be related to at most one entity')"
|
|
1932
|
-
);
|
|
1933
|
-
}
|
|
2024
|
+
assertSingleParentRef("update_task", { partyId, opportunityId, projectId });
|
|
1934
2025
|
const body = {};
|
|
1935
2026
|
for (const [k, v] of Object.entries(rest)) {
|
|
1936
2027
|
if (v !== void 0) body[k] = v;
|
|
@@ -1941,7 +2032,7 @@ async function updateTask(input) {
|
|
|
1941
2032
|
setNullableRef(body, "kase", projectId);
|
|
1942
2033
|
return capsulePut(`/tasks/${id}`, { task: body });
|
|
1943
2034
|
}
|
|
1944
|
-
var completeTaskSchema =
|
|
2035
|
+
var completeTaskSchema = z10.object({
|
|
1945
2036
|
id: positiveId
|
|
1946
2037
|
});
|
|
1947
2038
|
async function completeTask(input) {
|
|
@@ -1949,8 +2040,8 @@ async function completeTask(input) {
|
|
|
1949
2040
|
task: { status: "COMPLETED" }
|
|
1950
2041
|
});
|
|
1951
2042
|
}
|
|
1952
|
-
var batchCompleteTaskSchema =
|
|
1953
|
-
ids:
|
|
2043
|
+
var batchCompleteTaskSchema = z10.object({
|
|
2044
|
+
ids: z10.array(positiveId).min(1).max(50).describe(
|
|
1954
2045
|
"Array of 1\u201350 task ids to mark COMPLETED in parallel. Each id resolves to one PUT /tasks/{id}; failures (e.g. 404 for a deleted task) surface per-item in the result array, the rest still complete. Capped at 50."
|
|
1955
2046
|
)
|
|
1956
2047
|
});
|
|
@@ -1964,77 +2055,59 @@ var { schema: deleteTaskSchema, handler: deleteTask } = defineDelete({
|
|
|
1964
2055
|
});
|
|
1965
2056
|
|
|
1966
2057
|
// src/tools/entries.ts
|
|
1967
|
-
import { z as
|
|
2058
|
+
import { z as z11 } from "zod";
|
|
1968
2059
|
var listEntriesPagination = {
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
embed: z10.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
|
|
2060
|
+
...paginationFields,
|
|
2061
|
+
embed: embedParam(ENTRY_EMBEDS)
|
|
1972
2062
|
};
|
|
1973
|
-
var listPartyEntriesSchema =
|
|
2063
|
+
var listPartyEntriesSchema = z11.object({
|
|
1974
2064
|
partyId: positiveId,
|
|
1975
2065
|
...listEntriesPagination,
|
|
1976
|
-
includeLinkedPersons:
|
|
2066
|
+
includeLinkedPersons: z11.boolean().optional().describe(
|
|
1977
2067
|
"When true AND `partyId` is an ORGANISATION, also include entries filed against the organisation's linked people (the persons whose `organisation` field references this org). The connector enumerates linked persons via `GET /parties/{orgId}/people`, fans out `GET /parties/{personId}/entries` in parallel (concurrency-capped, default 5 / configurable via `CAPSULE_MCP_BATCH_CONCURRENCY`), and merges into a single feed sorted by `entryAt` descending, deduped by entry id. Default is `false` \u2014 single GET, existing behaviour unchanged. WHY THIS FLAG EXISTS: Capsule's API files each entry against exactly one party row (verified v1.6.6 wire-trace probe 4 \u2014 POST /entries rejects multi-party bodies with 422 'entry must be linked to either a party, opportunity or kase'). For an organisation with multiple contacts, captured emails almost always land on a person row, not the org. As a result, `list_party_entries(orgId)` with `includeLinkedPersons: false` will miss recent customer-facing email \u2014 even though the org's own `lastContactedAt` is updated by the activity. This flag is the correct call for any 'what's new with $ORG?' question. WHEN `partyId` IS A PERSON: silently no-op \u2014 persons have no linked-people relationship in Capsule's data model, so the flag is functionally inert (the connector still issues a cheap `/people` check; the response is empty). LATENCY: 1 + N round trips for an org with N linked people, concurrency-capped (typical: 2-3 waves for N=10). Linked-person enumeration reads the first 100 linked people; use list_employees for explicit pagination when an organisation has more contacts than that. Use `includeLinkedPersons: false` for fast pre-screen reads where you only need the org-row entries (e.g. invoice/contract notes that are typically filed at the org level). PAGINATION CAVEAT: `page` and `perPage` apply to the MERGED window, and the merge has a hard ceiling \u2014 it reliably orders only the most-recent ~100 entries across the org + its people (each party is fetched at Capsule's per-party cap of 100, and a top-100-per-party merge is correct only up to global position 100). Windows that cross the ceiling are truncated to the entries still inside that top-100 set; windows starting beyond it return no entries and end the feed. It does NOT continue into older history. To read a specific contact's full timeline beyond the merged ceiling, call `list_party_entries` on that person's id directly (the default single-GET path paginates natively with no ceiling). For the LLM-driven 'what's the latest with $ORG' query this is the typical use of, the first page is exact and the ceiling is never reached."
|
|
1978
2068
|
)
|
|
1979
2069
|
});
|
|
2070
|
+
var PER_PARTY_FETCH_CAP = 100;
|
|
1980
2071
|
async function fanOutPartyEntries(partyIds, embed, perPage) {
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
const id = partyIds[i];
|
|
1990
|
-
const { data, nextPage } = await capsuleGet(
|
|
1991
|
-
`/parties/${id}/entries`,
|
|
1992
|
-
{
|
|
1993
|
-
embed,
|
|
1994
|
-
page: 1,
|
|
1995
|
-
perPage
|
|
1996
|
-
}
|
|
1997
|
-
);
|
|
1998
|
-
results[i] = { entries: data.entries, nextPage };
|
|
1999
|
-
}
|
|
2000
|
-
}
|
|
2001
|
-
const workers = [];
|
|
2002
|
-
for (let w = 0; w < Math.min(concurrency, partyIds.length); w++) {
|
|
2003
|
-
workers.push(worker());
|
|
2004
|
-
}
|
|
2005
|
-
await Promise.all(workers);
|
|
2006
|
-
return results;
|
|
2072
|
+
return mapWithConcurrency(partyIds, getBatchConcurrency(), async (id) => {
|
|
2073
|
+
const { data, nextPage } = await capsuleGet(`/parties/${id}/entries`, {
|
|
2074
|
+
embed,
|
|
2075
|
+
page: 1,
|
|
2076
|
+
perPage
|
|
2077
|
+
});
|
|
2078
|
+
return { entries: data.entries, nextPage };
|
|
2079
|
+
});
|
|
2007
2080
|
}
|
|
2008
2081
|
function mergedTimelineCandidatePerParty(page, perPage) {
|
|
2009
|
-
return Math.min(page * perPage,
|
|
2082
|
+
return Math.min(page * perPage, PER_PARTY_FETCH_CAP);
|
|
2010
2083
|
}
|
|
2011
2084
|
function mergedTimelineNextPage(page, perPage, mergedLength, upstreamHasNextPage) {
|
|
2012
2085
|
const requestedWindowEnd = page * perPage;
|
|
2013
2086
|
if (mergedLength > requestedWindowEnd) return page + 1;
|
|
2014
|
-
const nextWindowWithinCap = requestedWindowEnd <
|
|
2087
|
+
const nextWindowWithinCap = requestedWindowEnd < PER_PARTY_FETCH_CAP;
|
|
2015
2088
|
if (nextWindowWithinCap && upstreamHasNextPage) return page + 1;
|
|
2016
2089
|
return void 0;
|
|
2017
2090
|
}
|
|
2018
2091
|
async function listPartyEntries(input) {
|
|
2019
2092
|
const { partyId, embed, page, perPage, includeLinkedPersons } = input;
|
|
2020
2093
|
if (!includeLinkedPersons) {
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2094
|
+
return capsuleGetList(`/parties/${partyId}/entries`, {
|
|
2095
|
+
embed,
|
|
2096
|
+
page,
|
|
2097
|
+
perPage
|
|
2098
|
+
});
|
|
2026
2099
|
}
|
|
2027
2100
|
const { data: peopleData } = await capsuleGet(
|
|
2028
2101
|
`/parties/${partyId}/people`,
|
|
2029
|
-
{ page: 1, perPage:
|
|
2102
|
+
{ page: 1, perPage: PER_PARTY_FETCH_CAP }
|
|
2030
2103
|
);
|
|
2031
2104
|
const peopleIds = (peopleData.parties ?? []).map((p) => p.id);
|
|
2032
2105
|
if (peopleIds.length === 0) {
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2106
|
+
return capsuleGetList(`/parties/${partyId}/entries`, {
|
|
2107
|
+
embed,
|
|
2108
|
+
page,
|
|
2109
|
+
perPage
|
|
2110
|
+
});
|
|
2038
2111
|
}
|
|
2039
2112
|
const targetIds = [partyId, ...peopleIds];
|
|
2040
2113
|
const perPartyPages = await fanOutPartyEntries(
|
|
@@ -2069,31 +2142,31 @@ async function listPartyEntries(input) {
|
|
|
2069
2142
|
);
|
|
2070
2143
|
return { entries: slice, ...nextPage !== void 0 ? { nextPage } : {} };
|
|
2071
2144
|
}
|
|
2072
|
-
var listOpportunityEntriesSchema =
|
|
2145
|
+
var listOpportunityEntriesSchema = z11.object({
|
|
2073
2146
|
opportunityId: positiveId,
|
|
2074
2147
|
...listEntriesPagination
|
|
2075
2148
|
});
|
|
2076
2149
|
async function listOpportunityEntries(input) {
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2150
|
+
return capsuleGetList(`/opportunities/${input.opportunityId}/entries`, {
|
|
2151
|
+
embed: input.embed,
|
|
2152
|
+
page: input.page,
|
|
2153
|
+
perPage: input.perPage
|
|
2154
|
+
});
|
|
2082
2155
|
}
|
|
2083
|
-
var listProjectEntriesSchema =
|
|
2156
|
+
var listProjectEntriesSchema = z11.object({
|
|
2084
2157
|
projectId: positiveId,
|
|
2085
2158
|
...listEntriesPagination
|
|
2086
2159
|
});
|
|
2087
2160
|
async function listProjectEntries(input) {
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2161
|
+
return capsuleGetList(`/kases/${input.projectId}/entries`, {
|
|
2162
|
+
embed: input.embed,
|
|
2163
|
+
page: input.page,
|
|
2164
|
+
perPage: input.perPage
|
|
2165
|
+
});
|
|
2093
2166
|
}
|
|
2094
|
-
var getEntrySchema =
|
|
2167
|
+
var getEntrySchema = z11.object({
|
|
2095
2168
|
id: positiveId,
|
|
2096
|
-
embed:
|
|
2169
|
+
embed: embedParam(ENTRY_EMBEDS)
|
|
2097
2170
|
});
|
|
2098
2171
|
async function getEntry(input) {
|
|
2099
2172
|
const { data } = await capsuleGet(`/entries/${input.id}`, {
|
|
@@ -2101,34 +2174,30 @@ async function getEntry(input) {
|
|
|
2101
2174
|
});
|
|
2102
2175
|
return data;
|
|
2103
2176
|
}
|
|
2104
|
-
var listEntriesSchema =
|
|
2177
|
+
var listEntriesSchema = z11.object({
|
|
2105
2178
|
...listEntriesPagination
|
|
2106
2179
|
});
|
|
2107
2180
|
async function listEntries(input) {
|
|
2108
|
-
|
|
2181
|
+
return capsuleGetList("/entries", {
|
|
2109
2182
|
embed: input.embed,
|
|
2110
2183
|
page: input.page,
|
|
2111
2184
|
perPage: input.perPage
|
|
2112
2185
|
});
|
|
2113
|
-
return { ...data, nextPage };
|
|
2114
2186
|
}
|
|
2115
|
-
var addNoteSchema =
|
|
2116
|
-
content:
|
|
2187
|
+
var addNoteSchema = z11.object({
|
|
2188
|
+
content: z11.string().min(1).describe(
|
|
2117
2189
|
"Note body text. Stored verbatim and treated as MARKDOWN \u2014 Capsule's web UI renders the markdown when displaying. Pass markdown source ('# Heading', '**bold**', '- bullet'), not HTML."
|
|
2118
2190
|
),
|
|
2119
2191
|
partyId: positiveId.optional().describe("Link note to a party (mutually exclusive with opportunityId/projectId)"),
|
|
2120
2192
|
opportunityId: positiveId.optional().describe("Link note to an opportunity (mutually exclusive with partyId/projectId)"),
|
|
2121
2193
|
projectId: positiveId.optional().describe("Link note to a project (mutually exclusive with partyId/opportunityId)"),
|
|
2122
|
-
entryAt:
|
|
2194
|
+
entryAt: z11.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/).optional().describe(
|
|
2123
2195
|
"ISO-8601 timestamp for when this note actually happened (e.g. '2024-03-15T14:30:00Z'). Defaults to now. Use this for backdating historical notes when migrating from another system. `entryAt` is preserved across subsequent update_entry calls; only `updatedAt` advances on edits. Note attribution flows to the API-token owner \u2014 there is no way to record a note as authored by a different user via this connector (a `creatorId` parameter would enable audit-attribution spoofing on shared-connector deployments, so it is intentionally not exposed)."
|
|
2124
2196
|
)
|
|
2125
2197
|
});
|
|
2126
2198
|
async function addNote(input) {
|
|
2127
2199
|
const { content, partyId, opportunityId, projectId, entryAt } = input;
|
|
2128
|
-
|
|
2129
|
-
if (linked.length !== 1) {
|
|
2130
|
-
throw new Error("Provide exactly one of partyId, opportunityId, or projectId");
|
|
2131
|
-
}
|
|
2200
|
+
assertSingleParentRef("add_note", input, { required: true });
|
|
2132
2201
|
const body = { type: "note", content };
|
|
2133
2202
|
setRef(body, "party", partyId);
|
|
2134
2203
|
setRef(body, "opportunity", opportunityId);
|
|
@@ -2136,12 +2205,12 @@ async function addNote(input) {
|
|
|
2136
2205
|
if (entryAt !== void 0) body["entryAt"] = entryAt;
|
|
2137
2206
|
return capsulePost("/entries", { entry: body });
|
|
2138
2207
|
}
|
|
2139
|
-
var updateEntrySchema =
|
|
2208
|
+
var updateEntrySchema = z11.object({
|
|
2140
2209
|
id: positiveId.describe("Entry ID to update"),
|
|
2141
|
-
content:
|
|
2210
|
+
content: z11.string().min(1).optional().describe(
|
|
2142
2211
|
"New body text for the entry. For notes, this is the markdown content; for emails, the body. Provide only if you want to change it."
|
|
2143
2212
|
),
|
|
2144
|
-
subject:
|
|
2213
|
+
subject: z11.string().optional().describe(
|
|
2145
2214
|
"New subject line. Mostly meaningful on email-type entries; on plain notes Capsule accepts the call (HTTP 200) but **does not store the subject and does not advance `updatedAt`** \u2014 a true no-op for inapplicable fields. `entryAt` (when the note was authored) is preserved across edits; `updatedAt` advances only when an applicable field actually changes. To sort/filter by 'when did this happen', use `entryAt`; for 'last touched', use `updatedAt`."
|
|
2146
2215
|
)
|
|
2147
2216
|
});
|
|
@@ -2163,103 +2232,89 @@ var { schema: deleteEntrySchema, handler: deleteEntry } = defineDelete({
|
|
|
2163
2232
|
});
|
|
2164
2233
|
|
|
2165
2234
|
// src/tools/pipelines.ts
|
|
2166
|
-
import { z as
|
|
2167
|
-
var
|
|
2168
|
-
page: z11.number().int().positive().optional(),
|
|
2169
|
-
perPage: z11.number().int().min(1).max(100).optional()
|
|
2170
|
-
};
|
|
2171
|
-
var listPipelinesSchema = z11.object({ ...paginationFields });
|
|
2235
|
+
import { z as z12 } from "zod";
|
|
2236
|
+
var listPipelinesSchema = z12.object({ ...paginationFieldsNoDefaults });
|
|
2172
2237
|
async function listPipelines(input) {
|
|
2173
|
-
|
|
2238
|
+
return capsuleGetCachedList("/pipelines", {
|
|
2174
2239
|
page: input.page ?? 1,
|
|
2175
2240
|
perPage: input.perPage ?? 100
|
|
2176
2241
|
});
|
|
2177
|
-
return { ...data, nextPage };
|
|
2178
2242
|
}
|
|
2179
|
-
var listMilestonesSchema =
|
|
2243
|
+
var listMilestonesSchema = z12.object({
|
|
2180
2244
|
pipelineId: positiveId,
|
|
2181
|
-
...
|
|
2245
|
+
...paginationFieldsNoDefaults
|
|
2182
2246
|
});
|
|
2183
2247
|
async function listMilestones(input) {
|
|
2184
|
-
|
|
2248
|
+
return capsuleGetCachedList(
|
|
2185
2249
|
`/pipelines/${input.pipelineId}/milestones`,
|
|
2186
2250
|
{ page: input.page ?? 1, perPage: input.perPage ?? 100 }
|
|
2187
2251
|
);
|
|
2188
|
-
return { ...data, nextPage };
|
|
2189
2252
|
}
|
|
2190
2253
|
|
|
2191
2254
|
// src/tools/boards.ts
|
|
2192
|
-
import { z as
|
|
2193
|
-
var
|
|
2194
|
-
page: z12.number().int().positive().optional(),
|
|
2195
|
-
perPage: z12.number().int().min(1).max(100).optional()
|
|
2196
|
-
};
|
|
2197
|
-
var listBoardsSchema = z12.object({ ...paginationFields2 });
|
|
2255
|
+
import { z as z13 } from "zod";
|
|
2256
|
+
var listBoardsSchema = z13.object({ ...paginationFieldsNoDefaults });
|
|
2198
2257
|
async function listBoards(input) {
|
|
2199
|
-
|
|
2258
|
+
return capsuleGetCachedList("/boards", {
|
|
2200
2259
|
page: input.page ?? 1,
|
|
2201
2260
|
perPage: input.perPage ?? 100
|
|
2202
2261
|
});
|
|
2203
|
-
return { ...data, nextPage };
|
|
2204
2262
|
}
|
|
2205
|
-
var listStagesSchema =
|
|
2263
|
+
var listStagesSchema = z13.object({
|
|
2206
2264
|
boardId: positiveId.optional().describe(
|
|
2207
2265
|
"Optional. If provided, returns only the stages defined on that specific board (uses /boards/{id}/stages). Omit to get all stages across all boards in one call."
|
|
2208
2266
|
),
|
|
2209
|
-
...
|
|
2267
|
+
...paginationFieldsNoDefaults
|
|
2210
2268
|
});
|
|
2211
2269
|
async function listStages(input) {
|
|
2212
2270
|
const path = input.boardId !== void 0 ? `/boards/${input.boardId}/stages` : "/stages";
|
|
2213
|
-
|
|
2271
|
+
return capsuleGetCachedList(path, {
|
|
2214
2272
|
page: input.page ?? 1,
|
|
2215
2273
|
perPage: input.perPage ?? 100
|
|
2216
2274
|
});
|
|
2217
|
-
return { ...data, nextPage };
|
|
2218
2275
|
}
|
|
2219
2276
|
|
|
2220
2277
|
// src/tools/tags.ts
|
|
2221
|
-
import { z as
|
|
2278
|
+
import { z as z14 } from "zod";
|
|
2222
2279
|
var TAG_LIST_PATH = {
|
|
2223
2280
|
parties: "/parties/tags",
|
|
2224
2281
|
opportunities: "/opportunities/tags",
|
|
2225
|
-
|
|
2282
|
+
projects: "/kases/tags"
|
|
2226
2283
|
};
|
|
2227
2284
|
var ENTITY_TO_WRAPPER = {
|
|
2228
2285
|
parties: "party",
|
|
2229
2286
|
opportunities: "opportunity",
|
|
2230
|
-
|
|
2287
|
+
projects: "kase"
|
|
2231
2288
|
};
|
|
2232
|
-
var TagEntity =
|
|
2233
|
-
var listTagsSchema =
|
|
2234
|
-
entity:
|
|
2235
|
-
|
|
2236
|
-
perPage: z13.number().int().min(1).max(100).optional()
|
|
2289
|
+
var TagEntity = z14.enum(["parties", "opportunities", "projects"]).describe("Which entity type.");
|
|
2290
|
+
var listTagsSchema = z14.object({
|
|
2291
|
+
entity: z14.enum(["parties", "opportunities", "projects"]).describe("The resource type to list tags for"),
|
|
2292
|
+
...paginationFieldsNoDefaults
|
|
2237
2293
|
});
|
|
2238
2294
|
async function listTags(input) {
|
|
2239
2295
|
const path = TAG_LIST_PATH[input.entity];
|
|
2240
|
-
|
|
2296
|
+
return capsuleGetCachedList(path, {
|
|
2241
2297
|
page: input.page ?? 1,
|
|
2242
2298
|
perPage: input.perPage ?? 100
|
|
2243
2299
|
});
|
|
2244
|
-
return { ...data, nextPage };
|
|
2245
2300
|
}
|
|
2246
|
-
var addTagSchema =
|
|
2301
|
+
var addTagSchema = z14.object({
|
|
2247
2302
|
entity: TagEntity,
|
|
2248
2303
|
entityId: positiveId.describe("The party/opportunity/kase id."),
|
|
2249
|
-
tagName:
|
|
2304
|
+
tagName: z14.string().min(1).describe(
|
|
2250
2305
|
"Name of the tag to attach. Capsule resolves by name: if a tag with this name already exists in the tenant it is attached to the entity; if not, Capsule creates the tag and attaches it. Names are tenant-global. Capsule matches case-INSENSITIVELY when resolving (so 'VIP' and 'vip' attach the same tag), preserving the canonical casing from whichever variant was created first. To ensure consistent casing in your tag list, call list_tags first and reuse the exact name from there. Idempotent \u2014 re-attaching an already-attached tag is harmless."
|
|
2251
2306
|
)
|
|
2252
2307
|
});
|
|
2253
2308
|
async function addTag(input) {
|
|
2254
2309
|
const { entity, entityId, tagName } = input;
|
|
2255
2310
|
const wrapper = ENTITY_TO_WRAPPER[entity];
|
|
2256
|
-
const result = await capsulePut(`/${entity}/${entityId}`, {
|
|
2311
|
+
const result = await capsulePut(`/${ENTITY_PATH[entity]}/${entityId}`, {
|
|
2257
2312
|
[wrapper]: { tags: [{ name: tagName }] }
|
|
2258
2313
|
});
|
|
2259
2314
|
invalidateByPrefix(TAG_LIST_PATH[entity], "add_tag");
|
|
2260
2315
|
return result;
|
|
2261
2316
|
}
|
|
2262
|
-
var removeTagByIdSchema =
|
|
2317
|
+
var removeTagByIdSchema = z14.object({
|
|
2263
2318
|
entity: TagEntity,
|
|
2264
2319
|
entityId: positiveId.describe("The party/opportunity/kase id."),
|
|
2265
2320
|
tagId: positiveId.describe(
|
|
@@ -2270,7 +2325,7 @@ async function removeTagById(input) {
|
|
|
2270
2325
|
const { entity, entityId, tagId } = input;
|
|
2271
2326
|
const wrapper = ENTITY_TO_WRAPPER[entity];
|
|
2272
2327
|
const result = await idempotentWithResult(
|
|
2273
|
-
() => capsulePut(`/${entity}/${entityId}`, {
|
|
2328
|
+
() => capsulePut(`/${ENTITY_PATH[entity]}/${entityId}`, {
|
|
2274
2329
|
[wrapper]: { tags: [{ id: tagId, _delete: true }] }
|
|
2275
2330
|
}),
|
|
2276
2331
|
(result2) => ({
|
|
@@ -2290,7 +2345,7 @@ async function removeTagById(input) {
|
|
|
2290
2345
|
invalidateByPrefix(TAG_LIST_PATH[entity], "remove_tag_by_id");
|
|
2291
2346
|
return result;
|
|
2292
2347
|
}
|
|
2293
|
-
var deleteTagDefinitionSchema =
|
|
2348
|
+
var deleteTagDefinitionSchema = z14.object({
|
|
2294
2349
|
entity: TagEntity,
|
|
2295
2350
|
tagId: positiveId.describe(
|
|
2296
2351
|
"The tag definition's id (from list_tags, or embed='tags' on a record). NOT an entity id."
|
|
@@ -2305,7 +2360,7 @@ async function deleteTagDefinition(input) {
|
|
|
2305
2360
|
throw new Error("delete_tag_definition requires confirm: true");
|
|
2306
2361
|
}
|
|
2307
2362
|
const result = await idempotent(
|
|
2308
|
-
() => capsuleDelete(`/${entity}/tags/${tagId}`),
|
|
2363
|
+
() => capsuleDelete(`/${ENTITY_PATH[entity]}/tags/${tagId}`),
|
|
2309
2364
|
() => ({ deleted: true, alreadyDeleted: false, entity, tagId }),
|
|
2310
2365
|
() => ({ deleted: true, alreadyDeleted: true, entity, tagId })
|
|
2311
2366
|
);
|
|
@@ -2326,44 +2381,41 @@ var { schema: batchRemoveTagByIdSchema, handler: batchRemoveTagById } = defineBa
|
|
|
2326
2381
|
});
|
|
2327
2382
|
|
|
2328
2383
|
// src/tools/users.ts
|
|
2329
|
-
import { z as
|
|
2330
|
-
var listUsersSchema =
|
|
2331
|
-
|
|
2332
|
-
perPage: z14.number().int().min(1).max(100).optional()
|
|
2384
|
+
import { z as z15 } from "zod";
|
|
2385
|
+
var listUsersSchema = z15.object({
|
|
2386
|
+
...paginationFieldsNoDefaults
|
|
2333
2387
|
});
|
|
2334
2388
|
async function listUsers(input) {
|
|
2335
|
-
|
|
2389
|
+
return capsuleGetCachedList("/users", {
|
|
2336
2390
|
page: input.page ?? 1,
|
|
2337
2391
|
perPage: input.perPage ?? 100
|
|
2338
2392
|
});
|
|
2339
|
-
return { ...data, nextPage };
|
|
2340
2393
|
}
|
|
2341
|
-
var getCurrentUserSchema =
|
|
2394
|
+
var getCurrentUserSchema = z15.object({});
|
|
2342
2395
|
async function getCurrentUser(_input) {
|
|
2343
2396
|
const { data } = await capsuleGet("/users/current");
|
|
2344
2397
|
return data;
|
|
2345
2398
|
}
|
|
2346
2399
|
|
|
2347
2400
|
// src/tools/filters.ts
|
|
2348
|
-
import { z as
|
|
2349
|
-
var FilterConditionSchema =
|
|
2350
|
-
field:
|
|
2401
|
+
import { z as z16 } from "zod";
|
|
2402
|
+
var FilterConditionSchema = z16.object({
|
|
2403
|
+
field: z16.string().describe(
|
|
2351
2404
|
"The Capsule filter-side field name (these differ from response field names \u2014 e.g. response.createdAt is filter-side 'addedOn', response.lastContactedAt is filter-side 'lastContactedOn'). Common: 'addedOn' (date created), 'updatedOn' (date last modified), 'lastContactedOn' (parties only), 'name', 'tag', 'owner', 'team', 'type' (parties: person|organisation), 'milestone' (opportunities), 'status' (opp/project: OPEN|CLOSED), 'closedOn' (opp/project), 'expectedCloseOn' (opp/project), 'hasTags', 'hasEmailAddress' (parties), 'isOpen', 'isStale' (opportunities), 'custom:{fieldId}'. Full per-entity list: https://developer.capsulecrm.com/v2/reference/filters"
|
|
2352
2405
|
),
|
|
2353
|
-
operator:
|
|
2406
|
+
operator: z16.string().describe(
|
|
2354
2407
|
"The filter operator. Common: 'is', 'is not' (use value=null to test for null), 'contains', 'does not contain', 'is greater than', 'is less than', 'is within last' (date fields, value=integer days), 'is more than' (date fields, value=integer days ago), 'starts with', 'ends with'. Operator validity depends on the field's type."
|
|
2355
2408
|
),
|
|
2356
|
-
value:
|
|
2409
|
+
value: z16.union([z16.string(), z16.number(), z16.boolean(), z16.null()]).describe(
|
|
2357
2410
|
"The value to compare against. For 'is within last' on date fields, pass an integer number of days. For tag filters, pass the tag name (string) or tag id (number). For 'is not' null tests, pass null literally."
|
|
2358
2411
|
)
|
|
2359
2412
|
});
|
|
2360
|
-
var FilterInputSchema =
|
|
2361
|
-
conditions:
|
|
2413
|
+
var FilterInputSchema = z16.object({
|
|
2414
|
+
conditions: z16.array(FilterConditionSchema).min(1).describe(
|
|
2362
2415
|
"Array of filter conditions. All conditions are ANDed together. To get newest records, use a date condition like {field: 'addedOn', operator: 'is within last', value: 7} and pick the highest-id row from the result (Capsule IDs are monotonic)."
|
|
2363
2416
|
),
|
|
2364
|
-
embed:
|
|
2365
|
-
|
|
2366
|
-
perPage: z15.number().int().min(1).max(100).optional().default(25)
|
|
2417
|
+
embed: embedParam(RECORD_EMBEDS),
|
|
2418
|
+
...paginationFields
|
|
2367
2419
|
});
|
|
2368
2420
|
async function runFilter(entityPath, input) {
|
|
2369
2421
|
const { data, nextPage } = await capsuleSearch(
|
|
@@ -2383,10 +2435,7 @@ async function filterParties(input) {
|
|
|
2383
2435
|
}
|
|
2384
2436
|
var filterOpportunitiesSchema = FilterInputSchema;
|
|
2385
2437
|
async function filterOpportunities(input) {
|
|
2386
|
-
return runFilter(
|
|
2387
|
-
"opportunities",
|
|
2388
|
-
input
|
|
2389
|
-
);
|
|
2438
|
+
return runFilter("opportunities", input);
|
|
2390
2439
|
}
|
|
2391
2440
|
var filterProjectsSchema = FilterInputSchema;
|
|
2392
2441
|
async function filterProjects(input) {
|
|
@@ -2394,139 +2443,129 @@ async function filterProjects(input) {
|
|
|
2394
2443
|
}
|
|
2395
2444
|
|
|
2396
2445
|
// src/tools/metadata.ts
|
|
2397
|
-
import { z as
|
|
2398
|
-
var
|
|
2399
|
-
|
|
2400
|
-
perPage:
|
|
2446
|
+
import { z as z17 } from "zod";
|
|
2447
|
+
var paginationFields2 = {
|
|
2448
|
+
...paginationFieldsNoDefaults,
|
|
2449
|
+
perPage: paginationFieldsNoDefaults.perPage.describe(
|
|
2450
|
+
"Page size, max 100. Defaults to 100 for reference data."
|
|
2451
|
+
)
|
|
2401
2452
|
};
|
|
2402
|
-
var listTeamsSchema =
|
|
2453
|
+
var listTeamsSchema = z17.object({ ...paginationFields2 });
|
|
2403
2454
|
async function listTeams(input) {
|
|
2404
|
-
|
|
2455
|
+
return capsuleGetCachedList("/teams", {
|
|
2405
2456
|
page: input.page ?? 1,
|
|
2406
2457
|
perPage: input.perPage ?? 100
|
|
2407
2458
|
});
|
|
2408
|
-
return { ...data, nextPage };
|
|
2409
2459
|
}
|
|
2410
|
-
var listLostReasonsSchema =
|
|
2460
|
+
var listLostReasonsSchema = z17.object({ ...paginationFields2 });
|
|
2411
2461
|
async function listLostReasons(input) {
|
|
2412
|
-
|
|
2462
|
+
return capsuleGetCachedList("/lostreasons", {
|
|
2413
2463
|
page: input.page ?? 1,
|
|
2414
2464
|
perPage: input.perPage ?? 100
|
|
2415
2465
|
});
|
|
2416
|
-
return { ...data, nextPage };
|
|
2417
2466
|
}
|
|
2418
|
-
var listActivityTypesSchema =
|
|
2467
|
+
var listActivityTypesSchema = z17.object({ ...paginationFields2 });
|
|
2419
2468
|
async function listActivityTypes(input) {
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
perPage: input.perPage ?? 100
|
|
2425
|
-
}
|
|
2426
|
-
);
|
|
2427
|
-
return { ...data, nextPage };
|
|
2469
|
+
return capsuleGetCachedList("/activitytypes", {
|
|
2470
|
+
page: input.page ?? 1,
|
|
2471
|
+
perPage: input.perPage ?? 100
|
|
2472
|
+
});
|
|
2428
2473
|
}
|
|
2429
|
-
var getSiteSchema =
|
|
2474
|
+
var getSiteSchema = z17.object({});
|
|
2430
2475
|
async function getSite(_input) {
|
|
2431
2476
|
const { data } = await capsuleGetCached("/site");
|
|
2432
2477
|
return data;
|
|
2433
2478
|
}
|
|
2434
|
-
var listTrackDefinitionsSchema =
|
|
2479
|
+
var listTrackDefinitionsSchema = z17.object({ ...paginationFields2 });
|
|
2435
2480
|
async function listTrackDefinitions(input) {
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
);
|
|
2440
|
-
return { ...data, nextPage };
|
|
2481
|
+
return capsuleGetCachedList("/trackdefinitions", {
|
|
2482
|
+
page: input.page ?? 1,
|
|
2483
|
+
perPage: input.perPage ?? 100
|
|
2484
|
+
});
|
|
2441
2485
|
}
|
|
2442
|
-
var listCategoriesSchema =
|
|
2486
|
+
var listCategoriesSchema = z17.object({ ...paginationFields2 });
|
|
2443
2487
|
async function listCategories(input) {
|
|
2444
|
-
|
|
2488
|
+
return capsuleGetCachedList("/categories", {
|
|
2445
2489
|
page: input.page ?? 1,
|
|
2446
2490
|
perPage: input.perPage ?? 100
|
|
2447
2491
|
});
|
|
2448
|
-
return { ...data, nextPage };
|
|
2449
2492
|
}
|
|
2450
|
-
var listGoalsSchema =
|
|
2493
|
+
var listGoalsSchema = z17.object({ ...paginationFields2 });
|
|
2451
2494
|
async function listGoals(input) {
|
|
2452
|
-
|
|
2495
|
+
return capsuleGetCachedList("/goals", {
|
|
2453
2496
|
page: input.page ?? 1,
|
|
2454
2497
|
perPage: input.perPage ?? 100
|
|
2455
2498
|
});
|
|
2456
|
-
return { ...data, nextPage };
|
|
2457
2499
|
}
|
|
2458
2500
|
|
|
2459
2501
|
// src/tools/audit.ts
|
|
2460
|
-
import { z as
|
|
2461
|
-
var listEmployeesSchema =
|
|
2502
|
+
import { z as z18 } from "zod";
|
|
2503
|
+
var listEmployeesSchema = z18.object({
|
|
2462
2504
|
partyId: positiveId.describe(
|
|
2463
2505
|
"The organisation's party id. Returns the people whose `organisation` field links to this party."
|
|
2464
2506
|
),
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
embed: z17.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
|
|
2507
|
+
...paginationFields,
|
|
2508
|
+
embed: embedParam(RECORD_EMBEDS)
|
|
2468
2509
|
});
|
|
2469
2510
|
async function listEmployees(input) {
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2511
|
+
return capsuleGetList(`/parties/${input.partyId}/people`, {
|
|
2512
|
+
page: input.page,
|
|
2513
|
+
perPage: input.perPage,
|
|
2514
|
+
embed: input.embed
|
|
2515
|
+
});
|
|
2475
2516
|
}
|
|
2476
|
-
var DeletedSinceSchema =
|
|
2517
|
+
var DeletedSinceSchema = z18.string().describe(
|
|
2477
2518
|
"REQUIRED. ISO-8601 timestamp; only deletions on or after this point are returned. Example: '2026-01-01T00:00:00Z'."
|
|
2478
2519
|
);
|
|
2479
2520
|
var DeletedPagination = {
|
|
2480
2521
|
since: DeletedSinceSchema,
|
|
2481
|
-
|
|
2482
|
-
perPage: z17.number().int().min(1).max(100).optional().default(25)
|
|
2522
|
+
...paginationFields
|
|
2483
2523
|
};
|
|
2484
|
-
var listDeletedPartiesSchema =
|
|
2524
|
+
var listDeletedPartiesSchema = z18.object(DeletedPagination);
|
|
2485
2525
|
async function listDeletedParties(input) {
|
|
2486
|
-
|
|
2526
|
+
return capsuleGetList("/parties/deleted", {
|
|
2487
2527
|
since: input.since,
|
|
2488
2528
|
page: input.page,
|
|
2489
2529
|
perPage: input.perPage
|
|
2490
2530
|
});
|
|
2491
|
-
return { ...data, nextPage };
|
|
2492
2531
|
}
|
|
2493
|
-
var listDeletedOpportunitiesSchema =
|
|
2532
|
+
var listDeletedOpportunitiesSchema = z18.object(DeletedPagination);
|
|
2494
2533
|
async function listDeletedOpportunities(input) {
|
|
2495
|
-
|
|
2534
|
+
return capsuleGetList("/opportunities/deleted", {
|
|
2496
2535
|
since: input.since,
|
|
2497
2536
|
page: input.page,
|
|
2498
2537
|
perPage: input.perPage
|
|
2499
2538
|
});
|
|
2500
|
-
return { ...data, nextPage };
|
|
2501
2539
|
}
|
|
2502
|
-
var listDeletedProjectsSchema =
|
|
2540
|
+
var listDeletedProjectsSchema = z18.object(DeletedPagination);
|
|
2503
2541
|
async function listDeletedProjects(input) {
|
|
2504
|
-
|
|
2542
|
+
return capsuleGetList("/kases/deleted", {
|
|
2505
2543
|
since: input.since,
|
|
2506
2544
|
page: input.page,
|
|
2507
2545
|
perPage: input.perPage
|
|
2508
2546
|
});
|
|
2509
|
-
return { ...data, nextPage };
|
|
2510
2547
|
}
|
|
2511
2548
|
|
|
2512
2549
|
// src/tools/relationships.ts
|
|
2513
|
-
import { z as
|
|
2514
|
-
var RelationshipEntity =
|
|
2515
|
-
var listAdditionalPartiesSchema =
|
|
2550
|
+
import { z as z19 } from "zod";
|
|
2551
|
+
var RelationshipEntity = z19.enum(["opportunities", "projects"]).describe("Which entity has the additional-party links.");
|
|
2552
|
+
var listAdditionalPartiesSchema = z19.object({
|
|
2516
2553
|
entity: RelationshipEntity,
|
|
2517
2554
|
entityId: positiveId.describe("ID of the opportunity or project."),
|
|
2518
|
-
embed:
|
|
2519
|
-
|
|
2520
|
-
perPage: z18.number().int().min(1).max(100).optional().default(25)
|
|
2555
|
+
embed: embedParam(RECORD_EMBEDS),
|
|
2556
|
+
...paginationFields
|
|
2521
2557
|
});
|
|
2522
2558
|
async function listAdditionalParties(input) {
|
|
2523
|
-
|
|
2524
|
-
`/${input.entity}/${input.entityId}/parties`,
|
|
2525
|
-
{
|
|
2559
|
+
return capsuleGetList(
|
|
2560
|
+
`/${ENTITY_PATH[input.entity]}/${input.entityId}/parties`,
|
|
2561
|
+
{
|
|
2562
|
+
embed: input.embed,
|
|
2563
|
+
page: input.page,
|
|
2564
|
+
perPage: input.perPage
|
|
2565
|
+
}
|
|
2526
2566
|
);
|
|
2527
|
-
return { ...data, nextPage };
|
|
2528
2567
|
}
|
|
2529
|
-
var addAdditionalPartySchema =
|
|
2568
|
+
var addAdditionalPartySchema = z19.object({
|
|
2530
2569
|
entity: RelationshipEntity,
|
|
2531
2570
|
entityId: positiveId,
|
|
2532
2571
|
partyId: positiveId.describe(
|
|
@@ -2535,7 +2574,9 @@ var addAdditionalPartySchema = z18.object({
|
|
|
2535
2574
|
});
|
|
2536
2575
|
async function addAdditionalParty(input) {
|
|
2537
2576
|
try {
|
|
2538
|
-
await capsulePostNoContent(
|
|
2577
|
+
await capsulePostNoContent(
|
|
2578
|
+
`/${ENTITY_PATH[input.entity]}/${input.entityId}/parties/${input.partyId}`
|
|
2579
|
+
);
|
|
2539
2580
|
return {
|
|
2540
2581
|
linked: true,
|
|
2541
2582
|
alreadyLinked: false,
|
|
@@ -2559,7 +2600,7 @@ async function addAdditionalParty(input) {
|
|
|
2559
2600
|
throw err;
|
|
2560
2601
|
}
|
|
2561
2602
|
}
|
|
2562
|
-
var removeAdditionalPartySchema =
|
|
2603
|
+
var removeAdditionalPartySchema = z19.object({
|
|
2563
2604
|
entity: RelationshipEntity,
|
|
2564
2605
|
entityId: positiveId,
|
|
2565
2606
|
partyId: positiveId,
|
|
@@ -2572,7 +2613,7 @@ async function removeAdditionalParty(input) {
|
|
|
2572
2613
|
throw new Error("remove_additional_party requires confirm: true");
|
|
2573
2614
|
}
|
|
2574
2615
|
return idempotent(
|
|
2575
|
-
() => capsuleDelete(`/${input.entity}/${input.entityId}/parties/${input.partyId}`),
|
|
2616
|
+
() => capsuleDelete(`/${ENTITY_PATH[input.entity]}/${input.entityId}/parties/${input.partyId}`),
|
|
2576
2617
|
() => ({
|
|
2577
2618
|
removed: true,
|
|
2578
2619
|
alreadyRemoved: false,
|
|
@@ -2589,70 +2630,69 @@ async function removeAdditionalParty(input) {
|
|
|
2589
2630
|
})
|
|
2590
2631
|
);
|
|
2591
2632
|
}
|
|
2592
|
-
var listAssociatedProjectsSchema =
|
|
2633
|
+
var listAssociatedProjectsSchema = z19.object({
|
|
2593
2634
|
opportunityId: positiveId,
|
|
2594
|
-
embed:
|
|
2595
|
-
|
|
2596
|
-
perPage: z18.number().int().min(1).max(100).optional().default(25)
|
|
2635
|
+
embed: embedParam(RECORD_EMBEDS),
|
|
2636
|
+
...paginationFields
|
|
2597
2637
|
});
|
|
2598
2638
|
async function listAssociatedProjects(input) {
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2639
|
+
return capsuleGetList(`/opportunities/${input.opportunityId}/kases`, {
|
|
2640
|
+
embed: input.embed,
|
|
2641
|
+
page: input.page,
|
|
2642
|
+
perPage: input.perPage
|
|
2643
|
+
});
|
|
2604
2644
|
}
|
|
2605
2645
|
|
|
2606
2646
|
// src/tools/custom-fields.ts
|
|
2607
|
-
import { z as
|
|
2608
|
-
var CustomFieldEntity =
|
|
2609
|
-
var listCustomFieldsSchema =
|
|
2647
|
+
import { z as z20 } from "zod";
|
|
2648
|
+
var CustomFieldEntity = z20.enum(["parties", "opportunities", "projects"]).describe("Which entity type's custom field schema to inspect.");
|
|
2649
|
+
var listCustomFieldsSchema = z20.object({
|
|
2610
2650
|
entity: CustomFieldEntity
|
|
2611
2651
|
});
|
|
2612
2652
|
async function listCustomFields(input) {
|
|
2613
2653
|
const { data } = await capsuleGetCached(
|
|
2614
|
-
`/${input.entity}/fields/definitions`
|
|
2654
|
+
`/${ENTITY_PATH[input.entity]}/fields/definitions`
|
|
2615
2655
|
);
|
|
2616
2656
|
return data;
|
|
2617
2657
|
}
|
|
2618
|
-
var getCustomFieldSchema =
|
|
2658
|
+
var getCustomFieldSchema = z20.object({
|
|
2619
2659
|
entity: CustomFieldEntity,
|
|
2620
|
-
|
|
2660
|
+
id: positiveId.describe("Custom field definition id.")
|
|
2621
2661
|
});
|
|
2622
2662
|
async function getCustomField(input) {
|
|
2623
2663
|
const { data } = await capsuleGetCached(
|
|
2624
|
-
`/${input.entity}/fields/definitions/${input.
|
|
2664
|
+
`/${input.entity}/fields/definitions/${input.id}`
|
|
2625
2665
|
);
|
|
2626
2666
|
return data;
|
|
2627
2667
|
}
|
|
2628
2668
|
|
|
2629
2669
|
// src/tools/tracks.ts
|
|
2630
|
-
import { z as
|
|
2631
|
-
var TrackEntity =
|
|
2632
|
-
var listEntityTracksSchema =
|
|
2670
|
+
import { z as z21 } from "zod";
|
|
2671
|
+
var TrackEntity = z21.enum(["parties", "opportunities", "projects"]).describe("Which entity type.");
|
|
2672
|
+
var listEntityTracksSchema = z21.object({
|
|
2633
2673
|
entity: TrackEntity,
|
|
2634
2674
|
entityId: positiveId
|
|
2635
2675
|
});
|
|
2636
2676
|
async function listEntityTracks(input) {
|
|
2637
2677
|
const { data } = await capsuleGet(
|
|
2638
|
-
`/${input.entity}/${input.entityId}/tracks`
|
|
2678
|
+
`/${ENTITY_PATH[input.entity]}/${input.entityId}/tracks`
|
|
2639
2679
|
);
|
|
2640
2680
|
return data;
|
|
2641
2681
|
}
|
|
2642
|
-
var
|
|
2643
|
-
|
|
2682
|
+
var getTrackSchema = z21.object({
|
|
2683
|
+
id: positiveId
|
|
2644
2684
|
});
|
|
2645
|
-
async function
|
|
2646
|
-
const { data } = await capsuleGet(`/tracks/${input.
|
|
2685
|
+
async function getTrack(input) {
|
|
2686
|
+
const { data } = await capsuleGet(`/tracks/${input.id}`);
|
|
2647
2687
|
return data;
|
|
2648
2688
|
}
|
|
2649
|
-
var applyTrackSchema =
|
|
2650
|
-
entity:
|
|
2689
|
+
var applyTrackSchema = z21.object({
|
|
2690
|
+
entity: z21.enum(["opportunities", "projects"]).describe("Which entity to apply the track to."),
|
|
2651
2691
|
entityId: positiveId,
|
|
2652
2692
|
trackDefinitionId: positiveId.describe(
|
|
2653
2693
|
"The trackDefinition to apply (from list_track_definitions). Auto-creates task definitions on the target entity per the track's rules."
|
|
2654
2694
|
),
|
|
2655
|
-
startDate:
|
|
2695
|
+
startDate: z21.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe(
|
|
2656
2696
|
"Optional ISO-8601 date (YYYY-MM-DD) the track should start from \u2014 drives task due-date calculations (each task's `dueOn` is computed as startDate + the track-definition's `daysAfter` offset). Defaults to today if omitted. Useful for scheduling a renewal-queue track against a future contract end-date, or backfilling tracks for historical projects."
|
|
2657
2697
|
)
|
|
2658
2698
|
});
|
|
@@ -2665,9 +2705,9 @@ async function applyTrack(input) {
|
|
|
2665
2705
|
if (input.startDate !== void 0) track["trackDateOn"] = input.startDate;
|
|
2666
2706
|
return capsulePost("/tracks", { track });
|
|
2667
2707
|
}
|
|
2668
|
-
var updateTrackSchema =
|
|
2669
|
-
|
|
2670
|
-
fields:
|
|
2708
|
+
var updateTrackSchema = z21.object({
|
|
2709
|
+
id: positiveId,
|
|
2710
|
+
fields: z21.record(z21.string(), z21.unknown()).describe(
|
|
2671
2711
|
"Object of fields to update on the track. Capsule's PUT semantics are partial \u2014 only the fields you provide are changed. Common: { complete: true } to mark a track completed. Capsule rejects unknown keys; consult Capsule's docs for the full updatable set."
|
|
2672
2712
|
)
|
|
2673
2713
|
});
|
|
@@ -2675,12 +2715,12 @@ async function updateTrack(input) {
|
|
|
2675
2715
|
if (Object.keys(input.fields).length === 0) {
|
|
2676
2716
|
throw new Error("update_track: provide at least one field in `fields`");
|
|
2677
2717
|
}
|
|
2678
|
-
return capsulePut(`/tracks/${input.
|
|
2718
|
+
return capsulePut(`/tracks/${input.id}`, {
|
|
2679
2719
|
track: input.fields
|
|
2680
2720
|
});
|
|
2681
2721
|
}
|
|
2682
|
-
var removeTrackSchema =
|
|
2683
|
-
|
|
2722
|
+
var removeTrackSchema = z21.object({
|
|
2723
|
+
id: positiveId,
|
|
2684
2724
|
confirm: confirmFlag().describe(
|
|
2685
2725
|
"Must be set to true. Removes the track instance from its entity. **Capsule also deletes the auto-tasks the track created when it was applied** \u2014 they go with the track and become unreachable (404 on GET /tasks/{id}, gone from list_tasks on the parent entity). If you need any of those tasks to outlive the track, copy their content into fresh tasks (or use the web UI) before calling remove_track."
|
|
2686
2726
|
)
|
|
@@ -2690,20 +2730,20 @@ async function removeTrack(input) {
|
|
|
2690
2730
|
throw new Error("remove_track requires confirm: true");
|
|
2691
2731
|
}
|
|
2692
2732
|
return idempotent(
|
|
2693
|
-
() => capsuleDelete(`/tracks/${input.
|
|
2694
|
-
() => ({ removed: true, alreadyRemoved: false,
|
|
2695
|
-
() => ({ removed: true, alreadyRemoved: true,
|
|
2733
|
+
() => capsuleDelete(`/tracks/${input.id}`),
|
|
2734
|
+
() => ({ removed: true, alreadyRemoved: false, id: input.id }),
|
|
2735
|
+
() => ({ removed: true, alreadyRemoved: true, id: input.id })
|
|
2696
2736
|
);
|
|
2697
2737
|
}
|
|
2698
2738
|
|
|
2699
2739
|
// src/tools/attachments.ts
|
|
2700
|
-
import { z as
|
|
2740
|
+
import { z as z22 } from "zod";
|
|
2701
2741
|
var DEFAULT_MAX_SIZE_BYTES = 5 * 1024 * 1024;
|
|
2702
2742
|
var HARD_MAX_SIZE_BYTES = 25 * 1024 * 1024;
|
|
2703
2743
|
var HARD_MAX_BASE64_CHARS = Math.ceil(HARD_MAX_SIZE_BYTES / 3) * 4;
|
|
2704
|
-
var getAttachmentSchema =
|
|
2744
|
+
var getAttachmentSchema = z22.object({
|
|
2705
2745
|
id: positiveId.describe("Attachment ID."),
|
|
2706
|
-
maxSizeBytes:
|
|
2746
|
+
maxSizeBytes: z22.number().int().positive().max(HARD_MAX_SIZE_BYTES).optional().describe(
|
|
2707
2747
|
`Refuse to return content over this size (default ${DEFAULT_MAX_SIZE_BYTES} bytes \u2248 5MB; max ${HARD_MAX_SIZE_BYTES} bytes \u2248 25MB). Files exceeding the cap return metadata only with a 'truncated: true' flag.`
|
|
2708
2748
|
)
|
|
2709
2749
|
});
|
|
@@ -2718,17 +2758,17 @@ async function getAttachment(input) {
|
|
|
2718
2758
|
}
|
|
2719
2759
|
return { contentType, buffer, sizeBytes };
|
|
2720
2760
|
}
|
|
2721
|
-
var uploadAttachmentSchema =
|
|
2722
|
-
filename:
|
|
2761
|
+
var uploadAttachmentSchema = z22.object({
|
|
2762
|
+
filename: z22.string().min(1).describe(
|
|
2723
2763
|
"Filename Capsule should record (e.g. 'contract.pdf'). Capsule does NOT validate consistency between filename, contentType, and the actual bytes \u2014 a typo in either is accepted and the file is stored as labelled."
|
|
2724
2764
|
),
|
|
2725
|
-
contentType:
|
|
2765
|
+
contentType: z22.string().min(1).describe(
|
|
2726
2766
|
"MIME type of the file (e.g. 'application/pdf', 'image/png', 'text/plain'). Trusted by Capsule verbatim; not cross-checked against `filename` or the actual bytes."
|
|
2727
2767
|
),
|
|
2728
|
-
dataBase64:
|
|
2768
|
+
dataBase64: z22.string().min(1).max(HARD_MAX_BASE64_CHARS).describe(
|
|
2729
2769
|
"File contents, base64-encoded. Decoded server-side and uploaded as the request body. Maximum 25 MB per attachment (Capsule's documented limit); the connector rejects oversized base64 before uploading. The inbound HTTP body limit is ~35 MB which leaves room for the base64 expansion of a 25 MB binary."
|
|
2730
2770
|
),
|
|
2731
|
-
content:
|
|
2771
|
+
content: z22.string().optional().describe(
|
|
2732
2772
|
"Body text for the note that will hold the attachment. Defaults to '[attachment]' if omitted."
|
|
2733
2773
|
),
|
|
2734
2774
|
partyId: positiveId.optional().describe("Link the new note to a party (mutually exclusive with opportunityId / projectId)."),
|
|
@@ -2746,12 +2786,7 @@ function decodedBase64Size(s) {
|
|
|
2746
2786
|
return s.length / 4 * 3 - padding;
|
|
2747
2787
|
}
|
|
2748
2788
|
async function uploadAttachment(input) {
|
|
2749
|
-
|
|
2750
|
-
if (linked.length !== 1) {
|
|
2751
|
-
throw new Error(
|
|
2752
|
-
"upload_attachment: provide exactly one of partyId, opportunityId, or projectId"
|
|
2753
|
-
);
|
|
2754
|
-
}
|
|
2789
|
+
assertSingleParentRef("upload_attachment", input, { required: true });
|
|
2755
2790
|
if (!isValidBase64(input.dataBase64)) {
|
|
2756
2791
|
throw new Error(
|
|
2757
2792
|
"upload_attachment: dataBase64 is not valid base64 \u2014 Node's tolerant decoder would silently produce corrupt bytes. Verify the encoding (RFC 4648, padded with '=' to a multiple of 4 chars)."
|
|
@@ -2776,37 +2811,39 @@ async function uploadAttachment(input) {
|
|
|
2776
2811
|
content: input.content ?? "[attachment]",
|
|
2777
2812
|
attachments: [{ token }]
|
|
2778
2813
|
};
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2814
|
+
setRef(entryBody, "party", input.partyId);
|
|
2815
|
+
setRef(entryBody, "opportunity", input.opportunityId);
|
|
2816
|
+
setRef(entryBody, "kase", input.projectId);
|
|
2782
2817
|
return capsulePost("/entries", { entry: entryBody });
|
|
2783
2818
|
}
|
|
2784
2819
|
|
|
2785
2820
|
// src/tools/saved-filters.ts
|
|
2786
|
-
import { z as
|
|
2787
|
-
var EntitySchema =
|
|
2788
|
-
|
|
2789
|
-
);
|
|
2790
|
-
var listSavedFiltersSchema = z22.object({
|
|
2821
|
+
import { z as z23 } from "zod";
|
|
2822
|
+
var EntitySchema = z23.enum(["parties", "opportunities", "projects"]).describe("Which entity type the filter operates over.");
|
|
2823
|
+
var listSavedFiltersSchema = z23.object({
|
|
2791
2824
|
entity: EntitySchema
|
|
2792
2825
|
});
|
|
2793
2826
|
async function listSavedFilters(input) {
|
|
2794
|
-
const { data } = await capsuleGetCached(
|
|
2827
|
+
const { data } = await capsuleGetCached(
|
|
2828
|
+
`/${ENTITY_PATH[input.entity]}/filters`
|
|
2829
|
+
);
|
|
2795
2830
|
return data;
|
|
2796
2831
|
}
|
|
2797
|
-
var runSavedFilterSchema =
|
|
2832
|
+
var runSavedFilterSchema = z23.object({
|
|
2798
2833
|
entity: EntitySchema,
|
|
2799
2834
|
id: positiveId.describe("The saved filter id (from list_saved_filters)."),
|
|
2800
|
-
embed:
|
|
2801
|
-
|
|
2802
|
-
perPage: z22.number().int().min(1).max(100).optional().default(25)
|
|
2835
|
+
embed: embedParam(RECORD_EMBEDS),
|
|
2836
|
+
...paginationFields
|
|
2803
2837
|
});
|
|
2804
2838
|
async function runSavedFilter(input) {
|
|
2805
|
-
|
|
2806
|
-
`/${input.entity}/filters/${input.id}/results`,
|
|
2807
|
-
{
|
|
2839
|
+
return capsuleGetList(
|
|
2840
|
+
`/${ENTITY_PATH[input.entity]}/filters/${input.id}/results`,
|
|
2841
|
+
{
|
|
2842
|
+
page: input.page,
|
|
2843
|
+
perPage: input.perPage,
|
|
2844
|
+
embed: input.embed
|
|
2845
|
+
}
|
|
2808
2846
|
);
|
|
2809
|
-
return { ...data, nextPage };
|
|
2810
2847
|
}
|
|
2811
2848
|
|
|
2812
2849
|
// src/server.ts
|
|
@@ -2817,7 +2854,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
2817
2854
|
const server2 = new McpServer(
|
|
2818
2855
|
{
|
|
2819
2856
|
name: "capsulemcp",
|
|
2820
|
-
version: "
|
|
2857
|
+
version: "2.0.0",
|
|
2821
2858
|
description: "Read and (optionally) modify Capsule CRM data \u2014 parties, opportunities, projects, tasks, timeline entries, pipelines, tags.",
|
|
2822
2859
|
websiteUrl: "https://github.com/soil-dev/capsulemcp",
|
|
2823
2860
|
icons: ICONS
|
|
@@ -2934,7 +2971,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
2934
2971
|
registerTool(
|
|
2935
2972
|
server2,
|
|
2936
2973
|
"delete_party",
|
|
2937
|
-
"DESTRUCTIVE & IRREVERSIBLE: permanently delete a party (person or organisation). Cascades to all linked notes, tasks, opportunities, AND projects
|
|
2974
|
+
"DESTRUCTIVE & IRREVERSIBLE: permanently delete a party (person or organisation). Cascades to all linked notes, tasks, opportunities, AND projects. Deleting an organisation does NOT delete people linked to it via organisationId \u2014 their `organisation` field is silently cleared to null and they survive as standalone records. TRACK INSTANCES applied to cascaded opportunities/projects are NOT cleaned up either \u2014 they survive as orphan records reachable only by track id via get_track. Use remove_track on each track explicitly before deleting the parent party if orphan accumulation matters (rare in practice \u2014 orphans are unreachable from normal navigation). Requires confirm=true. Always read the party first with get_party and confirm with the user before calling. Idempotent on retry: response is `{deleted: true, alreadyDeleted: false, id}` on a fresh delete or `{deleted: true, alreadyDeleted: true, id}` if the party was already gone (Capsule's 404 is caught internally so reconciliation loops can re-issue safely).",
|
|
2938
2975
|
deletePartySchema,
|
|
2939
2976
|
deleteParty
|
|
2940
2977
|
);
|
|
@@ -3033,7 +3070,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3033
3070
|
registerTool(
|
|
3034
3071
|
server2,
|
|
3035
3072
|
"list_additional_parties",
|
|
3036
|
-
"List secondary party links on an opportunity or project. The 'main' party is on the entity itself (opportunity.party); additional parties are e.g. partners, consultants, or referrers also involved in the deal. Set entity to 'opportunities' or '
|
|
3073
|
+
"List secondary party links on an opportunity or project. The 'main' party is on the entity itself (opportunity.party); additional parties are e.g. partners, consultants, or referrers also involved in the deal. Set entity to 'opportunities' or 'projects'.",
|
|
3037
3074
|
listAdditionalPartiesSchema,
|
|
3038
3075
|
listAdditionalParties
|
|
3039
3076
|
);
|
|
@@ -3074,10 +3111,17 @@ function createCapsuleMcpServer(opts) {
|
|
|
3074
3111
|
deleteOpportunity
|
|
3075
3112
|
);
|
|
3076
3113
|
}
|
|
3114
|
+
registerTool(
|
|
3115
|
+
server2,
|
|
3116
|
+
"search_projects",
|
|
3117
|
+
"Free-text search projects in Capsule CRM (matches name and description). Returns results in Capsule's default order (no sort parameter is supported here). Omit `q` to list all projects. For structured queries \u2014 'most recent project', 'projects opened this month', 'projects tagged X' \u2014 use filter_projects instead.",
|
|
3118
|
+
searchProjectsSchema,
|
|
3119
|
+
searchProjects
|
|
3120
|
+
);
|
|
3077
3121
|
registerTool(
|
|
3078
3122
|
server2,
|
|
3079
3123
|
"list_projects",
|
|
3080
|
-
"List projects (cases) in Capsule CRM, optionally filtered by status. Returns results in Capsule's default order (no sort parameter is supported here). For structured queries \u2014 'most recent project', 'projects opened this month', 'projects tagged X' \u2014 use filter_projects instead.",
|
|
3124
|
+
"List projects (cases) in Capsule CRM, optionally filtered by status. Returns results in Capsule's default order (no sort parameter is supported here). For free-text matching use search_projects; for structured queries \u2014 'most recent project', 'projects opened this month', 'projects tagged X' \u2014 use filter_projects instead.",
|
|
3081
3125
|
listProjectsSchema,
|
|
3082
3126
|
listProjects
|
|
3083
3127
|
);
|
|
@@ -3105,7 +3149,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3105
3149
|
registerTool(
|
|
3106
3150
|
server2,
|
|
3107
3151
|
"list_deleted_projects",
|
|
3108
|
-
"Audit feature: list projects deleted on or after a given timestamp. The `since` parameter is REQUIRED. Response also includes a `
|
|
3152
|
+
"Audit feature: list projects deleted on or after a given timestamp. The `since` parameter is REQUIRED. Response also includes a `restrictedProjects` key for records the integration user can't read fully.",
|
|
3109
3153
|
listDeletedProjectsSchema,
|
|
3110
3154
|
listDeletedProjects
|
|
3111
3155
|
);
|
|
@@ -3267,20 +3311,72 @@ function createCapsuleMcpServer(opts) {
|
|
|
3267
3311
|
listEntriesSchema,
|
|
3268
3312
|
listEntries
|
|
3269
3313
|
);
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3314
|
+
if (shouldRegister("get_attachment")) {
|
|
3315
|
+
server2.tool(
|
|
3316
|
+
"get_attachment",
|
|
3317
|
+
"Download an attachment by id. Returns image content for image/* types (Claude can describe it natively); decoded text for text/* and application/json (small files); JSON metadata + base64 payload for other binary types (PDF, Office docs, etc.). Files exceeding maxSizeBytes (default 5MB) return metadata only with a `truncated: true` flag.",
|
|
3318
|
+
getAttachmentSchema.shape,
|
|
3319
|
+
// get_attachment is read-only — downloads a binary, never mutates.
|
|
3320
|
+
// Mirrors the auto-inferred `{readOnlyHint: true, destructiveHint:
|
|
3321
|
+
// false}` that `registerTool` applies to every other `get_*` tool.
|
|
3322
|
+
// Explicit destructiveHint: false is load-bearing — MCP spec
|
|
3323
|
+
// defaults destructiveHint to `true`, so omitting it would (in
|
|
3324
|
+
// some client implementations) classify this read as destructive.
|
|
3325
|
+
{ readOnlyHint: true, destructiveHint: false },
|
|
3326
|
+
async (input) => {
|
|
3327
|
+
const result = await getAttachment(input);
|
|
3328
|
+
if (result.truncated) {
|
|
3329
|
+
return {
|
|
3330
|
+
content: [
|
|
3331
|
+
{
|
|
3332
|
+
type: "text",
|
|
3333
|
+
text: JSON.stringify(
|
|
3334
|
+
{
|
|
3335
|
+
id: input.id,
|
|
3336
|
+
contentType: result.contentType,
|
|
3337
|
+
sizeBytes: result.sizeBytes,
|
|
3338
|
+
truncated: true,
|
|
3339
|
+
message: `File exceeds the size cap (${input.maxSizeBytes ?? "default"} bytes). Increase maxSizeBytes if you need the bytes; max is 25MB.`
|
|
3340
|
+
},
|
|
3341
|
+
null,
|
|
3342
|
+
2
|
|
3343
|
+
)
|
|
3344
|
+
}
|
|
3345
|
+
]
|
|
3346
|
+
};
|
|
3347
|
+
}
|
|
3348
|
+
const baseType = result.contentType.split(";")[0].trim().toLowerCase();
|
|
3349
|
+
if (baseType.startsWith("image/")) {
|
|
3350
|
+
return {
|
|
3351
|
+
content: [
|
|
3352
|
+
{
|
|
3353
|
+
type: "image",
|
|
3354
|
+
data: result.buffer.toString("base64"),
|
|
3355
|
+
mimeType: result.contentType
|
|
3356
|
+
}
|
|
3357
|
+
]
|
|
3358
|
+
};
|
|
3359
|
+
}
|
|
3360
|
+
const isText = baseType.startsWith("text/") || baseType === "application/json" || baseType === "application/xml";
|
|
3361
|
+
if (isText) {
|
|
3362
|
+
return {
|
|
3363
|
+
content: [
|
|
3364
|
+
{
|
|
3365
|
+
type: "text",
|
|
3366
|
+
text: JSON.stringify(
|
|
3367
|
+
{
|
|
3368
|
+
id: input.id,
|
|
3369
|
+
contentType: result.contentType,
|
|
3370
|
+
sizeBytes: result.sizeBytes
|
|
3371
|
+
},
|
|
3372
|
+
null,
|
|
3373
|
+
2
|
|
3374
|
+
)
|
|
3375
|
+
},
|
|
3376
|
+
{ type: "text", text: result.buffer.toString("utf8") }
|
|
3377
|
+
]
|
|
3378
|
+
};
|
|
3379
|
+
}
|
|
3284
3380
|
return {
|
|
3285
3381
|
content: [
|
|
3286
3382
|
{
|
|
@@ -3290,8 +3386,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3290
3386
|
id: input.id,
|
|
3291
3387
|
contentType: result.contentType,
|
|
3292
3388
|
sizeBytes: result.sizeBytes,
|
|
3293
|
-
|
|
3294
|
-
message: `File exceeds the size cap (${input.maxSizeBytes ?? "default"} bytes). Increase maxSizeBytes if you need the bytes; max is 25MB.`
|
|
3389
|
+
base64: result.buffer.toString("base64")
|
|
3295
3390
|
},
|
|
3296
3391
|
null,
|
|
3297
3392
|
2
|
|
@@ -3300,57 +3395,8 @@ function createCapsuleMcpServer(opts) {
|
|
|
3300
3395
|
]
|
|
3301
3396
|
};
|
|
3302
3397
|
}
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
return {
|
|
3306
|
-
content: [
|
|
3307
|
-
{
|
|
3308
|
-
type: "image",
|
|
3309
|
-
data: result.buffer.toString("base64"),
|
|
3310
|
-
mimeType: result.contentType
|
|
3311
|
-
}
|
|
3312
|
-
]
|
|
3313
|
-
};
|
|
3314
|
-
}
|
|
3315
|
-
const isText = baseType.startsWith("text/") || baseType === "application/json" || baseType === "application/xml";
|
|
3316
|
-
if (isText) {
|
|
3317
|
-
return {
|
|
3318
|
-
content: [
|
|
3319
|
-
{
|
|
3320
|
-
type: "text",
|
|
3321
|
-
text: JSON.stringify(
|
|
3322
|
-
{
|
|
3323
|
-
id: input.id,
|
|
3324
|
-
contentType: result.contentType,
|
|
3325
|
-
sizeBytes: result.sizeBytes
|
|
3326
|
-
},
|
|
3327
|
-
null,
|
|
3328
|
-
2
|
|
3329
|
-
)
|
|
3330
|
-
},
|
|
3331
|
-
{ type: "text", text: result.buffer.toString("utf8") }
|
|
3332
|
-
]
|
|
3333
|
-
};
|
|
3334
|
-
}
|
|
3335
|
-
return {
|
|
3336
|
-
content: [
|
|
3337
|
-
{
|
|
3338
|
-
type: "text",
|
|
3339
|
-
text: JSON.stringify(
|
|
3340
|
-
{
|
|
3341
|
-
id: input.id,
|
|
3342
|
-
contentType: result.contentType,
|
|
3343
|
-
sizeBytes: result.sizeBytes,
|
|
3344
|
-
base64: result.buffer.toString("base64")
|
|
3345
|
-
},
|
|
3346
|
-
null,
|
|
3347
|
-
2
|
|
3348
|
-
)
|
|
3349
|
-
}
|
|
3350
|
-
]
|
|
3351
|
-
};
|
|
3352
|
-
}
|
|
3353
|
-
);
|
|
3398
|
+
);
|
|
3399
|
+
}
|
|
3354
3400
|
if (!readOnly) {
|
|
3355
3401
|
registerTool(
|
|
3356
3402
|
server2,
|
|
@@ -3418,14 +3464,14 @@ function createCapsuleMcpServer(opts) {
|
|
|
3418
3464
|
);
|
|
3419
3465
|
registerTool(
|
|
3420
3466
|
server2,
|
|
3421
|
-
"
|
|
3467
|
+
"list_lost_reasons",
|
|
3422
3468
|
"List all configured opportunity-loss reasons (e.g. 'Poor Qualification', 'Lost to competitor', 'Price too high'). Returns each reason's id and name; the set is account-configured rather than a fixed enum, so call this to discover valid ids before referencing a lostReason in update_opportunity when closing a deal as lost. Useful for analysing closed-lost opportunities by reason.",
|
|
3423
3469
|
listLostReasonsSchema,
|
|
3424
3470
|
listLostReasons
|
|
3425
3471
|
);
|
|
3426
3472
|
registerTool(
|
|
3427
3473
|
server2,
|
|
3428
|
-
"
|
|
3474
|
+
"list_activity_types",
|
|
3429
3475
|
"List all configured activity types (e.g. Call, Meeting, Email). These are the categories used when logging timeline entries via add_note. Returns each type's id and name. The set is account-configured rather than a fixed enum, so call this to discover valid values before referencing an activityType in entry creation.",
|
|
3430
3476
|
listActivityTypesSchema,
|
|
3431
3477
|
listActivityTypes
|
|
@@ -3453,10 +3499,10 @@ function createCapsuleMcpServer(opts) {
|
|
|
3453
3499
|
);
|
|
3454
3500
|
registerTool(
|
|
3455
3501
|
server2,
|
|
3456
|
-
"
|
|
3502
|
+
"get_track",
|
|
3457
3503
|
"Fetch a single track instance by id. Returns the minimal Capsule projection: id, description, trackDateOn, direction, and the array of tasks attached to the track. Capsule's GET /tracks/{id} does NOT include a trackDefinition link, an entity reference, or a completion field \u2014 to find the entity a track is applied to, use list_entity_tracks (which lists track instances by their parent entity); to check completion, the track-tasks' own statuses are the proxy.",
|
|
3458
|
-
|
|
3459
|
-
|
|
3504
|
+
getTrackSchema,
|
|
3505
|
+
getTrack
|
|
3460
3506
|
);
|
|
3461
3507
|
registerTool(
|
|
3462
3508
|
server2,
|
|
@@ -3489,7 +3535,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3489
3535
|
registerTool(
|
|
3490
3536
|
server2,
|
|
3491
3537
|
"list_tags",
|
|
3492
|
-
"List all tags available for a given entity type (parties, opportunities, or
|
|
3538
|
+
"List all tags available for a given entity type (parties, opportunities, or projects). Returns each tag's id, name, and any data-tag field schema. Tags are entity-specific \u2014 a party tag is not interchangeable with an opportunity tag. Use this to discover valid tag ids before calling add_tag, or to display the tag catalogue to the user when they ask 'what tags do we use?'",
|
|
3493
3539
|
listTagsSchema,
|
|
3494
3540
|
listTags
|
|
3495
3541
|
);
|
|
@@ -3511,7 +3557,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3511
3557
|
registerTool(
|
|
3512
3558
|
server2,
|
|
3513
3559
|
"delete_tag_definition",
|
|
3514
|
-
"DESTRUCTIVE & TENANT-WIDE: permanently delete a tag DEFINITION from an entity type's tag namespace (parties / opportunities /
|
|
3560
|
+
"DESTRUCTIVE & TENANT-WIDE: permanently delete a tag DEFINITION from an entity type's tag namespace (parties / opportunities / projects). Unlike remove_tag_by_id \u2014 which detaches a tag from ONE record and leaves the definition intact for others \u2014 this removes the definition itself, so the tag disappears from EVERY record that shared it. Use it to clean up stray / mistyped / test tag definitions polluting the tenant-global list. Requires confirm=true. Always read the affected tag first via list_tags and confirm with the user; if you only want to untag one record, use remove_tag_by_id instead. Irreversible (re-creating by name via add_tag mints a brand-new id). Idempotent on retry: `{deleted: true, alreadyDeleted: false, entity, tagId}` on a fresh delete, or `{deleted: true, alreadyDeleted: true, entity, tagId}` if the definition was already gone (Capsule's 404 is caught). Endpoint verified empirically (DELETE /<entity>/tags/{id} \u2192 204).",
|
|
3515
3561
|
deleteTagDefinitionSchema,
|
|
3516
3562
|
deleteTagDefinition
|
|
3517
3563
|
);
|
|
@@ -3560,6 +3606,22 @@ var transport = new StdioServerTransport();
|
|
|
3560
3606
|
if (isReadOnly()) {
|
|
3561
3607
|
console.error("[capsulemcp] read-only mode: write/delete tools are not registered");
|
|
3562
3608
|
}
|
|
3609
|
+
function exitOnDisconnect() {
|
|
3610
|
+
let exiting = false;
|
|
3611
|
+
const die = () => {
|
|
3612
|
+
if (exiting) return;
|
|
3613
|
+
exiting = true;
|
|
3614
|
+
process.exit(0);
|
|
3615
|
+
};
|
|
3616
|
+
process.stdin.on("end", die);
|
|
3617
|
+
process.stdin.on("close", die);
|
|
3618
|
+
process.stdin.on("error", die);
|
|
3619
|
+
process.stdout.on("error", die);
|
|
3620
|
+
const orphanCheck = setInterval(() => {
|
|
3621
|
+
if (process.ppid === 1) die();
|
|
3622
|
+
}, 3e4);
|
|
3623
|
+
orphanCheck.unref?.();
|
|
3624
|
+
}
|
|
3563
3625
|
try {
|
|
3564
3626
|
await server.connect(transport);
|
|
3565
3627
|
} catch (err) {
|
|
@@ -3567,3 +3629,4 @@ try {
|
|
|
3567
3629
|
console.error(`[capsulemcp] Failed to start: ${message}`);
|
|
3568
3630
|
process.exit(1);
|
|
3569
3631
|
}
|
|
3632
|
+
exitOnDisconnect();
|