@zackbart/connecta 0.38.0 → 0.40.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 +91 -0
- package/README.md +3 -2
- package/bin/connecta.mjs +4 -8
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.js +6 -4
- package/dist/agent-routes.js +231 -0
- package/dist/auth/downstream-oauth/consent.js +519 -0
- package/dist/auth/downstream-oauth/grant-store.js +439 -0
- package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
- package/dist/auth/downstream-oauth/refresh-records.js +447 -0
- package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
- package/dist/auth/downstream-oauth.js +66 -2382
- package/dist/auth/native.js +37 -0
- package/dist/auth/oauth-grant.js +13 -0
- package/dist/auth/static-oauth.js +153 -131
- package/dist/auth-elicitation.js +7 -1
- package/dist/authorization-handoff.js +11 -5
- package/dist/catalog-cache.js +219 -59
- package/dist/catalog-policy.js +18 -0
- package/dist/catalog-service.js +284 -47
- package/dist/catalog.js +18 -2
- package/dist/config.d.ts +16 -11
- package/dist/config.js +9 -6
- package/dist/connector-caller.d.ts +1 -1
- package/dist/connector-health.js +59 -0
- package/dist/connector-status.js +117 -0
- package/dist/connectors/api-connector.d.ts +7 -2
- package/dist/connectors/api-connector.js +7 -4
- package/dist/connectors/negotiation-cache.js +22 -4
- package/dist/connectors/remote-mcp/call.js +164 -0
- package/dist/connectors/remote-mcp/catalog.js +278 -0
- package/dist/connectors/remote-mcp/config.js +263 -0
- package/dist/connectors/remote-mcp/connection.js +682 -0
- package/dist/connectors/remote-mcp/errors.js +488 -0
- package/dist/connectors/remote-mcp/oauth.js +473 -0
- package/dist/connectors/remote-mcp/resources.js +473 -0
- package/dist/connectors/remote-mcp/runtime.js +1 -0
- package/dist/connectors/remote-mcp/transport.js +555 -0
- package/dist/connectors/remote-mcp.js +62 -2863
- package/dist/credential-contract.d.ts +23 -0
- package/dist/credential-reads.js +17 -0
- package/dist/credential-verification.js +53 -0
- package/dist/credentials.d.ts +7 -0
- package/dist/credentials.js +60 -25
- package/dist/describe-config.d.ts +12 -2
- package/dist/describe-config.js +2 -0
- package/dist/discovery-health.js +199 -0
- package/dist/downstream-input.js +2 -0
- package/dist/errors.d.ts +55 -17
- package/dist/errors.js +95 -41
- package/dist/execute.js +152 -114
- package/dist/executor-result.js +3 -1
- package/dist/guest-functions.js +83 -0
- package/dist/guest-runtime.js +60 -19
- package/dist/guest-types.d.ts +19 -5
- package/dist/http/argument-problems.js +35 -0
- package/dist/http/errors.js +5 -4
- package/dist/http/fields.js +192 -13
- package/dist/http/hatch-response.js +73 -0
- package/dist/http/hatch.js +88 -23
- package/dist/http/oauth.js +3 -2
- package/dist/http/provider.js +32 -4
- package/dist/http/safety.js +8 -0
- package/dist/http/tools.js +44 -23
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -2
- package/dist/invocation.js +26 -8
- package/dist/membership.d.ts +13 -5
- package/dist/membership.js +34 -2
- package/dist/meta-tools.js +54 -31
- package/dist/native-recovery.js +4 -16
- package/dist/native-tools.js +5 -2
- package/dist/oauth-sealing.js +10 -0
- package/dist/operation-vocabulary.js +135 -13
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +1 -0
- package/dist/operator-ui/contract.d.ts +10 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +22 -1
- package/dist/provider-call-facts.js +6 -2
- package/dist/providers/_shared/rest/operation-index.js +48 -50
- package/dist/providers/_shared/rest/value-safety.js +80 -0
- package/dist/providers/breeze/api.js +7 -1
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/breeze/tools.js +4 -3
- package/dist/providers/ccb/api.js +2 -1
- package/dist/providers/ccb/index.js +5 -0
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/cloudflare/api.js +8 -1
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/docs/lib/schemas.js +24 -22
- package/dist/providers/docs/safety.js +2 -0
- package/dist/providers/docs/skill.generated.js +1 -1
- package/dist/providers/docs/tools.js +2 -4
- package/dist/providers/drive/api.js +1 -0
- package/dist/providers/drive/lib/contracts.js +8 -5
- package/dist/providers/drive/skill.generated.js +1 -1
- package/dist/providers/drive/tools.js +11 -16
- package/dist/providers/forms/api.js +1 -0
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/forms/tools.js +10 -7
- package/dist/providers/github/api.js +1 -0
- package/dist/providers/github/lib/discovery.js +1 -1
- package/dist/providers/github/skill.generated.js +1 -1
- package/dist/providers/github/tools.js +61 -17
- package/dist/providers/gmail/lib/schemas.js +60 -50
- package/dist/providers/gmail/skill.generated.js +1 -1
- package/dist/providers/gmail/tools.js +7 -5
- package/dist/providers/granola/index.js +2 -0
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/api.js +7 -1
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/planning-center/api.js +3 -1
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/skill.generated.js +1 -1
- package/dist/providers/quickbooks/safety.js +80 -39
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/api.js +1 -0
- package/dist/providers/sheets/skill.generated.js +1 -1
- package/dist/providers/sheets/tools.js +13 -14
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/slides/tools.js +15 -13
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/api.js +3 -1
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/vercel/api.js +1 -1
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/providers/vercel/tools.js +4 -3
- package/dist/registry.d.ts +10 -4
- package/dist/registry.js +41 -13
- package/dist/routes/credentials.js +129 -60
- package/dist/routes/dispatch.js +5 -1
- package/dist/routes/mcp.js +30 -14
- package/dist/routes/operator.js +2 -2
- package/dist/routes/shared.d.ts +6 -1
- package/dist/routes/shared.js +7 -1
- package/dist/routes/ui-config.js +2 -2
- package/dist/routes/ui-data.js +126 -20
- package/dist/routes/ui.js +16 -0
- package/dist/runtime/call.js +7 -2
- package/dist/runtime/native-storage.js +12 -0
- package/dist/runtime/request-reads.js +207 -0
- package/dist/runtime/request-scope.js +16 -0
- package/dist/runtime/storage.js +12 -0
- package/dist/skills.js +37 -20
- package/dist/storage/keys.js +53 -3
- package/dist/storage/memory.js +8 -2
- package/dist/storage/sql-native.js +53 -1
- package/dist/storage/sql.js +12 -2
- package/dist/storage-digest.js +32 -0
- package/dist/timeout.js +8 -0
- package/dist/types.d.ts +33 -0
- package/dist/usage-guide.js +16 -35
- package/dist/validate.d.ts +3 -0
- package/dist/validate.js +220 -158
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/write-outcome.js +0 -3
- package/documentation/README.md +13 -13
- package/documentation/architecture.md +385 -20
- package/documentation/auth.md +233 -64
- package/documentation/code-mode.md +122 -39
- package/documentation/deploying.md +6 -3
- package/documentation/integrating.md +16 -2
- package/documentation/meta-tools.md +240 -49
- package/documentation/migration-0.33.md +5 -1
- package/documentation/migration-0.34.md +3 -1
- package/documentation/operator-tests.md +1 -1
- package/documentation/operator-ui.md +105 -12
- package/documentation/pool-membership-upgrade.md +44 -0
- package/documentation/providers.md +105 -0
- package/examples/worker/AGENTS.md +10 -10
- package/examples/worker/README.md +4 -3
- package/examples/worker/src/connecta.config.ts +1 -0
- package/guides/connecting-agents.md +2 -1
- package/guides/how-it-works.md +4 -2
- package/package.json +1 -1
- package/templates/node/AGENTS.md +13 -12
- package/templates/node/package.json +1 -1
- package/templates/node/src/connecta.config.ts +1 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// Real Effect declarations for Gmail's curated surface. MIME interpretation stays in tools.ts.
|
|
2
|
+
import { routeDescription } from "../../../agent-routes.js";
|
|
2
3
|
import { Schema } from "effect";
|
|
3
4
|
import { Integer, isMinChars, isMaxChars } from "../../../http/json-schema.js";
|
|
4
5
|
import { StandardPage } from "../../../http/paging.js";
|
|
5
|
-
import {
|
|
6
|
+
import { fieldsContract } from "../../../http/fields.js";
|
|
6
7
|
const optional = Schema.optionalKey;
|
|
7
8
|
const pattern = (regex) => Schema.makeFilter((value) => regex.test(value), { toJsonSchema: () => ({ pattern: regex.source }) });
|
|
8
9
|
const string = (description, max, min = 0) => Schema.String.check(isMinChars(min), isMaxChars(max)).annotate({ description });
|
|
@@ -29,54 +30,6 @@ const compose = {
|
|
|
29
30
|
htmlBody: optional(string("Optional HTML alternative to the plain-text body.", 500000)),
|
|
30
31
|
replyToMessageId: optional(id("Message to reply to; sets its thread and guarded In-Reply-To/References.")),
|
|
31
32
|
};
|
|
32
|
-
const resultFields = optional(fieldsInput("Dot paths to keep in the result. It only narrows the result."));
|
|
33
|
-
const itemFields = optional(fieldsInput("Dot paths to keep in each item. It only narrows the projection."));
|
|
34
|
-
export const inputs = {
|
|
35
|
-
search_threads: Schema.Struct({
|
|
36
|
-
query,
|
|
37
|
-
includeSpamTrash: optional(Schema.Boolean),
|
|
38
|
-
limit,
|
|
39
|
-
cursor,
|
|
40
|
-
fields: itemFields,
|
|
41
|
-
}),
|
|
42
|
-
get_thread: Schema.Struct({
|
|
43
|
-
threadId: id("Thread id from search_threads or a message."),
|
|
44
|
-
maxBodyChars: bodyChars(3000),
|
|
45
|
-
cursor,
|
|
46
|
-
fields: resultFields,
|
|
47
|
-
}),
|
|
48
|
-
get_message: Schema.Struct({
|
|
49
|
-
messageId: id("Message id from a thread or draft."),
|
|
50
|
-
maxBodyChars: bodyChars(12000),
|
|
51
|
-
fields: resultFields,
|
|
52
|
-
}),
|
|
53
|
-
get_attachment: Schema.Struct({
|
|
54
|
-
fields: resultFields,
|
|
55
|
-
messageId: id("Message carrying the attachment."),
|
|
56
|
-
partId: optional(string("Immutable partId from the attachment listing, including an empty root id. Takes precedence over attachmentId.", 256)),
|
|
57
|
-
attachmentId: optional(string("Alternative rotating attachmentId from a fresh listing; prefer partId.", 2048, 1).check(pattern(/^[A-Za-z0-9_-]+$/))),
|
|
58
|
-
maxChars: optional(Integer.check(Schema.isBetween({ minimum: 1, maximum: 100000 })).annotate({
|
|
59
|
-
default: 12000,
|
|
60
|
-
description: "Text code points kept; ignored for binaries.",
|
|
61
|
-
})),
|
|
62
|
-
maxBytes: optional(Integer.check(Schema.isBetween({ minimum: 65536, maximum: 4194304 })).annotate({
|
|
63
|
-
default: 196608,
|
|
64
|
-
description: "Whole-result byte budget. Raise only for a direct call_tool; execute_code cannot receive large results.",
|
|
65
|
-
})),
|
|
66
|
-
}),
|
|
67
|
-
list_labels: Schema.Struct({ fields: itemFields }),
|
|
68
|
-
list_drafts: Schema.Struct({ query, limit, cursor, fields: itemFields }),
|
|
69
|
-
get_draft: Schema.Struct({
|
|
70
|
-
fields: resultFields,
|
|
71
|
-
draftId: id("Stable draft id from list_drafts or create_draft."),
|
|
72
|
-
maxBodyChars: bodyChars(12000),
|
|
73
|
-
}),
|
|
74
|
-
create_draft: Schema.Struct(compose),
|
|
75
|
-
update_draft: Schema.Struct({
|
|
76
|
-
draftId: id("Draft to replace; attachments and inline images are refused."),
|
|
77
|
-
...compose,
|
|
78
|
-
}),
|
|
79
|
-
};
|
|
80
33
|
const loose = (fields) => Schema.Struct(Object.fromEntries(Object.entries(fields).map(([key, value]) => [key, optional(value)])));
|
|
81
34
|
const truncated = optional(Schema.Array(Schema.String));
|
|
82
35
|
const Attachment = loose({
|
|
@@ -107,7 +60,7 @@ const messageFields = {
|
|
|
107
60
|
};
|
|
108
61
|
const Message = loose(messageFields);
|
|
109
62
|
const { id: _id, ...draftFields } = messageFields;
|
|
110
|
-
|
|
63
|
+
const rows = {
|
|
111
64
|
search_threads: loose({
|
|
112
65
|
id: Schema.String,
|
|
113
66
|
subject: Schema.String,
|
|
@@ -174,3 +127,60 @@ export const outputs = {
|
|
|
174
127
|
create_draft: Saved,
|
|
175
128
|
update_draft: Saved,
|
|
176
129
|
};
|
|
130
|
+
/** Each read's `fields`, anchored at its result or, for a list, each item. */
|
|
131
|
+
export const fieldContracts = {
|
|
132
|
+
search_threads: fieldsContract("item", rows.search_threads),
|
|
133
|
+
list_labels: fieldsContract("item", rows.list_labels),
|
|
134
|
+
list_drafts: fieldsContract("item", rows.list_drafts),
|
|
135
|
+
get_thread: fieldsContract("result", outputs.get_thread),
|
|
136
|
+
get_message: fieldsContract("result", outputs.get_message),
|
|
137
|
+
get_draft: fieldsContract("result", outputs.get_draft),
|
|
138
|
+
get_attachment: fieldsContract("result", outputs.get_attachment),
|
|
139
|
+
};
|
|
140
|
+
const fields = (name) => optional(fieldContracts[name].input);
|
|
141
|
+
export const inputs = {
|
|
142
|
+
search_threads: Schema.Struct({
|
|
143
|
+
query,
|
|
144
|
+
includeSpamTrash: optional(Schema.Boolean),
|
|
145
|
+
limit,
|
|
146
|
+
cursor,
|
|
147
|
+
fields: fields("search_threads"),
|
|
148
|
+
}),
|
|
149
|
+
get_thread: Schema.Struct({
|
|
150
|
+
threadId: id("Thread id from search_threads or a message."),
|
|
151
|
+
maxBodyChars: bodyChars(3000),
|
|
152
|
+
cursor,
|
|
153
|
+
fields: fields("get_thread"),
|
|
154
|
+
}),
|
|
155
|
+
get_message: Schema.Struct({
|
|
156
|
+
messageId: id("Message id from a thread or draft."),
|
|
157
|
+
maxBodyChars: bodyChars(12000),
|
|
158
|
+
fields: fields("get_message"),
|
|
159
|
+
}),
|
|
160
|
+
get_attachment: Schema.Struct({
|
|
161
|
+
fields: fields("get_attachment"),
|
|
162
|
+
messageId: id("Message carrying the attachment."),
|
|
163
|
+
partId: optional(string("Immutable partId from the attachment listing, including an empty root id. Takes precedence over attachmentId.", 256)),
|
|
164
|
+
attachmentId: optional(string("Alternative rotating attachmentId from a fresh listing; prefer partId.", 2048, 1).check(pattern(/^[A-Za-z0-9_-]+$/))),
|
|
165
|
+
maxChars: optional(Integer.check(Schema.isBetween({ minimum: 1, maximum: 100000 })).annotate({
|
|
166
|
+
default: 12000,
|
|
167
|
+
description: "Text code points kept; ignored for binaries.",
|
|
168
|
+
})),
|
|
169
|
+
maxBytes: optional(Integer.check(Schema.isBetween({ minimum: 65536, maximum: 4194304 })).annotate({
|
|
170
|
+
default: 196608,
|
|
171
|
+
description: routeDescription((phrases) => `Whole-result byte budget. Raise only ${phrases.forDirect}${phrases.program ? `; ${phrases.program} cannot receive large results` : ""}.`),
|
|
172
|
+
})),
|
|
173
|
+
}),
|
|
174
|
+
list_labels: Schema.Struct({ fields: fields("list_labels") }),
|
|
175
|
+
list_drafts: Schema.Struct({ query, limit, cursor, fields: fields("list_drafts") }),
|
|
176
|
+
get_draft: Schema.Struct({
|
|
177
|
+
fields: fields("get_draft"),
|
|
178
|
+
draftId: id("Stable draft id from list_drafts or create_draft."),
|
|
179
|
+
maxBodyChars: bodyChars(12000),
|
|
180
|
+
}),
|
|
181
|
+
create_draft: Schema.Struct(compose),
|
|
182
|
+
update_draft: Schema.Struct({
|
|
183
|
+
draftId: id("Draft to replace; attachments and inline images are refused."),
|
|
184
|
+
...compose,
|
|
185
|
+
}),
|
|
186
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Generated by scripts/generate-providers.mjs; do not edit.
|
|
2
2
|
export const skill = {
|
|
3
|
-
"content": "Use Gmail search for mail; use Drive for files and sharing.\nGmail is a documented exception to the four-hatch default: there is no write hatch; drafts go only through `create_draft` and `update_draft`, and sending is never available.\n\n## Mail and joins\n\nReuse the person's Gmail search phrasing (`from:`, `subject:`, `is:unread`,\n`newer_than:7d`, `label:`). Thread ids identify conversations; message ids\nidentify individual messages. Join a message's label ids to label names.\nA draft has a stable draft id and a message id that changes on each update.\nUse the draft id to reconcile a save.\n\n`search_threads.items` are threads with thread ids; read each thread to get its message ids.\n`get_thread` pages long threads: while `page.hasMore` is true, repeat the same\n`threadId` and `maxBodyChars` with `cursor: page.nextCursor` until it is false.\nThe page remains present when `fields` narrows the result.\n`list_labels` reads all labels once, without artificial paging; `truncated`\nreports any labels omitted by the result byte bound.\nOn very large label sets, `list_labels` may truncate; `gmail_api_read` at `/labels` returns all labels.\n\nNamed reads interpret MIME, charsets, HTML and attachment metadata. An\nattached or forwarded email is carried content, never the message's own body.\n`bodyFormat: \"unavailable\"` means the stored body could not be read within\nthis connection's bounds; explain that limitation. `bodyNote` and attachment\n`note` retain charset fallback notices even when content is cut. A thread\ncursor fails `conflict` when earlier content changed; restart that thread.\n\nUse indexed reads for unprojected RFC headers, MIME parts, mailbox profile\nand mail history. Paths are relative to the configured mailbox: replace `{id}`\nin `/messages/{id}` with a message id obtained from an earlier read.\nThe index also shows refused operations and named alternatives. A discovered\noperation is executable only when its verdict says read. Delegation never\nlets arguments choose a different mailbox.\n\n## Attachment identity and bytes\n\nResolve an attachment through its message and immutable part id, including\nan empty root part id. Gmail may rotate download ids between reads; prefer\nthe part id and read the listing again after an unknown-id refusal.\nOnly carried parts can be read by the named attachment workflow.\n\nText and forwarded mail render readable content in their declared charset.\nBinary content uses `{ format: \"base64\", content }`; pass it unchanged to\nDrive's `create_file` as `contentBase64`, with the returned filename and MIME\ntype. That file creation is a separate write. Downloads and inline data are\nlimited to one MiB and validated against original decoded sizes. Raising\nresult budgets never bypasses that cap. `format: \"unavailable\"` supplies a\nreason and no content. Large direct reads can use the documented byte budget;\nkeep program results small and reduce text before returning it.\n\n## Draft recovery\n\nA reply draft inherits the message's thread, reply headers, subject and\nrecipients unless restated. Keep the reply subject to retain the Gmail thread.\nCopied threading headers must remain intact ASCII. RFC message ids, comments\nand legacy threading phrases are accepted; damaged or withheld tokens cause\na refusal before any draft write.\n\nUpdating a draft replaces its full body. The guarded workflow preserves\nsender, recipients, Reply-To, subject and threading headers, and refuses\nattachments or inline images because replacement would discard them. Edit\nsuch a draft in Gmail or save a separate new draft. After an uncertain save,\nread the stable draft id and inspect its body before deciding whether another\nwrite is needed. A saved draft remains in Gmail for the person to review.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `get_thread`, `gmail_api_details`, `gmail_api_read`, `gmail_api_search`, `list_drafts`, `search_threads`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. Keep the original method, path, filters, page size, fields and headers. Paged `list_*` and `search_*` reads take `limit` (default 10, at most 25). The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `gmail_api_search` finds this API's operations and reads no account data, `gmail_api_details` shows one operation's contract, and `gmail_api_read` calls a reviewed read. An operation without a reviewed verdict is refused. 68 operations are refused by design; use `gmail_api_details` with the method and path to
|
|
3
|
+
"content": "Use Gmail search for mail; use Drive for files and sharing.\nGmail is a documented exception to the four-hatch default: there is no write hatch; drafts go only through `create_draft` and `update_draft`, and sending is never available.\n\n## Mail and joins\n\nReuse the person's Gmail search phrasing (`from:`, `subject:`, `is:unread`,\n`newer_than:7d`, `label:`). Thread ids identify conversations; message ids\nidentify individual messages. Join a message's label ids to label names.\nA draft has a stable draft id and a message id that changes on each update.\nUse the draft id to reconcile a save.\n\n`search_threads.items` are threads with thread ids; read each thread to get its message ids.\n`get_thread` pages long threads: while `page.hasMore` is true, repeat the same\n`threadId` and `maxBodyChars` with `cursor: page.nextCursor` until it is false.\nThe page remains present when `fields` narrows the result.\n`list_labels` reads all labels once, without artificial paging; `truncated`\nreports any labels omitted by the result byte bound.\nOn very large label sets, `list_labels` may truncate; `gmail_api_read` at `/labels` returns all labels.\n\nNamed reads interpret MIME, charsets, HTML and attachment metadata. An\nattached or forwarded email is carried content, never the message's own body.\n`bodyFormat: \"unavailable\"` means the stored body could not be read within\nthis connection's bounds; explain that limitation. `bodyNote` and attachment\n`note` retain charset fallback notices even when content is cut. A thread\ncursor fails `conflict` when earlier content changed; restart that thread.\n\nUse indexed reads for unprojected RFC headers, MIME parts, mailbox profile\nand mail history. Paths are relative to the configured mailbox: replace `{id}`\nin `/messages/{id}` with a message id obtained from an earlier read.\nThe index also shows refused operations and named alternatives. A discovered\noperation is executable only when its verdict says read. Delegation never\nlets arguments choose a different mailbox.\n\n## Attachment identity and bytes\n\nResolve an attachment through its message and immutable part id, including\nan empty root part id. Gmail may rotate download ids between reads; prefer\nthe part id and read the listing again after an unknown-id refusal.\nOnly carried parts can be read by the named attachment workflow.\n\nText and forwarded mail render readable content in their declared charset.\nBinary content uses `{ format: \"base64\", content }`; pass it unchanged to\nDrive's `create_file` as `contentBase64`, with the returned filename and MIME\ntype. That file creation is a separate write. Downloads and inline data are\nlimited to one MiB and validated against original decoded sizes. Raising\nresult budgets never bypasses that cap. `format: \"unavailable\"` supplies a\nreason and no content. Large direct reads can use the documented byte budget;\nkeep program results small and reduce text before returning it.\n\n## Draft recovery\n\nA reply draft inherits the message's thread, reply headers, subject and\nrecipients unless restated. Keep the reply subject to retain the Gmail thread.\nCopied threading headers must remain intact ASCII. RFC message ids, comments\nand legacy threading phrases are accepted; damaged or withheld tokens cause\na refusal before any draft write.\n\nUpdating a draft replaces its full body. The guarded workflow preserves\nsender, recipients, Reply-To, subject and threading headers, and refuses\nattachments or inline images because replacement would discard them. Edit\nsuch a draft in Gmail or save a separate new draft. After an uncertain save,\nread the stable draft id and inspect its body before deciding whether another\nwrite is needed. A saved draft remains in Gmail for the person to review.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `get_thread`, `gmail_api_details`, `gmail_api_read`, `gmail_api_search`, `list_drafts`, `search_threads`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. Keep the original method, path, filters, page size, fields and headers. Paged `list_*` and `search_*` reads take `limit` (default 10, at most 25). The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `gmail_api_search` finds this API's operations and reads no account data, `gmail_api_details` shows one operation's contract, and `gmail_api_read` calls a reviewed read. Details' `response.fields` promises selection only; `withheld` hints are incomplete, and absence never means safe or readable. An operation without a reviewed verdict is refused. 68 operations are refused by design; use `gmail_api_details` with the method and path to see each summary and refusal, even for methods outside the read/write schemas.\n- **Writes:** a write is sent once and never retried. When its outcome is unknown, read the target before repeating it. Writes take no idempotency key.\n- **Errors:** codes come from the vendor's statuses and error facts, never its prose. `not_found` means the record does not exist; this API tells absence from a permission gap. `conflict` means: A thread changed before its content cursor.\n- **Withheld values:** a value withheld for safety reads `\"[redacted]\"`. Writes refuse whole marker values and withheld links; longer prose may mention the marker. Paths and queries refuse any containment.",
|
|
4
4
|
"instructionsHeading": "Connection instructions"
|
|
5
5
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { routePhrases } from "../../agent-routes.js";
|
|
1
2
|
import { asRecord, asArray, compact } from "../_shared/json.js";
|
|
2
3
|
import { decodeBase64url } from "../../bytes.js";
|
|
3
4
|
// Curated Gmail workflows over real Effect declarations and the standard kit boundary.
|
|
@@ -11,10 +12,10 @@ import { encodeForAgent, redactBeforeCut, redactSentSecrets } from "../../sent-s
|
|
|
11
12
|
import { errors, gmailBodyProvenance } from "./api.js";
|
|
12
13
|
import { read, write } from "../../http/tools.js";
|
|
13
14
|
import { boundCursor } from "../../http/paging.js";
|
|
14
|
-
import {
|
|
15
|
+
import { selectFields } from "../../http/fields.js";
|
|
15
16
|
import { Operation } from "../../runtime/call.js";
|
|
16
17
|
import { index } from "./lib/index.js";
|
|
17
|
-
import { inputs, outputs
|
|
18
|
+
import { fieldContracts, inputs, outputs } from "./lib/schemas.js";
|
|
18
19
|
import { threadingIds } from "./lib/threading.js";
|
|
19
20
|
/**
|
|
20
21
|
* Each summary row costs one metadata read, so a page stays small. Gmail's
|
|
@@ -392,7 +393,7 @@ function readAttachment(client, ctx, args) {
|
|
|
392
393
|
const fitting = Math.floor(Math.max(0, maxBytes - jsonBytes(envelope)) / 4) * 3;
|
|
393
394
|
const limit = isText ? MAX_ATTACHMENT_BYTES : Math.min(MAX_ATTACHMENT_BYTES, fitting);
|
|
394
395
|
const tooLarge = () => unavailable(limit < MAX_ATTACHMENT_BYTES
|
|
395
|
-
? `This binary attachment does not fit maxBytes (${maxBytes}), which holds ${limit} attachment bytes. Raise maxBytes, up to ${MAX_ATTACHMENT_RESULT_BYTES}
|
|
396
|
+
? `This binary attachment does not fit maxBytes (${maxBytes}), which holds ${limit} attachment bytes. Raise maxBytes, up to ${MAX_ATTACHMENT_RESULT_BYTES} ${routePhrases(ctx).directOnly}, to read up to ${MAX_ATTACHMENT_BYTES} bytes. No content is returned.`
|
|
396
397
|
: `This attachment exceeds the ${MAX_ATTACHMENT_BYTES}-byte download and inline cap. No content is returned; open it in Gmail.`);
|
|
397
398
|
// Declared size is checked before downloading, but actual bytes are checked
|
|
398
399
|
// too: inconsistent metadata must never bypass the cap or whole-result budget.
|
|
@@ -443,7 +444,7 @@ function readAttachment(client, ctx, args) {
|
|
|
443
444
|
if (decoded.length > maxChars || jsonBytes(decoded) > maxBytes)
|
|
444
445
|
decoded = redactBeforeCut(ctx, decoded);
|
|
445
446
|
const characters = Array.from(decoded);
|
|
446
|
-
const marker = (dropped) => `\n[… ${dropped} more characters truncated; raise maxChars or maxBytes
|
|
447
|
+
const marker = (dropped) => `\n[… ${dropped} more characters truncated; raise maxChars or maxBytes ${routePhrases(ctx).forDirect}, or open it in Gmail]`;
|
|
447
448
|
const byChars = characters.length > maxChars
|
|
448
449
|
? characters.slice(0, maxChars).join("") + marker(characters.length - maxChars)
|
|
449
450
|
: decoded;
|
|
@@ -1390,7 +1391,8 @@ export function gmailTools({ connection }) {
|
|
|
1390
1391
|
return yield* Effect.die(new Error("Gmail needs its call context."));
|
|
1391
1392
|
const ctx = active.value;
|
|
1392
1393
|
const { fields, ...args } = supplied;
|
|
1393
|
-
const
|
|
1394
|
+
const contract = fieldContracts[name];
|
|
1395
|
+
const problem = contract?.problem(fields);
|
|
1394
1396
|
if (problem)
|
|
1395
1397
|
return yield* Effect.fail(new Refusal({ error: problem }));
|
|
1396
1398
|
if (args["cursor"] !== undefined)
|
|
@@ -9,6 +9,8 @@ export const granola = defineApiProvider({
|
|
|
9
9
|
readme: "Granola",
|
|
10
10
|
options: optionsOf()(keys("purpose", "title", "instructions", "maxResultBytes", "readOnly")),
|
|
11
11
|
skill,
|
|
12
|
+
// The guide and vendor call meeting records notes.
|
|
13
|
+
searchSynonyms: [["meeting", "note"]],
|
|
12
14
|
bundle: {
|
|
13
15
|
baselineGzip: 360040,
|
|
14
16
|
note: "./providers/granola standalone measured 360,040 B gzip with Homebrew Node 22 on 2026-10-27. This includes the shared Effect provider kit, paid once per deployment; provider entries are reported, not capped, and standalone sizes are not additive.",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Generated by scripts/generate-providers.mjs; do not edit.
|
|
2
2
|
export const skill = {
|
|
3
|
-
"content": "## Accessible meeting records\n\nWorkspace keys reach public workspace notes and spaces explicitly enabled for\nGranola API access. Folder permissions inherit their space's setting. They do\nnot grant unrelated private notes. Only notes with a generated AI summary and\ntranscript appear; a missing result does not establish that a meeting never\nhappened. The owner's private handwritten notes are null for workspace keys.\n\nDiscover actual note, folder and space IDs. API IDs use not_, fol_ and spa_\nprefixes; a web-link UUID, title or human name is not an API ID. Notes can\nbelong to several folders and spaces; folder parent_folder_id describes the\nhierarchy and space_id joins a folder to its space. Folder filtering includes\ndescendant folders. Null space_id can also mean an inaccessible space.\n\nOperation search searches the API contract, not meeting content. The REST API\noffers date, folder and space filters, with no natural-language meeting search.\nFilter accessible notes and inspect their contents to answer a topic question;\ndisclose the period and scope examined.\n\n## Transcript interpretation\n\nInline transcripts are optional. If including one fails with HTTP 413\n(TRANSCRIPT_TOO_LARGE), read note details without include and retrieve the\nseparate transcript endpoint through its complete continuation sequence. No\npartial transcript proves a complete quotation or count. An empty page can\nstill have a continuation; follow the returned paging state.\n\nSpeaker source describes the audio input, not a person's identity. macOS may\ndistinguish microphone from speaker audio. iOS currently uses microphone and\nmay supply anonymous diarization labels; do not invent attendee attribution.\nPreserve start/end timestamps when quoting. Summaries are AI-generated, so\nconfirm important wording against the transcript. Return only the relevant\npassages and identifying meeting metadata.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `granola_api_details`, `granola_api_read`, `granola_api_search`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. Keep the original method, path, filters, page size, fields and headers. The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `granola_api_search` finds this API's operations and reads no account data, `granola_api_details` shows one operation's contract, and `granola_api_read` calls a reviewed read. An operation without a reviewed verdict is refused. 13 operations are refused by design; use `granola_api_details` with the method and path to
|
|
3
|
+
"content": "## Accessible meeting records\n\nWorkspace keys reach public workspace notes and spaces explicitly enabled for\nGranola API access. Folder permissions inherit their space's setting. They do\nnot grant unrelated private notes. Only notes with a generated AI summary and\ntranscript appear; a missing result does not establish that a meeting never\nhappened. The owner's private handwritten notes are null for workspace keys.\n\nDiscover actual note, folder and space IDs. API IDs use not_, fol_ and spa_\nprefixes; a web-link UUID, title or human name is not an API ID. Notes can\nbelong to several folders and spaces; folder parent_folder_id describes the\nhierarchy and space_id joins a folder to its space. Folder filtering includes\ndescendant folders. Null space_id can also mean an inaccessible space.\n\nOperation search searches the API contract, not meeting content. The REST API\noffers date, folder and space filters, with no natural-language meeting search.\nFilter accessible notes and inspect their contents to answer a topic question;\ndisclose the period and scope examined.\n\n## Transcript interpretation\n\nInline transcripts are optional. If including one fails with HTTP 413\n(TRANSCRIPT_TOO_LARGE), read note details without include and retrieve the\nseparate transcript endpoint through its complete continuation sequence. No\npartial transcript proves a complete quotation or count. An empty page can\nstill have a continuation; follow the returned paging state.\n\nSpeaker source describes the audio input, not a person's identity. macOS may\ndistinguish microphone from speaker audio. iOS currently uses microphone and\nmay supply anonymous diarization labels; do not invent attendee attribution.\nPreserve start/end timestamps when quoting. Summaries are AI-generated, so\nconfirm important wording against the transcript. Return only the relevant\npassages and identifying meeting metadata.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `granola_api_details`, `granola_api_read`, `granola_api_search`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. Keep the original method, path, filters, page size, fields and headers. The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `granola_api_search` finds this API's operations and reads no account data, `granola_api_details` shows one operation's contract, and `granola_api_read` calls a reviewed read. Details' `response.fields` promises selection only; `withheld` hints are incomplete, and absence never means safe or readable. An operation without a reviewed verdict is refused. 13 operations are refused by design; use `granola_api_details` with the method and path to see each summary and refusal, even for methods outside the read/write schemas.\n- **Writes:** a write is sent once and never retried. When its outcome is unknown, read the target before repeating it. Writes take no idempotency key.\n- **Errors:** codes come from the vendor's statuses and error facts, never its prose. An unknown id, or one these credentials cannot reach, fails as `connector_call_failed`; this API does not tell the two apart. It documents no conflict.\n- **Withheld values:** a value withheld for safety reads `\"[redacted]\"`. Writes refuse whole marker values and withheld links; longer prose may mention the marker. Paths and queries refuse any containment.",
|
|
4
4
|
"instructionsHeading": "Workspace instructions"
|
|
5
5
|
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Routes, routePhrases } from "../../agent-routes.js";
|
|
2
|
+
import { pathWithholds } from "../_shared/rest/value-safety.js";
|
|
1
3
|
import { asRecord } from "../_shared/json.js";
|
|
2
4
|
// Overflow v3: reviewed vendor declarations, derived operation index and paging contracts.
|
|
3
5
|
// Public input: staging OpenAPI 3.0; exact retrieval and known upstream drift are in evidence/review.md.
|
|
@@ -901,11 +903,12 @@ function detailFor(payload, status) {
|
|
|
901
903
|
/** Candidate keys are resolved afresh for each operation, never cached in a connection. */
|
|
902
904
|
export const authenticate = Effect.gen(function* () {
|
|
903
905
|
const values = yield* (yield* Credentials).values.pipe(Effect.mapError((error) => new Refusal({ error })));
|
|
906
|
+
const routes = yield* Routes;
|
|
904
907
|
const clientId = values?.["clientId"]?.trim();
|
|
905
908
|
const apiKey = values?.["apiKey"]?.trim();
|
|
906
909
|
if (!clientId || !apiKey)
|
|
907
910
|
return yield* Effect.fail(new Refusal({
|
|
908
|
-
error: new ConnectorCallError("auth_required",
|
|
911
|
+
error: new ConnectorCallError("auth_required", `No Overflow client id and API key are configured for this connector. ${routePhrases(routes).authorizeFor("recovery options")}; an operator adds both fields in this connection in the operator UI.`),
|
|
909
912
|
}));
|
|
910
913
|
return { "x-client-id": clientId, "x-api-key": apiKey };
|
|
911
914
|
});
|
|
@@ -987,6 +990,9 @@ export const safety = {
|
|
|
987
990
|
reviewed: {
|
|
988
991
|
resolve: (method, path) => ({ method, template: OVERFLOW_INDEX.resolve(method, path).op.path }),
|
|
989
992
|
redact: (body, operation) => (operation.template.startsWith("/webhooks") ? withoutWebhookSecrets(body) : body),
|
|
993
|
+
withholds: (operation) => operation.template.startsWith("/webhooks")
|
|
994
|
+
? pathWithholds({ names: (name) => WEBHOOK_SECRETS.test(name) })
|
|
995
|
+
: () => false,
|
|
990
996
|
withholdsErrors: (operation) => operation.template.startsWith("/webhooks"),
|
|
991
997
|
},
|
|
992
998
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Generated by scripts/generate-providers.mjs; do not edit.
|
|
2
2
|
export const skill = {
|
|
3
|
-
"content": "## Giving records and joins\n\nDonor ids join profiles, contributions, subscriptions and payment methods.\nCampaigns and subcampaigns allocate gifts; locations identify campuses.\nResolve names to ids before editing a donor or moving money. There is no funds\nendpoint: fund and sub-fund identifiers occur only in payment authorization.\nA donor profile owned by a donor account cannot be edited through this API;\nwhen reads work but its edit is denied, changing the credential does not fix it.\n\nTap groups contain devices and point to destinations. A tap is engagement,\nnot a gift; use contributions to establish giving.\n\n## Money and final status\n\nRead the unit per field. Values named `InCents` are cents, including deposits,\nrefunds, chargebacks and payment authorization. Subscription `amount` is\nin dollars. Contribution and saved-method charge `amount` have no stated unit\nin the public schema: establish it from a known gift before reporting a sum\nor authorizing a charge. Do not assume every numeric amount uses the same unit.\n\nA gift is final at `PAID_OUT` for `CASH`, `CONFIRMED` for manual cash or crypto,\n`CONTRIBUTION_RECEIVED` or `BILLED` for stock, and `CONTRIBUTION_RECEIVED` for\nDAF. A status bucket is coarser than those asset-specific states. Polling by\nupdated date finds later state changes; initiated date identifies when a donor gave.\n\n## Reconcile a deposit\n\nFind the bank payout, inspect its summary totals, then its line items.\nEach line item's `referenceId` names a contribution, refund or chargeback\naccording to its type; contributions can also carry a `depositId`.\nKeep gross, fee and net cent amounts separate and convert only for presentation.\nAn eligible cash gift under $1,000 is refunded with `POST /contributions/{contributionId}/initiate-refund`, which takes no body; its string acknowledgement means the refund started.\nA refund request starts processing; verify the gift or refund before claiming\nthat funds have reached the donor. Canceling a recurring gift stops future\npayments and does not refund earlier gifts.\n\n## Reduce donor and delivery data\n\nGiving history and donor contact details are personal data. Aggregate or join\ninside code and return names, email, phone and address only when the task needs\nthem. Lean contribution rows carry donor identity; the full donor lookup\ncarries contact details. Webhook delivery logs can include delivered donor\npayloads and the receiving server's response: select the status, response code\nand error needed for diagnosis instead of returning those payloads wholesale.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `list_campaigns`, `list_chargebacks`, `list_contributions`, `list_deposits`, `list_donors`, `list_locations`, `list_payment_methods`, `list_refunds`, `list_subscriptions`, `overflow_api_details`, `overflow_api_search`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. For `overflow_api_read`, put it in `query[page.param]` or `body[page.param]`, as `page.in` specifies. Keep the original method, path, filters, page size, fields and headers. Paged `list_*` and `search_*` reads take `limit` (default 25, at most 100). The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `overflow_api_search` finds this API's operations and reads no account data, `overflow_api_details` shows one operation's contract, and `overflow_api_read` calls a reviewed read. `overflow_api_write` calls a reviewed write; the host may ask the user to confirm it. An operation without a reviewed verdict is refused.\n- **Writes:** a write is sent once and never retried. When its outcome is unknown, read the target before repeating it. Writes take no idempotency key.\n- **Errors:** codes come from the vendor's statuses and error facts, never its prose. `not_found` means the record does not exist; this API tells absence from a permission gap. It documents no conflict.\n- **Withheld values:** a value withheld for safety reads `\"[redacted]\"`. Writes refuse whole marker values and withheld links; longer prose may mention the marker. Paths and queries refuse any containment.",
|
|
3
|
+
"content": "## Giving records and joins\n\nDonor ids join profiles, contributions, subscriptions and payment methods.\nCampaigns and subcampaigns allocate gifts; locations identify campuses.\nResolve names to ids before editing a donor or moving money. There is no funds\nendpoint: fund and sub-fund identifiers occur only in payment authorization.\nA donor profile owned by a donor account cannot be edited through this API;\nwhen reads work but its edit is denied, changing the credential does not fix it.\n\nTap groups contain devices and point to destinations. A tap is engagement,\nnot a gift; use contributions to establish giving.\n\n## Money and final status\n\nRead the unit per field. Values named `InCents` are cents, including deposits,\nrefunds, chargebacks and payment authorization. Subscription `amount` is\nin dollars. Contribution and saved-method charge `amount` have no stated unit\nin the public schema: establish it from a known gift before reporting a sum\nor authorizing a charge. Do not assume every numeric amount uses the same unit.\n\nA gift is final at `PAID_OUT` for `CASH`, `CONFIRMED` for manual cash or crypto,\n`CONTRIBUTION_RECEIVED` or `BILLED` for stock, and `CONTRIBUTION_RECEIVED` for\nDAF. A status bucket is coarser than those asset-specific states. Polling by\nupdated date finds later state changes; initiated date identifies when a donor gave.\n\n## Reconcile a deposit\n\nFind the bank payout, inspect its summary totals, then its line items.\nEach line item's `referenceId` names a contribution, refund or chargeback\naccording to its type; contributions can also carry a `depositId`.\nKeep gross, fee and net cent amounts separate and convert only for presentation.\nAn eligible cash gift under $1,000 is refunded with `POST /contributions/{contributionId}/initiate-refund`, which takes no body; its string acknowledgement means the refund started.\nA refund request starts processing; verify the gift or refund before claiming\nthat funds have reached the donor. Canceling a recurring gift stops future\npayments and does not refund earlier gifts.\n\n## Reduce donor and delivery data\n\nGiving history and donor contact details are personal data. Aggregate or join\ninside code and return names, email, phone and address only when the task needs\nthem. Lean contribution rows carry donor identity; the full donor lookup\ncarries contact details. Webhook delivery logs can include delivered donor\npayloads and the receiving server's response: select the status, response code\nand error needed for diagnosis instead of returning those payloads wholesale.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `list_campaigns`, `list_chargebacks`, `list_contributions`, `list_deposits`, `list_donors`, `list_locations`, `list_payment_methods`, `list_refunds`, `list_subscriptions`, `overflow_api_details`, `overflow_api_search`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. For `overflow_api_read`, put it in `query[page.param]` or `body[page.param]`, as `page.in` specifies. Keep the original method, path, filters, page size, fields and headers. Paged `list_*` and `search_*` reads take `limit` (default 25, at most 100). The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `overflow_api_search` finds this API's operations and reads no account data, `overflow_api_details` shows one operation's contract, and `overflow_api_read` calls a reviewed read. Details' `response.fields` promises selection only; `withheld` hints are incomplete, and absence never means safe or readable. `overflow_api_write` calls a reviewed write; the host may ask the user to confirm it. An operation without a reviewed verdict is refused.\n- **Writes:** a write is sent once and never retried. When its outcome is unknown, read the target before repeating it. Writes take no idempotency key.\n- **Errors:** codes come from the vendor's statuses and error facts, never its prose. `not_found` means the record does not exist; this API tells absence from a permission gap. It documents no conflict.\n- **Withheld values:** a value withheld for safety reads `\"[redacted]\"`. Writes refuse whole marker values and withheld links; longer prose may mention the marker. Paths and queries refuse any containment.",
|
|
4
4
|
"instructionsHeading": "Nonprofit instructions"
|
|
5
5
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Routes, routePhrases } from "../../agent-routes.js";
|
|
1
2
|
import { asArray, asRecord } from "../_shared/json.js";
|
|
2
3
|
import { Effect, Schema } from "effect";
|
|
3
4
|
import { ConnectorCallError } from "../../errors.js";
|
|
@@ -38,10 +39,11 @@ export const CREDENTIAL = {
|
|
|
38
39
|
const fail = (code, message, options) => Effect.fail(new Refusal({ error: new ConnectorCallError(code, message, options) }));
|
|
39
40
|
export const authenticate = Effect.gen(function* () {
|
|
40
41
|
const values = yield* (yield* Credentials).values.pipe(Effect.mapError((error) => new Refusal({ error })));
|
|
42
|
+
const routes = yield* Routes;
|
|
41
43
|
const applicationId = values?.["applicationId"]?.trim();
|
|
42
44
|
const secret = values?.["secret"]?.trim();
|
|
43
45
|
if (!applicationId || !secret) {
|
|
44
|
-
return yield* fail("auth_required",
|
|
46
|
+
return yield* fail("auth_required", `No Planning Center personal access token is configured for this connector; it needs both the Application ID and the Secret. ${routePhrases(routes).authorizeFor("recovery options")}. When available, an operator can add the token in this connection in the operator UI.`);
|
|
45
47
|
}
|
|
46
48
|
// HTTP Basic is `id:secret` in Latin-1 base64. A colon in the id, or any
|
|
47
49
|
// whitespace or non-ASCII character, is a pasting mistake no real token
|
|
@@ -55,4 +55,4 @@ export declare const PLANNING_CENTER_MAX_RESPONSE_BYTES: number;
|
|
|
55
55
|
*/
|
|
56
56
|
export declare const PLANNING_CENTER_ADMISSION: ConnectorCallAdmissionPolicy;
|
|
57
57
|
/** Planning Center refuses a request without a descriptive one (HTTP 403). */
|
|
58
|
-
export declare const DEFAULT_USER_AGENT = "connecta/0.
|
|
58
|
+
export declare const DEFAULT_USER_AGENT = "connecta/0.40.0 (+https://github.com/zackbart/connecta)";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Generated by scripts/generate-providers.mjs; do not edit.
|
|
2
2
|
export const skill = {
|
|
3
|
-
"content": "## Identity first\n\nA People person id joins records across products. Resolve people by identity,\nthen join on `person_id`; names can repeat. A membership, scheduled person,\nworkflow card and person each have their own record id.\n\n`get_me` identifies the token's user and the organization's time zone. Bare\ndates mean the organization's local day; use a timestamp with an offset for\nan exact boundary. Name filters are exact unless their value ends in `%`.\n\nAn old person id may have been merged. Read `/people/v2/person_mergers` with\n`where[person_to_remove_id]`, follow `person_to_keep_id`, then read that profile.\nAn empty history does not establish that a person was merged.\n\nReduce pastoral information inside `execute_code` and return aggregates unless\nthe task needs named people. A named projection omits some safe vendor fields;\nread the indexed record when the task needs those fields.\n\n<!-- when: people -->\n## People and follow-up\n\nResolve a person before changing their profile. An omitted attribute leaves\nthe current value alone; `null` clears it. Archiving a person is reversible;\ndeleting a profile is permanent.\n\nWorkflow cards join people to a workflow and its ordered steps. Resolve both\nthe card id and its person id before acting on a card. Snoozing needs a duration;\npromoting a card completes its step, while skipping moves on without completing\nit. A workflow card note and a profile note are different records. Profile note\ncategories can notify their subscribers.\n\nCustom field values need their field-definition records to supply names and\ndata types. Form answers similarly join submissions, values and fields; the\nAPI cannot submit a form on someone's behalf.\n<!-- end -->\n\n<!-- when: lists -->\nSaved People lists are live cross-product queries. Their results are only as\nfresh as their last refresh. Refreshing a list can fire its automations, such\nas adding a person to a workflow, and is a write even when the task is a count.\n<!-- end -->\n\n<!-- when: services -->\n## Services plans and schedules\n\nResolve the service type before finding a dated plan. Plan times define the\nservice dates. `get_plan` combines the plan, order of service and scheduled\nteam; a cut section can be read through its indexed items or team-members path.\nItem durations are seconds, so divide by 60 when reporting minutes.\n\nScheduling needs a team and may need a position. `C` means confirmed, `U`\nunconfirmed and `D` declined. Preparing a notification and sending it are\ndifferent actions. A song item's identity comes from its song and arrangement;\nmusical keys are not credentials. Services cannot upload a file through this\nconnection.\n<!-- end -->\n\n<!-- when: groups -->\n## Groups membership joins\n\nUse a group's membership records to distinguish leaders from members. On this\nAPI version memberships carry no person attributes; `include=person` supplies\nnames, and the related person id supplies contact details and other product\nrecords. The API can change memberships and group settings, but cannot create\na group, create a meeting or record group attendance.\n<!-- end -->\n\n<!-- when: check-ins -->\n## Check-Ins occurrences\n\nAn event is recurring; its event period is a dated occurrence. Period counts\nare the efficient attendance answer. Individual check-ins join a person to a\nperiod, and date filtering uses the period's start, not the check-in's creation\ntime. The API cannot check anyone in or out.\n<!-- end -->\n\n<!-- when: calendar -->\n## Calendar occurrences\n\nEvent instances expand recurrence into occurrences. A booking can return a\n`BookedEvent` stub when the user lacks access to its event. Events, instances\nand times are read-only; tags, resources, rooms and folders have write APIs.\n<!-- end -->\n\n<!-- when: registrations -->\n## Registrations\n\nA signup describes a registration event. Its attendee records join back to\nPeople and distinguish active, waitlisted and canceled attendees. This API\ncannot register someone.\n<!-- end -->\n\n<!-- when: giving -->\n## Giving amounts and donor joins\n\nAmounts are integer cents with an explicit currency. A donation's designations\nallocate it across funds; do not count a donation once per designation. Join\ndonors by person id and funds by fund id. Recurring schedules and the donations\nthey produce are different records. Creating/editing a donation, refunding it\nor committing a batch changes financial records; named giving tools only read.\n<!-- end -->\n\n<!-- when: publishing -->\n## Publishing\n\nEpisodes, channels and resources are available through the indexed API. A public\nChurch Center link and a media resource are different things; use the record\nthe task asks for. Signed download URL generation is refused because the\nprivate link would remain withheld.\n<!-- end -->\n\n<!-- when: webhooks -->\n## Webhooks\n\nA subscription names an event and callback destination. Signing keys, private\ndownload destinations and opaque event/delivery payloads or headers remain\nwithheld. Status and event metadata are available for audits.\nWebhook signing-key rotation is refused: the new credential cannot be returned\nor installed on the receiver through this connection.\n<!-- end -->\n\n<!-- when: api -->\n## Organization API\n\nThe organization API includes application metadata and personal-access-token\ninventory. Inventory records describe tokens; they do not return their values.\nRevoking a token is a write and may interrupt another integration, including\nthis connection if its own token is revoked.\n<!-- end -->\n\n<!-- when: limited -->\n## Product boundary\n\nSome products are unavailable on this connection. People lists and their rules\nare unavailable because they can query another product. Webhook payloads and\nchanges to existing subscriptions are unavailable because their ids do not name\ntheir product. A new subscription must name a reachable product's event.\n\nStored workflow cards, notes, custom fields and form answers remain readable\nwhen reachable: staff or automations may have copied information into them.\nThe connection does not infer the provenance of those stored records.\n<!-- end -->\n\n## Not available\n\nOnly reviewed product versions and indexed operations are callable. File\nuploads use a separate host this connection does not reach. Credentials and\nprivate calendar access codes cannot be recovered through a tool.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `list_person_field_data`, `list_person_notes`, `list_workflow_cards`, `list_workflows`, `pco_api_details`, `pco_api_search`, `search_people`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. For `pco_api_read`, put it in `query[page.param]` or `body[page.param]`, as `page.in` specifies. Keep the original method, path, filters, page size, fields and headers. Paged `list_*` and `search_*` reads take `limit` (default 25, at most 100). The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n<!-- when: services -->\n- **Paging:** For `list_person_schedules`, `list_plans`, `list_service_types`, `list_teams`, pass `page.nextCursor` as the top-level `cursor`.\n<!-- end -->\n<!-- when: groups -->\n- **Paging:** For `list_group_memberships`, `list_groups`, pass `page.nextCursor` as the top-level `cursor`.\n<!-- end -->\n<!-- when: giving -->\n- **Paging:** For `list_donations`, `list_funds`, pass `page.nextCursor` as the top-level `cursor`.\n<!-- end -->\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `pco_api_search` finds this API's operations and reads no account data, `pco_api_details` shows one operation's contract, and `pco_api_read` calls a reviewed read. `pco_api_write` calls a reviewed write; the host may ask the user to confirm it. An operation without a reviewed verdict is refused. 2 operations are refused by design; use `pco_api_details` with the method and path to
|
|
3
|
+
"content": "## Identity first\n\nA People person id joins records across products. Resolve people by identity,\nthen join on `person_id`; names can repeat. A membership, scheduled person,\nworkflow card and person each have their own record id.\n\n`get_me` identifies the token's user and the organization's time zone. Bare\ndates mean the organization's local day; use a timestamp with an offset for\nan exact boundary. Name filters are exact unless their value ends in `%`.\n\nAn old person id may have been merged. Read `/people/v2/person_mergers` with\n`where[person_to_remove_id]`, follow `person_to_keep_id`, then read that profile.\nAn empty history does not establish that a person was merged.\n\nReduce pastoral information inside `execute_code` and return aggregates unless\nthe task needs named people. A named projection omits some safe vendor fields;\nread the indexed record when the task needs those fields.\n\n<!-- when: people -->\n## People and follow-up\n\nResolve a person before changing their profile. An omitted attribute leaves\nthe current value alone; `null` clears it. Archiving a person is reversible;\ndeleting a profile is permanent.\n\nWorkflow cards join people to a workflow and its ordered steps. Resolve both\nthe card id and its person id before acting on a card. Snoozing needs a duration;\npromoting a card completes its step, while skipping moves on without completing\nit. A workflow card note and a profile note are different records. Profile note\ncategories can notify their subscribers.\n\nCustom field values need their field-definition records to supply names and\ndata types. Form answers similarly join submissions, values and fields; the\nAPI cannot submit a form on someone's behalf.\n<!-- end -->\n\n<!-- when: lists -->\nSaved People lists are live cross-product queries. Their results are only as\nfresh as their last refresh. Refreshing a list can fire its automations, such\nas adding a person to a workflow, and is a write even when the task is a count.\n<!-- end -->\n\n<!-- when: services -->\n## Services plans and schedules\n\nResolve the service type before finding a dated plan. Plan times define the\nservice dates. `get_plan` combines the plan, order of service and scheduled\nteam; a cut section can be read through its indexed items or team-members path.\nItem durations are seconds, so divide by 60 when reporting minutes.\n\nScheduling needs a team and may need a position. `C` means confirmed, `U`\nunconfirmed and `D` declined. Preparing a notification and sending it are\ndifferent actions. A song item's identity comes from its song and arrangement;\nmusical keys are not credentials. Services cannot upload a file through this\nconnection.\n<!-- end -->\n\n<!-- when: groups -->\n## Groups membership joins\n\nUse a group's membership records to distinguish leaders from members. On this\nAPI version memberships carry no person attributes; `include=person` supplies\nnames, and the related person id supplies contact details and other product\nrecords. The API can change memberships and group settings, but cannot create\na group, create a meeting or record group attendance.\n<!-- end -->\n\n<!-- when: check-ins -->\n## Check-Ins occurrences\n\nAn event is recurring; its event period is a dated occurrence. Period counts\nare the efficient attendance answer. Individual check-ins join a person to a\nperiod, and date filtering uses the period's start, not the check-in's creation\ntime. The API cannot check anyone in or out.\n<!-- end -->\n\n<!-- when: calendar -->\n## Calendar occurrences\n\nEvent instances expand recurrence into occurrences. A booking can return a\n`BookedEvent` stub when the user lacks access to its event. Events, instances\nand times are read-only; tags, resources, rooms and folders have write APIs.\n<!-- end -->\n\n<!-- when: registrations -->\n## Registrations\n\nA signup describes a registration event. Its attendee records join back to\nPeople and distinguish active, waitlisted and canceled attendees. This API\ncannot register someone.\n<!-- end -->\n\n<!-- when: giving -->\n## Giving amounts and donor joins\n\nAmounts are integer cents with an explicit currency. A donation's designations\nallocate it across funds; do not count a donation once per designation. Join\ndonors by person id and funds by fund id. Recurring schedules and the donations\nthey produce are different records. Creating/editing a donation, refunding it\nor committing a batch changes financial records; named giving tools only read.\n<!-- end -->\n\n<!-- when: publishing -->\n## Publishing\n\nEpisodes, channels and resources are available through the indexed API. A public\nChurch Center link and a media resource are different things; use the record\nthe task asks for. Signed download URL generation is refused because the\nprivate link would remain withheld.\n<!-- end -->\n\n<!-- when: webhooks -->\n## Webhooks\n\nA subscription names an event and callback destination. Signing keys, private\ndownload destinations and opaque event/delivery payloads or headers remain\nwithheld. Status and event metadata are available for audits.\nWebhook signing-key rotation is refused: the new credential cannot be returned\nor installed on the receiver through this connection.\n<!-- end -->\n\n<!-- when: api -->\n## Organization API\n\nThe organization API includes application metadata and personal-access-token\ninventory. Inventory records describe tokens; they do not return their values.\nRevoking a token is a write and may interrupt another integration, including\nthis connection if its own token is revoked.\n<!-- end -->\n\n<!-- when: limited -->\n## Product boundary\n\nSome products are unavailable on this connection. People lists and their rules\nare unavailable because they can query another product. Webhook payloads and\nchanges to existing subscriptions are unavailable because their ids do not name\ntheir product. A new subscription must name a reachable product's event.\n\nStored workflow cards, notes, custom fields and form answers remain readable\nwhen reachable: staff or automations may have copied information into them.\nThe connection does not infer the provenance of those stored records.\n<!-- end -->\n\n## Not available\n\nOnly reviewed product versions and indexed operations are callable. File\nuploads use a separate host this connection does not reach. Credentials and\nprivate calendar access codes cannot be recovered through a tool.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `list_person_field_data`, `list_person_notes`, `list_workflow_cards`, `list_workflows`, `pco_api_details`, `pco_api_search`, `search_people`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. For `pco_api_read`, put it in `query[page.param]` or `body[page.param]`, as `page.in` specifies. Keep the original method, path, filters, page size, fields and headers. Paged `list_*` and `search_*` reads take `limit` (default 25, at most 100). The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n<!-- when: services -->\n- **Paging:** For `list_person_schedules`, `list_plans`, `list_service_types`, `list_teams`, pass `page.nextCursor` as the top-level `cursor`.\n<!-- end -->\n<!-- when: groups -->\n- **Paging:** For `list_group_memberships`, `list_groups`, pass `page.nextCursor` as the top-level `cursor`.\n<!-- end -->\n<!-- when: giving -->\n- **Paging:** For `list_donations`, `list_funds`, pass `page.nextCursor` as the top-level `cursor`.\n<!-- end -->\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `pco_api_search` finds this API's operations and reads no account data, `pco_api_details` shows one operation's contract, and `pco_api_read` calls a reviewed read. Details' `response.fields` promises selection only; `withheld` hints are incomplete, and absence never means safe or readable. `pco_api_write` calls a reviewed write; the host may ask the user to confirm it. An operation without a reviewed verdict is refused. 2 operations are refused by design; use `pco_api_details` with the method and path to see each summary and refusal, even for methods outside the read/write schemas.\n- **Writes:** a write is sent once and never retried. When its outcome is unknown, read the target before repeating it. Writes take no idempotency key.\n- **Errors:** codes come from the vendor's statuses and error facts, never its prose. `not_found` means the record does not exist; this API tells absence from a permission gap. `conflict` means: HTTP 409 for duplicate records in People.\n- **Withheld values:** a value withheld for safety reads `\"[redacted]\"`. Writes refuse whole marker values and withheld links; longer prose may mention the marker. Paths and queries refuse any containment.",
|
|
4
4
|
"instructionsHeading": "Organization instructions"
|
|
5
5
|
};
|
|
@@ -139,43 +139,91 @@ function safeRecord(value) {
|
|
|
139
139
|
out["LinkedTxn"] = source["LinkedTxn"].map((row) => pick(row, ["TxnId", "TxnType", "TxnLineId"]));
|
|
140
140
|
return out;
|
|
141
141
|
}
|
|
142
|
+
const REPORT_KEYS = new Set([
|
|
143
|
+
"Header",
|
|
144
|
+
"Columns",
|
|
145
|
+
"Column",
|
|
146
|
+
"Rows",
|
|
147
|
+
"Row",
|
|
148
|
+
"Summary",
|
|
149
|
+
"ColData",
|
|
150
|
+
"ColTitle",
|
|
151
|
+
"ColType",
|
|
152
|
+
"MetaData",
|
|
153
|
+
"Name",
|
|
154
|
+
"Value",
|
|
155
|
+
"Option",
|
|
156
|
+
"Time",
|
|
157
|
+
"ReportName",
|
|
158
|
+
"DateMacro",
|
|
159
|
+
"ReportBasis",
|
|
160
|
+
"StartPeriod",
|
|
161
|
+
"EndPeriod",
|
|
162
|
+
"SummarizeColumnsBy",
|
|
163
|
+
"Currency",
|
|
164
|
+
"value",
|
|
165
|
+
"id",
|
|
166
|
+
"type",
|
|
167
|
+
"group",
|
|
168
|
+
]);
|
|
169
|
+
const ENTITIES = ["Customer", "Vendor", "Account", "Item", "Invoice", "Bill", "Payment", "BillPayment"];
|
|
170
|
+
const COMPANY_KEYS = [
|
|
171
|
+
"Id",
|
|
172
|
+
"SyncToken",
|
|
173
|
+
"CompanyName",
|
|
174
|
+
"LegalName",
|
|
175
|
+
"Country",
|
|
176
|
+
"FiscalYearStartMonth",
|
|
177
|
+
"SupportedLanguages",
|
|
178
|
+
];
|
|
142
179
|
// Report trees retain vendor hierarchy, column metadata and empty strings. No index has a fixed metric meaning.
|
|
143
180
|
function reportTree(value) {
|
|
144
181
|
if (Array.isArray(value))
|
|
145
182
|
return value.map(reportTree);
|
|
146
183
|
if (typeof value !== "object" || value === null)
|
|
147
184
|
return scalar(value);
|
|
148
|
-
const keys = new Set([
|
|
149
|
-
"Header",
|
|
150
|
-
"Columns",
|
|
151
|
-
"Column",
|
|
152
|
-
"Rows",
|
|
153
|
-
"Row",
|
|
154
|
-
"Summary",
|
|
155
|
-
"ColData",
|
|
156
|
-
"ColTitle",
|
|
157
|
-
"ColType",
|
|
158
|
-
"MetaData",
|
|
159
|
-
"Name",
|
|
160
|
-
"Value",
|
|
161
|
-
"Option",
|
|
162
|
-
"Time",
|
|
163
|
-
"ReportName",
|
|
164
|
-
"DateMacro",
|
|
165
|
-
"ReportBasis",
|
|
166
|
-
"StartPeriod",
|
|
167
|
-
"EndPeriod",
|
|
168
|
-
"SummarizeColumnsBy",
|
|
169
|
-
"Currency",
|
|
170
|
-
"value",
|
|
171
|
-
"id",
|
|
172
|
-
"type",
|
|
173
|
-
"group",
|
|
174
|
-
]);
|
|
175
185
|
return Object.fromEntries(Object.entries(value)
|
|
176
|
-
.filter(([key]) =>
|
|
186
|
+
.filter(([key]) => REPORT_KEYS.has(key))
|
|
177
187
|
.map(([key, entry]) => [key, reportTree(entry)]));
|
|
178
188
|
}
|
|
189
|
+
/** What `safeRecord` does to a path within a record: returns it whole, trims it to picked fields, or drops it. */
|
|
190
|
+
function recordPath(path) {
|
|
191
|
+
const [head, ...rest] = path;
|
|
192
|
+
if (head === undefined)
|
|
193
|
+
return "trimmed";
|
|
194
|
+
if (scalars.includes(head))
|
|
195
|
+
return rest.length ? "dropped" : "whole";
|
|
196
|
+
if (details.includes(head) || head === "Line")
|
|
197
|
+
return recordPath(rest);
|
|
198
|
+
const picked = head === "MetaData"
|
|
199
|
+
? ["CreateTime", "LastUpdatedTime"]
|
|
200
|
+
: head === "LinkedTxn"
|
|
201
|
+
? ["TxnId", "TxnType", "TxnLineId"]
|
|
202
|
+
: references.includes(head)
|
|
203
|
+
? ["value", "name", "type"]
|
|
204
|
+
: undefined;
|
|
205
|
+
if (!picked)
|
|
206
|
+
return "dropped";
|
|
207
|
+
if (rest.length === 0)
|
|
208
|
+
return "trimmed";
|
|
209
|
+
return rest.length === 1 && picked.includes(rest[0]) ? "whole" : "dropped";
|
|
210
|
+
}
|
|
211
|
+
/** The closed projection as a discovery hint: a path it always drops is withheld. */
|
|
212
|
+
function quickbooksWithholds(template) {
|
|
213
|
+
return (path) => {
|
|
214
|
+
const names = path.map((segment) => segment.replace(/\[\]$/, ""));
|
|
215
|
+
if (template.startsWith("/reports/"))
|
|
216
|
+
return !names.every((name) => REPORT_KEYS.has(name));
|
|
217
|
+
if (template.startsWith("/companyinfo/"))
|
|
218
|
+
return (names[0] !== "CompanyInfo" || names.length > 2 || (names.length === 2 && !COMPANY_KEYS.includes(names[1])));
|
|
219
|
+
const [root, ...rest] = template === "/query" && names[0] === "QueryResponse" ? names.slice(1) : names;
|
|
220
|
+
if (template === "/query" && names[0] !== "QueryResponse")
|
|
221
|
+
return true;
|
|
222
|
+
if (root === undefined)
|
|
223
|
+
return false;
|
|
224
|
+
return !ENTITIES.includes(root) || recordPath(rest) === "dropped";
|
|
225
|
+
};
|
|
226
|
+
}
|
|
179
227
|
export function quickbooksSafety(options) {
|
|
180
228
|
return {
|
|
181
229
|
reviewed: {
|
|
@@ -188,32 +236,25 @@ export function quickbooksSafety(options) {
|
|
|
188
236
|
return { method: op.method, template: op.path };
|
|
189
237
|
},
|
|
190
238
|
withholdsErrors: () => true,
|
|
239
|
+
withholds: (operation) => quickbooksWithholds(operation.template),
|
|
191
240
|
redact: (body, operation) => {
|
|
192
241
|
const root = object(body);
|
|
193
242
|
if (operation.template.startsWith("/reports/"))
|
|
194
243
|
return reportTree(root);
|
|
195
244
|
if (operation.template.startsWith("/companyinfo/"))
|
|
196
245
|
return {
|
|
197
|
-
CompanyInfo: pick(root["CompanyInfo"],
|
|
198
|
-
"Id",
|
|
199
|
-
"SyncToken",
|
|
200
|
-
"CompanyName",
|
|
201
|
-
"LegalName",
|
|
202
|
-
"Country",
|
|
203
|
-
"FiscalYearStartMonth",
|
|
204
|
-
"SupportedLanguages",
|
|
205
|
-
]),
|
|
246
|
+
CompanyInfo: pick(root["CompanyInfo"], COMPANY_KEYS),
|
|
206
247
|
};
|
|
207
248
|
if (operation.template === "/query") {
|
|
208
249
|
const query = object(root["QueryResponse"]);
|
|
209
250
|
return {
|
|
210
251
|
QueryResponse: Object.fromEntries(Object.entries(query)
|
|
211
|
-
.filter(([key]) =>
|
|
252
|
+
.filter(([key]) => ENTITIES.includes(key) && Array.isArray(query[key]))
|
|
212
253
|
.map(([key, rows]) => [key, rows.map(safeRecord)])),
|
|
213
254
|
};
|
|
214
255
|
}
|
|
215
256
|
return Object.fromEntries(Object.entries(root)
|
|
216
|
-
.filter(([key]) =>
|
|
257
|
+
.filter(([key]) => ENTITIES.includes(key))
|
|
217
258
|
.map(([key, record]) => [key, safeRecord(record)]));
|
|
218
259
|
},
|
|
219
260
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Generated by scripts/generate-providers.mjs; do not edit.
|
|
2
2
|
export const skill = {
|
|
3
|
-
"content": "## Accounting identity and joins\n\nAn Id identifies a record inside this company and record family. DocNumber is\na human label, may repeat, and is not an Id. SyncToken is the public record\nversion used for concurrency control, not an OAuth credential. References carry\nthe target Id in value; invoice CustomerRef joins Customer, bill VendorRef\njoins Vendor, and payment Line.LinkedTxn joins invoices or bills.\n\nAccounting Payment and BillPayment records describe received and paid money;\nreading them does not execute a payment. Desktop, Payments, Payroll, attachments,\nand unreviewed accounting entities are outside this connection.\n\n## Money, time and reports\n\nAmounts use decimal currency units, never integer cents. Preserve CurrencyRef\nand ExchangeRate when combining transactions; never sum unlike currencies.\nTxnDate is the company's accounting date, not a UTC instant. MetaData timestamps\ncarry their own offset. Customer and vendor searches omit inactive records\nunless the Active filter explicitly selects them.\n\nReports are trees with Header, Columns, nested Rows and section summaries.\nRead column types and metadata together with row values; column order and\nsection labels can change. Empty amount strings mean no value, not zero.\nDo not flatten away section identity or add totals to their component rows.\nCheck the returned period, basis, currency and NoReportData option before\ncomparing reports. Aging is an as-of snapshot, rather than a transaction range.\n\nReturn aggregates and the few identifying records the task needs; avoid\ncopying full customer or vendor directories into the conversation.\n\n\n## Named writes and reconciliation\n\nThe connector defaults to read-only. Deployment code must set `readOnly: false`\nto expose named writes; endpoint and pool read-only policies still apply. The\nAPI hatch remains read-only. `create_customer` accepts DisplayName and optional\nCompanyName. `update_customer` requires caller-observed Id and SyncToken and\nchanges only those naming fields with sparse:true. Omitted CompanyName stays\nunchanged. A stale-version conflict never fetches a newer version or overwrites\nit. Read and review the current record before proposing a new update.\nDisplayName must be unique across customers, vendors and employees.\n\n`create_invoice` creates a real accounting transaction, never a draft, for a\nverified US company and a customer using USD. This bounds the no-email claim\nto the reviewed US autosend contract and money to two-decimal USD units. First\nobserve an active customer with its CurrencyRef and active Service item Ids.\nSupply currency USD, a positive explicit exchange rate, accounting and\ndue dates, a reconciliation DocNumber and 1–20 lines. Each line is one service\nunit with an explicit positive amount in decimal currency units, supplied as a\nstring with exactly two fractional digits. No prices, dates or exchange rates\nare defaulted or fetched. Preflight reads verify the fixed company Country is US\nand recheck customer currency and item activity/type; they are not an atomic snapshot. Vendor validation can still\nreject changed references. Taxes and totals are computed by QuickBooks: inspect\nthe returned currency, exchange rate and totals; line sums are not a promise of\nan untaxed total. Locale/company tax requirements may reject this narrow body.\nNo customer/item creation happens implicitly.\nEach additional request charges the call-admission budget before dispatch.\nContradictory returned references, dates, rate or service prices produce an\nunknown outcome; reconcile through reads rather than repeating the write.\n\nInvoice email status is fixed to NotSet; online, IPN, credit-card, ACH, PayPal\nand Affirm payment flags are fixed false. No email field or send operation is\nexposed. Company invoice-autosend opt-in does not supply the documented\nNeedToSend request state. This prevents this creation request from asking Intuit\nto email the invoice, including when the customer has an email on file.\nInvoice updates are unavailable: preserving an existing send-ready state has\nnot been established safe, while resetting it would change payment settings.\nDeletes, voids, email delivery, charges, refunds, other accounting writes and\narbitrary mutations are unavailable.\n\nEvery write requires a caller-generated `requestId`: 1–50 ASCII letters,\ndigits, underscores or hyphens, starting with a letter or digit. Keep it stable\nfor the exact intent and unique across requests/tools for this company. It is\nsent in Intuit's requestid query parameter with minorversion 75, rather than an\nidempotency header. Intuit documents recognizing the same content and ID and\nreturning the original response; no retention duration is established. This\nnever permits automatic replay. A dispatched write is sent once even after\n401, timeout, server failure or an invalid success response.\n\nAfter an unknown customer-create outcome, search exact DisplayName, including\ninactive records when needed, and inspect the match. For an invoice, search\nDocNumber plus CustomerRef and inspect currency, dates, lines and computed\namounts. DocNumber can repeat, so it alone proves nothing. Reconcile and report\nambiguity before considering another create; never generate a fresh requestId\nand blindly repeat a potentially committed intent. Unknown update outcomes\nneed a read of the known Id and version. A requestId is not a lookup endpoint or\na permanent duplicate-prevention guarantee.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `quickbooks_api_details`, `quickbooks_api_search`, `search_records`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. Keep the original method, path, filters, page size, fields and headers. Paged `list_*` and `search_*` reads take `limit` (default 25, at most 100). The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `quickbooks_api_search` finds this API's operations and reads no account data, `quickbooks_api_details` shows one operation's contract, and `quickbooks_api_read` calls a reviewed read. An operation without a reviewed verdict is refused. 3 operations are refused by design; use `quickbooks_api_details` with the method and path to
|
|
3
|
+
"content": "## Accounting identity and joins\n\nAn Id identifies a record inside this company and record family. DocNumber is\na human label, may repeat, and is not an Id. SyncToken is the public record\nversion used for concurrency control, not an OAuth credential. References carry\nthe target Id in value; invoice CustomerRef joins Customer, bill VendorRef\njoins Vendor, and payment Line.LinkedTxn joins invoices or bills.\n\nAccounting Payment and BillPayment records describe received and paid money;\nreading them does not execute a payment. Desktop, Payments, Payroll, attachments,\nand unreviewed accounting entities are outside this connection.\n\n## Money, time and reports\n\nAmounts use decimal currency units, never integer cents. Preserve CurrencyRef\nand ExchangeRate when combining transactions; never sum unlike currencies.\nTxnDate is the company's accounting date, not a UTC instant. MetaData timestamps\ncarry their own offset. Customer and vendor searches omit inactive records\nunless the Active filter explicitly selects them.\n\nReports are trees with Header, Columns, nested Rows and section summaries.\nRead column types and metadata together with row values; column order and\nsection labels can change. Empty amount strings mean no value, not zero.\nDo not flatten away section identity or add totals to their component rows.\nCheck the returned period, basis, currency and NoReportData option before\ncomparing reports. Aging is an as-of snapshot, rather than a transaction range.\n\nReturn aggregates and the few identifying records the task needs; avoid\ncopying full customer or vendor directories into the conversation.\n\n\n## Named writes and reconciliation\n\nThe connector defaults to read-only. Deployment code must set `readOnly: false`\nto expose named writes; endpoint and pool read-only policies still apply. The\nAPI hatch remains read-only. `create_customer` accepts DisplayName and optional\nCompanyName. `update_customer` requires caller-observed Id and SyncToken and\nchanges only those naming fields with sparse:true. Omitted CompanyName stays\nunchanged. A stale-version conflict never fetches a newer version or overwrites\nit. Read and review the current record before proposing a new update.\nDisplayName must be unique across customers, vendors and employees.\n\n`create_invoice` creates a real accounting transaction, never a draft, for a\nverified US company and a customer using USD. This bounds the no-email claim\nto the reviewed US autosend contract and money to two-decimal USD units. First\nobserve an active customer with its CurrencyRef and active Service item Ids.\nSupply currency USD, a positive explicit exchange rate, accounting and\ndue dates, a reconciliation DocNumber and 1–20 lines. Each line is one service\nunit with an explicit positive amount in decimal currency units, supplied as a\nstring with exactly two fractional digits. No prices, dates or exchange rates\nare defaulted or fetched. Preflight reads verify the fixed company Country is US\nand recheck customer currency and item activity/type; they are not an atomic snapshot. Vendor validation can still\nreject changed references. Taxes and totals are computed by QuickBooks: inspect\nthe returned currency, exchange rate and totals; line sums are not a promise of\nan untaxed total. Locale/company tax requirements may reject this narrow body.\nNo customer/item creation happens implicitly.\nEach additional request charges the call-admission budget before dispatch.\nContradictory returned references, dates, rate or service prices produce an\nunknown outcome; reconcile through reads rather than repeating the write.\n\nInvoice email status is fixed to NotSet; online, IPN, credit-card, ACH, PayPal\nand Affirm payment flags are fixed false. No email field or send operation is\nexposed. Company invoice-autosend opt-in does not supply the documented\nNeedToSend request state. This prevents this creation request from asking Intuit\nto email the invoice, including when the customer has an email on file.\nInvoice updates are unavailable: preserving an existing send-ready state has\nnot been established safe, while resetting it would change payment settings.\nDeletes, voids, email delivery, charges, refunds, other accounting writes and\narbitrary mutations are unavailable.\n\nEvery write requires a caller-generated `requestId`: 1–50 ASCII letters,\ndigits, underscores or hyphens, starting with a letter or digit. Keep it stable\nfor the exact intent and unique across requests/tools for this company. It is\nsent in Intuit's requestid query parameter with minorversion 75, rather than an\nidempotency header. Intuit documents recognizing the same content and ID and\nreturning the original response; no retention duration is established. This\nnever permits automatic replay. A dispatched write is sent once even after\n401, timeout, server failure or an invalid success response.\n\nAfter an unknown customer-create outcome, search exact DisplayName, including\ninactive records when needed, and inspect the match. For an invoice, search\nDocNumber plus CustomerRef and inspect currency, dates, lines and computed\namounts. DocNumber can repeat, so it alone proves nothing. Reconcile and report\nambiguity before considering another create; never generate a fresh requestId\nand blindly repeat a potentially committed intent. Unknown update outcomes\nneed a read of the known Id and version. A requestId is not a lookup endpoint or\na permanent duplicate-prevention guarantee.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `quickbooks_api_details`, `quickbooks_api_search`, `search_records`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. Keep the original method, path, filters, page size, fields and headers. Paged `list_*` and `search_*` reads take `limit` (default 25, at most 100). The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `quickbooks_api_search` finds this API's operations and reads no account data, `quickbooks_api_details` shows one operation's contract, and `quickbooks_api_read` calls a reviewed read. Details' `response.fields` promises selection only; `withheld` hints are incomplete, and absence never means safe or readable. An operation without a reviewed verdict is refused. 3 operations are refused by design; use `quickbooks_api_details` with the method and path to see each summary and refusal, even for methods outside the read/write schemas.\n- **Writes:** a write is sent once and never retried. When its outcome is unknown, read the target before repeating it. Writes take no idempotency key.\n- **Errors:** codes come from the vendor's statuses and error facts, never its prose. An unknown id, or one these credentials cannot reach, fails as `connector_call_failed`; this API does not tell the two apart. `conflict` means: Intuit fault 5010 for a stale SyncToken; Intuit fault 6240 for a duplicate DisplayName.\n- **Withheld values:** a value withheld for safety reads `\"[redacted]\"`. Writes refuse whole marker values and withheld links; longer prose may mention the marker. Paths and queries refuse any containment.",
|
|
4
4
|
"instructionsHeading": "Company instructions"
|
|
5
5
|
};
|
|
@@ -121,6 +121,7 @@ export const safety = {
|
|
|
121
121
|
return { method: op.method, template: op.path };
|
|
122
122
|
},
|
|
123
123
|
redact: (data, op) => values.redact(data, op.method, op.template),
|
|
124
|
+
withholds: (op) => values.withholds(op.method, op.template),
|
|
124
125
|
withholdsErrors: (op) => values.withholdsErrors(sheetsIndex.ops.find((entry) => entry.method === op.method && entry.path === op.template)),
|
|
125
126
|
},
|
|
126
127
|
sensitiveFields: { names: [], choice: "drop" },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Generated by scripts/generate-providers.mjs; do not edit.
|
|
2
2
|
export const skill = {
|
|
3
|
-
"content": "## Find the workbook and resolve ranges\n\nSheets starts from a spreadsheet id. A spreadsheet id the person gives works\nas is; search Drive only for a name. The Drive file id is\nthe spreadsheet id. Moving, sharing and trashing the file also belong to Drive.\nRead `get_spreadsheet` first to resolve sheet titles, numeric sheet ids and named\nranges before writing. Sheet id zero is valid; a tab's position is not its id.\n\nA1 ranges use sheet titles; structural GridRanges use numeric sheet ids with\nzero-based, end-exclusive row and column indexes. Quote titles containing\nspaces or punctuation and double apostrophes: `'Alice''s costs'!A1:D7`.\nA bare title may resolve to a named range; quote it to address the whole tab.\n\n## Read cell meaning\n\nDisplayed values reflect spreadsheet locale and number formats. Use\nunformatted numbers for arithmetic and formulas when inspecting expressions.\nDate serials count days from 1899-12-30; interpret dates using the workbook's\ntime zone. Trailing empty rows and cells may be absent. An empty range is a\nvalid result, not proof that the sheet is missing.\n\nWhole-row content reads can resume partway through a range. A named range or\nwhole-sheet reference may be re-read and earlier rows skipped; rowOffset is\nrelative to Google's echoed range. Reduce amounts inside execute_code before\nreturning them. Read an oversized cell separately to recover its full text.\n\n## Edit and reconcile\n\nUse RAW for external data; USER_ENTERED can turn leading equals signs into\nformulas. In cell writes, null leaves a cell unchanged and an empty string\nclears it. Clearing preserves formatting and validation. `append_values` is\npinned to INSERT_ROWS and inserts rows after the detected table. A vendor append using\nOVERWRITE can replace existing cells below that table.\n\nRead just before replacing another person's cells. Sheets has no revision\nprecondition or replay key: concurrent edits can be overwritten. After an\nuncertain append, read the target table and identify the row before adding\nanything missing. After an uncertain workbook create, search Drive by title\nbefore creating another workbook.\n\nStructural batches apply atomically, but can delete rows, sheets or\nprotections. An oversized reply may be summarized after the edit saved;\ninspect the workbook to recover details. Preview comment mutations and\nexternal data-source configuration, refresh and administration are outside\nthis connection. Developer-metadata searches and data-filter reads are\nread-only even though their wire method is POST.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `get_values`, `sheets_api_details`, `sheets_api_search`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. Keep the original method, path, filters, page size, fields and headers. The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `sheets_api_search` finds this API's operations and reads no account data, `sheets_api_details` shows one operation's contract, and `sheets_api_read` calls a reviewed read. `sheets_api_write` calls a reviewed write; the host may ask the user to confirm it. An operation without a reviewed verdict is refused.\n- **Writes:** a write is sent once and never retried. When its outcome is unknown, read the target before repeating it. Writes take no idempotency key.\n- **Errors:** codes come from the vendor's statuses and error facts, never its prose. An unknown id, or one these credentials cannot reach, fails as `connector_call_failed`; this API does not tell the two apart. It documents no conflict.\n- **Withheld values:** a value withheld for safety reads `\"[redacted]\"`. Writes refuse whole marker values and withheld links; longer prose may mention the marker. Paths and queries refuse any containment.",
|
|
3
|
+
"content": "## Find the workbook and resolve ranges\n\nSheets starts from a spreadsheet id. A spreadsheet id the person gives works\nas is; search Drive only for a name. The Drive file id is\nthe spreadsheet id. Moving, sharing and trashing the file also belong to Drive.\nRead `get_spreadsheet` first to resolve sheet titles, numeric sheet ids and named\nranges before writing. Sheet id zero is valid; a tab's position is not its id.\n\nA1 ranges use sheet titles; structural GridRanges use numeric sheet ids with\nzero-based, end-exclusive row and column indexes. Quote titles containing\nspaces or punctuation and double apostrophes: `'Alice''s costs'!A1:D7`.\nA bare title may resolve to a named range; quote it to address the whole tab.\n\n## Read cell meaning\n\nDisplayed values reflect spreadsheet locale and number formats. Use\nunformatted numbers for arithmetic and formulas when inspecting expressions.\nDate serials count days from 1899-12-30; interpret dates using the workbook's\ntime zone. Trailing empty rows and cells may be absent. An empty range is a\nvalid result, not proof that the sheet is missing.\n\nWhole-row content reads can resume partway through a range. A named range or\nwhole-sheet reference may be re-read and earlier rows skipped; rowOffset is\nrelative to Google's echoed range. Reduce amounts inside execute_code before\nreturning them. Read an oversized cell separately to recover its full text.\n\n## Edit and reconcile\n\nUse RAW for external data; USER_ENTERED can turn leading equals signs into\nformulas. In cell writes, null leaves a cell unchanged and an empty string\nclears it. Clearing preserves formatting and validation. `append_values` is\npinned to INSERT_ROWS and inserts rows after the detected table. A vendor append using\nOVERWRITE can replace existing cells below that table.\n\nRead just before replacing another person's cells. Sheets has no revision\nprecondition or replay key: concurrent edits can be overwritten. After an\nuncertain append, read the target table and identify the row before adding\nanything missing. After an uncertain workbook create, search Drive by title\nbefore creating another workbook.\n\nStructural batches apply atomically, but can delete rows, sheets or\nprotections. An oversized reply may be summarized after the edit saved;\ninspect the workbook to recover details. Preview comment mutations and\nexternal data-source configuration, refresh and administration are outside\nthis connection. Developer-metadata searches and data-filter reads are\nread-only even though their wire method is POST.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `get_values`, `sheets_api_details`, `sheets_api_search`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. Keep the original method, path, filters, page size, fields and headers. The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `sheets_api_search` finds this API's operations and reads no account data, `sheets_api_details` shows one operation's contract, and `sheets_api_read` calls a reviewed read. Details' `response.fields` promises selection only; `withheld` hints are incomplete, and absence never means safe or readable. `sheets_api_write` calls a reviewed write; the host may ask the user to confirm it. An operation without a reviewed verdict is refused.\n- **Writes:** a write is sent once and never retried. When its outcome is unknown, read the target before repeating it. Writes take no idempotency key.\n- **Errors:** codes come from the vendor's statuses and error facts, never its prose. An unknown id, or one these credentials cannot reach, fails as `connector_call_failed`; this API does not tell the two apart. It documents no conflict.\n- **Withheld values:** a value withheld for safety reads `\"[redacted]\"`. Writes refuse whole marker values and withheld links; longer prose may mention the marker. Paths and queries refuse any containment.",
|
|
4
4
|
"instructionsHeading": "Connection instructions"
|
|
5
5
|
};
|