@zackbart/connecta 0.34.1 → 0.36.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 +418 -0
- package/README.md +65 -105
- package/SECURITY.md +3 -2
- package/bin/connecta.mjs +41 -16
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.d.ts +2 -1
- package/dist/access-tokens.js +66 -30
- package/dist/activity.d.ts +1 -1
- package/dist/activity.js +2 -0
- package/dist/agent-config.d.ts +23 -0
- package/dist/agent-config.js +59 -0
- package/dist/agent-description.js +16 -0
- package/dist/agent-identity.js +4 -4
- package/dist/agent-observation.js +39 -0
- package/dist/auth/clerk-sdk/client.d.ts +2 -0
- package/dist/auth/clerk-sdk/generated.js +554 -563
- package/dist/auth/clerk-transport.js +70 -3
- package/dist/auth/clerk.d.ts +13 -0
- package/dist/auth/clerk.js +653 -246
- package/dist/auth/cloudflare-access.js +46 -42
- package/dist/auth/downstream-client-metadata.js +2 -2
- package/dist/auth/downstream-oauth.js +2044 -1161
- package/dist/auth/downstream-revocation.js +88 -88
- package/dist/auth/native-call.js +20 -0
- package/dist/auth/native.js +184 -0
- package/dist/auth/oauth-start-reset.js +11 -5
- package/dist/auth/oauth-token-response.js +121 -102
- package/dist/auth/oauth-v2-migration.js +97 -78
- package/dist/auth/renewable-grant.js +10 -0
- package/dist/auth/static-oauth.js +751 -281
- package/dist/auth/vercel-agent.js +266 -0
- package/dist/auth-admission.js +204 -0
- package/dist/auth-elicitation.js +350 -241
- package/dist/authorization-handoff.js +164 -90
- package/dist/call-admission-context.js +3 -10
- package/dist/call-admission.js +2 -0
- package/dist/call-classification.js +9 -0
- package/dist/call-recovery.js +32 -13
- package/dist/canonical-json.js +8 -0
- package/dist/catalog-cache.js +369 -213
- package/dist/catalog-service.js +659 -530
- package/dist/catalog.js +5 -1
- package/dist/concurrency.js +0 -25
- package/dist/config-defaults.d.ts +1 -0
- package/dist/config-defaults.js +1 -0
- package/dist/config-schema.d.ts +3 -1
- package/dist/config-schema.js +13 -3
- package/dist/config.d.ts +40 -6
- package/dist/config.js +40 -26
- package/dist/connector-caller.d.ts +4 -0
- package/dist/connector-caller.js +17 -4
- package/dist/connectors/api-connector.d.ts +29 -7
- package/dist/connectors/api-connector.js +145 -57
- package/dist/connectors/guarded-fetch.d.ts +51 -17
- package/dist/connectors/guarded-fetch.js +215 -121
- package/dist/connectors/negotiation-cache.js +20 -10
- package/dist/connectors/remote-mcp.d.ts +10 -30
- package/dist/connectors/remote-mcp.js +955 -563
- package/dist/d1.js +10 -243
- package/dist/derived-context.js +5 -0
- package/dist/describe-config.d.ts +3 -1
- package/dist/describe-config.js +2 -0
- package/dist/downstream-input.js +347 -260
- package/dist/errors.d.ts +87 -6
- package/dist/errors.js +114 -4
- package/dist/execute.js +87 -50
- package/dist/executor-admission.js +3 -0
- package/dist/executors/quickjs.js +163 -112
- package/dist/guarded-transport.js +50 -0
- package/dist/guest-runtime.js +1 -1
- package/dist/http/client.js +439 -0
- package/dist/http/errors.js +558 -0
- package/dist/http/fields.js +49 -0
- package/dist/http/hatch-paging.js +318 -0
- package/dist/http/hatch.js +1279 -0
- package/dist/http/json-schema.js +267 -0
- package/dist/http/node-transport.js +241 -0
- package/dist/http/oauth.js +162 -0
- package/dist/http/paging.js +227 -0
- package/dist/http/provider.js +221 -0
- package/dist/http/safety.js +82 -0
- package/dist/http/services.js +79 -0
- package/dist/http/tools.js +379 -0
- package/dist/http/vendor-index.js +138 -0
- package/dist/http/vendor.js +448 -0
- package/dist/http/write-request.js +66 -0
- package/dist/http-policy.d.ts +11 -0
- package/dist/http-policy.js +45 -0
- package/dist/inbound-credential.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -0
- package/dist/invocation-auth.js +30 -19
- package/dist/invocation.js +218 -85
- package/dist/mcp-result.js +0 -4
- package/dist/membership.d.ts +4 -0
- package/dist/meta-output.js +1 -1
- package/dist/meta-tools.js +531 -406
- package/dist/native-recovery.js +52 -0
- package/dist/native-tools.js +382 -0
- package/dist/node.d.ts +4 -3
- package/dist/node.js +45 -8
- package/dist/oauth-handoff.js +177 -104
- package/dist/oauth-partition.js +6 -20
- package/dist/oauth-sealing.js +6 -7
- package/dist/operation-vocabulary.js +101 -0
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +17 -4
- package/dist/operator-ui/contract.d.ts +8 -3
- package/dist/operator-ui/generated.js +4 -4
- package/dist/operator-ui/model.d.ts +2 -3
- package/dist/program-writes.js +47 -26
- package/dist/provider-call-facts.d.ts +115 -0
- package/dist/provider-call-facts.js +182 -0
- package/dist/provider-call.d.ts +112 -0
- package/dist/provider-call.js +271 -0
- package/dist/provider.d.ts +65 -31
- package/dist/provider.js +116 -39
- package/dist/providers/_shared/google/hatch.js +91 -0
- package/dist/providers/_shared/google/kit.js +427 -0
- package/dist/providers/_shared/google/result-size.js +15 -2
- package/dist/providers/_shared/google/service-account.d.ts +2 -1
- package/dist/providers/_shared/google/service-account.js +8 -4
- package/dist/providers/_shared/google/workspace.d.ts +6 -210
- package/dist/providers/_shared/google/workspace.js +6 -425
- package/dist/providers/_shared/rest/compact-index.js +86 -0
- package/dist/providers/_shared/rest/kit.js +146 -0
- package/dist/providers/_shared/rest/operation-index.js +230 -14
- package/dist/providers/_shared/rest/response-media.js +11 -0
- package/dist/providers/_shared/rest/value-safety.js +353 -195
- package/dist/providers/_shared/sensitive-fields.js +27 -0
- package/dist/providers/breeze/api.js +584 -0
- package/dist/providers/breeze/index.d.ts +1 -3
- package/dist/providers/breeze/index.js +67 -1420
- package/dist/providers/breeze/openapi.generated.js +60 -0
- package/dist/providers/breeze/skill.generated.js +2 -6
- package/dist/providers/breeze/tools.js +696 -0
- package/dist/providers/ccb/api.js +496 -0
- package/dist/providers/ccb/index.d.ts +15 -37
- package/dist/providers/ccb/index.js +77 -1702
- package/dist/providers/ccb/lib/constants.d.ts +34 -0
- package/dist/providers/ccb/lib/constants.js +74 -0
- package/dist/providers/ccb/lib/guide.js +9 -0
- package/dist/providers/ccb/lib/verdicts.js +387 -0
- package/dist/providers/ccb/openapi.generated.js +397 -0
- package/dist/providers/ccb/safety.js +295 -0
- package/dist/providers/ccb/skill.generated.js +2 -6
- package/dist/providers/ccb/tools.js +853 -0
- package/dist/providers/cloudflare/api.js +1066 -0
- package/dist/providers/cloudflare/index.d.ts +5 -22
- package/dist/providers/cloudflare/index.js +79 -197
- package/dist/providers/cloudflare/lib/config.d.ts +16 -0
- package/dist/providers/cloudflare/lib/config.js +2 -0
- package/dist/providers/cloudflare/lib/discovery.js +29 -0
- package/dist/providers/cloudflare/{graphql.js → lib/graphql.js} +1 -1
- package/dist/providers/cloudflare/lib/hatch.js +103 -0
- package/dist/providers/cloudflare/lib/path-bounds.js +28 -0
- package/dist/providers/cloudflare/lib/response-media.js +75 -0
- package/dist/providers/cloudflare/openapi.generated.js +4 -3
- package/dist/providers/cloudflare/{value-safety.js → safety.js} +487 -0
- package/dist/providers/cloudflare/skill.generated.js +2 -10
- package/dist/providers/cloudflare/tools.js +360 -0
- package/dist/providers/docs/api.js +51 -0
- package/dist/providers/docs/index.d.ts +1 -12
- package/dist/providers/docs/index.js +53 -1165
- package/dist/providers/docs/lib/constants.d.ts +18 -0
- package/dist/providers/docs/lib/constants.js +18 -0
- package/dist/providers/docs/lib/discovery.js +26 -0
- package/dist/providers/docs/lib/requests.js +48 -0
- package/dist/providers/docs/lib/schemas.js +168 -0
- package/dist/providers/docs/openapi.generated.js +16 -0
- package/dist/providers/docs/safety.js +54 -0
- package/dist/providers/docs/skill.generated.js +2 -6
- package/dist/providers/docs/tools.js +801 -0
- package/dist/providers/drive/api.js +177 -0
- package/dist/providers/drive/index.d.ts +1 -12
- package/dist/providers/drive/index.js +38 -1448
- package/dist/providers/drive/lib/constants.d.ts +6 -0
- package/dist/providers/drive/lib/constants.js +6 -0
- package/dist/providers/drive/lib/contracts.js +532 -0
- package/dist/providers/drive/lib/hatch.js +197 -0
- package/dist/providers/drive/openapi.generated.js +77 -0
- package/dist/providers/drive/safety.js +90 -0
- package/dist/providers/drive/skill.generated.js +2 -6
- package/dist/providers/drive/tools.js +1290 -0
- package/dist/providers/forms/api.js +71 -0
- package/dist/providers/forms/index.d.ts +1 -12
- package/dist/providers/forms/index.js +49 -1145
- package/dist/providers/forms/lib/constants.d.ts +14 -0
- package/dist/providers/forms/lib/constants.js +19 -0
- package/dist/providers/forms/lib/hatch.js +72 -0
- package/dist/providers/forms/openapi.generated.js +23 -0
- package/dist/providers/forms/safety.js +34 -0
- package/dist/providers/forms/skill.generated.js +2 -8
- package/dist/providers/forms/tools.js +1023 -0
- package/dist/providers/github/api.js +150 -0
- package/dist/providers/github/index.d.ts +4 -5
- package/dist/providers/github/index.js +30 -87
- package/dist/providers/github/{auth.d.ts → lib/auth.d.ts} +5 -3
- package/dist/providers/github/{auth.js → lib/auth.js} +40 -43
- package/dist/providers/github/lib/discovery.js +132 -0
- package/dist/providers/github/lib/operations.js +58 -0
- package/dist/providers/github/{scope.d.ts → lib/scope.d.ts} +2 -0
- package/dist/providers/github/{scope.js → lib/scope.js} +25 -1
- package/dist/providers/github/openapi.generated.js +1216 -0
- package/dist/providers/github/safety.js +216 -0
- package/dist/providers/github/skill.generated.js +2 -5
- package/dist/providers/github/tools.js +506 -0
- package/dist/providers/gmail/api.js +114 -0
- package/dist/providers/gmail/index.d.ts +1 -12
- package/dist/providers/gmail/index.js +73 -1621
- package/dist/providers/gmail/lib/constants.d.ts +13 -0
- package/dist/providers/gmail/lib/constants.js +16 -0
- package/dist/providers/gmail/lib/index.js +173 -0
- package/dist/providers/gmail/lib/schemas.js +176 -0
- package/dist/providers/gmail/lib/threading.js +120 -0
- package/dist/providers/gmail/openapi.generated.js +92 -0
- package/dist/providers/gmail/safety.js +21 -0
- package/dist/providers/gmail/skill.generated.js +2 -6
- package/dist/providers/gmail/tools.js +1435 -0
- package/dist/providers/overflow/api.js +1008 -0
- package/dist/providers/overflow/index.d.ts +1 -17
- package/dist/providers/overflow/index.js +60 -1488
- package/dist/providers/overflow/skill.generated.js +2 -5
- package/dist/providers/overflow/tools.js +873 -0
- package/dist/providers/planning-center/api.js +467 -0
- package/dist/providers/planning-center/index.d.ts +4 -35
- package/dist/providers/planning-center/index.js +69 -2445
- package/dist/providers/planning-center/lib/constants.d.ts +58 -0
- package/dist/providers/planning-center/lib/constants.js +83 -0
- package/dist/providers/planning-center/lib/discovery.js +40 -0
- package/dist/providers/planning-center/lib/guide.js +39 -0
- package/dist/providers/planning-center/lib/json-api.js +83 -0
- package/dist/providers/planning-center/lib/verdicts.js +1758 -0
- package/dist/providers/planning-center/openapi.generated.js +1764 -0
- package/dist/providers/planning-center/safety.js +188 -0
- package/dist/providers/planning-center/skill.generated.js +2 -27
- package/dist/providers/planning-center/tools.js +1010 -0
- package/dist/providers/quickbooks/api.js +299 -0
- package/dist/providers/quickbooks/index.d.ts +13 -0
- package/dist/providers/quickbooks/index.js +93 -0
- package/dist/providers/quickbooks/lib/query.js +118 -0
- package/dist/providers/quickbooks/lib/writes.js +168 -0
- package/dist/providers/quickbooks/safety.js +222 -0
- package/dist/providers/quickbooks/skill.generated.js +5 -0
- package/dist/providers/quickbooks/tools.js +117 -0
- package/dist/providers/sheets/api.js +133 -0
- package/dist/providers/sheets/index.d.ts +4 -16
- package/dist/providers/sheets/index.js +61 -1172
- package/dist/providers/sheets/lib/constants.d.ts +14 -0
- package/dist/providers/sheets/lib/constants.js +14 -0
- package/dist/providers/sheets/lib/hatch.js +163 -0
- package/dist/providers/sheets/lib/requests.js +105 -0
- package/dist/providers/sheets/lib/schemas.js +138 -0
- package/dist/providers/sheets/openapi.generated.js +30 -0
- package/dist/providers/sheets/safety.js +27 -0
- package/dist/providers/sheets/skill.generated.js +2 -7
- package/dist/providers/sheets/tools.js +733 -0
- package/dist/providers/slides/api.js +147 -0
- package/dist/providers/slides/index.d.ts +2 -10
- package/dist/providers/slides/index.js +48 -2555
- package/dist/providers/slides/lib/constants.d.ts +11 -0
- package/dist/providers/slides/lib/constants.js +11 -0
- package/dist/providers/slides/lib/requests.js +60 -0
- package/dist/providers/slides/lib/schemas.js +565 -0
- package/dist/providers/slides/openapi.generated.js +18 -0
- package/dist/providers/slides/skill.generated.js +2 -6
- package/dist/providers/slides/tools.js +1968 -0
- package/dist/providers/stripe/api.js +401 -0
- package/dist/providers/stripe/index.d.ts +5 -27
- package/dist/providers/stripe/index.js +74 -210
- package/dist/providers/stripe/lib/config.d.ts +4 -0
- package/dist/providers/stripe/lib/config.js +2 -0
- package/dist/providers/stripe/openapi.generated.js +4 -3
- package/dist/providers/stripe/{value-safety.js → safety.js} +5 -5
- package/dist/providers/stripe/skill.generated.js +2 -11
- package/dist/providers/stripe/tools.js +77 -0
- package/dist/providers/tithely/api.js +531 -0
- package/dist/providers/tithely/index.d.ts +17 -0
- package/dist/providers/tithely/index.js +86 -965
- package/dist/providers/tithely/skill.generated.js +2 -5
- package/dist/providers/tithely/tools.js +426 -0
- package/dist/providers/vercel/api.js +551 -0
- package/dist/providers/vercel/index.d.ts +9 -25
- package/dist/providers/vercel/index.js +65 -260
- package/dist/providers/vercel/lib/config.d.ts +2 -0
- package/dist/providers/vercel/lib/config.js +2 -0
- package/dist/providers/vercel/lib/index.js +117 -0
- package/dist/providers/vercel/lib/logs.js +100 -0
- package/dist/providers/vercel/lib/pin.js +270 -0
- package/dist/providers/vercel/openapi.generated.js +10 -5
- package/dist/providers/vercel/{value-safety.js → safety.js} +247 -38
- package/dist/providers/vercel/skill.generated.js +2 -7
- package/dist/providers/vercel/tools.js +642 -0
- package/dist/redaction-identity.js +47 -0
- package/dist/redaction-marker.js +125 -0
- package/dist/registry.d.ts +21 -15
- package/dist/registry.js +409 -376
- package/dist/routes/access-tokens.js +1 -1
- package/dist/routes/actor-labels.js +1 -1
- package/dist/routes/connect.js +1 -1
- package/dist/routes/dispatch.js +103 -0
- package/dist/routes/mcp.js +170 -79
- package/dist/routes/oauth.js +6 -1
- package/dist/routes/operator.js +2 -2
- package/dist/routes/shared.d.ts +5 -4
- package/dist/routes/shared.js +244 -177
- package/dist/routes/skills.js +10 -8
- package/dist/routes/ui-config.js +27 -3
- package/dist/routes/ui-data.js +21 -20
- package/dist/routes/ui.js +1 -1
- package/dist/runtime/call-admission.js +0 -26
- package/dist/runtime/call-entry.js +30 -0
- package/dist/runtime/call.js +552 -0
- package/dist/runtime/connector-scope.js +7 -0
- package/dist/runtime/credential-probe.js +34 -0
- package/dist/runtime/executor.js +40 -0
- package/dist/runtime/native-storage.js +63 -0
- package/dist/runtime/registry.js +27 -0
- package/dist/runtime/request-scope.js +72 -0
- package/dist/runtime/run.js +15 -7
- package/dist/runtime/shared-read.js +34 -5
- package/dist/runtime/storage.js +34 -21
- package/dist/sent-secrets.d.ts +194 -37
- package/dist/sent-secrets.js +1743 -340
- package/dist/server.js +5 -56
- package/dist/skills.js +322 -223
- package/dist/sqlite.js +15 -14
- package/dist/storage/d1-copy.js +255 -0
- package/dist/storage/d1-driver.js +15 -0
- package/dist/storage/keys.js +28 -0
- package/dist/storage/sql-driver.js +88 -0
- package/dist/storage/sql-native.js +544 -0
- package/dist/storage/sql-schema.d.ts +6 -0
- package/dist/storage/sql-schema.js +15 -0
- package/dist/storage/sql.d.ts +14 -20
- package/dist/storage/sql.js +29 -535
- package/dist/timeout.js +0 -26
- package/dist/tool-safety.js +20 -10
- package/dist/types.d.ts +9 -109
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +0 -1
- package/dist/upward.js +3 -0
- package/dist/usage-guide.js +3 -1
- package/dist/validate.d.ts +4 -2
- package/dist/validate.js +86 -9
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +312 -292
- package/dist/workspace-subject.js +5 -1
- package/dist/write-outcome.js +454 -34
- package/documentation/README.md +28 -7
- package/documentation/architecture.md +2409 -255
- package/documentation/auth.md +867 -114
- package/documentation/code-mode.md +49 -16
- package/documentation/deploying.md +164 -20
- package/documentation/hosted-mcp-migration.md +460 -0
- package/documentation/integrating.md +129 -144
- package/documentation/meta-tools.md +409 -32
- package/documentation/operating.md +27 -3
- package/documentation/operator-tests.md +3 -0
- package/documentation/operator-ui.md +84 -48
- package/documentation/provider-auth-migration.md +16 -63
- package/documentation/provider-migration-0.29.md +10 -5
- package/documentation/provider-migration-0.35.md +975 -0
- package/documentation/providers.md +1347 -0
- package/examples/worker/AGENTS.md +23 -9
- package/examples/worker/README.md +254 -135
- package/examples/worker/alchemy.run.ts +116 -0
- package/examples/worker/gitignore +10 -0
- package/examples/worker/src/connecta.config.ts +23 -21
- package/examples/worker/wrangler.jsonc +14 -5
- package/guides/connecting-agents.md +157 -0
- package/guides/how-it-works.md +156 -0
- package/guides/safety-and-trust.md +153 -0
- package/package.json +30 -50
- package/templates/node/.env.example +3 -0
- package/templates/node/README.md +22 -2
- package/templates/node/docker-compose.yml +1 -0
- package/templates/node/package.json +3 -2
- package/templates/node/src/connecta.config.ts +8 -0
- package/dist/catalog-drift.d.ts +0 -138
- package/dist/catalog-drift.js +0 -578
- package/dist/operator-ui/agents.d.ts +0 -74
- package/dist/operator-ui/agents.js +0 -104
- package/dist/operator-ui/config-summary.js +0 -565
- package/dist/operator-ui/connection-map.js +0 -376
- package/dist/operator-ui/endpoints.js +0 -221
- package/dist/operator-ui/fix-prompts.d.ts +0 -18
- package/dist/operator-ui/fix-prompts.js +0 -87
- package/dist/operator-ui/health.js +0 -52
- package/dist/operator-ui/member-page.js +0 -97
- package/dist/operator-ui/overview.js +0 -123
- package/dist/operator-ui/setup-commands.js +0 -51
- package/dist/operator-ui/view.d.ts +0 -401
- package/dist/operator-ui/view.js +0 -705
- package/dist/providers/_shared/request-token.js +0 -83
- package/dist/providers/_shared/rest/dispatch.js +0 -41
- package/dist/providers/_shared/rest/operation-index.d.ts +0 -133
- package/dist/providers/_shared/rest/tools.d.ts +0 -141
- package/dist/providers/_shared/rest/tools.js +0 -569
- package/dist/providers/_shared/rest/value-safety.d.ts +0 -201
- package/dist/providers/basecamp/index.d.ts +0 -45
- package/dist/providers/basecamp/index.js +0 -997
- package/dist/providers/basecamp/skill.generated.js +0 -9
- package/dist/providers/cloudflare/rest.d.ts +0 -37
- package/dist/providers/cloudflare/rest.js +0 -1097
- package/dist/providers/github/hosted.js +0 -218
- package/dist/providers/github/rest.js +0 -321
- package/dist/providers/infisical/index.d.ts +0 -7
- package/dist/providers/infisical/index.js +0 -713
- package/dist/providers/infisical/skill.generated.js +0 -8
- package/dist/providers/intercom/index.d.ts +0 -24
- package/dist/providers/intercom/index.js +0 -121
- package/dist/providers/intercom/skill.generated.js +0 -8
- package/dist/providers/linear/index.d.ts +0 -55
- package/dist/providers/linear/index.js +0 -537
- package/dist/providers/linear/skill.generated.js +0 -8
- package/dist/providers/mixpanel/index.d.ts +0 -59
- package/dist/providers/mixpanel/index.js +0 -472
- package/dist/providers/mixpanel/operations.js +0 -20
- package/dist/providers/mixpanel/rest.js +0 -603
- package/dist/providers/mixpanel/skill.generated.js +0 -10
- package/dist/providers/mixpanel/value-safety.js +0 -16
- package/dist/providers/notion/index.d.ts +0 -67
- package/dist/providers/notion/index.js +0 -368
- package/dist/providers/notion/openapi.generated.js +0 -75
- package/dist/providers/notion/rest.d.ts +0 -24
- package/dist/providers/notion/rest.js +0 -1226
- package/dist/providers/notion/skill.generated.js +0 -11
- package/dist/providers/notion/value-safety.js +0 -56
- package/dist/providers/revenuecat/index.d.ts +0 -51
- package/dist/providers/revenuecat/index.js +0 -697
- package/dist/providers/revenuecat/skill.generated.js +0 -11
- package/dist/providers/stripe/rest.d.ts +0 -14
- package/dist/providers/stripe/rest.js +0 -402
- package/dist/providers/vercel/rest.d.ts +0 -20
- package/dist/providers/vercel/rest.js +0 -1020
- package/dist/providers/wispr-flow/index.d.ts +0 -14
- package/dist/providers/wispr-flow/index.js +0 -117
- package/dist/providers/wispr-flow/skill.generated.js +0 -8
- /package/dist/providers/github/{key.js → lib/key.js} +0 -0
|
@@ -2,12 +2,12 @@ import { skill } from "./skill.generated.js";
|
|
|
2
2
|
/**
|
|
3
3
|
* Google Sheets as the signed-in Workspace user: read a spreadsheet's shape
|
|
4
4
|
* and its values, write, append, and clear values, create spreadsheets and
|
|
5
|
-
* sheets, and
|
|
6
|
-
*
|
|
5
|
+
* sheets, and use indexed discovery, details, reads and writes for reviewed
|
|
6
|
+
* editor operations. Pinned against the Sheets API v4 Discovery contract
|
|
7
7
|
* (https://developers.google.com/workspace/sheets/api/reference/rest).
|
|
8
8
|
*
|
|
9
9
|
* Whose spreadsheets. Access is a service account with domain-wide delegation
|
|
10
|
-
* (`src/providers/_shared/google
|
|
10
|
+
* (`src/providers/_shared/google/`): deployment config maps the admitted
|
|
11
11
|
* identity to a Workspace address, and each call mints, or reuses, a token as
|
|
12
12
|
* that user. There is no per-user consent step, no operator credential slot,
|
|
13
13
|
* and no argument that names an account. Sheets has no `users/me` root to
|
|
@@ -19,1140 +19,27 @@ import { skill } from "./skill.generated.js";
|
|
|
19
19
|
* the Sheets API has no list method — so this connection starts from an id.
|
|
20
20
|
* It never moves, shares, or trashes a file, which are Drive's too.
|
|
21
21
|
*
|
|
22
|
-
* The safety classes. Reads are read-only.
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
22
|
+
* The safety classes. Reads are read-only. All writes use the host's
|
|
23
|
+
* destructive route. Named create/add/append catalog entries retain their
|
|
24
|
+
* explicit destructiveHint:false through the shared builder; approval
|
|
25
|
+
* routing is unchanged. Named append is pinned to `INSERT_ROWS`; hatch append
|
|
26
|
+
* may choose Google's `OVERWRITE`, which can replace cells below the table.
|
|
27
|
+
* Structural batches can delete sheets, rows and protections.
|
|
28
28
|
*
|
|
29
29
|
* Drift. Google publishes a credential-free Discovery document per API.
|
|
30
|
-
* `src/providers/sheets/drift.json` records
|
|
30
|
+
* `src/providers/sheets/drift.json` records all 17 indexed methods, including those the named tools
|
|
31
31
|
* call, and `pnpm run providers:check --provider sheets` reports a touched
|
|
32
32
|
* contract that moved or a method that stopped accepting the scope below.
|
|
33
33
|
*/
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
export const SHEETS_API_BASE_URL = "https://sheets.googleapis.com/v4";
|
|
44
|
-
/**
|
|
45
|
-
* Exactly the scope this connection requests, and what the Admin console's
|
|
46
|
-
* domain-wide delegation entry must add for it. One scope reads and writes
|
|
47
|
-
* every spreadsheet the user can open; Google offers no narrower one that
|
|
48
|
-
* writes.
|
|
49
|
-
*/
|
|
50
|
-
export const SHEETS_SCOPES = ["https://www.googleapis.com/auth/spreadsheets"];
|
|
51
|
-
/** Cells per page of get_values: connecta's cap, since Sheets has no paging. */
|
|
52
|
-
const DEFAULT_PAGE_CELLS = 2_000;
|
|
53
|
-
const MAX_PAGE_CELLS = 10_000;
|
|
54
|
-
/**
|
|
55
|
-
* Bytes per page of get_values as serialized, beside the cell count: a page
|
|
56
|
-
* has to be deliverable, and cells say nothing about size — 60 cells of 5,000
|
|
57
|
-
* characters already outgrow the QuickJS executor's host bridge. The default
|
|
58
|
-
* is the Workspace providers' shared result budget, so an execute_code
|
|
59
|
-
* program always receives a default page; unlike that budget's envelope
|
|
60
|
-
* allowance, maxBytes bounds the whole page, fields and cursor included. A
|
|
61
|
-
* direct call_tool caller may ask for more, up to 4 MiB: base64 in the result
|
|
62
|
-
* stash makes that about 5.6 MiB, under the stash's default 8 MiB, so an
|
|
63
|
-
* oversized page still pages through connecta.result rather than being dropped.
|
|
64
|
-
*/
|
|
65
|
-
const DEFAULT_PAGE_BYTES = RESULT_BUDGET_BYTES;
|
|
66
|
-
const MAX_PAGE_BYTES = 4 * 1024 * 1024;
|
|
67
|
-
/**
|
|
68
|
-
* Characters a read keeps per cell by default. A cell holds at most 50,000
|
|
69
|
-
* (Sheets' own limit), so `maxCellChars` at that ceiling always reads one whole.
|
|
70
|
-
*/
|
|
71
|
-
const DEFAULT_CELL_CHARS = 5_000;
|
|
72
|
-
const MAX_CELL_CHARS = 50_000;
|
|
73
|
-
/** Ranges per read or clear. */
|
|
74
|
-
const MAX_RANGES = 20;
|
|
75
|
-
/** Ranges per batch_update_values. */
|
|
76
|
-
const MAX_DATA_RANGES = 100;
|
|
77
|
-
/** Rows and cells one write may carry: connecta's cap, under Google's 10 MB request. */
|
|
78
|
-
const MAX_WRITE_ROWS = 10_000;
|
|
79
|
-
const MAX_WRITE_CELLS = 50_000;
|
|
80
|
-
/** Sheets' own column ceiling (ZZZ). */
|
|
81
|
-
const MAX_COLUMNS = 18_278;
|
|
82
|
-
/** Named ranges a metadata read lists before saying how many it left out. */
|
|
83
|
-
const MAX_NAMED_RANGES = 200;
|
|
84
|
-
/** Requests per raw batchUpdate. */
|
|
85
|
-
const MAX_BATCH_REQUESTS = 100;
|
|
86
|
-
/** A wide values read; Google itself recommends payloads under 2 MB. */
|
|
87
|
-
const SHEETS_MAX_RESPONSE_BYTES = 16 * 1024 * 1024;
|
|
88
|
-
function asRecord(value) {
|
|
89
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
90
|
-
}
|
|
91
|
-
function asArray(value) {
|
|
92
|
-
return Array.isArray(value) ? value : [];
|
|
93
|
-
}
|
|
94
|
-
function compact(value) {
|
|
95
|
-
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
96
|
-
}
|
|
97
|
-
function text(value) {
|
|
98
|
-
return typeof value === "string" && value !== "" ? value : undefined;
|
|
99
|
-
}
|
|
100
|
-
function integer(value) {
|
|
101
|
-
return typeof value === "number" && Number.isInteger(value) ? value : undefined;
|
|
102
|
-
}
|
|
103
|
-
/** A 0-based column index as its A1 letters: 0 → A, 26 → AA. */
|
|
104
|
-
function columnLetters(index) {
|
|
105
|
-
let letters = "";
|
|
106
|
-
for (let rest = index + 1; rest > 0; rest = Math.floor((rest - 1) / 26)) {
|
|
107
|
-
letters = String.fromCharCode(65 + ((rest - 1) % 26)) + letters;
|
|
108
|
-
}
|
|
109
|
-
return letters;
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* A sheet title as A1, always single-quoted. Bare, a whole-sheet range is
|
|
113
|
-
* indistinguishable from a named range of the same name — and Google resolves
|
|
114
|
-
* the named range first, so `Sheet1` handed to clear_values could clear
|
|
115
|
-
* something else. No named range can contain a quote, so quoted cannot.
|
|
116
|
-
*/
|
|
117
|
-
function sheetPrefix(title) {
|
|
118
|
-
return `'${title.replace(/'/g, "''")}'`;
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* A GridRange as A1, or `undefined` for a shape A1 cannot say. Google leaves
|
|
122
|
-
* a zero index out of the JSON, and a missing start index means "from the
|
|
123
|
-
* first", so both read as 0; a missing end means "to the edge".
|
|
124
|
-
*/
|
|
125
|
-
function gridRangeA1(range, title) {
|
|
126
|
-
if (title === undefined)
|
|
127
|
-
return undefined;
|
|
128
|
-
const startRow = integer(range["startRowIndex"]) ?? 0;
|
|
129
|
-
const startColumn = integer(range["startColumnIndex"]) ?? 0;
|
|
130
|
-
const endRow = integer(range["endRowIndex"]);
|
|
131
|
-
const endColumn = integer(range["endColumnIndex"]);
|
|
132
|
-
const prefix = sheetPrefix(title);
|
|
133
|
-
if (endRow === undefined && endColumn === undefined) {
|
|
134
|
-
return startRow === 0 && startColumn === 0 ? prefix : undefined;
|
|
135
|
-
}
|
|
136
|
-
if (endColumn === undefined) {
|
|
137
|
-
return startColumn === 0 ? `${prefix}!${startRow + 1}:${endRow}` : undefined;
|
|
138
|
-
}
|
|
139
|
-
const start = `${columnLetters(startColumn)}${startRow + 1}`;
|
|
140
|
-
return `${prefix}!${start}:${columnLetters(endColumn - 1)}${endRow ?? ""}`;
|
|
141
|
-
}
|
|
142
|
-
function base64Url(bytes) {
|
|
143
|
-
let binary = "";
|
|
144
|
-
for (const byte of bytes)
|
|
145
|
-
binary += String.fromCharCode(byte);
|
|
146
|
-
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
147
|
-
}
|
|
148
|
-
/** What a cursor is bound to: everything that decides which cells a page reads. */
|
|
149
|
-
async function readDigest(args) {
|
|
150
|
-
const bound = JSON.stringify([
|
|
151
|
-
args["spreadsheetId"],
|
|
152
|
-
args["ranges"],
|
|
153
|
-
args["valueRenderOption"] ?? null,
|
|
154
|
-
args["dateTimeRenderOption"] ?? null,
|
|
155
|
-
]);
|
|
156
|
-
const hash = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(bound)));
|
|
157
|
-
return base64Url(hash.subarray(0, 16));
|
|
158
|
-
}
|
|
159
|
-
function encodeCursor(cursor) {
|
|
160
|
-
return base64Url(new TextEncoder().encode(JSON.stringify(cursor)));
|
|
161
|
-
}
|
|
162
|
-
function decodeCursor(value, ranges, digest) {
|
|
163
|
-
const refuse = () => new ConnectorCallError("invalid_args", "cursor is not one get_values returned for this call. Pass page.nextCursor back unchanged, with the same spreadsheetId, ranges, and render options.");
|
|
164
|
-
let parsed;
|
|
165
|
-
try {
|
|
166
|
-
const base64 = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
167
|
-
const binary = atob(base64 + "=".repeat((4 - (base64.length % 4)) % 4));
|
|
168
|
-
parsed = JSON.parse(new TextDecoder().decode(Uint8Array.from(binary, (c) => c.charCodeAt(0))));
|
|
169
|
-
}
|
|
170
|
-
catch {
|
|
171
|
-
throw refuse();
|
|
172
|
-
}
|
|
173
|
-
const cursor = asRecord(parsed);
|
|
174
|
-
const index = integer(cursor["i"]);
|
|
175
|
-
const skip = integer(cursor["s"]);
|
|
176
|
-
if (Object.keys(cursor).length !== 3 ||
|
|
177
|
-
cursor["d"] !== digest ||
|
|
178
|
-
index === undefined ||
|
|
179
|
-
index < 0 ||
|
|
180
|
-
index >= ranges.length ||
|
|
181
|
-
skip === undefined ||
|
|
182
|
-
skip < 0) {
|
|
183
|
-
throw refuse();
|
|
184
|
-
}
|
|
185
|
-
return { d: digest, i: index, s: skip };
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* The caller's own range past its first `rows` rows, when the range says
|
|
189
|
-
* where it starts: `Sheet1!C5:F100` → `Sheet1!C8:F100`, `B:D` → `B4:D`. A
|
|
190
|
-
* sheet title, a named range, or R1C1 does not, and is resent whole with the
|
|
191
|
-
* rows skipped instead — correct, but it re-reads what earlier pages did.
|
|
192
|
-
*/
|
|
193
|
-
function rangeAfter(range, rows) {
|
|
194
|
-
if (rows === 0)
|
|
195
|
-
return range;
|
|
196
|
-
const match = /^(.*!)?([A-Z]+)(\d+)?:([A-Z]+)(\d+)?$/.exec(range);
|
|
197
|
-
if (!match)
|
|
198
|
-
return undefined;
|
|
199
|
-
const [, prefix = "", startColumn, startRow, endColumn, endRow] = match;
|
|
200
|
-
if (startRow === undefined && endRow !== undefined)
|
|
201
|
-
return undefined;
|
|
202
|
-
const first = Number(startRow ?? 1);
|
|
203
|
-
if (endRow !== undefined && first > Number(endRow))
|
|
204
|
-
return undefined;
|
|
205
|
-
const start = first + rows;
|
|
206
|
-
if (endRow !== undefined && start > Number(endRow))
|
|
207
|
-
return undefined;
|
|
208
|
-
return `${prefix}${startColumn}${start}:${endColumn}${endRow ?? ""}`;
|
|
209
|
-
}
|
|
210
|
-
/** A cell as read, cut at `max` characters with a marker saying how to read the rest. */
|
|
211
|
-
function cappedCell(value, max, cut) {
|
|
212
|
-
if (typeof value !== "string" || value.length <= max)
|
|
213
|
-
return value;
|
|
214
|
-
cut.cells += 1;
|
|
215
|
-
return `${value.slice(0, max)}[… ${value.length - max} more characters truncated; read this cell alone with maxCellChars up to ${MAX_CELL_CHARS} for the rest]`;
|
|
216
|
-
}
|
|
217
|
-
/** Refuse a row no page of `budget` bytes can hold, rather than overrun the cap. */
|
|
218
|
-
function rowTooLarge(range, row, needed, budget) {
|
|
219
|
-
return new ConnectorCallError("invalid_args", `Row ${row} of ${range} needs a page of ${needed} bytes with the page's own fields, more than this page's maxBytes of ${budget}; narrow the range's columns or lower maxCellChars${raiseBytes(needed)}.`);
|
|
220
|
-
}
|
|
221
|
-
/** Refuse a page too small for even one range's own fields. */
|
|
222
|
-
function pageTooSmall(range, needed, budget) {
|
|
223
|
-
return new ConnectorCallError("invalid_args", `A page reporting ${range} needs ${needed} bytes before any of its rows, more than this page's maxBytes of ${budget}; shorten the range${raiseBytes(needed)}.`);
|
|
224
|
-
}
|
|
225
|
-
function raiseBytes(needed) {
|
|
226
|
-
if (needed > MAX_PAGE_BYTES)
|
|
227
|
-
return "";
|
|
228
|
-
return needed <= DEFAULT_PAGE_BYTES
|
|
229
|
-
? `, or raise maxBytes to at least ${needed}`
|
|
230
|
-
: `, or raise maxBytes to at least ${needed} (above ${DEFAULT_PAGE_BYTES}, only for a direct call_tool read: execute_code cannot receive it)`;
|
|
231
|
-
}
|
|
232
|
-
/** Refuse a row no page of `budget` cells can hold, rather than overrun the cap. */
|
|
233
|
-
function rowTooWide(range, row, width, budget) {
|
|
234
|
-
const fix = width <= MAX_PAGE_CELLS
|
|
235
|
-
? `raise maxCells to at least ${width}, or narrow the range's columns`
|
|
236
|
-
: `narrow the range's columns; a page holds at most ${MAX_PAGE_CELLS} cells`;
|
|
237
|
-
return new ConnectorCallError("invalid_args", `Row ${row} of ${range} has ${width} cells, more than this page's maxCells of ${budget}; ${fix}.`);
|
|
238
|
-
}
|
|
239
|
-
// --- Writing values ---------------------------------------------------------------
|
|
240
|
-
/** Refuse a write past connecta's per-call cell cap before anything is sent. */
|
|
241
|
-
function checkWriteSize(blocks) {
|
|
242
|
-
let cells = 0;
|
|
243
|
-
for (const block of blocks)
|
|
244
|
-
for (const row of block)
|
|
245
|
-
cells += row.length;
|
|
246
|
-
if (cells > MAX_WRITE_CELLS) {
|
|
247
|
-
throw new ConnectorCallError("invalid_args", `This write carries ${cells} cells; connecta sends at most ${MAX_WRITE_CELLS} per call (Google's own request limit is larger). Split it into several calls. Nothing was written.`);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
function projectUpdate(value) {
|
|
251
|
-
const update = asRecord(value);
|
|
252
|
-
return compact({
|
|
253
|
-
updatedRange: text(update["updatedRange"]),
|
|
254
|
-
updatedRows: integer(update["updatedRows"]) ?? 0,
|
|
255
|
-
updatedColumns: integer(update["updatedColumns"]) ?? 0,
|
|
256
|
-
updatedCells: integer(update["updatedCells"]) ?? 0,
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* Every `Request` kind Sheets' batchUpdate accepts, from the Discovery
|
|
261
|
-
* document's `Request` schema at revision 20260930. A request must set exactly
|
|
262
|
-
* one of them; its body is Google's to validate. Too many to close in the
|
|
263
|
-
* schema without overrunning H7, so the handler checks the kind before
|
|
264
|
-
* anything is sent. A kind Google adds changes the batchUpdate contract digest,
|
|
265
|
-
* so the drift check surfaces it for this list rather than letting it pass.
|
|
266
|
-
*/
|
|
267
|
-
const REQUEST_KINDS = new Set([
|
|
268
|
-
"addBanding",
|
|
269
|
-
"addChart",
|
|
270
|
-
"addCommentReply",
|
|
271
|
-
"addConditionalFormatRule",
|
|
272
|
-
"addDataSource",
|
|
273
|
-
"addDimensionGroup",
|
|
274
|
-
"addFilterView",
|
|
275
|
-
"addNamedRange",
|
|
276
|
-
"addProtectedRange",
|
|
277
|
-
"addSheet",
|
|
278
|
-
"addSlicer",
|
|
279
|
-
"addTable",
|
|
280
|
-
"appendCells",
|
|
281
|
-
"appendDimension",
|
|
282
|
-
"autoFill",
|
|
283
|
-
"autoResizeDimensions",
|
|
284
|
-
"cancelDataSourceRefresh",
|
|
285
|
-
"clearBasicFilter",
|
|
286
|
-
"copyPaste",
|
|
287
|
-
"createDeveloperMetadata",
|
|
288
|
-
"cutPaste",
|
|
289
|
-
"deleteBanding",
|
|
290
|
-
"deleteComment",
|
|
291
|
-
"deleteCommentReply",
|
|
292
|
-
"deleteConditionalFormatRule",
|
|
293
|
-
"deleteDataSource",
|
|
294
|
-
"deleteDeveloperMetadata",
|
|
295
|
-
"deleteDimension",
|
|
296
|
-
"deleteDimensionGroup",
|
|
297
|
-
"deleteDuplicates",
|
|
298
|
-
"deleteEmbeddedObject",
|
|
299
|
-
"deleteFilterView",
|
|
300
|
-
"deleteNamedRange",
|
|
301
|
-
"deleteProtectedRange",
|
|
302
|
-
"deleteRange",
|
|
303
|
-
"deleteSheet",
|
|
304
|
-
"deleteTable",
|
|
305
|
-
"duplicateFilterView",
|
|
306
|
-
"duplicateSheet",
|
|
307
|
-
"findReplace",
|
|
308
|
-
"insertComment",
|
|
309
|
-
"insertDimension",
|
|
310
|
-
"insertRange",
|
|
311
|
-
"mergeCells",
|
|
312
|
-
"moveDimension",
|
|
313
|
-
"pasteData",
|
|
314
|
-
"randomizeRange",
|
|
315
|
-
"refreshDataSource",
|
|
316
|
-
"repeatCell",
|
|
317
|
-
"setBasicFilter",
|
|
318
|
-
"setDataValidation",
|
|
319
|
-
"sortRange",
|
|
320
|
-
"textToColumns",
|
|
321
|
-
"trimWhitespace",
|
|
322
|
-
"unmergeCells",
|
|
323
|
-
"updateBanding",
|
|
324
|
-
"updateBorders",
|
|
325
|
-
"updateCells",
|
|
326
|
-
"updateChartSpec",
|
|
327
|
-
"updateCommentPost",
|
|
328
|
-
"updateConditionalFormatRule",
|
|
329
|
-
"updateDataSource",
|
|
330
|
-
"updateDeveloperMetadata",
|
|
331
|
-
"updateDimensionGroup",
|
|
332
|
-
"updateDimensionProperties",
|
|
333
|
-
"updateEmbeddedObjectBorder",
|
|
334
|
-
"updateEmbeddedObjectPosition",
|
|
335
|
-
"updateFilterView",
|
|
336
|
-
"updateNamedRange",
|
|
337
|
-
"updateProtectedRange",
|
|
338
|
-
"updateSheetProperties",
|
|
339
|
-
"updateSlicerSpec",
|
|
340
|
-
"updateSpreadsheetProperties",
|
|
341
|
-
"updateTable",
|
|
342
|
-
]);
|
|
343
|
-
/** Refuse a raw request that is not exactly one known kind, unsent. */
|
|
344
|
-
function checkRequestKinds(requests) {
|
|
345
|
-
requests.forEach((request, index) => {
|
|
346
|
-
const kinds = Object.keys(asRecord(request));
|
|
347
|
-
if (kinds.length !== 1 || !REQUEST_KINDS.has(kinds[0])) {
|
|
348
|
-
throw new ConnectorCallError("invalid_args", `requests[${index}] must set exactly one Sheets Request kind (addSheet, repeatCell, deleteDimension, …); it sets ${kinds.length === 0 ? "none" : kinds.slice(0, 3).join(", ")}. Nothing was sent.`);
|
|
349
|
-
}
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
/**
|
|
353
|
-
* A write whose outcome is unknown — sent, and then anything but an answer
|
|
354
|
-
* Google refused it with: a reply that broke off, no reply, a redirect, or a
|
|
355
|
-
* 5xx after receiving it — told what to read before trying again, for the
|
|
356
|
-
* writes where the shared "re-read before repeating it" has nothing to re-read
|
|
357
|
-
* or where repeating does harm. The advice claims nothing either way; only
|
|
358
|
-
* the client's own message says "probably applied", and only for a 2xx whose
|
|
359
|
-
* body broke off. A refusal, and anything never sent, passes through as the
|
|
360
|
-
* client mapped it.
|
|
361
|
-
*/
|
|
362
|
-
async function uncertainWrite(advice, write) {
|
|
363
|
-
try {
|
|
364
|
-
return await write();
|
|
365
|
-
}
|
|
366
|
-
catch (error) {
|
|
367
|
-
const outcome = googleOutcomeOf(error);
|
|
368
|
-
if (!(error instanceof ConnectorCallError) || !outcome?.dispatched || outcome.phase === "refused") {
|
|
369
|
-
throw error;
|
|
370
|
-
}
|
|
371
|
-
throw new ConnectorCallError(error.code, `${error.message} ${advice}`, { retryable: false, cause: error });
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
// --- Raw batchUpdate replies -----------------------------------------------------
|
|
375
|
-
/** A string kept in a reply summary, cut so a long one cannot crowd out ids. */
|
|
376
|
-
const SUMMARY_TEXT_BYTES = 256;
|
|
377
|
-
/** The scalar fields of an object — ids, titles, counts — and nothing nested. */
|
|
378
|
-
function scalarsOf(value) {
|
|
379
|
-
const scalars = {};
|
|
380
|
-
for (const [key, field] of Object.entries(asRecord(value))) {
|
|
381
|
-
if (typeof field === "string") {
|
|
382
|
-
scalars[key] = clampText(field, SUMMARY_TEXT_BYTES, (dropped) => `[… ${dropped} more characters]`);
|
|
383
|
-
}
|
|
384
|
-
else if (typeof field === "number" || typeof field === "boolean") {
|
|
385
|
-
scalars[key] = field;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
return Object.keys(scalars).length > 0 ? scalars : undefined;
|
|
389
|
-
}
|
|
390
|
-
/**
|
|
391
|
-
* One reply reduced to its kind and the scalars an agent acts on next: a new
|
|
392
|
-
* sheet's id and title, a chart's id, findReplace's counts. Google's replies
|
|
393
|
-
* put those at the top of the kind's body or one object down (`properties`,
|
|
394
|
-
* `chart`, `namedRange`); arrays and anything deeper are dropped.
|
|
395
|
-
*/
|
|
396
|
-
function summarizeReply(reply) {
|
|
397
|
-
const kind = Object.keys(reply)[0];
|
|
398
|
-
if (kind === undefined)
|
|
399
|
-
return {};
|
|
400
|
-
const body = asRecord(reply[kind]);
|
|
401
|
-
const nested = {};
|
|
402
|
-
for (const [key, field] of Object.entries(body)) {
|
|
403
|
-
if (field && typeof field === "object" && !Array.isArray(field))
|
|
404
|
-
nested[key] = scalarsOf(field);
|
|
405
|
-
}
|
|
406
|
-
return compact({ kind, ...scalarsOf(body), ...compact(nested) });
|
|
407
|
-
}
|
|
408
|
-
/**
|
|
409
|
-
* A batchUpdate result that can be delivered. The write has already applied,
|
|
410
|
-
* so whatever does not fit is summarized, never dropped silently and never
|
|
411
|
-
* reported as a failure: replies are kept whole, in order, while the ones
|
|
412
|
-
* after them still fit as summaries; the rest become summaries; and a reply
|
|
413
|
-
* set too large even for that leaves the counts and where to re-read.
|
|
414
|
-
*/
|
|
415
|
-
function deliverableReplies(spreadsheetId, replies, requestCount) {
|
|
416
|
-
const whole = { spreadsheetId, replies };
|
|
417
|
-
if (jsonBytes(whole) <= RESULT_BUDGET_BYTES)
|
|
418
|
-
return whole;
|
|
419
|
-
const note = `All ${requestCount} requests applied. Replies too large to return here are cut to their kind, ids, and counts; read the spreadsheet with get_spreadsheet or get_values for the rest. Do not send the requests again.`;
|
|
420
|
-
const summaries = replies.map(summarizeReply);
|
|
421
|
-
// What every reply from index i on costs as a summary, comma included.
|
|
422
|
-
const rest = Array.from({ length: replies.length + 1 }, () => 0);
|
|
423
|
-
for (let index = replies.length - 1; index >= 0; index -= 1) {
|
|
424
|
-
rest[index] = rest[index + 1] + jsonBytes(summaries[index]) + 1;
|
|
425
|
-
}
|
|
426
|
-
let bytes = jsonBytes({ spreadsheetId, replies: [], repliesSummarized: replies.length, note });
|
|
427
|
-
if (bytes + rest[0] <= RESULT_BUDGET_BYTES) {
|
|
428
|
-
const kept = [];
|
|
429
|
-
let summarized = 0;
|
|
430
|
-
replies.forEach((reply, index) => {
|
|
431
|
-
const size = jsonBytes(reply) + 1;
|
|
432
|
-
if (summarized === 0 && bytes + size + rest[index + 1] <= RESULT_BUDGET_BYTES) {
|
|
433
|
-
bytes += size;
|
|
434
|
-
kept.push(reply);
|
|
435
|
-
}
|
|
436
|
-
else {
|
|
437
|
-
summarized += 1;
|
|
438
|
-
kept.push(summaries[index]);
|
|
439
|
-
}
|
|
440
|
-
});
|
|
441
|
-
return { spreadsheetId, replies: kept, repliesSummarized: summarized, note };
|
|
442
|
-
}
|
|
443
|
-
return {
|
|
444
|
-
spreadsheetId,
|
|
445
|
-
replies: [],
|
|
446
|
-
repliesOmitted: replies.length,
|
|
447
|
-
note: `All ${requestCount} requests applied, but their ${replies.length} replies are too large to return even summarized. Read the spreadsheet with get_spreadsheet or get_values to see the result. Do not send the requests again.`,
|
|
448
|
-
};
|
|
449
|
-
}
|
|
450
|
-
// --- Schemas ----------------------------------------------------------------------
|
|
451
|
-
function input(properties, required) {
|
|
452
|
-
return { type: "object", properties, required, additionalProperties: false };
|
|
453
|
-
}
|
|
454
|
-
const SPREADSHEET_ID_PROPERTY = {
|
|
455
|
-
type: "string",
|
|
456
|
-
minLength: 1,
|
|
457
|
-
maxLength: 256,
|
|
458
|
-
pattern: "^[A-Za-z0-9_-]+$",
|
|
459
|
-
description: "Spreadsheet id: the part of its URL between /d/ and /edit. Find one by name with a Drive search; Sheets cannot list.",
|
|
460
|
-
};
|
|
461
|
-
/**
|
|
462
|
-
* One line, at least one character that is not a dot: a range is a path
|
|
463
|
-
* segment for append, and `.` or `..` there would climb out of it.
|
|
464
|
-
*/
|
|
465
|
-
const RANGE_PATTERN = "^[^\\r\\n]*[^.\\r\\n][^\\r\\n]*$";
|
|
466
|
-
function rangeProperty(description) {
|
|
467
|
-
return { type: "string", minLength: 1, maxLength: 512, pattern: RANGE_PATTERN, description };
|
|
468
|
-
}
|
|
469
|
-
const A1_HELP = "A1 notation: Sheet1!A1:D20, 'Q3 Budget'!B:B, a sheet title alone, or a named range";
|
|
470
|
-
const RANGES_PROPERTY = {
|
|
471
|
-
type: "array",
|
|
472
|
-
minItems: 1,
|
|
473
|
-
maxItems: MAX_RANGES,
|
|
474
|
-
items: rangeProperty(`${A1_HELP}.`),
|
|
475
|
-
description: `Ranges to read, 1 to ${MAX_RANGES}, returned in this order.`,
|
|
476
|
-
};
|
|
477
|
-
const VALUE_INPUT_PROPERTY = {
|
|
478
|
-
type: "string",
|
|
479
|
-
enum: ["RAW", "USER_ENTERED"],
|
|
480
|
-
description: "RAW stores each value literally; USER_ENTERED parses it as if typed, so =SUM(A1:A3) is a formula and 2026-10-06 a date.",
|
|
481
|
-
};
|
|
482
|
-
const ROWS_PROPERTY = {
|
|
483
|
-
type: "array",
|
|
484
|
-
minItems: 1,
|
|
485
|
-
maxItems: MAX_WRITE_ROWS,
|
|
486
|
-
items: {
|
|
487
|
-
type: "array",
|
|
488
|
-
maxItems: MAX_COLUMNS,
|
|
489
|
-
items: { type: ["string", "number", "boolean", "null"], maxLength: 50_000 },
|
|
490
|
-
description: "One row's cells, left to right.",
|
|
491
|
-
},
|
|
492
|
-
description: `Rows of cells, top to bottom. null leaves a cell unchanged; "" empties it. At most ${MAX_WRITE_CELLS} cells per call, connecta's cap.`,
|
|
493
|
-
};
|
|
494
|
-
const PAGE_SCHEMA = {
|
|
495
|
-
type: "object",
|
|
496
|
-
properties: {
|
|
497
|
-
hasMore: { type: "boolean" },
|
|
498
|
-
nextCursor: { type: ["string", "null"] },
|
|
499
|
-
},
|
|
500
|
-
required: ["hasMore", "nextCursor"],
|
|
501
|
-
};
|
|
502
|
-
const CELL_SCHEMA = { type: ["string", "number", "boolean"] };
|
|
503
|
-
const UPDATE_SCHEMA = {
|
|
504
|
-
type: "object",
|
|
505
|
-
properties: {
|
|
506
|
-
updatedRange: { type: "string" },
|
|
507
|
-
updatedRows: { type: "integer" },
|
|
508
|
-
updatedColumns: { type: "integer" },
|
|
509
|
-
updatedCells: { type: "integer" },
|
|
510
|
-
},
|
|
511
|
-
};
|
|
512
|
-
const SHEET_SCHEMA = {
|
|
513
|
-
type: "object",
|
|
514
|
-
properties: {
|
|
515
|
-
sheetId: { type: "integer" },
|
|
516
|
-
title: { type: "string" },
|
|
517
|
-
index: { type: "integer" },
|
|
518
|
-
type: { type: "string" },
|
|
519
|
-
hidden: { type: "boolean" },
|
|
520
|
-
rowCount: { type: "integer" },
|
|
521
|
-
columnCount: { type: "integer" },
|
|
522
|
-
frozenRowCount: { type: "integer" },
|
|
523
|
-
frozenColumnCount: { type: "integer" },
|
|
524
|
-
},
|
|
525
|
-
};
|
|
526
|
-
function projectSheet(value) {
|
|
527
|
-
const properties = asRecord(asRecord(value)["properties"]);
|
|
528
|
-
const grid = asRecord(properties["gridProperties"]);
|
|
529
|
-
return compact({
|
|
530
|
-
// The first sheet's id is 0, which Google leaves out of the JSON.
|
|
531
|
-
sheetId: integer(properties["sheetId"]) ?? 0,
|
|
532
|
-
title: text(properties["title"]),
|
|
533
|
-
index: integer(properties["index"]) ?? 0,
|
|
534
|
-
type: text(properties["sheetType"]),
|
|
535
|
-
hidden: properties["hidden"] === true ? true : undefined,
|
|
536
|
-
rowCount: integer(grid["rowCount"]),
|
|
537
|
-
columnCount: integer(grid["columnCount"]),
|
|
538
|
-
frozenRowCount: integer(grid["frozenRowCount"]),
|
|
539
|
-
frozenColumnCount: integer(grid["frozenColumnCount"]),
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
/** The metadata get_spreadsheet reads: no grid data, no formatting. */
|
|
543
|
-
const SPREADSHEET_FIELDS = [
|
|
544
|
-
"spreadsheetId",
|
|
545
|
-
"spreadsheetUrl",
|
|
546
|
-
"properties(title,locale,timeZone)",
|
|
547
|
-
"sheets.properties(sheetId,title,index,sheetType,hidden,gridProperties(rowCount,columnCount,frozenRowCount,frozenColumnCount))",
|
|
548
|
-
"namedRanges(namedRangeId,name,range)",
|
|
549
|
-
].join(",");
|
|
550
|
-
// --- Tools ------------------------------------------------------------------------
|
|
551
|
-
function tools(client) {
|
|
552
|
-
const readOnly = { readOnlyHint: true };
|
|
553
|
-
// Additive: nothing that existed is lost. Writes require a writable
|
|
554
|
-
// endpoint: call_destructive_tool or a program. The host controls approval.
|
|
555
|
-
const additive = { readOnlyHint: false, destructiveHint: false };
|
|
556
|
-
const destructive = { readOnlyHint: false, destructiveHint: true };
|
|
557
|
-
const spreadsheetPath = (args) => `/spreadsheets/${encodeURIComponent(String(args["spreadsheetId"]))}`;
|
|
558
|
-
return [
|
|
559
|
-
{
|
|
560
|
-
name: "get_spreadsheet",
|
|
561
|
-
description: "Get a spreadsheet's title, sheets (ids, titles, grid sizes), and named ranges as A1, never cell values. To find one by name, search Drive; Sheets cannot list.",
|
|
562
|
-
annotations: readOnly,
|
|
563
|
-
inputSchema: input({
|
|
564
|
-
spreadsheetId: SPREADSHEET_ID_PROPERTY,
|
|
565
|
-
raw: {
|
|
566
|
-
type: "boolean",
|
|
567
|
-
description: "Return Google's untouched spreadsheet metadata — formatting, protections, charts — instead of the projection. Never cell values.",
|
|
568
|
-
},
|
|
569
|
-
}, ["spreadsheetId"]),
|
|
570
|
-
outputSchema: {
|
|
571
|
-
type: "object",
|
|
572
|
-
properties: {
|
|
573
|
-
spreadsheetId: { type: "string" },
|
|
574
|
-
title: { type: "string" },
|
|
575
|
-
url: { type: "string" },
|
|
576
|
-
locale: { type: "string" },
|
|
577
|
-
timeZone: { type: "string" },
|
|
578
|
-
sheets: { type: "array", items: SHEET_SCHEMA },
|
|
579
|
-
namedRanges: {
|
|
580
|
-
type: "array",
|
|
581
|
-
items: {
|
|
582
|
-
type: "object",
|
|
583
|
-
properties: {
|
|
584
|
-
namedRangeId: { type: "string" },
|
|
585
|
-
name: { type: "string" },
|
|
586
|
-
sheetId: { type: "integer" },
|
|
587
|
-
range: { type: "string" },
|
|
588
|
-
},
|
|
589
|
-
},
|
|
590
|
-
},
|
|
591
|
-
namedRangesOmitted: { type: "integer" },
|
|
592
|
-
},
|
|
593
|
-
required: ["spreadsheetId", "sheets"],
|
|
594
|
-
},
|
|
595
|
-
handler: async (args, ctx) => {
|
|
596
|
-
if (args["raw"] === true) {
|
|
597
|
-
return await client.json({ method: "GET", path: spreadsheetPath(args), query: { includeGridData: false } }, ctx);
|
|
598
|
-
}
|
|
599
|
-
const spreadsheet = asRecord(await client.json({ method: "GET", path: spreadsheetPath(args), query: { fields: SPREADSHEET_FIELDS } }, ctx));
|
|
600
|
-
const properties = asRecord(spreadsheet["properties"]);
|
|
601
|
-
const sheets = asArray(spreadsheet["sheets"]).map(projectSheet);
|
|
602
|
-
const titles = new Map(sheets.map((sheet) => [sheet["sheetId"], sheet["title"]]));
|
|
603
|
-
const named = asArray(spreadsheet["namedRanges"]);
|
|
604
|
-
return compact({
|
|
605
|
-
spreadsheetId: text(spreadsheet["spreadsheetId"]) ?? args["spreadsheetId"],
|
|
606
|
-
title: text(properties["title"]),
|
|
607
|
-
url: text(spreadsheet["spreadsheetUrl"]),
|
|
608
|
-
locale: text(properties["locale"]),
|
|
609
|
-
timeZone: text(properties["timeZone"]),
|
|
610
|
-
sheets,
|
|
611
|
-
namedRanges: named.length > 0
|
|
612
|
-
? named.slice(0, MAX_NAMED_RANGES).map((value) => {
|
|
613
|
-
const entry = asRecord(value);
|
|
614
|
-
const range = asRecord(entry["range"]);
|
|
615
|
-
const sheetId = integer(range["sheetId"]) ?? 0;
|
|
616
|
-
return compact({
|
|
617
|
-
namedRangeId: text(entry["namedRangeId"]),
|
|
618
|
-
name: text(entry["name"]),
|
|
619
|
-
sheetId,
|
|
620
|
-
range: gridRangeA1(range, titles.get(sheetId)),
|
|
621
|
-
});
|
|
622
|
-
})
|
|
623
|
-
: undefined,
|
|
624
|
-
namedRangesOmitted: named.length > MAX_NAMED_RANGES ? named.length - MAX_NAMED_RANGES : undefined,
|
|
625
|
-
});
|
|
626
|
-
},
|
|
627
|
-
},
|
|
628
|
-
{
|
|
629
|
-
name: "get_values",
|
|
630
|
-
description: "Read cell values from one or more A1 ranges as rows, paged by cell count. Long cells are cut with a marker; no formatting, notes, or formulas unless asked.",
|
|
631
|
-
annotations: readOnly,
|
|
632
|
-
inputSchema: input({
|
|
633
|
-
spreadsheetId: SPREADSHEET_ID_PROPERTY,
|
|
634
|
-
ranges: RANGES_PROPERTY,
|
|
635
|
-
valueRenderOption: {
|
|
636
|
-
type: "string",
|
|
637
|
-
enum: ["FORMATTED_VALUE", "UNFORMATTED_VALUE", "FORMULA"],
|
|
638
|
-
description: "FORMATTED_VALUE (default) as displayed; UNFORMATTED_VALUE as raw numbers; FORMULA shows formulas instead of results.",
|
|
639
|
-
},
|
|
640
|
-
dateTimeRenderOption: {
|
|
641
|
-
type: "string",
|
|
642
|
-
enum: ["SERIAL_NUMBER", "FORMATTED_STRING"],
|
|
643
|
-
description: "Dates when valueRenderOption is not FORMATTED_VALUE: SERIAL_NUMBER (Google's default, days since 1899-12-30) or FORMATTED_STRING.",
|
|
644
|
-
},
|
|
645
|
-
maxCells: {
|
|
646
|
-
type: "integer",
|
|
647
|
-
minimum: 1,
|
|
648
|
-
maximum: MAX_PAGE_CELLS,
|
|
649
|
-
description: `Cells per page, 1 to ${MAX_PAGE_CELLS}; defaults to ${DEFAULT_PAGE_CELLS}. Connecta's cap: Sheets returns whole ranges, so this pages by whole rows.`,
|
|
650
|
-
},
|
|
651
|
-
maxBytes: {
|
|
652
|
-
type: "integer",
|
|
653
|
-
minimum: 1_024,
|
|
654
|
-
maximum: MAX_PAGE_BYTES,
|
|
655
|
-
description: `Bytes of the whole page as JSON; defaults to ${DEFAULT_PAGE_BYTES}, what execute_code can receive. Raise it, to ${MAX_PAGE_BYTES}, only for a direct call_tool read.`,
|
|
656
|
-
},
|
|
657
|
-
maxCellChars: {
|
|
658
|
-
type: "integer",
|
|
659
|
-
minimum: 1,
|
|
660
|
-
maximum: MAX_CELL_CHARS,
|
|
661
|
-
description: `Characters kept per cell, to ${MAX_CELL_CHARS} (Sheets' cell limit, so the whole cell); defaults to ${DEFAULT_CELL_CHARS}. Longer cells end with a marker.`,
|
|
662
|
-
},
|
|
663
|
-
cursor: {
|
|
664
|
-
type: "string",
|
|
665
|
-
minLength: 1,
|
|
666
|
-
maxLength: 1024,
|
|
667
|
-
description: "Opaque page.nextCursor from the previous page. Pass it back unchanged with the same spreadsheetId, ranges, and render options.",
|
|
668
|
-
},
|
|
669
|
-
}, ["spreadsheetId", "ranges"]),
|
|
670
|
-
outputSchema: {
|
|
671
|
-
type: "object",
|
|
672
|
-
properties: {
|
|
673
|
-
spreadsheetId: { type: "string" },
|
|
674
|
-
valueRanges: {
|
|
675
|
-
type: "array",
|
|
676
|
-
items: {
|
|
677
|
-
type: "object",
|
|
678
|
-
properties: {
|
|
679
|
-
range: { type: "string" },
|
|
680
|
-
values: { type: "array", items: { type: "array", items: CELL_SCHEMA } },
|
|
681
|
-
rowOffset: { type: "integer" },
|
|
682
|
-
rowCount: { type: "integer" },
|
|
683
|
-
truncated: { type: "boolean" },
|
|
684
|
-
omittedRows: { type: "integer" },
|
|
685
|
-
},
|
|
686
|
-
},
|
|
687
|
-
},
|
|
688
|
-
cellCount: { type: "integer" },
|
|
689
|
-
truncatedCells: { type: "integer" },
|
|
690
|
-
page: PAGE_SCHEMA,
|
|
691
|
-
},
|
|
692
|
-
required: ["spreadsheetId", "valueRanges", "page"],
|
|
693
|
-
},
|
|
694
|
-
handler: async (args, ctx) => {
|
|
695
|
-
const ranges = args["ranges"];
|
|
696
|
-
const budget = args["maxCells"] ?? DEFAULT_PAGE_CELLS;
|
|
697
|
-
const byteBudget = args["maxBytes"] ?? DEFAULT_PAGE_BYTES;
|
|
698
|
-
const cellChars = args["maxCellChars"] ?? DEFAULT_CELL_CHARS;
|
|
699
|
-
const digest = await readDigest(args);
|
|
700
|
-
const resume = typeof args["cursor"] === "string" ? decodeCursor(args["cursor"], ranges, digest) : { d: digest, i: 0, s: 0 };
|
|
701
|
-
// Every range sent is the caller's own, or the caller's own rewritten
|
|
702
|
-
// to start further down; nothing in the cursor names a range.
|
|
703
|
-
const rewritten = rangeAfter(ranges[resume.i], resume.s);
|
|
704
|
-
const sent = [rewritten ?? ranges[resume.i], ...ranges.slice(resume.i + 1)];
|
|
705
|
-
const skip = rewritten === undefined ? resume.s : 0;
|
|
706
|
-
const response = asRecord(await client.json({
|
|
707
|
-
method: "GET",
|
|
708
|
-
path: `${spreadsheetPath(args)}/values:batchGet`,
|
|
709
|
-
query: {
|
|
710
|
-
ranges: sent,
|
|
711
|
-
majorDimension: "ROWS",
|
|
712
|
-
valueRenderOption: args["valueRenderOption"],
|
|
713
|
-
dateTimeRenderOption: args["dateTimeRenderOption"],
|
|
714
|
-
},
|
|
715
|
-
}, ctx));
|
|
716
|
-
const spreadsheetId = text(response["spreadsheetId"]) ?? args["spreadsheetId"];
|
|
717
|
-
const valueRanges = [];
|
|
718
|
-
const cut = { cells: 0 };
|
|
719
|
-
let cells = 0;
|
|
720
|
-
// maxBytes bounds the whole page as serialized, not just its cells. The
|
|
721
|
-
// envelope is reserved first at its largest: both counts at their
|
|
722
|
-
// ceiling and the longest cursor this call could end with.
|
|
723
|
-
let bytes = jsonBytes({
|
|
724
|
-
spreadsheetId,
|
|
725
|
-
valueRanges: [],
|
|
726
|
-
cellCount: MAX_PAGE_CELLS,
|
|
727
|
-
truncatedCells: MAX_PAGE_CELLS,
|
|
728
|
-
page: {
|
|
729
|
-
hasMore: false,
|
|
730
|
-
nextCursor: encodeCursor({ d: digest, i: ranges.length - 1, s: Number.MAX_SAFE_INTEGER }),
|
|
731
|
-
},
|
|
732
|
-
});
|
|
733
|
-
let next;
|
|
734
|
-
const returned = asArray(response["valueRanges"]);
|
|
735
|
-
for (let index = 0; index < returned.length && next === undefined; index += 1) {
|
|
736
|
-
const valueRange = asRecord(returned[index]);
|
|
737
|
-
// Rows of the caller's range earlier pages returned, and of those,
|
|
738
|
-
// how many Google sent again and this page drops.
|
|
739
|
-
const before = index === 0 ? resume.s : 0;
|
|
740
|
-
const dropped = index === 0 ? skip : 0;
|
|
741
|
-
const rows = asArray(valueRange["values"]).slice(dropped);
|
|
742
|
-
const echoed = text(valueRange["range"]) ?? sent[index];
|
|
743
|
-
const entry = (kept, truncated) => compact({
|
|
744
|
-
range: echoed,
|
|
745
|
-
rowOffset: dropped > 0 ? dropped : undefined,
|
|
746
|
-
values: kept,
|
|
747
|
-
rowCount: kept.length,
|
|
748
|
-
truncated: truncated ? true : undefined,
|
|
749
|
-
omittedRows: truncated ? rows.length - kept.length : undefined,
|
|
750
|
-
});
|
|
751
|
-
// This range's own fields at their largest, and the comma before it;
|
|
752
|
-
// an empty range costs them too.
|
|
753
|
-
const fields = jsonBytes(compact({
|
|
754
|
-
range: echoed,
|
|
755
|
-
rowOffset: dropped > 0 ? dropped : undefined,
|
|
756
|
-
values: [],
|
|
757
|
-
rowCount: rows.length,
|
|
758
|
-
truncated: true,
|
|
759
|
-
omittedRows: rows.length,
|
|
760
|
-
})) + 1;
|
|
761
|
-
if (bytes + fields > byteBudget) {
|
|
762
|
-
if (valueRanges.length === 0)
|
|
763
|
-
throw pageTooSmall(echoed, bytes + fields, byteBudget);
|
|
764
|
-
next = { d: digest, i: resume.i + index, s: before };
|
|
765
|
-
break;
|
|
766
|
-
}
|
|
767
|
-
bytes += fields;
|
|
768
|
-
const kept = [];
|
|
769
|
-
for (const row of rows) {
|
|
770
|
-
const empty = valueRanges.length === 0 && kept.length === 0;
|
|
771
|
-
// An empty row still costs one cell, so every page makes progress.
|
|
772
|
-
const width = Math.max(1, asArray(row).length);
|
|
773
|
-
if (cells + width > budget) {
|
|
774
|
-
if (cells === 0)
|
|
775
|
-
throw rowTooWide(echoed, dropped + kept.length + 1, width, budget);
|
|
776
|
-
break;
|
|
777
|
-
}
|
|
778
|
-
const rowCut = { cells: 0 };
|
|
779
|
-
const capped = asArray(row).map((cell) => cappedCell(cell, cellChars, rowCut));
|
|
780
|
-
// The row as serialized, plus the comma before it.
|
|
781
|
-
const size = jsonBytes(capped) + 1;
|
|
782
|
-
if (bytes + size > byteBudget) {
|
|
783
|
-
if (empty)
|
|
784
|
-
throw rowTooLarge(echoed, dropped + 1, bytes + size, byteBudget);
|
|
785
|
-
break;
|
|
786
|
-
}
|
|
787
|
-
cells += width;
|
|
788
|
-
bytes += size;
|
|
789
|
-
cut.cells += rowCut.cells;
|
|
790
|
-
kept.push(capped);
|
|
791
|
-
}
|
|
792
|
-
if (kept.length < rows.length) {
|
|
793
|
-
next = { d: digest, i: resume.i + index, s: before + kept.length };
|
|
794
|
-
// A range this page never reached is not reported half-read; the
|
|
795
|
-
// next page starts it.
|
|
796
|
-
if (kept.length > 0)
|
|
797
|
-
valueRanges.push(entry(kept, true));
|
|
798
|
-
break;
|
|
799
|
-
}
|
|
800
|
-
valueRanges.push(entry(kept, false));
|
|
801
|
-
if (cells >= budget && index + 1 < returned.length) {
|
|
802
|
-
next = { d: digest, i: resume.i + index + 1, s: 0 };
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
const nextCursor = next ? encodeCursor(next) : null;
|
|
806
|
-
return compact({
|
|
807
|
-
spreadsheetId,
|
|
808
|
-
valueRanges,
|
|
809
|
-
cellCount: cells,
|
|
810
|
-
truncatedCells: cut.cells > 0 ? cut.cells : undefined,
|
|
811
|
-
page: { hasMore: nextCursor !== null, nextCursor },
|
|
812
|
-
});
|
|
813
|
-
},
|
|
814
|
-
},
|
|
815
|
-
{
|
|
816
|
-
name: "update_values",
|
|
817
|
-
description: "Write rows of values into one A1 range, overwriting what is there. Cells past the given rows are untouched; to add rows below a table use append_values.",
|
|
818
|
-
annotations: destructive,
|
|
819
|
-
inputSchema: input({
|
|
820
|
-
spreadsheetId: SPREADSHEET_ID_PROPERTY,
|
|
821
|
-
range: rangeProperty(`${A1_HELP}; the top-left cell is where the first value goes.`),
|
|
822
|
-
values: ROWS_PROPERTY,
|
|
823
|
-
valueInputOption: VALUE_INPUT_PROPERTY,
|
|
824
|
-
}, ["spreadsheetId", "range", "values", "valueInputOption"]),
|
|
825
|
-
outputSchema: {
|
|
826
|
-
type: "object",
|
|
827
|
-
properties: { spreadsheetId: { type: "string" }, ...UPDATE_SCHEMA.properties },
|
|
828
|
-
required: ["spreadsheetId"],
|
|
829
|
-
},
|
|
830
|
-
handler: async (args, ctx) => {
|
|
831
|
-
checkWriteSize([args["values"]]);
|
|
832
|
-
const response = asRecord(await client.json({
|
|
833
|
-
method: "POST",
|
|
834
|
-
path: `${spreadsheetPath(args)}/values:batchUpdate`,
|
|
835
|
-
body: {
|
|
836
|
-
valueInputOption: args["valueInputOption"],
|
|
837
|
-
data: [{ range: args["range"], majorDimension: "ROWS", values: args["values"] }],
|
|
838
|
-
includeValuesInResponse: false,
|
|
839
|
-
},
|
|
840
|
-
}, ctx,
|
|
841
|
-
// The same cells set to the same values twice is one write: a 5xx
|
|
842
|
-
// stays retryable.
|
|
843
|
-
{ idempotent: true }));
|
|
844
|
-
return {
|
|
845
|
-
spreadsheetId: text(response["spreadsheetId"]) ?? args["spreadsheetId"],
|
|
846
|
-
...projectUpdate(asArray(response["responses"])[0]),
|
|
847
|
-
};
|
|
848
|
-
},
|
|
849
|
-
},
|
|
850
|
-
{
|
|
851
|
-
name: "batch_update_values",
|
|
852
|
-
description: "Write values into several A1 ranges in one call, overwriting each. All succeed or none do. For formatting or structure use batch_update_spreadsheet.",
|
|
853
|
-
annotations: destructive,
|
|
854
|
-
inputSchema: input({
|
|
855
|
-
spreadsheetId: SPREADSHEET_ID_PROPERTY,
|
|
856
|
-
data: {
|
|
857
|
-
type: "array",
|
|
858
|
-
minItems: 1,
|
|
859
|
-
maxItems: MAX_DATA_RANGES,
|
|
860
|
-
items: {
|
|
861
|
-
type: "object",
|
|
862
|
-
properties: {
|
|
863
|
-
range: rangeProperty(`${A1_HELP}.`),
|
|
864
|
-
values: ROWS_PROPERTY,
|
|
865
|
-
},
|
|
866
|
-
required: ["range", "values"],
|
|
867
|
-
additionalProperties: false,
|
|
868
|
-
},
|
|
869
|
-
description: `Ranges and their rows, 1 to ${MAX_DATA_RANGES}.`,
|
|
870
|
-
},
|
|
871
|
-
valueInputOption: VALUE_INPUT_PROPERTY,
|
|
872
|
-
}, ["spreadsheetId", "data", "valueInputOption"]),
|
|
873
|
-
outputSchema: {
|
|
874
|
-
type: "object",
|
|
875
|
-
properties: {
|
|
876
|
-
spreadsheetId: { type: "string" },
|
|
877
|
-
totalUpdatedRows: { type: "integer" },
|
|
878
|
-
totalUpdatedColumns: { type: "integer" },
|
|
879
|
-
totalUpdatedCells: { type: "integer" },
|
|
880
|
-
totalUpdatedSheets: { type: "integer" },
|
|
881
|
-
updates: { type: "array", items: UPDATE_SCHEMA },
|
|
882
|
-
},
|
|
883
|
-
required: ["spreadsheetId", "updates"],
|
|
884
|
-
},
|
|
885
|
-
handler: async (args, ctx) => {
|
|
886
|
-
const data = asArray(args["data"]).map(asRecord);
|
|
887
|
-
checkWriteSize(data.map((entry) => entry["values"]));
|
|
888
|
-
const response = asRecord(await client.json({
|
|
889
|
-
method: "POST",
|
|
890
|
-
path: `${spreadsheetPath(args)}/values:batchUpdate`,
|
|
891
|
-
body: {
|
|
892
|
-
valueInputOption: args["valueInputOption"],
|
|
893
|
-
data: data.map((entry) => ({ range: entry["range"], majorDimension: "ROWS", values: entry["values"] })),
|
|
894
|
-
includeValuesInResponse: false,
|
|
895
|
-
},
|
|
896
|
-
}, ctx, { idempotent: true }));
|
|
897
|
-
return {
|
|
898
|
-
spreadsheetId: text(response["spreadsheetId"]) ?? args["spreadsheetId"],
|
|
899
|
-
totalUpdatedRows: integer(response["totalUpdatedRows"]) ?? 0,
|
|
900
|
-
totalUpdatedColumns: integer(response["totalUpdatedColumns"]) ?? 0,
|
|
901
|
-
totalUpdatedCells: integer(response["totalUpdatedCells"]) ?? 0,
|
|
902
|
-
totalUpdatedSheets: integer(response["totalUpdatedSheets"]) ?? 0,
|
|
903
|
-
updates: asArray(response["responses"]).map(projectUpdate),
|
|
904
|
-
};
|
|
905
|
-
},
|
|
906
|
-
},
|
|
907
|
-
{
|
|
908
|
-
name: "append_values",
|
|
909
|
-
description: "Append rows after the last row of the table in an A1 range, inserting new rows so nothing below is overwritten. Returns where they landed.",
|
|
910
|
-
annotations: additive,
|
|
911
|
-
inputSchema: input({
|
|
912
|
-
spreadsheetId: SPREADSHEET_ID_PROPERTY,
|
|
913
|
-
range: rangeProperty(`${A1_HELP}; Sheets finds the table in it and appends below its last row.`),
|
|
914
|
-
values: ROWS_PROPERTY,
|
|
915
|
-
valueInputOption: VALUE_INPUT_PROPERTY,
|
|
916
|
-
}, ["spreadsheetId", "range", "values", "valueInputOption"]),
|
|
917
|
-
outputSchema: {
|
|
918
|
-
type: "object",
|
|
919
|
-
properties: {
|
|
920
|
-
spreadsheetId: { type: "string" },
|
|
921
|
-
tableRange: { type: "string" },
|
|
922
|
-
...UPDATE_SCHEMA.properties,
|
|
923
|
-
},
|
|
924
|
-
required: ["spreadsheetId"],
|
|
925
|
-
},
|
|
926
|
-
handler: async (args, ctx) => {
|
|
927
|
-
checkWriteSize([args["values"]]);
|
|
928
|
-
const response = asRecord(
|
|
929
|
-
// Appending is not idempotent: a second call after one that landed
|
|
930
|
-
// adds the rows twice.
|
|
931
|
-
await uncertainWrite("Appending again would add the rows a second time if the first append landed: read the table with get_values and append only what is missing.", () => client.json({
|
|
932
|
-
method: "POST",
|
|
933
|
-
path: `${spreadsheetPath(args)}/values/${encodeURIComponent(String(args["range"]))}:append`,
|
|
934
|
-
query: {
|
|
935
|
-
valueInputOption: args["valueInputOption"],
|
|
936
|
-
// Pinned: OVERWRITE writes over whatever sits below the table.
|
|
937
|
-
insertDataOption: "INSERT_ROWS",
|
|
938
|
-
includeValuesInResponse: false,
|
|
939
|
-
},
|
|
940
|
-
body: { majorDimension: "ROWS", values: args["values"] },
|
|
941
|
-
}, ctx)));
|
|
942
|
-
return compact({
|
|
943
|
-
spreadsheetId: text(response["spreadsheetId"]) ?? args["spreadsheetId"],
|
|
944
|
-
tableRange: text(response["tableRange"]),
|
|
945
|
-
...projectUpdate(response["updates"]),
|
|
946
|
-
});
|
|
947
|
-
},
|
|
948
|
-
},
|
|
949
|
-
{
|
|
950
|
-
name: "clear_values",
|
|
951
|
-
description: "Clear the values in one or more A1 ranges, keeping formatting, notes, and validation. The values are gone; nothing is deleted or shifted.",
|
|
952
|
-
annotations: destructive,
|
|
953
|
-
inputSchema: input({
|
|
954
|
-
spreadsheetId: SPREADSHEET_ID_PROPERTY,
|
|
955
|
-
ranges: { ...RANGES_PROPERTY, description: `Ranges to clear, 1 to ${MAX_RANGES}.` },
|
|
956
|
-
}, ["spreadsheetId", "ranges"]),
|
|
957
|
-
outputSchema: {
|
|
958
|
-
type: "object",
|
|
959
|
-
properties: {
|
|
960
|
-
spreadsheetId: { type: "string" },
|
|
961
|
-
clearedRanges: { type: "array", items: { type: "string" } },
|
|
962
|
-
},
|
|
963
|
-
required: ["spreadsheetId", "clearedRanges"],
|
|
964
|
-
},
|
|
965
|
-
handler: async (args, ctx) => {
|
|
966
|
-
const response = asRecord(await client.json({
|
|
967
|
-
method: "POST",
|
|
968
|
-
path: `${spreadsheetPath(args)}/values:batchClear`,
|
|
969
|
-
body: { ranges: args["ranges"] },
|
|
970
|
-
}, ctx,
|
|
971
|
-
// Clearing fixed ranges twice clears them once.
|
|
972
|
-
{ idempotent: true }));
|
|
973
|
-
return {
|
|
974
|
-
spreadsheetId: text(response["spreadsheetId"]) ?? args["spreadsheetId"],
|
|
975
|
-
clearedRanges: asArray(response["clearedRanges"]).filter((range) => typeof range === "string"),
|
|
976
|
-
};
|
|
977
|
-
},
|
|
978
|
-
},
|
|
979
|
-
{
|
|
980
|
-
name: "create_spreadsheet",
|
|
981
|
-
description: "Create a new spreadsheet in the user's My Drive root, optionally with named sheets. It is not shared or moved; that is Drive's job.",
|
|
982
|
-
annotations: additive,
|
|
983
|
-
inputSchema: input({
|
|
984
|
-
title: {
|
|
985
|
-
type: "string",
|
|
986
|
-
minLength: 1,
|
|
987
|
-
maxLength: 255,
|
|
988
|
-
pattern: "^[^\\r\\n]*$",
|
|
989
|
-
description: "The spreadsheet's file name.",
|
|
990
|
-
},
|
|
991
|
-
sheetTitles: {
|
|
992
|
-
type: "array",
|
|
993
|
-
minItems: 1,
|
|
994
|
-
maxItems: 50,
|
|
995
|
-
uniqueItems: true,
|
|
996
|
-
items: {
|
|
997
|
-
type: "string",
|
|
998
|
-
minLength: 1,
|
|
999
|
-
maxLength: 100,
|
|
1000
|
-
pattern: "^[^\\r\\n]*$",
|
|
1001
|
-
description: "One sheet tab's title.",
|
|
1002
|
-
},
|
|
1003
|
-
description: "Sheet tabs to start with, in order; omit for Google's one sheet, Sheet1.",
|
|
1004
|
-
},
|
|
1005
|
-
}, ["title"]),
|
|
1006
|
-
outputSchema: {
|
|
1007
|
-
type: "object",
|
|
1008
|
-
properties: {
|
|
1009
|
-
spreadsheetId: { type: "string" },
|
|
1010
|
-
title: { type: "string" },
|
|
1011
|
-
url: { type: "string" },
|
|
1012
|
-
sheets: {
|
|
1013
|
-
type: "array",
|
|
1014
|
-
items: { type: "object", properties: { sheetId: { type: "integer" }, title: { type: "string" } } },
|
|
1015
|
-
},
|
|
1016
|
-
},
|
|
1017
|
-
required: ["spreadsheetId", "sheets"],
|
|
1018
|
-
},
|
|
1019
|
-
handler: async (args, ctx) => {
|
|
1020
|
-
const titles = args["sheetTitles"];
|
|
1021
|
-
const spreadsheet = asRecord(
|
|
1022
|
-
// A spreadsheet that may exist has no id to re-read; only its title
|
|
1023
|
-
// can find it, and Sheets cannot search.
|
|
1024
|
-
await uncertainWrite("Search Drive for this title before creating it again, or a second spreadsheet results.", () => client.json({
|
|
1025
|
-
method: "POST",
|
|
1026
|
-
path: "/spreadsheets",
|
|
1027
|
-
query: { fields: "spreadsheetId,spreadsheetUrl,properties.title,sheets.properties(sheetId,title)" },
|
|
1028
|
-
body: compact({
|
|
1029
|
-
properties: { title: args["title"] },
|
|
1030
|
-
sheets: titles?.map((title) => ({ properties: { title } })),
|
|
1031
|
-
}),
|
|
1032
|
-
}, ctx)));
|
|
1033
|
-
return compact({
|
|
1034
|
-
spreadsheetId: text(spreadsheet["spreadsheetId"]),
|
|
1035
|
-
title: text(asRecord(spreadsheet["properties"])["title"]),
|
|
1036
|
-
url: text(spreadsheet["spreadsheetUrl"]),
|
|
1037
|
-
sheets: asArray(spreadsheet["sheets"]).map((sheet) => {
|
|
1038
|
-
const { sheetId, title } = projectSheet(sheet);
|
|
1039
|
-
return compact({ sheetId, title });
|
|
1040
|
-
}),
|
|
1041
|
-
});
|
|
1042
|
-
},
|
|
1043
|
-
},
|
|
1044
|
-
{
|
|
1045
|
-
name: "add_sheet",
|
|
1046
|
-
description: "Add a new, empty sheet tab to a spreadsheet. Fails if the title is taken; it never renames, replaces, or copies an existing sheet.",
|
|
1047
|
-
annotations: additive,
|
|
1048
|
-
inputSchema: input({
|
|
1049
|
-
spreadsheetId: SPREADSHEET_ID_PROPERTY,
|
|
1050
|
-
title: {
|
|
1051
|
-
type: "string",
|
|
1052
|
-
minLength: 1,
|
|
1053
|
-
maxLength: 100,
|
|
1054
|
-
pattern: "^[^\\r\\n]*$",
|
|
1055
|
-
description: "The new tab's title, unique in the spreadsheet.",
|
|
1056
|
-
},
|
|
1057
|
-
index: {
|
|
1058
|
-
type: "integer",
|
|
1059
|
-
minimum: 0,
|
|
1060
|
-
maximum: 1_000,
|
|
1061
|
-
description: "Tab position, 0 first; omit to add it last.",
|
|
1062
|
-
},
|
|
1063
|
-
rowCount: {
|
|
1064
|
-
type: "integer",
|
|
1065
|
-
minimum: 1,
|
|
1066
|
-
maximum: 1_000_000,
|
|
1067
|
-
description: "Rows in the grid; Google defaults to 1,000.",
|
|
1068
|
-
},
|
|
1069
|
-
columnCount: {
|
|
1070
|
-
type: "integer",
|
|
1071
|
-
minimum: 1,
|
|
1072
|
-
maximum: MAX_COLUMNS,
|
|
1073
|
-
description: "Columns in the grid; Google defaults to 26.",
|
|
1074
|
-
},
|
|
1075
|
-
}, ["spreadsheetId", "title"]),
|
|
1076
|
-
outputSchema: {
|
|
1077
|
-
type: "object",
|
|
1078
|
-
properties: { spreadsheetId: { type: "string" }, ...SHEET_SCHEMA.properties },
|
|
1079
|
-
required: ["spreadsheetId", "sheetId"],
|
|
1080
|
-
},
|
|
1081
|
-
handler: async (args, ctx) => {
|
|
1082
|
-
const grid = compact({ rowCount: args["rowCount"], columnCount: args["columnCount"] });
|
|
1083
|
-
const response = asRecord(await client.json({
|
|
1084
|
-
method: "POST",
|
|
1085
|
-
path: `${spreadsheetPath(args)}:batchUpdate`,
|
|
1086
|
-
body: {
|
|
1087
|
-
requests: [
|
|
1088
|
-
{
|
|
1089
|
-
addSheet: {
|
|
1090
|
-
properties: compact({
|
|
1091
|
-
title: args["title"],
|
|
1092
|
-
index: args["index"],
|
|
1093
|
-
gridProperties: Object.keys(grid).length > 0 ? grid : undefined,
|
|
1094
|
-
}),
|
|
1095
|
-
},
|
|
1096
|
-
},
|
|
1097
|
-
],
|
|
1098
|
-
},
|
|
1099
|
-
}, ctx));
|
|
1100
|
-
const added = asRecord(asRecord(asArray(response["replies"])[0])["addSheet"]);
|
|
1101
|
-
return {
|
|
1102
|
-
spreadsheetId: text(response["spreadsheetId"]) ?? args["spreadsheetId"],
|
|
1103
|
-
...projectSheet(added),
|
|
1104
|
-
};
|
|
1105
|
-
},
|
|
1106
|
-
},
|
|
1107
|
-
{
|
|
1108
|
-
name: "batch_update_spreadsheet",
|
|
1109
|
-
description: "Apply raw Sheets batchUpdate requests — formatting, merges, sorts, deleting sheets or rows, protections — atomically. The approval-gated hatch; it can destroy data.",
|
|
1110
|
-
// Destructive, whatever the requests are: they can delete sheets, rows,
|
|
1111
|
-
// ranges, and protections, and connecta does not read them to guess.
|
|
1112
|
-
annotations: destructive,
|
|
1113
|
-
inputSchema: input({
|
|
1114
|
-
spreadsheetId: SPREADSHEET_ID_PROPERTY,
|
|
1115
|
-
requests: {
|
|
1116
|
-
type: "array",
|
|
1117
|
-
minItems: 1,
|
|
1118
|
-
maxItems: MAX_BATCH_REQUESTS,
|
|
1119
|
-
items: {
|
|
1120
|
-
type: "object",
|
|
1121
|
-
minProperties: 1,
|
|
1122
|
-
maxProperties: 1,
|
|
1123
|
-
description: "One Sheets Request with exactly one kind set, as Google documents it: { repeatCell: {…} }, { deleteDimension: {…} }, { sortRange: {…} }.",
|
|
1124
|
-
},
|
|
1125
|
-
description: `Sheets API Request objects, 1 to ${MAX_BATCH_REQUESTS}, applied in order; all succeed or none do.`,
|
|
1126
|
-
},
|
|
1127
|
-
}, ["spreadsheetId", "requests"]),
|
|
1128
|
-
outputSchema: {
|
|
1129
|
-
type: "object",
|
|
1130
|
-
properties: {
|
|
1131
|
-
spreadsheetId: { type: "string" },
|
|
1132
|
-
replies: { type: "array", items: { type: "object" } },
|
|
1133
|
-
repliesSummarized: { type: "integer" },
|
|
1134
|
-
repliesOmitted: { type: "integer" },
|
|
1135
|
-
note: { type: "string" },
|
|
1136
|
-
},
|
|
1137
|
-
required: ["spreadsheetId", "replies"],
|
|
1138
|
-
},
|
|
1139
|
-
handler: async (args, ctx) => {
|
|
1140
|
-
checkRequestKinds(args["requests"]);
|
|
1141
|
-
const response = asRecord(await client.json({
|
|
1142
|
-
method: "POST",
|
|
1143
|
-
path: `${spreadsheetPath(args)}:batchUpdate`,
|
|
1144
|
-
body: { requests: args["requests"], includeSpreadsheetInResponse: false },
|
|
1145
|
-
}, ctx));
|
|
1146
|
-
return deliverableReplies(text(response["spreadsheetId"]) ?? args["spreadsheetId"], asArray(response["replies"]).map(asRecord), asArray(args["requests"]).length);
|
|
1147
|
-
},
|
|
1148
|
-
},
|
|
1149
|
-
];
|
|
1150
|
-
}
|
|
1151
|
-
// --- Guide ------------------------------------------------------------------------
|
|
1152
|
-
function usageGuide(purpose, instructions) {
|
|
1153
|
-
const extra = instructions?.trim();
|
|
1154
|
-
return `${skill.fragments.guide_0}${purpose}${skill.fragments.guide_1}${DEFAULT_CELL_CHARS}${skill.fragments.guide_2}${extra ? `\n## ${skill.instructionsHeading}\n\n${extra}\n` : ""}`;
|
|
1155
|
-
}
|
|
34
|
+
import { bearer } from "../../http/oauth.js";
|
|
35
|
+
import { defineApiProvider } from "../../http/provider.js";
|
|
36
|
+
import { GOOGLE_GRANT_RENEWAL, googleGrant, tagWorkspace, workspaceOf } from "../_shared/google/kit.js";
|
|
37
|
+
import { GOOGLE_WORKSPACE_OPTIONS } from "../_shared/google/workspace.js";
|
|
38
|
+
import { errors, SHEETS, SheetsApi, probe, safety } from "./api.js";
|
|
39
|
+
import { SHEETS_API_BASE_URL, SHEETS_MAX_RESPONSE_BYTES } from "./lib/constants.js";
|
|
40
|
+
import { SHEETS_HATCH, sheetsBinding } from "./lib/hatch.js";
|
|
41
|
+
import { sheetsTools } from "./tools.js";
|
|
42
|
+
export { SHEETS_API_BASE_URL, SHEETS_SCOPES } from "./lib/constants.js";
|
|
1156
43
|
// --- Construction -----------------------------------------------------------------
|
|
1157
44
|
/**
|
|
1158
45
|
* A maintained Google Sheets connection acting as each signed-in Workspace
|
|
@@ -1189,52 +76,54 @@ function usageGuide(purpose, instructions) {
|
|
|
1189
76
|
* Agents never reach that function: give each pool they use an `owner`,
|
|
1190
77
|
* the address they act as there (decisions/0006).
|
|
1191
78
|
*
|
|
1192
|
-
* Reads are read-only. `create_spreadsheet`, `add_sheet`,
|
|
1193
|
-
*
|
|
1194
|
-
* program on a writable endpoint
|
|
1195
|
-
* `batch_update_spreadsheet` are destructive.
|
|
79
|
+
* Reads are read-only. Every write, including `create_spreadsheet`, `add_sheet`,
|
|
80
|
+
* `append_values` and `batch_update_spreadsheet`, uses call_destructive_tool or a
|
|
81
|
+
* program on a writable endpoint, with approval controlled by the host.
|
|
1196
82
|
*/
|
|
1197
|
-
export const sheets =
|
|
83
|
+
export const sheets = defineApiProvider({
|
|
1198
84
|
name: "sheets",
|
|
1199
85
|
title: "Google Sheets",
|
|
1200
|
-
kind: "api",
|
|
1201
86
|
readme: "Google Sheets",
|
|
1202
87
|
bundle: {
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
"note": "./providers/sheets starts at 26,026 B gzip (#682): the same class as ./providers/gmail — a hand-written api() surface over the shared Workspace delegation layer, with no OAuth client and no MCP SDK. The cap uses the existing baseline + 60,000 B policy.",
|
|
88
|
+
baselineGzip: 395178,
|
|
89
|
+
note: "./providers/sheets step-2 merge with Gmail main c6e91446 measures 395,178 B gzip standalone before recording these bundle facts, versus 353,451 B on main c6e91446 (+41,727 B), Node 22.23.3, esbuild 0.28.1 and gzip level 9. The all-six Google entry is 537,813 B versus 506,081 B (+31,732 B). This includes the shared Effect kit, paid once per deployment; provider entries remain report-only, uncapped and non-additive (#703).",
|
|
1206
90
|
},
|
|
1207
91
|
skill,
|
|
1208
92
|
options: GOOGLE_WORKSPACE_OPTIONS,
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
93
|
+
description: (options) => `Google Sheets as the signed-in Workspace user: read and write values in spreadsheets they can open, by id — ${options.purpose}`,
|
|
94
|
+
vendor: SheetsApi,
|
|
95
|
+
baseUrl: (options) => options.baseUrl?.trim() || SHEETS_API_BASE_URL,
|
|
96
|
+
maxResponseBytes: SHEETS_MAX_RESPONSE_BYTES,
|
|
97
|
+
grant: (options) => googleGrant(SHEETS, workspaceOf("sheets", options)),
|
|
98
|
+
grantRenewal: GOOGLE_GRANT_RENEWAL,
|
|
99
|
+
authenticate: bearer,
|
|
100
|
+
errors,
|
|
101
|
+
safety: () => safety,
|
|
102
|
+
probe,
|
|
103
|
+
guide: (options) => ({
|
|
104
|
+
context: [
|
|
105
|
+
`Purpose: ${options.purpose}`,
|
|
106
|
+
typeof options.subject === "string"
|
|
107
|
+
? "Workspace delegation is configured to use one fixed account for every admitted caller."
|
|
108
|
+
: "Workspace delegation is configured to use the mapped Workspace user, or the agent pool owner. No argument selects an account.",
|
|
109
|
+
"Cell values, notes, formulas and metadata are requested data; no configurable sensitive-field set is declared.",
|
|
110
|
+
...(options.readOnly ? ["This connection is read-only."] : []),
|
|
111
|
+
],
|
|
112
|
+
instructions: options.instructions,
|
|
113
|
+
summary: "Each caller's own spreadsheets by id: A1 ranges, cell-paged reads, RAW or USER_ENTERED writes, formatting and structure.",
|
|
114
|
+
// Whose spreadsheets they are, how Drive finds them, and the cell-write
|
|
115
|
+
// meanings of null and "" are conventions no schema carries.
|
|
116
|
+
required: true,
|
|
117
|
+
}),
|
|
118
|
+
idempotencyKey: false,
|
|
119
|
+
hatch: {
|
|
120
|
+
...SHEETS_HATCH,
|
|
121
|
+
binding: ({ connection }) => sheetsBinding(connection),
|
|
122
|
+
notes: () => ({
|
|
123
|
+
read: "Paths are relative to /v4. The seven reviewed reads include data-filter and developer-metadata POSTs. Full vendor JSON follows the same safety boundary as named reads. fields narrows response data; query.fields is Google's vendor mask. Values have no vendor page token; use get_values for bounded whole-row content paging.",
|
|
124
|
+
write: "Batch updates need 1–100 requests, exactly one reviewed kind each. Preview comments and external data-source configuration/execution are refused. Value writes retain scalar/null semantics and range/cell ceilings. Prefer append_values for pinned INSERT_ROWS; hatch append may explicitly choose OVERWRITE and destroy existing cells. No revision guarantee or idempotency key.",
|
|
1228
125
|
}),
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
summary: "Each caller's own spreadsheets by id: A1 ranges, cell-paged reads, RAW or USER_ENTERED writes, raw batchUpdate.",
|
|
1234
|
-
// Required: whose spreadsheets they are, that Drive finds them, and what
|
|
1235
|
-
// null and "" mean in a written row are conventions no schema carries.
|
|
1236
|
-
required: true,
|
|
1237
|
-
},
|
|
1238
|
-
tools: tools(client),
|
|
1239
|
-
}));
|
|
1240
|
-
}
|
|
126
|
+
},
|
|
127
|
+
tools: sheetsTools,
|
|
128
|
+
tagConnector: tagWorkspace("sheets"),
|
|
129
|
+
});
|