@zackbart/connecta 0.34.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (428) hide show
  1. package/CHANGELOG.md +393 -0
  2. package/README.md +62 -105
  3. package/SECURITY.md +3 -2
  4. package/bin/connecta.mjs +41 -16
  5. package/bin/version.mjs +1 -1
  6. package/dist/access-tokens.d.ts +2 -1
  7. package/dist/access-tokens.js +66 -30
  8. package/dist/activity.d.ts +1 -1
  9. package/dist/activity.js +2 -0
  10. package/dist/agent-config.d.ts +23 -0
  11. package/dist/agent-config.js +59 -0
  12. package/dist/agent-description.js +16 -0
  13. package/dist/agent-identity.js +4 -4
  14. package/dist/agent-observation.js +39 -0
  15. package/dist/auth/clerk-sdk/client.d.ts +2 -0
  16. package/dist/auth/clerk-sdk/generated.js +554 -563
  17. package/dist/auth/clerk-transport.js +70 -3
  18. package/dist/auth/clerk.d.ts +13 -0
  19. package/dist/auth/clerk.js +653 -246
  20. package/dist/auth/cloudflare-access.js +46 -42
  21. package/dist/auth/downstream-client-metadata.js +2 -2
  22. package/dist/auth/downstream-oauth.js +2051 -1160
  23. package/dist/auth/downstream-revocation.js +88 -88
  24. package/dist/auth/native-call.js +20 -0
  25. package/dist/auth/native.js +184 -0
  26. package/dist/auth/oauth-start-reset.js +11 -5
  27. package/dist/auth/oauth-token-response.js +121 -102
  28. package/dist/auth/oauth-v2-migration.js +97 -78
  29. package/dist/auth/renewable-grant.js +10 -0
  30. package/dist/auth/static-oauth.js +663 -271
  31. package/dist/auth/vercel-agent.js +266 -0
  32. package/dist/auth-admission.js +204 -0
  33. package/dist/auth-elicitation.js +350 -241
  34. package/dist/authorization-handoff.js +164 -90
  35. package/dist/call-admission-context.js +3 -10
  36. package/dist/call-admission.js +2 -0
  37. package/dist/call-classification.js +9 -0
  38. package/dist/call-recovery.js +32 -13
  39. package/dist/canonical-json.js +8 -0
  40. package/dist/catalog-cache.js +369 -213
  41. package/dist/catalog-service.js +659 -530
  42. package/dist/catalog.js +5 -1
  43. package/dist/concurrency.js +0 -25
  44. package/dist/config-defaults.d.ts +1 -0
  45. package/dist/config-defaults.js +1 -0
  46. package/dist/config-schema.d.ts +3 -1
  47. package/dist/config-schema.js +13 -3
  48. package/dist/config.d.ts +40 -6
  49. package/dist/config.js +40 -26
  50. package/dist/connector-caller.d.ts +4 -0
  51. package/dist/connector-caller.js +17 -4
  52. package/dist/connectors/api-connector.d.ts +15 -7
  53. package/dist/connectors/api-connector.js +147 -57
  54. package/dist/connectors/guarded-fetch.d.ts +51 -17
  55. package/dist/connectors/guarded-fetch.js +215 -121
  56. package/dist/connectors/negotiation-cache.js +20 -10
  57. package/dist/connectors/option-shapes.js +1 -1
  58. package/dist/connectors/remote-mcp.d.ts +12 -30
  59. package/dist/connectors/remote-mcp.js +960 -564
  60. package/dist/d1.js +10 -243
  61. package/dist/derived-context.js +5 -0
  62. package/dist/describe-config.d.ts +5 -1
  63. package/dist/describe-config.js +3 -0
  64. package/dist/downstream-input.js +347 -260
  65. package/dist/errors.d.ts +87 -6
  66. package/dist/errors.js +114 -4
  67. package/dist/execute.js +87 -50
  68. package/dist/executor-admission.js +3 -0
  69. package/dist/executors/quickjs.js +163 -112
  70. package/dist/guarded-transport.js +50 -0
  71. package/dist/guest-runtime.js +1 -1
  72. package/dist/http/client.js +439 -0
  73. package/dist/http/errors.js +558 -0
  74. package/dist/http/fields.js +49 -0
  75. package/dist/http/hatch-paging.js +318 -0
  76. package/dist/http/hatch.js +1279 -0
  77. package/dist/http/json-schema.js +267 -0
  78. package/dist/http/node-transport.js +241 -0
  79. package/dist/http/oauth.js +162 -0
  80. package/dist/http/paging.js +228 -0
  81. package/dist/http/provider.js +221 -0
  82. package/dist/http/safety.js +82 -0
  83. package/dist/http/services.js +79 -0
  84. package/dist/http/tools.js +383 -0
  85. package/dist/http/vendor-index.js +138 -0
  86. package/dist/http/vendor.js +448 -0
  87. package/dist/http/write-request.js +66 -0
  88. package/dist/http-policy.d.ts +11 -0
  89. package/dist/http-policy.js +45 -0
  90. package/dist/inbound-credential.js +1 -1
  91. package/dist/index.d.ts +2 -2
  92. package/dist/index.js +4 -0
  93. package/dist/invocation-auth.js +30 -19
  94. package/dist/invocation.js +221 -87
  95. package/dist/mcp-result.js +0 -4
  96. package/dist/membership.d.ts +4 -0
  97. package/dist/meta-output.js +1 -1
  98. package/dist/meta-tools.js +534 -408
  99. package/dist/native-recovery.js +52 -0
  100. package/dist/native-tools.js +382 -0
  101. package/dist/node.d.ts +4 -3
  102. package/dist/node.js +45 -8
  103. package/dist/oauth-handoff.js +177 -104
  104. package/dist/oauth-partition.js +6 -20
  105. package/dist/oauth-sealing.js +6 -7
  106. package/dist/operation-vocabulary.js +101 -0
  107. package/dist/operator-record.d.ts +2 -2
  108. package/dist/operator-record.js +17 -4
  109. package/dist/operator-ui/agents.d.ts +5 -4
  110. package/dist/operator-ui/agents.js +17 -7
  111. package/dist/operator-ui/config-summary.js +8 -4
  112. package/dist/operator-ui/connection-map.js +9 -6
  113. package/dist/operator-ui/contract.d.ts +7 -3
  114. package/dist/operator-ui/endpoints.js +5 -14
  115. package/dist/operator-ui/fix-prompts.d.ts +1 -1
  116. package/dist/operator-ui/fix-prompts.js +0 -8
  117. package/dist/operator-ui/generated.js +3 -3
  118. package/dist/operator-ui/health.js +52 -0
  119. package/dist/operator-ui/model.d.ts +2 -3
  120. package/dist/operator-ui/overview.js +11 -19
  121. package/dist/operator-ui/setup-commands.js +0 -4
  122. package/dist/operator-ui/view.d.ts +5 -29
  123. package/dist/operator-ui/view.js +3 -59
  124. package/dist/program-writes.js +47 -26
  125. package/dist/provider-call-facts.d.ts +115 -0
  126. package/dist/provider-call-facts.js +182 -0
  127. package/dist/provider-call.d.ts +112 -0
  128. package/dist/provider-call.js +271 -0
  129. package/dist/provider.d.ts +71 -32
  130. package/dist/provider.js +134 -40
  131. package/dist/providers/_shared/google/hatch.js +91 -0
  132. package/dist/providers/_shared/google/kit.js +537 -0
  133. package/dist/providers/_shared/google/result-size.js +15 -2
  134. package/dist/providers/_shared/google/service-account.d.ts +2 -1
  135. package/dist/providers/_shared/google/service-account.js +8 -4
  136. package/dist/providers/_shared/google/workspace.d.ts +8 -209
  137. package/dist/providers/_shared/google/workspace.js +7 -426
  138. package/dist/providers/_shared/rest/compact-index.js +86 -0
  139. package/dist/providers/_shared/rest/dispatch.js +3 -23
  140. package/dist/providers/_shared/rest/kit.js +546 -0
  141. package/dist/providers/_shared/rest/operation-index.js +230 -14
  142. package/dist/providers/_shared/rest/response-media.js +11 -0
  143. package/dist/providers/_shared/rest/tools.js +101 -78
  144. package/dist/providers/_shared/rest/value-safety.js +353 -195
  145. package/dist/providers/_shared/sensitive-fields.js +27 -0
  146. package/dist/providers/breeze/api.js +584 -0
  147. package/dist/providers/breeze/index.d.ts +3 -3
  148. package/dist/providers/breeze/index.js +67 -1420
  149. package/dist/providers/breeze/openapi.generated.js +60 -0
  150. package/dist/providers/breeze/skill.generated.js +2 -6
  151. package/dist/providers/breeze/tools.js +696 -0
  152. package/dist/providers/ccb/api.js +496 -0
  153. package/dist/providers/ccb/index.d.ts +17 -37
  154. package/dist/providers/ccb/index.js +77 -1702
  155. package/dist/providers/ccb/lib/constants.d.ts +34 -0
  156. package/dist/providers/ccb/lib/constants.js +74 -0
  157. package/dist/providers/ccb/lib/guide.js +9 -0
  158. package/dist/providers/ccb/lib/verdicts.js +387 -0
  159. package/dist/providers/ccb/openapi.generated.js +397 -0
  160. package/dist/providers/ccb/safety.js +295 -0
  161. package/dist/providers/ccb/skill.generated.js +2 -6
  162. package/dist/providers/ccb/tools.js +853 -0
  163. package/dist/providers/cloudflare/api.js +1066 -0
  164. package/dist/providers/cloudflare/index.d.ts +7 -22
  165. package/dist/providers/cloudflare/index.js +80 -194
  166. package/dist/providers/cloudflare/lib/config.d.ts +16 -0
  167. package/dist/providers/cloudflare/lib/config.js +2 -0
  168. package/dist/providers/cloudflare/lib/discovery.js +29 -0
  169. package/dist/providers/cloudflare/{graphql.js → lib/graphql.js} +1 -1
  170. package/dist/providers/cloudflare/lib/hatch.js +103 -0
  171. package/dist/providers/cloudflare/lib/path-bounds.js +28 -0
  172. package/dist/providers/cloudflare/lib/response-media.js +75 -0
  173. package/dist/providers/cloudflare/openapi.generated.js +3299 -3208
  174. package/dist/providers/cloudflare/{value-safety.js → safety.js} +538 -18
  175. package/dist/providers/cloudflare/skill.generated.js +2 -10
  176. package/dist/providers/cloudflare/tools.js +360 -0
  177. package/dist/providers/docs/api.js +51 -0
  178. package/dist/providers/docs/index.d.ts +1 -12
  179. package/dist/providers/docs/index.js +53 -1165
  180. package/dist/providers/docs/lib/constants.d.ts +18 -0
  181. package/dist/providers/docs/lib/constants.js +18 -0
  182. package/dist/providers/docs/lib/discovery.js +26 -0
  183. package/dist/providers/docs/lib/requests.js +48 -0
  184. package/dist/providers/docs/lib/schemas.js +168 -0
  185. package/dist/providers/docs/openapi.generated.js +16 -0
  186. package/dist/providers/docs/safety.js +54 -0
  187. package/dist/providers/docs/skill.generated.js +2 -6
  188. package/dist/providers/docs/tools.js +802 -0
  189. package/dist/providers/drive/api.js +177 -0
  190. package/dist/providers/drive/index.d.ts +1 -12
  191. package/dist/providers/drive/index.js +38 -1448
  192. package/dist/providers/drive/lib/constants.d.ts +6 -0
  193. package/dist/providers/drive/lib/constants.js +6 -0
  194. package/dist/providers/drive/lib/contracts.js +532 -0
  195. package/dist/providers/drive/lib/hatch.js +197 -0
  196. package/dist/providers/drive/openapi.generated.js +77 -0
  197. package/dist/providers/drive/safety.js +90 -0
  198. package/dist/providers/drive/skill.generated.js +2 -6
  199. package/dist/providers/drive/tools.js +1290 -0
  200. package/dist/providers/forms/api.js +71 -0
  201. package/dist/providers/forms/index.d.ts +1 -12
  202. package/dist/providers/forms/index.js +49 -1145
  203. package/dist/providers/forms/lib/constants.d.ts +14 -0
  204. package/dist/providers/forms/lib/constants.js +19 -0
  205. package/dist/providers/forms/lib/hatch.js +72 -0
  206. package/dist/providers/forms/openapi.generated.js +23 -0
  207. package/dist/providers/forms/safety.js +34 -0
  208. package/dist/providers/forms/skill.generated.js +2 -8
  209. package/dist/providers/forms/tools.js +1024 -0
  210. package/dist/providers/github/api.js +150 -0
  211. package/dist/providers/github/index.d.ts +4 -5
  212. package/dist/providers/github/index.js +30 -87
  213. package/dist/providers/github/{auth.d.ts → lib/auth.d.ts} +5 -3
  214. package/dist/providers/github/{auth.js → lib/auth.js} +40 -43
  215. package/dist/providers/github/lib/discovery.js +132 -0
  216. package/dist/providers/github/lib/operations.js +58 -0
  217. package/dist/providers/github/{scope.d.ts → lib/scope.d.ts} +2 -0
  218. package/dist/providers/github/{scope.js → lib/scope.js} +25 -1
  219. package/dist/providers/github/openapi.generated.js +1216 -0
  220. package/dist/providers/github/safety.js +216 -0
  221. package/dist/providers/github/skill.generated.js +2 -5
  222. package/dist/providers/github/tools.js +506 -0
  223. package/dist/providers/gmail/api.js +114 -0
  224. package/dist/providers/gmail/index.d.ts +1 -12
  225. package/dist/providers/gmail/index.js +73 -1621
  226. package/dist/providers/gmail/lib/constants.d.ts +13 -0
  227. package/dist/providers/gmail/lib/constants.js +16 -0
  228. package/dist/providers/gmail/lib/index.js +173 -0
  229. package/dist/providers/gmail/lib/schemas.js +176 -0
  230. package/dist/providers/gmail/lib/threading.js +120 -0
  231. package/dist/providers/gmail/openapi.generated.js +92 -0
  232. package/dist/providers/gmail/safety.js +21 -0
  233. package/dist/providers/gmail/skill.generated.js +2 -6
  234. package/dist/providers/gmail/tools.js +1435 -0
  235. package/dist/providers/overflow/api.js +1008 -0
  236. package/dist/providers/overflow/index.d.ts +3 -17
  237. package/dist/providers/overflow/index.js +60 -1488
  238. package/dist/providers/overflow/skill.generated.js +2 -5
  239. package/dist/providers/overflow/tools.js +873 -0
  240. package/dist/providers/planning-center/api.js +467 -0
  241. package/dist/providers/planning-center/index.d.ts +6 -35
  242. package/dist/providers/planning-center/index.js +69 -2445
  243. package/dist/providers/planning-center/lib/constants.d.ts +58 -0
  244. package/dist/providers/planning-center/lib/constants.js +83 -0
  245. package/dist/providers/planning-center/lib/discovery.js +40 -0
  246. package/dist/providers/planning-center/lib/guide.js +39 -0
  247. package/dist/providers/planning-center/lib/json-api.js +83 -0
  248. package/dist/providers/planning-center/lib/verdicts.js +1758 -0
  249. package/dist/providers/planning-center/openapi.generated.js +1764 -0
  250. package/dist/providers/planning-center/safety.js +188 -0
  251. package/dist/providers/planning-center/skill.generated.js +2 -27
  252. package/dist/providers/planning-center/tools.js +1010 -0
  253. package/dist/providers/sheets/api.js +133 -0
  254. package/dist/providers/sheets/index.d.ts +4 -16
  255. package/dist/providers/sheets/index.js +61 -1172
  256. package/dist/providers/sheets/lib/constants.d.ts +14 -0
  257. package/dist/providers/sheets/lib/constants.js +14 -0
  258. package/dist/providers/sheets/lib/hatch.js +163 -0
  259. package/dist/providers/sheets/lib/requests.js +105 -0
  260. package/dist/providers/sheets/lib/schemas.js +138 -0
  261. package/dist/providers/sheets/openapi.generated.js +30 -0
  262. package/dist/providers/sheets/safety.js +27 -0
  263. package/dist/providers/sheets/skill.generated.js +2 -7
  264. package/dist/providers/sheets/tools.js +734 -0
  265. package/dist/providers/slides/api.js +147 -0
  266. package/dist/providers/slides/index.d.ts +2 -10
  267. package/dist/providers/slides/index.js +48 -2555
  268. package/dist/providers/slides/lib/constants.d.ts +11 -0
  269. package/dist/providers/slides/lib/constants.js +11 -0
  270. package/dist/providers/slides/lib/requests.js +60 -0
  271. package/dist/providers/slides/lib/schemas.js +565 -0
  272. package/dist/providers/slides/openapi.generated.js +18 -0
  273. package/dist/providers/slides/skill.generated.js +2 -6
  274. package/dist/providers/slides/tools.js +1968 -0
  275. package/dist/providers/stripe/api.js +401 -0
  276. package/dist/providers/stripe/index.d.ts +7 -27
  277. package/dist/providers/stripe/index.js +75 -191
  278. package/dist/providers/stripe/lib/config.d.ts +4 -0
  279. package/dist/providers/stripe/lib/config.js +2 -0
  280. package/dist/providers/stripe/openapi.generated.js +4 -3
  281. package/dist/providers/stripe/{value-safety.js → safety.js} +5 -5
  282. package/dist/providers/stripe/skill.generated.js +2 -10
  283. package/dist/providers/stripe/tools.js +77 -0
  284. package/dist/providers/tithely/api.js +531 -0
  285. package/dist/providers/tithely/index.d.ts +19 -0
  286. package/dist/providers/tithely/index.js +86 -965
  287. package/dist/providers/tithely/skill.generated.js +2 -5
  288. package/dist/providers/tithely/tools.js +426 -0
  289. package/dist/providers/vercel/api.js +551 -0
  290. package/dist/providers/vercel/index.d.ts +11 -25
  291. package/dist/providers/vercel/index.js +67 -261
  292. package/dist/providers/vercel/lib/config.d.ts +2 -0
  293. package/dist/providers/vercel/lib/config.js +2 -0
  294. package/dist/providers/vercel/lib/index.js +117 -0
  295. package/dist/providers/vercel/lib/logs.js +100 -0
  296. package/dist/providers/vercel/lib/pin.js +270 -0
  297. package/dist/providers/vercel/openapi.generated.js +398 -376
  298. package/dist/providers/vercel/{value-safety.js → safety.js} +261 -42
  299. package/dist/providers/vercel/skill.generated.js +2 -7
  300. package/dist/providers/vercel/tools.js +642 -0
  301. package/dist/redaction-identity.js +47 -0
  302. package/dist/redaction-marker.js +125 -0
  303. package/dist/registry.d.ts +30 -28
  304. package/dist/registry.js +446 -413
  305. package/dist/routes/access-tokens.js +1 -1
  306. package/dist/routes/actor-labels.js +1 -1
  307. package/dist/routes/connect.js +1 -1
  308. package/dist/routes/dispatch.js +103 -0
  309. package/dist/routes/mcp.js +170 -79
  310. package/dist/routes/oauth.js +3 -1
  311. package/dist/routes/operator.js +2 -2
  312. package/dist/routes/shared.d.ts +5 -4
  313. package/dist/routes/shared.js +244 -177
  314. package/dist/routes/skills.js +10 -8
  315. package/dist/routes/ui-config.js +27 -3
  316. package/dist/routes/ui-data.js +29 -22
  317. package/dist/routes/ui.js +1 -1
  318. package/dist/runtime/call-admission.js +0 -26
  319. package/dist/runtime/call-entry.js +30 -0
  320. package/dist/runtime/call.js +556 -0
  321. package/dist/runtime/connector-scope.js +7 -0
  322. package/dist/runtime/credential-probe.js +34 -0
  323. package/dist/runtime/executor.js +40 -0
  324. package/dist/runtime/native-storage.js +63 -0
  325. package/dist/runtime/registry.js +27 -0
  326. package/dist/runtime/request-scope.js +72 -0
  327. package/dist/runtime/run.js +15 -7
  328. package/dist/runtime/shared-read.js +34 -5
  329. package/dist/runtime/storage.js +34 -21
  330. package/dist/sent-secrets.d.ts +194 -37
  331. package/dist/sent-secrets.js +1743 -340
  332. package/dist/server.js +5 -56
  333. package/dist/skills.js +330 -224
  334. package/dist/sqlite.js +15 -14
  335. package/dist/storage/d1-copy.js +255 -0
  336. package/dist/storage/d1-driver.js +15 -0
  337. package/dist/storage/keys.js +28 -0
  338. package/dist/storage/sql-driver.js +88 -0
  339. package/dist/storage/sql-native.js +544 -0
  340. package/dist/storage/sql-schema.d.ts +6 -0
  341. package/dist/storage/sql-schema.js +15 -0
  342. package/dist/storage/sql.d.ts +14 -20
  343. package/dist/storage/sql.js +29 -535
  344. package/dist/timeout.js +0 -26
  345. package/dist/tool-safety.js +20 -10
  346. package/dist/types.d.ts +11 -99
  347. package/dist/upward.js +3 -0
  348. package/dist/usage-guide.js +3 -1
  349. package/dist/validate.d.ts +4 -2
  350. package/dist/validate.js +86 -9
  351. package/dist/version.d.ts +1 -1
  352. package/dist/version.js +1 -1
  353. package/dist/worker.js +312 -292
  354. package/dist/workspace-subject.js +5 -1
  355. package/dist/write-outcome.js +454 -34
  356. package/documentation/README.md +27 -7
  357. package/documentation/architecture.md +2550 -186
  358. package/documentation/auth.md +821 -98
  359. package/documentation/code-mode.md +56 -21
  360. package/documentation/deploying.md +164 -20
  361. package/documentation/hosted-mcp-migration.md +460 -0
  362. package/documentation/integrating.md +144 -105
  363. package/documentation/meta-tools.md +411 -34
  364. package/documentation/operating.md +28 -4
  365. package/documentation/operator-tests.md +11 -2
  366. package/documentation/operator-ui.md +103 -33
  367. package/documentation/provider-auth-migration.md +38 -63
  368. package/documentation/provider-migration-0.29.md +10 -5
  369. package/documentation/provider-migration-0.35.md +975 -0
  370. package/documentation/providers.md +965 -0
  371. package/examples/worker/AGENTS.md +23 -9
  372. package/examples/worker/README.md +254 -135
  373. package/examples/worker/alchemy.run.ts +116 -0
  374. package/examples/worker/gitignore +10 -0
  375. package/examples/worker/src/connecta.config.ts +23 -21
  376. package/examples/worker/wrangler.jsonc +14 -5
  377. package/guides/connecting-agents.md +157 -0
  378. package/guides/how-it-works.md +156 -0
  379. package/guides/safety-and-trust.md +153 -0
  380. package/package.json +27 -42
  381. package/templates/node/.env.example +3 -0
  382. package/templates/node/README.md +22 -2
  383. package/templates/node/docker-compose.yml +1 -0
  384. package/templates/node/package.json +3 -2
  385. package/templates/node/src/connecta.config.ts +8 -0
  386. package/dist/catalog-drift.d.ts +0 -134
  387. package/dist/catalog-drift.js +0 -493
  388. package/dist/providers/_shared/request-token.js +0 -83
  389. package/dist/providers/_shared/rest/operation-index.d.ts +0 -133
  390. package/dist/providers/_shared/rest/tools.d.ts +0 -141
  391. package/dist/providers/_shared/rest/value-safety.d.ts +0 -201
  392. package/dist/providers/basecamp/index.d.ts +0 -43
  393. package/dist/providers/basecamp/index.js +0 -997
  394. package/dist/providers/basecamp/skill.generated.js +0 -9
  395. package/dist/providers/cloudflare/rest.d.ts +0 -37
  396. package/dist/providers/cloudflare/rest.js +0 -1078
  397. package/dist/providers/github/hosted.js +0 -218
  398. package/dist/providers/github/rest.js +0 -321
  399. package/dist/providers/infisical/index.d.ts +0 -7
  400. package/dist/providers/infisical/index.js +0 -713
  401. package/dist/providers/infisical/skill.generated.js +0 -8
  402. package/dist/providers/linear/index.d.ts +0 -55
  403. package/dist/providers/linear/index.js +0 -352
  404. package/dist/providers/linear/skill.generated.js +0 -8
  405. package/dist/providers/mixpanel/index.d.ts +0 -57
  406. package/dist/providers/mixpanel/index.js +0 -468
  407. package/dist/providers/mixpanel/operations.js +0 -20
  408. package/dist/providers/mixpanel/rest.js +0 -603
  409. package/dist/providers/mixpanel/skill.generated.js +0 -10
  410. package/dist/providers/mixpanel/value-safety.js +0 -16
  411. package/dist/providers/notion/index.d.ts +0 -65
  412. package/dist/providers/notion/index.js +0 -368
  413. package/dist/providers/notion/openapi.generated.js +0 -75
  414. package/dist/providers/notion/rest.d.ts +0 -24
  415. package/dist/providers/notion/rest.js +0 -1226
  416. package/dist/providers/notion/skill.generated.js +0 -11
  417. package/dist/providers/notion/value-safety.js +0 -56
  418. package/dist/providers/revenuecat/index.d.ts +0 -49
  419. package/dist/providers/revenuecat/index.js +0 -557
  420. package/dist/providers/revenuecat/skill.generated.js +0 -11
  421. package/dist/providers/stripe/rest.d.ts +0 -14
  422. package/dist/providers/stripe/rest.js +0 -402
  423. package/dist/providers/vercel/rest.d.ts +0 -20
  424. package/dist/providers/vercel/rest.js +0 -1020
  425. package/dist/providers/wispr-flow/index.d.ts +0 -14
  426. package/dist/providers/wispr-flow/index.js +0 -117
  427. package/dist/providers/wispr-flow/skill.generated.js +0 -8
  428. /package/dist/providers/github/{key.js → lib/key.js} +0 -0
@@ -1,2461 +1,85 @@
1
1
  import { skill } from "./skill.generated.js";
2
- /**
3
- * Planning Center Online as a hand-written `api()` provider: named tools for
4
- * the jobs a church staff agent actually does, plus two hatches split by safety
5
- * that reach every endpoint of every product.
6
- *
7
- * Why not the hosted MCP server. Planning Center operates its own at
8
- * `https://mcp.planningcenteronline.com/mcp` (GA 2026-08-20). It was considered
9
- * and passed over for this connection: its writes stop at People, and a proxy
10
- * owns none of the names, schemas, projections, or errors (P1), so Services
11
- * scheduling, Groups membership, workflow cards, and the Giving reads would
12
- * all be out of reach. Full REST coverage is the maintainer's choice; the
13
- * hosted server remains reachable through `remoteMcp()` for a deployment that
14
- * wants it.
15
- *
16
- * Why no SDK. Planning Center publishes none worth a dependency, and the API is
17
- * uniform JSON:API 1.0 over HTTP Basic, so Web `fetch` behind the guarded
18
- * transport is the whole client and the provider stays Workers-clean.
19
- *
20
- * Drift. Planning Center publishes an OpenAPI 3.1 document per product and
21
- * dated version (`/<app>/v2/open_api/<version>`) and a credential-free
22
- * documentation graph listing every version (`/<app>/v2/documentation`).
23
- * `src/providers/planning-center/drift.json` records each endpoint a named
24
- * tool calls, digested at the version pinned below, and
25
- * `pnpm run providers:check --provider planning-center` reports a touched
26
- * contract that moved and a newly published version of any pinned product.
27
- * Both are evidence only: no tool here is generated from either document.
28
- */
29
- import { apiConnector as api } from "../../connectors/api-connector.js";
30
- import { guardedFetch, retryAfterMs, } from "../../connectors/guarded-fetch.js";
31
- import { ConnectorCallError } from "../../errors.js";
32
- import { CONNECTA_VERSION } from "../../version.js";
33
2
  import { keys, optionsOf } from "../../config-schema.js";
34
3
  import { PROVIDER_COMMON } from "../../connectors/option-shapes.js";
35
- import { asProviderFactory } from "../../provider.js";
36
- /** Planning Center's REST origin. Override only for a proxy or test double. */
37
- export const PLANNING_CENTER_API_BASE_URL = "https://api.planningcenteronline.com";
38
- /** The products behind `/<app>/v2`, in the order their guides list them. */
39
- export const PLANNING_CENTER_APPS = [
40
- "people",
41
- "services",
42
- "groups",
43
- "check-ins",
44
- "calendar",
45
- "registrations",
46
- "giving",
47
- "publishing",
48
- "webhooks",
49
- "api",
50
- ];
51
- /**
52
- * One reviewed `X-PCO-API-Version` per product, sent on every request.
53
- *
54
- * The header is optional, and omitting it is the trap: Planning Center then
55
- * applies a per-application default chosen in a web dashboard, so behavior
56
- * would follow whoever last clicked there rather than this file. Each date is
57
- * the newest non-beta version in the product's documentation graph on
58
- * 2026-10-05, and every projection below is written against it. A breaking
59
- * change arrives as a new dated version, never inside this one, so moving a
60
- * pin is a reviewed change to this table, its drift manifest, and whatever
61
- * projection the version's change list touches. Notes worth keeping:
62
- *
63
- * - groups `2023-07-10` removed every person attribute from `Membership`, so a
64
- * membership is nameless without `include=person`.
65
- * - calendar `2026-06-22` may return `BookedEvent*` stubs in place of events on
66
- * booking and conflict traversals for users without access to the event.
67
- * - webhooks `2022-10-20` renamed `Subscription` to `WebhookSubscription`.
68
- * - services `2018-11-01` and giving `2019-10-18` are simply the newest there is.
69
- *
70
- * The hatches send the same pin for the app their path names and accept a
71
- * per-call `version` override for an endpoint only a newer version serves.
72
- */
73
- export const PLANNING_CENTER_API_VERSIONS = {
74
- people: "2026-06-04",
75
- services: "2018-11-01",
76
- groups: "2023-07-10",
77
- "check-ins": "2025-05-28",
78
- calendar: "2026-06-22",
79
- registrations: "2025-05-01",
80
- giving: "2019-10-18",
81
- publishing: "2024-03-25",
82
- webhooks: "2022-10-20",
83
- api: "2026-09-24",
84
- };
85
- /** Planning Center's own cap on `per_page`; its default is 25. */
86
- const MAX_PAGE_SIZE = 100;
87
- const DEFAULT_PAGE_SIZE = 25;
88
- /**
89
- * A ceiling on absurdity, not a budget. `per_page` tops out at 100, and the
90
- * heaviest projection here — 100 people with emails and phone numbers
91
- * included — is a few hundred kilobytes.
92
- */
93
- const PLANNING_CENTER_MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
94
- /**
95
- * Planning Center documents 100 requests per 20 seconds per authenticated user
96
- * (429 with `Retry-After` in seconds past it, and `X-PCO-API-Request-Rate-*`
97
- * headers on every response). The rolling window transcribes that number, as
98
- * Cloudflare's per-user limit is transcribed: a per-runtime approximation, not
99
- * an enforcement, because N isolates each keep their own counter and every
100
- * other integration the token's user runs draws on the same bucket.
101
- * `maxConcurrency: 5` is connecta's choice, not Planning Center's — the bound
102
- * that stops one `execute_code` fan-out from spending the window in a tick.
103
- * Admission meters tool calls, not requests: `get_plan` spends three.
104
- */
105
- const PLANNING_CENTER_ADMISSION = {
106
- rules: [
107
- {
108
- maxConcurrency: 5,
109
- budget: { kind: "rolling-window", maxCalls: 100, windowMs: 20_000 },
110
- maxQueueSize: 32,
111
- queueTimeoutMs: 5_000,
112
- retryAfterMs: 1_000,
113
- },
114
- ],
115
- };
116
- /** Planning Center refuses a request without a descriptive one (HTTP 403). */
117
- const DEFAULT_USER_AGENT = `connecta/${CONNECTA_VERSION} (+https://github.com/zackbart/connecta)`;
118
- const PERSONAL_ACCESS_TOKEN = {
119
- credential: {
120
- label: "Planning Center personal access token",
121
- description: "Create one at api.planningcenteronline.com/oauth/applications → Personal Access Tokens. It acts with the permissions of the user who creates it, in every product that user can open, so create it as a user whose access matches this connection's purpose.",
122
- fields: [
123
- {
124
- name: "applicationId",
125
- label: "Application ID",
126
- description: "The personal access token's Application ID.",
127
- placeholder: "Paste Application ID",
128
- inputType: "text",
129
- },
130
- {
131
- name: "secret",
132
- label: "Secret",
133
- description: "The personal access token's Secret, shown once when the token is created.",
134
- placeholder: "Paste Secret",
135
- inputType: "password",
136
- },
137
- ],
138
- },
139
- async headers(ctx) {
140
- const applicationId = (await ctx.credential?.get("applicationId"))?.trim();
141
- const secret = (await ctx.credential?.get("secret"))?.trim();
142
- if (!applicationId || !secret) {
143
- throw new ConnectorCallError("auth_required", "No Planning Center personal access token is configured for this connector; it needs both the Application ID and the Secret. Call authorize_connector for recovery options. When available, an operator can add the token in this connection in the operator UI.");
144
- }
145
- // HTTP Basic is `id:secret` in Latin-1 base64. A colon in the id, or any
146
- // whitespace or non-ASCII character, is a pasting mistake no real token
147
- // contains — and btoa would either throw or frame it ambiguously.
148
- if (applicationId.includes(":") || /[^\x21-\x7e]/.test(applicationId + secret)) {
149
- throw new ConnectorCallError("auth_required", "The configured Planning Center Application ID or Secret contains characters a token never has. An operator must re-paste both fields.");
150
- }
151
- return { Authorization: `Basic ${btoa(`${applicationId}:${secret}`)}` };
152
- },
153
- };
154
- function asRecord(value) {
155
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
156
- }
157
- function asArray(value) {
158
- return Array.isArray(value) ? value : [];
159
- }
160
- function compact(value) {
161
- return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
162
- }
163
- function isResource(value) {
164
- const record = asRecord(value);
165
- return typeof record["id"] === "string" && typeof record["type"] === "string";
166
- }
167
- function resources(value) {
168
- return (Array.isArray(value) ? value : value ? [value] : []).filter(isResource);
169
- }
170
- function indexIncluded(document) {
171
- const index = new Map();
172
- for (const resource of resources(document["included"])) {
173
- index.set(`${resource.type}:${resource.id}`, resource);
174
- }
175
- return index;
176
- }
177
- function relationshipData(resource, name) {
178
- return resource.relationships?.[name]?.data;
179
- }
180
- /** The related id, `null` when Planning Center says there is none. */
181
- function relId(resource, name) {
182
- const data = relationshipData(resource, name);
183
- if (data === null)
184
- return null;
185
- const id = asRecord(data)["id"];
186
- return typeof id === "string" ? id : undefined;
187
- }
188
- function related(resource, name, index) {
189
- const data = asRecord(relationshipData(resource, name));
190
- return typeof data["type"] === "string" && typeof data["id"] === "string"
191
- ? index.get(`${data["type"]}:${data["id"]}`)
192
- : undefined;
193
- }
194
- function relatedMany(resource, name, index) {
195
- return asArray(relationshipData(resource, name))
196
- .map((entry) => {
197
- const ref = asRecord(entry);
198
- return index.get(`${String(ref["type"])}:${String(ref["id"])}`);
199
- })
200
- .filter((item) => item !== undefined);
201
- }
202
- /** `{ id, ...the named attributes }`, keeping Planning Center's own names. */
203
- function pick(resource, names) {
204
- const attributes = resource.attributes ?? {};
205
- const out = { id: resource.id };
206
- for (const name of names) {
207
- if (attributes[name] !== undefined)
208
- out[name] = attributes[name];
209
- }
210
- return out;
211
- }
212
- function attribute(resource, name) {
213
- return resource?.attributes?.[name];
214
- }
215
- /** A JSON:API write document. `undefined` drops a key; `null` clears a field. */
216
- function writeDocument(type, attributes) {
217
- return { data: { type, attributes: compact(attributes) } };
218
- }
219
- /** One branchable signal: `meta.next.offset`, Planning Center's own cursor. */
220
- function pageOf(document) {
221
- const meta = asRecord(document["meta"]);
222
- const next = asRecord(meta["next"])["offset"];
223
- const nextOffset = typeof next === "number" && Number.isInteger(next) ? next : null;
224
- const total = meta["total_count"];
225
- return {
226
- hasMore: nextOffset !== null,
227
- nextOffset,
228
- totalCount: typeof total === "number" ? total : null,
229
- };
230
- }
231
- const APP_PATH = /^\/([a-z-]+)\/v2(?:\/|$)/;
232
- function isApp(value) {
233
- return PLANNING_CENTER_APPS.includes(value);
234
- }
235
- /**
236
- * The product a path addresses, read after URL normalization — the same
237
- * normalization the guarded transport applies — so `..` cannot carry a request
238
- * from `/people/v2` to `/oauth` or anywhere else outside a product.
239
- */
240
- function appForPath(path, apps) {
241
- let normalized;
242
- if (path.startsWith("/") && !path.startsWith("//")) {
243
- try {
244
- normalized = new URL(`https://planning-center.invalid${path}`).pathname;
245
- }
246
- catch {
247
- normalized = undefined;
248
- }
249
- }
250
- const app = normalized === undefined ? undefined : APP_PATH.exec(normalized)?.[1];
251
- if (!app || !isApp(app)) {
252
- throw new ConnectorCallError("invalid_args", `A Planning Center path begins with /<app>/v2, where app is one of ${apps.join(", ")} — for example /people/v2/people. No host, query string, or traversal.`);
253
- }
254
- return app;
255
- }
256
- /** Display names, in guide order. The guide never lists `api` as a product. */
257
- const APP_TITLES = {
258
- people: "People",
259
- services: "Services",
260
- groups: "Groups",
261
- "check-ins": "Check-Ins",
262
- calendar: "Calendar",
263
- registrations: "Registrations",
264
- giving: "Giving",
265
- publishing: "Publishing",
266
- webhooks: "Webhooks",
267
- api: "API",
268
- };
269
- /**
270
- * A path's segments, each percent-decoded once, or a refusal when the path is
271
- * not literal. While a product is excluded no path may lean on normalization:
272
- * a dot segment, an empty segment, a backslash, whitespace, or an encoded
273
- * `/`, `\`, `?`, `#`, or `%` could each mean one thing here and another to a
274
- * server that decodes or resolves it, so none is admitted at all rather than
275
- * reasoned about.
276
- */
277
- function literalSegments(path) {
278
- const ambiguous = () => new ConnectorCallError("invalid_args", "While this deployment excludes a Planning Center product, a path must be literal: no . or .. segments, empty segments, backslashes, whitespace, or percent-encoded /, \\, ?, #, or %.");
279
- if (!path.startsWith("/"))
280
- throw ambiguous();
281
- const raw = path.slice(1).split("/");
282
- return raw.map((segment, index) => {
283
- if (segment === "" && index !== raw.length - 1)
284
- throw ambiguous();
285
- let decoded;
286
- try {
287
- decoded = decodeURIComponent(segment);
288
- }
289
- catch {
290
- throw ambiguous();
291
- }
292
- const control = [...decoded].some((char) => char <= " " || char === "\x7f");
293
- if (decoded === "." || decoded === ".." || control || /[/\\?#%]/.test(decoded))
294
- throw ambiguous();
295
- return decoded;
296
- });
297
- }
298
- /**
299
- * People resources a list is built from or answers with. A list's conditions
300
- * can draw on any product — a Giving condition makes a donor list — and one
301
- * list can be a condition of another, so while any product is excluded none
302
- * of them is read, by path or by `include`.
303
- */
304
- const LIST_RESOURCES = new Set(["lists", "list_results", "rules", "conditions"]);
305
- /** The People tools that read or refresh a list, removed with any exclusion. */
306
- const LIST_TOOLS = new Set(["list_lists", "list_list_people", "run_list"]);
307
- /** The webhook subscription collections, current and 2018-08-01. */
308
- const SUBSCRIPTION_COLLECTIONS = new Set(["webhook_subscriptions", "subscriptions"]);
309
- /**
310
- * Refuse, before anything is sent, a request that would reach an excluded
311
- * product. It reads literal segments, so it runs twice: on the path as given,
312
- * and on the exact URL the transport is about to fetch (`admit`), which is the
313
- * one that counts. Five doors lead to an excluded product:
314
- *
315
- * - its own paths, in any case or percent-encoding of a literal segment;
316
- * - People lists, rules, conditions, and results, whose criteria can come from
317
- * the excluded product, by path or by `include`;
318
- * - a new webhook subscription to its events, which delivers its records to
319
- * any URL — so a new subscription must name a reachable product's event;
320
- * - an existing subscription, changed by id: a path names a subscription by
321
- * id, not by product, so no subscription is enabled, disabled, deleted, or
322
- * re-keyed while any product is excluded; and
323
- * - webhook event and delivery payloads, which carry the record that fired
324
- * them, refused for every subscription for the same reason.
325
- *
326
- * People documents no other `include` or attribute that reaches another
327
- * product's records.
328
- */
329
- function refuseExcluded(segments, query, request, excluded) {
330
- const disabled = (app, detail) => new ConnectorCallError("provider_permission_denied", `Planning Center ${APP_TITLES[app]} (/${app}/v2) is disabled in this deployment, so this connection ${detail}. Retrying will not help; only an operator can remove "${app}" from excludeApps.`, { retryable: false });
331
- const because = (detail) => new ConnectorCallError("provider_permission_denied", `This deployment disables Planning Center ${series([...excluded].map((name) => APP_TITLES[name]))}, and ${detail} Retrying will not help.`, { retryable: false });
332
- const lower = segments.map((segment) => segment.toLowerCase());
333
- const app = lower[0] ?? "";
334
- if (isApp(app) && excluded.has(app))
335
- throw disabled(app, "does not reach its endpoints");
336
- // Lists are refused and workflows are not. A list is a live query: reading
337
- // it evaluates its conditions, Giving ones included, at read time, so it
338
- // is the excluded product's API by another path. A workflow card is a
339
- // stored People record. A Giving automation can create one, but so can
340
- // staff, and once created it is People data, like a note or custom field
341
- // value typed from a donor's details. Exclusion guards products' APIs,
342
- // not provenance; refusing workflows would cost every exclusion a core
343
- // People feature and argue equally for notes, fields, forms, and groups.
344
- // The hard boundary is the token user's own per-product permissions.
345
- const lists = "a People list's conditions can draw on a disabled product, so this connection reads no list, list result, rule, or condition.";
346
- if (app === "people" && lower.some((segment) => LIST_RESOURCES.has(segment)))
347
- throw because(lists);
348
- for (const [key, value] of query) {
349
- if (/include/i.test(key) &&
350
- value
351
- .toLowerCase()
352
- .split(/[\s,.]+/)
353
- .some((part) => LIST_RESOURCES.has(part))) {
354
- throw because(lists);
355
- }
356
- }
357
- if (app !== "webhooks")
358
- return;
359
- if (lower.some((segment) => /^(?:events|deliveries)\b/.test(segment))) {
360
- throw because("a webhook event or delivery payload may carry any product's records, so this connection does not read them. Subscriptions themselves stay readable.");
361
- }
362
- if (request.method === "GET" || request.method === "HEAD")
363
- return;
364
- const resource = lower.slice(2).filter((segment) => segment !== "");
365
- if (request.method !== "POST" || resource.length !== 1 || !SUBSCRIPTION_COLLECTIONS.has(resource[0])) {
366
- throw because("a webhook subscription's id does not name its product, so this connection does not enable, disable, delete, or re-key an existing subscription.");
367
- }
368
- const data = asRecord(request.body)["data"];
369
- const name = Array.isArray(data) ? undefined : asRecord(asRecord(data)["attributes"])["name"];
370
- const event = typeof name === "string" ? /^([a-z-]+)\.v2\.events\./.exec(name.trim().toLowerCase())?.[1] : undefined;
371
- if (event === undefined || !isApp(event)) {
372
- throw because("a new webhook subscription must name one product's event, such as people.v2.events.person.created, in data.attributes.name.");
373
- }
374
- if (excluded.has(event))
375
- throw disabled(event, "cannot subscribe to its webhook events");
376
- }
377
- /**
378
- * The exclusion, enforced on the exact URL about to be fetched: after the
379
- * transport has resolved and encoded it, so what is checked is what Planning
380
- * Center routes. The origin and base path are asserted again here, not taken
381
- * on trust from the confinement before it.
382
- */
383
- function admit(baseUrl, url, request, excluded) {
384
- const base = new URL(baseUrl);
385
- const basePath = base.pathname.replace(/\/+$/, "");
386
- if (url.origin !== base.origin || !url.pathname.startsWith(`${basePath}/`) || url.hash) {
387
- throw new ConnectorCallError("invalid_args", "A Planning Center request left the Planning Center API origin.");
388
- }
389
- refuseExcluded(literalSegments(url.pathname.slice(basePath.length)), url.searchParams, request, excluded);
390
- }
391
- function errorDetail(payload, status) {
392
- const parts = [];
393
- for (const entry of asArray(asRecord(payload)["errors"])) {
394
- const error = asRecord(entry);
395
- const title = typeof error["title"] === "string" ? error["title"].trim() : "";
396
- const detail = typeof error["detail"] === "string" ? error["detail"].trim() : "";
397
- const source = asRecord(error["source"]);
398
- const at = typeof source["pointer"] === "string"
399
- ? ` (${source["pointer"]})`
400
- : typeof source["parameter"] === "string"
401
- ? ` (${source["parameter"]})`
402
- : "";
403
- const text = [title, detail].filter(Boolean).join(": ");
404
- if (text)
405
- parts.push(`${text}${at}`);
406
- }
407
- return parts.length > 0 ? `Planning Center: ${parts.join("; ")}` : `Planning Center returned HTTP ${status}.`;
408
- }
409
- /**
410
- * Map by the caller's next move.
411
- *
412
- * - **401** is the token: revoked, mistyped, or the id and secret of two
413
- * different tokens. Only an operator can fix it.
414
- * - **403** is the token's *user*, not the token: Planning Center permissions
415
- * belong to whoever created it, per product and per record. Neither
416
- * `authorize_connector` nor a retry helps, so it is a non-retryable failure
417
- * that says so.
418
- * - **404** is absence — Planning Center answers a permission gap with 403 —
419
- * but a person id that used to work was most often merged into another.
420
- */
421
- function planningCenterFailure(status, headers, payload) {
422
- const detail = errorDetail(payload, status);
423
- if (status === 429) {
424
- const wait = retryAfterMs(headers);
425
- return new ConnectorCallError("rate_limited", `${detail} Planning Center allows 100 requests per 20 seconds per user, shared by every integration that user's tokens run.`, wait === undefined ? {} : { retryAfterMs: wait });
426
- }
427
- if (status === 401) {
428
- return new ConnectorCallError("auth_required", `${detail} The personal access token was rejected: revoked, mistyped, or an Application ID and Secret from different tokens. An operator must replace it.`);
429
- }
430
- if (status === 403) {
431
- return new ConnectorCallError("provider_permission_denied", `${detail} The token's Planning Center user lacks permission for this product or record. Permissions belong to that user; an operator must raise them or use another user's token. Retrying will not help.`, { retryable: false });
432
- }
433
- if (status === 404) {
434
- return new ConnectorCallError("not_found", `${detail} Confirm the id with its list tool. A person id that used to resolve was probably merged: look it up in /people/v2/person_mergers with where[person_to_remove_id].`);
435
- }
436
- if (status === 400 || status === 409 || status === 422) {
437
- return new ConnectorCallError("invalid_args", detail);
438
- }
439
- if (status >= 500) {
440
- const wait = retryAfterMs(headers);
441
- return new ConnectorCallError("unavailable", `${detail} Planning Center is failing upstream.`, wait === undefined ? {} : { retryAfterMs: wait });
442
- }
443
- return new ConnectorCallError("connector_call_failed", detail, {
444
- retryable: false,
445
- });
446
- }
447
- function planningCenterCall(send, versions, excluded) {
448
- const apps = PLANNING_CENTER_APPS.filter((app) => !excluded.has(app));
449
- return async (request, ctx) => {
450
- if (excluded.size > 0)
451
- refuseExcluded(literalSegments(request.path), [], request, excluded);
452
- const app = appForPath(request.path, apps);
453
- return await send(compact({
454
- method: request.method,
455
- path: request.path,
456
- query: request.query,
457
- body: request.body,
458
- headers: { "X-PCO-API-Version": request.version ?? versions[app] },
459
- }), ctx, async (response) => {
460
- const parsed = await response.jsonResult();
461
- if (!response.ok) {
462
- throw planningCenterFailure(response.status, response.headers, "value" in parsed ? parsed.value : undefined);
463
- }
464
- if ("parseError" in parsed) {
465
- throw new ConnectorCallError("connector_call_failed", "Planning Center returned a successful response that is not JSON.", { retryable: false });
466
- }
467
- return parsed.value;
468
- });
469
- };
470
- }
471
- // ---------------------------------------------------------------------------
472
- // Schemas
473
- // ---------------------------------------------------------------------------
474
- const STRING = { type: "string" };
475
- const NULLABLE_STRING = { type: ["string", "null"] };
476
- const INTEGER = { type: "integer" };
477
- const NULLABLE_INTEGER = { type: ["integer", "null"] };
478
- const BOOLEAN = { type: "boolean" };
479
- const ARRAY = { type: "array" };
480
- const OBJECT = { type: "object" };
481
- function record(properties, required = ["id"]) {
482
- return { type: "object", properties, required };
483
- }
484
- function input(properties, required = []) {
485
- return { type: "object", properties, required, additionalProperties: false };
486
- }
487
- function idProperty(description) {
488
- return { type: "string", pattern: "^[0-9]+$", description };
489
- }
490
- const RAW_PROPERTY = {
491
- type: "boolean",
492
- description: "Return Planning Center's untouched JSON:API resources (and `included`) instead of the lean projection.",
493
- };
494
- const OFFSET_PROPERTY = {
495
- type: "integer",
496
- minimum: 0,
497
- description: "Rows to skip. Pass page.nextOffset back unchanged; omit for the first page.",
498
- };
499
- const DATE_PROPERTY_PATTERN = "^\\d{4}-\\d{2}-\\d{2}(T[0-9:.]+(Z|[+-]\\d{2}:?\\d{2})?)?$";
500
- function dateProperty(description) {
501
- return {
502
- type: "string",
503
- pattern: DATE_PROPERTY_PATTERN,
504
- description: `${description} ISO 8601 date or date-time; a bare date is the organization's local day.`,
505
- };
506
- }
507
- function perPageProperty(defaultPageSize) {
508
- return {
509
- type: "integer",
510
- minimum: 1,
511
- maximum: MAX_PAGE_SIZE,
512
- description: `Rows per page, 1 to Planning Center's maximum of ${MAX_PAGE_SIZE}. Defaults to this connector's ${defaultPageSize}.`,
513
- };
514
- }
515
- function orderProperty(values) {
516
- return {
517
- type: "string",
518
- enum: values.flatMap((value) => [value, `-${value}`]),
519
- description: "Sort attribute; a leading '-' reverses it.",
520
- };
521
- }
522
- const PAGE_SCHEMA = {
523
- type: "object",
524
- properties: {
525
- hasMore: BOOLEAN,
526
- nextOffset: { type: ["integer", "null"], description: "Pass back as offset while hasMore is true." },
527
- totalCount: NULLABLE_INTEGER,
528
- },
529
- required: ["hasMore", "nextOffset"],
530
- };
531
- function listOutput(key, item) {
532
- return {
533
- type: "object",
534
- properties: {
535
- [key]: { type: "array", items: item },
536
- page: PAGE_SCHEMA,
537
- included: { type: "array", description: "Sideloaded resources, only with raw: true." },
538
- },
539
- required: [key, "page"],
540
- };
541
- }
542
- const QUERY_PROPERTY = {
543
- type: "array",
544
- description: "Query parameters as name/value pairs, e.g. where[status]=active, include=emails, order=-created_at, per_page, offset.",
545
- items: {
546
- type: "object",
547
- properties: {
548
- name: { type: "string", minLength: 1, description: "Parameter name, brackets literal: where[first_name]." },
549
- value: {
550
- type: ["string", "number", "boolean"],
551
- description: "Parameter value; the transport encodes it once.",
552
- },
553
- },
554
- required: ["name", "value"],
555
- additionalProperties: false,
556
- },
557
- };
558
- const HATCH_PATH_PROPERTY = {
559
- type: "string",
560
- minLength: 1,
561
- description: "Path beginning /<app>/v2, as Planning Center documents it: /people/v2/people/1/emails. No host or query string.",
562
- };
563
- const VERSION_PROPERTY = {
564
- type: "string",
565
- pattern: "^\\d{4}-\\d{2}-\\d{2}$",
566
- description: "X-PCO-API-Version override. Omit to send this connector's reviewed pin for the path's app.",
567
- };
568
- function queryPairs(value) {
569
- const query = {};
570
- for (const row of asArray(value)) {
571
- const pair = asRecord(row);
572
- if (typeof pair["name"] !== "string")
573
- continue;
574
- const item = pair["value"];
575
- if (typeof item === "string" || typeof item === "number" || typeof item === "boolean") {
576
- query[pair["name"]] = item;
577
- }
578
- }
579
- return query;
580
- }
581
- // --- Output records ---------------------------------------------------------
582
- const PERSON_SUMMARY = {
583
- id: STRING,
584
- name: STRING,
585
- first_name: STRING,
586
- last_name: STRING,
587
- nickname: NULLABLE_STRING,
588
- status: STRING,
589
- membership: NULLABLE_STRING,
590
- child: BOOLEAN,
591
- primary_email: NULLABLE_STRING,
592
- primary_phone: NULLABLE_STRING,
593
- updated_at: STRING,
594
- };
595
- const PERSON_SUMMARY_SCHEMA = record(PERSON_SUMMARY);
596
- const PERSON_SCHEMA = record({
597
- ...PERSON_SUMMARY,
598
- given_name: NULLABLE_STRING,
599
- middle_name: NULLABLE_STRING,
600
- birthdate: NULLABLE_STRING,
601
- anniversary: NULLABLE_STRING,
602
- gender: NULLABLE_STRING,
603
- grade: NULLABLE_INTEGER,
604
- inactivated_at: NULLABLE_STRING,
605
- created_at: STRING,
606
- primary_campus: OBJECT,
607
- emails: ARRAY,
608
- phone_numbers: ARRAY,
609
- addresses: ARRAY,
610
- households: ARRAY,
611
- });
612
- const EMAIL_SCHEMA = record({
613
- id: STRING,
614
- address: STRING,
615
- location: NULLABLE_STRING,
616
- primary: BOOLEAN,
617
- person_id: STRING,
618
- });
619
- const PHONE_SCHEMA = record({
620
- id: STRING,
621
- number: STRING,
622
- location: NULLABLE_STRING,
623
- primary: BOOLEAN,
624
- person_id: STRING,
625
- });
626
- const CARD_SCHEMA = record({
627
- id: STRING,
628
- stage: STRING,
629
- person_id: NULLABLE_STRING,
630
- person_name: NULLABLE_STRING,
631
- assignee_id: NULLABLE_STRING,
632
- current_step_id: NULLABLE_STRING,
633
- current_step_name: NULLABLE_STRING,
634
- moved_to_step_at: NULLABLE_STRING,
635
- snooze_until: NULLABLE_STRING,
636
- overdue: BOOLEAN,
637
- completed_at: NULLABLE_STRING,
638
- removed_at: NULLABLE_STRING,
639
- });
640
- const NOTE_SCHEMA = record({
641
- id: STRING,
642
- note: STRING,
643
- note_category_id: STRING,
644
- category: NULLABLE_STRING,
645
- person_id: STRING,
646
- display_date: NULLABLE_STRING,
647
- created_at: STRING,
648
- created_by_id: NULLABLE_STRING,
649
- });
650
- const PLAN_PERSON_SCHEMA = record({
651
- id: STRING,
652
- name: STRING,
653
- status: STRING,
654
- team_position_name: NULLABLE_STRING,
655
- team_id: NULLABLE_STRING,
656
- team_name: NULLABLE_STRING,
657
- person_id: NULLABLE_STRING,
658
- decline_reason: NULLABLE_STRING,
659
- });
660
- const ITEM_SCHEMA = record({
661
- id: STRING,
662
- sequence: INTEGER,
663
- item_type: STRING,
664
- title: STRING,
665
- length: NULLABLE_INTEGER,
666
- key_name: NULLABLE_STRING,
667
- service_position: NULLABLE_STRING,
668
- song_id: NULLABLE_STRING,
669
- arrangement_id: NULLABLE_STRING,
670
- });
671
- const MEMBERSHIP_SCHEMA = record({
672
- id: STRING,
673
- role: STRING,
674
- joined_at: NULLABLE_STRING,
675
- person_id: STRING,
676
- first_name: STRING,
677
- last_name: STRING,
678
- });
679
- const DESIGNATIONS = {
680
- type: "array",
681
- items: record({ fund_id: NULLABLE_STRING, fund: NULLABLE_STRING, amount_cents: INTEGER }, ["amount_cents"]),
682
- };
683
- const DONATION_SCHEMA = record({
684
- id: STRING,
685
- amount_cents: INTEGER,
686
- amount_currency: STRING,
687
- fee_cents: NULLABLE_INTEGER,
688
- received_at: NULLABLE_STRING,
689
- payment_method: NULLABLE_STRING,
690
- payment_status: NULLABLE_STRING,
691
- refunded: BOOLEAN,
692
- person_id: NULLABLE_STRING,
693
- batch_id: NULLABLE_STRING,
694
- campus_id: NULLABLE_STRING,
695
- designations: DESIGNATIONS,
696
- });
697
- // --- Projections ------------------------------------------------------------
698
- function primaryOf(rows, key) {
699
- const primary = rows.find((row) => attribute(row, "primary") === true) ?? rows[0];
700
- const value = attribute(primary, key);
701
- return typeof value === "string" ? value : null;
702
- }
703
- function personSummary(person, index) {
704
- return {
705
- ...pick(person, ["name", "first_name", "last_name", "nickname", "status", "membership", "child"]),
706
- primary_email: primaryOf(relatedMany(person, "emails", index), "address"),
707
- primary_phone: primaryOf(relatedMany(person, "phone_numbers", index), "number"),
708
- ...pick(person, ["updated_at"]),
709
- id: person.id,
710
- };
711
- }
712
- function emailRecord(email) {
713
- return compact({
714
- ...pick(email, ["address", "location", "primary"]),
715
- person_id: relId(email, "person") ?? undefined,
716
- });
717
- }
718
- function phoneRecord(phone) {
719
- return compact({ ...pick(phone, ["number", "location", "primary"]), person_id: relId(phone, "person") ?? undefined });
720
- }
721
- function personRecord(person, index) {
722
- const campus = related(person, "primary_campus", index);
723
- return compact({
724
- ...personSummary(person, index),
725
- ...pick(person, [
726
- "given_name",
727
- "middle_name",
728
- "birthdate",
729
- "anniversary",
730
- "gender",
731
- "grade",
732
- "inactivated_at",
733
- "created_at",
734
- ]),
735
- id: person.id,
736
- primary_campus: campus
737
- ? { id: campus.id, name: attribute(campus, "name") }
738
- : relId(person, "primary_campus")
739
- ? { id: relId(person, "primary_campus") }
740
- : undefined,
741
- emails: relatedMany(person, "emails", index).map((email) => pick(email, ["address", "location", "primary"])),
742
- phone_numbers: relatedMany(person, "phone_numbers", index).map((phone) => pick(phone, ["number", "location", "primary"])),
743
- addresses: relatedMany(person, "addresses", index).map((address) => pick(address, ["street_line_1", "street_line_2", "city", "state", "zip", "country_code", "location", "primary"])),
744
- households: relatedMany(person, "households", index).map((household) => pick(household, ["name", "primary_contact_id", "member_count"])),
745
- });
746
- }
747
- function cardRecord(card, index) {
748
- const person = related(card, "person", index);
749
- const step = related(card, "current_step", index);
750
- return {
751
- ...pick(card, ["stage"]),
752
- person_id: relId(card, "person") ?? null,
753
- person_name: attribute(person, "name") ?? null,
754
- assignee_id: relId(card, "assignee") ?? null,
755
- current_step_id: relId(card, "current_step") ?? null,
756
- current_step_name: attribute(step, "name") ?? null,
757
- ...pick(card, ["moved_to_step_at", "snooze_until", "overdue", "completed_at", "removed_at"]),
758
- id: card.id,
759
- };
760
- }
761
- function noteRecord(note, index) {
762
- // `include=category` sideloads what the resource itself calls
763
- // `note_category`; the flat `note_category_id` settles it either way.
764
- const category = related(note, "category", index) ??
765
- related(note, "note_category", index) ??
766
- index.get(`NoteCategory:${String(attribute(note, "note_category_id"))}`);
767
- return {
768
- ...pick(note, ["note", "note_category_id", "person_id", "display_date", "created_at", "created_by_id"]),
769
- category: attribute(category, "name") ?? null,
770
- };
771
- }
772
- function planPersonRecord(member, index) {
773
- const team = related(member, "team", index);
774
- return {
775
- ...pick(member, ["name", "status", "team_position_name", "decline_reason"]),
776
- team_id: relId(member, "team") ?? null,
777
- team_name: attribute(team, "name") ?? null,
778
- person_id: relId(member, "person") ?? null,
779
- };
780
- }
781
- function itemRecord(item) {
782
- return {
783
- ...pick(item, ["sequence", "item_type", "title", "length", "key_name", "service_position"]),
784
- song_id: relId(item, "song") ?? null,
785
- arrangement_id: relId(item, "arrangement") ?? null,
786
- };
787
- }
788
- function membershipRecord(membership, index) {
789
- const person = related(membership, "person", index);
790
- return {
791
- ...pick(membership, ["role", "joined_at"]),
792
- person_id: relId(membership, "person") ?? null,
793
- first_name: attribute(person, "first_name") ?? null,
794
- last_name: attribute(person, "last_name") ?? null,
795
- };
796
- }
797
- function designations(owner, index) {
798
- return relatedMany(owner, "designations", index).map((designation) => {
799
- const fund = related(designation, "fund", index);
800
- return {
801
- fund_id: relId(designation, "fund") ?? null,
802
- fund: attribute(fund, "name") ?? null,
803
- amount_cents: attribute(designation, "amount_cents"),
804
- };
805
- });
806
- }
807
- function donationRecord(donation, index) {
808
- return {
809
- ...pick(donation, [
810
- "amount_cents",
811
- "amount_currency",
812
- "fee_cents",
813
- "received_at",
814
- "payment_method",
815
- "payment_status",
816
- "refunded",
817
- ]),
818
- person_id: relId(donation, "person") ?? null,
819
- batch_id: relId(donation, "batch") ?? null,
820
- campus_id: relId(donation, "campus") ?? null,
821
- designations: designations(donation, index),
822
- };
823
- }
824
- // ---------------------------------------------------------------------------
825
- // Tools
826
- // ---------------------------------------------------------------------------
827
- const READ = { readOnlyHint: true };
828
- /** Creates a record and changes nothing that existed: write-routed, not destructive. */
829
- const ADDITIVE = { readOnlyHint: false, destructiveHint: false };
830
- const DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true };
831
- /**
832
- * A hand-written list tool. The spec is authored per tool; the helper only
833
- * owns the parts every list shares — paging, `raw`, and the envelope.
834
- */
835
- function listTool(call, defaultPageSize, spec) {
836
- return {
837
- name: spec.name,
838
- description: spec.description,
839
- annotations: READ,
840
- inputSchema: input({
841
- ...spec.properties,
842
- perPage: perPageProperty(defaultPageSize),
843
- offset: OFFSET_PROPERTY,
844
- raw: RAW_PROPERTY,
845
- }, spec.required),
846
- outputSchema: listOutput(spec.key, spec.item),
847
- handler: async (args, ctx) => {
848
- const document = asRecord(await call({
849
- method: "GET",
850
- path: spec.path(args),
851
- query: {
852
- ...spec.query?.(args),
853
- include: spec.include,
854
- per_page: args["perPage"] ?? defaultPageSize,
855
- offset: args["offset"],
856
- },
857
- }, ctx));
858
- const rows = resources(document["data"]);
859
- if (args["raw"] === true) {
860
- return { [spec.key]: rows, included: asArray(document["included"]), page: pageOf(document) };
861
- }
862
- const index = indexIncluded(document);
863
- return { [spec.key]: rows.map((row) => spec.project(row, index)), page: pageOf(document) };
864
- },
865
- };
866
- }
867
- function segment(value) {
868
- return encodeURIComponent(String(value));
869
- }
870
- /** The single resource a response carries, or a refusal to invent one. */
871
- function created(document, what) {
872
- const resource = resources(asRecord(document)["data"])[0];
873
- if (!resource) {
874
- throw new ConnectorCallError("connector_call_failed", `Planning Center answered without the ${what} it should return.`, { retryable: false });
875
- }
876
- return resource;
877
- }
878
- /** `where[a][b][gte]` and `[lt]` for a half-open range on a (nested) attribute. */
879
- function range(attribute, after, before) {
880
- const key = `where[${attribute.join("][")}]`;
881
- return {
882
- [`${key}[gte]`]: typeof after === "string" ? after : undefined,
883
- [`${key}[lt]`]: typeof before === "string" ? before : undefined,
884
- };
885
- }
886
- const PERSON_ID = idProperty("Person id from search_people. The same id resolves in every Planning Center product.");
887
- const SERVICE_TYPE_ID = idProperty("Service type id from list_service_types.");
888
- const PLAN_ID = idProperty("Plan id from list_plans.");
889
- const PERSON_WRITE_PROPERTIES = {
890
- firstName: { type: "string", minLength: 1, description: "First name." },
891
- lastName: { type: "string", minLength: 1, description: "Last name." },
892
- nickname: { type: ["string", "null"], description: "Nickname; null clears it." },
893
- middleName: { type: ["string", "null"], description: "Middle name; null clears it." },
894
- birthdate: {
895
- type: ["string", "null"],
896
- pattern: "^\\d{4}-\\d{2}-\\d{2}$",
897
- description: "Birthdate, YYYY-MM-DD; null clears it.",
898
- },
899
- anniversary: {
900
- type: ["string", "null"],
901
- pattern: "^\\d{4}-\\d{2}-\\d{2}$",
902
- description: "Anniversary, YYYY-MM-DD; null clears it.",
903
- },
904
- gender: { type: ["string", "null"], description: "Gender as the organization records it, e.g. Male or Female." },
905
- membership: {
906
- type: ["string", "null"],
907
- description: "Membership type, one of the organization's own values such as Member or Visitor.",
908
- },
909
- child: { type: "boolean", description: "Whether the person is a child." },
910
- primaryCampusId: { type: "string", pattern: "^[0-9]+$", description: "Campus id from /people/v2/campuses." },
911
- };
912
- function personAttributes(args) {
913
- return {
914
- first_name: args["firstName"],
915
- last_name: args["lastName"],
916
- nickname: args["nickname"],
917
- middle_name: args["middleName"],
918
- birthdate: args["birthdate"],
919
- anniversary: args["anniversary"],
920
- gender: args["gender"],
921
- membership: args["membership"],
922
- child: args["child"],
923
- primary_campus_id: args["primaryCampusId"],
924
- status: args["status"],
925
- };
926
- }
927
- const WORKFLOW_CARD_ACTIONS = ["promote", "go_back", "skip_step", "snooze", "unsnooze", "remove", "restore"];
928
- function tools(call, defaultPageSize, excluded) {
929
- const perPage = (args) => args["perPage"] ?? defaultPageSize;
930
- const list = (spec) => listTool(call, defaultPageSize, spec);
931
- // --- Hatches --------------------------------------------------------------
932
- const hatches = [
933
- {
934
- name: "pco_api_get",
935
- description: "Call any Planning Center GET endpoint in any product and return its untouched JSON:API body. Prefer a named read; this one does not flatten or resolve includes.",
936
- annotations: READ,
937
- inputSchema: input({ path: HATCH_PATH_PROPERTY, query: QUERY_PROPERTY, version: VERSION_PROPERTY }, ["path"]),
938
- outputSchema: {
939
- type: "object",
940
- properties: { result: { description: "Planning Center's untouched response body." } },
941
- required: ["result"],
942
- },
943
- handler: async (args, ctx) => ({
944
- result: (await call(compact({
945
- method: "GET",
946
- path: String(args["path"]),
947
- query: queryPairs(args["query"]),
948
- version: args["version"],
949
- }), ctx)) ?? null,
950
- }),
951
- },
952
- {
953
- name: "pco_api_mutate",
954
- description: `Call any Planning Center POST, PATCH, or DELETE endpoint with a JSON:API body. The approval-gated hatch for ${excluded.has("giving") ? "writes and actions" : "writes, actions, and money movement"} no named tool covers.`,
955
- annotations: DESTRUCTIVE,
956
- inputSchema: input({
957
- method: {
958
- type: "string",
959
- enum: ["POST", "PATCH", "DELETE"],
960
- description: "Mutation method; Planning Center uses PATCH, never PUT, for updates.",
961
- },
962
- path: HATCH_PATH_PROPERTY,
963
- query: QUERY_PROPERTY,
964
- body: {
965
- type: "object",
966
- description: 'JSON:API document, never flat attributes: {"data":{"type":"Email","attributes":{...}}}. Omit for DELETE and bodiless actions.',
967
- },
968
- version: VERSION_PROPERTY,
969
- }, ["method", "path"]),
970
- outputSchema: {
971
- type: "object",
972
- properties: { result: { description: "Planning Center's untouched response body, or null for 204." } },
973
- required: ["result"],
974
- },
975
- handler: async (args, ctx) => ({
976
- result: (await call(compact({
977
- method: args["method"],
978
- path: String(args["path"]),
979
- query: queryPairs(args["query"]),
980
- body: args["body"],
981
- version: args["version"],
982
- }), ctx)) ?? null,
983
- }),
984
- },
985
- ];
986
- // --- People ---------------------------------------------------------------
987
- const people = [
988
- {
989
- name: "get_me",
990
- description: "Get the Planning Center person this token acts as, with the organization's name and time zone. Supplies personId for my-schedule and assignee questions.",
991
- annotations: READ,
992
- inputSchema: input({}),
993
- outputSchema: record({
994
- id: STRING,
995
- name: STRING,
996
- first_name: STRING,
997
- last_name: STRING,
998
- status: STRING,
999
- organization: record({ id: STRING, name: STRING, time_zone: STRING }),
1000
- }),
1001
- handler: async (_args, ctx) => {
1002
- const document = asRecord(await call({ method: "GET", path: "/people/v2/me", query: { include: "organization" } }, ctx));
1003
- const me = created(document, "person record");
1004
- const organization = resources(document["included"]).find((row) => row.type === "Organization");
1005
- return {
1006
- ...pick(me, ["name", "first_name", "last_name", "status"]),
1007
- ...(organization ? { organization: pick(organization, ["name", "time_zone"]) } : {}),
1008
- };
1009
- },
1010
- },
1011
- list({
1012
- name: "search_people",
1013
- description: "Search People profiles by name, email, or phone, with each person's primary email and phone. Omit search to list everyone; never searches custom fields or notes.",
1014
- key: "people",
1015
- item: PERSON_SUMMARY_SCHEMA,
1016
- properties: {
1017
- search: { type: "string", minLength: 1, description: "Matches name, email address, or phone number." },
1018
- status: { type: "string", enum: ["active", "inactive"], description: "Profile status." },
1019
- membership: { type: "string", minLength: 1, description: "Exact membership type, e.g. Member." },
1020
- updatedSince: dateProperty("Only people updated at or after this time."),
1021
- order: orderProperty(["last_name", "first_name", "created_at", "updated_at"]),
1022
- },
1023
- path: () => "/people/v2/people",
1024
- query: (args) => ({
1025
- "where[search_name_or_email_or_phone_number]": args["search"],
1026
- "where[status]": args["status"],
1027
- "where[membership]": args["membership"],
1028
- "where[updated_at][gte]": args["updatedSince"],
1029
- order: args["order"],
1030
- }),
1031
- include: "emails,phone_numbers",
1032
- project: personSummary,
1033
- }),
1034
- {
1035
- name: "get_person",
1036
- description: "Get one person's profile with every email, phone number, address, household, and primary campus. Custom fields are list_person_field_data.",
1037
- annotations: READ,
1038
- inputSchema: input({ personId: PERSON_ID, raw: RAW_PROPERTY }, ["personId"]),
1039
- outputSchema: PERSON_SCHEMA,
1040
- handler: async (args, ctx) => {
1041
- const document = asRecord(await call({
1042
- method: "GET",
1043
- path: `/people/v2/people/${segment(args["personId"])}`,
1044
- query: { include: "emails,phone_numbers,addresses,households,primary_campus" },
1045
- }, ctx));
1046
- const person = created(document, "person record");
1047
- return args["raw"] === true
1048
- ? { ...person, included: asArray(document["included"]) }
1049
- : personRecord(person, indexIncluded(document));
1050
- },
1051
- },
1052
- {
1053
- name: "create_person",
1054
- description: "Create a People profile. Emails and phone numbers are separate records; add them after with add_person_email and add_person_phone_number.",
1055
- annotations: ADDITIVE,
1056
- inputSchema: input(PERSON_WRITE_PROPERTIES, ["firstName", "lastName"]),
1057
- outputSchema: PERSON_SUMMARY_SCHEMA,
1058
- handler: async (args, ctx) => {
1059
- const document = await call({ method: "POST", path: "/people/v2/people", body: writeDocument("Person", personAttributes(args)) }, ctx);
1060
- return personSummary(created(document, "person"), new Map());
1061
- },
1062
- },
1063
- {
1064
- name: "update_person",
1065
- description: "Update fields on one People profile; omitted fields stay as they are. status inactive archives the person reversibly; this never deletes a profile.",
1066
- annotations: DESTRUCTIVE,
1067
- inputSchema: input({
1068
- personId: PERSON_ID,
1069
- ...PERSON_WRITE_PROPERTIES,
1070
- status: {
1071
- type: "string",
1072
- enum: ["active", "inactive"],
1073
- description: "inactive archives the profile; active restores it.",
1074
- },
1075
- }, ["personId"]),
1076
- outputSchema: PERSON_SUMMARY_SCHEMA,
1077
- handler: async (args, ctx) => {
1078
- const attributes = compact(personAttributes(args));
1079
- if (Object.keys(attributes).length === 0) {
1080
- throw new ConnectorCallError("invalid_args", "Nothing to update: provide at least one profile field.");
1081
- }
1082
- const document = await call({
1083
- method: "PATCH",
1084
- path: `/people/v2/people/${segment(args["personId"])}`,
1085
- body: { data: { type: "Person", id: String(args["personId"]), attributes } },
1086
- }, ctx);
1087
- return personSummary(created(document, "person update"), new Map());
1088
- },
1089
- },
1090
- {
1091
- name: "add_person_email",
1092
- description: "Add an email address to a person. Making it primary demotes the current primary; edit or delete existing addresses with pco_api_mutate.",
1093
- annotations: ADDITIVE,
1094
- inputSchema: input({
1095
- personId: PERSON_ID,
1096
- address: { type: "string", minLength: 3, description: "Email address." },
1097
- location: { type: "string", minLength: 1, description: "Label such as Home or Work. Defaults to Home." },
1098
- primary: { type: "boolean", description: "Make this the primary address." },
1099
- }, ["personId", "address"]),
1100
- outputSchema: EMAIL_SCHEMA,
1101
- handler: async (args, ctx) => emailRecord(created(await call({
1102
- method: "POST",
1103
- path: `/people/v2/people/${segment(args["personId"])}/emails`,
1104
- body: writeDocument("Email", {
1105
- address: args["address"],
1106
- location: args["location"] ?? "Home",
1107
- primary: args["primary"],
1108
- }),
1109
- }, ctx), "email")),
1110
- },
1111
- {
1112
- name: "add_person_phone_number",
1113
- description: "Add a phone number to a person. Making it primary demotes the current primary; edit or delete existing numbers with pco_api_mutate.",
1114
- annotations: ADDITIVE,
1115
- inputSchema: input({
1116
- personId: PERSON_ID,
1117
- number: { type: "string", minLength: 3, description: "Phone number as dialed; Planning Center formats it." },
1118
- location: {
1119
- type: "string",
1120
- minLength: 1,
1121
- description: "Label such as Mobile, Home, or Work. Defaults to Mobile.",
1122
- },
1123
- primary: { type: "boolean", description: "Make this the primary number." },
1124
- }, ["personId", "number"]),
1125
- outputSchema: PHONE_SCHEMA,
1126
- handler: async (args, ctx) => phoneRecord(created(await call({
1127
- method: "POST",
1128
- path: `/people/v2/people/${segment(args["personId"])}/phone_numbers`,
1129
- body: writeDocument("PhoneNumber", {
1130
- number: args["number"],
1131
- location: args["location"] ?? "Mobile",
1132
- primary: args["primary"],
1133
- }),
1134
- }, ctx), "phone number")),
1135
- },
1136
- list({
1137
- name: "list_lists",
1138
- description: "List People lists (saved, rule-based searches) with size and last refresh. Members come from list_list_people; results are only as fresh as refreshed_at.",
1139
- key: "lists",
1140
- item: record({
1141
- id: STRING,
1142
- name: NULLABLE_STRING,
1143
- description: NULLABLE_STRING,
1144
- total_people: INTEGER,
1145
- status: STRING,
1146
- refreshed_at: NULLABLE_STRING,
1147
- auto_refresh: BOOLEAN,
1148
- automations_active: BOOLEAN,
1149
- }),
1150
- properties: {
1151
- name: { type: "string", minLength: 1, description: "Exact list name; end it with % for a prefix match." },
1152
- order: orderProperty(["name", "updated_at", "refreshed_at"]),
1153
- },
1154
- path: () => "/people/v2/lists",
1155
- query: (args) => ({ "where[name]": args["name"], order: args["order"] }),
1156
- project: (row) => pick(row, [
1157
- "name",
1158
- "description",
1159
- "total_people",
1160
- "status",
1161
- "refreshed_at",
1162
- "auto_refresh",
1163
- "automations_active",
1164
- ]),
1165
- }),
1166
- list({
1167
- name: "list_list_people",
1168
- description: "List the people in one People list as of its last refresh, with primary email and phone. Call run_list first when the results must be current.",
1169
- key: "people",
1170
- item: PERSON_SUMMARY_SCHEMA,
1171
- properties: { listId: idProperty("List id from list_lists.") },
1172
- required: ["listId"],
1173
- path: (args) => `/people/v2/lists/${segment(args["listId"])}/people`,
1174
- include: "emails,phone_numbers",
1175
- project: personSummary,
1176
- }),
1177
- {
1178
- name: "run_list",
1179
- description: "Refresh a People list's results now. Destructive: a refresh can fire the list's automations, such as adding people to workflows or forms.",
1180
- annotations: DESTRUCTIVE,
1181
- inputSchema: input({ listId: idProperty("List id from list_lists.") }, ["listId"]),
1182
- outputSchema: record({ listId: STRING, queued: BOOLEAN }, ["listId", "queued"]),
1183
- handler: async (args, ctx) => {
1184
- await call({ method: "POST", path: `/people/v2/lists/${segment(args["listId"])}/run` }, ctx);
1185
- return { listId: String(args["listId"]), queued: true };
1186
- },
1187
- },
1188
- list({
1189
- name: "list_workflows",
1190
- description: "List People workflows with their steps and ready, overdue, and unassigned card counts. Archived workflows are excluded unless asked for.",
1191
- key: "workflows",
1192
- item: record({
1193
- id: STRING,
1194
- name: STRING,
1195
- total_cards_count: INTEGER,
1196
- total_ready_card_count: INTEGER,
1197
- total_overdue_card_count: INTEGER,
1198
- total_unassigned_card_count: INTEGER,
1199
- my_ready_card_count: INTEGER,
1200
- archived_at: NULLABLE_STRING,
1201
- steps: ARRAY,
1202
- }),
1203
- properties: {
1204
- name: { type: "string", minLength: 1, description: "Exact workflow name; end it with % for a prefix match." },
1205
- includeArchived: { type: "boolean", description: "Include archived workflows." },
1206
- },
1207
- path: () => "/people/v2/workflows",
1208
- query: (args) => ({
1209
- "where[name]": args["name"],
1210
- filter: args["includeArchived"] === true ? undefined : "not_archived",
1211
- }),
1212
- include: "steps",
1213
- project: (row, index) => ({
1214
- ...pick(row, [
1215
- "name",
1216
- "total_cards_count",
1217
- "total_ready_card_count",
1218
- "total_overdue_card_count",
1219
- "total_unassigned_card_count",
1220
- "my_ready_card_count",
1221
- "archived_at",
1222
- ]),
1223
- steps: relatedMany(row, "steps", index)
1224
- .map((step) => pick(step, ["name", "sequence"]))
1225
- .sort((a, b) => Number(a["sequence"] ?? 0) - Number(b["sequence"] ?? 0)),
1226
- }),
1227
- }),
1228
- list({
1229
- name: "list_workflow_cards",
1230
- description: "List the cards in one workflow with each card's person, assignee, and current step. Card actions need both the card id and its person_id.",
1231
- key: "cards",
1232
- item: CARD_SCHEMA,
1233
- properties: {
1234
- workflowId: idProperty("Workflow id from list_workflows."),
1235
- stage: {
1236
- type: "string",
1237
- minLength: 1,
1238
- description: "Card stage as Planning Center reports it, e.g. ready, snoozed, completed, removed.",
1239
- },
1240
- stepId: idProperty("Only cards on this step (list_workflows steps[].id)."),
1241
- assigneeId: idProperty("Only cards assigned to this person id."),
1242
- overdue: { type: "boolean", description: "Only overdue (true) or on-time (false) cards." },
1243
- order: orderProperty(["created_at", "moved_to_step_at", "last_name"]),
1244
- },
1245
- required: ["workflowId"],
1246
- path: (args) => `/people/v2/workflows/${segment(args["workflowId"])}/cards`,
1247
- query: (args) => ({
1248
- "where[stage]": args["stage"],
1249
- "where[step_id]": args["stepId"],
1250
- "where[assignee_id]": args["assigneeId"],
1251
- "where[overdue]": args["overdue"],
1252
- order: args["order"],
1253
- }),
1254
- include: "person,current_step",
1255
- project: cardRecord,
1256
- }),
1257
- {
1258
- name: "add_workflow_card",
1259
- description: "Add a person to a workflow as a new card on its first step. Optionally assign it; the workflow's own step defaults apply otherwise.",
1260
- annotations: ADDITIVE,
1261
- inputSchema: input({
1262
- workflowId: idProperty("Workflow id from list_workflows."),
1263
- personId: PERSON_ID,
1264
- assigneeId: idProperty("Person id to assign the card to."),
1265
- }, ["workflowId", "personId"]),
1266
- outputSchema: CARD_SCHEMA,
1267
- handler: async (args, ctx) => cardRecord(created(await call({
1268
- method: "POST",
1269
- path: `/people/v2/workflows/${segment(args["workflowId"])}/cards`,
1270
- body: writeDocument("WorkflowCard", { person_id: args["personId"], assignee_id: args["assigneeId"] }),
1271
- }, ctx), "workflow card"), new Map()),
1272
- },
1273
- {
1274
- name: "apply_workflow_card_action",
1275
- description: "Promote, send back, skip a step on, snooze, unsnooze, remove, or restore one workflow card. Never emails; sending a card email is pco_api_mutate.",
1276
- annotations: DESTRUCTIVE,
1277
- inputSchema: input({
1278
- personId: { ...PERSON_ID, description: "The card's person_id from list_workflow_cards." },
1279
- cardId: idProperty("Workflow card id from list_workflow_cards."),
1280
- action: {
1281
- type: "string",
1282
- enum: [...WORKFLOW_CARD_ACTIONS],
1283
- description: "promote completes the step; skip_step moves on without completing it; go_back returns to the previous step.",
1284
- },
1285
- snoozeDays: {
1286
- type: "integer",
1287
- minimum: 1,
1288
- maximum: 365,
1289
- description: "Days to snooze; required with action snooze and refused otherwise.",
1290
- },
1291
- }, ["personId", "cardId", "action"]),
1292
- outputSchema: record({ cardId: STRING, action: STRING, applied: BOOLEAN }, ["cardId", "action", "applied"]),
1293
- handler: async (args, ctx) => {
1294
- const snooze = args["action"] === "snooze";
1295
- if (snooze !== (args["snoozeDays"] !== undefined)) {
1296
- throw new ConnectorCallError("invalid_args", snooze ? "action snooze needs snoozeDays." : "snoozeDays applies only to action snooze.");
1297
- }
1298
- await call({
1299
- method: "POST",
1300
- path: `/people/v2/people/${segment(args["personId"])}/workflow_cards/${segment(args["cardId"])}/${String(args["action"])}`,
1301
- ...(snooze ? { body: { data: { attributes: { duration: args["snoozeDays"] } } } } : {}),
1302
- }, ctx);
1303
- return { cardId: String(args["cardId"]), action: String(args["action"]), applied: true };
1304
- },
1305
- },
1306
- {
1307
- name: "add_workflow_card_note",
1308
- description: "Add a note to one workflow card's activity. Notifies nobody by itself and does not move the card.",
1309
- annotations: ADDITIVE,
1310
- inputSchema: input({
1311
- personId: { ...PERSON_ID, description: "The card's person_id from list_workflow_cards." },
1312
- cardId: idProperty("Workflow card id from list_workflow_cards."),
1313
- note: { type: "string", minLength: 1, description: "Note text." },
1314
- }, ["personId", "cardId", "note"]),
1315
- outputSchema: record({ id: STRING, note: STRING, created_at: STRING }),
1316
- handler: async (args, ctx) => pick(created(await call({
1317
- method: "POST",
1318
- path: `/people/v2/people/${segment(args["personId"])}/workflow_cards/${segment(args["cardId"])}/notes`,
1319
- body: writeDocument("WorkflowCardNote", { note: args["note"] }),
1320
- }, ctx), "card note"), ["note", "created_at"]),
1321
- },
1322
- list({
1323
- name: "list_person_notes",
1324
- description: "List the notes on one person's profile with their category names, newest first by default. Workflow card notes are not profile notes.",
1325
- key: "notes",
1326
- item: NOTE_SCHEMA,
1327
- properties: {
1328
- personId: PERSON_ID,
1329
- noteCategoryId: idProperty("Only notes in this category (/people/v2/note_categories)."),
1330
- order: orderProperty(["created_at", "display_date"]),
1331
- },
1332
- required: ["personId"],
1333
- path: (args) => `/people/v2/people/${segment(args["personId"])}/notes`,
1334
- query: (args) => ({ "where[note_category_id]": args["noteCategoryId"], order: args["order"] ?? "-created_at" }),
1335
- include: "category",
1336
- project: noteRecord,
1337
- }),
1338
- {
1339
- name: "add_person_note",
1340
- description: "Add a note to a person's profile in a note category. Category subscribers may be notified; read categories at /people/v2/note_categories.",
1341
- annotations: ADDITIVE,
1342
- inputSchema: input({
1343
- personId: PERSON_ID,
1344
- note: { type: "string", minLength: 1, description: "Note text." },
1345
- noteCategoryId: idProperty("Note category id; Planning Center files every note under one."),
1346
- }, ["personId", "note", "noteCategoryId"]),
1347
- outputSchema: NOTE_SCHEMA,
1348
- handler: async (args, ctx) => noteRecord(created(await call({
1349
- method: "POST",
1350
- path: `/people/v2/people/${segment(args["personId"])}/notes`,
1351
- body: writeDocument("Note", { note: args["note"], note_category_id: args["noteCategoryId"] }),
1352
- }, ctx), "note"), new Map()),
1353
- },
1354
- list({
1355
- name: "list_person_field_data",
1356
- description: "List one person's custom field values, each labeled with its field name and data type. Writing a value is pco_api_mutate at /people/v2/field_data.",
1357
- key: "fields",
1358
- item: record({
1359
- id: STRING,
1360
- field_definition_id: STRING,
1361
- field: NULLABLE_STRING,
1362
- data_type: NULLABLE_STRING,
1363
- value: NULLABLE_STRING,
1364
- file_name: NULLABLE_STRING,
1365
- }),
1366
- properties: { personId: PERSON_ID },
1367
- required: ["personId"],
1368
- path: (args) => `/people/v2/people/${segment(args["personId"])}/field_data`,
1369
- include: "field_definition",
1370
- project: (row, index) => {
1371
- const definition = related(row, "field_definition", index);
1372
- return {
1373
- id: row.id,
1374
- field_definition_id: relId(row, "field_definition") ?? null,
1375
- field: attribute(definition, "name") ?? null,
1376
- data_type: attribute(definition, "data_type") ?? null,
1377
- value: attribute(row, "value") ?? null,
1378
- file_name: attribute(row, "file_name") ?? null,
1379
- };
1380
- },
1381
- }),
1382
- list({
1383
- name: "list_forms",
1384
- description: "List People forms with their submission counts and public URLs. Archived forms are excluded unless asked for.",
1385
- key: "forms",
1386
- item: record({
1387
- id: STRING,
1388
- name: STRING,
1389
- description: NULLABLE_STRING,
1390
- active: BOOLEAN,
1391
- submission_count: INTEGER,
1392
- public_url: NULLABLE_STRING,
1393
- archived_at: NULLABLE_STRING,
1394
- }),
1395
- properties: { includeArchived: { type: "boolean", description: "Include archived forms." } },
1396
- path: () => "/people/v2/forms",
1397
- query: (args) => ({ filter: args["includeArchived"] === true ? undefined : "not_archived" }),
1398
- project: (row) => pick(row, ["name", "description", "active", "submission_count", "public_url", "archived_at"]),
1399
- }),
1400
- list({
1401
- name: "list_form_submissions",
1402
- description: "List one form's submissions with each answer labeled by its field, newest first by default. Read-only; the API cannot submit a form.",
1403
- key: "submissions",
1404
- item: record({
1405
- id: STRING,
1406
- created_at: STRING,
1407
- person_id: NULLABLE_STRING,
1408
- person_name: NULLABLE_STRING,
1409
- values: { type: "array", items: record({ field: NULLABLE_STRING, value: NULLABLE_STRING }, []) },
1410
- }),
1411
- properties: {
1412
- formId: idProperty("Form id from list_forms."),
1413
- submittedAfter: dateProperty("Only submissions created at or after this time."),
1414
- order: orderProperty(["created_at"]),
1415
- },
1416
- required: ["formId"],
1417
- path: (args) => `/people/v2/forms/${segment(args["formId"])}/form_submissions`,
1418
- query: (args) => ({ "where[created_at][gte]": args["submittedAfter"], order: args["order"] ?? "-created_at" }),
1419
- include: "form_submission_values,form_fields,person",
1420
- project: (row, index) => {
1421
- const person = related(row, "person", index);
1422
- const values = relatedMany(row, "form_submission_values", index);
1423
- return {
1424
- id: row.id,
1425
- created_at: attribute(row, "created_at"),
1426
- person_id: relId(row, "person") ?? null,
1427
- person_name: attribute(person, "name") ?? null,
1428
- values: values.map((value) => ({
1429
- field: attribute(related(value, "form_field", index), "label") ?? null,
1430
- value: attribute(value, "display_value") ?? attribute(value, "value") ?? null,
1431
- })),
1432
- };
1433
- },
1434
- }),
1435
- ];
1436
- // --- Services -------------------------------------------------------------
1437
- const services = [
1438
- list({
1439
- name: "list_service_types",
1440
- description: "List Services service types — the recurring services such as Sunday Morning. Supplies serviceTypeId for plans and teams.",
1441
- key: "serviceTypes",
1442
- item: record({
1443
- id: STRING,
1444
- name: STRING,
1445
- frequency: NULLABLE_STRING,
1446
- sequence: NULLABLE_INTEGER,
1447
- parent_id: NULLABLE_STRING,
1448
- }),
1449
- path: () => "/services/v2/service_types",
1450
- query: () => ({ order: "sequence" }),
1451
- project: (row) => ({ ...pick(row, ["name", "frequency", "sequence"]), parent_id: relId(row, "parent") ?? null }),
1452
- }),
1453
- list({
1454
- name: "list_plans",
1455
- description: "List one service type's plans (individual services) by date. Defaults to upcoming plans, soonest first; get_plan returns the order of service and team.",
1456
- key: "plans",
1457
- item: record({
1458
- id: STRING,
1459
- title: NULLABLE_STRING,
1460
- series_title: NULLABLE_STRING,
1461
- dates: STRING,
1462
- sort_date: NULLABLE_STRING,
1463
- items_count: INTEGER,
1464
- plan_people_count: INTEGER,
1465
- needed_positions_count: INTEGER,
1466
- planning_center_url: STRING,
1467
- }),
1468
- properties: {
1469
- serviceTypeId: SERVICE_TYPE_ID,
1470
- when: {
1471
- type: "string",
1472
- enum: ["future", "past", "all"],
1473
- description: "future (default) sorts soonest first; past sorts most recent first.",
1474
- },
1475
- },
1476
- required: ["serviceTypeId"],
1477
- path: (args) => `/services/v2/service_types/${segment(args["serviceTypeId"])}/plans`,
1478
- query: (args) => {
1479
- const when = args["when"] ?? "future";
1480
- return {
1481
- filter: when === "all" ? undefined : when,
1482
- order: when === "past" ? "-sort_date" : "sort_date",
1483
- };
1484
- },
1485
- project: (row) => pick(row, [
1486
- "title",
1487
- "series_title",
1488
- "dates",
1489
- "sort_date",
1490
- "items_count",
1491
- "plan_people_count",
1492
- "needed_positions_count",
1493
- "planning_center_url",
1494
- ]),
1495
- }),
1496
- {
1497
- name: "get_plan",
1498
- description: "Get one plan with its times, order of service, and scheduled team in a single call. Flags a section past 100 rows rather than dropping it silently.",
1499
- annotations: READ,
1500
- inputSchema: input({ serviceTypeId: SERVICE_TYPE_ID, planId: PLAN_ID, raw: RAW_PROPERTY }, [
1501
- "serviceTypeId",
1502
- "planId",
1503
- ]),
1504
- outputSchema: {
1505
- type: "object",
1506
- properties: {
1507
- plan: OBJECT,
1508
- items: { type: "array", items: ITEM_SCHEMA },
1509
- teamMembers: { type: "array", items: PLAN_PERSON_SCHEMA },
1510
- itemsTruncated: BOOLEAN,
1511
- teamMembersTruncated: BOOLEAN,
1512
- },
1513
- required: ["plan", "items", "teamMembers", "itemsTruncated", "teamMembersTruncated"],
1514
- },
1515
- handler: async (args, ctx) => {
1516
- const base = `/services/v2/service_types/${segment(args["serviceTypeId"])}/plans/${segment(args["planId"])}`;
1517
- const [planDocument, itemDocument, teamDocument] = (await Promise.all([
1518
- call({ method: "GET", path: base, query: { include: "plan_times" } }, ctx),
1519
- call({ method: "GET", path: `${base}/items`, query: { per_page: MAX_PAGE_SIZE } }, ctx),
1520
- call({ method: "GET", path: `${base}/team_members`, query: { include: "team", per_page: MAX_PAGE_SIZE } }, ctx),
1521
- ])).map(asRecord);
1522
- const plan = created(planDocument, "plan record");
1523
- const items = resources(itemDocument["data"]);
1524
- const members = resources(teamDocument["data"]);
1525
- const truncation = {
1526
- itemsTruncated: pageOf(itemDocument).hasMore,
1527
- teamMembersTruncated: pageOf(teamDocument).hasMore,
1528
- };
1529
- if (args["raw"] === true) {
1530
- return {
1531
- plan: { ...plan, included: asArray(planDocument["included"]) },
1532
- items,
1533
- teamMembers: members,
1534
- ...truncation,
1535
- };
1536
- }
1537
- const planIndex = indexIncluded(planDocument);
1538
- const teamIndex = indexIncluded(teamDocument);
1539
- return {
1540
- plan: {
1541
- ...pick(plan, [
1542
- "title",
1543
- "series_title",
1544
- "dates",
1545
- "sort_date",
1546
- "total_length",
1547
- "needed_positions_count",
1548
- "planning_center_url",
1549
- ]),
1550
- times: relatedMany(plan, "plan_times", planIndex).map((time) => pick(time, ["name", "time_type", "starts_at", "ends_at"])),
1551
- },
1552
- items: items.map(itemRecord).sort((a, b) => Number(a["sequence"] ?? 0) - Number(b["sequence"] ?? 0)),
1553
- teamMembers: members.map((member) => planPersonRecord(member, teamIndex)),
1554
- ...truncation,
1555
- };
1556
- },
1557
- },
1558
- list({
1559
- name: "list_teams",
1560
- description: "List one service type's teams with their positions. Supplies teamId and position names for schedule_plan_person.",
1561
- key: "teams",
1562
- item: record({ id: STRING, name: STRING, archived_at: NULLABLE_STRING, positions: ARRAY }),
1563
- properties: { serviceTypeId: SERVICE_TYPE_ID },
1564
- required: ["serviceTypeId"],
1565
- path: (args) => `/services/v2/service_types/${segment(args["serviceTypeId"])}/teams`,
1566
- include: "team_positions",
1567
- project: (row, index) => ({
1568
- ...pick(row, ["name", "archived_at"]),
1569
- positions: relatedMany(row, "team_positions", index).map((position) => pick(position, ["name"])),
1570
- }),
1571
- }),
1572
- list({
1573
- name: "list_person_schedules",
1574
- description: "List where one person is scheduled to serve across every service type, with team, position, and response. Defaults to upcoming.",
1575
- key: "schedules",
1576
- item: record({
1577
- id: STRING,
1578
- dates: STRING,
1579
- sort_date: NULLABLE_STRING,
1580
- service_type_name: STRING,
1581
- team_name: NULLABLE_STRING,
1582
- team_position_name: NULLABLE_STRING,
1583
- status: STRING,
1584
- decline_reason: NULLABLE_STRING,
1585
- plan_id: NULLABLE_STRING,
1586
- }),
1587
- properties: {
1588
- personId: PERSON_ID,
1589
- when: { type: "string", enum: ["future", "past"], description: "future (default) or past schedules." },
1590
- },
1591
- required: ["personId"],
1592
- path: (args) => `/services/v2/people/${segment(args["personId"])}/schedules`,
1593
- query: (args) => ({
1594
- filter: args["when"] ?? "future",
1595
- order: args["when"] === "past" ? "-starts_at" : "starts_at",
1596
- }),
1597
- project: (row) => ({
1598
- ...pick(row, [
1599
- "dates",
1600
- "sort_date",
1601
- "service_type_name",
1602
- "team_name",
1603
- "team_position_name",
1604
- "status",
1605
- "decline_reason",
1606
- ]),
1607
- plan_id: relId(row, "plan") ?? null,
1608
- }),
1609
- }),
1610
- {
1611
- name: "schedule_plan_person",
1612
- description: "Schedule a person on a plan's team, unconfirmed by default. Prepares a notification only when asked; Planning Center sends it, not this call.",
1613
- annotations: ADDITIVE,
1614
- inputSchema: input({
1615
- serviceTypeId: SERVICE_TYPE_ID,
1616
- planId: PLAN_ID,
1617
- personId: PERSON_ID,
1618
- teamId: idProperty("Team id from list_teams."),
1619
- teamPositionName: {
1620
- type: "string",
1621
- minLength: 1,
1622
- description: "Position name from list_teams positions[].name.",
1623
- },
1624
- status: { type: "string", enum: ["U", "C"], description: "U unconfirmed (default) or C confirmed." },
1625
- prepareNotification: {
1626
- type: "boolean",
1627
- description: "Queue Planning Center's scheduling notification for this person.",
1628
- },
1629
- notes: { type: "string", description: "Note shown to the person." },
1630
- }, ["serviceTypeId", "planId", "personId", "teamId"]),
1631
- outputSchema: PLAN_PERSON_SCHEMA,
1632
- handler: async (args, ctx) => planPersonRecord(created(await call({
1633
- method: "POST",
1634
- path: `/services/v2/service_types/${segment(args["serviceTypeId"])}/plans/${segment(args["planId"])}/team_members`,
1635
- body: writeDocument("PlanPerson", {
1636
- person_id: args["personId"],
1637
- team_id: args["teamId"],
1638
- team_position_name: args["teamPositionName"],
1639
- status: args["status"] ?? "U",
1640
- prepare_notification: args["prepareNotification"],
1641
- notes: args["notes"],
1642
- }),
1643
- }, ctx), "scheduled person"), new Map()),
1644
- },
1645
- {
1646
- name: "add_plan_item",
1647
- description: "Add an item — a song, header, or other element — to a plan's order of service. A songId makes a song item; media_ids, set through pco_api_mutate, make a media item.",
1648
- annotations: ADDITIVE,
1649
- inputSchema: input({
1650
- serviceTypeId: SERVICE_TYPE_ID,
1651
- planId: PLAN_ID,
1652
- title: { type: "string", minLength: 1, description: "Item title; for a song, usually the song title." },
1653
- itemType: {
1654
- type: "string",
1655
- enum: ["item", "header"],
1656
- description: "header for a section header, fixed at creation; item (the default) for anything else. Song and media types follow from what is attached, never this.",
1657
- },
1658
- songId: idProperty("Song id from list_songs, for a song item."),
1659
- arrangementId: idProperty("Arrangement id from list_song_arrangements."),
1660
- keyId: idProperty("Key id from list_song_arrangements keys[].id."),
1661
- length: { type: "integer", minimum: 0, maximum: 86_400, description: "Length in seconds." },
1662
- sequence: { type: "integer", minimum: 0, description: "Position in the order; omit to append." },
1663
- servicePosition: {
1664
- type: "string",
1665
- enum: ["pre", "during", "post"],
1666
- description: "Before, during, or after the service. Defaults to during.",
1667
- },
1668
- description: { type: "string", description: "Item description." },
1669
- }, ["serviceTypeId", "planId", "title"]),
1670
- outputSchema: ITEM_SCHEMA,
1671
- handler: async (args, ctx) => itemRecord(created(await call({
1672
- method: "POST",
1673
- path: `/services/v2/service_types/${segment(args["serviceTypeId"])}/plans/${segment(args["planId"])}/items`,
1674
- body: writeDocument("Item", {
1675
- title: args["title"],
1676
- // Planning Center assigns only `header`; omitting it is `item`.
1677
- item_type: args["itemType"] === "header" ? "header" : undefined,
1678
- song_id: args["songId"],
1679
- arrangement_id: args["arrangementId"],
1680
- key_id: args["keyId"],
1681
- length: args["length"],
1682
- sequence: args["sequence"],
1683
- service_position: args["servicePosition"],
1684
- description: args["description"],
1685
- }),
1686
- }, ctx), "plan item")),
1687
- },
1688
- list({
1689
- name: "list_songs",
1690
- description: "Search the Services song library by title, author, CCLI number, or theme. Lyrics and chord charts live on arrangements.",
1691
- key: "songs",
1692
- item: record({
1693
- id: STRING,
1694
- title: STRING,
1695
- author: NULLABLE_STRING,
1696
- ccli_number: NULLABLE_INTEGER,
1697
- themes: NULLABLE_STRING,
1698
- hidden: BOOLEAN,
1699
- last_scheduled_at: NULLABLE_STRING,
1700
- }),
1701
- properties: {
1702
- title: { type: "string", minLength: 1, description: "Song title; end it with % for a prefix match." },
1703
- author: { type: "string", minLength: 1, description: "Author." },
1704
- ccliNumber: { type: "integer", minimum: 1, description: "CCLI song number." },
1705
- theme: { type: "string", minLength: 1, description: "Theme." },
1706
- order: orderProperty(["title", "last_scheduled_at"]),
1707
- },
1708
- path: () => "/services/v2/songs",
1709
- query: (args) => ({
1710
- "where[title]": args["title"],
1711
- "where[author]": args["author"],
1712
- "where[ccli_number]": args["ccliNumber"],
1713
- "where[themes]": args["theme"],
1714
- order: args["order"],
1715
- }),
1716
- project: (row) => pick(row, ["title", "author", "ccli_number", "themes", "hidden", "last_scheduled_at"]),
1717
- }),
1718
- list({
1719
- name: "list_song_arrangements",
1720
- description: "List one song's arrangements with tempo, meter, sequence, and keys. Supplies arrangementId and keyId for add_plan_item.",
1721
- key: "arrangements",
1722
- item: record({
1723
- id: STRING,
1724
- name: STRING,
1725
- bpm: { type: ["number", "null"] },
1726
- meter: NULLABLE_STRING,
1727
- length: NULLABLE_INTEGER,
1728
- chord_chart_key: NULLABLE_STRING,
1729
- sequence_short: ARRAY,
1730
- keys: ARRAY,
1731
- }),
1732
- properties: { songId: idProperty("Song id from list_songs.") },
1733
- required: ["songId"],
1734
- path: (args) => `/services/v2/songs/${segment(args["songId"])}/arrangements`,
1735
- include: "keys",
1736
- project: (row, index) => ({
1737
- ...pick(row, ["name", "bpm", "meter", "length", "chord_chart_key", "sequence_short"]),
1738
- keys: relatedMany(row, "keys", index).map((key) => pick(key, ["name", "starting_key", "ending_key"])),
1739
- }),
1740
- }),
1741
- ];
1742
- // --- Groups ---------------------------------------------------------------
1743
- const groups = [
1744
- list({
1745
- name: "list_groups",
1746
- description: "List Groups groups with type, schedule, and member count. Archived groups are excluded unless asked for; the API cannot create a group.",
1747
- key: "groups",
1748
- item: record({
1749
- id: STRING,
1750
- name: STRING,
1751
- memberships_count: INTEGER,
1752
- schedule: NULLABLE_STRING,
1753
- group_type_id: NULLABLE_STRING,
1754
- contact_email: NULLABLE_STRING,
1755
- archived_at: NULLABLE_STRING,
1756
- public_church_center_web_url: NULLABLE_STRING,
1757
- }),
1758
- properties: {
1759
- name: { type: "string", minLength: 1, description: "Exact group name; end it with % for a prefix match." },
1760
- groupTypeId: idProperty("Only groups of this type (/groups/v2/group_types)."),
1761
- archived: {
1762
- type: "string",
1763
- enum: ["exclude", "include", "only"],
1764
- description: "Archived groups: exclude (default), include, or only.",
1765
- },
1766
- order: orderProperty(["name", "created_at", "memberships_count"]),
1767
- },
1768
- path: () => "/groups/v2/groups",
1769
- query: (args) => ({
1770
- "where[name]": args["name"],
1771
- "where[group_type][id]": args["groupTypeId"],
1772
- "where[archive_status]": args["archived"] === "include" || args["archived"] === "only" ? args["archived"] : undefined,
1773
- order: args["order"],
1774
- }),
1775
- project: (row) => ({
1776
- ...pick(row, [
1777
- "name",
1778
- "memberships_count",
1779
- "schedule",
1780
- "contact_email",
1781
- "archived_at",
1782
- "public_church_center_web_url",
1783
- ]),
1784
- group_type_id: relId(row, "group_type") ?? null,
1785
- }),
1786
- }),
1787
- list({
1788
- name: "list_group_memberships",
1789
- description: "List one group's members and leaders with their names and join dates. Contact details come from get_person with person_id.",
1790
- key: "memberships",
1791
- item: MEMBERSHIP_SCHEMA,
1792
- properties: {
1793
- groupId: idProperty("Group id from list_groups."),
1794
- role: { type: "string", enum: ["member", "leader"], description: "Only members or only leaders." },
1795
- order: orderProperty(["last_name", "first_name", "joined_at", "role"]),
1796
- },
1797
- required: ["groupId"],
1798
- path: (args) => `/groups/v2/groups/${segment(args["groupId"])}/memberships`,
1799
- query: (args) => ({ "where[role]": args["role"], order: args["order"] }),
1800
- include: "person",
1801
- project: membershipRecord,
1802
- }),
1803
- {
1804
- name: "add_group_member",
1805
- description: "Add a person to a group as a member or leader. Fails when they already belong; change a role or remove someone with pco_api_mutate.",
1806
- annotations: ADDITIVE,
1807
- inputSchema: input({
1808
- groupId: idProperty("Group id from list_groups."),
1809
- personId: PERSON_ID,
1810
- role: { type: "string", enum: ["member", "leader"], description: "Defaults to member." },
1811
- joinedAt: dateProperty("When they joined. Defaults to now."),
1812
- }, ["groupId", "personId"]),
1813
- outputSchema: MEMBERSHIP_SCHEMA,
1814
- handler: async (args, ctx) => {
1815
- const document = asRecord(await call({
1816
- method: "POST",
1817
- path: `/groups/v2/groups/${segment(args["groupId"])}/memberships`,
1818
- query: { include: "person" },
1819
- body: writeDocument("Membership", {
1820
- person_id: args["personId"],
1821
- role: args["role"] ?? "member",
1822
- joined_at: args["joinedAt"],
1823
- }),
1824
- }, ctx));
1825
- return membershipRecord(created(document, "membership"), indexIncluded(document));
1826
- },
1827
- },
1828
- list({
1829
- name: "list_group_events",
1830
- description: "List Groups meetings in a date range, for one group or all groups. Read-only: the API cannot create events or record attendance.",
1831
- key: "events",
1832
- item: record({
1833
- id: STRING,
1834
- name: STRING,
1835
- starts_at: STRING,
1836
- ends_at: NULLABLE_STRING,
1837
- canceled: BOOLEAN,
1838
- group_id: NULLABLE_STRING,
1839
- visitors_count: NULLABLE_INTEGER,
1840
- virtual_location_url: NULLABLE_STRING,
1841
- }),
1842
- properties: {
1843
- groupId: idProperty("One group's events. Omit for every group."),
1844
- startsAfter: dateProperty("Only events starting at or after this time."),
1845
- startsBefore: dateProperty("Only events starting before this time."),
1846
- includeCanceled: { type: "boolean", description: "Include canceled events." },
1847
- },
1848
- path: (args) => (args["groupId"] ? `/groups/v2/groups/${segment(args["groupId"])}/events` : "/groups/v2/events"),
1849
- query: (args) => ({
1850
- ...range(["starts_at"], args["startsAfter"], args["startsBefore"]),
1851
- filter: args["includeCanceled"] === true ? undefined : "not_canceled",
1852
- order: "starts_at",
1853
- }),
1854
- project: (row) => ({
1855
- ...pick(row, ["name", "starts_at", "ends_at", "canceled", "visitors_count", "virtual_location_url"]),
1856
- group_id: relId(row, "group") ?? null,
1857
- }),
1858
- }),
1859
- ];
1860
- // --- Check-Ins (read-only product) -----------------------------------------
1861
- const checkIns = [
1862
- list({
1863
- name: "list_check_in_events",
1864
- description: "List Check-Ins events, the recurring things people check in to. Supplies eventId for event periods and check-ins.",
1865
- key: "events",
1866
- item: record({ id: STRING, name: STRING, frequency: NULLABLE_STRING, archived_at: NULLABLE_STRING }),
1867
- properties: {
1868
- name: { type: "string", minLength: 1, description: "Exact event name; end it with % for a prefix match." },
1869
- includeArchived: { type: "boolean", description: "Include archived events." },
1870
- },
1871
- path: () => "/check-ins/v2/events",
1872
- query: (args) => ({
1873
- "where[name]": args["name"],
1874
- filter: args["includeArchived"] === true ? undefined : "not_archived",
1875
- }),
1876
- project: (row) => pick(row, ["name", "frequency", "archived_at"]),
1877
- }),
1878
- list({
1879
- name: "list_check_in_event_periods",
1880
- description: "List one Check-Ins event's dated occurrences with regular, guest, and volunteer counts — the cheapest attendance answer. Newest first.",
1881
- key: "periods",
1882
- item: record({
1883
- id: STRING,
1884
- starts_at: STRING,
1885
- ends_at: NULLABLE_STRING,
1886
- regular_count: INTEGER,
1887
- guest_count: INTEGER,
1888
- volunteer_count: INTEGER,
1889
- unique_total_count: NULLABLE_INTEGER,
1890
- note: NULLABLE_STRING,
1891
- }),
1892
- properties: {
1893
- eventId: idProperty("Check-Ins event id from list_check_in_events."),
1894
- startsAfter: dateProperty("Only periods starting at or after this time."),
1895
- startsBefore: dateProperty("Only periods starting before this time."),
1896
- },
1897
- required: ["eventId"],
1898
- path: (args) => `/check-ins/v2/events/${segment(args["eventId"])}/event_periods`,
1899
- query: (args) => ({ ...range(["starts_at"], args["startsAfter"], args["startsBefore"]), order: "-starts_at" }),
1900
- project: (row) => pick(row, [
1901
- "starts_at",
1902
- "ends_at",
1903
- "regular_count",
1904
- "guest_count",
1905
- "volunteer_count",
1906
- "unique_total_count",
1907
- "note",
1908
- ]),
1909
- }),
1910
- list({
1911
- name: "list_check_ins",
1912
- description: "List individual check-ins, scoped by service date through the event period. Read-only: the API cannot check anyone in or out.",
1913
- key: "checkIns",
1914
- item: record({
1915
- id: STRING,
1916
- first_name: STRING,
1917
- last_name: STRING,
1918
- kind: STRING,
1919
- number: NULLABLE_INTEGER,
1920
- security_code: NULLABLE_STRING,
1921
- created_at: STRING,
1922
- checked_out_at: NULLABLE_STRING,
1923
- one_time_guest: BOOLEAN,
1924
- person_id: NULLABLE_STRING,
1925
- event_period_id: NULLABLE_STRING,
1926
- }),
1927
- properties: {
1928
- eventId: idProperty("Only this Check-Ins event. Omit for every event."),
1929
- serviceAfter: dateProperty("Only check-ins whose event period starts at or after this time."),
1930
- serviceBefore: dateProperty("Only check-ins whose event period starts before this time."),
1931
- kind: { type: "string", enum: ["regular", "guest", "volunteer"], description: "Only this kind of check-in." },
1932
- order: orderProperty(["created_at", "last_name", "first_name"]),
1933
- },
1934
- path: (args) => args["eventId"] ? `/check-ins/v2/events/${segment(args["eventId"])}/check_ins` : "/check-ins/v2/check_ins",
1935
- query: (args) => ({
1936
- ...range(["event_period", "starts_at"], args["serviceAfter"], args["serviceBefore"]),
1937
- filter: args["kind"],
1938
- order: args["order"],
1939
- }),
1940
- project: (row) => ({
1941
- ...pick(row, [
1942
- "first_name",
1943
- "last_name",
1944
- "kind",
1945
- "number",
1946
- "security_code",
1947
- "created_at",
1948
- "checked_out_at",
1949
- "one_time_guest",
1950
- ]),
1951
- person_id: relId(row, "person") ?? null,
1952
- event_period_id: relId(row, "event_period") ?? null,
1953
- }),
1954
- }),
1955
- ];
1956
- // --- Calendar (events read-only) ------------------------------------------
1957
- const calendar = [
1958
- list({
1959
- name: "list_calendar_event_instances",
1960
- description: "List what is on the Calendar in a date range: each occurrence with times and location, recurrence expanded. Events cannot be written through the API.",
1961
- key: "instances",
1962
- item: record({
1963
- id: STRING,
1964
- name: NULLABLE_STRING,
1965
- starts_at: STRING,
1966
- ends_at: STRING,
1967
- all_day_event: BOOLEAN,
1968
- location: NULLABLE_STRING,
1969
- kind: NULLABLE_STRING,
1970
- recurrence_description: NULLABLE_STRING,
1971
- event_id: NULLABLE_STRING,
1972
- church_center_url: NULLABLE_STRING,
1973
- }),
1974
- properties: {
1975
- startsAfter: dateProperty("Only occurrences starting at or after this time."),
1976
- startsBefore: dateProperty("Only occurrences starting before this time."),
1977
- eventName: { type: "string", minLength: 1, description: "Exact event name; end it with % for a prefix match." },
1978
- },
1979
- path: () => "/calendar/v2/event_instances",
1980
- query: (args) => ({
1981
- ...range(["starts_at"], args["startsAfter"], args["startsBefore"]),
1982
- "where[event_name]": args["eventName"],
1983
- order: "starts_at",
1984
- }),
1985
- project: (row) => ({
1986
- ...pick(row, [
1987
- "name",
1988
- "starts_at",
1989
- "ends_at",
1990
- "all_day_event",
1991
- "location",
1992
- "kind",
1993
- "recurrence_description",
1994
- "church_center_url",
1995
- ]),
1996
- event_id: relId(row, "event") ?? null,
1997
- }),
1998
- }),
1999
- ];
2000
- // --- Registrations (read-only product) ------------------------------------
2001
- const registrations = [
2002
- list({
2003
- name: "list_signups",
2004
- description: "List Registrations signups (registration events) with open, close, and capacity state. Read-only: the API cannot register anyone.",
2005
- key: "signups",
2006
- item: record({
2007
- id: STRING,
2008
- name: STRING,
2009
- open: BOOLEAN,
2010
- closed: BOOLEAN,
2011
- open_at: NULLABLE_STRING,
2012
- close_at: NULLABLE_STRING,
2013
- maximum_capacity: NULLABLE_INTEGER,
2014
- at_maximum_capacity: BOOLEAN,
2015
- archived: BOOLEAN,
2016
- new_registration_url: NULLABLE_STRING,
2017
- }),
2018
- properties: {
2019
- archived: {
2020
- type: "string",
2021
- enum: ["unarchived", "archived", "all"],
2022
- description: "unarchived (default), archived, or all.",
2023
- },
2024
- },
2025
- path: () => "/registrations/v2/signups",
2026
- query: (args) => ({ filter: args["archived"] === "all" ? undefined : (args["archived"] ?? "unarchived") }),
2027
- project: (row) => pick(row, [
2028
- "name",
2029
- "open",
2030
- "closed",
2031
- "open_at",
2032
- "close_at",
2033
- "maximum_capacity",
2034
- "at_maximum_capacity",
2035
- "archived",
2036
- "new_registration_url",
2037
- ]),
2038
- }),
2039
- list({
2040
- name: "list_signup_attendees",
2041
- description: "List one signup's attendees with active, waitlisted, and canceled state. person_id joins them to People.",
2042
- key: "attendees",
2043
- item: record({
2044
- id: STRING,
2045
- name: NULLABLE_STRING,
2046
- active: BOOLEAN,
2047
- waitlisted: BOOLEAN,
2048
- canceled: BOOLEAN,
2049
- complete: BOOLEAN,
2050
- created_at: STRING,
2051
- person_id: NULLABLE_STRING,
2052
- }),
2053
- properties: {
2054
- signupId: idProperty("Signup id from list_signups."),
2055
- status: {
2056
- type: "string",
2057
- enum: ["active", "waitlist", "canceled"],
2058
- description: "Only attendees in this state.",
2059
- },
2060
- },
2061
- required: ["signupId"],
2062
- path: (args) => `/registrations/v2/signups/${segment(args["signupId"])}/attendees`,
2063
- query: (args) => ({ filter: args["status"] }),
2064
- include: "person",
2065
- project: (row) => ({
2066
- ...pick(row, ["name", "active", "waitlisted", "canceled", "complete", "created_at"]),
2067
- person_id: relId(row, "person") ?? null,
2068
- }),
2069
- }),
2070
- ];
2071
- // --- Giving (reads; money moves only through pco_api_mutate) ---------------
2072
- const giving = [
2073
- list({
2074
- name: "list_donations",
2075
- description: "List Giving donations with their fund designations, newest first, by date, fund, or donor. Amounts are integer cents. Read-only.",
2076
- key: "donations",
2077
- item: DONATION_SCHEMA,
2078
- properties: {
2079
- personId: idProperty("Only this donor's donations."),
2080
- receivedAfter: dateProperty("Only donations received at or after this time."),
2081
- receivedBefore: dateProperty("Only donations received before this time."),
2082
- fundId: idProperty("Only donations designated to this fund (list_funds)."),
2083
- succeededOnly: { type: "boolean", description: "Exclude pending and failed card or ACH payments." },
2084
- order: orderProperty(["received_at", "created_at", "updated_at"]),
2085
- },
2086
- path: (args) => args["personId"] ? `/giving/v2/people/${segment(args["personId"])}/donations` : "/giving/v2/donations",
2087
- query: (args) => ({
2088
- ...range(["received_at"], args["receivedAfter"], args["receivedBefore"]),
2089
- "where[fund_id]": args["fundId"],
2090
- filter: args["succeededOnly"] === true ? "succeeded" : undefined,
2091
- order: args["order"] ?? "-received_at",
2092
- }),
2093
- include: "designations,designations.fund",
2094
- project: donationRecord,
2095
- }),
2096
- list({
2097
- name: "list_funds",
2098
- description: "List Giving funds with ledger codes and visibility. Supplies fundId for donation filters.",
2099
- key: "funds",
2100
- item: record({
2101
- id: STRING,
2102
- name: STRING,
2103
- ledger_code: NULLABLE_STRING,
2104
- visibility: STRING,
2105
- default: BOOLEAN,
2106
- description: NULLABLE_STRING,
2107
- }),
2108
- path: () => "/giving/v2/funds",
2109
- project: (row) => pick(row, ["name", "ledger_code", "visibility", "default", "description"]),
2110
- }),
2111
- list({
2112
- name: "list_batches",
2113
- description: "List Giving batches with totals and commit state, most recently updated first. Committing a batch is pco_api_mutate.",
2114
- key: "batches",
2115
- item: record({
2116
- id: STRING,
2117
- description: NULLABLE_STRING,
2118
- status: STRING,
2119
- donations_count: INTEGER,
2120
- total_cents: INTEGER,
2121
- total_currency: STRING,
2122
- committed_at: NULLABLE_STRING,
2123
- created_at: STRING,
2124
- batch_group_id: NULLABLE_STRING,
2125
- }),
2126
- properties: {
2127
- status: {
2128
- type: "string",
2129
- enum: ["committed", "in_progress"],
2130
- description: "Only committed or in-progress batches.",
2131
- },
2132
- },
2133
- path: () => "/giving/v2/batches",
2134
- query: (args) => ({ filter: args["status"], order: "-updated_at" }),
2135
- project: (row) => ({
2136
- ...pick(row, [
2137
- "description",
2138
- "status",
2139
- "donations_count",
2140
- "total_cents",
2141
- "total_currency",
2142
- "committed_at",
2143
- "created_at",
2144
- ]),
2145
- batch_group_id: relId(row, "batch_group") ?? null,
2146
- }),
2147
- }),
2148
- list({
2149
- name: "list_pledge_campaigns",
2150
- description: "List Giving pledge campaigns with goal and received totals in cents. Supplies pledgeCampaignId for list_pledges.",
2151
- key: "campaigns",
2152
- item: record({
2153
- id: STRING,
2154
- name: STRING,
2155
- starts_at: NULLABLE_STRING,
2156
- ends_at: NULLABLE_STRING,
2157
- goal_cents: NULLABLE_INTEGER,
2158
- received_total_from_pledges_cents: INTEGER,
2159
- received_total_outside_of_pledges_cents: INTEGER,
2160
- fund_id: NULLABLE_STRING,
2161
- }),
2162
- path: () => "/giving/v2/pledge_campaigns",
2163
- query: () => ({ order: "-starts_at" }),
2164
- project: (row) => ({
2165
- ...pick(row, [
2166
- "name",
2167
- "starts_at",
2168
- "ends_at",
2169
- "goal_cents",
2170
- "received_total_from_pledges_cents",
2171
- "received_total_outside_of_pledges_cents",
2172
- ]),
2173
- fund_id: relId(row, "fund") ?? null,
2174
- }),
2175
- }),
2176
- list({
2177
- name: "list_pledges",
2178
- description: "List the pledges in one Giving pledge campaign with pledged and donated totals in cents per donor.",
2179
- key: "pledges",
2180
- item: record({
2181
- id: STRING,
2182
- amount_cents: INTEGER,
2183
- donated_total_cents: INTEGER,
2184
- joint_giver_amount_cents: NULLABLE_INTEGER,
2185
- person_id: NULLABLE_STRING,
2186
- created_at: STRING,
2187
- }),
2188
- properties: { pledgeCampaignId: idProperty("Pledge campaign id from list_pledge_campaigns.") },
2189
- required: ["pledgeCampaignId"],
2190
- path: (args) => `/giving/v2/pledge_campaigns/${segment(args["pledgeCampaignId"])}/pledges`,
2191
- project: (row) => ({
2192
- ...pick(row, ["amount_cents", "donated_total_cents", "joint_giver_amount_cents", "created_at"]),
2193
- person_id: relId(row, "person") ?? null,
2194
- }),
2195
- }),
2196
- list({
2197
- name: "list_recurring_donations",
2198
- description: "List Giving recurring donations with schedule, status, next occurrence, and fund designations. Read-only; amounts in cents.",
2199
- key: "recurringDonations",
2200
- item: record({
2201
- id: STRING,
2202
- amount_cents: INTEGER,
2203
- amount_currency: STRING,
2204
- schedule: NULLABLE_STRING,
2205
- status: STRING,
2206
- next_occurrence: NULLABLE_STRING,
2207
- last_donation_received_at: NULLABLE_STRING,
2208
- person_id: NULLABLE_STRING,
2209
- designations: DESIGNATIONS,
2210
- }),
2211
- properties: {
2212
- status: { type: "string", minLength: 1, description: "Status as Planning Center reports it, e.g. active." },
2213
- },
2214
- path: () => "/giving/v2/recurring_donations",
2215
- query: (args) => ({ "where[status]": args["status"] }),
2216
- include: "designations,designations.fund",
2217
- project: (row, index) => ({
2218
- ...pick(row, [
2219
- "amount_cents",
2220
- "amount_currency",
2221
- "schedule",
2222
- "status",
2223
- "next_occurrence",
2224
- "last_donation_received_at",
2225
- ]),
2226
- person_id: relId(row, "person") ?? null,
2227
- designations: designations(row, index),
2228
- }),
2229
- }),
2230
- ];
2231
- // --- Webhooks -------------------------------------------------------------
2232
- const webhooks = [
2233
- {
2234
- name: "list_webhook_subscriptions",
2235
- description: "List the organization's webhook subscriptions: event name, URL, and active state. Never returns the signing secret; that needs pco_api_get.",
2236
- annotations: READ,
2237
- inputSchema: input({ perPage: perPageProperty(defaultPageSize), offset: OFFSET_PROPERTY }),
2238
- outputSchema: listOutput("subscriptions", record({
2239
- id: STRING,
2240
- name: STRING,
2241
- url: STRING,
2242
- active: BOOLEAN,
2243
- application_id: NULLABLE_STRING,
2244
- created_at: STRING,
2245
- })),
2246
- handler: async (args, ctx) => {
2247
- const document = asRecord(await call({
2248
- method: "GET",
2249
- path: "/webhooks/v2/webhook_subscriptions",
2250
- query: { per_page: perPage(args), offset: args["offset"] },
2251
- }, ctx));
2252
- // `authenticity_secret` verifies deliveries. A projection is the
2253
- // place it never appears, so this read has no `raw` mode.
2254
- return {
2255
- subscriptions: resources(document["data"]).map((row) => pick(row, ["name", "url", "active", "application_id", "created_at"])),
2256
- page: pageOf(document),
2257
- };
2258
- },
2259
- },
2260
- ];
2261
- return {
2262
- hatches,
2263
- apps: {
2264
- people,
2265
- services,
2266
- groups,
2267
- "check-ins": checkIns,
2268
- calendar,
2269
- registrations,
2270
- giving,
2271
- publishing: [],
2272
- webhooks,
2273
- api: [],
2274
- },
2275
- };
2276
- }
2277
- // ---------------------------------------------------------------------------
2278
- // Guide and construction
2279
- // ---------------------------------------------------------------------------
2280
- /** `a, b, and c`, the way the guide and description list products. */
2281
- function series(items) {
2282
- return items.length < 3 ? items.join(" and ") : `${items.slice(0, -1).join(", ")}, and ${items.at(-1)}`;
2283
- }
2284
- /** The products the description names: those with named tools for staff work. */
2285
- const DESCRIBED_APPS = [
2286
- "people",
2287
- "services",
2288
- "groups",
2289
- "check-ins",
2290
- "calendar",
2291
- "registrations",
2292
- "giving",
2293
- ];
2294
- /** What a person id joins in each other product, for the guide's identity rule. */
2295
- const PERSON_JOINS = [
2296
- ["services", "Services schedules"],
2297
- ["groups", "Groups memberships"],
2298
- ["giving", "Giving donations"],
2299
- ["check-ins", "Check-Ins"],
2300
- ];
2301
- /**
2302
- * The maintained guide for the products this connection reaches. Every
2303
- * product-specific line is its own fragment, so an excluded product is
2304
- * neither described, pinned, nor named by a tool the guide points to, and an
2305
- * exclusion adds the line stating what the hatches refuse. With nothing
2306
- * excluded, the text is the whole guide as before; the `published` fragment
2307
- * is that same text without a purpose.
2308
- */
2309
- function usageGuide(purpose, instructions, excluded) {
2310
- const fragments = skill.fragments;
2311
- const has = (app) => !excluded.has(app);
2312
- const pair = (a, b, both, onlyA, onlyB) => has(a) && has(b) ? both : has(a) ? onlyA : has(b) ? onlyB : "";
2313
- const apps = PLANNING_CENTER_APPS.filter(has);
2314
- const joins = PERSON_JOINS.filter(([app]) => has(app)).map(([, label]) => label);
2315
- const organizationInstructions = instructions?.trim();
2316
- return [
2317
- fragments.heading,
2318
- apps
2319
- .filter((app) => app !== "api")
2320
- .map((app) => APP_TITLES[app])
2321
- .join(", "),
2322
- fragments.purpose,
2323
- purpose,
2324
- fragments.identity,
2325
- joins.length > 0 ? series(joins) : "every People record",
2326
- fragments.conventions,
2327
- has("services") ? fragments.plans : "",
2328
- fragments.writes,
2329
- excluded.size === 0 ? fragments.people : fragments.people_without_lists,
2330
- has("services") ? fragments.services : "",
2331
- has("groups") ? fragments.groups : "",
2332
- pair("check-ins", "registrations", fragments.check_ins_registrations, fragments.check_ins, fragments.registrations),
2333
- has("calendar") ? fragments.calendar : "",
2334
- has("giving") ? fragments.giving : "",
2335
- pair("publishing", "api", fragments.publishing_api, fragments.publishing, fragments.api),
2336
- fragments.hatches,
2337
- apps.map((app) => `${app} ${PLANNING_CENTER_API_VERSIONS[app]}`).join(", "),
2338
- fragments.pins,
2339
- excluded.size === 0 ? "" : fragments.disabled,
2340
- fragments.limits,
2341
- organizationInstructions ? `\n## ${skill.instructionsHeading}\n\n${organizationInstructions}\n` : "",
2342
- ].join("");
2343
- }
2344
- /**
2345
- * `excludeApps` as a set, refusing at construction what cannot be meant
2346
- * (INV-11): a product Planning Center does not have, a misspelling, or People.
2347
- */
2348
- function excludedApps(value) {
2349
- const excluded = new Set();
2350
- if (value === undefined)
2351
- return excluded;
2352
- const excludable = PLANNING_CENTER_APPS.filter((app) => app !== "people").join(", ");
2353
- if (!Array.isArray(value)) {
2354
- throw new Error(`planningCenter() excludeApps must be an array of app names from: ${excludable}.`);
2355
- }
2356
- for (const entry of value) {
2357
- if (entry === "people") {
2358
- throw new Error('planningCenter() excludeApps cannot exclude "people": every product joins through People person ids, and the credential test reads /people/v2/me.');
2359
- }
2360
- if (typeof entry !== "string" || !isApp(entry)) {
2361
- const lower = typeof entry === "string" ? entry.trim().toLowerCase() : "";
2362
- throw new Error(`planningCenter() excludeApps has unknown app ${JSON.stringify(entry)}; expected any of ${excludable}.${isApp(lower) && lower !== "people" ? ` App names are lowercase path segments: "${lower}".` : ""}`);
2363
- }
2364
- excluded.add(entry);
2365
- }
2366
- return excluded;
2367
- }
4
+ import { defineApiProvider } from "../../http/provider.js";
5
+ import { PlanningCenterApi, PLANNING_CENTER_APPS, PLANNING_CENTER_API_VERSIONS, PLANNING_CENTER_API_BASE_URL, MAX_PAGE_SIZE, DEFAULT_PAGE_SIZE, PLANNING_CENTER_MAX_RESPONSE_BYTES, PLANNING_CENTER_ADMISSION, CREDENTIAL, authenticate, errors, safety, transport, probe, APP_TITLES, PCO_PREFIX, PCO_INDEX, PCO_VERDICTS, PCO_NAMED, PCO_PAGES, pcoBinding, } from "./api.js";
6
+ import { planningCenterTools } from "./tools.js";
7
+ import { series, DESCRIBED_APPS, excludedApps } from "./lib/guide.js";
8
+ export { PLANNING_CENTER_API_BASE_URL, PLANNING_CENTER_APPS, PLANNING_CENTER_API_VERSIONS } from "./lib/constants.js";
2368
9
  /** The closed options planningCenter() accepts; see `assertKnownOptions`. */
2369
10
  const PLANNING_CENTER_OPTIONS = optionsOf()({
2370
11
  ...PROVIDER_COMMON,
2371
12
  ...keys("userAgent", "defaultPageSize", "excludeApps", "baseUrl"),
2372
13
  });
2373
- /** A maintained Planning Center Online connection over the REST API. */
2374
- export const planningCenter = asProviderFactory({
14
+ /** Planning Center over the reviewed product pins and standard agent surface. */
15
+ export const planningCenter = defineApiProvider({
2375
16
  name: "planning-center",
2376
17
  title: "Planning Center",
2377
- kind: "api",
2378
18
  readme: "Planning Center",
2379
19
  bundle: {
2380
- "baselineGzip": 26970,
2381
- "maxGzip": 86970,
2382
- "note": "./providers/planning-center starts at 26,970 B gzip. It is api() over the guarded transport with no hosted-MCP surface, so neither the MCP client nor the Effect core is in its graph; the cap uses the existing baseline + 60,000 B policy.",
20
+ baselineGzip: 635569,
21
+ note: "./providers/planning-center initial step-2 baseline is 635,569 B gzip, versus 349,553 B on main 8e92eed9 (+286,016 B), Node 22.23.3 with system zlib. The ten products now index 1,752 operations after omitting one empty-path artifact. Review attributes about 263 KB gzip to the index, mostly repeated where[] rows; lossless shared compaction is deferred. Reviewed safety and standard discovery remain. The shared Effect kit is paid once per deployment; provider entries are reported, not capped, and sizes are not additive (#899).",
2383
22
  },
2384
23
  skill,
2385
24
  options: PLANNING_CENTER_OPTIONS,
2386
- create: planningCenterConnector,
2387
- });
2388
- function planningCenterConnector(id, options) {
2389
- const purpose = options.purpose.trim();
2390
- if (!purpose) {
2391
- throw new Error("planningCenter() requires a non-empty organization purpose.");
2392
- }
2393
- const defaultPageSize = options.defaultPageSize ?? DEFAULT_PAGE_SIZE;
2394
- if (!Number.isInteger(defaultPageSize) || defaultPageSize < 1 || defaultPageSize > MAX_PAGE_SIZE) {
2395
- throw new Error(`planningCenter() defaultPageSize must be a whole number between 1 and ${MAX_PAGE_SIZE}.`);
2396
- }
2397
- const userAgent = options.userAgent?.trim() ?? DEFAULT_USER_AGENT;
2398
- if (!userAgent || /[\r\n]/.test(userAgent)) {
2399
- throw new Error("planningCenter() userAgent must be a non-empty single line.");
2400
- }
2401
- const excluded = excludedApps(options.excludeApps);
2402
- const auth = PERSONAL_ACCESS_TOKEN;
2403
- const baseUrl = options.baseUrl?.trim() || PLANNING_CENTER_API_BASE_URL;
2404
- const send = guardedFetch({
2405
- provider: "Planning Center",
2406
- baseUrl,
2407
- headers: { Accept: "application/json", "User-Agent": userAgent },
2408
- maxResponseBytes: PLANNING_CENTER_MAX_RESPONSE_BYTES,
2409
- authenticate: (ctx) => auth.headers(ctx),
2410
- ...(excluded.size > 0
2411
- ? { admit: (url, request) => admit(baseUrl, url, request, excluded) }
2412
- : {}),
2413
- });
2414
- const call = planningCenterCall(send, PLANNING_CENTER_API_VERSIONS, excluded);
2415
- const surface = tools(call, defaultPageSize, excluded);
2416
- return api(id, {
2417
- ...(options.authScope ? { authScope: options.authScope } : {}),
2418
- title: options.title ?? "Planning Center",
2419
- description: `Planning Center church management — ${series(DESCRIBED_APPS.filter((app) => !excluded.has(app)).map((app) => APP_TITLES[app]))}: ${purpose}`,
2420
- credential: auth.credential,
2421
- async testCredentials(values, ctx) {
2422
- try {
2423
- const document = asRecord(await call({ method: "GET", path: "/people/v2/me", query: { include: "organization" } }, {
2424
- ...ctx,
2425
- credential: {
2426
- get: async (field) => (field ? (values[field] ?? null) : null),
2427
- getAll: async () => values,
2428
- },
2429
- }));
2430
- const me = created(document, "person record");
2431
- const organization = resources(document["included"]).find((row) => row.type === "Organization");
2432
- const name = attribute(me, "name") ?? `person ${me.id}`;
2433
- const where = attribute(organization, "name");
2434
- return {
2435
- ok: true,
2436
- message: `Authenticated as ${String(name)} (person ${me.id})${where ? ` in ${String(where)}` : ""}.`,
2437
- };
2438
- }
2439
- catch (error) {
2440
- return {
2441
- ok: false,
2442
- message: error instanceof ConnectorCallError ? error.message : "Planning Center rejected the token.",
2443
- };
2444
- }
2445
- },
2446
- callAdmission: options.callAdmission === undefined ? PLANNING_CENTER_ADMISSION : options.callAdmission,
2447
- usageGuide: {
2448
- content: usageGuide(purpose, options.instructions, excluded),
2449
- summary: "Person ids join every product, offset paging, per-product write limits, JSON:API hatches, pinned versions.",
2450
- // Deliberately not `required`. Every named tool's schema is complete
2451
- // enough to call it on its own; the guide carries the cross-product
2452
- // facts (merges, write limits, hatch framing) an agent needs only when
2453
- // it leaves the named surface or hits one of them.
2454
- },
2455
- ...(options.maxResultBytes !== undefined ? { maxResultBytes: options.maxResultBytes } : {}),
2456
- tools: [
2457
- ...surface.hatches,
2458
- ...PLANNING_CENTER_APPS.flatMap((app) => (excluded.has(app) ? [] : surface.apps[app])).filter((tool) => excluded.size === 0 || !LIST_TOOLS.has(tool.name)),
25
+ description: (options) => `Planning Center church management — ${series(DESCRIBED_APPS.filter((app) => !excludedApps(options.excludeApps).has(app)).map((app) => APP_TITLES[app]))}: ${options.purpose.trim()}`,
26
+ vendor: PlanningCenterApi,
27
+ baseUrl: (options) => options.baseUrl?.trim() || PLANNING_CENTER_API_BASE_URL,
28
+ maxResponseBytes: PLANNING_CENTER_MAX_RESPONSE_BYTES,
29
+ pageSize: { default: DEFAULT_PAGE_SIZE, max: MAX_PAGE_SIZE },
30
+ defaultCallAdmission: PLANNING_CENTER_ADMISSION,
31
+ transport,
32
+ idempotencyKey: false,
33
+ credential: CREDENTIAL,
34
+ authenticate,
35
+ errors,
36
+ safety: () => safety,
37
+ probe,
38
+ blocks: {
39
+ keys: [...PLANNING_CENTER_APPS, "lists", "limited"],
40
+ select: (options) => [
41
+ ...PLANNING_CENTER_APPS.filter((app) => !excludedApps(options.excludeApps).has(app)),
42
+ excludedApps(options.excludeApps).size === 0 ? "lists" : "limited",
2459
43
  ],
2460
- });
2461
- }
44
+ },
45
+ guide: (options) => ({
46
+ heading: "Planning Center usage",
47
+ summary: "People ids join reachable products; use their local dates, units, roles and documented status values.",
48
+ required: true,
49
+ context: [
50
+ `Purpose: ${options.purpose.trim()}`,
51
+ `Reachable products: ${PLANNING_CENTER_APPS.filter((app) => app !== "api" && !excludedApps(options.excludeApps).has(app))
52
+ .map((app) => APP_TITLES[app])
53
+ .join(", ")}.`,
54
+ `API pins: ${PLANNING_CENTER_APPS.filter((app) => !excludedApps(options.excludeApps).has(app))
55
+ .map((app) => `${app} ${PLANNING_CENTER_API_VERSIONS[app]}`)
56
+ .join(", ")}.`,
57
+ ...(options.readOnly === true ? ["This connection is read-only; writes are refused."] : []),
58
+ ...(options.defaultPageSize === undefined
59
+ ? []
60
+ : [`This connection's default list limit is ${options.defaultPageSize}.`]),
61
+ "Stored credentials and private calendar access codes are permanently withheld.",
62
+ ],
63
+ }),
64
+ hatch: {
65
+ name: PCO_PREFIX,
66
+ title: "Planning Center",
67
+ index: PCO_INDEX,
68
+ verdicts: PCO_VERDICTS,
69
+ named: PCO_NAMED,
70
+ pages: PCO_PAGES,
71
+ binding: (kit) => {
72
+ const available = new Set(planningCenterTools({ ...kit, pageSize: kit.options.defaultPageSize ?? DEFAULT_PAGE_SIZE }).map((tool) => tool.name));
73
+ return pcoBinding({
74
+ ...kit,
75
+ named: Object.fromEntries(Object.entries(PCO_NAMED).filter(([, tool]) => available.has(tool))),
76
+ });
77
+ },
78
+ pathExample: "/people/v2/people/123",
79
+ notes: () => ({
80
+ read: "JSON:API records retain data, included and meta; named reads resolve common joins.",
81
+ write: "Bodies are JSON:API documents, not flat attributes.",
82
+ }),
83
+ },
84
+ tools: planningCenterTools,
85
+ });