capsulemcp 1.7.0 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/http.js +854 -819
- package/dist/index.js +837 -758
- package/package.json +8 -9
package/dist/index.js
CHANGED
|
@@ -31,6 +31,22 @@ var chainHandlers = {
|
|
|
31
31
|
"capsule.request": (ctx) => {
|
|
32
32
|
ctx.capsuleCalls += 1;
|
|
33
33
|
},
|
|
34
|
+
// A timed-out or connection-failed call is still an attempt that
|
|
35
|
+
// never reaches the `capsule.request` emit (it throws at the fetch
|
|
36
|
+
// stage). Count it here so `tool.chain.capsuleCalls` stays honest and
|
|
37
|
+
// a chain whose duration ballooned is explained by a visible failure.
|
|
38
|
+
"capsule.timeout": (ctx) => {
|
|
39
|
+
ctx.capsuleCalls += 1;
|
|
40
|
+
},
|
|
41
|
+
"capsule.error": (ctx) => {
|
|
42
|
+
ctx.capsuleCalls += 1;
|
|
43
|
+
},
|
|
44
|
+
// A request that exhausted its 429 retry is a real (doubly-attempted)
|
|
45
|
+
// outbound call that throws before `capsule.request` fires — count it
|
|
46
|
+
// so a chain whose latency ballooned on rate-limit backoff is explained.
|
|
47
|
+
"capsule.ratelimit": (ctx) => {
|
|
48
|
+
ctx.capsuleCalls += 1;
|
|
49
|
+
},
|
|
34
50
|
// Cache-hit events feed the aggregate so the chain stat is right
|
|
35
51
|
// even on tools whose Capsule calls all hit the cache.
|
|
36
52
|
"cache.hit": (ctx) => {
|
|
@@ -166,6 +182,15 @@ var CapsuleApiError = class extends Error {
|
|
|
166
182
|
}
|
|
167
183
|
status;
|
|
168
184
|
};
|
|
185
|
+
var CapsuleTimeoutError = class extends CapsuleApiError {
|
|
186
|
+
constructor() {
|
|
187
|
+
super(
|
|
188
|
+
504,
|
|
189
|
+
`Capsule API request timed out after ${REQUEST_TIMEOUT_MS / 1e3}s. The Capsule API may be slow or hung; retry after a short wait. If the failed call was a write/delete, read the entity first to see whether the change actually applied before retrying.`
|
|
190
|
+
);
|
|
191
|
+
this.name = "CapsuleTimeoutError";
|
|
192
|
+
}
|
|
193
|
+
};
|
|
169
194
|
function getToken() {
|
|
170
195
|
const token = process.env["CAPSULE_API_TOKEN"];
|
|
171
196
|
if (!token) {
|
|
@@ -228,72 +253,74 @@ async function parseErrorBody(res) {
|
|
|
228
253
|
}
|
|
229
254
|
}
|
|
230
255
|
var REQUEST_TIMEOUT_MS = 6e4;
|
|
231
|
-
function
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const controller = new AbortController();
|
|
237
|
-
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
238
|
-
timer.unref();
|
|
239
|
-
return {
|
|
240
|
-
options: { ...options ?? {}, signal: controller.signal },
|
|
241
|
-
cleanup: () => clearTimeout(timer)
|
|
242
|
-
};
|
|
256
|
+
function isTimeoutAbort(err) {
|
|
257
|
+
return err instanceof Error && // AbortSignal.timeout rejects with a DOMException named
|
|
258
|
+
// "TimeoutError"; plain aborts (and older undici paths) surface
|
|
259
|
+
// as "AbortError" or carry "aborted" in the message.
|
|
260
|
+
(err.name === "TimeoutError" || err.name === "AbortError" || /aborted/i.test(err.message));
|
|
243
261
|
}
|
|
244
262
|
async function mapAbort(p) {
|
|
245
263
|
try {
|
|
246
264
|
return await p;
|
|
247
265
|
} catch (err) {
|
|
248
|
-
if (
|
|
249
|
-
throw new
|
|
250
|
-
504,
|
|
251
|
-
`Capsule API request timed out after ${REQUEST_TIMEOUT_MS / 1e3}s. The Capsule API may be slow or hung; retry after a short wait. If the failed call was a write/delete, read the entity first to see whether the change actually applied before retrying.`
|
|
252
|
-
);
|
|
266
|
+
if (isTimeoutAbort(err)) {
|
|
267
|
+
throw new CapsuleTimeoutError();
|
|
253
268
|
}
|
|
254
269
|
throw err;
|
|
255
270
|
}
|
|
256
271
|
}
|
|
257
272
|
async function fetchWithTimeout(url, options) {
|
|
258
|
-
const
|
|
273
|
+
const startedAt = Date.now();
|
|
259
274
|
try {
|
|
260
|
-
|
|
261
|
-
|
|
275
|
+
return await fetch(url, {
|
|
276
|
+
...options ?? {},
|
|
277
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
278
|
+
});
|
|
262
279
|
} catch (err) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
280
|
+
const isAbort = isTimeoutAbort(err);
|
|
281
|
+
emitCapsuleFailure(
|
|
282
|
+
options?.method ?? "GET",
|
|
283
|
+
url,
|
|
284
|
+
Date.now() - startedAt,
|
|
285
|
+
isAbort ? "timeout" : "network",
|
|
286
|
+
isAbort ? void 0 : err
|
|
287
|
+
);
|
|
288
|
+
if (isAbort) {
|
|
289
|
+
throw new CapsuleTimeoutError();
|
|
269
290
|
}
|
|
270
291
|
throw err;
|
|
271
292
|
}
|
|
272
293
|
}
|
|
294
|
+
async function drainBody(res) {
|
|
295
|
+
try {
|
|
296
|
+
await res.body?.cancel();
|
|
297
|
+
} catch {
|
|
298
|
+
}
|
|
299
|
+
}
|
|
273
300
|
async function doFetch(url, options) {
|
|
274
301
|
const startedAt = Date.now();
|
|
275
302
|
const method = options?.method ?? "GET";
|
|
276
303
|
const first = await fetchWithTimeout(url, options);
|
|
277
|
-
if (first.
|
|
278
|
-
const delay = parseRateLimitDelay(first
|
|
279
|
-
first
|
|
304
|
+
if (first.status === 429) {
|
|
305
|
+
const delay = parseRateLimitDelay(first);
|
|
306
|
+
await drainBody(first);
|
|
280
307
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
281
308
|
const retried = await fetchWithTimeout(url, options);
|
|
282
|
-
if (retried.
|
|
283
|
-
retried
|
|
309
|
+
if (retried.status === 429) {
|
|
310
|
+
await drainBody(retried);
|
|
311
|
+
emitCapsuleRateLimited(method, url, Date.now() - startedAt);
|
|
284
312
|
throw new CapsuleApiError(
|
|
285
313
|
429,
|
|
286
314
|
"Rate limit exceeded after one retry. Please slow down your requests."
|
|
287
315
|
);
|
|
288
316
|
}
|
|
289
|
-
return {
|
|
317
|
+
return { res: retried, startedAt, method, url, retriedAfter429: true };
|
|
290
318
|
}
|
|
291
|
-
return {
|
|
319
|
+
return { res: first, startedAt, method, url, retriedAfter429: false };
|
|
292
320
|
}
|
|
293
321
|
async function consumeBody(start, body) {
|
|
294
322
|
try {
|
|
295
|
-
|
|
296
|
-
} finally {
|
|
323
|
+
const result = await body();
|
|
297
324
|
emitCapsuleRequest(
|
|
298
325
|
start.method,
|
|
299
326
|
start.url,
|
|
@@ -301,15 +328,31 @@ async function consumeBody(start, body) {
|
|
|
301
328
|
Date.now() - start.startedAt,
|
|
302
329
|
start.retriedAfter429
|
|
303
330
|
);
|
|
331
|
+
return result;
|
|
332
|
+
} catch (err) {
|
|
333
|
+
if (err instanceof CapsuleTimeoutError) {
|
|
334
|
+
emitCapsuleFailure(start.method, start.url, Date.now() - start.startedAt, "timeout");
|
|
335
|
+
} else {
|
|
336
|
+
emitCapsuleRequest(
|
|
337
|
+
start.method,
|
|
338
|
+
start.url,
|
|
339
|
+
start.res,
|
|
340
|
+
Date.now() - start.startedAt,
|
|
341
|
+
start.retriedAfter429
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
throw err;
|
|
304
345
|
}
|
|
305
346
|
}
|
|
306
|
-
function
|
|
307
|
-
let path = "";
|
|
347
|
+
function redactedPath(url) {
|
|
308
348
|
try {
|
|
309
|
-
|
|
349
|
+
return redactPath(new URL(url).pathname);
|
|
310
350
|
} catch {
|
|
311
|
-
|
|
351
|
+
return "?";
|
|
312
352
|
}
|
|
353
|
+
}
|
|
354
|
+
function emitCapsuleRequest(method, url, res, durationMs, retriedAfter429) {
|
|
355
|
+
const path = redactedPath(url);
|
|
313
356
|
const lenHeader = res.headers.get("content-length");
|
|
314
357
|
const responseBytes = lenHeader ? Number.parseInt(lenHeader, 10) : 0;
|
|
315
358
|
logEvent("capsule.request", {
|
|
@@ -321,6 +364,37 @@ function emitCapsuleRequest(method, url, res, durationMs, retriedAfter429) {
|
|
|
321
364
|
...retriedAfter429 ? { retriedAfter429: true } : {}
|
|
322
365
|
});
|
|
323
366
|
}
|
|
367
|
+
function emitCapsuleFailure(method, url, elapsedMs, reason, err) {
|
|
368
|
+
const path = redactedPath(url);
|
|
369
|
+
if (reason === "timeout") {
|
|
370
|
+
logEvent(
|
|
371
|
+
"capsule.timeout",
|
|
372
|
+
{ method, path, elapsedMs, timeoutMs: REQUEST_TIMEOUT_MS },
|
|
373
|
+
{ force: true }
|
|
374
|
+
);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const code = extractErrorCode(err);
|
|
378
|
+
logEvent(
|
|
379
|
+
"capsule.error",
|
|
380
|
+
{ method, path, elapsedMs, ...code ? { code } : {} },
|
|
381
|
+
{ force: true }
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
function emitCapsuleRateLimited(method, url, elapsedMs) {
|
|
385
|
+
logEvent(
|
|
386
|
+
"capsule.ratelimit",
|
|
387
|
+
{ method, path: redactedPath(url), elapsedMs, status: 429 },
|
|
388
|
+
{ force: true }
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
function extractErrorCode(err) {
|
|
392
|
+
const e = err;
|
|
393
|
+
const code = e?.cause?.code ?? e?.code;
|
|
394
|
+
if (typeof code === "string") return code;
|
|
395
|
+
if (typeof e?.name === "string" && e.name !== "Error") return e.name;
|
|
396
|
+
return void 0;
|
|
397
|
+
}
|
|
324
398
|
async function throwForStatus(res) {
|
|
325
399
|
if (res.status === 401) {
|
|
326
400
|
const detail = await parseErrorBody(res);
|
|
@@ -352,15 +426,19 @@ async function capsuleGet(path, params) {
|
|
|
352
426
|
const token = getToken();
|
|
353
427
|
const url = buildUrl(path, params);
|
|
354
428
|
const start = await doFetch(url, { headers: baseHeaders(token) });
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
429
|
+
return consumeBody(start, async () => {
|
|
430
|
+
const data = await handleResponse(start.res);
|
|
431
|
+
const nextPage = parseNextPage(start.res.headers.get("Link"));
|
|
432
|
+
return { data, nextPage };
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
async function capsuleGetList(path, params) {
|
|
436
|
+
const { data, nextPage } = await capsuleGet(path, params);
|
|
437
|
+
return { ...data, nextPage };
|
|
438
|
+
}
|
|
439
|
+
async function capsuleGetCachedList(path, params) {
|
|
440
|
+
const { data, nextPage } = await capsuleGetCached(path, params);
|
|
441
|
+
return { ...data, nextPage };
|
|
364
442
|
}
|
|
365
443
|
async function capsuleGetCached(path, params) {
|
|
366
444
|
if (cacheDisabled()) return capsuleGet(path, params);
|
|
@@ -399,11 +477,7 @@ async function capsulePost(path, body) {
|
|
|
399
477
|
headers: { ...baseHeaders(token), "Content-Type": "application/json" },
|
|
400
478
|
body: JSON.stringify(body)
|
|
401
479
|
});
|
|
402
|
-
|
|
403
|
-
return await consumeBody(start, () => handleResponse(start.res));
|
|
404
|
-
} finally {
|
|
405
|
-
start.cleanup();
|
|
406
|
-
}
|
|
480
|
+
return consumeBody(start, () => handleResponse(start.res));
|
|
407
481
|
}
|
|
408
482
|
async function capsulePostNoContent(path) {
|
|
409
483
|
if (isReadOnly()) throw new CapsuleReadOnlyError("POST");
|
|
@@ -413,15 +487,11 @@ async function capsulePostNoContent(path) {
|
|
|
413
487
|
method: "POST",
|
|
414
488
|
headers: baseHeaders(token)
|
|
415
489
|
});
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
});
|
|
422
|
-
} finally {
|
|
423
|
-
start.cleanup();
|
|
424
|
-
}
|
|
490
|
+
await consumeBody(start, async () => {
|
|
491
|
+
if (start.res.status === 204) return;
|
|
492
|
+
await throwForStatus(start.res);
|
|
493
|
+
await mapAbort(start.res.text());
|
|
494
|
+
});
|
|
425
495
|
}
|
|
426
496
|
async function capsuleSearch(path, body, params) {
|
|
427
497
|
const token = getToken();
|
|
@@ -431,15 +501,11 @@ async function capsuleSearch(path, body, params) {
|
|
|
431
501
|
headers: { ...baseHeaders(token), "Content-Type": "application/json" },
|
|
432
502
|
body: JSON.stringify(body)
|
|
433
503
|
});
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
});
|
|
440
|
-
} finally {
|
|
441
|
-
start.cleanup();
|
|
442
|
-
}
|
|
504
|
+
return consumeBody(start, async () => {
|
|
505
|
+
const data = await handleResponse(start.res);
|
|
506
|
+
const nextPage = parseNextPage(start.res.headers.get("Link"));
|
|
507
|
+
return { data, nextPage };
|
|
508
|
+
});
|
|
443
509
|
}
|
|
444
510
|
async function capsulePut(path, body) {
|
|
445
511
|
if (isReadOnly()) throw new CapsuleReadOnlyError("PUT");
|
|
@@ -450,68 +516,60 @@ async function capsulePut(path, body) {
|
|
|
450
516
|
headers: { ...baseHeaders(token), "Content-Type": "application/json" },
|
|
451
517
|
body: JSON.stringify(body)
|
|
452
518
|
});
|
|
453
|
-
|
|
454
|
-
return await consumeBody(start, () => handleResponse(start.res));
|
|
455
|
-
} finally {
|
|
456
|
-
start.cleanup();
|
|
457
|
-
}
|
|
519
|
+
return consumeBody(start, () => handleResponse(start.res));
|
|
458
520
|
}
|
|
459
521
|
async function capsuleGetBinary(path, maxBytes) {
|
|
460
522
|
const token = getToken();
|
|
461
523
|
const url = buildUrl(path);
|
|
462
524
|
const start = await doFetch(url, { headers: baseHeaders(token) });
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
if (
|
|
471
|
-
|
|
472
|
-
|
|
525
|
+
return consumeBody(start, async () => {
|
|
526
|
+
const res = start.res;
|
|
527
|
+
await throwForStatus(res);
|
|
528
|
+
const contentType = res.headers.get("Content-Type") ?? "application/octet-stream";
|
|
529
|
+
const declared = res.headers.get("Content-Length");
|
|
530
|
+
const declaredBytes = declared ? Number(declared) : NaN;
|
|
531
|
+
if (maxBytes !== void 0 && Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {
|
|
532
|
+
if (res.body) await res.body.cancel().catch(() => {
|
|
533
|
+
});
|
|
534
|
+
return {
|
|
535
|
+
contentType,
|
|
536
|
+
buffer: Buffer.alloc(0),
|
|
537
|
+
truncated: true,
|
|
538
|
+
sizeBytes: declaredBytes
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
if (maxBytes !== void 0 && res.body) {
|
|
542
|
+
const reader = res.body.getReader();
|
|
543
|
+
const chunks = [];
|
|
544
|
+
let total = 0;
|
|
545
|
+
let truncated = false;
|
|
546
|
+
while (true) {
|
|
547
|
+
const { done, value } = await mapAbort(reader.read());
|
|
548
|
+
if (done) break;
|
|
549
|
+
total += value.byteLength;
|
|
550
|
+
if (total > maxBytes) {
|
|
551
|
+
truncated = true;
|
|
552
|
+
await reader.cancel().catch(() => {
|
|
553
|
+
});
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
chunks.push(value);
|
|
557
|
+
}
|
|
558
|
+
if (truncated) {
|
|
473
559
|
return {
|
|
474
560
|
contentType,
|
|
475
561
|
buffer: Buffer.alloc(0),
|
|
476
562
|
truncated: true,
|
|
477
|
-
sizeBytes:
|
|
563
|
+
sizeBytes: total
|
|
478
564
|
};
|
|
479
565
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
if (done) break;
|
|
488
|
-
total += value.byteLength;
|
|
489
|
-
if (total > maxBytes) {
|
|
490
|
-
truncated = true;
|
|
491
|
-
await reader.cancel().catch(() => {
|
|
492
|
-
});
|
|
493
|
-
break;
|
|
494
|
-
}
|
|
495
|
-
chunks.push(value);
|
|
496
|
-
}
|
|
497
|
-
if (truncated) {
|
|
498
|
-
return {
|
|
499
|
-
contentType,
|
|
500
|
-
buffer: Buffer.alloc(0),
|
|
501
|
-
truncated: true,
|
|
502
|
-
sizeBytes: total
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
const buffer2 = Buffer.concat(chunks.map((c) => Buffer.from(c)));
|
|
506
|
-
return { contentType, buffer: buffer2, sizeBytes: buffer2.length };
|
|
507
|
-
}
|
|
508
|
-
const arrayBuffer = await mapAbort(res.arrayBuffer());
|
|
509
|
-
const buffer = Buffer.from(arrayBuffer);
|
|
510
|
-
return { contentType, buffer, sizeBytes: buffer.length };
|
|
511
|
-
});
|
|
512
|
-
} finally {
|
|
513
|
-
start.cleanup();
|
|
514
|
-
}
|
|
566
|
+
const buffer2 = Buffer.concat(chunks.map((c) => Buffer.from(c)));
|
|
567
|
+
return { contentType, buffer: buffer2, sizeBytes: buffer2.length };
|
|
568
|
+
}
|
|
569
|
+
const arrayBuffer = await mapAbort(res.arrayBuffer());
|
|
570
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
571
|
+
return { contentType, buffer, sizeBytes: buffer.length };
|
|
572
|
+
});
|
|
515
573
|
}
|
|
516
574
|
async function capsulePostBinary(path, body, contentType, filename) {
|
|
517
575
|
if (isReadOnly()) throw new CapsuleReadOnlyError("POST");
|
|
@@ -527,11 +585,7 @@ async function capsulePostBinary(path, body, contentType, filename) {
|
|
|
527
585
|
},
|
|
528
586
|
body
|
|
529
587
|
});
|
|
530
|
-
|
|
531
|
-
return await consumeBody(start, () => handleResponse(start.res));
|
|
532
|
-
} finally {
|
|
533
|
-
start.cleanup();
|
|
534
|
-
}
|
|
588
|
+
return consumeBody(start, () => handleResponse(start.res));
|
|
535
589
|
}
|
|
536
590
|
async function capsuleDelete(path) {
|
|
537
591
|
if (isReadOnly()) throw new CapsuleReadOnlyError("DELETE");
|
|
@@ -541,15 +595,11 @@ async function capsuleDelete(path) {
|
|
|
541
595
|
method: "DELETE",
|
|
542
596
|
headers: baseHeaders(token)
|
|
543
597
|
});
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
});
|
|
550
|
-
} finally {
|
|
551
|
-
start.cleanup();
|
|
552
|
-
}
|
|
598
|
+
await consumeBody(start, async () => {
|
|
599
|
+
if (start.res.status === 204) return;
|
|
600
|
+
await throwForStatus(start.res);
|
|
601
|
+
await mapAbort(start.res.text());
|
|
602
|
+
});
|
|
553
603
|
}
|
|
554
604
|
|
|
555
605
|
// src/server.ts
|
|
@@ -585,6 +635,44 @@ var ICONS = [
|
|
|
585
635
|
}
|
|
586
636
|
];
|
|
587
637
|
|
|
638
|
+
// src/server/tier.ts
|
|
639
|
+
var CORE_TOOLS = /* @__PURE__ */ new Set([
|
|
640
|
+
// Parties
|
|
641
|
+
"search_parties",
|
|
642
|
+
"filter_parties",
|
|
643
|
+
"get_party",
|
|
644
|
+
"create_party",
|
|
645
|
+
"update_party",
|
|
646
|
+
"list_party_entries",
|
|
647
|
+
// Opportunities
|
|
648
|
+
"search_opportunities",
|
|
649
|
+
"filter_opportunities",
|
|
650
|
+
"get_opportunity",
|
|
651
|
+
"create_opportunity",
|
|
652
|
+
"update_opportunity",
|
|
653
|
+
// Projects
|
|
654
|
+
"filter_projects",
|
|
655
|
+
"list_projects",
|
|
656
|
+
"get_project",
|
|
657
|
+
"create_project",
|
|
658
|
+
"update_project",
|
|
659
|
+
// Tasks
|
|
660
|
+
"list_tasks",
|
|
661
|
+
"get_task",
|
|
662
|
+
"create_task",
|
|
663
|
+
"update_task",
|
|
664
|
+
"complete_task",
|
|
665
|
+
// Timeline + tags + identity
|
|
666
|
+
"add_note",
|
|
667
|
+
"list_tags",
|
|
668
|
+
"add_tag",
|
|
669
|
+
"get_current_user"
|
|
670
|
+
]);
|
|
671
|
+
function shouldRegister(name) {
|
|
672
|
+
if (process.env["CAPSULE_MCP_TIER"] !== "core") return true;
|
|
673
|
+
return CORE_TOOLS.has(name);
|
|
674
|
+
}
|
|
675
|
+
|
|
588
676
|
// src/tasks/store.ts
|
|
589
677
|
import { InMemoryTaskStore } from "@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js";
|
|
590
678
|
import {
|
|
@@ -639,6 +727,22 @@ var abortControllers = /* @__PURE__ */ new Map();
|
|
|
639
727
|
function registerAbortController(taskId, controller) {
|
|
640
728
|
abortControllers.set(taskId, controller);
|
|
641
729
|
}
|
|
730
|
+
var evictionTimers = /* @__PURE__ */ new Map();
|
|
731
|
+
var taskTtls = /* @__PURE__ */ new Map();
|
|
732
|
+
function scheduleEviction(taskId, clientId, ttlMs) {
|
|
733
|
+
const existing = evictionTimers.get(taskId);
|
|
734
|
+
if (existing) clearTimeout(existing);
|
|
735
|
+
taskTtls.set(taskId, ttlMs);
|
|
736
|
+
const timer = setTimeout(() => {
|
|
737
|
+
owners.delete(taskId);
|
|
738
|
+
abortControllers.delete(taskId);
|
|
739
|
+
evictionTimers.delete(taskId);
|
|
740
|
+
taskTtls.delete(taskId);
|
|
741
|
+
logEvent("task.evicted", { taskId, clientId, reason: "ttl" });
|
|
742
|
+
}, ttlMs);
|
|
743
|
+
timer.unref?.();
|
|
744
|
+
evictionTimers.set(taskId, timer);
|
|
745
|
+
}
|
|
642
746
|
function countPerClient(clientId) {
|
|
643
747
|
let n = 0;
|
|
644
748
|
for (const owner of owners.values()) {
|
|
@@ -692,12 +796,7 @@ function createScopedTaskStore(clientId) {
|
|
|
692
796
|
sessionId
|
|
693
797
|
);
|
|
694
798
|
owners.set(task.taskId, clientId);
|
|
695
|
-
|
|
696
|
-
owners.delete(task.taskId);
|
|
697
|
-
abortControllers.delete(task.taskId);
|
|
698
|
-
logEvent("task.evicted", { taskId: task.taskId, clientId, reason: "ttl" });
|
|
699
|
-
}, clampedTtl);
|
|
700
|
-
timer.unref?.();
|
|
799
|
+
scheduleEviction(task.taskId, clientId, clampedTtl);
|
|
701
800
|
logEvent("task.created", {
|
|
702
801
|
taskId: task.taskId,
|
|
703
802
|
clientId,
|
|
@@ -716,6 +815,7 @@ function createScopedTaskStore(clientId) {
|
|
|
716
815
|
}
|
|
717
816
|
logEvent("task.transition", { taskId, clientId, status });
|
|
718
817
|
await global.storeTaskResult(taskId, status, result, sessionId);
|
|
818
|
+
scheduleEviction(taskId, clientId, taskTtls.get(taskId) ?? getTasksConfig().defaultTtlMs);
|
|
719
819
|
},
|
|
720
820
|
async getTaskResult(taskId, sessionId) {
|
|
721
821
|
if (owners.get(taskId) !== clientId) {
|
|
@@ -735,6 +835,7 @@ function createScopedTaskStore(clientId) {
|
|
|
735
835
|
}
|
|
736
836
|
if (status === "completed" || status === "failed" || status === "cancelled") {
|
|
737
837
|
abortControllers.delete(taskId);
|
|
838
|
+
scheduleEviction(taskId, clientId, taskTtls.get(taskId) ?? getTasksConfig().defaultTtlMs);
|
|
738
839
|
}
|
|
739
840
|
},
|
|
740
841
|
async listTasks(cursor, sessionId) {
|
|
@@ -780,27 +881,25 @@ function wrapAsText(result) {
|
|
|
780
881
|
};
|
|
781
882
|
}
|
|
782
883
|
function registerTool(server2, name, description, schema, handler) {
|
|
884
|
+
if (!shouldRegister(name)) return;
|
|
783
885
|
const registerWithSchema = server2.registerTool.bind(server2);
|
|
784
886
|
const annotations = inferAnnotations(name);
|
|
785
|
-
registerWithSchema(
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
const
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
} catch (err) {
|
|
797
|
-
emitToolCall({ tool: name, clientId, argFields, startedAt, outcome: "error" });
|
|
798
|
-
throw err;
|
|
799
|
-
}
|
|
887
|
+
registerWithSchema(name, { description, inputSchema: schema, annotations }, async (input) => {
|
|
888
|
+
const startedAt = Date.now();
|
|
889
|
+
const argFields = argFieldNames(input);
|
|
890
|
+
const clientId = getRequestContext()?.clientId;
|
|
891
|
+
try {
|
|
892
|
+
const result = await handler(input);
|
|
893
|
+
emitToolCall({ tool: name, clientId, argFields, startedAt, outcome: "success" });
|
|
894
|
+
return wrapAsText(result);
|
|
895
|
+
} catch (err) {
|
|
896
|
+
emitToolCall({ tool: name, clientId, argFields, startedAt, outcome: "error" });
|
|
897
|
+
throw err;
|
|
800
898
|
}
|
|
801
|
-
);
|
|
899
|
+
});
|
|
802
900
|
}
|
|
803
901
|
function registerToolTask(server2, name, description, schema, handler) {
|
|
902
|
+
if (!shouldRegister(name)) return;
|
|
804
903
|
const registerWithSchema = server2.experimental.tasks.registerToolTask.bind(
|
|
805
904
|
server2.experimental.tasks
|
|
806
905
|
);
|
|
@@ -811,7 +910,7 @@ function registerToolTask(server2, name, description, schema, handler) {
|
|
|
811
910
|
description,
|
|
812
911
|
inputSchema: schema,
|
|
813
912
|
execution: { taskSupport: "optional" },
|
|
814
|
-
|
|
913
|
+
annotations
|
|
815
914
|
},
|
|
816
915
|
{
|
|
817
916
|
createTask: async (input, extra) => {
|
|
@@ -871,7 +970,7 @@ function registerToolTask(server2, name, description, schema, handler) {
|
|
|
871
970
|
}
|
|
872
971
|
|
|
873
972
|
// src/tools/parties.ts
|
|
874
|
-
import { z as
|
|
973
|
+
import { z as z7 } from "zod";
|
|
875
974
|
|
|
876
975
|
// src/tools/body-helpers.ts
|
|
877
976
|
function setRef(body, key, id) {
|
|
@@ -881,9 +980,22 @@ function setNullableRef(body, key, id) {
|
|
|
881
980
|
if (id === null) body[key] = null;
|
|
882
981
|
else if (id !== void 0) body[key] = { id };
|
|
883
982
|
}
|
|
983
|
+
function assertSingleParentRef(toolName, refs, opts = {}) {
|
|
984
|
+
const set = [refs.partyId, refs.opportunityId, refs.projectId].filter(
|
|
985
|
+
(v) => typeof v === "number"
|
|
986
|
+
).length;
|
|
987
|
+
if (opts.required && set !== 1) {
|
|
988
|
+
throw new Error(`${toolName}: provide exactly one of partyId, opportunityId, or projectId`);
|
|
989
|
+
}
|
|
990
|
+
if (set > 1) {
|
|
991
|
+
throw new Error(
|
|
992
|
+
`${toolName}: provide at most one of partyId, opportunityId, or projectId \u2014 Capsule allows a record to be related to at most one entity`
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
884
996
|
|
|
885
997
|
// src/tools/define-batch.ts
|
|
886
|
-
import { z } from "zod";
|
|
998
|
+
import { z as z2 } from "zod";
|
|
887
999
|
|
|
888
1000
|
// src/capsule/batch.ts
|
|
889
1001
|
function chunk(arr, size) {
|
|
@@ -902,37 +1014,43 @@ function getBatchConcurrency() {
|
|
|
902
1014
|
MAX_CONCURRENCY
|
|
903
1015
|
);
|
|
904
1016
|
}
|
|
905
|
-
async function
|
|
906
|
-
const concurrency = getBatchConcurrency();
|
|
1017
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
907
1018
|
const results = new Array(items.length);
|
|
908
|
-
const startedAt = Date.now();
|
|
909
|
-
const signal = options.signal;
|
|
910
1019
|
let cursor = 0;
|
|
911
1020
|
async function worker() {
|
|
912
1021
|
while (true) {
|
|
913
1022
|
const i = cursor;
|
|
914
1023
|
cursor += 1;
|
|
915
1024
|
if (i >= items.length) return;
|
|
916
|
-
|
|
917
|
-
results[i] = {
|
|
918
|
-
ok: false,
|
|
919
|
-
error: { message: "cancelled by tasks/cancel" }
|
|
920
|
-
};
|
|
921
|
-
continue;
|
|
922
|
-
}
|
|
923
|
-
try {
|
|
924
|
-
const result = await action(items[i], i);
|
|
925
|
-
results[i] = { ok: true, result };
|
|
926
|
-
} catch (err) {
|
|
927
|
-
results[i] = { ok: false, error: extractError(err) };
|
|
928
|
-
}
|
|
1025
|
+
results[i] = await fn(items[i], i);
|
|
929
1026
|
}
|
|
930
1027
|
}
|
|
931
1028
|
const workers = [];
|
|
932
|
-
for (let w = 0; w < Math.min(
|
|
1029
|
+
for (let w = 0; w < Math.min(limit, items.length); w++) {
|
|
933
1030
|
workers.push(worker());
|
|
934
1031
|
}
|
|
935
1032
|
await Promise.all(workers);
|
|
1033
|
+
return results;
|
|
1034
|
+
}
|
|
1035
|
+
async function batchExecute(tool, items, action, options = {}) {
|
|
1036
|
+
const concurrency = getBatchConcurrency();
|
|
1037
|
+
const startedAt = Date.now();
|
|
1038
|
+
const signal = options.signal;
|
|
1039
|
+
const results = await mapWithConcurrency(
|
|
1040
|
+
items,
|
|
1041
|
+
concurrency,
|
|
1042
|
+
async (item, i) => {
|
|
1043
|
+
if (signal?.aborted) {
|
|
1044
|
+
return { ok: false, error: { message: "cancelled by tasks/cancel" } };
|
|
1045
|
+
}
|
|
1046
|
+
try {
|
|
1047
|
+
const result = await action(item, i);
|
|
1048
|
+
return { ok: true, result };
|
|
1049
|
+
} catch (err) {
|
|
1050
|
+
return { ok: false, error: extractError(err) };
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
);
|
|
936
1054
|
const succeeded = results.filter((r) => r.ok).length;
|
|
937
1055
|
const failed = results.length - succeeded;
|
|
938
1056
|
const summary = { total: results.length, succeeded, failed };
|
|
@@ -977,10 +1095,52 @@ function topFailureReasons(results, n) {
|
|
|
977
1095
|
return Array.from(counts.values()).sort((a, b) => b.count - a.count).slice(0, n);
|
|
978
1096
|
}
|
|
979
1097
|
|
|
1098
|
+
// src/tools/strip-descriptions.ts
|
|
1099
|
+
import { z } from "zod";
|
|
1100
|
+
function cloneWithDef(node, patch) {
|
|
1101
|
+
const def = node.def;
|
|
1102
|
+
return node.clone({ ...def, ...patch });
|
|
1103
|
+
}
|
|
1104
|
+
function stripDescriptions(schema) {
|
|
1105
|
+
let node = schema;
|
|
1106
|
+
if (node instanceof z.ZodObject) {
|
|
1107
|
+
const shape = node.def.shape;
|
|
1108
|
+
const next = {};
|
|
1109
|
+
let changed = false;
|
|
1110
|
+
for (const [key, child] of Object.entries(shape)) {
|
|
1111
|
+
next[key] = stripDescriptions(child);
|
|
1112
|
+
if (next[key] !== child) changed = true;
|
|
1113
|
+
}
|
|
1114
|
+
if (changed) node = cloneWithDef(node, { shape: next });
|
|
1115
|
+
} else if (node instanceof z.ZodArray) {
|
|
1116
|
+
const element = stripDescriptions(node.def.element);
|
|
1117
|
+
if (element !== node.def.element) node = cloneWithDef(node, { element });
|
|
1118
|
+
} else if (node instanceof z.ZodOptional || node instanceof z.ZodNullable || node instanceof z.ZodDefault || node instanceof z.ZodReadonly) {
|
|
1119
|
+
const innerType = stripDescriptions(node.def.innerType);
|
|
1120
|
+
if (innerType !== node.def.innerType) node = cloneWithDef(node, { innerType });
|
|
1121
|
+
} else if (node instanceof z.ZodUnion) {
|
|
1122
|
+
const options = node.def.options.map(stripDescriptions);
|
|
1123
|
+
if (options.some((o, i) => o !== node.def.options[i])) {
|
|
1124
|
+
node = cloneWithDef(node, { options });
|
|
1125
|
+
}
|
|
1126
|
+
} else if (node instanceof z.ZodPipe) {
|
|
1127
|
+
const inSchema = stripDescriptions(node.def.in);
|
|
1128
|
+
const outSchema = stripDescriptions(node.def.out);
|
|
1129
|
+
if (inSchema !== node.def.in || outSchema !== node.def.out) {
|
|
1130
|
+
node = cloneWithDef(node, { in: inSchema, out: outSchema });
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
if (node.description !== void 0) {
|
|
1134
|
+
node = node.meta({ description: void 0 });
|
|
1135
|
+
}
|
|
1136
|
+
return node;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
980
1139
|
// src/tools/define-batch.ts
|
|
981
1140
|
function defineBatch(args) {
|
|
982
|
-
const
|
|
983
|
-
|
|
1141
|
+
const itemSchema = stripDescriptions(args.itemSchema);
|
|
1142
|
+
const schema = z2.object({
|
|
1143
|
+
items: z2.array(itemSchema).min(1).max(50).describe(args.itemDescription)
|
|
984
1144
|
});
|
|
985
1145
|
async function handler(input, opts = {}) {
|
|
986
1146
|
return batchExecute(args.toolName, input.items, args.itemHandler, opts);
|
|
@@ -993,22 +1153,30 @@ var EMBED_TAGS_FIELDS_DESCRIPTION = "Comma-separated embeds, e.g. 'tags,fields'"
|
|
|
993
1153
|
var EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION = "Comma-separated embeds, e.g. 'attachments,participants'";
|
|
994
1154
|
|
|
995
1155
|
// src/tools/define-delete.ts
|
|
996
|
-
import { z as
|
|
1156
|
+
import { z as z5 } from "zod";
|
|
997
1157
|
|
|
998
1158
|
// src/tools/confirm-flag.ts
|
|
999
|
-
import { z as
|
|
1159
|
+
import { z as z3 } from "zod";
|
|
1000
1160
|
var CONFIRM_REQUIRED_MESSAGE = "confirm: true is required to perform this destructive operation (set the parameter explicitly to acknowledge the destructive intent)";
|
|
1001
1161
|
function confirmFlag() {
|
|
1002
|
-
return
|
|
1162
|
+
return z3.literal(true, { error: () => CONFIRM_REQUIRED_MESSAGE });
|
|
1003
1163
|
}
|
|
1004
1164
|
|
|
1005
1165
|
// src/tools/shared-schemas.ts
|
|
1006
|
-
import { z as
|
|
1007
|
-
var positiveId =
|
|
1166
|
+
import { z as z4 } from "zod";
|
|
1167
|
+
var positiveId = z4.preprocess((input) => {
|
|
1008
1168
|
if (typeof input !== "string") return input;
|
|
1009
1169
|
const trimmed = input.trim();
|
|
1010
1170
|
return /^\d+$/.test(trimmed) ? Number(trimmed) : input;
|
|
1011
|
-
},
|
|
1171
|
+
}, z4.number().int().positive());
|
|
1172
|
+
var paginationFields = {
|
|
1173
|
+
page: z4.number().int().positive().optional().default(1),
|
|
1174
|
+
perPage: z4.number().int().min(1).max(100).optional().default(25)
|
|
1175
|
+
};
|
|
1176
|
+
var paginationFieldsNoDefaults = {
|
|
1177
|
+
page: z4.number().int().positive().optional(),
|
|
1178
|
+
perPage: z4.number().int().min(1).max(100).optional()
|
|
1179
|
+
};
|
|
1012
1180
|
|
|
1013
1181
|
// src/capsule/idempotent.ts
|
|
1014
1182
|
var isCapsule404 = (err) => err instanceof CapsuleApiError && err.status === 404;
|
|
@@ -1035,7 +1203,7 @@ async function idempotentWithResult(op, success, alreadyDone, isAlreadyDoneError
|
|
|
1035
1203
|
// src/tools/define-delete.ts
|
|
1036
1204
|
function defineDelete(args) {
|
|
1037
1205
|
const { toolName, pathPrefix, confirmHint, idDescription } = args;
|
|
1038
|
-
const schema =
|
|
1206
|
+
const schema = z5.object({
|
|
1039
1207
|
id: idDescription ? positiveId.describe(idDescription) : positiveId,
|
|
1040
1208
|
confirm: confirmFlag().describe(confirmHint)
|
|
1041
1209
|
});
|
|
@@ -1078,16 +1246,17 @@ async function chunkedMultiGet(base, responseKey, ids, params) {
|
|
|
1078
1246
|
(chunkIds) => capsuleGet(`${base}/${chunkIds.join(",")}`, params)
|
|
1079
1247
|
)
|
|
1080
1248
|
);
|
|
1081
|
-
|
|
1249
|
+
const merged = responses.flatMap((r) => r.data[responseKey] ?? []);
|
|
1250
|
+
return { ...responses[0]?.data ?? {}, [responseKey]: merged };
|
|
1082
1251
|
}
|
|
1083
1252
|
|
|
1084
1253
|
// src/tools/custom-field-helpers.ts
|
|
1085
|
-
import { z as
|
|
1086
|
-
var CustomFieldWriteSchema =
|
|
1254
|
+
import { z as z6 } from "zod";
|
|
1255
|
+
var CustomFieldWriteSchema = z6.object({
|
|
1087
1256
|
definitionId: positiveId.describe(
|
|
1088
1257
|
"The custom-field definition id from list_custom_fields. Identifies which field on the entity to set."
|
|
1089
1258
|
),
|
|
1090
|
-
value:
|
|
1259
|
+
value: z6.union([z6.string(), z6.number(), z6.boolean(), z6.null()]).describe(
|
|
1091
1260
|
"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."
|
|
1092
1261
|
)
|
|
1093
1262
|
});
|
|
@@ -1103,24 +1272,24 @@ function mapFieldsForBody(fields) {
|
|
|
1103
1272
|
}
|
|
1104
1273
|
|
|
1105
1274
|
// src/tools/parties.ts
|
|
1106
|
-
var EmailAddressSchema =
|
|
1107
|
-
address:
|
|
1108
|
-
type:
|
|
1275
|
+
var EmailAddressSchema = z7.object({
|
|
1276
|
+
address: z7.string().email(),
|
|
1277
|
+
type: z7.string().optional()
|
|
1109
1278
|
});
|
|
1110
|
-
var PhoneNumberSchema =
|
|
1279
|
+
var PhoneNumberSchema = z7.object({
|
|
1111
1280
|
// Capsule rejects empty strings with `phoneNumber.number: number is
|
|
1112
1281
|
// required`. Enforce at the schema layer to catch typos pre-call,
|
|
1113
1282
|
// matching how EmailAddressSchema's address field behaves.
|
|
1114
|
-
number:
|
|
1115
|
-
type:
|
|
1283
|
+
number: z7.string().min(1),
|
|
1284
|
+
type: z7.string().optional()
|
|
1116
1285
|
});
|
|
1117
1286
|
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.";
|
|
1118
|
-
var AddressSchema =
|
|
1119
|
-
street:
|
|
1120
|
-
city:
|
|
1121
|
-
state:
|
|
1122
|
-
country:
|
|
1123
|
-
zip:
|
|
1287
|
+
var AddressSchema = z7.object({
|
|
1288
|
+
street: z7.string().optional(),
|
|
1289
|
+
city: z7.string().optional(),
|
|
1290
|
+
state: z7.string().optional(),
|
|
1291
|
+
country: z7.string().optional().describe(CountryDescription),
|
|
1292
|
+
zip: z7.string().optional()
|
|
1124
1293
|
});
|
|
1125
1294
|
function validateWebsiteAddress(data, ctx) {
|
|
1126
1295
|
const isUrlService = data.service === void 0 || data.service === "URL";
|
|
@@ -1143,7 +1312,7 @@ function validateWebsiteAddress(data, ctx) {
|
|
|
1143
1312
|
});
|
|
1144
1313
|
}
|
|
1145
1314
|
}
|
|
1146
|
-
var WebsiteServiceEnum =
|
|
1315
|
+
var WebsiteServiceEnum = z7.enum([
|
|
1147
1316
|
"URL",
|
|
1148
1317
|
"SKYPE",
|
|
1149
1318
|
"TWITTER",
|
|
@@ -1162,33 +1331,31 @@ var WebsiteServiceEnum = z6.enum([
|
|
|
1162
1331
|
"BLUESKY",
|
|
1163
1332
|
"SNAPCHAT"
|
|
1164
1333
|
]);
|
|
1165
|
-
var WebsiteSchema =
|
|
1166
|
-
address:
|
|
1334
|
+
var WebsiteSchema = z7.object({
|
|
1335
|
+
address: z7.string().min(1).describe(
|
|
1167
1336
|
"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."
|
|
1168
1337
|
),
|
|
1169
1338
|
service: WebsiteServiceEnum.optional().describe(
|
|
1170
1339
|
"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."
|
|
1171
1340
|
)
|
|
1172
1341
|
}).superRefine(validateWebsiteAddress);
|
|
1173
|
-
var searchPartiesSchema =
|
|
1174
|
-
q:
|
|
1175
|
-
embed:
|
|
1176
|
-
|
|
1177
|
-
perPage: z6.number().int().min(1).max(100).optional().default(25)
|
|
1342
|
+
var searchPartiesSchema = z7.object({
|
|
1343
|
+
q: z7.string().optional().describe("Free-text search query"),
|
|
1344
|
+
embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
|
|
1345
|
+
...paginationFields
|
|
1178
1346
|
});
|
|
1179
1347
|
async function searchParties(input) {
|
|
1180
1348
|
const path = input.q ? "/parties/search" : "/parties";
|
|
1181
|
-
|
|
1349
|
+
return capsuleGetList(path, {
|
|
1182
1350
|
q: input.q,
|
|
1183
1351
|
embed: input.embed,
|
|
1184
1352
|
page: input.page,
|
|
1185
1353
|
perPage: input.perPage
|
|
1186
1354
|
});
|
|
1187
|
-
return { ...data, nextPage };
|
|
1188
1355
|
}
|
|
1189
|
-
var getPartySchema =
|
|
1356
|
+
var getPartySchema = z7.object({
|
|
1190
1357
|
id: positiveId.describe("Party ID"),
|
|
1191
|
-
embed:
|
|
1358
|
+
embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
|
|
1192
1359
|
});
|
|
1193
1360
|
async function getParty(input) {
|
|
1194
1361
|
const { data } = await capsuleGet(`/parties/${input.id}`, {
|
|
@@ -1196,51 +1363,47 @@ async function getParty(input) {
|
|
|
1196
1363
|
});
|
|
1197
1364
|
return data;
|
|
1198
1365
|
}
|
|
1199
|
-
var getPartiesSchema =
|
|
1200
|
-
ids:
|
|
1366
|
+
var getPartiesSchema = z7.object({
|
|
1367
|
+
ids: z7.array(positiveId).min(1).max(50).describe(
|
|
1201
1368
|
"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."
|
|
1202
1369
|
),
|
|
1203
|
-
embed:
|
|
1370
|
+
embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
|
|
1204
1371
|
});
|
|
1205
1372
|
async function getParties(input) {
|
|
1206
1373
|
return chunkedMultiGet("/parties", "parties", input.ids, { embed: input.embed });
|
|
1207
1374
|
}
|
|
1208
|
-
var listPartyOpportunitiesSchema =
|
|
1375
|
+
var listPartyOpportunitiesSchema = z7.object({
|
|
1209
1376
|
partyId: positiveId,
|
|
1210
|
-
|
|
1211
|
-
perPage: z6.number().int().min(1).max(100).optional().default(25)
|
|
1377
|
+
...paginationFields
|
|
1212
1378
|
});
|
|
1213
1379
|
async function listPartyOpportunities(input) {
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
);
|
|
1218
|
-
return { ...data, nextPage };
|
|
1380
|
+
return capsuleGetList(`/parties/${input.partyId}/opportunities`, {
|
|
1381
|
+
page: input.page,
|
|
1382
|
+
perPage: input.perPage
|
|
1383
|
+
});
|
|
1219
1384
|
}
|
|
1220
|
-
var listPartyProjectsSchema =
|
|
1385
|
+
var listPartyProjectsSchema = z7.object({
|
|
1221
1386
|
partyId: positiveId,
|
|
1222
|
-
|
|
1223
|
-
perPage: z6.number().int().min(1).max(100).optional().default(25)
|
|
1387
|
+
...paginationFields
|
|
1224
1388
|
});
|
|
1225
1389
|
async function listPartyProjects(input) {
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
);
|
|
1230
|
-
return { ...data, nextPage };
|
|
1390
|
+
return capsuleGetList(`/parties/${input.partyId}/kases`, {
|
|
1391
|
+
page: input.page,
|
|
1392
|
+
perPage: input.perPage
|
|
1393
|
+
});
|
|
1231
1394
|
}
|
|
1232
1395
|
var PartyWriteBaseSchema = {
|
|
1233
|
-
about:
|
|
1234
|
-
emailAddresses:
|
|
1396
|
+
about: z7.string().optional(),
|
|
1397
|
+
emailAddresses: z7.array(EmailAddressSchema).optional().describe(
|
|
1235
1398
|
"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)."
|
|
1236
1399
|
),
|
|
1237
|
-
phoneNumbers:
|
|
1400
|
+
phoneNumbers: z7.array(PhoneNumberSchema).optional().describe(
|
|
1238
1401
|
"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."
|
|
1239
1402
|
),
|
|
1240
|
-
addresses:
|
|
1403
|
+
addresses: z7.array(AddressSchema).optional().describe(
|
|
1241
1404
|
"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)."
|
|
1242
1405
|
),
|
|
1243
|
-
websites:
|
|
1406
|
+
websites: z7.array(WebsiteSchema).optional().describe(
|
|
1244
1407
|
"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."
|
|
1245
1408
|
),
|
|
1246
1409
|
ownerId: positiveId.nullable().optional().describe(
|
|
@@ -1250,16 +1413,16 @@ var PartyWriteBaseSchema = {
|
|
|
1250
1413
|
"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)."
|
|
1251
1414
|
)
|
|
1252
1415
|
};
|
|
1253
|
-
var createPartySchema =
|
|
1254
|
-
type:
|
|
1416
|
+
var createPartySchema = z7.object({
|
|
1417
|
+
type: z7.enum(["person", "organisation"]),
|
|
1255
1418
|
// person
|
|
1256
|
-
firstName:
|
|
1257
|
-
lastName:
|
|
1258
|
-
title:
|
|
1259
|
-
jobTitle:
|
|
1419
|
+
firstName: z7.string().optional(),
|
|
1420
|
+
lastName: z7.string().optional(),
|
|
1421
|
+
title: z7.string().optional(),
|
|
1422
|
+
jobTitle: z7.string().optional(),
|
|
1260
1423
|
organisationId: positiveId.optional().describe("Link person to an existing organisation ID"),
|
|
1261
1424
|
// organisation
|
|
1262
|
-
name:
|
|
1425
|
+
name: z7.string().optional(),
|
|
1263
1426
|
...PartyWriteBaseSchema,
|
|
1264
1427
|
ownerId: positiveId.optional().describe(
|
|
1265
1428
|
"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`."
|
|
@@ -1267,7 +1430,7 @@ var createPartySchema = z6.object({
|
|
|
1267
1430
|
teamId: positiveId.optional().describe(
|
|
1268
1431
|
"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."
|
|
1269
1432
|
),
|
|
1270
|
-
fields:
|
|
1433
|
+
fields: z7.array(CustomFieldWriteSchema).optional().describe(
|
|
1271
1434
|
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."
|
|
1272
1435
|
)
|
|
1273
1436
|
});
|
|
@@ -1281,17 +1444,17 @@ async function createParty(input) {
|
|
|
1281
1444
|
if (mappedFields !== void 0) body["fields"] = mappedFields;
|
|
1282
1445
|
return capsulePost("/parties", { party: body });
|
|
1283
1446
|
}
|
|
1284
|
-
var updatePartySchema =
|
|
1447
|
+
var updatePartySchema = z7.object({
|
|
1285
1448
|
id: positiveId,
|
|
1286
|
-
firstName:
|
|
1287
|
-
lastName:
|
|
1288
|
-
title:
|
|
1289
|
-
jobTitle:
|
|
1290
|
-
name:
|
|
1449
|
+
firstName: z7.string().optional(),
|
|
1450
|
+
lastName: z7.string().optional(),
|
|
1451
|
+
title: z7.string().optional(),
|
|
1452
|
+
jobTitle: z7.string().optional(),
|
|
1453
|
+
name: z7.string().optional(),
|
|
1291
1454
|
organisationId: positiveId.nullable().optional().describe(
|
|
1292
1455
|
"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."
|
|
1293
1456
|
),
|
|
1294
|
-
fields:
|
|
1457
|
+
fields: z7.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_party")),
|
|
1295
1458
|
...PartyWriteBaseSchema
|
|
1296
1459
|
});
|
|
1297
1460
|
async function updateParty(input) {
|
|
@@ -1322,10 +1485,42 @@ var { schema: deletePartySchema, handler: deleteParty } = defineDelete({
|
|
|
1322
1485
|
pathPrefix: "/parties",
|
|
1323
1486
|
confirmHint: "Must be set to true. Deletes the party AND all linked notes, tasks, opportunities, and projects (kases). 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."
|
|
1324
1487
|
});
|
|
1325
|
-
|
|
1488
|
+
function definePartySubResourceRemove(opts) {
|
|
1489
|
+
const shape = {
|
|
1490
|
+
partyId: positiveId,
|
|
1491
|
+
[opts.idField]: positiveId.describe(
|
|
1492
|
+
`Capsule's id for the ${opts.rowNoun} row. Read it from get_party (each entry in ${opts.arrayKey} carries an id).`
|
|
1493
|
+
)
|
|
1494
|
+
};
|
|
1495
|
+
const schema = z7.object(shape);
|
|
1496
|
+
async function handler(input) {
|
|
1497
|
+
const partyId = input["partyId"];
|
|
1498
|
+
const rowId = input[opts.idField];
|
|
1499
|
+
return idempotentWithResult(
|
|
1500
|
+
() => capsulePut(`/parties/${partyId}`, {
|
|
1501
|
+
party: { [opts.arrayKey]: [{ id: rowId, _delete: true }] }
|
|
1502
|
+
}),
|
|
1503
|
+
(result) => ({
|
|
1504
|
+
removed: true,
|
|
1505
|
+
alreadyRemoved: false,
|
|
1506
|
+
partyId,
|
|
1507
|
+
[opts.idField]: rowId,
|
|
1508
|
+
...result
|
|
1509
|
+
}),
|
|
1510
|
+
() => ({
|
|
1511
|
+
removed: true,
|
|
1512
|
+
alreadyRemoved: true,
|
|
1513
|
+
partyId,
|
|
1514
|
+
[opts.idField]: rowId
|
|
1515
|
+
})
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
return { schema, handler };
|
|
1519
|
+
}
|
|
1520
|
+
var addPartyEmailAddressSchema = z7.object({
|
|
1326
1521
|
partyId: positiveId,
|
|
1327
|
-
address:
|
|
1328
|
-
type:
|
|
1522
|
+
address: z7.string().email(),
|
|
1523
|
+
type: z7.string().optional().describe("Free-form label, e.g. 'Work', 'Home'.")
|
|
1329
1524
|
});
|
|
1330
1525
|
async function addPartyEmailAddress(input) {
|
|
1331
1526
|
const { partyId, address, type } = input;
|
|
@@ -1335,32 +1530,17 @@ async function addPartyEmailAddress(input) {
|
|
|
1335
1530
|
party: { emailAddresses: [item] }
|
|
1336
1531
|
});
|
|
1337
1532
|
}
|
|
1338
|
-
var
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
)
|
|
1533
|
+
var removePartyEmailAddress = definePartySubResourceRemove({
|
|
1534
|
+
arrayKey: "emailAddresses",
|
|
1535
|
+
idField: "emailAddressId",
|
|
1536
|
+
rowNoun: "email-address"
|
|
1343
1537
|
});
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
() => capsulePut(`/parties/${partyId}`, {
|
|
1348
|
-
party: { emailAddresses: [{ id: emailAddressId, _delete: true }] }
|
|
1349
|
-
}),
|
|
1350
|
-
(result) => ({
|
|
1351
|
-
removed: true,
|
|
1352
|
-
alreadyRemoved: false,
|
|
1353
|
-
partyId,
|
|
1354
|
-
emailAddressId,
|
|
1355
|
-
...result
|
|
1356
|
-
}),
|
|
1357
|
-
() => ({ removed: true, alreadyRemoved: true, partyId, emailAddressId })
|
|
1358
|
-
);
|
|
1359
|
-
}
|
|
1360
|
-
var addPartyPhoneNumberSchema = z6.object({
|
|
1538
|
+
var removePartyEmailAddressByIdSchema = removePartyEmailAddress.schema;
|
|
1539
|
+
var removePartyEmailAddressById = removePartyEmailAddress.handler;
|
|
1540
|
+
var addPartyPhoneNumberSchema = z7.object({
|
|
1361
1541
|
partyId: positiveId,
|
|
1362
|
-
number:
|
|
1363
|
-
type:
|
|
1542
|
+
number: z7.string().min(1),
|
|
1543
|
+
type: z7.string().optional().describe("Free-form label, e.g. 'Work', 'Mobile'.")
|
|
1364
1544
|
});
|
|
1365
1545
|
async function addPartyPhoneNumber(input) {
|
|
1366
1546
|
const { partyId, number, type } = input;
|
|
@@ -1370,36 +1550,21 @@ async function addPartyPhoneNumber(input) {
|
|
|
1370
1550
|
party: { phoneNumbers: [item] }
|
|
1371
1551
|
});
|
|
1372
1552
|
}
|
|
1373
|
-
var
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
)
|
|
1553
|
+
var removePartyPhoneNumber = definePartySubResourceRemove({
|
|
1554
|
+
arrayKey: "phoneNumbers",
|
|
1555
|
+
idField: "phoneNumberId",
|
|
1556
|
+
rowNoun: "phone-number"
|
|
1378
1557
|
});
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
() => capsulePut(`/parties/${partyId}`, {
|
|
1383
|
-
party: { phoneNumbers: [{ id: phoneNumberId, _delete: true }] }
|
|
1384
|
-
}),
|
|
1385
|
-
(result) => ({
|
|
1386
|
-
removed: true,
|
|
1387
|
-
alreadyRemoved: false,
|
|
1388
|
-
partyId,
|
|
1389
|
-
phoneNumberId,
|
|
1390
|
-
...result
|
|
1391
|
-
}),
|
|
1392
|
-
() => ({ removed: true, alreadyRemoved: true, partyId, phoneNumberId })
|
|
1393
|
-
);
|
|
1394
|
-
}
|
|
1395
|
-
var addPartyAddressSchema = z6.object({
|
|
1558
|
+
var removePartyPhoneNumberByIdSchema = removePartyPhoneNumber.schema;
|
|
1559
|
+
var removePartyPhoneNumberById = removePartyPhoneNumber.handler;
|
|
1560
|
+
var addPartyAddressSchema = z7.object({
|
|
1396
1561
|
partyId: positiveId,
|
|
1397
|
-
street:
|
|
1398
|
-
city:
|
|
1399
|
-
state:
|
|
1400
|
-
country:
|
|
1401
|
-
zip:
|
|
1402
|
-
type:
|
|
1562
|
+
street: z7.string().optional(),
|
|
1563
|
+
city: z7.string().optional(),
|
|
1564
|
+
state: z7.string().optional(),
|
|
1565
|
+
country: z7.string().optional().describe(CountryDescription),
|
|
1566
|
+
zip: z7.string().optional(),
|
|
1567
|
+
type: z7.string().optional().describe("Free-form label, e.g. 'Office', 'Home'.")
|
|
1403
1568
|
});
|
|
1404
1569
|
async function addPartyAddress(input) {
|
|
1405
1570
|
const { partyId, ...rest } = input;
|
|
@@ -1411,31 +1576,16 @@ async function addPartyAddress(input) {
|
|
|
1411
1576
|
party: { addresses: [item] }
|
|
1412
1577
|
});
|
|
1413
1578
|
}
|
|
1414
|
-
var
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
)
|
|
1579
|
+
var removePartyAddress = definePartySubResourceRemove({
|
|
1580
|
+
arrayKey: "addresses",
|
|
1581
|
+
idField: "addressId",
|
|
1582
|
+
rowNoun: "address"
|
|
1419
1583
|
});
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
() => capsulePut(`/parties/${partyId}`, {
|
|
1424
|
-
party: { addresses: [{ id: addressId, _delete: true }] }
|
|
1425
|
-
}),
|
|
1426
|
-
(result) => ({
|
|
1427
|
-
removed: true,
|
|
1428
|
-
alreadyRemoved: false,
|
|
1429
|
-
partyId,
|
|
1430
|
-
addressId,
|
|
1431
|
-
...result
|
|
1432
|
-
}),
|
|
1433
|
-
() => ({ removed: true, alreadyRemoved: true, partyId, addressId })
|
|
1434
|
-
);
|
|
1435
|
-
}
|
|
1436
|
-
var addPartyWebsiteSchema = z6.object({
|
|
1584
|
+
var removePartyAddressByIdSchema = removePartyAddress.schema;
|
|
1585
|
+
var removePartyAddressById = removePartyAddress.handler;
|
|
1586
|
+
var addPartyWebsiteSchema = z7.object({
|
|
1437
1587
|
partyId: positiveId,
|
|
1438
|
-
address:
|
|
1588
|
+
address: z7.string().min(1).describe(
|
|
1439
1589
|
"The website address. A URL when service='URL', or a handle (e.g. '@acmeco') for social services."
|
|
1440
1590
|
),
|
|
1441
1591
|
service: WebsiteServiceEnum.optional().describe("Defaults to 'URL' if omitted.")
|
|
@@ -1448,58 +1598,41 @@ async function addPartyWebsite(input) {
|
|
|
1448
1598
|
party: { websites: [item] }
|
|
1449
1599
|
});
|
|
1450
1600
|
}
|
|
1451
|
-
var
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
)
|
|
1601
|
+
var removePartyWebsite = definePartySubResourceRemove({
|
|
1602
|
+
arrayKey: "websites",
|
|
1603
|
+
idField: "websiteId",
|
|
1604
|
+
rowNoun: "website"
|
|
1456
1605
|
});
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
return idempotentWithResult(
|
|
1460
|
-
() => capsulePut(`/parties/${partyId}`, {
|
|
1461
|
-
party: { websites: [{ id: websiteId, _delete: true }] }
|
|
1462
|
-
}),
|
|
1463
|
-
(result) => ({
|
|
1464
|
-
removed: true,
|
|
1465
|
-
alreadyRemoved: false,
|
|
1466
|
-
partyId,
|
|
1467
|
-
websiteId,
|
|
1468
|
-
...result
|
|
1469
|
-
}),
|
|
1470
|
-
() => ({ removed: true, alreadyRemoved: true, partyId, websiteId })
|
|
1471
|
-
);
|
|
1472
|
-
}
|
|
1606
|
+
var removePartyWebsiteByIdSchema = removePartyWebsite.schema;
|
|
1607
|
+
var removePartyWebsiteById = removePartyWebsite.handler;
|
|
1473
1608
|
|
|
1474
1609
|
// src/tools/opportunities.ts
|
|
1475
|
-
import { z as
|
|
1476
|
-
var OpportunityValueSchema =
|
|
1477
|
-
amount:
|
|
1478
|
-
currency:
|
|
1610
|
+
import { z as z8 } from "zod";
|
|
1611
|
+
var OpportunityValueSchema = z8.object({
|
|
1612
|
+
amount: z8.number().nonnegative(),
|
|
1613
|
+
currency: z8.string({
|
|
1479
1614
|
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
|
|
1480
1615
|
}).length(3).describe(
|
|
1481
1616
|
"ISO 4217 currency code (3 letters), e.g. 'GBP', 'USD', 'EUR'. Required when amount is set."
|
|
1482
1617
|
)
|
|
1483
1618
|
});
|
|
1484
|
-
var searchOpportunitiesSchema =
|
|
1485
|
-
q:
|
|
1486
|
-
embed:
|
|
1487
|
-
|
|
1488
|
-
perPage: z7.number().int().min(1).max(100).optional().default(25)
|
|
1619
|
+
var searchOpportunitiesSchema = z8.object({
|
|
1620
|
+
q: z8.string().optional().describe("Free-text search query"),
|
|
1621
|
+
embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
|
|
1622
|
+
...paginationFields
|
|
1489
1623
|
});
|
|
1490
1624
|
async function searchOpportunities(input) {
|
|
1491
1625
|
const path = input.q ? "/opportunities/search" : "/opportunities";
|
|
1492
|
-
|
|
1626
|
+
return capsuleGetList(path, {
|
|
1493
1627
|
q: input.q,
|
|
1494
1628
|
embed: input.embed,
|
|
1495
1629
|
page: input.page,
|
|
1496
1630
|
perPage: input.perPage
|
|
1497
1631
|
});
|
|
1498
|
-
return { ...data, nextPage };
|
|
1499
1632
|
}
|
|
1500
|
-
var getOpportunitySchema =
|
|
1633
|
+
var getOpportunitySchema = z8.object({
|
|
1501
1634
|
id: positiveId,
|
|
1502
|
-
embed:
|
|
1635
|
+
embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
|
|
1503
1636
|
});
|
|
1504
1637
|
async function getOpportunity(input) {
|
|
1505
1638
|
const { data } = await capsuleGet(`/opportunities/${input.id}`, {
|
|
@@ -1507,32 +1640,32 @@ async function getOpportunity(input) {
|
|
|
1507
1640
|
});
|
|
1508
1641
|
return data;
|
|
1509
1642
|
}
|
|
1510
|
-
var getOpportunitiesSchema =
|
|
1511
|
-
ids:
|
|
1643
|
+
var getOpportunitiesSchema = z8.object({
|
|
1644
|
+
ids: z8.array(positiveId).min(1).max(50).describe(
|
|
1512
1645
|
"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."
|
|
1513
1646
|
),
|
|
1514
|
-
embed:
|
|
1647
|
+
embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
|
|
1515
1648
|
});
|
|
1516
1649
|
async function getOpportunities(input) {
|
|
1517
1650
|
return chunkedMultiGet("/opportunities", "opportunities", input.ids, { embed: input.embed });
|
|
1518
1651
|
}
|
|
1519
|
-
var createOpportunitySchema =
|
|
1520
|
-
name:
|
|
1652
|
+
var createOpportunitySchema = z8.object({
|
|
1653
|
+
name: z8.string().min(1),
|
|
1521
1654
|
partyId: positiveId.describe("ID of the party this opportunity belongs to"),
|
|
1522
1655
|
milestoneId: positiveId.describe(
|
|
1523
1656
|
"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."
|
|
1524
1657
|
),
|
|
1525
|
-
description:
|
|
1658
|
+
description: z8.string().optional(),
|
|
1526
1659
|
value: OpportunityValueSchema.optional(),
|
|
1527
|
-
expectedCloseOn:
|
|
1528
|
-
probability:
|
|
1660
|
+
expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
|
|
1661
|
+
probability: z8.number().int().min(0).max(100).optional(),
|
|
1529
1662
|
ownerId: positiveId.optional().describe(
|
|
1530
1663
|
"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."
|
|
1531
1664
|
),
|
|
1532
1665
|
teamId: positiveId.optional().describe(
|
|
1533
1666
|
"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)."
|
|
1534
1667
|
),
|
|
1535
|
-
fields:
|
|
1668
|
+
fields: z8.array(CustomFieldWriteSchema).optional().describe(
|
|
1536
1669
|
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."
|
|
1537
1670
|
)
|
|
1538
1671
|
});
|
|
@@ -1549,19 +1682,19 @@ async function createOpportunity(input) {
|
|
|
1549
1682
|
if (mappedFields !== void 0) body["fields"] = mappedFields;
|
|
1550
1683
|
return capsulePost("/opportunities", { opportunity: body });
|
|
1551
1684
|
}
|
|
1552
|
-
var updateOpportunitySchema =
|
|
1685
|
+
var updateOpportunitySchema = z8.object({
|
|
1553
1686
|
id: positiveId,
|
|
1554
|
-
name:
|
|
1687
|
+
name: z8.string().min(1).optional(),
|
|
1555
1688
|
partyId: positiveId.optional().describe(
|
|
1556
1689
|
"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`."
|
|
1557
1690
|
),
|
|
1558
1691
|
milestoneId: positiveId.optional().describe(
|
|
1559
1692
|
"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."
|
|
1560
1693
|
),
|
|
1561
|
-
description:
|
|
1694
|
+
description: z8.string().optional(),
|
|
1562
1695
|
value: OpportunityValueSchema.optional(),
|
|
1563
|
-
expectedCloseOn:
|
|
1564
|
-
probability:
|
|
1696
|
+
expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
|
|
1697
|
+
probability: z8.number().int().min(0).max(100).optional().describe(
|
|
1565
1698
|
"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)."
|
|
1566
1699
|
),
|
|
1567
1700
|
lostReasonId: positiveId.optional().describe(
|
|
@@ -1573,7 +1706,7 @@ var updateOpportunitySchema = z7.object({
|
|
|
1573
1706
|
teamId: positiveId.nullable().optional().describe(
|
|
1574
1707
|
"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."
|
|
1575
1708
|
),
|
|
1576
|
-
fields:
|
|
1709
|
+
fields: z8.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_opportunity"))
|
|
1577
1710
|
});
|
|
1578
1711
|
async function updateOpportunity(input) {
|
|
1579
1712
|
const { id, partyId, milestoneId, ownerId, teamId, lostReasonId, fields, ...rest } = input;
|
|
@@ -1609,25 +1742,23 @@ var { schema: deleteOpportunitySchema, handler: deleteOpportunity } = defineDele
|
|
|
1609
1742
|
});
|
|
1610
1743
|
|
|
1611
1744
|
// src/tools/projects.ts
|
|
1612
|
-
import { z as
|
|
1613
|
-
var listProjectsSchema =
|
|
1614
|
-
status:
|
|
1615
|
-
embed:
|
|
1616
|
-
|
|
1617
|
-
perPage: z8.number().int().min(1).max(100).optional().default(25)
|
|
1745
|
+
import { z as z9 } from "zod";
|
|
1746
|
+
var listProjectsSchema = z9.object({
|
|
1747
|
+
status: z9.enum(["OPEN", "CLOSED"]).optional(),
|
|
1748
|
+
embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
|
|
1749
|
+
...paginationFields
|
|
1618
1750
|
});
|
|
1619
1751
|
async function listProjects(input) {
|
|
1620
|
-
|
|
1752
|
+
return capsuleGetList("/kases", {
|
|
1621
1753
|
status: input.status,
|
|
1622
1754
|
embed: input.embed,
|
|
1623
1755
|
page: input.page,
|
|
1624
1756
|
perPage: input.perPage
|
|
1625
1757
|
});
|
|
1626
|
-
return { ...data, nextPage };
|
|
1627
1758
|
}
|
|
1628
|
-
var getProjectSchema =
|
|
1759
|
+
var getProjectSchema = z9.object({
|
|
1629
1760
|
id: positiveId,
|
|
1630
|
-
embed:
|
|
1761
|
+
embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
|
|
1631
1762
|
});
|
|
1632
1763
|
async function getProject(input) {
|
|
1633
1764
|
const { data } = await capsuleGet(`/kases/${input.id}`, {
|
|
@@ -1635,20 +1766,20 @@ async function getProject(input) {
|
|
|
1635
1766
|
});
|
|
1636
1767
|
return data;
|
|
1637
1768
|
}
|
|
1638
|
-
var getProjectsSchema =
|
|
1639
|
-
ids:
|
|
1769
|
+
var getProjectsSchema = z9.object({
|
|
1770
|
+
ids: z9.array(positiveId).min(1).max(50).describe(
|
|
1640
1771
|
"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."
|
|
1641
1772
|
),
|
|
1642
|
-
embed:
|
|
1773
|
+
embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
|
|
1643
1774
|
});
|
|
1644
1775
|
async function getProjects(input) {
|
|
1645
1776
|
return chunkedMultiGet("/kases", "kases", input.ids, { embed: input.embed });
|
|
1646
1777
|
}
|
|
1647
|
-
var createProjectSchema =
|
|
1648
|
-
name:
|
|
1778
|
+
var createProjectSchema = z9.object({
|
|
1779
|
+
name: z9.string().min(1),
|
|
1649
1780
|
partyId: positiveId.describe("ID of the party linked to this project"),
|
|
1650
|
-
description:
|
|
1651
|
-
status:
|
|
1781
|
+
description: z9.string().optional(),
|
|
1782
|
+
status: z9.enum(["OPEN", "CLOSED"]).optional().describe("Defaults to OPEN when omitted."),
|
|
1652
1783
|
ownerId: positiveId.optional().describe(
|
|
1653
1784
|
"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."
|
|
1654
1785
|
),
|
|
@@ -1658,8 +1789,8 @@ var createProjectSchema = z8.object({
|
|
|
1658
1789
|
stageId: positiveId.optional().describe(
|
|
1659
1790
|
"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."
|
|
1660
1791
|
),
|
|
1661
|
-
expectedCloseOn:
|
|
1662
|
-
fields:
|
|
1792
|
+
expectedCloseOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
|
|
1793
|
+
fields: z9.array(CustomFieldWriteSchema).optional().describe(
|
|
1663
1794
|
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."
|
|
1664
1795
|
)
|
|
1665
1796
|
});
|
|
@@ -1677,11 +1808,11 @@ async function createProject(input) {
|
|
|
1677
1808
|
if (mappedFields !== void 0) body["fields"] = mappedFields;
|
|
1678
1809
|
return capsulePost("/kases", { kase: body });
|
|
1679
1810
|
}
|
|
1680
|
-
var updateProjectSchema =
|
|
1811
|
+
var updateProjectSchema = z9.object({
|
|
1681
1812
|
id: positiveId,
|
|
1682
|
-
name:
|
|
1683
|
-
description:
|
|
1684
|
-
status:
|
|
1813
|
+
name: z9.string().min(1).optional(),
|
|
1814
|
+
description: z9.string().optional(),
|
|
1815
|
+
status: z9.enum(["OPEN", "CLOSED"]).optional(),
|
|
1685
1816
|
partyId: positiveId.optional().describe(
|
|
1686
1817
|
"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`."
|
|
1687
1818
|
),
|
|
@@ -1694,8 +1825,8 @@ var updateProjectSchema = z8.object({
|
|
|
1694
1825
|
stageId: positiveId.nullable().optional().describe(
|
|
1695
1826
|
"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."
|
|
1696
1827
|
),
|
|
1697
|
-
expectedCloseOn:
|
|
1698
|
-
fields:
|
|
1828
|
+
expectedCloseOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
|
|
1829
|
+
fields: z9.array(CustomFieldWriteSchema).optional().describe(
|
|
1699
1830
|
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."
|
|
1700
1831
|
)
|
|
1701
1832
|
});
|
|
@@ -1734,23 +1865,22 @@ var { schema: deleteProjectSchema, handler: deleteProject } = defineDelete({
|
|
|
1734
1865
|
});
|
|
1735
1866
|
|
|
1736
1867
|
// src/tools/tasks.ts
|
|
1737
|
-
import { z as
|
|
1738
|
-
var listTasksSchema =
|
|
1868
|
+
import { z as z10 } from "zod";
|
|
1869
|
+
var listTasksSchema = z10.object({
|
|
1739
1870
|
// Note: Capsule has a third internal status `PENDING` (a task that's
|
|
1740
1871
|
// part of an active track but not yet "open"), but it can only be
|
|
1741
1872
|
// reached via track machinery — it is NOT directly settable by
|
|
1742
1873
|
// /tasks PUT, and a list filter for it returns the same as OPEN
|
|
1743
1874
|
// anyway. We expose only the two values that are actually filterable
|
|
1744
1875
|
// by the v2 API.
|
|
1745
|
-
status:
|
|
1876
|
+
status: z10.enum(["OPEN", "COMPLETED"]).optional().describe(
|
|
1746
1877
|
"Defaults to OPEN when omitted. Pass COMPLETED to filter to completed tasks, or 'OPEN' explicitly."
|
|
1747
1878
|
),
|
|
1748
1879
|
ownerId: positiveId.optional().describe("Filter to tasks owned by this user ID"),
|
|
1749
|
-
|
|
1750
|
-
perPage: z9.number().int().min(1).max(100).optional().default(25)
|
|
1880
|
+
...paginationFields
|
|
1751
1881
|
});
|
|
1752
1882
|
async function listTasks(input) {
|
|
1753
|
-
|
|
1883
|
+
return capsuleGetList("/tasks", {
|
|
1754
1884
|
// Default 'OPEN' applied here (not via zod .default()) so that
|
|
1755
1885
|
// z.infer keeps `status` optional for callers that omit it.
|
|
1756
1886
|
status: input.status ?? "OPEN",
|
|
@@ -1759,28 +1889,27 @@ async function listTasks(input) {
|
|
|
1759
1889
|
page: input.page,
|
|
1760
1890
|
perPage: input.perPage
|
|
1761
1891
|
});
|
|
1762
|
-
return { ...data, nextPage };
|
|
1763
1892
|
}
|
|
1764
|
-
var getTaskSchema =
|
|
1893
|
+
var getTaskSchema = z10.object({
|
|
1765
1894
|
id: positiveId.describe("Task ID")
|
|
1766
1895
|
});
|
|
1767
1896
|
async function getTask(input) {
|
|
1768
1897
|
const { data } = await capsuleGet(`/tasks/${input.id}`);
|
|
1769
1898
|
return data;
|
|
1770
1899
|
}
|
|
1771
|
-
var getTasksSchema =
|
|
1772
|
-
ids:
|
|
1900
|
+
var getTasksSchema = z10.object({
|
|
1901
|
+
ids: z10.array(positiveId).min(1).max(50).describe(
|
|
1773
1902
|
"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."
|
|
1774
1903
|
)
|
|
1775
1904
|
});
|
|
1776
1905
|
async function getTasks(input) {
|
|
1777
1906
|
return chunkedMultiGet("/tasks", "tasks", input.ids);
|
|
1778
1907
|
}
|
|
1779
|
-
var createTaskSchema =
|
|
1780
|
-
description:
|
|
1781
|
-
dueOn:
|
|
1782
|
-
dueTime:
|
|
1783
|
-
detail:
|
|
1908
|
+
var createTaskSchema = z10.object({
|
|
1909
|
+
description: z10.string().min(1),
|
|
1910
|
+
dueOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("YYYY-MM-DD"),
|
|
1911
|
+
dueTime: z10.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
|
|
1912
|
+
detail: z10.string().optional(),
|
|
1784
1913
|
ownerId: positiveId.optional().describe(
|
|
1785
1914
|
"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."
|
|
1786
1915
|
),
|
|
@@ -1789,10 +1918,7 @@ var createTaskSchema = z9.object({
|
|
|
1789
1918
|
projectId: positiveId.optional().describe("Link task to a project (mutually exclusive with partyId/opportunityId)")
|
|
1790
1919
|
});
|
|
1791
1920
|
async function createTask(input) {
|
|
1792
|
-
|
|
1793
|
-
if (linked.length > 1) {
|
|
1794
|
-
throw new Error("Provide at most one of partyId, opportunityId, or projectId");
|
|
1795
|
-
}
|
|
1921
|
+
assertSingleParentRef("create_task", input);
|
|
1796
1922
|
const { ownerId, partyId, opportunityId, projectId, ...rest } = input;
|
|
1797
1923
|
const body = { ...rest };
|
|
1798
1924
|
setRef(body, "owner", ownerId);
|
|
@@ -1801,16 +1927,16 @@ async function createTask(input) {
|
|
|
1801
1927
|
setRef(body, "kase", projectId);
|
|
1802
1928
|
return capsulePost("/tasks", { task: body });
|
|
1803
1929
|
}
|
|
1804
|
-
var updateTaskSchema =
|
|
1930
|
+
var updateTaskSchema = z10.object({
|
|
1805
1931
|
id: positiveId,
|
|
1806
|
-
description:
|
|
1807
|
-
dueOn:
|
|
1808
|
-
dueTime:
|
|
1809
|
-
detail:
|
|
1932
|
+
description: z10.string().min(1).optional(),
|
|
1933
|
+
dueOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
|
|
1934
|
+
dueTime: z10.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
|
|
1935
|
+
detail: z10.string().optional(),
|
|
1810
1936
|
// Capsule rejects direct sets of `PENDING` (which is a track-machinery
|
|
1811
1937
|
// internal state) with 422 "cannot set task status to PENDING".
|
|
1812
1938
|
// Only OPEN and COMPLETED are settable here.
|
|
1813
|
-
status:
|
|
1939
|
+
status: z10.enum(["OPEN", "COMPLETED"]).optional().describe(
|
|
1814
1940
|
"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)."
|
|
1815
1941
|
),
|
|
1816
1942
|
ownerId: positiveId.optional().describe(
|
|
@@ -1828,12 +1954,7 @@ var updateTaskSchema = z9.object({
|
|
|
1828
1954
|
});
|
|
1829
1955
|
async function updateTask(input) {
|
|
1830
1956
|
const { id, ownerId, partyId, opportunityId, projectId, ...rest } = input;
|
|
1831
|
-
|
|
1832
|
-
if (setCount > 1) {
|
|
1833
|
-
throw new Error(
|
|
1834
|
-
"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')"
|
|
1835
|
-
);
|
|
1836
|
-
}
|
|
1957
|
+
assertSingleParentRef("update_task", { partyId, opportunityId, projectId });
|
|
1837
1958
|
const body = {};
|
|
1838
1959
|
for (const [k, v] of Object.entries(rest)) {
|
|
1839
1960
|
if (v !== void 0) body[k] = v;
|
|
@@ -1844,7 +1965,7 @@ async function updateTask(input) {
|
|
|
1844
1965
|
setNullableRef(body, "kase", projectId);
|
|
1845
1966
|
return capsulePut(`/tasks/${id}`, { task: body });
|
|
1846
1967
|
}
|
|
1847
|
-
var completeTaskSchema =
|
|
1968
|
+
var completeTaskSchema = z10.object({
|
|
1848
1969
|
id: positiveId
|
|
1849
1970
|
});
|
|
1850
1971
|
async function completeTask(input) {
|
|
@@ -1852,8 +1973,8 @@ async function completeTask(input) {
|
|
|
1852
1973
|
task: { status: "COMPLETED" }
|
|
1853
1974
|
});
|
|
1854
1975
|
}
|
|
1855
|
-
var batchCompleteTaskSchema =
|
|
1856
|
-
ids:
|
|
1976
|
+
var batchCompleteTaskSchema = z10.object({
|
|
1977
|
+
ids: z10.array(positiveId).min(1).max(50).describe(
|
|
1857
1978
|
"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."
|
|
1858
1979
|
)
|
|
1859
1980
|
});
|
|
@@ -1867,77 +1988,59 @@ var { schema: deleteTaskSchema, handler: deleteTask } = defineDelete({
|
|
|
1867
1988
|
});
|
|
1868
1989
|
|
|
1869
1990
|
// src/tools/entries.ts
|
|
1870
|
-
import { z as
|
|
1991
|
+
import { z as z11 } from "zod";
|
|
1871
1992
|
var listEntriesPagination = {
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
embed: z10.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
|
|
1993
|
+
...paginationFields,
|
|
1994
|
+
embed: z11.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
|
|
1875
1995
|
};
|
|
1876
|
-
var listPartyEntriesSchema =
|
|
1996
|
+
var listPartyEntriesSchema = z11.object({
|
|
1877
1997
|
partyId: positiveId,
|
|
1878
1998
|
...listEntriesPagination,
|
|
1879
|
-
includeLinkedPersons:
|
|
1999
|
+
includeLinkedPersons: z11.boolean().optional().describe(
|
|
1880
2000
|
"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."
|
|
1881
2001
|
)
|
|
1882
2002
|
});
|
|
2003
|
+
var PER_PARTY_FETCH_CAP = 100;
|
|
1883
2004
|
async function fanOutPartyEntries(partyIds, embed, perPage) {
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
const id = partyIds[i];
|
|
1893
|
-
const { data, nextPage } = await capsuleGet(
|
|
1894
|
-
`/parties/${id}/entries`,
|
|
1895
|
-
{
|
|
1896
|
-
embed,
|
|
1897
|
-
page: 1,
|
|
1898
|
-
perPage
|
|
1899
|
-
}
|
|
1900
|
-
);
|
|
1901
|
-
results[i] = { entries: data.entries, nextPage };
|
|
1902
|
-
}
|
|
1903
|
-
}
|
|
1904
|
-
const workers = [];
|
|
1905
|
-
for (let w = 0; w < Math.min(concurrency, partyIds.length); w++) {
|
|
1906
|
-
workers.push(worker());
|
|
1907
|
-
}
|
|
1908
|
-
await Promise.all(workers);
|
|
1909
|
-
return results;
|
|
2005
|
+
return mapWithConcurrency(partyIds, getBatchConcurrency(), async (id) => {
|
|
2006
|
+
const { data, nextPage } = await capsuleGet(`/parties/${id}/entries`, {
|
|
2007
|
+
embed,
|
|
2008
|
+
page: 1,
|
|
2009
|
+
perPage
|
|
2010
|
+
});
|
|
2011
|
+
return { entries: data.entries, nextPage };
|
|
2012
|
+
});
|
|
1910
2013
|
}
|
|
1911
2014
|
function mergedTimelineCandidatePerParty(page, perPage) {
|
|
1912
|
-
return Math.min(page * perPage,
|
|
2015
|
+
return Math.min(page * perPage, PER_PARTY_FETCH_CAP);
|
|
1913
2016
|
}
|
|
1914
2017
|
function mergedTimelineNextPage(page, perPage, mergedLength, upstreamHasNextPage) {
|
|
1915
2018
|
const requestedWindowEnd = page * perPage;
|
|
1916
2019
|
if (mergedLength > requestedWindowEnd) return page + 1;
|
|
1917
|
-
const nextWindowWithinCap = requestedWindowEnd <
|
|
2020
|
+
const nextWindowWithinCap = requestedWindowEnd < PER_PARTY_FETCH_CAP;
|
|
1918
2021
|
if (nextWindowWithinCap && upstreamHasNextPage) return page + 1;
|
|
1919
2022
|
return void 0;
|
|
1920
2023
|
}
|
|
1921
2024
|
async function listPartyEntries(input) {
|
|
1922
2025
|
const { partyId, embed, page, perPage, includeLinkedPersons } = input;
|
|
1923
2026
|
if (!includeLinkedPersons) {
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
2027
|
+
return capsuleGetList(`/parties/${partyId}/entries`, {
|
|
2028
|
+
embed,
|
|
2029
|
+
page,
|
|
2030
|
+
perPage
|
|
2031
|
+
});
|
|
1929
2032
|
}
|
|
1930
2033
|
const { data: peopleData } = await capsuleGet(
|
|
1931
2034
|
`/parties/${partyId}/people`,
|
|
1932
|
-
{ page: 1, perPage:
|
|
2035
|
+
{ page: 1, perPage: PER_PARTY_FETCH_CAP }
|
|
1933
2036
|
);
|
|
1934
2037
|
const peopleIds = (peopleData.parties ?? []).map((p) => p.id);
|
|
1935
2038
|
if (peopleIds.length === 0) {
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
2039
|
+
return capsuleGetList(`/parties/${partyId}/entries`, {
|
|
2040
|
+
embed,
|
|
2041
|
+
page,
|
|
2042
|
+
perPage
|
|
2043
|
+
});
|
|
1941
2044
|
}
|
|
1942
2045
|
const targetIds = [partyId, ...peopleIds];
|
|
1943
2046
|
const perPartyPages = await fanOutPartyEntries(
|
|
@@ -1972,31 +2075,31 @@ async function listPartyEntries(input) {
|
|
|
1972
2075
|
);
|
|
1973
2076
|
return { entries: slice, ...nextPage !== void 0 ? { nextPage } : {} };
|
|
1974
2077
|
}
|
|
1975
|
-
var listOpportunityEntriesSchema =
|
|
2078
|
+
var listOpportunityEntriesSchema = z11.object({
|
|
1976
2079
|
opportunityId: positiveId,
|
|
1977
2080
|
...listEntriesPagination
|
|
1978
2081
|
});
|
|
1979
2082
|
async function listOpportunityEntries(input) {
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
2083
|
+
return capsuleGetList(`/opportunities/${input.opportunityId}/entries`, {
|
|
2084
|
+
embed: input.embed,
|
|
2085
|
+
page: input.page,
|
|
2086
|
+
perPage: input.perPage
|
|
2087
|
+
});
|
|
1985
2088
|
}
|
|
1986
|
-
var listProjectEntriesSchema =
|
|
2089
|
+
var listProjectEntriesSchema = z11.object({
|
|
1987
2090
|
projectId: positiveId,
|
|
1988
2091
|
...listEntriesPagination
|
|
1989
2092
|
});
|
|
1990
2093
|
async function listProjectEntries(input) {
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
2094
|
+
return capsuleGetList(`/kases/${input.projectId}/entries`, {
|
|
2095
|
+
embed: input.embed,
|
|
2096
|
+
page: input.page,
|
|
2097
|
+
perPage: input.perPage
|
|
2098
|
+
});
|
|
1996
2099
|
}
|
|
1997
|
-
var getEntrySchema =
|
|
2100
|
+
var getEntrySchema = z11.object({
|
|
1998
2101
|
id: positiveId,
|
|
1999
|
-
embed:
|
|
2102
|
+
embed: z11.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
|
|
2000
2103
|
});
|
|
2001
2104
|
async function getEntry(input) {
|
|
2002
2105
|
const { data } = await capsuleGet(`/entries/${input.id}`, {
|
|
@@ -2004,34 +2107,30 @@ async function getEntry(input) {
|
|
|
2004
2107
|
});
|
|
2005
2108
|
return data;
|
|
2006
2109
|
}
|
|
2007
|
-
var listEntriesSchema =
|
|
2110
|
+
var listEntriesSchema = z11.object({
|
|
2008
2111
|
...listEntriesPagination
|
|
2009
2112
|
});
|
|
2010
2113
|
async function listEntries(input) {
|
|
2011
|
-
|
|
2114
|
+
return capsuleGetList("/entries", {
|
|
2012
2115
|
embed: input.embed,
|
|
2013
2116
|
page: input.page,
|
|
2014
2117
|
perPage: input.perPage
|
|
2015
2118
|
});
|
|
2016
|
-
return { ...data, nextPage };
|
|
2017
2119
|
}
|
|
2018
|
-
var addNoteSchema =
|
|
2019
|
-
content:
|
|
2120
|
+
var addNoteSchema = z11.object({
|
|
2121
|
+
content: z11.string().min(1).describe(
|
|
2020
2122
|
"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."
|
|
2021
2123
|
),
|
|
2022
2124
|
partyId: positiveId.optional().describe("Link note to a party (mutually exclusive with opportunityId/projectId)"),
|
|
2023
2125
|
opportunityId: positiveId.optional().describe("Link note to an opportunity (mutually exclusive with partyId/projectId)"),
|
|
2024
2126
|
projectId: positiveId.optional().describe("Link note to a project (mutually exclusive with partyId/opportunityId)"),
|
|
2025
|
-
entryAt:
|
|
2127
|
+
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(
|
|
2026
2128
|
"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)."
|
|
2027
2129
|
)
|
|
2028
2130
|
});
|
|
2029
2131
|
async function addNote(input) {
|
|
2030
2132
|
const { content, partyId, opportunityId, projectId, entryAt } = input;
|
|
2031
|
-
|
|
2032
|
-
if (linked.length !== 1) {
|
|
2033
|
-
throw new Error("Provide exactly one of partyId, opportunityId, or projectId");
|
|
2034
|
-
}
|
|
2133
|
+
assertSingleParentRef("add_note", input, { required: true });
|
|
2035
2134
|
const body = { type: "note", content };
|
|
2036
2135
|
setRef(body, "party", partyId);
|
|
2037
2136
|
setRef(body, "opportunity", opportunityId);
|
|
@@ -2039,12 +2138,12 @@ async function addNote(input) {
|
|
|
2039
2138
|
if (entryAt !== void 0) body["entryAt"] = entryAt;
|
|
2040
2139
|
return capsulePost("/entries", { entry: body });
|
|
2041
2140
|
}
|
|
2042
|
-
var updateEntrySchema =
|
|
2141
|
+
var updateEntrySchema = z11.object({
|
|
2043
2142
|
id: positiveId.describe("Entry ID to update"),
|
|
2044
|
-
content:
|
|
2143
|
+
content: z11.string().min(1).optional().describe(
|
|
2045
2144
|
"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."
|
|
2046
2145
|
),
|
|
2047
|
-
subject:
|
|
2146
|
+
subject: z11.string().optional().describe(
|
|
2048
2147
|
"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`."
|
|
2049
2148
|
)
|
|
2050
2149
|
});
|
|
@@ -2066,62 +2165,50 @@ var { schema: deleteEntrySchema, handler: deleteEntry } = defineDelete({
|
|
|
2066
2165
|
});
|
|
2067
2166
|
|
|
2068
2167
|
// src/tools/pipelines.ts
|
|
2069
|
-
import { z as
|
|
2070
|
-
var
|
|
2071
|
-
page: z11.number().int().positive().optional(),
|
|
2072
|
-
perPage: z11.number().int().min(1).max(100).optional()
|
|
2073
|
-
};
|
|
2074
|
-
var listPipelinesSchema = z11.object({ ...paginationFields });
|
|
2168
|
+
import { z as z12 } from "zod";
|
|
2169
|
+
var listPipelinesSchema = z12.object({ ...paginationFieldsNoDefaults });
|
|
2075
2170
|
async function listPipelines(input) {
|
|
2076
|
-
|
|
2171
|
+
return capsuleGetCachedList("/pipelines", {
|
|
2077
2172
|
page: input.page ?? 1,
|
|
2078
2173
|
perPage: input.perPage ?? 100
|
|
2079
2174
|
});
|
|
2080
|
-
return { ...data, nextPage };
|
|
2081
2175
|
}
|
|
2082
|
-
var listMilestonesSchema =
|
|
2176
|
+
var listMilestonesSchema = z12.object({
|
|
2083
2177
|
pipelineId: positiveId,
|
|
2084
|
-
...
|
|
2178
|
+
...paginationFieldsNoDefaults
|
|
2085
2179
|
});
|
|
2086
2180
|
async function listMilestones(input) {
|
|
2087
|
-
|
|
2181
|
+
return capsuleGetCachedList(
|
|
2088
2182
|
`/pipelines/${input.pipelineId}/milestones`,
|
|
2089
2183
|
{ page: input.page ?? 1, perPage: input.perPage ?? 100 }
|
|
2090
2184
|
);
|
|
2091
|
-
return { ...data, nextPage };
|
|
2092
2185
|
}
|
|
2093
2186
|
|
|
2094
2187
|
// src/tools/boards.ts
|
|
2095
|
-
import { z as
|
|
2096
|
-
var
|
|
2097
|
-
page: z12.number().int().positive().optional(),
|
|
2098
|
-
perPage: z12.number().int().min(1).max(100).optional()
|
|
2099
|
-
};
|
|
2100
|
-
var listBoardsSchema = z12.object({ ...paginationFields2 });
|
|
2188
|
+
import { z as z13 } from "zod";
|
|
2189
|
+
var listBoardsSchema = z13.object({ ...paginationFieldsNoDefaults });
|
|
2101
2190
|
async function listBoards(input) {
|
|
2102
|
-
|
|
2191
|
+
return capsuleGetCachedList("/boards", {
|
|
2103
2192
|
page: input.page ?? 1,
|
|
2104
2193
|
perPage: input.perPage ?? 100
|
|
2105
2194
|
});
|
|
2106
|
-
return { ...data, nextPage };
|
|
2107
2195
|
}
|
|
2108
|
-
var listStagesSchema =
|
|
2196
|
+
var listStagesSchema = z13.object({
|
|
2109
2197
|
boardId: positiveId.optional().describe(
|
|
2110
2198
|
"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."
|
|
2111
2199
|
),
|
|
2112
|
-
...
|
|
2200
|
+
...paginationFieldsNoDefaults
|
|
2113
2201
|
});
|
|
2114
2202
|
async function listStages(input) {
|
|
2115
2203
|
const path = input.boardId !== void 0 ? `/boards/${input.boardId}/stages` : "/stages";
|
|
2116
|
-
|
|
2204
|
+
return capsuleGetCachedList(path, {
|
|
2117
2205
|
page: input.page ?? 1,
|
|
2118
2206
|
perPage: input.perPage ?? 100
|
|
2119
2207
|
});
|
|
2120
|
-
return { ...data, nextPage };
|
|
2121
2208
|
}
|
|
2122
2209
|
|
|
2123
2210
|
// src/tools/tags.ts
|
|
2124
|
-
import { z as
|
|
2211
|
+
import { z as z14 } from "zod";
|
|
2125
2212
|
var TAG_LIST_PATH = {
|
|
2126
2213
|
parties: "/parties/tags",
|
|
2127
2214
|
opportunities: "/opportunities/tags",
|
|
@@ -2132,24 +2219,22 @@ var ENTITY_TO_WRAPPER = {
|
|
|
2132
2219
|
opportunities: "opportunity",
|
|
2133
2220
|
kases: "kase"
|
|
2134
2221
|
};
|
|
2135
|
-
var TagEntity =
|
|
2136
|
-
var listTagsSchema =
|
|
2137
|
-
entity:
|
|
2138
|
-
|
|
2139
|
-
perPage: z13.number().int().min(1).max(100).optional()
|
|
2222
|
+
var TagEntity = z14.enum(["parties", "opportunities", "kases"]).describe("Which entity type. Use 'kases' for projects (Capsule's legacy path name).");
|
|
2223
|
+
var listTagsSchema = z14.object({
|
|
2224
|
+
entity: z14.enum(["parties", "opportunities", "kases"]).describe("The resource type to list tags for"),
|
|
2225
|
+
...paginationFieldsNoDefaults
|
|
2140
2226
|
});
|
|
2141
2227
|
async function listTags(input) {
|
|
2142
2228
|
const path = TAG_LIST_PATH[input.entity];
|
|
2143
|
-
|
|
2229
|
+
return capsuleGetCachedList(path, {
|
|
2144
2230
|
page: input.page ?? 1,
|
|
2145
2231
|
perPage: input.perPage ?? 100
|
|
2146
2232
|
});
|
|
2147
|
-
return { ...data, nextPage };
|
|
2148
2233
|
}
|
|
2149
|
-
var addTagSchema =
|
|
2234
|
+
var addTagSchema = z14.object({
|
|
2150
2235
|
entity: TagEntity,
|
|
2151
2236
|
entityId: positiveId.describe("The party/opportunity/kase id."),
|
|
2152
|
-
tagName:
|
|
2237
|
+
tagName: z14.string().min(1).describe(
|
|
2153
2238
|
"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."
|
|
2154
2239
|
)
|
|
2155
2240
|
});
|
|
@@ -2162,7 +2247,7 @@ async function addTag(input) {
|
|
|
2162
2247
|
invalidateByPrefix(TAG_LIST_PATH[entity], "add_tag");
|
|
2163
2248
|
return result;
|
|
2164
2249
|
}
|
|
2165
|
-
var removeTagByIdSchema =
|
|
2250
|
+
var removeTagByIdSchema = z14.object({
|
|
2166
2251
|
entity: TagEntity,
|
|
2167
2252
|
entityId: positiveId.describe("The party/opportunity/kase id."),
|
|
2168
2253
|
tagId: positiveId.describe(
|
|
@@ -2193,7 +2278,7 @@ async function removeTagById(input) {
|
|
|
2193
2278
|
invalidateByPrefix(TAG_LIST_PATH[entity], "remove_tag_by_id");
|
|
2194
2279
|
return result;
|
|
2195
2280
|
}
|
|
2196
|
-
var deleteTagDefinitionSchema =
|
|
2281
|
+
var deleteTagDefinitionSchema = z14.object({
|
|
2197
2282
|
entity: TagEntity,
|
|
2198
2283
|
tagId: positiveId.describe(
|
|
2199
2284
|
"The tag definition's id (from list_tags, or embed='tags' on a record). NOT an entity id."
|
|
@@ -2229,44 +2314,41 @@ var { schema: batchRemoveTagByIdSchema, handler: batchRemoveTagById } = defineBa
|
|
|
2229
2314
|
});
|
|
2230
2315
|
|
|
2231
2316
|
// src/tools/users.ts
|
|
2232
|
-
import { z as
|
|
2233
|
-
var listUsersSchema =
|
|
2234
|
-
|
|
2235
|
-
perPage: z14.number().int().min(1).max(100).optional()
|
|
2317
|
+
import { z as z15 } from "zod";
|
|
2318
|
+
var listUsersSchema = z15.object({
|
|
2319
|
+
...paginationFieldsNoDefaults
|
|
2236
2320
|
});
|
|
2237
2321
|
async function listUsers(input) {
|
|
2238
|
-
|
|
2322
|
+
return capsuleGetCachedList("/users", {
|
|
2239
2323
|
page: input.page ?? 1,
|
|
2240
2324
|
perPage: input.perPage ?? 100
|
|
2241
2325
|
});
|
|
2242
|
-
return { ...data, nextPage };
|
|
2243
2326
|
}
|
|
2244
|
-
var getCurrentUserSchema =
|
|
2327
|
+
var getCurrentUserSchema = z15.object({});
|
|
2245
2328
|
async function getCurrentUser(_input) {
|
|
2246
2329
|
const { data } = await capsuleGet("/users/current");
|
|
2247
2330
|
return data;
|
|
2248
2331
|
}
|
|
2249
2332
|
|
|
2250
2333
|
// src/tools/filters.ts
|
|
2251
|
-
import { z as
|
|
2252
|
-
var FilterConditionSchema =
|
|
2253
|
-
field:
|
|
2334
|
+
import { z as z16 } from "zod";
|
|
2335
|
+
var FilterConditionSchema = z16.object({
|
|
2336
|
+
field: z16.string().describe(
|
|
2254
2337
|
"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"
|
|
2255
2338
|
),
|
|
2256
|
-
operator:
|
|
2339
|
+
operator: z16.string().describe(
|
|
2257
2340
|
"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."
|
|
2258
2341
|
),
|
|
2259
|
-
value:
|
|
2342
|
+
value: z16.union([z16.string(), z16.number(), z16.boolean(), z16.null()]).describe(
|
|
2260
2343
|
"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."
|
|
2261
2344
|
)
|
|
2262
2345
|
});
|
|
2263
|
-
var FilterInputSchema =
|
|
2264
|
-
conditions:
|
|
2346
|
+
var FilterInputSchema = z16.object({
|
|
2347
|
+
conditions: z16.array(FilterConditionSchema).min(1).describe(
|
|
2265
2348
|
"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)."
|
|
2266
2349
|
),
|
|
2267
|
-
embed:
|
|
2268
|
-
|
|
2269
|
-
perPage: z15.number().int().min(1).max(100).optional().default(25)
|
|
2350
|
+
embed: z16.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
|
|
2351
|
+
...paginationFields
|
|
2270
2352
|
});
|
|
2271
2353
|
async function runFilter(entityPath, input) {
|
|
2272
2354
|
const { data, nextPage } = await capsuleSearch(
|
|
@@ -2286,10 +2368,7 @@ async function filterParties(input) {
|
|
|
2286
2368
|
}
|
|
2287
2369
|
var filterOpportunitiesSchema = FilterInputSchema;
|
|
2288
2370
|
async function filterOpportunities(input) {
|
|
2289
|
-
return runFilter(
|
|
2290
|
-
"opportunities",
|
|
2291
|
-
input
|
|
2292
|
-
);
|
|
2371
|
+
return runFilter("opportunities", input);
|
|
2293
2372
|
}
|
|
2294
2373
|
var filterProjectsSchema = FilterInputSchema;
|
|
2295
2374
|
async function filterProjects(input) {
|
|
@@ -2297,139 +2376,126 @@ async function filterProjects(input) {
|
|
|
2297
2376
|
}
|
|
2298
2377
|
|
|
2299
2378
|
// src/tools/metadata.ts
|
|
2300
|
-
import { z as
|
|
2301
|
-
var
|
|
2302
|
-
|
|
2303
|
-
perPage:
|
|
2379
|
+
import { z as z17 } from "zod";
|
|
2380
|
+
var paginationFields2 = {
|
|
2381
|
+
...paginationFieldsNoDefaults,
|
|
2382
|
+
perPage: paginationFieldsNoDefaults.perPage.describe(
|
|
2383
|
+
"Page size, max 100. Defaults to 100 for reference data."
|
|
2384
|
+
)
|
|
2304
2385
|
};
|
|
2305
|
-
var listTeamsSchema =
|
|
2386
|
+
var listTeamsSchema = z17.object({ ...paginationFields2 });
|
|
2306
2387
|
async function listTeams(input) {
|
|
2307
|
-
|
|
2388
|
+
return capsuleGetCachedList("/teams", {
|
|
2308
2389
|
page: input.page ?? 1,
|
|
2309
2390
|
perPage: input.perPage ?? 100
|
|
2310
2391
|
});
|
|
2311
|
-
return { ...data, nextPage };
|
|
2312
2392
|
}
|
|
2313
|
-
var listLostReasonsSchema =
|
|
2393
|
+
var listLostReasonsSchema = z17.object({ ...paginationFields2 });
|
|
2314
2394
|
async function listLostReasons(input) {
|
|
2315
|
-
|
|
2395
|
+
return capsuleGetCachedList("/lostreasons", {
|
|
2316
2396
|
page: input.page ?? 1,
|
|
2317
2397
|
perPage: input.perPage ?? 100
|
|
2318
2398
|
});
|
|
2319
|
-
return { ...data, nextPage };
|
|
2320
2399
|
}
|
|
2321
|
-
var listActivityTypesSchema =
|
|
2400
|
+
var listActivityTypesSchema = z17.object({ ...paginationFields2 });
|
|
2322
2401
|
async function listActivityTypes(input) {
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
perPage: input.perPage ?? 100
|
|
2328
|
-
}
|
|
2329
|
-
);
|
|
2330
|
-
return { ...data, nextPage };
|
|
2402
|
+
return capsuleGetCachedList("/activitytypes", {
|
|
2403
|
+
page: input.page ?? 1,
|
|
2404
|
+
perPage: input.perPage ?? 100
|
|
2405
|
+
});
|
|
2331
2406
|
}
|
|
2332
|
-
var getSiteSchema =
|
|
2407
|
+
var getSiteSchema = z17.object({});
|
|
2333
2408
|
async function getSite(_input) {
|
|
2334
2409
|
const { data } = await capsuleGetCached("/site");
|
|
2335
2410
|
return data;
|
|
2336
2411
|
}
|
|
2337
|
-
var listTrackDefinitionsSchema =
|
|
2412
|
+
var listTrackDefinitionsSchema = z17.object({ ...paginationFields2 });
|
|
2338
2413
|
async function listTrackDefinitions(input) {
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
);
|
|
2343
|
-
return { ...data, nextPage };
|
|
2414
|
+
return capsuleGetCachedList("/trackdefinitions", {
|
|
2415
|
+
page: input.page ?? 1,
|
|
2416
|
+
perPage: input.perPage ?? 100
|
|
2417
|
+
});
|
|
2344
2418
|
}
|
|
2345
|
-
var listCategoriesSchema =
|
|
2419
|
+
var listCategoriesSchema = z17.object({ ...paginationFields2 });
|
|
2346
2420
|
async function listCategories(input) {
|
|
2347
|
-
|
|
2421
|
+
return capsuleGetCachedList("/categories", {
|
|
2348
2422
|
page: input.page ?? 1,
|
|
2349
2423
|
perPage: input.perPage ?? 100
|
|
2350
2424
|
});
|
|
2351
|
-
return { ...data, nextPage };
|
|
2352
2425
|
}
|
|
2353
|
-
var listGoalsSchema =
|
|
2426
|
+
var listGoalsSchema = z17.object({ ...paginationFields2 });
|
|
2354
2427
|
async function listGoals(input) {
|
|
2355
|
-
|
|
2428
|
+
return capsuleGetCachedList("/goals", {
|
|
2356
2429
|
page: input.page ?? 1,
|
|
2357
2430
|
perPage: input.perPage ?? 100
|
|
2358
2431
|
});
|
|
2359
|
-
return { ...data, nextPage };
|
|
2360
2432
|
}
|
|
2361
2433
|
|
|
2362
2434
|
// src/tools/audit.ts
|
|
2363
|
-
import { z as
|
|
2364
|
-
var listEmployeesSchema =
|
|
2435
|
+
import { z as z18 } from "zod";
|
|
2436
|
+
var listEmployeesSchema = z18.object({
|
|
2365
2437
|
partyId: positiveId.describe(
|
|
2366
2438
|
"The organisation's party id. Returns the people whose `organisation` field links to this party."
|
|
2367
2439
|
),
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
embed: z17.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
|
|
2440
|
+
...paginationFields,
|
|
2441
|
+
embed: z18.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
|
|
2371
2442
|
});
|
|
2372
2443
|
async function listEmployees(input) {
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2444
|
+
return capsuleGetList(`/parties/${input.partyId}/people`, {
|
|
2445
|
+
page: input.page,
|
|
2446
|
+
perPage: input.perPage,
|
|
2447
|
+
embed: input.embed
|
|
2448
|
+
});
|
|
2378
2449
|
}
|
|
2379
|
-
var DeletedSinceSchema =
|
|
2450
|
+
var DeletedSinceSchema = z18.string().describe(
|
|
2380
2451
|
"REQUIRED. ISO-8601 timestamp; only deletions on or after this point are returned. Example: '2026-01-01T00:00:00Z'."
|
|
2381
2452
|
);
|
|
2382
2453
|
var DeletedPagination = {
|
|
2383
2454
|
since: DeletedSinceSchema,
|
|
2384
|
-
|
|
2385
|
-
perPage: z17.number().int().min(1).max(100).optional().default(25)
|
|
2455
|
+
...paginationFields
|
|
2386
2456
|
};
|
|
2387
|
-
var listDeletedPartiesSchema =
|
|
2457
|
+
var listDeletedPartiesSchema = z18.object(DeletedPagination);
|
|
2388
2458
|
async function listDeletedParties(input) {
|
|
2389
|
-
|
|
2459
|
+
return capsuleGetList("/parties/deleted", {
|
|
2390
2460
|
since: input.since,
|
|
2391
2461
|
page: input.page,
|
|
2392
2462
|
perPage: input.perPage
|
|
2393
2463
|
});
|
|
2394
|
-
return { ...data, nextPage };
|
|
2395
2464
|
}
|
|
2396
|
-
var listDeletedOpportunitiesSchema =
|
|
2465
|
+
var listDeletedOpportunitiesSchema = z18.object(DeletedPagination);
|
|
2397
2466
|
async function listDeletedOpportunities(input) {
|
|
2398
|
-
|
|
2467
|
+
return capsuleGetList("/opportunities/deleted", {
|
|
2399
2468
|
since: input.since,
|
|
2400
2469
|
page: input.page,
|
|
2401
2470
|
perPage: input.perPage
|
|
2402
2471
|
});
|
|
2403
|
-
return { ...data, nextPage };
|
|
2404
2472
|
}
|
|
2405
|
-
var listDeletedProjectsSchema =
|
|
2473
|
+
var listDeletedProjectsSchema = z18.object(DeletedPagination);
|
|
2406
2474
|
async function listDeletedProjects(input) {
|
|
2407
|
-
|
|
2475
|
+
return capsuleGetList("/kases/deleted", {
|
|
2408
2476
|
since: input.since,
|
|
2409
2477
|
page: input.page,
|
|
2410
2478
|
perPage: input.perPage
|
|
2411
2479
|
});
|
|
2412
|
-
return { ...data, nextPage };
|
|
2413
2480
|
}
|
|
2414
2481
|
|
|
2415
2482
|
// src/tools/relationships.ts
|
|
2416
|
-
import { z as
|
|
2417
|
-
var RelationshipEntity =
|
|
2418
|
-
var listAdditionalPartiesSchema =
|
|
2483
|
+
import { z as z19 } from "zod";
|
|
2484
|
+
var RelationshipEntity = z19.enum(["opportunities", "kases"]).describe("Which entity has the additional-party links. Use 'kases' for projects.");
|
|
2485
|
+
var listAdditionalPartiesSchema = z19.object({
|
|
2419
2486
|
entity: RelationshipEntity,
|
|
2420
2487
|
entityId: positiveId.describe("ID of the opportunity or project."),
|
|
2421
|
-
embed:
|
|
2422
|
-
|
|
2423
|
-
perPage: z18.number().int().min(1).max(100).optional().default(25)
|
|
2488
|
+
embed: z19.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
|
|
2489
|
+
...paginationFields
|
|
2424
2490
|
});
|
|
2425
2491
|
async function listAdditionalParties(input) {
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2492
|
+
return capsuleGetList(`/${input.entity}/${input.entityId}/parties`, {
|
|
2493
|
+
embed: input.embed,
|
|
2494
|
+
page: input.page,
|
|
2495
|
+
perPage: input.perPage
|
|
2496
|
+
});
|
|
2431
2497
|
}
|
|
2432
|
-
var addAdditionalPartySchema =
|
|
2498
|
+
var addAdditionalPartySchema = z19.object({
|
|
2433
2499
|
entity: RelationshipEntity,
|
|
2434
2500
|
entityId: positiveId,
|
|
2435
2501
|
partyId: positiveId.describe(
|
|
@@ -2462,7 +2528,7 @@ async function addAdditionalParty(input) {
|
|
|
2462
2528
|
throw err;
|
|
2463
2529
|
}
|
|
2464
2530
|
}
|
|
2465
|
-
var removeAdditionalPartySchema =
|
|
2531
|
+
var removeAdditionalPartySchema = z19.object({
|
|
2466
2532
|
entity: RelationshipEntity,
|
|
2467
2533
|
entityId: positiveId,
|
|
2468
2534
|
partyId: positiveId,
|
|
@@ -2492,24 +2558,23 @@ async function removeAdditionalParty(input) {
|
|
|
2492
2558
|
})
|
|
2493
2559
|
);
|
|
2494
2560
|
}
|
|
2495
|
-
var listAssociatedProjectsSchema =
|
|
2561
|
+
var listAssociatedProjectsSchema = z19.object({
|
|
2496
2562
|
opportunityId: positiveId,
|
|
2497
|
-
embed:
|
|
2498
|
-
|
|
2499
|
-
perPage: z18.number().int().min(1).max(100).optional().default(25)
|
|
2563
|
+
embed: z19.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
|
|
2564
|
+
...paginationFields
|
|
2500
2565
|
});
|
|
2501
2566
|
async function listAssociatedProjects(input) {
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2567
|
+
return capsuleGetList(`/opportunities/${input.opportunityId}/kases`, {
|
|
2568
|
+
embed: input.embed,
|
|
2569
|
+
page: input.page,
|
|
2570
|
+
perPage: input.perPage
|
|
2571
|
+
});
|
|
2507
2572
|
}
|
|
2508
2573
|
|
|
2509
2574
|
// src/tools/custom-fields.ts
|
|
2510
|
-
import { z as
|
|
2511
|
-
var CustomFieldEntity =
|
|
2512
|
-
var listCustomFieldsSchema =
|
|
2575
|
+
import { z as z20 } from "zod";
|
|
2576
|
+
var CustomFieldEntity = z20.enum(["parties", "opportunities", "kases"]).describe("Which entity type's custom field schema to inspect. Use 'kases' for projects.");
|
|
2577
|
+
var listCustomFieldsSchema = z20.object({
|
|
2513
2578
|
entity: CustomFieldEntity
|
|
2514
2579
|
});
|
|
2515
2580
|
async function listCustomFields(input) {
|
|
@@ -2518,7 +2583,7 @@ async function listCustomFields(input) {
|
|
|
2518
2583
|
);
|
|
2519
2584
|
return data;
|
|
2520
2585
|
}
|
|
2521
|
-
var getCustomFieldSchema =
|
|
2586
|
+
var getCustomFieldSchema = z20.object({
|
|
2522
2587
|
entity: CustomFieldEntity,
|
|
2523
2588
|
fieldId: positiveId.describe("Custom field definition id.")
|
|
2524
2589
|
});
|
|
@@ -2530,9 +2595,9 @@ async function getCustomField(input) {
|
|
|
2530
2595
|
}
|
|
2531
2596
|
|
|
2532
2597
|
// src/tools/tracks.ts
|
|
2533
|
-
import { z as
|
|
2534
|
-
var TrackEntity =
|
|
2535
|
-
var listEntityTracksSchema =
|
|
2598
|
+
import { z as z21 } from "zod";
|
|
2599
|
+
var TrackEntity = z21.enum(["parties", "opportunities", "kases"]).describe("Use 'kases' for projects.");
|
|
2600
|
+
var listEntityTracksSchema = z21.object({
|
|
2536
2601
|
entity: TrackEntity,
|
|
2537
2602
|
entityId: positiveId
|
|
2538
2603
|
});
|
|
@@ -2542,20 +2607,20 @@ async function listEntityTracks(input) {
|
|
|
2542
2607
|
);
|
|
2543
2608
|
return data;
|
|
2544
2609
|
}
|
|
2545
|
-
var showTrackSchema =
|
|
2610
|
+
var showTrackSchema = z21.object({
|
|
2546
2611
|
trackId: positiveId
|
|
2547
2612
|
});
|
|
2548
2613
|
async function showTrack(input) {
|
|
2549
2614
|
const { data } = await capsuleGet(`/tracks/${input.trackId}`);
|
|
2550
2615
|
return data;
|
|
2551
2616
|
}
|
|
2552
|
-
var applyTrackSchema =
|
|
2553
|
-
entity:
|
|
2617
|
+
var applyTrackSchema = z21.object({
|
|
2618
|
+
entity: z21.enum(["opportunities", "kases"]).describe("Which entity to apply the track to. Use 'kases' for projects."),
|
|
2554
2619
|
entityId: positiveId,
|
|
2555
2620
|
trackDefinitionId: positiveId.describe(
|
|
2556
2621
|
"The trackDefinition to apply (from list_track_definitions). Auto-creates task definitions on the target entity per the track's rules."
|
|
2557
2622
|
),
|
|
2558
|
-
startDate:
|
|
2623
|
+
startDate: z21.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe(
|
|
2559
2624
|
"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."
|
|
2560
2625
|
)
|
|
2561
2626
|
});
|
|
@@ -2568,9 +2633,9 @@ async function applyTrack(input) {
|
|
|
2568
2633
|
if (input.startDate !== void 0) track["trackDateOn"] = input.startDate;
|
|
2569
2634
|
return capsulePost("/tracks", { track });
|
|
2570
2635
|
}
|
|
2571
|
-
var updateTrackSchema =
|
|
2636
|
+
var updateTrackSchema = z21.object({
|
|
2572
2637
|
trackId: positiveId,
|
|
2573
|
-
fields:
|
|
2638
|
+
fields: z21.record(z21.string(), z21.unknown()).describe(
|
|
2574
2639
|
"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."
|
|
2575
2640
|
)
|
|
2576
2641
|
});
|
|
@@ -2582,7 +2647,7 @@ async function updateTrack(input) {
|
|
|
2582
2647
|
track: input.fields
|
|
2583
2648
|
});
|
|
2584
2649
|
}
|
|
2585
|
-
var removeTrackSchema =
|
|
2650
|
+
var removeTrackSchema = z21.object({
|
|
2586
2651
|
trackId: positiveId,
|
|
2587
2652
|
confirm: confirmFlag().describe(
|
|
2588
2653
|
"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."
|
|
@@ -2600,13 +2665,13 @@ async function removeTrack(input) {
|
|
|
2600
2665
|
}
|
|
2601
2666
|
|
|
2602
2667
|
// src/tools/attachments.ts
|
|
2603
|
-
import { z as
|
|
2668
|
+
import { z as z22 } from "zod";
|
|
2604
2669
|
var DEFAULT_MAX_SIZE_BYTES = 5 * 1024 * 1024;
|
|
2605
2670
|
var HARD_MAX_SIZE_BYTES = 25 * 1024 * 1024;
|
|
2606
2671
|
var HARD_MAX_BASE64_CHARS = Math.ceil(HARD_MAX_SIZE_BYTES / 3) * 4;
|
|
2607
|
-
var getAttachmentSchema =
|
|
2672
|
+
var getAttachmentSchema = z22.object({
|
|
2608
2673
|
id: positiveId.describe("Attachment ID."),
|
|
2609
|
-
maxSizeBytes:
|
|
2674
|
+
maxSizeBytes: z22.number().int().positive().max(HARD_MAX_SIZE_BYTES).optional().describe(
|
|
2610
2675
|
`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.`
|
|
2611
2676
|
)
|
|
2612
2677
|
});
|
|
@@ -2621,17 +2686,17 @@ async function getAttachment(input) {
|
|
|
2621
2686
|
}
|
|
2622
2687
|
return { contentType, buffer, sizeBytes };
|
|
2623
2688
|
}
|
|
2624
|
-
var uploadAttachmentSchema =
|
|
2625
|
-
filename:
|
|
2689
|
+
var uploadAttachmentSchema = z22.object({
|
|
2690
|
+
filename: z22.string().min(1).describe(
|
|
2626
2691
|
"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."
|
|
2627
2692
|
),
|
|
2628
|
-
contentType:
|
|
2693
|
+
contentType: z22.string().min(1).describe(
|
|
2629
2694
|
"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."
|
|
2630
2695
|
),
|
|
2631
|
-
dataBase64:
|
|
2696
|
+
dataBase64: z22.string().min(1).max(HARD_MAX_BASE64_CHARS).describe(
|
|
2632
2697
|
"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."
|
|
2633
2698
|
),
|
|
2634
|
-
content:
|
|
2699
|
+
content: z22.string().optional().describe(
|
|
2635
2700
|
"Body text for the note that will hold the attachment. Defaults to '[attachment]' if omitted."
|
|
2636
2701
|
),
|
|
2637
2702
|
partyId: positiveId.optional().describe("Link the new note to a party (mutually exclusive with opportunityId / projectId)."),
|
|
@@ -2649,12 +2714,7 @@ function decodedBase64Size(s) {
|
|
|
2649
2714
|
return s.length / 4 * 3 - padding;
|
|
2650
2715
|
}
|
|
2651
2716
|
async function uploadAttachment(input) {
|
|
2652
|
-
|
|
2653
|
-
if (linked.length !== 1) {
|
|
2654
|
-
throw new Error(
|
|
2655
|
-
"upload_attachment: provide exactly one of partyId, opportunityId, or projectId"
|
|
2656
|
-
);
|
|
2657
|
-
}
|
|
2717
|
+
assertSingleParentRef("upload_attachment", input, { required: true });
|
|
2658
2718
|
if (!isValidBase64(input.dataBase64)) {
|
|
2659
2719
|
throw new Error(
|
|
2660
2720
|
"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)."
|
|
@@ -2679,37 +2739,36 @@ async function uploadAttachment(input) {
|
|
|
2679
2739
|
content: input.content ?? "[attachment]",
|
|
2680
2740
|
attachments: [{ token }]
|
|
2681
2741
|
};
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2742
|
+
setRef(entryBody, "party", input.partyId);
|
|
2743
|
+
setRef(entryBody, "opportunity", input.opportunityId);
|
|
2744
|
+
setRef(entryBody, "kase", input.projectId);
|
|
2685
2745
|
return capsulePost("/entries", { entry: entryBody });
|
|
2686
2746
|
}
|
|
2687
2747
|
|
|
2688
2748
|
// src/tools/saved-filters.ts
|
|
2689
|
-
import { z as
|
|
2690
|
-
var EntitySchema =
|
|
2749
|
+
import { z as z23 } from "zod";
|
|
2750
|
+
var EntitySchema = z23.enum(["parties", "opportunities", "kases"]).describe(
|
|
2691
2751
|
"Which entity type the filter operates over. Use 'kases' for projects (Capsule's legacy name)."
|
|
2692
2752
|
);
|
|
2693
|
-
var listSavedFiltersSchema =
|
|
2753
|
+
var listSavedFiltersSchema = z23.object({
|
|
2694
2754
|
entity: EntitySchema
|
|
2695
2755
|
});
|
|
2696
2756
|
async function listSavedFilters(input) {
|
|
2697
2757
|
const { data } = await capsuleGetCached(`/${input.entity}/filters`);
|
|
2698
2758
|
return data;
|
|
2699
2759
|
}
|
|
2700
|
-
var runSavedFilterSchema =
|
|
2760
|
+
var runSavedFilterSchema = z23.object({
|
|
2701
2761
|
entity: EntitySchema,
|
|
2702
2762
|
id: positiveId.describe("The saved filter id (from list_saved_filters)."),
|
|
2703
|
-
embed:
|
|
2704
|
-
|
|
2705
|
-
perPage: z22.number().int().min(1).max(100).optional().default(25)
|
|
2763
|
+
embed: z23.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
|
|
2764
|
+
...paginationFields
|
|
2706
2765
|
});
|
|
2707
2766
|
async function runSavedFilter(input) {
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2767
|
+
return capsuleGetList(`/${input.entity}/filters/${input.id}/results`, {
|
|
2768
|
+
page: input.page,
|
|
2769
|
+
perPage: input.perPage,
|
|
2770
|
+
embed: input.embed
|
|
2771
|
+
});
|
|
2713
2772
|
}
|
|
2714
2773
|
|
|
2715
2774
|
// src/server.ts
|
|
@@ -2720,7 +2779,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
2720
2779
|
const server2 = new McpServer(
|
|
2721
2780
|
{
|
|
2722
2781
|
name: "capsulemcp",
|
|
2723
|
-
version: "1.
|
|
2782
|
+
version: "1.8.1",
|
|
2724
2783
|
description: "Read and (optionally) modify Capsule CRM data \u2014 parties, opportunities, projects, tasks, timeline entries, pipelines, tags.",
|
|
2725
2784
|
websiteUrl: "https://github.com/soil-dev/capsulemcp",
|
|
2726
2785
|
icons: ICONS
|
|
@@ -3170,20 +3229,72 @@ function createCapsuleMcpServer(opts) {
|
|
|
3170
3229
|
listEntriesSchema,
|
|
3171
3230
|
listEntries
|
|
3172
3231
|
);
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3232
|
+
if (shouldRegister("get_attachment")) {
|
|
3233
|
+
server2.tool(
|
|
3234
|
+
"get_attachment",
|
|
3235
|
+
"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.",
|
|
3236
|
+
getAttachmentSchema.shape,
|
|
3237
|
+
// get_attachment is read-only — downloads a binary, never mutates.
|
|
3238
|
+
// Mirrors the auto-inferred `{readOnlyHint: true, destructiveHint:
|
|
3239
|
+
// false}` that `registerTool` applies to every other `get_*` tool.
|
|
3240
|
+
// Explicit destructiveHint: false is load-bearing — MCP spec
|
|
3241
|
+
// defaults destructiveHint to `true`, so omitting it would (in
|
|
3242
|
+
// some client implementations) classify this read as destructive.
|
|
3243
|
+
{ readOnlyHint: true, destructiveHint: false },
|
|
3244
|
+
async (input) => {
|
|
3245
|
+
const result = await getAttachment(input);
|
|
3246
|
+
if (result.truncated) {
|
|
3247
|
+
return {
|
|
3248
|
+
content: [
|
|
3249
|
+
{
|
|
3250
|
+
type: "text",
|
|
3251
|
+
text: JSON.stringify(
|
|
3252
|
+
{
|
|
3253
|
+
id: input.id,
|
|
3254
|
+
contentType: result.contentType,
|
|
3255
|
+
sizeBytes: result.sizeBytes,
|
|
3256
|
+
truncated: true,
|
|
3257
|
+
message: `File exceeds the size cap (${input.maxSizeBytes ?? "default"} bytes). Increase maxSizeBytes if you need the bytes; max is 25MB.`
|
|
3258
|
+
},
|
|
3259
|
+
null,
|
|
3260
|
+
2
|
|
3261
|
+
)
|
|
3262
|
+
}
|
|
3263
|
+
]
|
|
3264
|
+
};
|
|
3265
|
+
}
|
|
3266
|
+
const baseType = result.contentType.split(";")[0].trim().toLowerCase();
|
|
3267
|
+
if (baseType.startsWith("image/")) {
|
|
3268
|
+
return {
|
|
3269
|
+
content: [
|
|
3270
|
+
{
|
|
3271
|
+
type: "image",
|
|
3272
|
+
data: result.buffer.toString("base64"),
|
|
3273
|
+
mimeType: result.contentType
|
|
3274
|
+
}
|
|
3275
|
+
]
|
|
3276
|
+
};
|
|
3277
|
+
}
|
|
3278
|
+
const isText = baseType.startsWith("text/") || baseType === "application/json" || baseType === "application/xml";
|
|
3279
|
+
if (isText) {
|
|
3280
|
+
return {
|
|
3281
|
+
content: [
|
|
3282
|
+
{
|
|
3283
|
+
type: "text",
|
|
3284
|
+
text: JSON.stringify(
|
|
3285
|
+
{
|
|
3286
|
+
id: input.id,
|
|
3287
|
+
contentType: result.contentType,
|
|
3288
|
+
sizeBytes: result.sizeBytes
|
|
3289
|
+
},
|
|
3290
|
+
null,
|
|
3291
|
+
2
|
|
3292
|
+
)
|
|
3293
|
+
},
|
|
3294
|
+
{ type: "text", text: result.buffer.toString("utf8") }
|
|
3295
|
+
]
|
|
3296
|
+
};
|
|
3297
|
+
}
|
|
3187
3298
|
return {
|
|
3188
3299
|
content: [
|
|
3189
3300
|
{
|
|
@@ -3193,8 +3304,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3193
3304
|
id: input.id,
|
|
3194
3305
|
contentType: result.contentType,
|
|
3195
3306
|
sizeBytes: result.sizeBytes,
|
|
3196
|
-
|
|
3197
|
-
message: `File exceeds the size cap (${input.maxSizeBytes ?? "default"} bytes). Increase maxSizeBytes if you need the bytes; max is 25MB.`
|
|
3307
|
+
base64: result.buffer.toString("base64")
|
|
3198
3308
|
},
|
|
3199
3309
|
null,
|
|
3200
3310
|
2
|
|
@@ -3203,57 +3313,8 @@ function createCapsuleMcpServer(opts) {
|
|
|
3203
3313
|
]
|
|
3204
3314
|
};
|
|
3205
3315
|
}
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
return {
|
|
3209
|
-
content: [
|
|
3210
|
-
{
|
|
3211
|
-
type: "image",
|
|
3212
|
-
data: result.buffer.toString("base64"),
|
|
3213
|
-
mimeType: result.contentType
|
|
3214
|
-
}
|
|
3215
|
-
]
|
|
3216
|
-
};
|
|
3217
|
-
}
|
|
3218
|
-
const isText = baseType.startsWith("text/") || baseType === "application/json" || baseType === "application/xml";
|
|
3219
|
-
if (isText) {
|
|
3220
|
-
return {
|
|
3221
|
-
content: [
|
|
3222
|
-
{
|
|
3223
|
-
type: "text",
|
|
3224
|
-
text: JSON.stringify(
|
|
3225
|
-
{
|
|
3226
|
-
id: input.id,
|
|
3227
|
-
contentType: result.contentType,
|
|
3228
|
-
sizeBytes: result.sizeBytes
|
|
3229
|
-
},
|
|
3230
|
-
null,
|
|
3231
|
-
2
|
|
3232
|
-
)
|
|
3233
|
-
},
|
|
3234
|
-
{ type: "text", text: result.buffer.toString("utf8") }
|
|
3235
|
-
]
|
|
3236
|
-
};
|
|
3237
|
-
}
|
|
3238
|
-
return {
|
|
3239
|
-
content: [
|
|
3240
|
-
{
|
|
3241
|
-
type: "text",
|
|
3242
|
-
text: JSON.stringify(
|
|
3243
|
-
{
|
|
3244
|
-
id: input.id,
|
|
3245
|
-
contentType: result.contentType,
|
|
3246
|
-
sizeBytes: result.sizeBytes,
|
|
3247
|
-
base64: result.buffer.toString("base64")
|
|
3248
|
-
},
|
|
3249
|
-
null,
|
|
3250
|
-
2
|
|
3251
|
-
)
|
|
3252
|
-
}
|
|
3253
|
-
]
|
|
3254
|
-
};
|
|
3255
|
-
}
|
|
3256
|
-
);
|
|
3316
|
+
);
|
|
3317
|
+
}
|
|
3257
3318
|
if (!readOnly) {
|
|
3258
3319
|
registerTool(
|
|
3259
3320
|
server2,
|
|
@@ -3457,11 +3518,28 @@ if (!process.env["CAPSULE_API_TOKEN"]) {
|
|
|
3457
3518
|
);
|
|
3458
3519
|
process.exit(1);
|
|
3459
3520
|
}
|
|
3460
|
-
var
|
|
3521
|
+
var STDIO_CLIENT_ID = "stdio-local";
|
|
3522
|
+
var server = createCapsuleMcpServer({ clientId: STDIO_CLIENT_ID });
|
|
3461
3523
|
var transport = new StdioServerTransport();
|
|
3462
3524
|
if (isReadOnly()) {
|
|
3463
3525
|
console.error("[capsulemcp] read-only mode: write/delete tools are not registered");
|
|
3464
3526
|
}
|
|
3527
|
+
function exitOnDisconnect() {
|
|
3528
|
+
let exiting = false;
|
|
3529
|
+
const die = () => {
|
|
3530
|
+
if (exiting) return;
|
|
3531
|
+
exiting = true;
|
|
3532
|
+
process.exit(0);
|
|
3533
|
+
};
|
|
3534
|
+
process.stdin.on("end", die);
|
|
3535
|
+
process.stdin.on("close", die);
|
|
3536
|
+
process.stdin.on("error", die);
|
|
3537
|
+
process.stdout.on("error", die);
|
|
3538
|
+
const orphanCheck = setInterval(() => {
|
|
3539
|
+
if (process.ppid === 1) die();
|
|
3540
|
+
}, 3e4);
|
|
3541
|
+
orphanCheck.unref?.();
|
|
3542
|
+
}
|
|
3465
3543
|
try {
|
|
3466
3544
|
await server.connect(transport);
|
|
3467
3545
|
} catch (err) {
|
|
@@ -3469,3 +3547,4 @@ try {
|
|
|
3469
3547
|
console.error(`[capsulemcp] Failed to start: ${message}`);
|
|
3470
3548
|
process.exit(1);
|
|
3471
3549
|
}
|
|
3550
|
+
exitOnDisconnect();
|