@zackbart/connecta 0.31.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +97 -0
- package/README.md +30 -20
- package/bin/connecta.mjs +25 -6
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.d.ts +167 -23
- package/dist/access-tokens.js +676 -140
- package/dist/agent-identity.js +47 -0
- package/dist/auth/cloudflare-access.js +13 -5
- package/dist/auth-elicitation.js +1 -1
- package/dist/call-admission-context.js +15 -0
- package/dist/call-admission.js +30 -43
- package/dist/call-recovery.js +6 -2
- package/dist/catalog-service.js +7 -2
- package/dist/config-value-sources.js +14 -9
- package/dist/config.d.ts +58 -78
- package/dist/config.js +143 -47
- package/dist/connector-access.d.ts +4 -33
- package/dist/connector-access.js +15 -99
- package/dist/connector-caller.d.ts +7 -1
- package/dist/connector-caller.js +3 -2
- package/dist/connectors/api-connector.d.ts +27 -4
- package/dist/connectors/api-connector.js +2 -0
- package/dist/connectors/guarded-fetch.d.ts +9 -0
- package/dist/connectors/guarded-fetch.js +7 -0
- package/dist/connectors/remote-mcp.js +2 -0
- package/dist/describe-config.d.ts +32 -7
- package/dist/describe-config.js +9 -18
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -0
- package/dist/execute.js +16 -14
- package/dist/index.d.ts +4 -3
- package/dist/index.js +30 -10
- package/dist/invocation.js +12 -7
- package/dist/membership.d.ts +51 -0
- package/dist/membership.js +121 -0
- package/dist/meta-tools.js +17 -19
- package/dist/module-contracts.d.ts +23 -6
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +2 -2
- package/dist/operator-ui/agents.d.ts +74 -0
- package/dist/operator-ui/agents.js +104 -0
- package/dist/operator-ui/contract.d.ts +151 -29
- package/dist/operator-ui/endpoints.js +192 -0
- package/dist/operator-ui/fix-prompts.js +4 -4
- package/dist/operator-ui/generated.js +5 -4
- package/dist/operator-ui/member-page.js +112 -0
- package/dist/operator-ui/model.d.ts +15 -7
- package/dist/operator-ui/view.d.ts +84 -19
- package/dist/operator-ui/view.js +83 -21
- package/dist/page-styles.js +1 -1
- package/dist/program-writes.js +2 -2
- package/dist/provider.d.ts +63 -11
- package/dist/provider.js +46 -7
- package/dist/providers/_shared/google/workspace.d.ts +53 -16
- package/dist/providers/_shared/google/workspace.js +31 -9
- package/dist/providers/_shared/rest/tools.js +2 -2
- package/dist/providers/_shared/rest/value-safety.d.ts +50 -0
- package/dist/providers/_shared/rest/value-safety.js +699 -7
- package/dist/providers/breeze/skill.generated.js +2 -2
- package/dist/providers/ccb/index.js +1 -1
- package/dist/providers/cloudflare/index.js +3 -3
- package/dist/providers/cloudflare/rest.js +25 -3
- package/dist/providers/cloudflare/skill.generated.js +2 -2
- package/dist/providers/cloudflare/value-safety.js +32 -0
- package/dist/providers/docs/index.d.ts +1 -1
- package/dist/providers/docs/index.js +5 -5
- package/dist/providers/docs/skill.generated.js +2 -2
- package/dist/providers/drive/index.d.ts +1 -1
- package/dist/providers/drive/index.js +4 -4
- package/dist/providers/drive/skill.generated.js +2 -2
- package/dist/providers/forms/index.d.ts +1 -1
- package/dist/providers/forms/index.js +5 -5
- package/dist/providers/forms/skill.generated.js +2 -2
- package/dist/providers/github/skill.generated.js +2 -2
- package/dist/providers/gmail/index.d.ts +1 -1
- package/dist/providers/gmail/index.js +279 -26
- package/dist/providers/gmail/skill.generated.js +2 -2
- package/dist/providers/linear/index.js +1 -1
- package/dist/providers/mixpanel/index.d.ts +27 -12
- package/dist/providers/mixpanel/index.js +58 -6
- package/dist/providers/mixpanel/operations.js +20 -0
- package/dist/providers/mixpanel/rest.js +603 -0
- package/dist/providers/mixpanel/skill.generated.js +3 -2
- package/dist/providers/mixpanel/value-safety.js +16 -0
- package/dist/providers/notion/index.d.ts +7 -0
- package/dist/providers/notion/index.js +116 -11
- package/dist/providers/notion/rest.js +183 -24
- package/dist/providers/notion/skill.generated.js +4 -4
- package/dist/providers/notion/value-safety.js +30 -5
- package/dist/providers/overflow/index.js +1 -1
- package/dist/providers/planning-center/index.d.ts +27 -0
- package/dist/providers/planning-center/index.js +287 -25
- package/dist/providers/planning-center/skill.generated.js +24 -4
- package/dist/providers/sheets/index.d.ts +4 -1
- package/dist/providers/sheets/index.js +9 -6
- package/dist/providers/sheets/skill.generated.js +2 -2
- package/dist/providers/slides/index.d.ts +1 -1
- package/dist/providers/slides/index.js +6 -6
- package/dist/providers/slides/skill.generated.js +2 -2
- package/dist/providers/stripe/rest.js +9 -2
- package/dist/providers/stripe/skill.generated.js +2 -2
- package/dist/providers/stripe/value-safety.js +22 -0
- package/dist/providers/vercel/index.js +2 -2
- package/dist/providers/vercel/rest.js +20 -2
- package/dist/providers/vercel/skill.generated.js +2 -2
- package/dist/providers/vercel/value-safety.js +22 -0
- package/dist/providers/wispr-flow/index.d.ts +14 -0
- package/dist/providers/wispr-flow/index.js +117 -0
- package/dist/providers/wispr-flow/skill.generated.js +8 -0
- package/dist/registry.d.ts +2 -3
- package/dist/registry.js +8 -9
- package/dist/routes/access-tokens.js +82 -75
- package/dist/routes/activity-disclosure.js +17 -18
- package/dist/routes/activity.js +34 -54
- package/dist/routes/actor-labels.js +33 -4
- package/dist/routes/agent-path.js +43 -0
- package/dist/routes/connect.js +16 -12
- package/dist/routes/credentials.js +16 -40
- package/dist/routes/mcp.js +155 -85
- package/dist/routes/oauth-management.js +11 -5
- package/dist/routes/oauth.js +3 -9
- package/dist/routes/operator.js +25 -17
- package/dist/routes/shared.d.ts +60 -20
- package/dist/routes/shared.js +187 -96
- package/dist/routes/ui-config.js +437 -97
- package/dist/routes/ui-data.js +2 -2
- package/dist/routes/ui.js +9 -16
- package/dist/runtime/call-admission.js +9 -0
- package/dist/server.js +8 -0
- package/dist/skills.js +5 -4
- package/dist/storage/keys.js +9 -1
- package/dist/tool-safety.js +13 -5
- package/dist/types.d.ts +12 -1
- package/dist/ui.d.ts +2 -2
- package/dist/ui.js +17 -15
- package/dist/usage-guide.js +3 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/workspace-address.js +9 -0
- package/dist/workspace-subject.js +23 -0
- package/documentation/README.md +46 -22
- package/documentation/architecture.md +378 -58
- package/documentation/auth.md +806 -215
- package/documentation/code-mode.md +93 -81
- package/documentation/deploying.md +128 -26
- package/documentation/integrating.md +145 -22
- package/documentation/meta-tools.md +58 -46
- package/documentation/migration-0.33.md +344 -0
- package/documentation/operating.md +14 -9
- package/documentation/operator-tests.md +117 -31
- package/documentation/operator-ui.md +326 -55
- package/documentation/provider-auth-migration.md +42 -27
- package/documentation/provider-migration-0.29.md +3 -3
- package/examples/worker/AGENTS.md +35 -8
- package/examples/worker/README.md +92 -53
- package/examples/worker/src/connecta.config.ts +41 -20
- package/examples/worker/wrangler.jsonc +3 -0
- package/package.json +7 -2
- package/templates/node/.env.example +3 -0
- package/templates/node/AGENTS.md +33 -10
- package/templates/node/Dockerfile +2 -2
- package/templates/node/README.md +71 -44
- package/templates/node/docker-compose.yml +3 -2
- package/templates/node/package.json +4 -3
- package/templates/node/src/adopt-token.ts +23 -0
- package/templates/node/src/connecta.config.ts +33 -12
- package/templates/node/src/rotate-token.ts +17 -0
- package/dist/tool-safety.d.ts +0 -9
- package/templates/node/src/provision-token.ts +0 -17
|
@@ -237,7 +237,7 @@ function isApp(value) {
|
|
|
237
237
|
* normalization the guarded transport applies — so `..` cannot carry a request
|
|
238
238
|
* from `/people/v2` to `/oauth` or anywhere else outside a product.
|
|
239
239
|
*/
|
|
240
|
-
function appForPath(path) {
|
|
240
|
+
function appForPath(path, apps) {
|
|
241
241
|
let normalized;
|
|
242
242
|
if (path.startsWith("/") && !path.startsWith("//")) {
|
|
243
243
|
try {
|
|
@@ -249,10 +249,145 @@ function appForPath(path) {
|
|
|
249
249
|
}
|
|
250
250
|
const app = normalized === undefined ? undefined : APP_PATH.exec(normalized)?.[1];
|
|
251
251
|
if (!app || !isApp(app)) {
|
|
252
|
-
throw new ConnectorCallError("invalid_args", `A Planning Center path begins with /<app>/v2, where app is one of ${
|
|
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
253
|
}
|
|
254
254
|
return app;
|
|
255
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
|
+
}
|
|
256
391
|
function errorDetail(payload, status) {
|
|
257
392
|
const parts = [];
|
|
258
393
|
for (const entry of asArray(asRecord(payload)["errors"])) {
|
|
@@ -309,9 +444,12 @@ function planningCenterFailure(status, headers, payload) {
|
|
|
309
444
|
retryable: false,
|
|
310
445
|
});
|
|
311
446
|
}
|
|
312
|
-
function planningCenterCall(send, versions) {
|
|
447
|
+
function planningCenterCall(send, versions, excluded) {
|
|
448
|
+
const apps = PLANNING_CENTER_APPS.filter((app) => !excluded.has(app));
|
|
313
449
|
return async (request, ctx) => {
|
|
314
|
-
|
|
450
|
+
if (excluded.size > 0)
|
|
451
|
+
refuseExcluded(literalSegments(request.path), [], request, excluded);
|
|
452
|
+
const app = appForPath(request.path, apps);
|
|
315
453
|
return await send(compact({
|
|
316
454
|
method: request.method,
|
|
317
455
|
path: request.path,
|
|
@@ -787,11 +925,11 @@ function personAttributes(args) {
|
|
|
787
925
|
};
|
|
788
926
|
}
|
|
789
927
|
const WORKFLOW_CARD_ACTIONS = ["promote", "go_back", "skip_step", "snooze", "unsnooze", "remove", "restore"];
|
|
790
|
-
function tools(call, defaultPageSize) {
|
|
928
|
+
function tools(call, defaultPageSize, excluded) {
|
|
791
929
|
const perPage = (args) => args["perPage"] ?? defaultPageSize;
|
|
792
930
|
const list = (spec) => listTool(call, defaultPageSize, spec);
|
|
793
|
-
|
|
794
|
-
|
|
931
|
+
// --- Hatches --------------------------------------------------------------
|
|
932
|
+
const hatches = [
|
|
795
933
|
{
|
|
796
934
|
name: "pco_api_get",
|
|
797
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.",
|
|
@@ -813,7 +951,7 @@ function tools(call, defaultPageSize) {
|
|
|
813
951
|
},
|
|
814
952
|
{
|
|
815
953
|
name: "pco_api_mutate",
|
|
816
|
-
description:
|
|
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.`,
|
|
817
955
|
annotations: DESTRUCTIVE,
|
|
818
956
|
inputSchema: input({
|
|
819
957
|
method: {
|
|
@@ -844,7 +982,9 @@ function tools(call, defaultPageSize) {
|
|
|
844
982
|
}), ctx)) ?? null,
|
|
845
983
|
}),
|
|
846
984
|
},
|
|
847
|
-
|
|
985
|
+
];
|
|
986
|
+
// --- People ---------------------------------------------------------------
|
|
987
|
+
const people = [
|
|
848
988
|
{
|
|
849
989
|
name: "get_me",
|
|
850
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.",
|
|
@@ -1292,7 +1432,9 @@ function tools(call, defaultPageSize) {
|
|
|
1292
1432
|
};
|
|
1293
1433
|
},
|
|
1294
1434
|
}),
|
|
1295
|
-
|
|
1435
|
+
];
|
|
1436
|
+
// --- Services -------------------------------------------------------------
|
|
1437
|
+
const services = [
|
|
1296
1438
|
list({
|
|
1297
1439
|
name: "list_service_types",
|
|
1298
1440
|
description: "List Services service types — the recurring services such as Sunday Morning. Supplies serviceTypeId for plans and teams.",
|
|
@@ -1596,7 +1738,9 @@ function tools(call, defaultPageSize) {
|
|
|
1596
1738
|
keys: relatedMany(row, "keys", index).map((key) => pick(key, ["name", "starting_key", "ending_key"])),
|
|
1597
1739
|
}),
|
|
1598
1740
|
}),
|
|
1599
|
-
|
|
1741
|
+
];
|
|
1742
|
+
// --- Groups ---------------------------------------------------------------
|
|
1743
|
+
const groups = [
|
|
1600
1744
|
list({
|
|
1601
1745
|
name: "list_groups",
|
|
1602
1746
|
description: "List Groups groups with type, schedule, and member count. Archived groups are excluded unless asked for; the API cannot create a group.",
|
|
@@ -1712,7 +1856,9 @@ function tools(call, defaultPageSize) {
|
|
|
1712
1856
|
group_id: relId(row, "group") ?? null,
|
|
1713
1857
|
}),
|
|
1714
1858
|
}),
|
|
1715
|
-
|
|
1859
|
+
];
|
|
1860
|
+
// --- Check-Ins (read-only product) -----------------------------------------
|
|
1861
|
+
const checkIns = [
|
|
1716
1862
|
list({
|
|
1717
1863
|
name: "list_check_in_events",
|
|
1718
1864
|
description: "List Check-Ins events, the recurring things people check in to. Supplies eventId for event periods and check-ins.",
|
|
@@ -1806,7 +1952,9 @@ function tools(call, defaultPageSize) {
|
|
|
1806
1952
|
event_period_id: relId(row, "event_period") ?? null,
|
|
1807
1953
|
}),
|
|
1808
1954
|
}),
|
|
1809
|
-
|
|
1955
|
+
];
|
|
1956
|
+
// --- Calendar (events read-only) ------------------------------------------
|
|
1957
|
+
const calendar = [
|
|
1810
1958
|
list({
|
|
1811
1959
|
name: "list_calendar_event_instances",
|
|
1812
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.",
|
|
@@ -1848,7 +1996,9 @@ function tools(call, defaultPageSize) {
|
|
|
1848
1996
|
event_id: relId(row, "event") ?? null,
|
|
1849
1997
|
}),
|
|
1850
1998
|
}),
|
|
1851
|
-
|
|
1999
|
+
];
|
|
2000
|
+
// --- Registrations (read-only product) ------------------------------------
|
|
2001
|
+
const registrations = [
|
|
1852
2002
|
list({
|
|
1853
2003
|
name: "list_signups",
|
|
1854
2004
|
description: "List Registrations signups (registration events) with open, close, and capacity state. Read-only: the API cannot register anyone.",
|
|
@@ -1917,7 +2067,9 @@ function tools(call, defaultPageSize) {
|
|
|
1917
2067
|
person_id: relId(row, "person") ?? null,
|
|
1918
2068
|
}),
|
|
1919
2069
|
}),
|
|
1920
|
-
|
|
2070
|
+
];
|
|
2071
|
+
// --- Giving (reads; money moves only through pco_api_mutate) ---------------
|
|
2072
|
+
const giving = [
|
|
1921
2073
|
list({
|
|
1922
2074
|
name: "list_donations",
|
|
1923
2075
|
description: "List Giving donations with their fund designations, newest first, by date, fund, or donor. Amounts are integer cents. Read-only.",
|
|
@@ -2075,7 +2227,9 @@ function tools(call, defaultPageSize) {
|
|
|
2075
2227
|
designations: designations(row, index),
|
|
2076
2228
|
}),
|
|
2077
2229
|
}),
|
|
2078
|
-
|
|
2230
|
+
];
|
|
2231
|
+
// --- Webhooks -------------------------------------------------------------
|
|
2232
|
+
const webhooks = [
|
|
2079
2233
|
{
|
|
2080
2234
|
name: "list_webhook_subscriptions",
|
|
2081
2235
|
description: "List the organization's webhook subscriptions: event name, URL, and active state. Never returns the signing secret; that needs pco_api_get.",
|
|
@@ -2104,18 +2258,117 @@ function tools(call, defaultPageSize) {
|
|
|
2104
2258
|
},
|
|
2105
2259
|
},
|
|
2106
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
|
+
};
|
|
2107
2276
|
}
|
|
2108
2277
|
// ---------------------------------------------------------------------------
|
|
2109
2278
|
// Guide and construction
|
|
2110
2279
|
// ---------------------------------------------------------------------------
|
|
2111
|
-
|
|
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);
|
|
2112
2315
|
const organizationInstructions = instructions?.trim();
|
|
2113
|
-
return
|
|
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;
|
|
2114
2367
|
}
|
|
2115
2368
|
/** The closed options planningCenter() accepts; see `assertKnownOptions`. */
|
|
2116
2369
|
const PLANNING_CENTER_OPTIONS = optionsOf()({
|
|
2117
2370
|
...PROVIDER_COMMON,
|
|
2118
|
-
...keys("userAgent", "defaultPageSize", "baseUrl"),
|
|
2371
|
+
...keys("userAgent", "defaultPageSize", "excludeApps", "baseUrl"),
|
|
2119
2372
|
});
|
|
2120
2373
|
/** A maintained Planning Center Online connection over the REST API. */
|
|
2121
2374
|
export const planningCenter = asProviderFactory({
|
|
@@ -2145,19 +2398,25 @@ function planningCenterConnector(id, options) {
|
|
|
2145
2398
|
if (!userAgent || /[\r\n]/.test(userAgent)) {
|
|
2146
2399
|
throw new Error("planningCenter() userAgent must be a non-empty single line.");
|
|
2147
2400
|
}
|
|
2401
|
+
const excluded = excludedApps(options.excludeApps);
|
|
2148
2402
|
const auth = PERSONAL_ACCESS_TOKEN;
|
|
2403
|
+
const baseUrl = options.baseUrl?.trim() || PLANNING_CENTER_API_BASE_URL;
|
|
2149
2404
|
const send = guardedFetch({
|
|
2150
2405
|
provider: "Planning Center",
|
|
2151
|
-
baseUrl
|
|
2406
|
+
baseUrl,
|
|
2152
2407
|
headers: { Accept: "application/json", "User-Agent": userAgent },
|
|
2153
2408
|
maxResponseBytes: PLANNING_CENTER_MAX_RESPONSE_BYTES,
|
|
2154
2409
|
authenticate: (ctx) => auth.headers(ctx),
|
|
2410
|
+
...(excluded.size > 0
|
|
2411
|
+
? { admit: (url, request) => admit(baseUrl, url, request, excluded) }
|
|
2412
|
+
: {}),
|
|
2155
2413
|
});
|
|
2156
|
-
const call = planningCenterCall(send, PLANNING_CENTER_API_VERSIONS);
|
|
2414
|
+
const call = planningCenterCall(send, PLANNING_CENTER_API_VERSIONS, excluded);
|
|
2415
|
+
const surface = tools(call, defaultPageSize, excluded);
|
|
2157
2416
|
return api(id, {
|
|
2158
2417
|
...(options.authScope ? { authScope: options.authScope } : {}),
|
|
2159
2418
|
title: options.title ?? "Planning Center",
|
|
2160
|
-
description: `Planning Center church management —
|
|
2419
|
+
description: `Planning Center church management — ${series(DESCRIBED_APPS.filter((app) => !excluded.has(app)).map((app) => APP_TITLES[app]))}: ${purpose}`,
|
|
2161
2420
|
credential: auth.credential,
|
|
2162
2421
|
async testCredentials(values, ctx) {
|
|
2163
2422
|
try {
|
|
@@ -2184,9 +2443,9 @@ function planningCenterConnector(id, options) {
|
|
|
2184
2443
|
};
|
|
2185
2444
|
}
|
|
2186
2445
|
},
|
|
2187
|
-
callAdmission: options.callAdmission
|
|
2446
|
+
callAdmission: options.callAdmission === undefined ? PLANNING_CENTER_ADMISSION : options.callAdmission,
|
|
2188
2447
|
usageGuide: {
|
|
2189
|
-
content: usageGuide(purpose, options.instructions),
|
|
2448
|
+
content: usageGuide(purpose, options.instructions, excluded),
|
|
2190
2449
|
summary: "Person ids join every product, offset paging, per-product write limits, JSON:API hatches, pinned versions.",
|
|
2191
2450
|
// Deliberately not `required`. Every named tool's schema is complete
|
|
2192
2451
|
// enough to call it on its own; the guide carries the cross-product
|
|
@@ -2194,6 +2453,9 @@ function planningCenterConnector(id, options) {
|
|
|
2194
2453
|
// it leaves the named surface or hits one of them.
|
|
2195
2454
|
},
|
|
2196
2455
|
...(options.maxResultBytes !== undefined ? { maxResultBytes: options.maxResultBytes } : {}),
|
|
2197
|
-
tools:
|
|
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)),
|
|
2459
|
+
],
|
|
2198
2460
|
});
|
|
2199
2461
|
}
|
|
@@ -1,10 +1,30 @@
|
|
|
1
1
|
// Generated by scripts/generate-providers.mjs; do not edit.
|
|
2
2
|
export const skill = {
|
|
3
|
-
"content": "# Planning Center usage\n\nPlanning Center Online church management — People, Services, Groups, Check-Ins, Calendar, Registrations, Giving, Publishing, Webhooks
|
|
3
|
+
"content": "# Planning Center usage\n\nPlanning Center Online church management — People, Services, Groups, Check-Ins, Calendar, Registrations, Giving, Publishing, Webhooks.\n\n## Identity first\n\n- A person id is the join key across every product: the id `search_people` returns is the `personId` for Services schedules, Groups memberships, Giving donations, and Check-Ins. Resolve ids with list tools; never guess one.\n- A person id that used to resolve and now answers `not_found` was most likely merged. Read `/people/v2/person_mergers` with `where[person_to_remove_id]` and follow `person_to_keep_id`.\n- `get_me` names the user this token acts as. Every read and write runs with that user's permissions, product by product; a `provider_permission_denied` means an administrator must grant that user access to the product or record.\n\n## Results and paging\n\n- Records keep Planning Center's own snake_case attribute names; a relationship appears as `<relationship>_id`. Pass `raw: true` for the untouched JSON:API resources and `included`.\n- Lists page by offset: `perPage` up to 100, then pass `page.nextOffset` back as `offset` while `page.hasMore` is true. `page.totalCount` answers \"how many\" without paging — request `perPage: 1`.\n- A bare date in a date filter is the organization's local day; pass a full timestamp for exact bounds. Name filters are exact; end one with `%` for a prefix match.\n- `get_plan` fetches the plan, its items, and its team in one call and sets `itemsTruncated`/`teamMembersTruncated` past 100 rows; page the rest with `pco_api_get`.\n\n## What each product lets the API write\n\n- People: full read/write. `update_person` with `status: inactive` archives reversibly; a DELETE is permanent and only reachable through `pco_api_mutate`. `run_list` can fire list automations.\n- Services: full read/write. A plan's dates come from its plan times. Scheduling needs a team.\n- Groups: memberships and group settings are writable; groups cannot be created, and events and attendance are read-only.\n- Check-Ins and Registrations: read-only. Nobody can be checked in or registered through the API.\n- Calendar: events, instances, and times are read-only; tags, resources, rooms, and folders are writable through `pco_api_mutate`.\n- Giving: the named tools only read, amounts in integer cents. Creating or editing donations, refunds, and committing a batch move money and go through `pco_api_mutate`.\n- Publishing and the `api` product (organization, OAuth applications, personal access tokens) have no named tools; use the hatches.\n\n## The hatches\n\n- `pco_api_get` reaches any GET; `pco_api_mutate` any POST, PATCH, or DELETE, always approval-gated. Paths start `/<app>/v2`, exactly as Planning Center documents them; the connector owns host, auth, content type, and the `X-PCO-API-Version` header.\n- Bodies are JSON:API documents: `{\"data\":{\"type\":\"Email\",\"attributes\":{...}}}`. `null` clears an attribute; an omitted key is left alone.\n- Each product is pinned to a reviewed API version: people 2026-06-04, services 2018-11-01, groups 2023-07-10, check-ins 2025-05-28, calendar 2026-06-22, registrations 2025-05-01, giving 2019-10-18, publishing 2024-03-25, webhooks 2022-10-20, api 2026-09-24. Pass `version` only for an endpoint those versions lack.\n- Query parameters are name/value pairs with literal brackets: `where[status]`, `include`, `order`, `filter`, `fields[Person]`. Every list response's `meta` names the `can_query_by`, `can_order_by`, and `can_include` values for that endpoint; an unknown `where` key is ignored rather than refused.\n- File uploads go to a separate host (upload.planningcenteronline.com), which this connection does not reach.\n\n## Rate limits\n\nPlanning Center allows 100 requests per 20 seconds per user, shared by every integration running as that user. This connection admits 100 calls per 20 seconds, five at a time, per runtime — an approximation, not a guarantee. A `rate_limited` failure carries Planning Center's wait. Filter server-side and use `totalCount` rather than paging to count.\n",
|
|
4
4
|
"instructionsHeading": "Organization instructions",
|
|
5
5
|
"fragments": {
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
6
|
+
"heading": "# Planning Center usage\n\nPlanning Center Online church management — ",
|
|
7
|
+
"purpose": " — for: ",
|
|
8
|
+
"identity": "\n\n## Identity first\n\n- A person id is the join key across every product: the id `search_people` returns is the `personId` for ",
|
|
9
|
+
"conventions": ". Resolve ids with list tools; never guess one.\n- A person id that used to resolve and now answers `not_found` was most likely merged. Read `/people/v2/person_mergers` with `where[person_to_remove_id]` and follow `person_to_keep_id`.\n- `get_me` names the user this token acts as. Every read and write runs with that user's permissions, product by product; a `provider_permission_denied` means an administrator must grant that user access to the product or record.\n\n## Results and paging\n\n- Records keep Planning Center's own snake_case attribute names; a relationship appears as `<relationship>_id`. Pass `raw: true` for the untouched JSON:API resources and `included`.\n- Lists page by offset: `perPage` up to 100, then pass `page.nextOffset` back as `offset` while `page.hasMore` is true. `page.totalCount` answers \"how many\" without paging — request `perPage: 1`.\n- A bare date in a date filter is the organization's local day; pass a full timestamp for exact bounds. Name filters are exact; end one with `%` for a prefix match.\n",
|
|
10
|
+
"plans": "- `get_plan` fetches the plan, its items, and its team in one call and sets `itemsTruncated`/`teamMembersTruncated` past 100 rows; page the rest with `pco_api_get`.\n",
|
|
11
|
+
"writes": "\n## What each product lets the API write\n\n",
|
|
12
|
+
"people": "- People: full read/write. `update_person` with `status: inactive` archives reversibly; a DELETE is permanent and only reachable through `pco_api_mutate`. `run_list` can fire list automations.\n",
|
|
13
|
+
"people_without_lists": "- People: full read/write, except lists. `update_person` with `status: inactive` archives reversibly; a DELETE is permanent and only reachable through `pco_api_mutate`.\n",
|
|
14
|
+
"services": "- Services: full read/write. A plan's dates come from its plan times. Scheduling needs a team.\n",
|
|
15
|
+
"groups": "- Groups: memberships and group settings are writable; groups cannot be created, and events and attendance are read-only.\n",
|
|
16
|
+
"check_ins_registrations": "- Check-Ins and Registrations: read-only. Nobody can be checked in or registered through the API.\n",
|
|
17
|
+
"check_ins": "- Check-Ins: read-only. Nobody can be checked in through the API.\n",
|
|
18
|
+
"registrations": "- Registrations: read-only. Nobody can be registered through the API.\n",
|
|
19
|
+
"calendar": "- Calendar: events, instances, and times are read-only; tags, resources, rooms, and folders are writable through `pco_api_mutate`.\n",
|
|
20
|
+
"giving": "- Giving: the named tools only read, amounts in integer cents. Creating or editing donations, refunds, and committing a batch move money and go through `pco_api_mutate`.\n",
|
|
21
|
+
"publishing_api": "- Publishing and the `api` product (organization, OAuth applications, personal access tokens) have no named tools; use the hatches.\n",
|
|
22
|
+
"publishing": "- Publishing has no named tools; use the hatches.\n",
|
|
23
|
+
"api": "- The `api` product (organization, OAuth applications, personal access tokens) has no named tools; use the hatches.\n",
|
|
24
|
+
"hatches": "\n## The hatches\n\n- `pco_api_get` reaches any GET; `pco_api_mutate` any POST, PATCH, or DELETE, always approval-gated. Paths start `/<app>/v2`, exactly as Planning Center documents them; the connector owns host, auth, content type, and the `X-PCO-API-Version` header.\n- Bodies are JSON:API documents: `{\"data\":{\"type\":\"Email\",\"attributes\":{...}}}`. `null` clears an attribute; an omitted key is left alone.\n- Each product is pinned to a reviewed API version: ",
|
|
25
|
+
"pins": ". Pass `version` only for an endpoint those versions lack.\n- Query parameters are name/value pairs with literal brackets: `where[status]`, `include`, `order`, `filter`, `fields[Person]`. Every list response's `meta` names the `can_query_by`, `can_order_by`, and `can_include` values for that endpoint; an unknown `where` key is ignored rather than refused.\n- File uploads go to a separate host (upload.planningcenteronline.com), which this connection does not reach.\n",
|
|
26
|
+
"disabled": "- This deployment disables some products, and the hatches refuse their paths before sending. While any product is disabled they also refuse People lists, list results, rules, and conditions, by path or `include`, because a list's conditions can draw on any product; webhook event and delivery payloads; and any change to an existing webhook subscription, whose id does not name its product. A new subscription must name a reachable product's event.\n",
|
|
27
|
+
"limits": "\n## Rate limits\n\nPlanning Center allows 100 requests per 20 seconds per user, shared by every integration running as that user. This connection admits 100 calls per 20 seconds, five at a time, per runtime — an approximation, not a guarantee. A `rate_limited` failure carries Planning Center's wait. Filter server-side and use `totalCount` rather than paging to count.\n",
|
|
28
|
+
"published": "# Planning Center usage\n\nPlanning Center Online church management — People, Services, Groups, Check-Ins, Calendar, Registrations, Giving, Publishing, Webhooks.\n\n## Identity first\n\n- A person id is the join key across every product: the id `search_people` returns is the `personId` for Services schedules, Groups memberships, Giving donations, and Check-Ins. Resolve ids with list tools; never guess one.\n- A person id that used to resolve and now answers `not_found` was most likely merged. Read `/people/v2/person_mergers` with `where[person_to_remove_id]` and follow `person_to_keep_id`.\n- `get_me` names the user this token acts as. Every read and write runs with that user's permissions, product by product; a `provider_permission_denied` means an administrator must grant that user access to the product or record.\n\n## Results and paging\n\n- Records keep Planning Center's own snake_case attribute names; a relationship appears as `<relationship>_id`. Pass `raw: true` for the untouched JSON:API resources and `included`.\n- Lists page by offset: `perPage` up to 100, then pass `page.nextOffset` back as `offset` while `page.hasMore` is true. `page.totalCount` answers \"how many\" without paging — request `perPage: 1`.\n- A bare date in a date filter is the organization's local day; pass a full timestamp for exact bounds. Name filters are exact; end one with `%` for a prefix match.\n- `get_plan` fetches the plan, its items, and its team in one call and sets `itemsTruncated`/`teamMembersTruncated` past 100 rows; page the rest with `pco_api_get`.\n\n## What each product lets the API write\n\n- People: full read/write. `update_person` with `status: inactive` archives reversibly; a DELETE is permanent and only reachable through `pco_api_mutate`. `run_list` can fire list automations.\n- Services: full read/write. A plan's dates come from its plan times. Scheduling needs a team.\n- Groups: memberships and group settings are writable; groups cannot be created, and events and attendance are read-only.\n- Check-Ins and Registrations: read-only. Nobody can be checked in or registered through the API.\n- Calendar: events, instances, and times are read-only; tags, resources, rooms, and folders are writable through `pco_api_mutate`.\n- Giving: the named tools only read, amounts in integer cents. Creating or editing donations, refunds, and committing a batch move money and go through `pco_api_mutate`.\n- Publishing and the `api` product (organization, OAuth applications, personal access tokens) have no named tools; use the hatches.\n\n## The hatches\n\n- `pco_api_get` reaches any GET; `pco_api_mutate` any POST, PATCH, or DELETE, always approval-gated. Paths start `/<app>/v2`, exactly as Planning Center documents them; the connector owns host, auth, content type, and the `X-PCO-API-Version` header.\n- Bodies are JSON:API documents: `{\"data\":{\"type\":\"Email\",\"attributes\":{...}}}`. `null` clears an attribute; an omitted key is left alone.\n- Each product is pinned to a reviewed API version: people 2026-06-04, services 2018-11-01, groups 2023-07-10, check-ins 2025-05-28, calendar 2026-06-22, registrations 2025-05-01, giving 2019-10-18, publishing 2024-03-25, webhooks 2022-10-20, api 2026-09-24. Pass `version` only for an endpoint those versions lack.\n- Query parameters are name/value pairs with literal brackets: `where[status]`, `include`, `order`, `filter`, `fields[Person]`. Every list response's `meta` names the `can_query_by`, `can_order_by`, and `can_include` values for that endpoint; an unknown `where` key is ignored rather than refused.\n- File uploads go to a separate host (upload.planningcenteronline.com), which this connection does not reach.\n\n## Rate limits\n\nPlanning Center allows 100 requests per 20 seconds per user, shared by every integration running as that user. This connection admits 100 calls per 20 seconds, five at a time, per runtime — an approximation, not a guarantee. A `rate_limited` failure carries Planning Center's wait. Filter server-side and use `totalCount` rather than paging to count.\n"
|
|
9
29
|
}
|
|
10
30
|
};
|
|
@@ -46,9 +46,12 @@ export type SheetsOptions = GoogleWorkspaceOptions;
|
|
|
46
46
|
* });
|
|
47
47
|
* ```
|
|
48
48
|
*
|
|
49
|
+
* Agents never reach that function: give each pool they use an `owner`,
|
|
50
|
+
* the address they act as there (decisions/0006).
|
|
51
|
+
*
|
|
49
52
|
* Reads are read-only. `create_spreadsheet`, `add_sheet`, and `append_values`
|
|
50
53
|
* are additive writes the host approves through call_destructive_tool or a
|
|
51
|
-
*
|
|
54
|
+
* program on a writable endpoint; the value writes, `clear_values`, and the raw
|
|
52
55
|
* `batch_update_spreadsheet` are destructive.
|
|
53
56
|
*/
|
|
54
57
|
export declare const sheets: import("../../provider.js").ProviderFactory<GoogleWorkspaceOptions>;
|
|
@@ -33,7 +33,7 @@ import { skill } from "./skill.generated.js";
|
|
|
33
33
|
*/
|
|
34
34
|
import { apiConnector as api, defined } from "../../connectors/api-connector.js";
|
|
35
35
|
import { ConnectorCallError } from "../../errors.js";
|
|
36
|
-
import { googleOutcomeOf, GOOGLE_WORKSPACE_OPTIONS, googleWorkspaceClient, workspaceConnection, } from "../_shared/google/workspace.js";
|
|
36
|
+
import { googleOutcomeOf, GOOGLE_WORKSPACE_OPTIONS, googleWorkspaceClient, workspaceConnection, workspaceConnector, } from "../_shared/google/workspace.js";
|
|
37
37
|
import { clampText, jsonBytes, RESULT_BUDGET_BYTES } from "../_shared/google/result-size.js";
|
|
38
38
|
import { asProviderFactory } from "../../provider.js";
|
|
39
39
|
/**
|
|
@@ -550,8 +550,8 @@ const SPREADSHEET_FIELDS = [
|
|
|
550
550
|
// --- Tools ------------------------------------------------------------------------
|
|
551
551
|
function tools(client) {
|
|
552
552
|
const readOnly = { readOnlyHint: true };
|
|
553
|
-
// Additive: nothing that existed is lost.
|
|
554
|
-
//
|
|
553
|
+
// Additive: nothing that existed is lost. Writes require a writable
|
|
554
|
+
// endpoint: call_destructive_tool or a program. The host controls approval.
|
|
555
555
|
const additive = { readOnlyHint: false, destructiveHint: false };
|
|
556
556
|
const destructive = { readOnlyHint: false, destructiveHint: true };
|
|
557
557
|
const spreadsheetPath = (args) => `/spreadsheets/${encodeURIComponent(String(args["spreadsheetId"]))}`;
|
|
@@ -1186,9 +1186,12 @@ function usageGuide(purpose, instructions) {
|
|
|
1186
1186
|
* });
|
|
1187
1187
|
* ```
|
|
1188
1188
|
*
|
|
1189
|
+
* Agents never reach that function: give each pool they use an `owner`,
|
|
1190
|
+
* the address they act as there (decisions/0006).
|
|
1191
|
+
*
|
|
1189
1192
|
* Reads are read-only. `create_spreadsheet`, `add_sheet`, and `append_values`
|
|
1190
1193
|
* are additive writes the host approves through call_destructive_tool or a
|
|
1191
|
-
*
|
|
1194
|
+
* program on a writable endpoint; the value writes, `clear_values`, and the raw
|
|
1192
1195
|
* `batch_update_spreadsheet` are destructive.
|
|
1193
1196
|
*/
|
|
1194
1197
|
export const sheets = asProviderFactory({
|
|
@@ -1218,7 +1221,7 @@ function sheetsConnector(id, options) {
|
|
|
1218
1221
|
notFound: "ambiguous",
|
|
1219
1222
|
connection,
|
|
1220
1223
|
});
|
|
1221
|
-
return api(id, {
|
|
1224
|
+
return workspaceConnector(connection, api(id, {
|
|
1222
1225
|
...defined({
|
|
1223
1226
|
callAdmission: options.callAdmission,
|
|
1224
1227
|
maxResultBytes: options.maxResultBytes,
|
|
@@ -1233,5 +1236,5 @@ function sheetsConnector(id, options) {
|
|
|
1233
1236
|
required: true,
|
|
1234
1237
|
},
|
|
1235
1238
|
tools: tools(client),
|
|
1236
|
-
});
|
|
1239
|
+
}));
|
|
1237
1240
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// Generated by scripts/generate-providers.mjs; do not edit.
|
|
2
2
|
export const skill = {
|
|
3
|
-
"content": "# Google Sheets usage\n\nActs as the signed-in person in their own Google Sheets through Workspace delegation: it reaches only spreadsheets they can open, starting from an id.\n\nConnection purpose: \n\n## Whose spreadsheets\n\nEvery call acts as the Workspace account deployment config maps the caller\nto. No argument
|
|
3
|
+
"content": "# Google Sheets usage\n\nActs as the signed-in person in their own Google Sheets through Workspace delegation: it reaches only spreadsheets they can open, starting from an id.\n\nConnection purpose: \n\n## Whose spreadsheets\n\nEvery call acts as the Workspace account deployment config maps the caller\nto; an agent acts as the owner of the pool it came through, unless the deployment fixes one shared account that every caller, agent or person, acts as. No argument\nnames an account. A call with none mapped, or from an agent whose pool has\nno owner, fails `auth_required`; only an operator can change either. An id that is\nunknown or not shared with this person fails the same way — Google does not\nsay which.\n\n## Finding a spreadsheet\n\n- Sheets cannot list or search files. Find one by name with a Google Drive\n search, or take the id from its URL (between `/d/` and `/edit`).\n- `get_spreadsheet` first: sheet titles, ids, grid sizes, and named ranges,\n so ranges are named right rather than guessed.\n\n## Ranges\n\n- A1 notation. Quote a sheet title with spaces or punctuation:\n `'Q3 Budget'!A1:D20`, doubling any `'` inside it. A bare title is the\n whole sheet; `Sheet1!B:B` a whole column; a named range works by name.\n- `get_values` pages by whole rows under `maxCells` and `maxBytes`:\n page with `page.nextCursor` and the same spreadsheetId, ranges, and\n render options. A range with explicit rows (`A1:D500`, `B:D`) resumes\n where it stopped; a sheet title or named range is re-read and its earlier\n rows skipped, so `rowOffset` says how far into Google's echoed range the\n page starts.\n- A row too wide or too large for one page is refused, not cut: narrow the\n columns or lower `maxCellChars`. The default `maxBytes` is what\n `execute_code` can receive; raise it only for a direct `call_tool`\n read.\n- A cell over characters ends with a truncation marker;\n read it alone with `maxCellChars` for the rest. Reduce inside\n `execute_code` before returning a large read.\n\n## Writing\n\n- `valueInputOption` is required: `RAW` stores text as given;\n `USER_ENTERED` parses it as typed, so a leading `=` makes a formula. Use\n RAW for data from outside the spreadsheet.\n- In written rows, `null` leaves a cell as it was and `\"\"` empties it.\n- `append_values` inserts rows below the table and overwrites nothing;\n `update_values` and `batch_update_values` overwrite the cells they\n cover; `clear_values` empties values but keeps formatting.\n- `batch_update_spreadsheet` takes Google's own Request objects for\n everything else (formatting, sorting, deleting rows or sheets); it is\n atomic, always needs approval, and can destroy data. Replies too large\n to return come back cut to their kind, ids, and counts with a `note`;\n the requests still applied, so never send them again to see the rest.\n- Sheets has no revision check on writes: edits are last-writer-wins, so a\n person editing the same cells meanwhile is overwritten without warning.\n Read just before writing over anything a person may be editing.\n- Moving, sharing, or deleting a spreadsheet file is Drive's job.\n",
|
|
4
4
|
"instructionsHeading": "Connection instructions",
|
|
5
5
|
"fragments": {
|
|
6
6
|
"guide_0": "# Google Sheets usage\n\nActs as the signed-in person in their own Google Sheets through Workspace delegation: it reaches only spreadsheets they can open, starting from an id.\n\nConnection purpose: ",
|
|
7
|
-
"guide_1": "\n\n## Whose spreadsheets\n\nEvery call acts as the Workspace account deployment config maps the caller\nto. No argument
|
|
7
|
+
"guide_1": "\n\n## Whose spreadsheets\n\nEvery call acts as the Workspace account deployment config maps the caller\nto; an agent acts as the owner of the pool it came through, unless the deployment fixes one shared account that every caller, agent or person, acts as. No argument\nnames an account. A call with none mapped, or from an agent whose pool has\nno owner, fails `auth_required`; only an operator can change either. An id that is\nunknown or not shared with this person fails the same way — Google does not\nsay which.\n\n## Finding a spreadsheet\n\n- Sheets cannot list or search files. Find one by name with a Google Drive\n search, or take the id from its URL (between `/d/` and `/edit`).\n- `get_spreadsheet` first: sheet titles, ids, grid sizes, and named ranges,\n so ranges are named right rather than guessed.\n\n## Ranges\n\n- A1 notation. Quote a sheet title with spaces or punctuation:\n `'Q3 Budget'!A1:D20`, doubling any `'` inside it. A bare title is the\n whole sheet; `Sheet1!B:B` a whole column; a named range works by name.\n- `get_values` pages by whole rows under `maxCells` and `maxBytes`:\n page with `page.nextCursor` and the same spreadsheetId, ranges, and\n render options. A range with explicit rows (`A1:D500`, `B:D`) resumes\n where it stopped; a sheet title or named range is re-read and its earlier\n rows skipped, so `rowOffset` says how far into Google's echoed range the\n page starts.\n- A row too wide or too large for one page is refused, not cut: narrow the\n columns or lower `maxCellChars`. The default `maxBytes` is what\n `execute_code` can receive; raise it only for a direct `call_tool`\n read.\n- A cell over ",
|
|
8
8
|
"guide_2": " characters ends with a truncation marker;\n read it alone with `maxCellChars` for the rest. Reduce inside\n `execute_code` before returning a large read.\n\n## Writing\n\n- `valueInputOption` is required: `RAW` stores text as given;\n `USER_ENTERED` parses it as typed, so a leading `=` makes a formula. Use\n RAW for data from outside the spreadsheet.\n- In written rows, `null` leaves a cell as it was and `\"\"` empties it.\n- `append_values` inserts rows below the table and overwrites nothing;\n `update_values` and `batch_update_values` overwrite the cells they\n cover; `clear_values` empties values but keeps formatting.\n- `batch_update_spreadsheet` takes Google's own Request objects for\n everything else (formatting, sorting, deleting rows or sheets); it is\n atomic, always needs approval, and can destroy data. Replies too large\n to return come back cut to their kind, ids, and counts with a `note`;\n the requests still applied, so never send them again to see the rest.\n- Sheets has no revision check on writes: edits are last-writer-wins, so a\n person editing the same cells meanwhile is overwritten without warning.\n Read just before writing over anything a person may be editing.\n- Moving, sharing, or deleting a spreadsheet file is Drive's job.\n"
|
|
9
9
|
}
|
|
10
10
|
};
|
|
@@ -45,7 +45,7 @@ export type SlidesOptions = GoogleWorkspaceOptions;
|
|
|
45
45
|
*
|
|
46
46
|
* Reads run in programs. `create_presentation`, `create_slide`,
|
|
47
47
|
* `create_comment`, and `create_comment_reply` are additive writes the host
|
|
48
|
-
* approves through call_destructive_tool or a
|
|
48
|
+
* approves through call_destructive_tool or a program on a writable endpoint;
|
|
49
49
|
* `replace_all_text`, `update_comment_thread`, `update_comment_post`,
|
|
50
50
|
* `delete_comment`, `delete_comment_reply`, and `batch_update_presentation`
|
|
51
51
|
* are destructive.
|