@zackbart/connecta 0.34.1 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +363 -0
- package/README.md +62 -106
- 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 +2051 -1160
- 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 +663 -271
- 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 +12 -7
- package/dist/connectors/api-connector.js +142 -56
- 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 +228 -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 +383 -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/agents.d.ts +5 -4
- package/dist/operator-ui/agents.js +17 -7
- package/dist/operator-ui/config-summary.js +5 -4
- package/dist/operator-ui/connection-map.js +7 -5
- package/dist/operator-ui/contract.d.ts +7 -3
- package/dist/operator-ui/endpoints.js +4 -13
- package/dist/operator-ui/fix-prompts.d.ts +1 -1
- package/dist/operator-ui/fix-prompts.js +0 -8
- package/dist/operator-ui/generated.js +3 -3
- package/dist/operator-ui/model.d.ts +2 -3
- package/dist/operator-ui/overview.js +5 -2
- package/dist/operator-ui/setup-commands.js +0 -4
- package/dist/operator-ui/view.d.ts +2 -25
- package/dist/operator-ui/view.js +2 -76
- 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 +537 -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/dispatch.js +3 -23
- package/dist/providers/_shared/rest/kit.js +546 -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/tools.js +101 -78
- 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 +802 -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 +1024 -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/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 +734 -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 +66 -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 +3 -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 +556 -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 -222
- 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 +7 -109
- 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 +27 -7
- package/documentation/architecture.md +2547 -226
- package/documentation/auth.md +821 -98
- 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 +68 -45
- 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 +965 -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 +27 -46
- 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/providers/_shared/request-token.js +0 -83
- 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/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
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
// Google Sheets named projections, bounded cell reads and guarded writes.
|
|
2
|
+
import { Effect, Option, Schema } from "effect";
|
|
3
|
+
import { ConnectorCallError } from "../../errors.js";
|
|
4
|
+
import { Refusal } from "../../http/errors.js";
|
|
5
|
+
import { googleClient, googleFailureMessage, googleOutcomeOf } from "../_shared/google/kit.js";
|
|
6
|
+
import { clampText, jsonBytes, RESULT_BUDGET_BYTES } from "../_shared/google/result-size.js";
|
|
7
|
+
import { encodeForAgent, redactBeforeCut } from "../../sent-secrets.js";
|
|
8
|
+
import { SHEETS } from "./api.js";
|
|
9
|
+
import { read, write } from "../../http/tools.js";
|
|
10
|
+
import { boundCursor } from "../../http/paging.js";
|
|
11
|
+
import { fieldsInput, fieldsProblem, selectFields } from "../../http/fields.js";
|
|
12
|
+
import { Operation } from "../../runtime/call.js";
|
|
13
|
+
import { sheetsIndex } from "./lib/hatch.js";
|
|
14
|
+
import { SHEETS_ADMITTED_KINDS, SHEETS_REFUSED_KINDS, sheetsExternalDataProblem } from "./lib/requests.js";
|
|
15
|
+
import { inputs, outputs } from "./lib/schemas.js";
|
|
16
|
+
/** Cells per page of get_values: connecta's cap, since Sheets has no paging. */
|
|
17
|
+
const DEFAULT_PAGE_CELLS = 2_000;
|
|
18
|
+
const MAX_PAGE_CELLS = 10_000;
|
|
19
|
+
/**
|
|
20
|
+
* Bytes per page of get_values as serialized, beside the cell count: a page
|
|
21
|
+
* has to be deliverable, and cells say nothing about size — 60 cells of 5,000
|
|
22
|
+
* characters already outgrow the QuickJS executor's host bridge. The default
|
|
23
|
+
* is the Workspace providers' shared result budget, so an execute_code
|
|
24
|
+
* program always receives a default page; unlike that budget's envelope
|
|
25
|
+
* allowance, maxBytes bounds the whole page, fields and cursor included. A
|
|
26
|
+
* direct call_tool caller may ask for more, up to 4 MiB: base64 in the result
|
|
27
|
+
* stash makes that about 5.6 MiB, under the stash's default 8 MiB, so an
|
|
28
|
+
* oversized page still pages through connecta.result rather than being dropped.
|
|
29
|
+
*/
|
|
30
|
+
const DEFAULT_PAGE_BYTES = RESULT_BUDGET_BYTES;
|
|
31
|
+
const MAX_PAGE_BYTES = 4 * 1024 * 1024;
|
|
32
|
+
/**
|
|
33
|
+
* Characters a read keeps per cell by default. A cell holds at most 50,000
|
|
34
|
+
* (Sheets' own limit), so `maxCellChars` at that ceiling always reads one whole.
|
|
35
|
+
*/
|
|
36
|
+
const DEFAULT_CELL_CHARS = 5_000;
|
|
37
|
+
const MAX_CELL_CHARS = 50_000;
|
|
38
|
+
/** Rows and cells one write may carry: connecta's cap, under Google's 10 MB request. */
|
|
39
|
+
const MAX_WRITE_CELLS = 50_000;
|
|
40
|
+
/** Named ranges a metadata read lists before reporting a size cut. */
|
|
41
|
+
const MAX_NAMED_RANGES = 200;
|
|
42
|
+
function asRecord(value) {
|
|
43
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
44
|
+
}
|
|
45
|
+
function asArray(value) {
|
|
46
|
+
return Array.isArray(value) ? value : [];
|
|
47
|
+
}
|
|
48
|
+
function compact(value) {
|
|
49
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
50
|
+
}
|
|
51
|
+
function text(value) {
|
|
52
|
+
return typeof value === "string" && value !== "" ? value : undefined;
|
|
53
|
+
}
|
|
54
|
+
function integer(value) {
|
|
55
|
+
return typeof value === "number" && Number.isInteger(value) ? value : undefined;
|
|
56
|
+
}
|
|
57
|
+
/** A 0-based column index as its A1 letters: 0 → A, 26 → AA. */
|
|
58
|
+
function columnLetters(index) {
|
|
59
|
+
let letters = "";
|
|
60
|
+
for (let rest = index + 1; rest > 0; rest = Math.floor((rest - 1) / 26)) {
|
|
61
|
+
letters = String.fromCharCode(65 + ((rest - 1) % 26)) + letters;
|
|
62
|
+
}
|
|
63
|
+
return letters;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* A sheet title as A1, always single-quoted. Bare, a whole-sheet range is
|
|
67
|
+
* indistinguishable from a named range of the same name — and Google resolves
|
|
68
|
+
* the named range first, so `Sheet1` handed to clear_values could clear
|
|
69
|
+
* something else. No named range can contain a quote, so quoted cannot.
|
|
70
|
+
*/
|
|
71
|
+
function sheetPrefix(title) {
|
|
72
|
+
return `'${title.replace(/'/g, "''")}'`;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A GridRange as A1, or `undefined` for a shape A1 cannot say. Google leaves
|
|
76
|
+
* a zero index out of the JSON, and a missing start index means "from the
|
|
77
|
+
* first", so both read as 0; a missing end means "to the edge".
|
|
78
|
+
*/
|
|
79
|
+
function gridRangeA1(range, title) {
|
|
80
|
+
if (title === undefined)
|
|
81
|
+
return undefined;
|
|
82
|
+
const startRow = integer(range["startRowIndex"]) ?? 0;
|
|
83
|
+
const startColumn = integer(range["startColumnIndex"]) ?? 0;
|
|
84
|
+
const endRow = integer(range["endRowIndex"]);
|
|
85
|
+
const endColumn = integer(range["endColumnIndex"]);
|
|
86
|
+
const prefix = sheetPrefix(title);
|
|
87
|
+
if (endRow === undefined && endColumn === undefined) {
|
|
88
|
+
return startRow === 0 && startColumn === 0 ? prefix : undefined;
|
|
89
|
+
}
|
|
90
|
+
if (endColumn === undefined) {
|
|
91
|
+
return startColumn === 0 ? `${prefix}!${startRow + 1}:${endRow}` : undefined;
|
|
92
|
+
}
|
|
93
|
+
const start = `${columnLetters(startColumn)}${startRow + 1}`;
|
|
94
|
+
return `${prefix}!${start}:${columnLetters(endColumn - 1)}${endRow ?? ""}`;
|
|
95
|
+
}
|
|
96
|
+
function valuesCursorArgs(args) {
|
|
97
|
+
return {
|
|
98
|
+
spreadsheetId: args["spreadsheetId"],
|
|
99
|
+
ranges: args["ranges"],
|
|
100
|
+
valueRenderOption: args["valueRenderOption"] ?? null,
|
|
101
|
+
dateTimeRenderOption: args["dateTimeRenderOption"] ?? null,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function encodeCursor(ctx, args, cursor) {
|
|
105
|
+
return boundCursor.encode("get_values", valuesCursorArgs(args), JSON.stringify(cursor), (json) => encodeForAgent(ctx, json, { url: true, claims: true }));
|
|
106
|
+
}
|
|
107
|
+
function decodeCursor(value, ranges) {
|
|
108
|
+
let parsed;
|
|
109
|
+
try {
|
|
110
|
+
parsed = JSON.parse(value);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
parsed = undefined;
|
|
114
|
+
}
|
|
115
|
+
const cursor = asRecord(parsed), index = integer(cursor["i"]), skip = integer(cursor["s"]);
|
|
116
|
+
if (Object.keys(cursor).length !== 2 ||
|
|
117
|
+
index === undefined ||
|
|
118
|
+
index < 0 ||
|
|
119
|
+
index >= ranges.length ||
|
|
120
|
+
skip === undefined ||
|
|
121
|
+
skip < 0 ||
|
|
122
|
+
!Number.isSafeInteger(skip)) {
|
|
123
|
+
throw new ConnectorCallError("invalid_args", "Pass the get_values page.nextCursor back unchanged with the same spreadsheet, ranges and render options.");
|
|
124
|
+
}
|
|
125
|
+
return { i: index, s: skip };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The caller's own range past its first `rows` rows, when the range says
|
|
129
|
+
* where it starts: `Sheet1!C5:F100` → `Sheet1!C8:F100`, `B:D` → `B4:D`. A
|
|
130
|
+
* sheet title, a named range, or R1C1 does not, and is resent whole with the
|
|
131
|
+
* rows skipped instead — correct, but it re-reads what earlier pages did.
|
|
132
|
+
*/
|
|
133
|
+
function rangeAfter(range, rows) {
|
|
134
|
+
if (rows === 0)
|
|
135
|
+
return range;
|
|
136
|
+
const match = /^(.*!)?([A-Z]+)(\d+)?:([A-Z]+)(\d+)?$/.exec(range);
|
|
137
|
+
if (!match)
|
|
138
|
+
return undefined;
|
|
139
|
+
const [, prefix = "", startColumn, startRow, endColumn, endRow] = match;
|
|
140
|
+
if (startRow === undefined && endRow !== undefined)
|
|
141
|
+
return undefined;
|
|
142
|
+
const first = Number(startRow ?? 1);
|
|
143
|
+
if (endRow !== undefined && first > Number(endRow))
|
|
144
|
+
return undefined;
|
|
145
|
+
const start = first + rows;
|
|
146
|
+
if (endRow !== undefined && start > Number(endRow))
|
|
147
|
+
return undefined;
|
|
148
|
+
return `${prefix}${startColumn}${start}:${endColumn}${endRow ?? ""}`;
|
|
149
|
+
}
|
|
150
|
+
/** A cell as read, cut at `max` characters with a marker saying how to read the rest. Every
|
|
151
|
+
* secret the request sent is replaced first: a cut through an echo leaves a fragment. */
|
|
152
|
+
function cappedCell(ctx, whole, max, cut) {
|
|
153
|
+
if (typeof whole !== "string" || whole.length <= max)
|
|
154
|
+
return whole;
|
|
155
|
+
const value = redactBeforeCut(ctx, whole);
|
|
156
|
+
if (value.length <= max)
|
|
157
|
+
return value;
|
|
158
|
+
cut.cells += 1;
|
|
159
|
+
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]`;
|
|
160
|
+
}
|
|
161
|
+
/** Refuse a row no page of `budget` bytes can hold, rather than overrun the cap. */
|
|
162
|
+
function rowTooLarge(range, row, needed, budget) {
|
|
163
|
+
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)}.`);
|
|
164
|
+
}
|
|
165
|
+
/** Refuse a page too small for even one range's own fields. */
|
|
166
|
+
function pageTooSmall(range, needed, budget) {
|
|
167
|
+
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)}.`);
|
|
168
|
+
}
|
|
169
|
+
function raiseBytes(needed) {
|
|
170
|
+
if (needed > MAX_PAGE_BYTES)
|
|
171
|
+
return "";
|
|
172
|
+
return needed <= DEFAULT_PAGE_BYTES
|
|
173
|
+
? `, or raise maxBytes to at least ${needed}`
|
|
174
|
+
: `, or raise maxBytes to at least ${needed} (above ${DEFAULT_PAGE_BYTES}, only for a direct call_tool read: execute_code cannot receive it)`;
|
|
175
|
+
}
|
|
176
|
+
/** Refuse a row no page of `budget` cells can hold, rather than overrun the cap. */
|
|
177
|
+
function rowTooWide(range, row, width, budget) {
|
|
178
|
+
const fix = width <= MAX_PAGE_CELLS
|
|
179
|
+
? `raise maxCells to at least ${width}, or narrow the range's columns`
|
|
180
|
+
: `narrow the range's columns; a page holds at most ${MAX_PAGE_CELLS} cells`;
|
|
181
|
+
return new ConnectorCallError("invalid_args", `Row ${row} of ${range} has ${width} cells, more than this page's maxCells of ${budget}; ${fix}.`);
|
|
182
|
+
}
|
|
183
|
+
// --- Writing values ---------------------------------------------------------------
|
|
184
|
+
/** Refuse a write past connecta's per-call cell cap before anything is sent. */
|
|
185
|
+
function checkWriteSize(blocks) {
|
|
186
|
+
let cells = 0;
|
|
187
|
+
for (const block of blocks)
|
|
188
|
+
for (const row of block)
|
|
189
|
+
cells += row.length;
|
|
190
|
+
if (cells > MAX_WRITE_CELLS) {
|
|
191
|
+
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.`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function projectUpdate(value) {
|
|
195
|
+
const update = asRecord(value);
|
|
196
|
+
return compact({
|
|
197
|
+
updatedRange: text(update["updatedRange"]),
|
|
198
|
+
updatedRows: integer(update["updatedRows"]) ?? 0,
|
|
199
|
+
updatedColumns: integer(update["updatedColumns"]) ?? 0,
|
|
200
|
+
updatedCells: integer(update["updatedCells"]) ?? 0,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
/** Refuse a raw request that is not exactly one known kind, unsent. */
|
|
204
|
+
function checkRequestKinds(requests) {
|
|
205
|
+
for (const request of requests)
|
|
206
|
+
for (const kind of Object.keys(asRecord(request)))
|
|
207
|
+
if (Object.hasOwn(SHEETS_REFUSED_KINDS, kind))
|
|
208
|
+
throw new ConnectorCallError("invalid_args", SHEETS_REFUSED_KINDS[kind]);
|
|
209
|
+
const problem = sheetsExternalDataProblem(requests);
|
|
210
|
+
if (problem)
|
|
211
|
+
throw new ConnectorCallError("invalid_args", problem);
|
|
212
|
+
requests.forEach((request, index) => {
|
|
213
|
+
const kinds = Object.keys(asRecord(request));
|
|
214
|
+
if (kinds.length !== 1 || !SHEETS_ADMITTED_KINDS.includes(kinds[0])) {
|
|
215
|
+
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.`);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* A write whose outcome is unknown — sent, and then anything but an answer
|
|
221
|
+
* Google refused it with: a reply that broke off, no reply, a redirect, or a
|
|
222
|
+
* 5xx after receiving it — told what to read before trying again, for the
|
|
223
|
+
* writes where the shared "re-read before repeating it" has nothing to re-read
|
|
224
|
+
* or where repeating does harm. The advice claims nothing either way; only
|
|
225
|
+
* the client's own message says "probably applied", and only for a 2xx whose
|
|
226
|
+
* body broke off. A refusal, and anything never sent, passes through as the
|
|
227
|
+
* client mapped it.
|
|
228
|
+
*/
|
|
229
|
+
function uncertainWrite(connection, advice, write) {
|
|
230
|
+
return write.pipe(Effect.catch((failure) => {
|
|
231
|
+
const outcome = googleOutcomeOf(failure);
|
|
232
|
+
return outcome?.dispatched && outcome.phase !== "refused"
|
|
233
|
+
? googleFailureMessage(connection, failure, (message) => `${message} ${advice}`)
|
|
234
|
+
: Effect.fail(failure);
|
|
235
|
+
}));
|
|
236
|
+
}
|
|
237
|
+
// --- Raw batchUpdate replies -----------------------------------------------------
|
|
238
|
+
/** A string kept in a reply summary, cut so a long one cannot crowd out ids. */
|
|
239
|
+
const SUMMARY_TEXT_BYTES = 256;
|
|
240
|
+
/** The scalar fields of an object — ids, titles, counts — and nothing nested. */
|
|
241
|
+
function scalarsOf(ctx, value) {
|
|
242
|
+
const scalars = {};
|
|
243
|
+
for (const [key, field] of Object.entries(asRecord(value))) {
|
|
244
|
+
if (typeof field === "string") {
|
|
245
|
+
scalars[key] = clampText(ctx, field, SUMMARY_TEXT_BYTES, (dropped) => `[… ${dropped} more characters]`);
|
|
246
|
+
}
|
|
247
|
+
else if (typeof field === "number" || typeof field === "boolean") {
|
|
248
|
+
scalars[key] = field;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return Object.keys(scalars).length > 0 ? scalars : undefined;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* One reply reduced to its kind and the scalars an agent acts on next: a new
|
|
255
|
+
* sheet's id and title, a chart's id, findReplace's counts. Google's replies
|
|
256
|
+
* put those at the top of the kind's body or one object down (`properties`,
|
|
257
|
+
* `chart`, `namedRange`); arrays and anything deeper are dropped.
|
|
258
|
+
*/
|
|
259
|
+
function summarizeReply(ctx, reply) {
|
|
260
|
+
const kind = Object.keys(reply)[0];
|
|
261
|
+
if (kind === undefined)
|
|
262
|
+
return {};
|
|
263
|
+
const body = asRecord(reply[kind]);
|
|
264
|
+
const nested = {};
|
|
265
|
+
for (const [key, field] of Object.entries(body)) {
|
|
266
|
+
if (field && typeof field === "object" && !Array.isArray(field))
|
|
267
|
+
nested[key] = scalarsOf(ctx, field);
|
|
268
|
+
}
|
|
269
|
+
return compact({ kind, ...scalarsOf(ctx, body), ...compact(nested) });
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* A batchUpdate result that can be delivered. The write has already applied,
|
|
273
|
+
* so whatever does not fit is summarized, never dropped silently and never
|
|
274
|
+
* reported as a failure: replies are kept whole, in order, while the ones
|
|
275
|
+
* after them still fit as summaries; the rest become summaries; and a reply
|
|
276
|
+
* set too large even for that leaves the counts and where to re-read.
|
|
277
|
+
*/
|
|
278
|
+
function deliverableReplies(ctx, spreadsheetId, replies, requestCount) {
|
|
279
|
+
const whole = { spreadsheetId, replies };
|
|
280
|
+
if (jsonBytes(whole) <= RESULT_BUDGET_BYTES)
|
|
281
|
+
return whole;
|
|
282
|
+
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.`;
|
|
283
|
+
const summaries = replies.map((reply) => summarizeReply(ctx, reply));
|
|
284
|
+
// What every reply from index i on costs as a summary, comma included.
|
|
285
|
+
const rest = Array.from({ length: replies.length + 1 }, () => 0);
|
|
286
|
+
for (let index = replies.length - 1; index >= 0; index -= 1) {
|
|
287
|
+
rest[index] = rest[index + 1] + jsonBytes(summaries[index]) + 1;
|
|
288
|
+
}
|
|
289
|
+
let bytes = jsonBytes({ spreadsheetId, replies: [], truncated: ["replies[]"], note });
|
|
290
|
+
if (bytes + rest[0] <= RESULT_BUDGET_BYTES) {
|
|
291
|
+
const kept = [];
|
|
292
|
+
let summarized = 0;
|
|
293
|
+
replies.forEach((reply, index) => {
|
|
294
|
+
const size = jsonBytes(reply) + 1;
|
|
295
|
+
if (summarized === 0 && bytes + size + rest[index + 1] <= RESULT_BUDGET_BYTES) {
|
|
296
|
+
bytes += size;
|
|
297
|
+
kept.push(reply);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
summarized += 1;
|
|
301
|
+
kept.push(summaries[index]);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
return { spreadsheetId, replies: kept, truncated: ["replies[]"], note };
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
spreadsheetId,
|
|
308
|
+
replies: [],
|
|
309
|
+
truncated: ["replies[]"],
|
|
310
|
+
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.`,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function projectSheet(value) {
|
|
314
|
+
const properties = asRecord(asRecord(value)["properties"]);
|
|
315
|
+
const grid = asRecord(properties["gridProperties"]);
|
|
316
|
+
return compact({
|
|
317
|
+
// The first sheet's id is 0, which Google leaves out of the JSON.
|
|
318
|
+
sheetId: integer(properties["sheetId"]) ?? 0,
|
|
319
|
+
title: text(properties["title"]),
|
|
320
|
+
index: integer(properties["index"]) ?? 0,
|
|
321
|
+
type: text(properties["sheetType"]),
|
|
322
|
+
hidden: properties["hidden"] === true ? true : undefined,
|
|
323
|
+
rowCount: integer(grid["rowCount"]),
|
|
324
|
+
columnCount: integer(grid["columnCount"]),
|
|
325
|
+
frozenRowCount: integer(grid["frozenRowCount"]),
|
|
326
|
+
frozenColumnCount: integer(grid["frozenColumnCount"]),
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
/** The metadata get_spreadsheet reads: no grid data, no formatting. */
|
|
330
|
+
const SPREADSHEET_FIELDS = [
|
|
331
|
+
"spreadsheetId",
|
|
332
|
+
"spreadsheetUrl",
|
|
333
|
+
"properties(title,locale,timeZone)",
|
|
334
|
+
"sheets.properties(sheetId,title,index,sheetType,hidden,gridProperties(rowCount,columnCount,frozenRowCount,frozenColumnCount))",
|
|
335
|
+
"namedRanges(namedRangeId,name,range)",
|
|
336
|
+
].join(",");
|
|
337
|
+
// --- Tools ------------------------------------------------------------------------
|
|
338
|
+
export function sheetsTools({ connection }) {
|
|
339
|
+
const client = googleClient(connection, SHEETS.provider, { standard: true, index: sheetsIndex });
|
|
340
|
+
const readOnly = { readOnlyHint: true };
|
|
341
|
+
// Additive: nothing that existed is lost. Writes require a writable
|
|
342
|
+
// endpoint: call_destructive_tool or a program. The host controls approval.
|
|
343
|
+
const additive = { readOnlyHint: false, destructiveHint: false };
|
|
344
|
+
const destructive = { readOnlyHint: false, destructiveHint: true };
|
|
345
|
+
const spreadsheetPath = (args) => `/spreadsheets/${encodeURIComponent(String(args["spreadsheetId"]))}`;
|
|
346
|
+
const declarations = [
|
|
347
|
+
{
|
|
348
|
+
name: "get_spreadsheet",
|
|
349
|
+
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.",
|
|
350
|
+
annotations: readOnly,
|
|
351
|
+
input: inputs.get_spreadsheet,
|
|
352
|
+
output: outputs.get_spreadsheet,
|
|
353
|
+
run: (args) => Effect.gen(function* () {
|
|
354
|
+
const spreadsheet = asRecord(yield* client.json({
|
|
355
|
+
method: "GET",
|
|
356
|
+
path: spreadsheetPath(args),
|
|
357
|
+
template: "/spreadsheets/{spreadsheetId}",
|
|
358
|
+
query: { fields: SPREADSHEET_FIELDS },
|
|
359
|
+
}));
|
|
360
|
+
const properties = asRecord(spreadsheet["properties"]);
|
|
361
|
+
const sheets = asArray(spreadsheet["sheets"]).map(projectSheet);
|
|
362
|
+
const titles = new Map(sheets.map((sheet) => [sheet["sheetId"], sheet["title"]]));
|
|
363
|
+
const named = asArray(spreadsheet["namedRanges"]);
|
|
364
|
+
return compact({
|
|
365
|
+
spreadsheetId: text(spreadsheet["spreadsheetId"]) ?? args["spreadsheetId"],
|
|
366
|
+
title: text(properties["title"]),
|
|
367
|
+
url: text(spreadsheet["spreadsheetUrl"]),
|
|
368
|
+
locale: text(properties["locale"]),
|
|
369
|
+
timeZone: text(properties["timeZone"]),
|
|
370
|
+
sheets,
|
|
371
|
+
namedRanges: named.length > 0
|
|
372
|
+
? named.slice(0, MAX_NAMED_RANGES).map((value) => {
|
|
373
|
+
const entry = asRecord(value);
|
|
374
|
+
const range = asRecord(entry["range"]);
|
|
375
|
+
const sheetId = integer(range["sheetId"]) ?? 0;
|
|
376
|
+
return compact({
|
|
377
|
+
namedRangeId: text(entry["namedRangeId"]),
|
|
378
|
+
name: text(entry["name"]),
|
|
379
|
+
sheetId,
|
|
380
|
+
range: gridRangeA1(range, titles.get(sheetId)),
|
|
381
|
+
});
|
|
382
|
+
})
|
|
383
|
+
: undefined,
|
|
384
|
+
truncated: named.length > MAX_NAMED_RANGES ? ["namedRanges[]"] : undefined,
|
|
385
|
+
});
|
|
386
|
+
}),
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
name: "get_values",
|
|
390
|
+
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.",
|
|
391
|
+
annotations: readOnly,
|
|
392
|
+
input: inputs.get_values,
|
|
393
|
+
output: outputs.get_values,
|
|
394
|
+
run: (args, ctx) => Effect.gen(function* () {
|
|
395
|
+
const ranges = args["ranges"];
|
|
396
|
+
const budget = args["maxCells"] ?? DEFAULT_PAGE_CELLS;
|
|
397
|
+
const byteBudget = args["maxBytes"] ?? DEFAULT_PAGE_BYTES;
|
|
398
|
+
const cellChars = args["maxCellChars"] ?? DEFAULT_CELL_CHARS;
|
|
399
|
+
const resume = typeof args["cursor"] === "string"
|
|
400
|
+
? decodeCursor(yield* boundCursor.decode("get_values", valuesCursorArgs(args), args["cursor"]), ranges)
|
|
401
|
+
: { i: 0, s: 0 };
|
|
402
|
+
// Every range sent is the caller's own, or the caller's own rewritten
|
|
403
|
+
// to start further down; nothing in the cursor names a range.
|
|
404
|
+
const rewritten = rangeAfter(ranges[resume.i], resume.s);
|
|
405
|
+
const sent = [rewritten ?? ranges[resume.i], ...ranges.slice(resume.i + 1)];
|
|
406
|
+
const skip = rewritten === undefined ? resume.s : 0;
|
|
407
|
+
const response = asRecord(yield* client.json({
|
|
408
|
+
method: "GET",
|
|
409
|
+
path: `${spreadsheetPath(args)}/values:batchGet`,
|
|
410
|
+
template: "/spreadsheets/{spreadsheetId}/values:batchGet",
|
|
411
|
+
query: {
|
|
412
|
+
ranges: sent,
|
|
413
|
+
majorDimension: "ROWS",
|
|
414
|
+
valueRenderOption: args["valueRenderOption"],
|
|
415
|
+
dateTimeRenderOption: args["dateTimeRenderOption"],
|
|
416
|
+
},
|
|
417
|
+
}));
|
|
418
|
+
const spreadsheetId = text(response["spreadsheetId"]) ?? args["spreadsheetId"];
|
|
419
|
+
const valueRanges = [];
|
|
420
|
+
const cut = { cells: 0 };
|
|
421
|
+
let cells = 0;
|
|
422
|
+
// maxBytes bounds the whole page as serialized, not just its cells. The
|
|
423
|
+
// envelope is reserved first at its largest: both counts at their
|
|
424
|
+
// ceiling and the longest cursor this call could end with.
|
|
425
|
+
let bytes = jsonBytes({
|
|
426
|
+
spreadsheetId,
|
|
427
|
+
valueRanges: [],
|
|
428
|
+
cellCount: MAX_PAGE_CELLS,
|
|
429
|
+
truncated: ["valueRanges[].values"],
|
|
430
|
+
page: {
|
|
431
|
+
hasMore: false,
|
|
432
|
+
nextCursor: yield* encodeCursor(ctx, args, { i: ranges.length - 1, s: Number.MAX_SAFE_INTEGER }),
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
let next;
|
|
436
|
+
const returned = asArray(response["valueRanges"]);
|
|
437
|
+
for (let index = 0; index < returned.length && next === undefined; index += 1) {
|
|
438
|
+
const valueRange = asRecord(returned[index]);
|
|
439
|
+
// Rows of the caller's range earlier pages returned, and of those,
|
|
440
|
+
// how many Google sent again and this page drops.
|
|
441
|
+
const before = index === 0 ? resume.s : 0;
|
|
442
|
+
const dropped = index === 0 ? skip : 0;
|
|
443
|
+
const rows = asArray(valueRange["values"]).slice(dropped);
|
|
444
|
+
const echoed = text(valueRange["range"]) ?? sent[index];
|
|
445
|
+
const entry = (kept) => compact({
|
|
446
|
+
range: echoed,
|
|
447
|
+
rowOffset: dropped > 0 ? dropped : undefined,
|
|
448
|
+
values: kept,
|
|
449
|
+
rowCount: kept.length,
|
|
450
|
+
});
|
|
451
|
+
// This range's own fields at their largest, and the comma before it;
|
|
452
|
+
// an empty range costs them too.
|
|
453
|
+
const fields = jsonBytes(compact({
|
|
454
|
+
range: echoed,
|
|
455
|
+
rowOffset: dropped > 0 ? dropped : undefined,
|
|
456
|
+
values: [],
|
|
457
|
+
rowCount: rows.length,
|
|
458
|
+
})) + 1;
|
|
459
|
+
if (bytes + fields > byteBudget) {
|
|
460
|
+
if (valueRanges.length === 0)
|
|
461
|
+
throw pageTooSmall(echoed, bytes + fields, byteBudget);
|
|
462
|
+
next = { i: resume.i + index, s: before };
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
bytes += fields;
|
|
466
|
+
const kept = [];
|
|
467
|
+
for (const row of rows) {
|
|
468
|
+
const empty = valueRanges.length === 0 && kept.length === 0;
|
|
469
|
+
// An empty row still costs one cell, so every page makes progress.
|
|
470
|
+
const width = Math.max(1, asArray(row).length);
|
|
471
|
+
if (cells + width > budget) {
|
|
472
|
+
if (cells === 0)
|
|
473
|
+
throw rowTooWide(echoed, dropped + kept.length + 1, width, budget);
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
const rowCut = { cells: 0 };
|
|
477
|
+
const capped = asArray(row).map((cell) => cappedCell(ctx, cell, cellChars, rowCut));
|
|
478
|
+
// The row as serialized, plus the comma before it.
|
|
479
|
+
const size = jsonBytes(capped) + 1;
|
|
480
|
+
if (bytes + size > byteBudget) {
|
|
481
|
+
if (empty)
|
|
482
|
+
throw rowTooLarge(echoed, dropped + 1, bytes + size, byteBudget);
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
cells += width;
|
|
486
|
+
bytes += size;
|
|
487
|
+
cut.cells += rowCut.cells;
|
|
488
|
+
kept.push(capped);
|
|
489
|
+
}
|
|
490
|
+
if (kept.length < rows.length) {
|
|
491
|
+
next = { i: resume.i + index, s: before + kept.length };
|
|
492
|
+
// A range this page never reached is not reported half-read; the
|
|
493
|
+
// next page starts it.
|
|
494
|
+
if (kept.length > 0)
|
|
495
|
+
valueRanges.push(entry(kept));
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
valueRanges.push(entry(kept));
|
|
499
|
+
if (cells >= budget && index + 1 < returned.length) {
|
|
500
|
+
next = { i: resume.i + index + 1, s: 0 };
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
const nextCursor = next ? yield* encodeCursor(ctx, args, next) : null;
|
|
504
|
+
return compact({
|
|
505
|
+
spreadsheetId,
|
|
506
|
+
valueRanges,
|
|
507
|
+
cellCount: cells,
|
|
508
|
+
truncated: cut.cells > 0 || next ? ["valueRanges[].values"] : undefined,
|
|
509
|
+
page: compact({ hasMore: nextCursor !== null, nextCursor: nextCursor ?? undefined }),
|
|
510
|
+
});
|
|
511
|
+
}),
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
name: "update_values",
|
|
515
|
+
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.",
|
|
516
|
+
annotations: destructive,
|
|
517
|
+
input: inputs.update_values,
|
|
518
|
+
output: outputs.update_values,
|
|
519
|
+
run: (args) => Effect.gen(function* () {
|
|
520
|
+
checkWriteSize([args["values"]]);
|
|
521
|
+
const response = asRecord(yield* client.json({
|
|
522
|
+
method: "POST",
|
|
523
|
+
path: `${spreadsheetPath(args)}/values:batchUpdate`,
|
|
524
|
+
template: "/spreadsheets/{spreadsheetId}/values:batchUpdate",
|
|
525
|
+
body: {
|
|
526
|
+
valueInputOption: args["valueInputOption"],
|
|
527
|
+
data: [{ range: args["range"], majorDimension: "ROWS", values: args["values"] }],
|
|
528
|
+
includeValuesInResponse: false,
|
|
529
|
+
},
|
|
530
|
+
}));
|
|
531
|
+
return {
|
|
532
|
+
spreadsheetId: text(response["spreadsheetId"]) ?? args["spreadsheetId"],
|
|
533
|
+
...projectUpdate(asArray(response["responses"])[0]),
|
|
534
|
+
};
|
|
535
|
+
}),
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
name: "batch_update_values",
|
|
539
|
+
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.",
|
|
540
|
+
annotations: destructive,
|
|
541
|
+
input: inputs.batch_update_values,
|
|
542
|
+
output: outputs.batch_update_values,
|
|
543
|
+
run: (args) => Effect.gen(function* () {
|
|
544
|
+
const data = asArray(args["data"]).map(asRecord);
|
|
545
|
+
checkWriteSize(data.map((entry) => entry["values"]));
|
|
546
|
+
const response = asRecord(yield* client.json({
|
|
547
|
+
method: "POST",
|
|
548
|
+
path: `${spreadsheetPath(args)}/values:batchUpdate`,
|
|
549
|
+
template: "/spreadsheets/{spreadsheetId}/values:batchUpdate",
|
|
550
|
+
body: {
|
|
551
|
+
valueInputOption: args["valueInputOption"],
|
|
552
|
+
data: data.map((entry) => ({ range: entry["range"], majorDimension: "ROWS", values: entry["values"] })),
|
|
553
|
+
includeValuesInResponse: false,
|
|
554
|
+
},
|
|
555
|
+
}));
|
|
556
|
+
return {
|
|
557
|
+
spreadsheetId: text(response["spreadsheetId"]) ?? args["spreadsheetId"],
|
|
558
|
+
totalUpdatedRows: integer(response["totalUpdatedRows"]) ?? 0,
|
|
559
|
+
totalUpdatedColumns: integer(response["totalUpdatedColumns"]) ?? 0,
|
|
560
|
+
totalUpdatedCells: integer(response["totalUpdatedCells"]) ?? 0,
|
|
561
|
+
totalUpdatedSheets: integer(response["totalUpdatedSheets"]) ?? 0,
|
|
562
|
+
updates: asArray(response["responses"]).map(projectUpdate),
|
|
563
|
+
};
|
|
564
|
+
}),
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
name: "append_values",
|
|
568
|
+
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.",
|
|
569
|
+
annotations: additive,
|
|
570
|
+
input: inputs.append_values,
|
|
571
|
+
output: outputs.append_values,
|
|
572
|
+
run: (args) => Effect.gen(function* () {
|
|
573
|
+
checkWriteSize([args["values"]]);
|
|
574
|
+
const response = asRecord(
|
|
575
|
+
// A second call after one that landed adds the rows twice.
|
|
576
|
+
yield* uncertainWrite(connection, "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({
|
|
577
|
+
method: "POST",
|
|
578
|
+
path: `${spreadsheetPath(args)}/values/${encodeURIComponent(String(args["range"]))}:append`,
|
|
579
|
+
template: "/spreadsheets/{spreadsheetId}/values/{range}:append",
|
|
580
|
+
query: {
|
|
581
|
+
valueInputOption: args["valueInputOption"],
|
|
582
|
+
// Pinned: OVERWRITE writes over whatever sits below the table.
|
|
583
|
+
insertDataOption: "INSERT_ROWS",
|
|
584
|
+
includeValuesInResponse: false,
|
|
585
|
+
},
|
|
586
|
+
body: { majorDimension: "ROWS", values: args["values"] },
|
|
587
|
+
})));
|
|
588
|
+
return compact({
|
|
589
|
+
spreadsheetId: text(response["spreadsheetId"]) ?? args["spreadsheetId"],
|
|
590
|
+
tableRange: text(response["tableRange"]),
|
|
591
|
+
...projectUpdate(response["updates"]),
|
|
592
|
+
});
|
|
593
|
+
}),
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
name: "clear_values",
|
|
597
|
+
description: "Clear the values in one or more A1 ranges, keeping formatting, notes, and validation. The values are gone; nothing is deleted or shifted.",
|
|
598
|
+
annotations: destructive,
|
|
599
|
+
input: inputs.clear_values,
|
|
600
|
+
output: outputs.clear_values,
|
|
601
|
+
run: (args) => Effect.gen(function* () {
|
|
602
|
+
const response = asRecord(yield* client.json({
|
|
603
|
+
method: "POST",
|
|
604
|
+
path: `${spreadsheetPath(args)}/values:batchClear`,
|
|
605
|
+
template: "/spreadsheets/{spreadsheetId}/values:batchClear",
|
|
606
|
+
body: { ranges: args["ranges"] },
|
|
607
|
+
}));
|
|
608
|
+
return {
|
|
609
|
+
spreadsheetId: text(response["spreadsheetId"]) ?? args["spreadsheetId"],
|
|
610
|
+
clearedRanges: asArray(response["clearedRanges"]).filter((range) => typeof range === "string"),
|
|
611
|
+
};
|
|
612
|
+
}),
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
name: "create_spreadsheet",
|
|
616
|
+
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.",
|
|
617
|
+
annotations: additive,
|
|
618
|
+
input: inputs.create_spreadsheet,
|
|
619
|
+
output: outputs.create_spreadsheet,
|
|
620
|
+
run: (args) => Effect.gen(function* () {
|
|
621
|
+
const titles = args["sheetTitles"];
|
|
622
|
+
const spreadsheet = asRecord(
|
|
623
|
+
// A spreadsheet that may exist has no id to re-read; only its title
|
|
624
|
+
// can find it, and Sheets cannot search.
|
|
625
|
+
yield* uncertainWrite(connection, "Search Drive for this title before creating it again, or a second spreadsheet results.", client.json({
|
|
626
|
+
method: "POST",
|
|
627
|
+
path: "/spreadsheets",
|
|
628
|
+
template: "/spreadsheets",
|
|
629
|
+
query: { fields: "spreadsheetId,spreadsheetUrl,properties.title,sheets.properties(sheetId,title)" },
|
|
630
|
+
body: compact({
|
|
631
|
+
properties: { title: args["title"] },
|
|
632
|
+
sheets: titles?.map((title) => ({ properties: { title } })),
|
|
633
|
+
}),
|
|
634
|
+
})));
|
|
635
|
+
return compact({
|
|
636
|
+
spreadsheetId: text(spreadsheet["spreadsheetId"]),
|
|
637
|
+
title: text(asRecord(spreadsheet["properties"])["title"]),
|
|
638
|
+
url: text(spreadsheet["spreadsheetUrl"]),
|
|
639
|
+
sheets: asArray(spreadsheet["sheets"]).map((sheet) => {
|
|
640
|
+
const { sheetId, title } = projectSheet(sheet);
|
|
641
|
+
return compact({ sheetId, title });
|
|
642
|
+
}),
|
|
643
|
+
});
|
|
644
|
+
}),
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
name: "add_sheet",
|
|
648
|
+
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.",
|
|
649
|
+
annotations: additive,
|
|
650
|
+
input: inputs.add_sheet,
|
|
651
|
+
output: outputs.add_sheet,
|
|
652
|
+
run: (args) => Effect.gen(function* () {
|
|
653
|
+
const grid = compact({ rowCount: args["rowCount"], columnCount: args["columnCount"] });
|
|
654
|
+
const response = asRecord(yield* client.json({
|
|
655
|
+
method: "POST",
|
|
656
|
+
path: `${spreadsheetPath(args)}:batchUpdate`,
|
|
657
|
+
template: "/spreadsheets/{spreadsheetId}:batchUpdate",
|
|
658
|
+
body: {
|
|
659
|
+
requests: [
|
|
660
|
+
{
|
|
661
|
+
addSheet: {
|
|
662
|
+
properties: compact({
|
|
663
|
+
title: args["title"],
|
|
664
|
+
index: args["index"],
|
|
665
|
+
gridProperties: Object.keys(grid).length > 0 ? grid : undefined,
|
|
666
|
+
}),
|
|
667
|
+
},
|
|
668
|
+
},
|
|
669
|
+
],
|
|
670
|
+
},
|
|
671
|
+
}));
|
|
672
|
+
const added = asRecord(asRecord(asArray(response["replies"])[0])["addSheet"]);
|
|
673
|
+
return {
|
|
674
|
+
spreadsheetId: text(response["spreadsheetId"]) ?? args["spreadsheetId"],
|
|
675
|
+
...projectSheet(added),
|
|
676
|
+
};
|
|
677
|
+
}),
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
name: "batch_update_spreadsheet",
|
|
681
|
+
description: "Apply Sheets batchUpdate requests — formatting, merges, sorts, deleting sheets or rows, protections — atomically. This named structural workflow can destroy data.",
|
|
682
|
+
// Destructive, whatever the requests are: they can delete sheets, rows,
|
|
683
|
+
// ranges, and protections, and connecta does not read them to guess.
|
|
684
|
+
annotations: destructive,
|
|
685
|
+
input: inputs.batch_update_spreadsheet,
|
|
686
|
+
output: outputs.batch_update_spreadsheet,
|
|
687
|
+
run: (args, ctx) => Effect.gen(function* () {
|
|
688
|
+
checkRequestKinds(args["requests"]);
|
|
689
|
+
const response = asRecord(yield* client.json({
|
|
690
|
+
method: "POST",
|
|
691
|
+
path: `${spreadsheetPath(args)}:batchUpdate`,
|
|
692
|
+
template: "/spreadsheets/{spreadsheetId}:batchUpdate",
|
|
693
|
+
body: { requests: args["requests"], includeSpreadsheetInResponse: false },
|
|
694
|
+
}));
|
|
695
|
+
return deliverableReplies(ctx, text(response["spreadsheetId"]) ?? args["spreadsheetId"], asArray(response["replies"]).map(asRecord), asArray(args["requests"]).length);
|
|
696
|
+
}),
|
|
697
|
+
},
|
|
698
|
+
];
|
|
699
|
+
return declarations.map((declaration) => {
|
|
700
|
+
const spec = {
|
|
701
|
+
name: declaration.name,
|
|
702
|
+
description: declaration.description,
|
|
703
|
+
input: declaration.input,
|
|
704
|
+
output: declaration.output,
|
|
705
|
+
run: (args) => Effect.gen(function* () {
|
|
706
|
+
const current = yield* Effect.serviceOption(Operation);
|
|
707
|
+
if (Option.isNone(current) || !current.value)
|
|
708
|
+
return yield* Effect.die(new Error("Sheets tools require a call context."));
|
|
709
|
+
return yield* declaration.run(args, current.value);
|
|
710
|
+
}).pipe(Effect.catchDefect((defect) => defect instanceof ConnectorCallError ? Effect.fail(new Refusal({ error: defect })) : Effect.die(defect))),
|
|
711
|
+
};
|
|
712
|
+
if (!declaration.annotations.readOnlyHint)
|
|
713
|
+
return write(connection, { ...spec, destructive: declaration.annotations.destructiveHint });
|
|
714
|
+
return read(connection, {
|
|
715
|
+
...spec,
|
|
716
|
+
input: Schema.Struct({
|
|
717
|
+
...declaration.input.fields,
|
|
718
|
+
fields: Schema.optionalKey(fieldsInput("Dot paths to keep; truncation and content continuation metadata remain available.")),
|
|
719
|
+
}),
|
|
720
|
+
run: (args) => Effect.gen(function* () {
|
|
721
|
+
const { fields, ...rest } = args;
|
|
722
|
+
const problem = fieldsProblem(fields, new Set(Object.keys(declaration.output.fields)), "the Sheets result");
|
|
723
|
+
if (problem)
|
|
724
|
+
return yield* Effect.fail(new Refusal({ error: problem }));
|
|
725
|
+
const result = yield* spec.run(rest);
|
|
726
|
+
return {
|
|
727
|
+
...selectFields(result, fields),
|
|
728
|
+
...(result["truncated"] ? { truncated: result["truncated"] } : {}),
|
|
729
|
+
...(declaration.name === "get_values" ? { page: result["page"] } : {}),
|
|
730
|
+
};
|
|
731
|
+
}),
|
|
732
|
+
});
|
|
733
|
+
});
|
|
734
|
+
}
|