@zackbart/connecta 0.34.0 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +393 -0
- package/README.md +62 -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 +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 +15 -7
- package/dist/connectors/api-connector.js +147 -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/option-shapes.js +1 -1
- package/dist/connectors/remote-mcp.d.ts +12 -30
- package/dist/connectors/remote-mcp.js +960 -564
- package/dist/d1.js +10 -243
- package/dist/derived-context.js +5 -0
- package/dist/describe-config.d.ts +5 -1
- package/dist/describe-config.js +3 -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 +221 -87
- 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 +534 -408
- 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 +8 -4
- package/dist/operator-ui/connection-map.js +9 -6
- package/dist/operator-ui/contract.d.ts +7 -3
- package/dist/operator-ui/endpoints.js +5 -14
- 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/health.js +52 -0
- package/dist/operator-ui/model.d.ts +2 -3
- package/dist/operator-ui/overview.js +11 -19
- package/dist/operator-ui/setup-commands.js +0 -4
- package/dist/operator-ui/view.d.ts +5 -29
- package/dist/operator-ui/view.js +3 -59
- 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 +71 -32
- package/dist/provider.js +134 -40
- 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 +8 -209
- package/dist/providers/_shared/google/workspace.js +7 -426
- 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 +3 -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 +17 -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 +7 -22
- package/dist/providers/cloudflare/index.js +80 -194
- 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 +3299 -3208
- package/dist/providers/cloudflare/{value-safety.js → safety.js} +538 -18
- 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 +3 -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 +6 -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 +7 -27
- package/dist/providers/stripe/index.js +75 -191
- 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 -10
- package/dist/providers/stripe/tools.js +77 -0
- package/dist/providers/tithely/api.js +531 -0
- package/dist/providers/tithely/index.d.ts +19 -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 +11 -25
- package/dist/providers/vercel/index.js +67 -261
- 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 +398 -376
- package/dist/providers/vercel/{value-safety.js → safety.js} +261 -42
- 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 +30 -28
- package/dist/registry.js +446 -413
- 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 +29 -22
- 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 +330 -224
- 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 +11 -99
- 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 +2550 -186
- package/documentation/auth.md +821 -98
- package/documentation/code-mode.md +56 -21
- package/documentation/deploying.md +164 -20
- package/documentation/hosted-mcp-migration.md +460 -0
- package/documentation/integrating.md +144 -105
- package/documentation/meta-tools.md +411 -34
- package/documentation/operating.md +28 -4
- package/documentation/operator-tests.md +11 -2
- package/documentation/operator-ui.md +103 -33
- package/documentation/provider-auth-migration.md +38 -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 -42
- 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 -134
- package/dist/catalog-drift.js +0 -493
- 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 -43
- 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 -1078
- 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/linear/index.d.ts +0 -55
- package/dist/providers/linear/index.js +0 -352
- package/dist/providers/linear/skill.generated.js +0 -8
- package/dist/providers/mixpanel/index.d.ts +0 -57
- package/dist/providers/mixpanel/index.js +0 -468
- 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 -65
- 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 -49
- package/dist/providers/revenuecat/index.js +0 -557
- 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,802 @@
|
|
|
1
|
+
// Google Docs named projections and bounded writes over the provider kit.
|
|
2
|
+
import { Effect, Option } from "effect";
|
|
3
|
+
import { canonicalJson } from "../../canonical-json.js";
|
|
4
|
+
import { ConnectorCallError } from "../../errors.js";
|
|
5
|
+
import { Refusal } from "../../http/errors.js";
|
|
6
|
+
import { fieldsProblem, selectFields } from "../../http/fields.js";
|
|
7
|
+
import { Recovery } from "../../http/services.js";
|
|
8
|
+
import { requestSentSecrets, redactBeforeCut } from "../../sent-secrets.js";
|
|
9
|
+
import { providerCallFailure } from "../../write-outcome.js";
|
|
10
|
+
import { googleClient, googleFailureMessage, googleOutcomeOf } from "../_shared/google/kit.js";
|
|
11
|
+
import { RESULT_BUDGET_BYTES, clampText, jsonBytes } from "../_shared/google/result-size.js";
|
|
12
|
+
import { DOCS } from "./api.js";
|
|
13
|
+
import { DOCS_INDEX } from "./lib/discovery.js";
|
|
14
|
+
import { Operation } from "../../runtime/call.js";
|
|
15
|
+
import { boundCursor } from "../../http/paging.js";
|
|
16
|
+
import { read, write } from "../../http/tools.js";
|
|
17
|
+
import * as schemas from "./lib/schemas.js";
|
|
18
|
+
/**
|
|
19
|
+
* Rendered characters kept across a document's tabs by default: at most about
|
|
20
|
+
* 60 KB of JSON even in a three-byte script, so a default read crosses the
|
|
21
|
+
* 256 KiB execute_code bridge with room for its index page and tab rows.
|
|
22
|
+
*/
|
|
23
|
+
const DEFAULT_MAX_CHARS = 20_000;
|
|
24
|
+
/**
|
|
25
|
+
* Serialized bytes of index rows in one page. Rows are a few hundred bytes at
|
|
26
|
+
* most, so a page is hundreds of them; a long document pages by cursor rather
|
|
27
|
+
* than returning more than the execute_code bridge carries.
|
|
28
|
+
*/
|
|
29
|
+
const ELEMENT_PAGE_BYTES = 64 * 1024;
|
|
30
|
+
/** Characters of each index row's text preview. */
|
|
31
|
+
const PREVIEW_CHARS = 80;
|
|
32
|
+
function asRecord(value) {
|
|
33
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
34
|
+
}
|
|
35
|
+
function asArray(value) {
|
|
36
|
+
return Array.isArray(value) ? value : [];
|
|
37
|
+
}
|
|
38
|
+
function compact(value) {
|
|
39
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
40
|
+
}
|
|
41
|
+
function text(value) {
|
|
42
|
+
return typeof value === "string" && value !== "" ? value : undefined;
|
|
43
|
+
}
|
|
44
|
+
function integer(value) {
|
|
45
|
+
return typeof value === "number" && Number.isInteger(value) ? value : undefined;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Ids Google sends back are copied into results only if a tool could take
|
|
49
|
+
* them as input again — the same pattern and length the input schemas
|
|
50
|
+
* enforce. Anything else is dropped whole and named in the result's
|
|
51
|
+
* `dropped`, never cut: a shortened id addresses something else.
|
|
52
|
+
*/
|
|
53
|
+
const ID_RULES = {
|
|
54
|
+
documentId: { pattern: /^[A-Za-z0-9_-]+$/, max: 256 },
|
|
55
|
+
revisionId: { pattern: /^\S+$/, max: 512 },
|
|
56
|
+
tabId: { pattern: /^[A-Za-z0-9._-]+$/, max: 128 },
|
|
57
|
+
};
|
|
58
|
+
function keptId(value, kind, name, dropped) {
|
|
59
|
+
const id = text(value);
|
|
60
|
+
if (id === undefined)
|
|
61
|
+
return undefined;
|
|
62
|
+
const rule = ID_RULES[kind];
|
|
63
|
+
if (id.length <= rule.max && rule.pattern.test(id))
|
|
64
|
+
return id;
|
|
65
|
+
dropped.push(name);
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
/** Titles are prose: cut to this many JSON bytes with a marker, never dropped. */
|
|
69
|
+
const TITLE_BYTES = 4 * 1024;
|
|
70
|
+
function boundedTitle(ctx, value) {
|
|
71
|
+
const title = text(value);
|
|
72
|
+
return title === undefined ? undefined : clampText(ctx, title, TITLE_BYTES, (more) => `… [${more} more characters]`);
|
|
73
|
+
}
|
|
74
|
+
function droppedField(dropped) {
|
|
75
|
+
return dropped.length > 0 ? [...new Set(dropped)].slice(0, 20) : undefined;
|
|
76
|
+
}
|
|
77
|
+
function documentUrl(documentId) {
|
|
78
|
+
return `https://docs.google.com/document/d/${encodeURIComponent(documentId)}/edit`;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* What a tab holds that its text does not show, named so a reader never
|
|
82
|
+
* mistakes the rendering for the whole document: headers, footers, floating
|
|
83
|
+
* (positioned) images, in the inline view which text is a suggestion, and any
|
|
84
|
+
* element of a kind Google added after this rendering was written. Every
|
|
85
|
+
* paragraph element kind Docs v1 defines renders as something.
|
|
86
|
+
*/
|
|
87
|
+
const NOT_RENDERED = ["headers", "footers", "positioned_objects", "suggestion_marks", "unknown_elements"];
|
|
88
|
+
const AUTO_TEXT = {
|
|
89
|
+
PAGE_NUMBER: "[page number]",
|
|
90
|
+
PAGE_COUNT: "[page count]",
|
|
91
|
+
};
|
|
92
|
+
function suggested(part) {
|
|
93
|
+
return asArray(part["suggestedInsertionIds"]).length > 0 || asArray(part["suggestedDeletionIds"]).length > 0;
|
|
94
|
+
}
|
|
95
|
+
const HEADINGS = {
|
|
96
|
+
TITLE: "# ",
|
|
97
|
+
SUBTITLE: "## ",
|
|
98
|
+
HEADING_1: "# ",
|
|
99
|
+
HEADING_2: "## ",
|
|
100
|
+
HEADING_3: "### ",
|
|
101
|
+
HEADING_4: "#### ",
|
|
102
|
+
HEADING_5: "##### ",
|
|
103
|
+
HEADING_6: "###### ",
|
|
104
|
+
};
|
|
105
|
+
/** Whether a list level numbers its items, from the tab's list definitions. */
|
|
106
|
+
function ordered(sources, bullet) {
|
|
107
|
+
const list = asRecord(sources.lists[String(bullet["listId"] ?? "")]);
|
|
108
|
+
const levels = asArray(asRecord(list["listProperties"])["nestingLevels"]);
|
|
109
|
+
const level = asRecord(levels[integer(bullet["nestingLevel"]) ?? 0]);
|
|
110
|
+
const glyph = text(level["glyphType"]);
|
|
111
|
+
return glyph !== undefined && glyph !== "NONE" && glyph !== "GLYPH_TYPE_UNSPECIFIED";
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* One paragraph element as text: a run's content, a link's text and target,
|
|
115
|
+
* a person's name, a rich link's title, and a bracketed marker for what has
|
|
116
|
+
* no text of its own.
|
|
117
|
+
*/
|
|
118
|
+
function elementText(element, state) {
|
|
119
|
+
for (const part of Object.values(element)) {
|
|
120
|
+
if (part && typeof part === "object" && suggested(asRecord(part)))
|
|
121
|
+
state.marks.suggested = true;
|
|
122
|
+
}
|
|
123
|
+
const run = asRecord(element["textRun"]);
|
|
124
|
+
if (typeof run["content"] === "string") {
|
|
125
|
+
const url = text(asRecord(asRecord(run["textStyle"])["link"])["url"]);
|
|
126
|
+
return url ? { text: run["content"], url } : { text: run["content"] };
|
|
127
|
+
}
|
|
128
|
+
if (element["person"]) {
|
|
129
|
+
const person = asRecord(asRecord(element["person"])["personProperties"]);
|
|
130
|
+
return { text: text(person["name"]) ?? text(person["email"]) ?? "[person]" };
|
|
131
|
+
}
|
|
132
|
+
if (element["richLink"]) {
|
|
133
|
+
const link = asRecord(asRecord(element["richLink"])["richLinkProperties"]);
|
|
134
|
+
const uri = text(link["uri"]);
|
|
135
|
+
const title = text(link["title"]) ?? uri ?? "link";
|
|
136
|
+
return uri ? { text: title, url: uri } : { text: title };
|
|
137
|
+
}
|
|
138
|
+
if (element["footnoteReference"]) {
|
|
139
|
+
const reference = asRecord(element["footnoteReference"]);
|
|
140
|
+
const id = text(reference["footnoteId"]);
|
|
141
|
+
const number = text(reference["footnoteNumber"]) ?? String(state.footnotes.length + 1);
|
|
142
|
+
if (id && !state.footnotes.some((entry) => entry.id === id))
|
|
143
|
+
state.footnotes.push({ id, number });
|
|
144
|
+
return { text: `[^${number}]` };
|
|
145
|
+
}
|
|
146
|
+
if (element["inlineObjectElement"]) {
|
|
147
|
+
const id = String(asRecord(element["inlineObjectElement"])["inlineObjectId"] ?? "");
|
|
148
|
+
const embedded = asRecord(asRecord(asRecord(state.sources.inlineObjects[id])["inlineObjectProperties"])["embeddedObject"]);
|
|
149
|
+
const label = text(embedded["title"]) ?? text(embedded["description"]);
|
|
150
|
+
return { text: label ? `[image: ${label}]` : "[image]" };
|
|
151
|
+
}
|
|
152
|
+
if (element["dateElement"]) {
|
|
153
|
+
const date = asRecord(asRecord(element["dateElement"])["dateElementProperties"]);
|
|
154
|
+
return { text: text(date["displayText"]) ?? "[date]" };
|
|
155
|
+
}
|
|
156
|
+
if (element["dropdown"]) {
|
|
157
|
+
const dropdown = asRecord(asRecord(element["dropdown"])["dropdownProperties"]);
|
|
158
|
+
return { text: text(dropdown["displayValue"]) ?? "[dropdown: no selection]" };
|
|
159
|
+
}
|
|
160
|
+
if (element["autoText"]) {
|
|
161
|
+
return { text: AUTO_TEXT[String(asRecord(element["autoText"])["type"] ?? "")] ?? "[auto text]" };
|
|
162
|
+
}
|
|
163
|
+
if (element["horizontalRule"])
|
|
164
|
+
return { text: "---" };
|
|
165
|
+
// The API exposes no equation content, only that one is there.
|
|
166
|
+
if (element["equation"])
|
|
167
|
+
return { text: "[equation]" };
|
|
168
|
+
if (element["pageBreak"])
|
|
169
|
+
return { text: "[page break]" };
|
|
170
|
+
if (element["columnBreak"])
|
|
171
|
+
return { text: "[column break]" };
|
|
172
|
+
if (Object.keys(element).some((key) => key !== "startIndex" && key !== "endIndex")) {
|
|
173
|
+
state.marks.unknown = true;
|
|
174
|
+
}
|
|
175
|
+
return { text: "" };
|
|
176
|
+
}
|
|
177
|
+
/** Docs' soft line break (Shift+Enter) inside one paragraph. */
|
|
178
|
+
const VERTICAL_TAB = String.fromCharCode(0x0b);
|
|
179
|
+
/**
|
|
180
|
+
* A paragraph's text with its links as `[text](url)`. Adjacent runs that
|
|
181
|
+
* share a link become one link, and the paragraph's own closing newline is
|
|
182
|
+
* dropped; a soft line break (U+000B) becomes a newline.
|
|
183
|
+
*/
|
|
184
|
+
function paragraphText(paragraph, state) {
|
|
185
|
+
const pieces = [];
|
|
186
|
+
for (const element of asArray(paragraph["elements"])) {
|
|
187
|
+
const piece = elementText(asRecord(element), state);
|
|
188
|
+
const last = pieces[pieces.length - 1];
|
|
189
|
+
if (last && last.url === piece.url)
|
|
190
|
+
last.text += piece.text;
|
|
191
|
+
else
|
|
192
|
+
pieces.push(piece);
|
|
193
|
+
}
|
|
194
|
+
const safe = requestSentSecrets(state.ctx).parts(pieces.map((piece) => piece.text));
|
|
195
|
+
for (const [index, piece] of pieces.entries()) {
|
|
196
|
+
piece.text = safe[index];
|
|
197
|
+
if (piece.url)
|
|
198
|
+
piece.url = redactBeforeCut(state.ctx, piece.url);
|
|
199
|
+
}
|
|
200
|
+
const lastPiece = pieces[pieces.length - 1];
|
|
201
|
+
if (lastPiece)
|
|
202
|
+
lastPiece.text = lastPiece.text.replace(/\n$/, "");
|
|
203
|
+
return pieces
|
|
204
|
+
.map((piece) => (piece.url && piece.text.trim() ? `[${piece.text}](${piece.url})` : piece.text))
|
|
205
|
+
.join("")
|
|
206
|
+
.replaceAll(VERTICAL_TAB, "\n");
|
|
207
|
+
}
|
|
208
|
+
/** Where to cut `value` near `max` UTF-16 units without splitting a surrogate pair. */
|
|
209
|
+
function safeCut(value, max) {
|
|
210
|
+
const code = value.charCodeAt(max - 1);
|
|
211
|
+
return max > 0 && code >= 0xd800 && code <= 0xdbff ? max - 1 : max;
|
|
212
|
+
}
|
|
213
|
+
function preview(ctx, value) {
|
|
214
|
+
// Collapsing whitespace can join a value that spans lines, so the flat text is checked too.
|
|
215
|
+
const flat = redactBeforeCut(ctx, redactBeforeCut(ctx, value).replace(/\s+/g, " ").trim());
|
|
216
|
+
return {
|
|
217
|
+
text: flat.length > PREVIEW_CHARS ? `${flat.slice(0, safeCut(flat, PREVIEW_CHARS))}…` : flat,
|
|
218
|
+
cut: flat.length > PREVIEW_CHARS,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/** Render structural elements as markdown-ish lines, collecting index rows. */
|
|
222
|
+
function renderContent(content, state, cell) {
|
|
223
|
+
const lines = [];
|
|
224
|
+
for (const value of asArray(content)) {
|
|
225
|
+
const element = asRecord(value);
|
|
226
|
+
const start = integer(element["startIndex"]) ?? 0;
|
|
227
|
+
const end = integer(element["endIndex"]) ?? start;
|
|
228
|
+
if (element["paragraph"]) {
|
|
229
|
+
const paragraph = asRecord(element["paragraph"]);
|
|
230
|
+
const style = text(asRecord(paragraph["paragraphStyle"])["namedStyleType"]);
|
|
231
|
+
const body = paragraphText(paragraph, state);
|
|
232
|
+
const bullet = paragraph["bullet"] ? asRecord(paragraph["bullet"]) : undefined;
|
|
233
|
+
let prefix = "";
|
|
234
|
+
if (bullet) {
|
|
235
|
+
prefix = `${" ".repeat(integer(bullet["nestingLevel"]) ?? 0)}${ordered(state.sources, bullet) ? "1." : "-"} `;
|
|
236
|
+
}
|
|
237
|
+
else if (style && HEADINGS[style] && body.trim()) {
|
|
238
|
+
prefix = HEADINGS[style];
|
|
239
|
+
}
|
|
240
|
+
lines.push(`${prefix}${body}`);
|
|
241
|
+
const peek = preview(state.ctx, body);
|
|
242
|
+
state.rows?.push(compact({
|
|
243
|
+
type: "paragraph",
|
|
244
|
+
startIndex: start,
|
|
245
|
+
endIndex: end,
|
|
246
|
+
style: style && style !== "NORMAL_TEXT" ? style : undefined,
|
|
247
|
+
row: cell?.row,
|
|
248
|
+
column: cell?.column,
|
|
249
|
+
text: peek.text,
|
|
250
|
+
previewCut: peek.cut || undefined,
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
else if (element["table"]) {
|
|
254
|
+
state.rows?.push(compact({ type: "table", startIndex: start, endIndex: end, row: cell?.row, column: cell?.column }));
|
|
255
|
+
const rows = asArray(asRecord(element["table"])["tableRows"]);
|
|
256
|
+
rows.forEach((rowValue, rowIndex) => {
|
|
257
|
+
const cells = asArray(asRecord(rowValue)["tableCells"]).map((cellValue, columnIndex) => renderContent(asRecord(cellValue)["content"], state, { row: rowIndex, column: columnIndex })
|
|
258
|
+
.join("<br>")
|
|
259
|
+
.replace(/\n/g, "<br>")
|
|
260
|
+
.replace(/\|/g, "\\|")
|
|
261
|
+
.trim());
|
|
262
|
+
lines.push(`| ${cells.join(" | ")} |`);
|
|
263
|
+
if (rowIndex === 0)
|
|
264
|
+
lines.push(`|${cells.map(() => " --- |").join("")}`);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
else if (element["tableOfContents"]) {
|
|
268
|
+
state.rows?.push(compact({ type: "table_of_contents", startIndex: start, endIndex: end }));
|
|
269
|
+
// The headings it lists are rendered where they stand.
|
|
270
|
+
lines.push("[Table of contents]");
|
|
271
|
+
}
|
|
272
|
+
else if (!element["sectionBreak"] && Object.keys(element).some((key) => !key.endsWith("Index"))) {
|
|
273
|
+
state.marks.unknown = true;
|
|
274
|
+
}
|
|
275
|
+
// A section break has no text and is not an edit target.
|
|
276
|
+
}
|
|
277
|
+
return lines;
|
|
278
|
+
}
|
|
279
|
+
/** One tab's body as text, with its footnotes after it. */
|
|
280
|
+
function renderTab(ctx, tab, sources, rows) {
|
|
281
|
+
const marks = { suggested: false, unknown: false };
|
|
282
|
+
const state = { ctx, sources, footnotes: [], rows, marks };
|
|
283
|
+
const lines = renderContent(asRecord(tab["body"])["content"], state);
|
|
284
|
+
const notes = state.footnotes.map(({ id, number }) => {
|
|
285
|
+
const footnote = asRecord(sources.footnotes[id]);
|
|
286
|
+
// Footnote content is its own segment: rendered, never indexed.
|
|
287
|
+
const inner = { ctx, sources, footnotes: [], rows: undefined, marks };
|
|
288
|
+
return `[^${number}]: ${renderContent(footnote["content"], inner).join(" ").trim()}`;
|
|
289
|
+
});
|
|
290
|
+
const present = (key) => Object.keys(asRecord(tab[key])).length > 0;
|
|
291
|
+
const notRendered = NOT_RENDERED.filter((part) => part === "suggestion_marks"
|
|
292
|
+
? marks.suggested
|
|
293
|
+
: part === "unknown_elements"
|
|
294
|
+
? marks.unknown
|
|
295
|
+
: present(part === "positioned_objects" ? "positionedObjects" : part));
|
|
296
|
+
return {
|
|
297
|
+
text: [...lines, ...(notes.length > 0 ? ["", ...notes] : [])]
|
|
298
|
+
.join("\n")
|
|
299
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
300
|
+
.trim(),
|
|
301
|
+
notRendered,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Every tab, depth first in the order the document shows them. A response
|
|
306
|
+
* without tabs — a document read without `includeTabsContent` — is one
|
|
307
|
+
* untitled tab holding the legacy top-level body.
|
|
308
|
+
*/
|
|
309
|
+
function flattenTabs(document) {
|
|
310
|
+
const tabs = [];
|
|
311
|
+
const visit = (value, depth) => {
|
|
312
|
+
const tab = asRecord(value);
|
|
313
|
+
const properties = asRecord(tab["tabProperties"]);
|
|
314
|
+
tabs.push({
|
|
315
|
+
tabId: text(properties["tabId"]),
|
|
316
|
+
title: text(properties["title"]),
|
|
317
|
+
parentTabId: text(properties["parentTabId"]),
|
|
318
|
+
nestingLevel: integer(properties["nestingLevel"]),
|
|
319
|
+
documentTab: asRecord(tab["documentTab"]),
|
|
320
|
+
});
|
|
321
|
+
if (depth > 20)
|
|
322
|
+
return;
|
|
323
|
+
for (const child of asArray(tab["childTabs"]))
|
|
324
|
+
visit(child, depth + 1);
|
|
325
|
+
};
|
|
326
|
+
for (const tab of asArray(document["tabs"]))
|
|
327
|
+
visit(tab, 0);
|
|
328
|
+
if (tabs.length === 0) {
|
|
329
|
+
tabs.push({
|
|
330
|
+
tabId: undefined,
|
|
331
|
+
title: undefined,
|
|
332
|
+
parentTabId: undefined,
|
|
333
|
+
nestingLevel: undefined,
|
|
334
|
+
documentTab: document,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
return tabs;
|
|
338
|
+
}
|
|
339
|
+
/** The index just past a body's last element: insert before `endIndex - 1`. */
|
|
340
|
+
function endIndexOf(body) {
|
|
341
|
+
const content = asArray(asRecord(body)["content"]);
|
|
342
|
+
return integer(asRecord(content[content.length - 1])["endIndex"]);
|
|
343
|
+
}
|
|
344
|
+
/** Cut text at `max` characters, never inside a surrogate pair, and say so. Every secret the
|
|
345
|
+
* request sent is replaced first: a cut through an echo leaves a fragment nothing redacts. */
|
|
346
|
+
function capped(ctx, whole, max) {
|
|
347
|
+
if (whole.length <= max)
|
|
348
|
+
return { text: whole, textTruncated: false };
|
|
349
|
+
const value = redactBeforeCut(ctx, whole);
|
|
350
|
+
if (value.length <= max)
|
|
351
|
+
return { text: value, textTruncated: false };
|
|
352
|
+
const cut = safeCut(value, max);
|
|
353
|
+
const rest = value.length - cut;
|
|
354
|
+
return {
|
|
355
|
+
text: `${value.slice(0, cut)}${cut > 0 ? "\n" : ""}[… ${rest} more characters truncated; raise maxChars or pass tabId to read them]`,
|
|
356
|
+
textTruncated: true,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
const SUGGESTION_MODES = {
|
|
360
|
+
inline: "SUGGESTIONS_INLINE",
|
|
361
|
+
accepted: "PREVIEW_SUGGESTIONS_ACCEPTED",
|
|
362
|
+
rejected: "PREVIEW_WITHOUT_SUGGESTIONS",
|
|
363
|
+
};
|
|
364
|
+
// --- Editing -----------------------------------------------------------------------
|
|
365
|
+
function batchPath(documentId) {
|
|
366
|
+
return `/documents/${encodeURIComponent(documentId)}:batchUpdate`;
|
|
367
|
+
}
|
|
368
|
+
/** The revision a batchUpdate left the document at, when Google reports a usable one. */
|
|
369
|
+
function revisionAfter(reply, dropped) {
|
|
370
|
+
return keptId(asRecord(reply["writeControl"])["requiredRevisionId"], "revisionId", "revisionId", dropped);
|
|
371
|
+
}
|
|
372
|
+
// No cause: what it would carry is a transport, stream, or parser error that can quote the reply (#695).
|
|
373
|
+
function uncertain(message, ctx) {
|
|
374
|
+
return providerCallFailure(ctx, { title: "Google Docs" }, new ConnectorCallError("connector_call_failed", message, { retryable: false }), {
|
|
375
|
+
dispatched: true,
|
|
376
|
+
status: 200,
|
|
377
|
+
fallback: "write",
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
function writeOutcome(error) {
|
|
381
|
+
const outcome = googleOutcomeOf(error);
|
|
382
|
+
switch (outcome?.phase) {
|
|
383
|
+
case "before-send":
|
|
384
|
+
return "not-sent";
|
|
385
|
+
case "refused": {
|
|
386
|
+
const status = outcome.status;
|
|
387
|
+
return status !== undefined && status >= 400 && status < 500 ? "refused" : "unknown";
|
|
388
|
+
}
|
|
389
|
+
case "reading-body":
|
|
390
|
+
return "probably-applied";
|
|
391
|
+
case "awaiting-response":
|
|
392
|
+
case "server-error":
|
|
393
|
+
case "redirected":
|
|
394
|
+
case "unauthorized":
|
|
395
|
+
default:
|
|
396
|
+
return "unknown";
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* One `documents.batchUpdate`, atomic on Google's side: every request applies
|
|
401
|
+
* or none does. A write naming `requiredRevisionId` is sent revision-guarded,
|
|
402
|
+
* so the shared client reads Google's own reason (FAILED_PRECONDITION or
|
|
403
|
+
* ABORTED on a 400 or 409) and answers a stale revision with `conflict`.
|
|
404
|
+
* Nothing here infers one: a revision read afterwards would prove nothing,
|
|
405
|
+
* since a collaborator may have edited in between. See {@link writeOutcome}
|
|
406
|
+
* for what every other failure means.
|
|
407
|
+
*/
|
|
408
|
+
function batchUpdate(client, documentId, requests, requiredRevisionId, step) {
|
|
409
|
+
return client
|
|
410
|
+
.json({
|
|
411
|
+
method: "POST",
|
|
412
|
+
path: batchPath(documentId),
|
|
413
|
+
template: "/documents/{documentId}:batchUpdate",
|
|
414
|
+
body: compact({ requests, writeControl: requiredRevisionId ? { requiredRevisionId } : undefined }),
|
|
415
|
+
}, { ...(requiredRevisionId !== undefined ? { revisionGuarded: true } : {}), ...(step ? { step } : {}) })
|
|
416
|
+
.pipe(Effect.map(asRecord));
|
|
417
|
+
}
|
|
418
|
+
/** Characters of one projected reply string: an id or a short value, never prose. */
|
|
419
|
+
const REPLY_STRING_CHARS = 512;
|
|
420
|
+
/**
|
|
421
|
+
* One batchUpdate reply kept to what a later request needs: its kind, and
|
|
422
|
+
* the ids and counts beneath it to two levels. A string too long to be an id
|
|
423
|
+
* is dropped whole, never cut into a different one; `truncated` names
|
|
424
|
+
* the projected replies. Most replies are `{}` or one id; this only
|
|
425
|
+
* bites on the rare kind that echoes more.
|
|
426
|
+
*/
|
|
427
|
+
function projectReply(value, depth = 0) {
|
|
428
|
+
const kept = {};
|
|
429
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
430
|
+
if (typeof entry === "string") {
|
|
431
|
+
if (entry.length <= REPLY_STRING_CHARS)
|
|
432
|
+
kept[key] = entry;
|
|
433
|
+
}
|
|
434
|
+
else if (typeof entry === "number" || typeof entry === "boolean") {
|
|
435
|
+
kept[key] = entry;
|
|
436
|
+
}
|
|
437
|
+
else if (entry && typeof entry === "object" && !Array.isArray(entry) && depth < 2) {
|
|
438
|
+
kept[key] = projectReply(entry, depth + 1);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return kept;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* A successful batch's result, inside the delivery budget whatever Google
|
|
445
|
+
* echoed. Replies come back whole when they fit; otherwise projected to ids
|
|
446
|
+
* and counts; otherwise the longest prefix that fits, so `replies[i]` still
|
|
447
|
+
* answers `requests[i]`. A cut result still says the batch applied — it did.
|
|
448
|
+
*/
|
|
449
|
+
function boundedReplies(saved, replies) {
|
|
450
|
+
const whole = { ...saved, replies };
|
|
451
|
+
if (jsonBytes(whole) <= RESULT_BUDGET_BYTES)
|
|
452
|
+
return whole;
|
|
453
|
+
const projected = replies.map((reply) => projectReply(reply));
|
|
454
|
+
const slim = { ...saved, replies: projected, truncated: ["replies[]"] };
|
|
455
|
+
const notice = (shown) => `The batch applied. ${shown === replies.length ? "Every" : `${shown} of ${replies.length}`} replies ${shown === replies.length ? "is" : "are"} shown, projected to ids and counts, to stay inside the result budget. Do not repeat it; re-read the document with get_document for anything else.`;
|
|
456
|
+
if (jsonBytes({ ...slim, notice: notice(replies.length) }) <= RESULT_BUDGET_BYTES) {
|
|
457
|
+
return { ...slim, notice: notice(replies.length) };
|
|
458
|
+
}
|
|
459
|
+
// The longest prefix whose complete result — its own notice included —
|
|
460
|
+
// fits. JSON adds each element's bytes plus one comma between elements,
|
|
461
|
+
// so a candidate's size is exact without re-serializing it.
|
|
462
|
+
const sizes = projected.map((reply) => jsonBytes(reply));
|
|
463
|
+
const sizeWith = (count) => jsonBytes({ ...slim, replies: [], notice: notice(count) }) +
|
|
464
|
+
sizes.slice(0, count).reduce((sum, size) => sum + size, 0) +
|
|
465
|
+
Math.max(0, count - 1);
|
|
466
|
+
let count = 0;
|
|
467
|
+
while (count < projected.length && sizeWith(count + 1) <= RESULT_BUDGET_BYTES)
|
|
468
|
+
count += 1;
|
|
469
|
+
return { ...slim, replies: projected.slice(0, count), notice: notice(count) };
|
|
470
|
+
}
|
|
471
|
+
/** An edit's result: the document it addressed, and the revision it left, if usable. */
|
|
472
|
+
function savedEdit(documentId, reply) {
|
|
473
|
+
const dropped = [];
|
|
474
|
+
return compact({ documentId, revisionId: revisionAfter(reply, dropped), dropped: droppedField(dropped) });
|
|
475
|
+
}
|
|
476
|
+
// --- Tools -------------------------------------------------------------------------
|
|
477
|
+
/**
|
|
478
|
+
* Connecta's ceiling on a rendered read: inside the 8 MiB stash a
|
|
479
|
+
* direct call pages from, with room for its envelope.
|
|
480
|
+
*/
|
|
481
|
+
const MAX_READ_RESULT_BYTES = 4 * 1024 * 1024;
|
|
482
|
+
/**
|
|
483
|
+
* The one check every result passes on its way out, whatever built it. The
|
|
484
|
+
* tools bound what they copy, so this is a backstop, not a plan. A read
|
|
485
|
+
* stopped here refuses with its way out; a write was already applied, so it
|
|
486
|
+
* is acknowledged with only its smallest facts and told not to repeat.
|
|
487
|
+
*/
|
|
488
|
+
function delivered(name, args, result) {
|
|
489
|
+
const read = name === "get_document";
|
|
490
|
+
// A default read must also cross the execute_code bridge; an explicit
|
|
491
|
+
// maxChars is for a direct call, which the stash pages.
|
|
492
|
+
const explicit = read && args["maxChars"] !== undefined;
|
|
493
|
+
const limit = explicit ? MAX_READ_RESULT_BYTES : RESULT_BUDGET_BYTES;
|
|
494
|
+
const bytes = jsonBytes(result);
|
|
495
|
+
if (bytes <= limit)
|
|
496
|
+
return result;
|
|
497
|
+
if (read) {
|
|
498
|
+
throw new ConnectorCallError("invalid_args", `This document's result is ${bytes} bytes, past connecta's ${limit}-byte ceiling for it. Pass tabId for one tab, or a smaller maxChars.`);
|
|
499
|
+
}
|
|
500
|
+
const full = asRecord(result);
|
|
501
|
+
const small = {};
|
|
502
|
+
for (const key of ["documentId", "url", "occurrencesChanged"]) {
|
|
503
|
+
const value = full[key];
|
|
504
|
+
if (typeof value === "number" || (typeof value === "string" && value.length <= 1024))
|
|
505
|
+
small[key] = value;
|
|
506
|
+
}
|
|
507
|
+
if (name === "batch_update_document") {
|
|
508
|
+
small["replies"] = [];
|
|
509
|
+
small["truncated"] = ["replies[]"];
|
|
510
|
+
}
|
|
511
|
+
small["notice"] =
|
|
512
|
+
`Google Docs applied this ${name === "create_document" ? "create" : "edit"}, but its full result (${bytes} bytes) is too large to return. Do not repeat it; re-read the document with get_document.`;
|
|
513
|
+
return small;
|
|
514
|
+
}
|
|
515
|
+
export function docsTools({ connection }) {
|
|
516
|
+
const client = googleClient(connection, DOCS.provider, { standard: true, index: DOCS_INDEX });
|
|
517
|
+
return toolDefinitions(connection, client).map((tool) => {
|
|
518
|
+
const isRead = tool.annotations.readOnlyHint === true;
|
|
519
|
+
const spec = {
|
|
520
|
+
name: tool.name,
|
|
521
|
+
description: tool.description,
|
|
522
|
+
input: tool.input,
|
|
523
|
+
output: tool.output,
|
|
524
|
+
...(tool.writeSteps ? { writeSteps: tool.writeSteps } : {}),
|
|
525
|
+
run: (args) => Effect.gen(function* () {
|
|
526
|
+
const fields = isRead ? args["fields"] : undefined;
|
|
527
|
+
const problem = isRead
|
|
528
|
+
? fieldsProblem(fields, new Set(Object.keys(schemas.GET_DOCUMENT_OUTPUT.fields)), "the result")
|
|
529
|
+
: undefined;
|
|
530
|
+
if (problem)
|
|
531
|
+
return yield* Effect.fail(new Refusal({ error: problem }));
|
|
532
|
+
const operation = yield* Effect.serviceOption(Operation);
|
|
533
|
+
if (Option.isNone(operation) || !operation.value)
|
|
534
|
+
return yield* Effect.die(new Error("Docs needs a call context."));
|
|
535
|
+
const result = delivered(tool.name, args, yield* tool.run(args, operation.value));
|
|
536
|
+
if (!isRead)
|
|
537
|
+
return result;
|
|
538
|
+
const record = asRecord(result);
|
|
539
|
+
return {
|
|
540
|
+
...asRecord(selectFields(record, fields)),
|
|
541
|
+
...(record["page"] ? { page: record["page"] } : {}),
|
|
542
|
+
...(record["truncated"] ? { truncated: record["truncated"] } : {}),
|
|
543
|
+
};
|
|
544
|
+
}).pipe(Effect.catchDefect((defect) => defect instanceof ConnectorCallError ? Effect.fail(new Refusal({ error: defect })) : Effect.die(defect))),
|
|
545
|
+
};
|
|
546
|
+
return isRead
|
|
547
|
+
? read(connection, spec)
|
|
548
|
+
: write(connection, { ...spec, destructive: tool.annotations.destructiveHint !== false });
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
function toolDefinitions(connection, client) {
|
|
552
|
+
const revision = (args) => typeof args["requiredRevisionId"] === "string" ? args["requiredRevisionId"] : undefined;
|
|
553
|
+
return [
|
|
554
|
+
{
|
|
555
|
+
name: "get_document",
|
|
556
|
+
description: "Get one Google Doc as readable markdown-ish text per tab, with its title and revisionId; withIndexes adds the start/end indexes edits target. Cannot search or list documents.",
|
|
557
|
+
annotations: { readOnlyHint: true },
|
|
558
|
+
input: schemas.GET_DOCUMENT_INPUT,
|
|
559
|
+
output: schemas.GET_DOCUMENT_OUTPUT,
|
|
560
|
+
run: (args, ctx) => Effect.gen(function* () {
|
|
561
|
+
const documentId = String(args["documentId"]);
|
|
562
|
+
if (args["cursor"] !== undefined && args["withIndexes"] !== true) {
|
|
563
|
+
throw new ConnectorCallError("invalid_args", "cursor pages index rows; send it with withIndexes: true.");
|
|
564
|
+
}
|
|
565
|
+
const binding = { ...args, fields: undefined };
|
|
566
|
+
let from = 0;
|
|
567
|
+
let expectedSnapshot;
|
|
568
|
+
if (typeof args["cursor"] === "string") {
|
|
569
|
+
const payload = yield* boundCursor.decode("get_document", binding, args["cursor"]);
|
|
570
|
+
let parsed;
|
|
571
|
+
try {
|
|
572
|
+
parsed = JSON.parse(payload);
|
|
573
|
+
}
|
|
574
|
+
catch {
|
|
575
|
+
throw new ConnectorCallError("invalid_args", "Invalid document cursor; start a fresh indexed read.");
|
|
576
|
+
}
|
|
577
|
+
const claim = asRecord(parsed);
|
|
578
|
+
if (typeof claim["snapshot"] !== "string" || !Number.isSafeInteger(claim["from"]) || claim["from"] <= 0)
|
|
579
|
+
throw new ConnectorCallError("invalid_args", "Invalid document cursor position.");
|
|
580
|
+
from = claim["from"];
|
|
581
|
+
expectedSnapshot = claim["snapshot"];
|
|
582
|
+
}
|
|
583
|
+
const suggestions = typeof args["suggestions"] === "string" ? SUGGESTION_MODES[args["suggestions"]] : undefined;
|
|
584
|
+
const document = asRecord(yield* client.json({
|
|
585
|
+
method: "GET",
|
|
586
|
+
path: `/documents/${encodeURIComponent(documentId)}`,
|
|
587
|
+
template: "/documents/{documentId}",
|
|
588
|
+
query: { includeTabsContent: true, suggestionsViewMode: suggestions },
|
|
589
|
+
}));
|
|
590
|
+
let tabs = flattenTabs(document);
|
|
591
|
+
const wanted = typeof args["tabId"] === "string" ? args["tabId"] : undefined;
|
|
592
|
+
if (wanted !== undefined) {
|
|
593
|
+
tabs = tabs.filter((tab) => tab.tabId === wanted);
|
|
594
|
+
if (tabs.length === 0) {
|
|
595
|
+
const known = flattenTabs(document)
|
|
596
|
+
.map((tab) => keptId(tab.tabId, "tabId", "tabId", []))
|
|
597
|
+
.filter((id) => id !== undefined);
|
|
598
|
+
throw new ConnectorCallError("not_found", `This document has no tab ${wanted}. Its tabs are: ${known.slice(0, 20).join(", ") || "none listed"}${known.length > 20 ? ", …" : ""}.`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
const withIndexes = args["withIndexes"] === true;
|
|
602
|
+
let budget = typeof args["maxChars"] === "number" ? args["maxChars"] : DEFAULT_MAX_CHARS;
|
|
603
|
+
// Index rows form one sequence across tabs; a page is the rows from
|
|
604
|
+
// the cursor's ordinal until the byte budget is spent.
|
|
605
|
+
const snapshot = withIndexes
|
|
606
|
+
? yield* Effect.promise(async () => {
|
|
607
|
+
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonicalJson(document)));
|
|
608
|
+
return Array.from(new Uint8Array(hash), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
609
|
+
})
|
|
610
|
+
: undefined;
|
|
611
|
+
if (expectedSnapshot !== undefined && expectedSnapshot !== snapshot)
|
|
612
|
+
throw new ConnectorCallError("conflict", "This document changed since the indexed read. Start again without a cursor and plan edits at the new revision.");
|
|
613
|
+
let ordinal = 0;
|
|
614
|
+
let pageBytes = 0;
|
|
615
|
+
let next;
|
|
616
|
+
const dropped = [];
|
|
617
|
+
const truncated = [];
|
|
618
|
+
const title = boundedTitle(ctx, document["title"]);
|
|
619
|
+
if (title !== undefined && title !== document["title"])
|
|
620
|
+
truncated.push("title");
|
|
621
|
+
const rendered = tabs.map((tab, index) => {
|
|
622
|
+
const rows = withIndexes ? [] : undefined;
|
|
623
|
+
const sources = {
|
|
624
|
+
lists: asRecord(tab.documentTab["lists"]),
|
|
625
|
+
footnotes: asRecord(tab.documentTab["footnotes"]),
|
|
626
|
+
inlineObjects: asRecord(tab.documentTab["inlineObjects"]),
|
|
627
|
+
};
|
|
628
|
+
const { text: full, notRendered } = renderTab(ctx, tab.documentTab, sources, rows);
|
|
629
|
+
const cut = capped(ctx, full, budget);
|
|
630
|
+
const tabTitle = boundedTitle(ctx, tab.title);
|
|
631
|
+
if (tabTitle !== undefined && tabTitle !== tab.title)
|
|
632
|
+
truncated.push(`tabs[${index}].title`);
|
|
633
|
+
budget = Math.max(0, budget - full.length);
|
|
634
|
+
if (cut.textTruncated)
|
|
635
|
+
truncated.push(`tabs[${index}].text`);
|
|
636
|
+
let elements;
|
|
637
|
+
if (rows) {
|
|
638
|
+
elements = [];
|
|
639
|
+
for (const row of rows) {
|
|
640
|
+
const at = ordinal;
|
|
641
|
+
ordinal += 1;
|
|
642
|
+
if (at < from || next !== undefined)
|
|
643
|
+
continue;
|
|
644
|
+
const size = jsonBytes(row) + 1;
|
|
645
|
+
if (pageBytes + size > ELEMENT_PAGE_BYTES) {
|
|
646
|
+
next = at;
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
pageBytes += size;
|
|
650
|
+
const { previewCut, ...kept } = row;
|
|
651
|
+
if (previewCut)
|
|
652
|
+
truncated.push(`tabs[${index}].elements[].text`);
|
|
653
|
+
elements.push(kept);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return compact({
|
|
657
|
+
tabId: keptId(tab.tabId, "tabId", `tabs[${index}].tabId`, dropped),
|
|
658
|
+
title: tabTitle,
|
|
659
|
+
parentTabId: keptId(tab.parentTabId, "tabId", `tabs[${index}].parentTabId`, dropped),
|
|
660
|
+
nestingLevel: tab.nestingLevel,
|
|
661
|
+
endIndex: endIndexOf(tab.documentTab["body"]),
|
|
662
|
+
text: cut.text,
|
|
663
|
+
notRendered: notRendered.length > 0 ? notRendered : undefined,
|
|
664
|
+
elements,
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
if (from > ordinal)
|
|
668
|
+
throw new ConnectorCallError("invalid_args", "Invalid document cursor position.");
|
|
669
|
+
const nextCursor = next === undefined
|
|
670
|
+
? undefined
|
|
671
|
+
: yield* boundCursor.encode("get_document", binding, JSON.stringify({ snapshot, from: next }));
|
|
672
|
+
return compact({
|
|
673
|
+
documentId,
|
|
674
|
+
title,
|
|
675
|
+
revisionId: keptId(document["revisionId"], "revisionId", "revisionId", dropped),
|
|
676
|
+
url: documentUrl(documentId),
|
|
677
|
+
dropped: droppedField(dropped),
|
|
678
|
+
tabs: rendered,
|
|
679
|
+
truncated: truncated.length ? [...new Set(truncated)] : undefined,
|
|
680
|
+
page: withIndexes ? compact({ hasMore: next !== undefined, nextCursor }) : undefined,
|
|
681
|
+
});
|
|
682
|
+
}),
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
name: "create_document",
|
|
686
|
+
writeSteps: { create: "once", initialText: "once" },
|
|
687
|
+
description: "Create a new Google Doc in the user's My Drive root with a title and optional plain-text body. Cannot choose a folder, apply a template, or copy a document.",
|
|
688
|
+
// Additive: a new document changes nothing that existed. Not read-only,
|
|
689
|
+
// so program calls require a writable endpoint; other writes use
|
|
690
|
+
// call_destructive_tool. The host controls approval.
|
|
691
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
692
|
+
input: schemas.CREATE_DOCUMENT_INPUT,
|
|
693
|
+
output: schemas.CREATE_DOCUMENT_OUTPUT,
|
|
694
|
+
run: (args, ctx) => Effect.gen(function* () {
|
|
695
|
+
// Google's create takes a title and ignores any content, so a body is
|
|
696
|
+
// a second, separate edit. Creating is not idempotent: every failure
|
|
697
|
+
// that may have left a document behind says so and is not retryable.
|
|
698
|
+
const title = String(args["title"]);
|
|
699
|
+
const createdButUnknown = `Google Docs created the document (HTTP 2xx), but its reply could not be read, so its id is unknown. Do not create it again; find "${title}" in Drive (a Google Drive connection's search) and use that id.`;
|
|
700
|
+
const created = yield* client
|
|
701
|
+
.json({ method: "POST", path: "/documents", template: "/documents", body: { title } }, { step: "create" })
|
|
702
|
+
.pipe(Effect.map(asRecord), Effect.catch((failure) => !ctx.signal?.aborted && writeOutcome(failure) === "probably-applied"
|
|
703
|
+
? googleFailureMessage(connection, failure, () => createdButUnknown)
|
|
704
|
+
: Effect.fail(failure)));
|
|
705
|
+
const dropped = [];
|
|
706
|
+
// An id that could not be passed back is no id at all.
|
|
707
|
+
const documentId = keptId(created["documentId"], "documentId", "documentId", dropped);
|
|
708
|
+
if (!documentId)
|
|
709
|
+
throw uncertain(createdButUnknown, ctx);
|
|
710
|
+
yield* (yield* Recovery).record({ documentId });
|
|
711
|
+
let revisionId = keptId(created["revisionId"], "revisionId", "revisionId", dropped);
|
|
712
|
+
if (typeof args["text"] === "string") {
|
|
713
|
+
const reply = yield* batchUpdate(client, documentId, [{ insertText: { text: args["text"], endOfSegmentLocation: {} } }], undefined, "initialText").pipe(Effect.catch((cause) => {
|
|
714
|
+
// The document exists now. A retry of this call would make a
|
|
715
|
+
// second one, so the failure names it and is not retryable. Only
|
|
716
|
+
// an explicit refusal proves the text is absent; anything else
|
|
717
|
+
// sends the caller to look before appending.
|
|
718
|
+
return googleFailureMessage(connection, cause, (detail) => {
|
|
719
|
+
const sentence = /[.!?]$/.test(detail.trim()) ? detail.trim() : `${detail.trim()}.`;
|
|
720
|
+
return ["refused", "not-sent"].includes(writeOutcome(cause))
|
|
721
|
+
? `Created document ${documentId}, but Google refused its initial text: ${sentence} Do not create it again; add the text with append_text on ${documentId}.`
|
|
722
|
+
: `Created document ${documentId}, but its initial text was not confirmed: ${sentence} Do not create it again; read ${documentId} with get_document, and append the text with append_text only if it is missing.`;
|
|
723
|
+
});
|
|
724
|
+
}));
|
|
725
|
+
revisionId = revisionAfter(reply, dropped) ?? revisionId;
|
|
726
|
+
}
|
|
727
|
+
return compact({
|
|
728
|
+
documentId,
|
|
729
|
+
// The title this call sent, not Google's echo of it.
|
|
730
|
+
title,
|
|
731
|
+
revisionId,
|
|
732
|
+
url: documentUrl(documentId),
|
|
733
|
+
dropped: droppedField(dropped),
|
|
734
|
+
});
|
|
735
|
+
}),
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
name: "append_text",
|
|
739
|
+
description: "Append plain text to the end of a Google Doc's body, or of one tab. It joins the last paragraph; start with \\n for a new one. Never removes or restyles text.",
|
|
740
|
+
// Additive: the text lands after everything that was there.
|
|
741
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
742
|
+
input: schemas.APPEND_TEXT_INPUT,
|
|
743
|
+
output: schemas.APPEND_TEXT_OUTPUT,
|
|
744
|
+
run: (args) => Effect.gen(function* () {
|
|
745
|
+
const reply = yield* batchUpdate(client, args["documentId"], [{ insertText: { text: args["text"], endOfSegmentLocation: compact({ tabId: args["tabId"] }) } }], revision(args));
|
|
746
|
+
return savedEdit(args["documentId"], reply);
|
|
747
|
+
}),
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
name: "insert_text",
|
|
751
|
+
description: "Insert plain text at one UTF-16 index of a Google Doc's body, from get_document withIndexes. Inserts only; nothing existing is removed or restyled.",
|
|
752
|
+
// Additive: text goes in at a point; nothing that was there is lost.
|
|
753
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
754
|
+
input: schemas.INSERT_TEXT_INPUT,
|
|
755
|
+
output: schemas.INSERT_TEXT_OUTPUT,
|
|
756
|
+
run: (args) => Effect.gen(function* () {
|
|
757
|
+
const reply = yield* batchUpdate(client, args["documentId"], [{ insertText: { text: args["text"], location: compact({ index: args["index"], tabId: args["tabId"] }) } }], revision(args));
|
|
758
|
+
return savedEdit(args["documentId"], reply);
|
|
759
|
+
}),
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
name: "replace_all_text",
|
|
763
|
+
description: "Replace every occurrence of a string (or regex) in a Google Doc, in all tabs or the listed ones, and report how many changed. Overwrites text; no preview.",
|
|
764
|
+
// Destructive: the matched text is gone, and a broad match can rewrite
|
|
765
|
+
// far more of the document than was meant.
|
|
766
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
767
|
+
input: schemas.REPLACE_ALL_TEXT_INPUT,
|
|
768
|
+
output: schemas.REPLACE_ALL_TEXT_OUTPUT,
|
|
769
|
+
run: (args) => Effect.gen(function* () {
|
|
770
|
+
const reply = yield* batchUpdate(client, args["documentId"], [
|
|
771
|
+
{
|
|
772
|
+
replaceAllText: compact({
|
|
773
|
+
containsText: compact({
|
|
774
|
+
text: args["find"],
|
|
775
|
+
matchCase: args["matchCase"] === true,
|
|
776
|
+
searchByRegex: args["regex"] === true ? true : undefined,
|
|
777
|
+
}),
|
|
778
|
+
replaceText: args["replaceWith"],
|
|
779
|
+
tabsCriteria: Array.isArray(args["tabIds"]) ? { tabIds: args["tabIds"] } : undefined,
|
|
780
|
+
}),
|
|
781
|
+
},
|
|
782
|
+
], revision(args));
|
|
783
|
+
const changed = integer(asRecord(asRecord(asArray(reply["replies"])[0])["replaceAllText"])["occurrencesChanged"]);
|
|
784
|
+
// Google omits a zero count from the reply.
|
|
785
|
+
return { ...savedEdit(args["documentId"], reply), occurrencesChanged: changed ?? 0 };
|
|
786
|
+
}),
|
|
787
|
+
},
|
|
788
|
+
{
|
|
789
|
+
name: "batch_update_document",
|
|
790
|
+
description: "Apply raw Docs API documents.batchUpdate requests to one Google Doc at a required revision, atomically: all apply or none. Always destructive; prefer named edits.",
|
|
791
|
+
// Destructive by construction: a raw request can delete ranges, restyle
|
|
792
|
+
// the whole document, or drop tables, and no schema here can tell which.
|
|
793
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
794
|
+
input: schemas.BATCH_UPDATE_DOCUMENT_INPUT,
|
|
795
|
+
output: schemas.BATCH_UPDATE_DOCUMENT_OUTPUT,
|
|
796
|
+
run: (args) => Effect.gen(function* () {
|
|
797
|
+
const reply = yield* batchUpdate(client, args["documentId"], args["requests"], revision(args));
|
|
798
|
+
return boundedReplies(savedEdit(args["documentId"], reply), asArray(reply["replies"]).map(asRecord));
|
|
799
|
+
}),
|
|
800
|
+
},
|
|
801
|
+
];
|
|
802
|
+
}
|