@bendyline/gilde 0.1.22 → 0.1.23

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.
Files changed (38) hide show
  1. package/data/connector-types/bl/bluesky-posts/versions/1.0.0/manifest.json +7 -1
  2. package/data/connector-types/ca/calendar-google/versions/1.0.0/manifest.json +14 -0
  3. package/data/connector-types/in/instagram-media/manifest.json +1 -1
  4. package/data/connector-types/in/instagram-media/versions/1.0.0/manifest.json +29 -3
  5. package/data/connector-types/index.json +1 -1
  6. package/data/connector-types/li/linkedin-posts/manifest.json +16 -0
  7. package/data/connector-types/li/linkedin-posts/versions/1.0.0/manifest.json +54 -0
  8. package/data/connector-types/ma/mail-gmail/versions/1.0.0/manifest.json +14 -0
  9. package/data/connector-types/ma/mail-microsoft365/versions/1.0.0/manifest.json +14 -1
  10. package/data/connector-types/ma/mail-outlook/versions/1.0.0/manifest.json +14 -1
  11. package/data/connector-types/x-/x-posts/manifest.json +1 -1
  12. package/data/connector-types/x-/x-posts/versions/1.0.0/manifest.json +30 -4
  13. package/data/craftbook-templates/dr/draft-social-post/manifest.json +2 -1
  14. package/data/craftbook-templates/dr/draft-social-post/versions/1.0.0/craftbook.json +8 -6
  15. package/data/craftbook-templates/index.json +1 -1
  16. package/data/project-types/ca/caregiving-binder/versions/1.1.0/about.md +8 -0
  17. package/data/project-types/ca/caregiving-binder/versions/1.1.0/care.json +4 -0
  18. package/data/project-types/ca/caregiving-binder/versions/1.1.0/manifest.json +178 -0
  19. package/data/project-types/ca/caregiving-binder/versions/1.1.0/meds.json +8 -0
  20. package/data/project-types/ca/caregiving-binder/versions/1.1.0/mission.md +8 -0
  21. package/data/project-types/ca/caregiving-binder/versions/1.1.0/pages/dashboard/index.html +265 -0
  22. package/data/project-types/ch/checkers/versions/1.2.0/about.md +9 -0
  23. package/data/project-types/ch/checkers/versions/1.2.0/game.json +77 -0
  24. package/data/project-types/ch/checkers/versions/1.2.0/manifest.json +167 -0
  25. package/data/project-types/ch/checkers/versions/1.2.0/mission.md +9 -0
  26. package/data/project-types/ch/checkers/versions/1.2.0/pages/board/index.html +1157 -0
  27. package/data/project-types/fl/flashcards/versions/1.1.0/about.md +8 -0
  28. package/data/project-types/fl/flashcards/versions/1.1.0/deck.json +7 -0
  29. package/data/project-types/fl/flashcards/versions/1.1.0/manifest.json +168 -0
  30. package/data/project-types/fl/flashcards/versions/1.1.0/mission.md +6 -0
  31. package/data/project-types/fl/flashcards/versions/1.1.0/pages/review/index.html +476 -0
  32. package/data/project-types/im/image-feed/versions/1.0.0/pages/gallery/index.html +2 -1
  33. package/data/project-types/index.json +1 -1
  34. package/data/project-types/so/social-feed/manifest.json +1 -0
  35. package/data/project-types/so/social-feed/versions/1.0.0/about.md +2 -2
  36. package/data/project-types/so/social-feed/versions/1.0.0/manifest.json +6 -0
  37. package/data/project-types/so/social-feed/versions/1.0.0/pages/dashboard/index.html +4 -2
  38. package/package.json +1 -1
@@ -44,6 +44,12 @@
44
44
  "type": "number",
45
45
  "title": "Hours between account-stats snapshots",
46
46
  "default": 6
47
+ },
48
+ "metricsHistory": {
49
+ "type": "boolean",
50
+ "title": "Record daily engagement history records",
51
+ "description": "One per post per day while a post stays inside the metrics window",
52
+ "default": true
47
53
  }
48
54
  },
49
55
  "required": [
@@ -66,5 +72,5 @@
66
72
  }
67
73
  ],
68
74
  "completeness": "window",
69
- "notes": "Syncs three things from the signed-in account: the account's own posts (text, language, reply threading, link and image-description context, plus like/repost/reply/quote counts), mentions of the account (mention, reply, and quote notifications), and a once-per-day account-stats record (followers, following, total posts) refreshed at the configured interval. Engagement counts on posts from the last metricsWindowDays are re-observed roughly every metricsRefreshHours, updating the existing files in place. The first sync captures up to 500 recent posts; older history is not backfilled. Reposts are skipped in this version, and images are represented by their alt text rather than downloaded. Publishing is off by default: a gezel can only draft posts until allowPublish is enabled on the binding, and every publish is individually consented through the daemon (night shift defers publishes to the outbox for daytime approval)."
75
+ "notes": "Syncs three things from the signed-in account: the account's own posts (text, language, reply threading, link and image-description context, plus like/repost/reply/quote counts), mentions of the account (mention, reply, and quote notifications), and a once-per-day account-stats record (followers, following, total posts) refreshed at the configured interval. Engagement counts on posts from the last metricsWindowDays are re-observed roughly every metricsRefreshHours, updating the existing files in place; unless metricsHistory is turned off, each observation also writes a per-post per-day engagement history record under the posts corpus's metrics-history/ bucket, so engagement curves stay reconstructable after counts move on. The first sync captures up to 500 recent posts; older history is not backfilled. Reposts are skipped in this version, and synced images are represented by their alt text rather than downloaded. Publishing is off by default: a gezel can only draft posts until allowPublish is enabled on the binding, and every publish is individually consented through the daemon (night shift defers publishes to the outbox for daytime approval). Published posts can embed up to 4 images (1,000,000 bytes each at most; png/jpg/jpeg/webp/gif), read from project-relative paths resolved against the project's artifacts directory first, then its workspace."
70
76
  }
@@ -28,6 +28,20 @@
28
28
  "authParams": {
29
29
  "access_type": "offline",
30
30
  "prompt": "consent"
31
+ },
32
+ "clientSetup": {
33
+ "providerLabel": "Google",
34
+ "docsUrl": "https://console.cloud.google.com/apis/credentials",
35
+ "secretRequired": true,
36
+ "appTypeNote": "Create an OAuth client ID of type Desktop app in Google Cloud Console. Desktop clients are issued a client secret Google does not treat as confidential — enter both here. Any loopback port works for the redirect.",
37
+ "steps": [
38
+ "Sign in at console.cloud.google.com and create (or pick) a project.",
39
+ "Under APIs & Services, open Library and enable the Google Calendar API.",
40
+ "Under APIs & Services, open OAuth consent screen: configure the app as External and add yourself as a test user.",
41
+ "Set the publishing status to In production — apps left in Testing get their sign-ins expired every 7 days. An unverified-app warning on your own app is safe to click through.",
42
+ "Under APIs & Services, open Credentials and choose Create credentials, then OAuth client ID, application type 'Desktop app'.",
43
+ "Copy the Client ID and Client Secret from the created client and paste them below. Desktop clients accept any 127.0.0.1 port, so there is no redirect URI to register."
44
+ ]
31
45
  }
32
46
  },
33
47
  "normalize": {
@@ -3,7 +3,7 @@
3
3
  "kind": "connector-type",
4
4
  "id": "instagram-media",
5
5
  "name": "Instagram Media",
6
- "description": "Keep a searchable copy of your Instagram presence: your posts, reels, and carousels with captions, links, like and comment counts, plus a daily follower count. Works with Instagram professional (business or creator) accounts through Meta's own sign-in. Your crew can draft posts for you, but Gezel never publishes to Instagram itself.",
6
+ "description": "Keep a searchable copy of your Instagram presence: your posts, reels, and carousels with captions, links, like and comment counts and a daily engagement history, plus a daily follower count. Works with Instagram professional (business or creator) accounts through Meta's own sign-in. Your crew can draft posts for review and — once you allow publishing on the connector — publish images with your per-post consent. One honest catch: Instagram's API only accepts images already hosted at a public https URL, so Gezel cannot publish local files directly.",
7
7
  "tags": [
8
8
  "instagram",
9
9
  "social",
@@ -14,6 +14,11 @@
14
14
  "title": "Download media files (reserved — this version stores captions and links only)",
15
15
  "default": false
16
16
  },
17
+ "allowPublish": {
18
+ "type": "boolean",
19
+ "title": "Allow this project's gezels to queue posts for publishing (each publish still needs your consent)",
20
+ "default": false
21
+ },
17
22
  "maxMedia": {
18
23
  "type": "integer",
19
24
  "title": "Most recent posts to keep synced",
@@ -35,6 +40,11 @@
35
40
  "minimum": 0.25,
36
41
  "maximum": 720
37
42
  },
43
+ "metricsHistory": {
44
+ "type": "boolean",
45
+ "title": "Record daily engagement history records",
46
+ "default": true
47
+ },
38
48
  "statsIntervalHours": {
39
49
  "type": "number",
40
50
  "title": "Hours between account-stats snapshots",
@@ -48,12 +58,28 @@
48
58
  "kind": "oauth2",
49
59
  "authorizeUrl": "https://www.instagram.com/oauth/authorize",
50
60
  "tokenUrl": "https://api.instagram.com/oauth/access_token",
51
- "scopes": "instagram_business_basic",
61
+ "scopes": "instagram_business_basic instagram_business_content_publish",
52
62
  "clientIdEnv": "GEZEL_INSTAGRAM_CLIENT_ID",
53
63
  "clientSecretEnv": "GEZEL_INSTAGRAM_CLIENT_SECRET",
54
64
  "longLivedExchange": {
55
65
  "exchangeUrl": "https://graph.instagram.com/access_token",
56
66
  "refreshUrl": "https://graph.instagram.com/refresh_access_token"
67
+ },
68
+ "clientSetup": {
69
+ "providerLabel": "Instagram (Meta)",
70
+ "docsUrl": "https://developers.facebook.com/apps/",
71
+ "secretRequired": true,
72
+ "appTypeNote": "Create a Meta app with Instagram API (Instagram Login) for a business or creator account. The app secret stays on this machine, in your OS keychain.",
73
+ "redirectPort": 6242,
74
+ "redirectNote": "Meta matches redirect URIs exactly and may require an https URI; if it refuses the http form, tell us — https loopback support is planned.",
75
+ "steps": [
76
+ "Sign in at developers.facebook.com, choose My Apps, then Create app (the Business type, or the default when not asked).",
77
+ "On the app dashboard, add the 'Instagram' product and choose 'API setup with Instagram Login'.",
78
+ "Your Instagram account must be Professional (business or creator) — switch it in the Instagram app under Settings if needed, then connect it as the tester account when prompted.",
79
+ "In the product's API setup, open 'Set up Instagram business login' and add the redirect URI shown below under OAuth redirect URIs, exactly.",
80
+ "Copy the Instagram App ID and Instagram App Secret from the same setup page (they also live under App settings, Basic).",
81
+ "Paste both below — the secret stays in your OS keychain. Your own account works while the Meta app stays in development mode."
82
+ ]
57
83
  }
58
84
  },
59
85
  "normalize": {
@@ -62,9 +88,9 @@
62
88
  "actions": [
63
89
  {
64
90
  "name": "publish",
65
- "consentScope": "manual-export"
91
+ "consentScope": "social-publish"
66
92
  }
67
93
  ],
68
94
  "completeness": "window",
69
- "notes": "Works only with Instagram professional accounts (business or creator) via the Instagram API with Instagram Login — personal accounts cannot link. Requires your own Meta developer app: set GEZEL_INSTAGRAM_CLIENT_ID and GEZEL_INSTAGRAM_CLIENT_SECRET, and note Meta requires app review before accounts other than the app's own testers can link in production. Meta issues no refresh token: linking trades the sign-in for a long-lived (~60 day) token that Gezel refreshes in place whenever a sync runs with under 7 days remaining; if the account sits unsynced past expiry, re-link it. Syncs the account's media (posts, reels, carousels) as caption plus permalink — media files are not downloaded in this version — with like and comment counts on posts from the last metricsWindowDays re-observed roughly every metricsRefreshHours, plus a once-per-day account-stats record (followers, following, total posts). The first sync captures up to maxMedia recent posts; older history is not backfilled. Publishing is manual-export only: gezels can draft posts for review, but Gezel never publishes to Instagram on your behalf."
95
+ "notes": "Works only with Instagram professional accounts (business or creator) via the Instagram API with Instagram Login — personal accounts cannot link. Requires your own Meta developer app: set GEZEL_INSTAGRAM_CLIENT_ID and GEZEL_INSTAGRAM_CLIENT_SECRET, and note Meta requires app review before accounts other than the app's own testers can link in production. Meta issues no refresh token: linking trades the sign-in for a long-lived (~60 day) token that Gezel refreshes in place whenever a sync runs with under 7 days remaining; if the account sits unsynced past expiry, re-link it. Syncs the account's media (posts, reels, carousels) as caption plus permalink — media files are not downloaded in this version — with like and comment counts on posts from the last metricsWindowDays re-observed roughly every metricsRefreshHours (each observation also appends a per-day metrics-history record per post, the engagement curve over time, unless metricsHistory is turned off), plus a once-per-day account-stats record (followers, following, total posts). The first sync captures up to maxMedia recent posts; older history is not backfilled. Publishing works, with Meta's contract stated honestly: Instagram's publishing API has Meta's servers fetch the image from a public https URL, and Gezel is local-first and does not host your files — so publishing needs an image that is already hosted somewhere public (any CDN or static host will do), caption-only posts do not exist on Instagram, and drafts stay fully usable without any public URL. The caption travels as the draft's post text, so Gezel's draft flow requires one (Instagram itself allows empty captions). Publishing is off by default: a gezel can only draft until allowPublish is enabled on the binding, and every publish is individually consented through the daemon (night shift defers publishes to the outbox for daytime approval). Accounts linked before publishing existed must re-link from the connector's settings to grant the instagram_business_content_publish permission."
70
96
  }
@@ -1 +1 @@
1
- {"schemaVersion":1,"kind":"connector-type","count":14,"entries":[{"manifest":{"schemaVersion":1,"kind":"connector-type","id":"airtable-records","name":"Airtable Records","description":"Mirror an Airtable table into a searchable corpus — one file per record — via the Airtable API, hosted off-platform through a vendored Prismatic component. Captures the table's current rows each sync.","tags":["airtable","records","spectral"],"maintainer":{"name":"Gezel"},"version":"1.0.0","releasedAt":"2026-07-08T00:00:00Z","driver":"spectral","configSchema":{"type":"object","properties":{"baseId":{"type":"string","title":"Base ID"},"tableName":{"type":"string","title":"Table name"},"view":{"type":"string","title":"View (optional)"},"filterByFormula":{"type":"string","title":"Filter by formula (optional)"}},"required":["baseId","tableName"]},"secretShape":{"kind":"apikey","field":"apiKey"},"source":{"component":"airtable","action":"listRecords","connectionKey":"personalAccessToken","connectionInput":"airtableConnection","inputs":{"baseId":"$config.baseId","tableName":"$config.tableName","view":"$config.view","filterByFormula":"$config.filterByFormula"},"itemsPath":"$.data"},"normalize":{"kind":"mapping","map":{"id":"$.id","title":"$.fields.Name","timestamp":"$.createdTime","body":"$.fields","frontmatter":{"recordId":"$.id","createdTime":"$.createdTime"}}},"actions":[],"completeness":"mirror","notes":"Personal Access Token connection. Each sync re-fetches every row in the table (no incremental cursor); the writer is content-hash idempotent, so re-syncs are cheap and only changed rows rewrite. Deleted Airtable rows are not pruned from the corpus. `title` maps to the `Name` field (Airtable's usual primary column) and falls back to the record id when absent.","availableVersions":["1.0.0"]}},{"manifest":{"schemaVersion":1,"kind":"connector-type","id":"bluesky-posts","name":"Bluesky Posts","description":"Keep a searchable copy of your Bluesky presence: your posts with their likes and reposts, mentions and replies from others, and a daily follower count. Sign in with an app password from Bluesky's settings — never your real password. Your crew can also draft posts for you, and with your permission publish them; every publish still asks for your consent.","tags":["bluesky","social","posts","native"],"maintainer":{"name":"Gezel"},"version":"1.0.0","releasedAt":"2026-08-12T00:00:00Z","driver":"native","configSchema":{"type":"object","properties":{"handle":{"type":"string","title":"Bluesky handle (e.g. yourname.bsky.social)"},"service":{"type":"string","title":"Service URL","default":"https://bsky.social"},"syncMentions":{"type":"boolean","title":"Sync mentions, replies, and quotes","default":true},"allowPublish":{"type":"boolean","title":"Allow this project's gezels to queue posts for publishing (each publish still needs your consent)","default":false},"metricsWindowDays":{"type":"integer","title":"Days of recent posts to keep engagement counts fresh for","default":30,"minimum":1,"maximum":365},"metricsRefreshHours":{"type":"number","title":"Hours between engagement-count refreshes","default":6},"statsIntervalHours":{"type":"number","title":"Hours between account-stats snapshots","default":6}},"required":["handle"]},"secretShape":{"kind":"apikey","field":"appPassword","label":"Bluesky app password","required":true},"source":{"adapterId":"bluesky-posts"},"normalize":{"kind":"native"},"actions":[{"name":"publish","consentScope":"social-publish"}],"completeness":"window","notes":"Syncs three things from the signed-in account: the account's own posts (text, language, reply threading, link and image-description context, plus like/repost/reply/quote counts), mentions of the account (mention, reply, and quote notifications), and a once-per-day account-stats record (followers, following, total posts) refreshed at the configured interval. Engagement counts on posts from the last metricsWindowDays are re-observed roughly every metricsRefreshHours, updating the existing files in place. The first sync captures up to 500 recent posts; older history is not backfilled. Reposts are skipped in this version, and images are represented by their alt text rather than downloaded. Publishing is off by default: a gezel can only draft posts until allowPublish is enabled on the binding, and every publish is individually consented through the daemon (night shift defers publishes to the outbox for daytime approval).","availableVersions":["1.0.0"]}},{"manifest":{"schemaVersion":1,"kind":"connector-type","id":"mail-imap","name":"Email (IMAP)","description":"Mirror an IMAP mailbox into a normalized, searchable corpus of messages.","tags":["email","native"],"maintainer":{"name":"Gezel"},"version":"1.0.0","releasedAt":"2026-07-08T00:00:00Z","driver":"native","configSchema":{"type":"object","properties":{"address":{"type":"string","title":"Email address"},"provider":{"type":"string","const":"imap"},"syncFolders":{"type":"array","title":"Folders to sync","items":{"type":"string"}},"allowedRecipients":{"type":"array","title":"Allowed send recipients","items":{"type":"string"}},"allowedDomains":{"type":"array","title":"Allowed send domains","items":{"type":"string"}}},"required":["address"]},"secretShape":{"kind":"imap"},"source":{"adapterId":"mail-imap"},"normalize":{"kind":"native"},"actions":[{"name":"send","consentScope":"recipient-allowlist"}],"completeness":"mirror","availableVersions":["1.0.0"]}},{"manifest":{"schemaVersion":1,"kind":"connector-type","id":"github-issues","name":"GitHub Issues","description":"Mirror recent issues from one GitHub repository into searchable project files. Public repositories work without sign-in; add a token for private repositories or higher rate limits.","tags":["github","issues","script"],"maintainer":{"name":"Gezel"},"version":"1.0.1","releasedAt":"2026-08-12T00:00:00Z","driver":"script","configSchema":{"type":"object","properties":{"owner":{"type":"string","title":"Repository owner"},"repository":{"type":"string","title":"Repository name"},"apiBaseUrl":{"type":"string","title":"API base URL (GitHub Enterprise only)"}},"required":["owner","repository"]},"secretShape":{"kind":"apikey","field":"token","label":"GitHub personal access token","required":false},"source":{"inlineFetch":"import { defineScript, gezel } from '@bendyline/gezel-sdk';\n\nexport const meta = defineScript({\n name: 'github-issues-fetch',\n description: 'Fetches a bounded, incremental window of GitHub repository issues.',\n inputs: {\n cursor: { type: 'json', description: 'Last successfully synced GitHub updated_at value.' },\n config: { type: 'json', description: 'Repository owner, name, and optional API base URL.', required: true },\n },\n outputs: {\n records: { type: 'array', description: 'GitHub issues ready for connector normalization.', itemType: 'object' },\n cursor: { type: 'string', description: 'Newest updated_at value processed.', nullable: true },\n rateLimited: { type: 'boolean', description: 'True when GitHub throttled the pass; the engine backs off and retries from this cursor.', nullable: true },\n partial: { type: 'boolean', description: 'True when more pages remained after the per-pass page budget.', nullable: true },\n },\n requires: ['network', 'credential:$credential'],\n} as const);\n\ntype Config = { owner?: unknown; repository?: unknown; apiBaseUrl?: unknown };\ntype Issue = Record<string, unknown> & { updated_at?: unknown; pull_request?: unknown };\nconst input = gezel.input as { cursor?: unknown; config?: Config };\nconst config = input.config ?? {};\nconst owner = typeof config.owner === 'string' ? config.owner.trim() : '';\nconst repository = typeof config.repository === 'string' ? config.repository.trim() : '';\nif (!owner || !repository) throw new Error('GitHub owner and repository are required.');\n\nconst apiBaseUrl =\n typeof config.apiBaseUrl === 'string' && config.apiBaseUrl.trim()\n ? config.apiBaseUrl.trim().replace(/\\/+$/, '')\n : 'https://api.github.com';\nlet apiOrigin: string;\ntry {\n const parsed = new URL(apiBaseUrl);\n if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') throw new Error('bad protocol');\n apiOrigin = parsed.origin;\n} catch {\n throw new Error('GitHub API base URL must be an absolute HTTP or HTTPS URL.');\n}\n\nconst params = new URLSearchParams({\n state: 'all',\n sort: 'updated',\n direction: typeof input.cursor === 'string' ? 'asc' : 'desc',\n per_page: '100',\n});\nif (typeof input.cursor === 'string' && input.cursor) params.set('since', input.cursor);\nlet nextUrl =\n apiBaseUrl + '/repos/' + encodeURIComponent(owner) + '/' + encodeURIComponent(repository) + '/issues?' + params;\nlet nextCursor = typeof input.cursor === 'string' ? input.cursor : null;\nconst records: Issue[] = [];\nlet rateLimited = false;\n\nfunction nextPage(link: string | undefined): string | null {\n if (!link) return null;\n for (const part of link.split(',')) {\n const match = part.match(/<([^>]+)>;\\s*rel=\"next\"/);\n if (match?.[1]) return match[1];\n }\n return null;\n}\n\nfunction isThrottle(status: number, headers: Record<string, string | undefined>): boolean {\n if (status === 429) return true;\n return status === 403 && (headers['x-ratelimit-remaining'] === '0' || Boolean(headers['retry-after']));\n}\n\nfor (let page = 0; page < 5 && nextUrl; page++) {\n const target = new URL(nextUrl);\n if (target.origin !== apiOrigin) throw new Error('GitHub pagination changed origin; refusing to forward the token.');\n const headers = {\n Accept: 'application/vnd.github+json',\n 'X-GitHub-Api-Version': '2022-11-28',\n 'User-Agent': 'Gezel-GitHub-Issues-Connector',\n };\n let response;\n try {\n response = await gezel.http.authed(target.toString(), {\n credential: '$credential',\n headers,\n });\n } catch (error) {\n if ((error as { code?: string }).code !== 'CREDENTIAL_MISSING') throw error;\n response = await gezel.http.request(target.toString(), { headers });\n }\n if (isThrottle(response.status, response.headers)) {\n // Emit what this pass already collected with the cursor those records\n // reached, and flag the throttle instead of throwing: a throw voids the\n // batch and re-fetches the same window next tick, which is exactly how\n // rate-limited APIs get hammered. The engine persists the cursor only\n // after a clean write and applies its jittered backoff to the binding.\n rateLimited = true;\n break;\n }\n if (!response.ok) {\n throw new Error('GitHub issues request failed (' + response.status + '): ' + response.body.slice(0, 240));\n }\n const pageItems = JSON.parse(response.body) as unknown;\n if (!Array.isArray(pageItems)) throw new Error('GitHub returned a non-array issues response.');\n for (const raw of pageItems) {\n if (!raw || typeof raw !== 'object') continue;\n const issue = raw as Issue;\n const updatedAt = typeof issue.updated_at === 'string' ? issue.updated_at : '';\n if (updatedAt && (!nextCursor || updatedAt > nextCursor)) nextCursor = updatedAt;\n if (issue.pull_request) continue;\n const labels = Array.isArray(issue.labels)\n ? issue.labels\n .map((label) =>\n typeof label === 'string'\n ? label\n : label && typeof label === 'object'\n ? String((label as Record<string, unknown>).name ?? '')\n : '',\n )\n .filter(Boolean)\n : [];\n const assignees = Array.isArray(issue.assignees)\n ? issue.assignees\n .map((person) =>\n person && typeof person === 'object'\n ? String((person as Record<string, unknown>).login ?? '')\n : '',\n )\n .filter(Boolean)\n : [];\n records.push({\n ...issue,\n gezel_repository: owner + '/' + repository,\n gezel_labels: labels,\n gezel_assignees: assignees,\n });\n }\n const candidate = nextPage(response.headers.link);\n nextUrl = candidate && new URL(candidate).origin === apiOrigin ? candidate : '';\n}\n\nconst cursorOut = records.length > 0 ? nextCursor : typeof input.cursor === 'string' ? input.cursor : null;\ngezel.output({\n records,\n cursor: rateLimited ? cursorOut : nextCursor,\n ...(rateLimited ? { rateLimited: true } : {}),\n ...(nextUrl && !rateLimited ? { partial: true } : {}),\n});\n","idPath":"$.id"},"normalize":{"kind":"mapping","map":{"id":"$.id","title":"$.title","group":"$.gezel_repository","timestamp":"$.updated_at","body":"$.body","frontmatter":{"number":"$.number","state":"$.state","stateReason":"$.state_reason","author":"$.user.login","assignees":"$.gezel_assignees","labels":"$.gezel_labels","milestone":"$.milestone.title","comments":"$.comments","url":"$.html_url","createdAt":"$.created_at","closedAt":"$.closed_at","locked":"$.locked"}}},"actions":[],"completeness":"window","notes":"Uses GitHub's REST list-repository-issues endpoint anonymously for public repositories, or with an optional personal access token for private repositories and higher rate limits. Pull requests are excluded because GitHub's Issues API returns both. The first sync captures up to 500 recently updated issues; later syncs resume from updated_at in oldest-first order so busy repositories catch up over multiple passes. Issue comments are represented by their count but comment bodies are not mirrored in this version. Since 1.0.1 the fetch reports GitHub throttling (HTTP 429, or 403 with an exhausted rate-limit budget) as a rateLimited batch instead of an error, so the sync engine backs off and resumes from the last clean cursor, and declares its credential destination origins for the daemon-side allowlist.","availableVersions":["1.0.1","1.0.0"]}},{"manifest":{"schemaVersion":1,"kind":"connector-type","id":"github-pulls","name":"GitHub Pull Requests","description":"Mirror a project's open pull requests into local files: a summary, one file per changed file with its patch, and the complete unified diff. Lets a gezel review a PR by reading the workspace instead of calling the GitHub API mid-turn.","tags":["github","pull-requests","code-review","diff","native"],"maintainer":{"name":"Gezel"},"version":"1.0.0","releasedAt":"2026-08-10T00:00:00Z","driver":"native","configSchema":{"type":"object","properties":{"maxPulls":{"type":"integer","title":"Open pull requests to mirror","minimum":1,"maximum":200,"default":20},"includeComments":{"type":"boolean","title":"Include review and issue comments","default":true}}},"secretShape":{"kind":"apikey","field":"token","label":"GitHub token (optional; uses your GitHub sign-in)","required":false},"source":{"adapterId":"github-pulls"},"normalize":{"kind":"native"},"actions":[],"completeness":"window","notes":"Takes the repository from the project's own GitHub link rather than binding config, so the coordinates cannot drift from the project. The corpus lives in the project's managed artifacts tree, so mirrored data never dirties the user's checkout. Each open pull request becomes one scope (`pr-<number>`) holding an overview record — title, author, branches, counts, description, and comment digest — plus one record per changed file under `files/` carrying that file's patch, and the complete unified diff as an attachment. Patches and the diff are stored untruncated because a gezel opens these as files. Comment and description text is untrusted and passes through the standard content scan and quarantine. Completeness is `window`: only the newest open pull requests are mirrored, so absence from the corpus does not mean absence upstream. A task launch can target one pull request directly, which syncs just that scope.","availableVersions":["1.0.0"]}},{"manifest":{"schemaVersion":1,"kind":"connector-type","id":"github-releases","name":"GitHub Releases","description":"Mirror GitHub release notes and uploaded release assets into local project files. Public repositories work anonymously; Gezel's GitHub sign-in or an optional token also enables private repositories and draft releases.","tags":["github","releases","artifacts","binaries","native"],"maintainer":{"name":"Gezel"},"version":"1.0.0","releasedAt":"2026-08-09T00:00:00Z","driver":"native","configSchema":{"type":"object","properties":{"owner":{"type":"string","title":"Repository owner"},"repository":{"type":"string","title":"Repository name"},"maxReleases":{"type":"integer","title":"Latest releases (blank = all)","minimum":1,"maximum":10000},"includeDrafts":{"type":"boolean","title":"Include draft releases when signed in","default":true},"includePrereleases":{"type":"boolean","title":"Include prereleases","default":true},"downloadAssets":{"type":"boolean","title":"Download uploaded release assets","default":true},"assetNamePattern":{"type":"string","title":"Asset filename globs (optional, comma-separated)"},"maxAssetSizeMb":{"type":"number","title":"Maximum asset size in MB (blank = unlimited)","exclusiveMinimum":0},"apiBaseUrl":{"type":"string","title":"GitHub Enterprise API base URL (optional)"}},"required":["owner","repository"]},"secretShape":{"kind":"apikey","field":"token","label":"GitHub token (optional; uses your GitHub sign-in)","required":false},"source":{"adapterId":"github-releases"},"normalize":{"kind":"native"},"actions":[],"completeness":"mirror","notes":"Lists releases through GitHub's REST API and stores one release-notes record per release. Uploaded assets are streamed to disk, checked against GitHub's size and sha256 digest when supplied, and published as file-backed connector attachments without buffering the entire asset in memory. Public repositories work anonymously. For github.com, a per-binding token wins, then Gezel's stored GitHub sign-in, GH_TOKEN/GITHUB_TOKEN, or a signed-in GitHub CLI. Users with push access receive draft releases from GitHub's list-releases endpoint. Optional filters bound the newest release count, prerelease/draft inclusion, asset filename globs, and per-asset size. GitHub-generated source archives are not release assets and are not downloaded.","availableVersions":["1.0.0"]}},{"manifest":{"schemaVersion":1,"kind":"connector-type","id":"github-wiki","name":"GitHub Wiki","description":"Mirror a repository's wiki pages into searchable project files. Public wikis work anonymously; add a token for private repositories.","tags":["github","wiki","documentation","native"],"maintainer":{"name":"Gezel"},"version":"1.0.0","releasedAt":"2026-07-13T00:00:00Z","driver":"native","configSchema":{"type":"object","properties":{"owner":{"type":"string","title":"Repository owner"},"repository":{"type":"string","title":"Repository name"},"host":{"type":"string","title":"GitHub Enterprise host (optional)"}},"required":["owner","repository"]},"secretShape":{"kind":"apikey","field":"token","label":"GitHub personal access token","required":false},"source":{"adapterId":"github-wiki"},"normalize":{"kind":"native"},"actions":[],"completeness":"mirror","notes":"Uses GitHub's separate .wiki.git repository as a read-only transport. Public wikis clone anonymously. An optional personal access token enables private repository wikis. Supported textual wiki markup is mirrored and scanned as untrusted external content; binary files and Git history are not exposed. Changed pages are stored as immutable revisions. Deletions are not currently pruned from the local corpus.","availableVersions":["1.0.0"]}},{"manifest":{"schemaVersion":1,"kind":"connector-type","id":"mail-gmail","name":"Gmail","description":"Mirror a Gmail mailbox into a normalized, searchable corpus of messages (OAuth).","tags":["email","native","oauth"],"maintainer":{"name":"Gezel"},"version":"1.0.0","releasedAt":"2026-07-08T00:00:00Z","driver":"native","configSchema":{"type":"object","properties":{"address":{"type":"string","title":"Email address"},"provider":{"type":"string","const":"gmail"},"syncFolders":{"type":"array","title":"Labels to sync","items":{"type":"string"}},"allowedRecipients":{"type":"array","title":"Allowed send recipients","items":{"type":"string"}},"allowedDomains":{"type":"array","title":"Allowed send domains","items":{"type":"string"}}},"required":["address"]},"secretShape":{"kind":"oauth2","authorizeUrl":"https://accounts.google.com/o/oauth2/v2/auth","tokenUrl":"https://oauth2.googleapis.com/token","scopes":"https://mail.google.com/","clientIdEnv":"GEZEL_GOOGLE_CLIENT_ID","clientSecretEnv":"GEZEL_GOOGLE_CLIENT_SECRET","authParams":{"access_type":"offline","prompt":"consent"}},"source":{"adapterId":"mail-gmail"},"normalize":{"kind":"native"},"actions":[{"name":"send","consentScope":"recipient-allowlist"}],"completeness":"mirror","availableVersions":["1.0.0"]}},{"manifest":{"schemaVersion":1,"kind":"connector-type","id":"calendar-google","name":"Google Calendar","description":"Mirror Google Calendar events into a normalized, searchable corpus (OAuth). Pairs with Gmail for cross-corpus reports.","tags":["calendar","native","oauth"],"maintainer":{"name":"Gezel"},"version":"1.0.0","releasedAt":"2026-07-08T00:00:00Z","driver":"native","configSchema":{"type":"object","properties":{"calendars":{"type":"array","title":"Calendar IDs (blank = all)","items":{"type":"string"}}}},"secretShape":{"kind":"oauth2","authorizeUrl":"https://accounts.google.com/o/oauth2/v2/auth","tokenUrl":"https://oauth2.googleapis.com/token","scopes":"https://www.googleapis.com/auth/calendar.readonly","clientIdEnv":"GEZEL_GOOGLE_CLIENT_ID","clientSecretEnv":"GEZEL_GOOGLE_CLIENT_SECRET","authParams":{"access_type":"offline","prompt":"consent"}},"source":{"adapterId":"calendar-google"},"normalize":{"kind":"native"},"actions":[],"completeness":"mirror","availableVersions":["1.0.0"]}},{"manifest":{"schemaVersion":1,"kind":"connector-type","id":"instagram-media","name":"Instagram Media","description":"Keep a searchable copy of your Instagram presence: your posts, reels, and carousels with captions, links, like and comment counts, plus a daily follower count. Works with Instagram professional (business or creator) accounts through Meta's own sign-in. Your crew can draft posts for you, but Gezel never publishes to Instagram itself.","tags":["instagram","social","media","posts","native"],"maintainer":{"name":"Gezel"},"version":"1.0.0","releasedAt":"2026-08-12T00:00:00Z","driver":"native","configSchema":{"type":"object","properties":{"downloadMedia":{"type":"boolean","title":"Download media files (reserved — this version stores captions and links only)","default":false},"maxMedia":{"type":"integer","title":"Most recent posts to keep synced","default":200,"minimum":1,"maximum":10000},"metricsWindowDays":{"type":"integer","title":"Days of recent posts to keep like/comment counts fresh for","default":30,"minimum":1,"maximum":365},"metricsRefreshHours":{"type":"number","title":"Hours between like/comment-count refreshes","default":12,"minimum":0.25,"maximum":720},"statsIntervalHours":{"type":"number","title":"Hours between account-stats snapshots","default":6,"minimum":0.25,"maximum":720}}},"secretShape":{"kind":"oauth2","authorizeUrl":"https://www.instagram.com/oauth/authorize","tokenUrl":"https://api.instagram.com/oauth/access_token","scopes":"instagram_business_basic","clientIdEnv":"GEZEL_INSTAGRAM_CLIENT_ID","clientSecretEnv":"GEZEL_INSTAGRAM_CLIENT_SECRET","longLivedExchange":{"exchangeUrl":"https://graph.instagram.com/access_token","refreshUrl":"https://graph.instagram.com/refresh_access_token"}},"source":{"adapterId":"instagram-media"},"normalize":{"kind":"native"},"actions":[{"name":"publish","consentScope":"manual-export"}],"completeness":"window","notes":"Works only with Instagram professional accounts (business or creator) via the Instagram API with Instagram Login — personal accounts cannot link. Requires your own Meta developer app: set GEZEL_INSTAGRAM_CLIENT_ID and GEZEL_INSTAGRAM_CLIENT_SECRET, and note Meta requires app review before accounts other than the app's own testers can link in production. Meta issues no refresh token: linking trades the sign-in for a long-lived (~60 day) token that Gezel refreshes in place whenever a sync runs with under 7 days remaining; if the account sits unsynced past expiry, re-link it. Syncs the account's media (posts, reels, carousels) as caption plus permalink — media files are not downloaded in this version — with like and comment counts on posts from the last metricsWindowDays re-observed roughly every metricsRefreshHours, plus a once-per-day account-stats record (followers, following, total posts). The first sync captures up to maxMedia recent posts; older history is not backfilled. Publishing is manual-export only: gezels can draft posts for review, but Gezel never publishes to Instagram on your behalf.","availableVersions":["1.0.0"]}},{"manifest":{"schemaVersion":1,"kind":"connector-type","id":"linear-issues","name":"Linear Issues","description":"Mirror recent Linear issues into a searchable corpus via Linear's MCP server. A rolling window of recent activity, not full history.","tags":["issues","mcp"],"maintainer":{"name":"Gezel"},"version":"1.0.0","releasedAt":"2026-07-08T00:00:00Z","driver":"mcp","configSchema":{"type":"object","properties":{}},"secretShape":{"kind":"apikey","field":"Authorization"},"source":{"server":{"kind":"http","transport":"streamable-http","url":"https://mcp.linear.app/mcp","headersFromSecret":{"Authorization":"Bearer $secret"}},"list":{"tool":"list_issues","cursorArg":"updatedAfter","itemsPath":"$.issues","idPath":"$.id","tsPath":"$.updatedAt"},"fetch":{"tool":"get_issue","idArg":"id"}},"normalize":{"kind":"mapping","map":{"id":"$.id","title":"$.title","group":"$.team.name","timestamp":"$.updatedAt","body":"$.description","frontmatter":{"identifier":"$.identifier","state":"$.state.name","assignee":"$.assignee.name","url":"$.url"}}},"actions":[],"completeness":"window","availableVersions":["1.0.0"]}},{"manifest":{"schemaVersion":1,"kind":"connector-type","id":"mail-microsoft365","name":"Microsoft 365 Mail","description":"Mirror a Microsoft 365 (work/school) mailbox into a normalized, searchable corpus of messages (OAuth).","tags":["email","native","oauth"],"maintainer":{"name":"Gezel"},"version":"1.0.0","releasedAt":"2026-08-09T00:00:00Z","driver":"native","configSchema":{"type":"object","properties":{"address":{"type":"string","title":"Email address"},"provider":{"type":"string","const":"microsoft365"},"tenant":{"type":"string","const":"organizations"},"syncFolders":{"type":"array","title":"Folders to sync","items":{"type":"string"}},"allowedRecipients":{"type":"array","title":"Allowed send recipients","items":{"type":"string"}},"allowedDomains":{"type":"array","title":"Allowed send domains","items":{"type":"string"}}},"required":["address"]},"secretShape":{"kind":"oauth2","authorizeUrl":"https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize","tokenUrl":"https://login.microsoftonline.com/organizations/oauth2/v2.0/token","scopes":"offline_access https://graph.microsoft.com/Mail.Read https://graph.microsoft.com/Mail.Send","clientIdEnv":"GEZEL_MICROSOFT_CLIENT_ID"},"source":{"adapterId":"mail-microsoft365"},"normalize":{"kind":"native"},"actions":[{"name":"send","consentScope":"recipient-allowlist"}],"completeness":"mirror","notes":"Work/school accounts in the 'organizations' Azure AD tenant. Sending is deny-by-default: only recipients on the binding's allowed recipients/domains lists can be sent to, and night shift defers sends to the outbox for daytime approval.","availableVersions":["1.0.0"]}},{"manifest":{"schemaVersion":1,"kind":"connector-type","id":"mail-outlook","name":"Outlook.com Mail","description":"Mirror a personal Outlook.com mailbox into a normalized, searchable corpus of messages (OAuth).","tags":["email","native","oauth"],"maintainer":{"name":"Gezel"},"version":"1.0.0","releasedAt":"2026-08-09T00:00:00Z","driver":"native","configSchema":{"type":"object","properties":{"address":{"type":"string","title":"Email address"},"provider":{"type":"string","const":"outlook"},"tenant":{"type":"string","const":"consumers"},"syncFolders":{"type":"array","title":"Folders to sync","items":{"type":"string"}},"allowedRecipients":{"type":"array","title":"Allowed send recipients","items":{"type":"string"}},"allowedDomains":{"type":"array","title":"Allowed send domains","items":{"type":"string"}}},"required":["address"]},"secretShape":{"kind":"oauth2","authorizeUrl":"https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize","tokenUrl":"https://login.microsoftonline.com/consumers/oauth2/v2.0/token","scopes":"offline_access https://graph.microsoft.com/Mail.Read https://graph.microsoft.com/Mail.Send","clientIdEnv":"GEZEL_MICROSOFT_CLIENT_ID"},"source":{"adapterId":"mail-outlook"},"normalize":{"kind":"native"},"actions":[{"name":"send","consentScope":"recipient-allowlist"}],"completeness":"mirror","notes":"Personal Outlook.com accounts in the 'consumers' tenant. Sending is deny-by-default: only recipients on the binding's allowed recipients/domains lists can be sent to, and night shift defers sends to the outbox for daytime approval.","availableVersions":["1.0.0"]}},{"manifest":{"schemaVersion":1,"kind":"connector-type","id":"x-posts","name":"X Posts","description":"Keep a searchable copy of your X (Twitter) presence: your posts with their like, repost, and view counts, optional mentions, and a daily follower count. Links through X's own sign-in. X charges for API reads, so syncs are spaced out by default. Your crew can draft posts for you, but Gezel never posts to X itself — you copy a draft and publish it yourself.","tags":["x","twitter","social","posts","native"],"maintainer":{"name":"Gezel"},"version":"1.0.0","releasedAt":"2026-08-12T00:00:00Z","driver":"native","configSchema":{"type":"object","properties":{"syncMentions":{"type":"boolean","title":"Sync mentions of the account (costs extra paid reads)","default":false},"syncIntervalHours":{"type":"number","title":"Hours between post syncs (X reads are paid — keep this generous)","default":4,"minimum":0.25,"maximum":720},"metricsWindowDays":{"type":"integer","title":"Days of recent posts to keep engagement counts fresh for","default":7,"minimum":1,"maximum":365},"metricsRefreshHours":{"type":"number","title":"Hours between engagement-count refreshes","default":24,"minimum":0.25,"maximum":720},"statsIntervalHours":{"type":"number","title":"Hours between account-stats snapshots","default":6,"minimum":0.25,"maximum":720}}},"secretShape":{"kind":"oauth2","authorizeUrl":"https://x.com/i/oauth2/authorize","tokenUrl":"https://api.x.com/2/oauth2/token","scopes":"tweet.read users.read offline.access","clientIdEnv":"GEZEL_X_CLIENT_ID","clientSecretEnv":"GEZEL_X_CLIENT_SECRET"},"source":{"adapterId":"x-posts"},"normalize":{"kind":"native"},"actions":[{"name":"publish","consentScope":"manual-export"}],"completeness":"window","notes":"Syncs three things from the linked account: the account's own posts (text with links expanded, language, reply threading, plus like/repost/reply/quote/bookmark/view counts), mentions of the account when enabled, and a once-per-day account-stats record (followers, following, total posts). Be blunt about cost: X charges for API reads on every tier, and the free tier allows roughly 100 reads per month — that fits only the default generous intervals (posts every 4 hours, stats every 6, engagement refresh daily); for real day-to-day use you need the paid Basic tier or higher. Every read scope is interval-gated (syncIntervalHours for posts and mentions, statsIntervalHours for stats) so background syncing can never burn reads faster than configured, and mentions are OFF by default to conserve reads. Engagement counts on posts from the last metricsWindowDays are re-observed roughly every metricsRefreshHours as one extra page per refresh, updating existing files in place. The first sync captures up to 500 recent posts; older history is not backfilled. Publishing is manual-export only in this version: gezels can draft posts for review, but Gezel never posts to X on your behalf — you copy a draft and publish it yourself. Requires your own X developer app: set GEZEL_X_CLIENT_ID (and GEZEL_X_CLIENT_SECRET for confidential apps) to enable linking.","availableVersions":["1.0.0"]}}]}
1
+ {"schemaVersion":1,"kind":"connector-type","count":15,"entries":[{"manifest":{"schemaVersion":1,"kind":"connector-type","id":"airtable-records","name":"Airtable Records","description":"Mirror an Airtable table into a searchable corpus — one file per record — via the Airtable API, hosted off-platform through a vendored Prismatic component. Captures the table's current rows each sync.","tags":["airtable","records","spectral"],"maintainer":{"name":"Gezel"},"version":"1.0.0","releasedAt":"2026-07-08T00:00:00Z","driver":"spectral","configSchema":{"type":"object","properties":{"baseId":{"type":"string","title":"Base ID"},"tableName":{"type":"string","title":"Table name"},"view":{"type":"string","title":"View (optional)"},"filterByFormula":{"type":"string","title":"Filter by formula (optional)"}},"required":["baseId","tableName"]},"secretShape":{"kind":"apikey","field":"apiKey"},"source":{"component":"airtable","action":"listRecords","connectionKey":"personalAccessToken","connectionInput":"airtableConnection","inputs":{"baseId":"$config.baseId","tableName":"$config.tableName","view":"$config.view","filterByFormula":"$config.filterByFormula"},"itemsPath":"$.data"},"normalize":{"kind":"mapping","map":{"id":"$.id","title":"$.fields.Name","timestamp":"$.createdTime","body":"$.fields","frontmatter":{"recordId":"$.id","createdTime":"$.createdTime"}}},"actions":[],"completeness":"mirror","notes":"Personal Access Token connection. Each sync re-fetches every row in the table (no incremental cursor); the writer is content-hash idempotent, so re-syncs are cheap and only changed rows rewrite. Deleted Airtable rows are not pruned from the corpus. `title` maps to the `Name` field (Airtable's usual primary column) and falls back to the record id when absent.","availableVersions":["1.0.0"]}},{"manifest":{"schemaVersion":1,"kind":"connector-type","id":"bluesky-posts","name":"Bluesky Posts","description":"Keep a searchable copy of your Bluesky presence: your posts with their likes and reposts, mentions and replies from others, and a daily follower count. Sign in with an app password from Bluesky's settings — never your real password. Your crew can also draft posts for you, and with your permission publish them; every publish still asks for your consent.","tags":["bluesky","social","posts","native"],"maintainer":{"name":"Gezel"},"version":"1.0.0","releasedAt":"2026-08-12T00:00:00Z","driver":"native","configSchema":{"type":"object","properties":{"handle":{"type":"string","title":"Bluesky handle (e.g. yourname.bsky.social)"},"service":{"type":"string","title":"Service URL","default":"https://bsky.social"},"syncMentions":{"type":"boolean","title":"Sync mentions, replies, and quotes","default":true},"allowPublish":{"type":"boolean","title":"Allow this project's gezels to queue posts for publishing (each publish still needs your consent)","default":false},"metricsWindowDays":{"type":"integer","title":"Days of recent posts to keep engagement counts fresh for","default":30,"minimum":1,"maximum":365},"metricsRefreshHours":{"type":"number","title":"Hours between engagement-count refreshes","default":6},"statsIntervalHours":{"type":"number","title":"Hours between account-stats snapshots","default":6},"metricsHistory":{"type":"boolean","title":"Record daily engagement history records","description":"One per post per day while a post stays inside the metrics window","default":true}},"required":["handle"]},"secretShape":{"kind":"apikey","field":"appPassword","label":"Bluesky app password","required":true},"source":{"adapterId":"bluesky-posts"},"normalize":{"kind":"native"},"actions":[{"name":"publish","consentScope":"social-publish"}],"completeness":"window","notes":"Syncs three things from the signed-in account: the account's own posts (text, language, reply threading, link and image-description context, plus like/repost/reply/quote counts), mentions of the account (mention, reply, and quote notifications), and a once-per-day account-stats record (followers, following, total posts) refreshed at the configured interval. Engagement counts on posts from the last metricsWindowDays are re-observed roughly every metricsRefreshHours, updating the existing files in place; unless metricsHistory is turned off, each observation also writes a per-post per-day engagement history record under the posts corpus's metrics-history/ bucket, so engagement curves stay reconstructable after counts move on. The first sync captures up to 500 recent posts; older history is not backfilled. Reposts are skipped in this version, and synced images are represented by their alt text rather than downloaded. Publishing is off by default: a gezel can only draft posts until allowPublish is enabled on the binding, and every publish is individually consented through the daemon (night shift defers publishes to the outbox for daytime approval). Published posts can embed up to 4 images (1,000,000 bytes each at most; png/jpg/jpeg/webp/gif), read from project-relative paths resolved against the project's artifacts directory first, then its workspace.","availableVersions":["1.0.0"]}},{"manifest":{"schemaVersion":1,"kind":"connector-type","id":"mail-imap","name":"Email (IMAP)","description":"Mirror an IMAP mailbox into a normalized, searchable corpus of messages.","tags":["email","native"],"maintainer":{"name":"Gezel"},"version":"1.0.0","releasedAt":"2026-07-08T00:00:00Z","driver":"native","configSchema":{"type":"object","properties":{"address":{"type":"string","title":"Email address"},"provider":{"type":"string","const":"imap"},"syncFolders":{"type":"array","title":"Folders to sync","items":{"type":"string"}},"allowedRecipients":{"type":"array","title":"Allowed send recipients","items":{"type":"string"}},"allowedDomains":{"type":"array","title":"Allowed send domains","items":{"type":"string"}}},"required":["address"]},"secretShape":{"kind":"imap"},"source":{"adapterId":"mail-imap"},"normalize":{"kind":"native"},"actions":[{"name":"send","consentScope":"recipient-allowlist"}],"completeness":"mirror","availableVersions":["1.0.0"]}},{"manifest":{"schemaVersion":1,"kind":"connector-type","id":"github-issues","name":"GitHub Issues","description":"Mirror recent issues from one GitHub repository into searchable project files. Public repositories work without sign-in; add a token for private repositories or higher rate limits.","tags":["github","issues","script"],"maintainer":{"name":"Gezel"},"version":"1.0.1","releasedAt":"2026-08-12T00:00:00Z","driver":"script","configSchema":{"type":"object","properties":{"owner":{"type":"string","title":"Repository owner"},"repository":{"type":"string","title":"Repository name"},"apiBaseUrl":{"type":"string","title":"API base URL (GitHub Enterprise only)"}},"required":["owner","repository"]},"secretShape":{"kind":"apikey","field":"token","label":"GitHub personal access token","required":false},"source":{"inlineFetch":"import { defineScript, gezel } from '@bendyline/gezel-sdk';\n\nexport const meta = defineScript({\n name: 'github-issues-fetch',\n description: 'Fetches a bounded, incremental window of GitHub repository issues.',\n inputs: {\n cursor: { type: 'json', description: 'Last successfully synced GitHub updated_at value.' },\n config: { type: 'json', description: 'Repository owner, name, and optional API base URL.', required: true },\n },\n outputs: {\n records: { type: 'array', description: 'GitHub issues ready for connector normalization.', itemType: 'object' },\n cursor: { type: 'string', description: 'Newest updated_at value processed.', nullable: true },\n rateLimited: { type: 'boolean', description: 'True when GitHub throttled the pass; the engine backs off and retries from this cursor.', nullable: true },\n partial: { type: 'boolean', description: 'True when more pages remained after the per-pass page budget.', nullable: true },\n },\n requires: ['network', 'credential:$credential'],\n} as const);\n\ntype Config = { owner?: unknown; repository?: unknown; apiBaseUrl?: unknown };\ntype Issue = Record<string, unknown> & { updated_at?: unknown; pull_request?: unknown };\nconst input = gezel.input as { cursor?: unknown; config?: Config };\nconst config = input.config ?? {};\nconst owner = typeof config.owner === 'string' ? config.owner.trim() : '';\nconst repository = typeof config.repository === 'string' ? config.repository.trim() : '';\nif (!owner || !repository) throw new Error('GitHub owner and repository are required.');\n\nconst apiBaseUrl =\n typeof config.apiBaseUrl === 'string' && config.apiBaseUrl.trim()\n ? config.apiBaseUrl.trim().replace(/\\/+$/, '')\n : 'https://api.github.com';\nlet apiOrigin: string;\ntry {\n const parsed = new URL(apiBaseUrl);\n if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') throw new Error('bad protocol');\n apiOrigin = parsed.origin;\n} catch {\n throw new Error('GitHub API base URL must be an absolute HTTP or HTTPS URL.');\n}\n\nconst params = new URLSearchParams({\n state: 'all',\n sort: 'updated',\n direction: typeof input.cursor === 'string' ? 'asc' : 'desc',\n per_page: '100',\n});\nif (typeof input.cursor === 'string' && input.cursor) params.set('since', input.cursor);\nlet nextUrl =\n apiBaseUrl + '/repos/' + encodeURIComponent(owner) + '/' + encodeURIComponent(repository) + '/issues?' + params;\nlet nextCursor = typeof input.cursor === 'string' ? input.cursor : null;\nconst records: Issue[] = [];\nlet rateLimited = false;\n\nfunction nextPage(link: string | undefined): string | null {\n if (!link) return null;\n for (const part of link.split(',')) {\n const match = part.match(/<([^>]+)>;\\s*rel=\"next\"/);\n if (match?.[1]) return match[1];\n }\n return null;\n}\n\nfunction isThrottle(status: number, headers: Record<string, string | undefined>): boolean {\n if (status === 429) return true;\n return status === 403 && (headers['x-ratelimit-remaining'] === '0' || Boolean(headers['retry-after']));\n}\n\nfor (let page = 0; page < 5 && nextUrl; page++) {\n const target = new URL(nextUrl);\n if (target.origin !== apiOrigin) throw new Error('GitHub pagination changed origin; refusing to forward the token.');\n const headers = {\n Accept: 'application/vnd.github+json',\n 'X-GitHub-Api-Version': '2022-11-28',\n 'User-Agent': 'Gezel-GitHub-Issues-Connector',\n };\n let response;\n try {\n response = await gezel.http.authed(target.toString(), {\n credential: '$credential',\n headers,\n });\n } catch (error) {\n if ((error as { code?: string }).code !== 'CREDENTIAL_MISSING') throw error;\n response = await gezel.http.request(target.toString(), { headers });\n }\n if (isThrottle(response.status, response.headers)) {\n // Emit what this pass already collected with the cursor those records\n // reached, and flag the throttle instead of throwing: a throw voids the\n // batch and re-fetches the same window next tick, which is exactly how\n // rate-limited APIs get hammered. The engine persists the cursor only\n // after a clean write and applies its jittered backoff to the binding.\n rateLimited = true;\n break;\n }\n if (!response.ok) {\n throw new Error('GitHub issues request failed (' + response.status + '): ' + response.body.slice(0, 240));\n }\n const pageItems = JSON.parse(response.body) as unknown;\n if (!Array.isArray(pageItems)) throw new Error('GitHub returned a non-array issues response.');\n for (const raw of pageItems) {\n if (!raw || typeof raw !== 'object') continue;\n const issue = raw as Issue;\n const updatedAt = typeof issue.updated_at === 'string' ? issue.updated_at : '';\n if (updatedAt && (!nextCursor || updatedAt > nextCursor)) nextCursor = updatedAt;\n if (issue.pull_request) continue;\n const labels = Array.isArray(issue.labels)\n ? issue.labels\n .map((label) =>\n typeof label === 'string'\n ? label\n : label && typeof label === 'object'\n ? String((label as Record<string, unknown>).name ?? '')\n : '',\n )\n .filter(Boolean)\n : [];\n const assignees = Array.isArray(issue.assignees)\n ? issue.assignees\n .map((person) =>\n person && typeof person === 'object'\n ? String((person as Record<string, unknown>).login ?? '')\n : '',\n )\n .filter(Boolean)\n : [];\n records.push({\n ...issue,\n gezel_repository: owner + '/' + repository,\n gezel_labels: labels,\n gezel_assignees: assignees,\n });\n }\n const candidate = nextPage(response.headers.link);\n nextUrl = candidate && new URL(candidate).origin === apiOrigin ? candidate : '';\n}\n\nconst cursorOut = records.length > 0 ? nextCursor : typeof input.cursor === 'string' ? input.cursor : null;\ngezel.output({\n records,\n cursor: rateLimited ? cursorOut : nextCursor,\n ...(rateLimited ? { rateLimited: true } : {}),\n ...(nextUrl && !rateLimited ? { partial: true } : {}),\n});\n","idPath":"$.id"},"normalize":{"kind":"mapping","map":{"id":"$.id","title":"$.title","group":"$.gezel_repository","timestamp":"$.updated_at","body":"$.body","frontmatter":{"number":"$.number","state":"$.state","stateReason":"$.state_reason","author":"$.user.login","assignees":"$.gezel_assignees","labels":"$.gezel_labels","milestone":"$.milestone.title","comments":"$.comments","url":"$.html_url","createdAt":"$.created_at","closedAt":"$.closed_at","locked":"$.locked"}}},"actions":[],"completeness":"window","notes":"Uses GitHub's REST list-repository-issues endpoint anonymously for public repositories, or with an optional personal access token for private repositories and higher rate limits. Pull requests are excluded because GitHub's Issues API returns both. The first sync captures up to 500 recently updated issues; later syncs resume from updated_at in oldest-first order so busy repositories catch up over multiple passes. Issue comments are represented by their count but comment bodies are not mirrored in this version. Since 1.0.1 the fetch reports GitHub throttling (HTTP 429, or 403 with an exhausted rate-limit budget) as a rateLimited batch instead of an error, so the sync engine backs off and resumes from the last clean cursor, and declares its credential destination origins for the daemon-side allowlist.","availableVersions":["1.0.1","1.0.0"]}},{"manifest":{"schemaVersion":1,"kind":"connector-type","id":"github-pulls","name":"GitHub Pull Requests","description":"Mirror a project's open pull requests into local files: a summary, one file per changed file with its patch, and the complete unified diff. Lets a gezel review a PR by reading the workspace instead of calling the GitHub API mid-turn.","tags":["github","pull-requests","code-review","diff","native"],"maintainer":{"name":"Gezel"},"version":"1.0.0","releasedAt":"2026-08-10T00:00:00Z","driver":"native","configSchema":{"type":"object","properties":{"maxPulls":{"type":"integer","title":"Open pull requests to mirror","minimum":1,"maximum":200,"default":20},"includeComments":{"type":"boolean","title":"Include review and issue comments","default":true}}},"secretShape":{"kind":"apikey","field":"token","label":"GitHub token (optional; uses your GitHub sign-in)","required":false},"source":{"adapterId":"github-pulls"},"normalize":{"kind":"native"},"actions":[],"completeness":"window","notes":"Takes the repository from the project's own GitHub link rather than binding config, so the coordinates cannot drift from the project. The corpus lives in the project's managed artifacts tree, so mirrored data never dirties the user's checkout. Each open pull request becomes one scope (`pr-<number>`) holding an overview record — title, author, branches, counts, description, and comment digest — plus one record per changed file under `files/` carrying that file's patch, and the complete unified diff as an attachment. Patches and the diff are stored untruncated because a gezel opens these as files. Comment and description text is untrusted and passes through the standard content scan and quarantine. Completeness is `window`: only the newest open pull requests are mirrored, so absence from the corpus does not mean absence upstream. A task launch can target one pull request directly, which syncs just that scope.","availableVersions":["1.0.0"]}},{"manifest":{"schemaVersion":1,"kind":"connector-type","id":"github-releases","name":"GitHub Releases","description":"Mirror GitHub release notes and uploaded release assets into local project files. Public repositories work anonymously; Gezel's GitHub sign-in or an optional token also enables private repositories and draft releases.","tags":["github","releases","artifacts","binaries","native"],"maintainer":{"name":"Gezel"},"version":"1.0.0","releasedAt":"2026-08-09T00:00:00Z","driver":"native","configSchema":{"type":"object","properties":{"owner":{"type":"string","title":"Repository owner"},"repository":{"type":"string","title":"Repository name"},"maxReleases":{"type":"integer","title":"Latest releases (blank = all)","minimum":1,"maximum":10000},"includeDrafts":{"type":"boolean","title":"Include draft releases when signed in","default":true},"includePrereleases":{"type":"boolean","title":"Include prereleases","default":true},"downloadAssets":{"type":"boolean","title":"Download uploaded release assets","default":true},"assetNamePattern":{"type":"string","title":"Asset filename globs (optional, comma-separated)"},"maxAssetSizeMb":{"type":"number","title":"Maximum asset size in MB (blank = unlimited)","exclusiveMinimum":0},"apiBaseUrl":{"type":"string","title":"GitHub Enterprise API base URL (optional)"}},"required":["owner","repository"]},"secretShape":{"kind":"apikey","field":"token","label":"GitHub token (optional; uses your GitHub sign-in)","required":false},"source":{"adapterId":"github-releases"},"normalize":{"kind":"native"},"actions":[],"completeness":"mirror","notes":"Lists releases through GitHub's REST API and stores one release-notes record per release. Uploaded assets are streamed to disk, checked against GitHub's size and sha256 digest when supplied, and published as file-backed connector attachments without buffering the entire asset in memory. Public repositories work anonymously. For github.com, a per-binding token wins, then Gezel's stored GitHub sign-in, GH_TOKEN/GITHUB_TOKEN, or a signed-in GitHub CLI. Users with push access receive draft releases from GitHub's list-releases endpoint. Optional filters bound the newest release count, prerelease/draft inclusion, asset filename globs, and per-asset size. GitHub-generated source archives are not release assets and are not downloaded.","availableVersions":["1.0.0"]}},{"manifest":{"schemaVersion":1,"kind":"connector-type","id":"github-wiki","name":"GitHub Wiki","description":"Mirror a repository's wiki pages into searchable project files. Public wikis work anonymously; add a token for private repositories.","tags":["github","wiki","documentation","native"],"maintainer":{"name":"Gezel"},"version":"1.0.0","releasedAt":"2026-07-13T00:00:00Z","driver":"native","configSchema":{"type":"object","properties":{"owner":{"type":"string","title":"Repository owner"},"repository":{"type":"string","title":"Repository name"},"host":{"type":"string","title":"GitHub Enterprise host (optional)"}},"required":["owner","repository"]},"secretShape":{"kind":"apikey","field":"token","label":"GitHub personal access token","required":false},"source":{"adapterId":"github-wiki"},"normalize":{"kind":"native"},"actions":[],"completeness":"mirror","notes":"Uses GitHub's separate .wiki.git repository as a read-only transport. Public wikis clone anonymously. An optional personal access token enables private repository wikis. Supported textual wiki markup is mirrored and scanned as untrusted external content; binary files and Git history are not exposed. Changed pages are stored as immutable revisions. Deletions are not currently pruned from the local corpus.","availableVersions":["1.0.0"]}},{"manifest":{"schemaVersion":1,"kind":"connector-type","id":"mail-gmail","name":"Gmail","description":"Mirror a Gmail mailbox into a normalized, searchable corpus of messages (OAuth).","tags":["email","native","oauth"],"maintainer":{"name":"Gezel"},"version":"1.0.0","releasedAt":"2026-07-08T00:00:00Z","driver":"native","configSchema":{"type":"object","properties":{"address":{"type":"string","title":"Email address"},"provider":{"type":"string","const":"gmail"},"syncFolders":{"type":"array","title":"Labels to sync","items":{"type":"string"}},"allowedRecipients":{"type":"array","title":"Allowed send recipients","items":{"type":"string"}},"allowedDomains":{"type":"array","title":"Allowed send domains","items":{"type":"string"}}},"required":["address"]},"secretShape":{"kind":"oauth2","authorizeUrl":"https://accounts.google.com/o/oauth2/v2/auth","tokenUrl":"https://oauth2.googleapis.com/token","scopes":"https://mail.google.com/","clientIdEnv":"GEZEL_GOOGLE_CLIENT_ID","clientSecretEnv":"GEZEL_GOOGLE_CLIENT_SECRET","authParams":{"access_type":"offline","prompt":"consent"},"clientSetup":{"providerLabel":"Google","docsUrl":"https://console.cloud.google.com/apis/credentials","secretRequired":true,"appTypeNote":"Create an OAuth client ID of type Desktop app in Google Cloud Console. Desktop clients are issued a client secret Google does not treat as confidential — enter both here. Any loopback port works for the redirect.","steps":["Sign in at console.cloud.google.com and create (or pick) a project.","Under APIs & Services, open Library and enable the Gmail API.","Under APIs & Services, open OAuth consent screen: configure the app as External and add yourself as a test user.","Set the publishing status to In production — apps left in Testing get their sign-ins expired every 7 days. An unverified-app warning on your own app is safe to click through.","Under APIs & Services, open Credentials and choose Create credentials, then OAuth client ID, application type 'Desktop app'.","Copy the Client ID and Client Secret from the created client and paste them below. Desktop clients accept any 127.0.0.1 port, so there is no redirect URI to register."]}},"source":{"adapterId":"mail-gmail"},"normalize":{"kind":"native"},"actions":[{"name":"send","consentScope":"recipient-allowlist"}],"completeness":"mirror","availableVersions":["1.0.0"]}},{"manifest":{"schemaVersion":1,"kind":"connector-type","id":"calendar-google","name":"Google Calendar","description":"Mirror Google Calendar events into a normalized, searchable corpus (OAuth). Pairs with Gmail for cross-corpus reports.","tags":["calendar","native","oauth"],"maintainer":{"name":"Gezel"},"version":"1.0.0","releasedAt":"2026-07-08T00:00:00Z","driver":"native","configSchema":{"type":"object","properties":{"calendars":{"type":"array","title":"Calendar IDs (blank = all)","items":{"type":"string"}}}},"secretShape":{"kind":"oauth2","authorizeUrl":"https://accounts.google.com/o/oauth2/v2/auth","tokenUrl":"https://oauth2.googleapis.com/token","scopes":"https://www.googleapis.com/auth/calendar.readonly","clientIdEnv":"GEZEL_GOOGLE_CLIENT_ID","clientSecretEnv":"GEZEL_GOOGLE_CLIENT_SECRET","authParams":{"access_type":"offline","prompt":"consent"},"clientSetup":{"providerLabel":"Google","docsUrl":"https://console.cloud.google.com/apis/credentials","secretRequired":true,"appTypeNote":"Create an OAuth client ID of type Desktop app in Google Cloud Console. Desktop clients are issued a client secret Google does not treat as confidential — enter both here. Any loopback port works for the redirect.","steps":["Sign in at console.cloud.google.com and create (or pick) a project.","Under APIs & Services, open Library and enable the Google Calendar API.","Under APIs & Services, open OAuth consent screen: configure the app as External and add yourself as a test user.","Set the publishing status to In production — apps left in Testing get their sign-ins expired every 7 days. An unverified-app warning on your own app is safe to click through.","Under APIs & Services, open Credentials and choose Create credentials, then OAuth client ID, application type 'Desktop app'.","Copy the Client ID and Client Secret from the created client and paste them below. Desktop clients accept any 127.0.0.1 port, so there is no redirect URI to register."]}},"source":{"adapterId":"calendar-google"},"normalize":{"kind":"native"},"actions":[],"completeness":"mirror","availableVersions":["1.0.0"]}},{"manifest":{"schemaVersion":1,"kind":"connector-type","id":"instagram-media","name":"Instagram Media","description":"Keep a searchable copy of your Instagram presence: your posts, reels, and carousels with captions, links, like and comment counts and a daily engagement history, plus a daily follower count. Works with Instagram professional (business or creator) accounts through Meta's own sign-in. Your crew can draft posts for review and — once you allow publishing on the connector — publish images with your per-post consent. One honest catch: Instagram's API only accepts images already hosted at a public https URL, so Gezel cannot publish local files directly.","tags":["instagram","social","media","posts","native"],"maintainer":{"name":"Gezel"},"version":"1.0.0","releasedAt":"2026-08-12T00:00:00Z","driver":"native","configSchema":{"type":"object","properties":{"downloadMedia":{"type":"boolean","title":"Download media files (reserved — this version stores captions and links only)","default":false},"allowPublish":{"type":"boolean","title":"Allow this project's gezels to queue posts for publishing (each publish still needs your consent)","default":false},"maxMedia":{"type":"integer","title":"Most recent posts to keep synced","default":200,"minimum":1,"maximum":10000},"metricsWindowDays":{"type":"integer","title":"Days of recent posts to keep like/comment counts fresh for","default":30,"minimum":1,"maximum":365},"metricsRefreshHours":{"type":"number","title":"Hours between like/comment-count refreshes","default":12,"minimum":0.25,"maximum":720},"metricsHistory":{"type":"boolean","title":"Record daily engagement history records","default":true},"statsIntervalHours":{"type":"number","title":"Hours between account-stats snapshots","default":6,"minimum":0.25,"maximum":720}}},"secretShape":{"kind":"oauth2","authorizeUrl":"https://www.instagram.com/oauth/authorize","tokenUrl":"https://api.instagram.com/oauth/access_token","scopes":"instagram_business_basic instagram_business_content_publish","clientIdEnv":"GEZEL_INSTAGRAM_CLIENT_ID","clientSecretEnv":"GEZEL_INSTAGRAM_CLIENT_SECRET","longLivedExchange":{"exchangeUrl":"https://graph.instagram.com/access_token","refreshUrl":"https://graph.instagram.com/refresh_access_token"},"clientSetup":{"providerLabel":"Instagram (Meta)","docsUrl":"https://developers.facebook.com/apps/","secretRequired":true,"appTypeNote":"Create a Meta app with Instagram API (Instagram Login) for a business or creator account. The app secret stays on this machine, in your OS keychain.","redirectPort":6242,"redirectNote":"Meta matches redirect URIs exactly and may require an https URI; if it refuses the http form, tell us — https loopback support is planned.","steps":["Sign in at developers.facebook.com, choose My Apps, then Create app (the Business type, or the default when not asked).","On the app dashboard, add the 'Instagram' product and choose 'API setup with Instagram Login'.","Your Instagram account must be Professional (business or creator) — switch it in the Instagram app under Settings if needed, then connect it as the tester account when prompted.","In the product's API setup, open 'Set up Instagram business login' and add the redirect URI shown below under OAuth redirect URIs, exactly.","Copy the Instagram App ID and Instagram App Secret from the same setup page (they also live under App settings, Basic).","Paste both below — the secret stays in your OS keychain. Your own account works while the Meta app stays in development mode."]}},"source":{"adapterId":"instagram-media"},"normalize":{"kind":"native"},"actions":[{"name":"publish","consentScope":"social-publish"}],"completeness":"window","notes":"Works only with Instagram professional accounts (business or creator) via the Instagram API with Instagram Login — personal accounts cannot link. Requires your own Meta developer app: set GEZEL_INSTAGRAM_CLIENT_ID and GEZEL_INSTAGRAM_CLIENT_SECRET, and note Meta requires app review before accounts other than the app's own testers can link in production. Meta issues no refresh token: linking trades the sign-in for a long-lived (~60 day) token that Gezel refreshes in place whenever a sync runs with under 7 days remaining; if the account sits unsynced past expiry, re-link it. Syncs the account's media (posts, reels, carousels) as caption plus permalink — media files are not downloaded in this version — with like and comment counts on posts from the last metricsWindowDays re-observed roughly every metricsRefreshHours (each observation also appends a per-day metrics-history record per post, the engagement curve over time, unless metricsHistory is turned off), plus a once-per-day account-stats record (followers, following, total posts). The first sync captures up to maxMedia recent posts; older history is not backfilled. Publishing works, with Meta's contract stated honestly: Instagram's publishing API has Meta's servers fetch the image from a public https URL, and Gezel is local-first and does not host your files — so publishing needs an image that is already hosted somewhere public (any CDN or static host will do), caption-only posts do not exist on Instagram, and drafts stay fully usable without any public URL. The caption travels as the draft's post text, so Gezel's draft flow requires one (Instagram itself allows empty captions). Publishing is off by default: a gezel can only draft until allowPublish is enabled on the binding, and every publish is individually consented through the daemon (night shift defers publishes to the outbox for daytime approval). Accounts linked before publishing existed must re-link from the connector's settings to grant the instagram_business_content_publish permission.","availableVersions":["1.0.0"]}},{"manifest":{"schemaVersion":1,"kind":"connector-type","id":"linear-issues","name":"Linear Issues","description":"Mirror recent Linear issues into a searchable corpus via Linear's MCP server. A rolling window of recent activity, not full history.","tags":["issues","mcp"],"maintainer":{"name":"Gezel"},"version":"1.0.0","releasedAt":"2026-07-08T00:00:00Z","driver":"mcp","configSchema":{"type":"object","properties":{}},"secretShape":{"kind":"apikey","field":"Authorization"},"source":{"server":{"kind":"http","transport":"streamable-http","url":"https://mcp.linear.app/mcp","headersFromSecret":{"Authorization":"Bearer $secret"}},"list":{"tool":"list_issues","cursorArg":"updatedAfter","itemsPath":"$.issues","idPath":"$.id","tsPath":"$.updatedAt"},"fetch":{"tool":"get_issue","idArg":"id"}},"normalize":{"kind":"mapping","map":{"id":"$.id","title":"$.title","group":"$.team.name","timestamp":"$.updatedAt","body":"$.description","frontmatter":{"identifier":"$.identifier","state":"$.state.name","assignee":"$.assignee.name","url":"$.url"}}},"actions":[],"completeness":"window","availableVersions":["1.0.0"]}},{"manifest":{"schemaVersion":1,"kind":"connector-type","id":"linkedin-posts","name":"LinkedIn Posts","description":"Publish to LinkedIn from Gezel: your crew drafts posts in your voice, and with your permission queues them — every publish still asks for your consent before anything goes out. LinkedIn does not let ordinary apps read your feed (that part is reserved for its partner program), so nothing syncs back: reception and analytics stay on LinkedIn, and this connection is honestly posting-only.","tags":["linkedin","social","posts","native"],"maintainer":{"name":"Gezel"},"version":"1.0.0","releasedAt":"2026-08-12T00:00:00Z","driver":"native","configSchema":{"type":"object","properties":{"allowPublish":{"type":"boolean","title":"Allow this project's gezels to queue posts for publishing (each publish still needs your consent)","default":false}}},"secretShape":{"kind":"oauth2","authorizeUrl":"https://www.linkedin.com/oauth/v2/authorization","tokenUrl":"https://www.linkedin.com/oauth/v2/accessToken","scopes":"openid profile w_member_social","clientIdEnv":"GEZEL_LINKEDIN_CLIENT_ID","clientSecretEnv":"GEZEL_LINKEDIN_CLIENT_SECRET","longLived":true,"clientSetup":{"providerLabel":"LinkedIn","docsUrl":"https://www.linkedin.com/developers/apps","secretRequired":true,"appTypeNote":"Create a LinkedIn developer app and request the 'Share on LinkedIn' and 'Sign in with LinkedIn using OpenID Connect' products. LinkedIn requires the client secret; it stays in your OS keychain.","redirectPort":6243,"redirectNote":"LinkedIn matches redirect URLs exactly. Register the URI shown here character-for-character. Tokens last about 60 days; reconnect when prompted.","steps":["Sign in at linkedin.com/developers/apps and create an app — it must be tied to a LinkedIn Page (create a free one if you have none).","On the Products tab, request 'Share on LinkedIn' and 'Sign in with LinkedIn using OpenID Connect'; both are approved instantly.","On the Auth tab, add the redirect URI shown below under 'Authorized redirect URLs for your app', exactly.","Copy the Client ID and Client Secret from the Auth tab.","Paste both below — the secret stays in your OS keychain."]}},"source":{"adapterId":"linkedin-posts"},"normalize":{"kind":"native"},"actions":[{"name":"publish","consentScope":"social-publish"}],"completeness":"window","notes":"Publishing-only, by LinkedIn's design: reading a member's posts requires LinkedIn's restricted Community Management partner program, which ordinary developer apps cannot join, so nothing syncs back from the feed — no post history, no engagement counts, no mentions. The corpus root holds only what Gezel itself did: the _meta.json marker plus a receipt for every published post under _actions/_sent/; reception and analytics stay on linkedin.com. Publishing works with the standard 'Share on LinkedIn' product: text posts up to 3000 characters, posted to the linked member's own feed with a permalink returned for the record. Publishing is off by default — a gezel can only draft until allowPublish is enabled on the binding, and every publish is individually consented through the daemon (night shift defers publishes to the outbox for daytime approval). Requires your own LinkedIn developer app: set GEZEL_LINKEDIN_CLIENT_ID and GEZEL_LINKEDIN_CLIENT_SECRET, or register the app under Settings — LinkedIn requires the client secret (it has no public-client PKCE support; Gezel still sends PKCE params, which LinkedIn ignores). LinkedIn issues no refresh token to ordinary apps, so the connection lasts about 60 days and then asks to be reconnected.","availableVersions":["1.0.0"]}},{"manifest":{"schemaVersion":1,"kind":"connector-type","id":"mail-microsoft365","name":"Microsoft 365 Mail","description":"Mirror a Microsoft 365 (work/school) mailbox into a normalized, searchable corpus of messages (OAuth).","tags":["email","native","oauth"],"maintainer":{"name":"Gezel"},"version":"1.0.0","releasedAt":"2026-08-09T00:00:00Z","driver":"native","configSchema":{"type":"object","properties":{"address":{"type":"string","title":"Email address"},"provider":{"type":"string","const":"microsoft365"},"tenant":{"type":"string","const":"organizations"},"syncFolders":{"type":"array","title":"Folders to sync","items":{"type":"string"}},"allowedRecipients":{"type":"array","title":"Allowed send recipients","items":{"type":"string"}},"allowedDomains":{"type":"array","title":"Allowed send domains","items":{"type":"string"}}},"required":["address"]},"secretShape":{"kind":"oauth2","authorizeUrl":"https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize","tokenUrl":"https://login.microsoftonline.com/organizations/oauth2/v2.0/token","scopes":"offline_access https://graph.microsoft.com/Mail.Read https://graph.microsoft.com/Mail.Send","clientIdEnv":"GEZEL_MICROSOFT_CLIENT_ID","clientSetup":{"providerLabel":"Microsoft","docsUrl":"https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade","secretRequired":false,"appTypeNote":"Register an app in Microsoft Entra (Azure portal) and add the Mobile and desktop applications platform — a public client needs no client secret. Any loopback port works for the redirect.","steps":["Sign in at portal.azure.com and open Microsoft Entra ID, then App registrations, then New registration.","For supported account types choose 'Accounts in any organizational directory and personal Microsoft accounts'.","Under Authentication, add the 'Mobile and desktop applications' platform and enable the loopback redirect — any 127.0.0.1 port works.","No client secret is needed: this registration is a public client.","Copy the Application (client) ID from the Overview page and paste it below."]}},"source":{"adapterId":"mail-microsoft365"},"normalize":{"kind":"native"},"actions":[{"name":"send","consentScope":"recipient-allowlist"}],"completeness":"mirror","notes":"Work/school accounts in the 'organizations' Azure AD tenant. Sending is deny-by-default: only recipients on the binding's allowed recipients/domains lists can be sent to, and night shift defers sends to the outbox for daytime approval.","availableVersions":["1.0.0"]}},{"manifest":{"schemaVersion":1,"kind":"connector-type","id":"mail-outlook","name":"Outlook.com Mail","description":"Mirror a personal Outlook.com mailbox into a normalized, searchable corpus of messages (OAuth).","tags":["email","native","oauth"],"maintainer":{"name":"Gezel"},"version":"1.0.0","releasedAt":"2026-08-09T00:00:00Z","driver":"native","configSchema":{"type":"object","properties":{"address":{"type":"string","title":"Email address"},"provider":{"type":"string","const":"outlook"},"tenant":{"type":"string","const":"consumers"},"syncFolders":{"type":"array","title":"Folders to sync","items":{"type":"string"}},"allowedRecipients":{"type":"array","title":"Allowed send recipients","items":{"type":"string"}},"allowedDomains":{"type":"array","title":"Allowed send domains","items":{"type":"string"}}},"required":["address"]},"secretShape":{"kind":"oauth2","authorizeUrl":"https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize","tokenUrl":"https://login.microsoftonline.com/consumers/oauth2/v2.0/token","scopes":"offline_access https://graph.microsoft.com/Mail.Read https://graph.microsoft.com/Mail.Send","clientIdEnv":"GEZEL_MICROSOFT_CLIENT_ID","clientSetup":{"providerLabel":"Microsoft","docsUrl":"https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade","secretRequired":false,"appTypeNote":"Register an app in Microsoft Entra (Azure portal) and add the Mobile and desktop applications platform — a public client needs no client secret. Any loopback port works for the redirect.","steps":["Sign in at portal.azure.com and open Microsoft Entra ID, then App registrations, then New registration.","For supported account types choose 'Accounts in any organizational directory and personal Microsoft accounts'.","Under Authentication, add the 'Mobile and desktop applications' platform and enable the loopback redirect — any 127.0.0.1 port works.","No client secret is needed: this registration is a public client.","Copy the Application (client) ID from the Overview page and paste it below."]}},"source":{"adapterId":"mail-outlook"},"normalize":{"kind":"native"},"actions":[{"name":"send","consentScope":"recipient-allowlist"}],"completeness":"mirror","notes":"Personal Outlook.com accounts in the 'consumers' tenant. Sending is deny-by-default: only recipients on the binding's allowed recipients/domains lists can be sent to, and night shift defers sends to the outbox for daytime approval.","availableVersions":["1.0.0"]}},{"manifest":{"schemaVersion":1,"kind":"connector-type","id":"x-posts","name":"X Posts","description":"Keep a searchable copy of your X (Twitter) presence: your posts with their like, repost, and view counts and a daily engagement history, optional mentions, and a daily follower count. Links through X's own sign-in. X charges for API reads, so syncs are spaced out by default. Your crew can draft posts for review and — once you allow publishing on the connector — post them with your per-post consent; posting fits X's free tier.","tags":["x","twitter","social","posts","native"],"maintainer":{"name":"Gezel"},"version":"1.0.0","releasedAt":"2026-08-12T00:00:00Z","driver":"native","configSchema":{"type":"object","properties":{"syncMentions":{"type":"boolean","title":"Sync mentions of the account (costs extra paid reads)","default":false},"allowPublish":{"type":"boolean","title":"Allow this project's gezels to queue posts for publishing (each publish still needs your consent)","default":false},"syncIntervalHours":{"type":"number","title":"Hours between post syncs (X reads are paid — keep this generous)","default":4,"minimum":0.25,"maximum":720},"metricsWindowDays":{"type":"integer","title":"Days of recent posts to keep engagement counts fresh for","default":7,"minimum":1,"maximum":365},"metricsRefreshHours":{"type":"number","title":"Hours between engagement-count refreshes","default":24,"minimum":0.25,"maximum":720},"metricsHistory":{"type":"boolean","title":"Record daily engagement history records","default":true},"statsIntervalHours":{"type":"number","title":"Hours between account-stats snapshots","default":6,"minimum":0.25,"maximum":720}}},"secretShape":{"kind":"oauth2","authorizeUrl":"https://x.com/i/oauth2/authorize","tokenUrl":"https://api.x.com/2/oauth2/token","scopes":"tweet.read users.read offline.access tweet.write","clientIdEnv":"GEZEL_X_CLIENT_ID","clientSecretEnv":"GEZEL_X_CLIENT_SECRET","clientSetup":{"providerLabel":"X (Twitter)","docsUrl":"https://developer.x.com/en/portal/dashboard","secretRequired":false,"appTypeNote":"Create a free developer app and set its type to a Native/Public client — PKCE needs no client secret. Your API tier and read limits are your own.","redirectPort":6241,"redirectNote":"X matches callback URLs exactly. Register the URI shown here character-for-character.","steps":["Sign in at developer.x.com and create a project with an app — the free plan works for publishing.","In the app's settings, open 'User authentication settings' and click Set up.","Choose the 'Native App' (public client) type and enable OAuth 2.0.","Paste the redirect URI shown below into Callback URI / Redirect URL, character-for-character.","Fill the required website URL field with any page of yours and save.","Copy the OAuth 2.0 Client ID from the app's Keys and tokens page and paste it below — native apps use PKCE, so there is no secret to copy."]}},"source":{"adapterId":"x-posts"},"normalize":{"kind":"native"},"actions":[{"name":"publish","consentScope":"social-publish"}],"completeness":"window","notes":"Syncs three things from the linked account: the account's own posts (text with links expanded, language, reply threading, plus like/repost/reply/quote/bookmark/view counts), mentions of the account when enabled, and a once-per-day account-stats record (followers, following, total posts). Be blunt about cost: X charges for API reads on every tier, and the free tier allows roughly 100 reads per month — that fits only the default generous intervals (posts every 4 hours, stats every 6, engagement refresh daily); for real day-to-day use you need the paid Basic tier or higher. Every read scope is interval-gated (syncIntervalHours for posts and mentions, statsIntervalHours for stats) so background syncing can never burn reads faster than configured, and mentions are OFF by default to conserve reads. Engagement counts on posts from the last metricsWindowDays are re-observed roughly every metricsRefreshHours as one extra page per refresh, updating existing files in place; each observation also appends a per-day metrics-history record per post (the engagement curve over time) unless metricsHistory is turned off. The first sync captures up to 500 recent posts; older history is not backfilled. Publishing works even on the free tier — X budgets writes separately at roughly 500 posts per month — and is off by default: a gezel can only draft posts until allowPublish is enabled on the binding, and every publish is individually consented through the daemon (night shift defers publishes to the outbox for daytime approval). Accounts linked before publishing existed must reconnect from the connector's settings to grant the new tweet.write scope. Requires your own X developer app: set GEZEL_X_CLIENT_ID (and GEZEL_X_CLIENT_SECRET for confidential apps) to enable linking.","availableVersions":["1.0.0"]}}]}
@@ -0,0 +1,16 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "kind": "connector-type",
4
+ "id": "linkedin-posts",
5
+ "name": "LinkedIn Posts",
6
+ "description": "Publish to LinkedIn from Gezel: your crew drafts posts in your voice, and with your permission queues them — every publish still asks for your consent before anything goes out. LinkedIn does not let ordinary apps read your feed (that part is reserved for its partner program), so nothing syncs back: reception and analytics stay on LinkedIn, and this connection is honestly posting-only.",
7
+ "tags": [
8
+ "linkedin",
9
+ "social",
10
+ "posts",
11
+ "native"
12
+ ],
13
+ "maintainer": {
14
+ "name": "Gezel"
15
+ }
16
+ }
@@ -0,0 +1,54 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "version": "1.0.0",
4
+ "releasedAt": "2026-08-12T00:00:00Z",
5
+ "driver": "native",
6
+ "source": {
7
+ "adapterId": "linkedin-posts"
8
+ },
9
+ "configSchema": {
10
+ "type": "object",
11
+ "properties": {
12
+ "allowPublish": {
13
+ "type": "boolean",
14
+ "title": "Allow this project's gezels to queue posts for publishing (each publish still needs your consent)",
15
+ "default": false
16
+ }
17
+ }
18
+ },
19
+ "secretShape": {
20
+ "kind": "oauth2",
21
+ "authorizeUrl": "https://www.linkedin.com/oauth/v2/authorization",
22
+ "tokenUrl": "https://www.linkedin.com/oauth/v2/accessToken",
23
+ "scopes": "openid profile w_member_social",
24
+ "clientIdEnv": "GEZEL_LINKEDIN_CLIENT_ID",
25
+ "clientSecretEnv": "GEZEL_LINKEDIN_CLIENT_SECRET",
26
+ "longLived": true,
27
+ "clientSetup": {
28
+ "providerLabel": "LinkedIn",
29
+ "docsUrl": "https://www.linkedin.com/developers/apps",
30
+ "secretRequired": true,
31
+ "appTypeNote": "Create a LinkedIn developer app and request the 'Share on LinkedIn' and 'Sign in with LinkedIn using OpenID Connect' products. LinkedIn requires the client secret; it stays in your OS keychain.",
32
+ "redirectPort": 6243,
33
+ "redirectNote": "LinkedIn matches redirect URLs exactly. Register the URI shown here character-for-character. Tokens last about 60 days; reconnect when prompted.",
34
+ "steps": [
35
+ "Sign in at linkedin.com/developers/apps and create an app — it must be tied to a LinkedIn Page (create a free one if you have none).",
36
+ "On the Products tab, request 'Share on LinkedIn' and 'Sign in with LinkedIn using OpenID Connect'; both are approved instantly.",
37
+ "On the Auth tab, add the redirect URI shown below under 'Authorized redirect URLs for your app', exactly.",
38
+ "Copy the Client ID and Client Secret from the Auth tab.",
39
+ "Paste both below — the secret stays in your OS keychain."
40
+ ]
41
+ }
42
+ },
43
+ "normalize": {
44
+ "kind": "native"
45
+ },
46
+ "actions": [
47
+ {
48
+ "name": "publish",
49
+ "consentScope": "social-publish"
50
+ }
51
+ ],
52
+ "completeness": "window",
53
+ "notes": "Publishing-only, by LinkedIn's design: reading a member's posts requires LinkedIn's restricted Community Management partner program, which ordinary developer apps cannot join, so nothing syncs back from the feed — no post history, no engagement counts, no mentions. The corpus root holds only what Gezel itself did: the _meta.json marker plus a receipt for every published post under _actions/_sent/; reception and analytics stay on linkedin.com. Publishing works with the standard 'Share on LinkedIn' product: text posts up to 3000 characters, posted to the linked member's own feed with a permalink returned for the record. Publishing is off by default — a gezel can only draft until allowPublish is enabled on the binding, and every publish is individually consented through the daemon (night shift defers publishes to the outbox for daytime approval). Requires your own LinkedIn developer app: set GEZEL_LINKEDIN_CLIENT_ID and GEZEL_LINKEDIN_CLIENT_SECRET, or register the app under Settings — LinkedIn requires the client secret (it has no public-client PKCE support; Gezel still sends PKCE params, which LinkedIn ignores). LinkedIn issues no refresh token to ordinary apps, so the connection lasts about 60 days and then asks to be reconnected."
54
+ }
@@ -53,6 +53,20 @@
53
53
  "authParams": {
54
54
  "access_type": "offline",
55
55
  "prompt": "consent"
56
+ },
57
+ "clientSetup": {
58
+ "providerLabel": "Google",
59
+ "docsUrl": "https://console.cloud.google.com/apis/credentials",
60
+ "secretRequired": true,
61
+ "appTypeNote": "Create an OAuth client ID of type Desktop app in Google Cloud Console. Desktop clients are issued a client secret Google does not treat as confidential — enter both here. Any loopback port works for the redirect.",
62
+ "steps": [
63
+ "Sign in at console.cloud.google.com and create (or pick) a project.",
64
+ "Under APIs & Services, open Library and enable the Gmail API.",
65
+ "Under APIs & Services, open OAuth consent screen: configure the app as External and add yourself as a test user.",
66
+ "Set the publishing status to In production — apps left in Testing get their sign-ins expired every 7 days. An unverified-app warning on your own app is safe to click through.",
67
+ "Under APIs & Services, open Credentials and choose Create credentials, then OAuth client ID, application type 'Desktop app'.",
68
+ "Copy the Client ID and Client Secret from the created client and paste them below. Desktop clients accept any 127.0.0.1 port, so there is no redirect URI to register."
69
+ ]
56
70
  }
57
71
  },
58
72
  "normalize": {
@@ -52,7 +52,20 @@
52
52
  "authorizeUrl": "https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize",
53
53
  "tokenUrl": "https://login.microsoftonline.com/organizations/oauth2/v2.0/token",
54
54
  "scopes": "offline_access https://graph.microsoft.com/Mail.Read https://graph.microsoft.com/Mail.Send",
55
- "clientIdEnv": "GEZEL_MICROSOFT_CLIENT_ID"
55
+ "clientIdEnv": "GEZEL_MICROSOFT_CLIENT_ID",
56
+ "clientSetup": {
57
+ "providerLabel": "Microsoft",
58
+ "docsUrl": "https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade",
59
+ "secretRequired": false,
60
+ "appTypeNote": "Register an app in Microsoft Entra (Azure portal) and add the Mobile and desktop applications platform — a public client needs no client secret. Any loopback port works for the redirect.",
61
+ "steps": [
62
+ "Sign in at portal.azure.com and open Microsoft Entra ID, then App registrations, then New registration.",
63
+ "For supported account types choose 'Accounts in any organizational directory and personal Microsoft accounts'.",
64
+ "Under Authentication, add the 'Mobile and desktop applications' platform and enable the loopback redirect — any 127.0.0.1 port works.",
65
+ "No client secret is needed: this registration is a public client.",
66
+ "Copy the Application (client) ID from the Overview page and paste it below."
67
+ ]
68
+ }
56
69
  },
57
70
  "normalize": {
58
71
  "kind": "native"
@@ -52,7 +52,20 @@
52
52
  "authorizeUrl": "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize",
53
53
  "tokenUrl": "https://login.microsoftonline.com/consumers/oauth2/v2.0/token",
54
54
  "scopes": "offline_access https://graph.microsoft.com/Mail.Read https://graph.microsoft.com/Mail.Send",
55
- "clientIdEnv": "GEZEL_MICROSOFT_CLIENT_ID"
55
+ "clientIdEnv": "GEZEL_MICROSOFT_CLIENT_ID",
56
+ "clientSetup": {
57
+ "providerLabel": "Microsoft",
58
+ "docsUrl": "https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade",
59
+ "secretRequired": false,
60
+ "appTypeNote": "Register an app in Microsoft Entra (Azure portal) and add the Mobile and desktop applications platform — a public client needs no client secret. Any loopback port works for the redirect.",
61
+ "steps": [
62
+ "Sign in at portal.azure.com and open Microsoft Entra ID, then App registrations, then New registration.",
63
+ "For supported account types choose 'Accounts in any organizational directory and personal Microsoft accounts'.",
64
+ "Under Authentication, add the 'Mobile and desktop applications' platform and enable the loopback redirect — any 127.0.0.1 port works.",
65
+ "No client secret is needed: this registration is a public client.",
66
+ "Copy the Application (client) ID from the Overview page and paste it below."
67
+ ]
68
+ }
56
69
  },
57
70
  "normalize": {
58
71
  "kind": "native"
@@ -3,7 +3,7 @@
3
3
  "kind": "connector-type",
4
4
  "id": "x-posts",
5
5
  "name": "X Posts",
6
- "description": "Keep a searchable copy of your X (Twitter) presence: your posts with their like, repost, and view counts, optional mentions, and a daily follower count. Links through X's own sign-in. X charges for API reads, so syncs are spaced out by default. Your crew can draft posts for you, but Gezel never posts to X itselfyou copy a draft and publish it yourself.",
6
+ "description": "Keep a searchable copy of your X (Twitter) presence: your posts with their like, repost, and view counts and a daily engagement history, optional mentions, and a daily follower count. Links through X's own sign-in. X charges for API reads, so syncs are spaced out by default. Your crew can draft posts for review and once you allow publishing on the connector post them with your per-post consent; posting fits X's free tier.",
7
7
  "tags": [
8
8
  "x",
9
9
  "twitter",
@@ -14,6 +14,11 @@
14
14
  "title": "Sync mentions of the account (costs extra paid reads)",
15
15
  "default": false
16
16
  },
17
+ "allowPublish": {
18
+ "type": "boolean",
19
+ "title": "Allow this project's gezels to queue posts for publishing (each publish still needs your consent)",
20
+ "default": false
21
+ },
17
22
  "syncIntervalHours": {
18
23
  "type": "number",
19
24
  "title": "Hours between post syncs (X reads are paid — keep this generous)",
@@ -35,6 +40,11 @@
35
40
  "minimum": 0.25,
36
41
  "maximum": 720
37
42
  },
43
+ "metricsHistory": {
44
+ "type": "boolean",
45
+ "title": "Record daily engagement history records",
46
+ "default": true
47
+ },
38
48
  "statsIntervalHours": {
39
49
  "type": "number",
40
50
  "title": "Hours between account-stats snapshots",
@@ -48,9 +58,25 @@
48
58
  "kind": "oauth2",
49
59
  "authorizeUrl": "https://x.com/i/oauth2/authorize",
50
60
  "tokenUrl": "https://api.x.com/2/oauth2/token",
51
- "scopes": "tweet.read users.read offline.access",
61
+ "scopes": "tweet.read users.read offline.access tweet.write",
52
62
  "clientIdEnv": "GEZEL_X_CLIENT_ID",
53
- "clientSecretEnv": "GEZEL_X_CLIENT_SECRET"
63
+ "clientSecretEnv": "GEZEL_X_CLIENT_SECRET",
64
+ "clientSetup": {
65
+ "providerLabel": "X (Twitter)",
66
+ "docsUrl": "https://developer.x.com/en/portal/dashboard",
67
+ "secretRequired": false,
68
+ "appTypeNote": "Create a free developer app and set its type to a Native/Public client — PKCE needs no client secret. Your API tier and read limits are your own.",
69
+ "redirectPort": 6241,
70
+ "redirectNote": "X matches callback URLs exactly. Register the URI shown here character-for-character.",
71
+ "steps": [
72
+ "Sign in at developer.x.com and create a project with an app — the free plan works for publishing.",
73
+ "In the app's settings, open 'User authentication settings' and click Set up.",
74
+ "Choose the 'Native App' (public client) type and enable OAuth 2.0.",
75
+ "Paste the redirect URI shown below into Callback URI / Redirect URL, character-for-character.",
76
+ "Fill the required website URL field with any page of yours and save.",
77
+ "Copy the OAuth 2.0 Client ID from the app's Keys and tokens page and paste it below — native apps use PKCE, so there is no secret to copy."
78
+ ]
79
+ }
54
80
  },
55
81
  "normalize": {
56
82
  "kind": "native"
@@ -58,9 +84,9 @@
58
84
  "actions": [
59
85
  {
60
86
  "name": "publish",
61
- "consentScope": "manual-export"
87
+ "consentScope": "social-publish"
62
88
  }
63
89
  ],
64
90
  "completeness": "window",
65
- "notes": "Syncs three things from the linked account: the account's own posts (text with links expanded, language, reply threading, plus like/repost/reply/quote/bookmark/view counts), mentions of the account when enabled, and a once-per-day account-stats record (followers, following, total posts). Be blunt about cost: X charges for API reads on every tier, and the free tier allows roughly 100 reads per month — that fits only the default generous intervals (posts every 4 hours, stats every 6, engagement refresh daily); for real day-to-day use you need the paid Basic tier or higher. Every read scope is interval-gated (syncIntervalHours for posts and mentions, statsIntervalHours for stats) so background syncing can never burn reads faster than configured, and mentions are OFF by default to conserve reads. Engagement counts on posts from the last metricsWindowDays are re-observed roughly every metricsRefreshHours as one extra page per refresh, updating existing files in place. The first sync captures up to 500 recent posts; older history is not backfilled. Publishing is manual-export only in this version: gezels can draft posts for review, but Gezel never posts to X on your behalf you copy a draft and publish it yourself. Requires your own X developer app: set GEZEL_X_CLIENT_ID (and GEZEL_X_CLIENT_SECRET for confidential apps) to enable linking."
91
+ "notes": "Syncs three things from the linked account: the account's own posts (text with links expanded, language, reply threading, plus like/repost/reply/quote/bookmark/view counts), mentions of the account when enabled, and a once-per-day account-stats record (followers, following, total posts). Be blunt about cost: X charges for API reads on every tier, and the free tier allows roughly 100 reads per month — that fits only the default generous intervals (posts every 4 hours, stats every 6, engagement refresh daily); for real day-to-day use you need the paid Basic tier or higher. Every read scope is interval-gated (syncIntervalHours for posts and mentions, statsIntervalHours for stats) so background syncing can never burn reads faster than configured, and mentions are OFF by default to conserve reads. Engagement counts on posts from the last metricsWindowDays are re-observed roughly every metricsRefreshHours as one extra page per refresh, updating existing files in place; each observation also appends a per-day metrics-history record per post (the engagement curve over time) unless metricsHistory is turned off. The first sync captures up to 500 recent posts; older history is not backfilled. Publishing works even on the free tier — X budgets writes separately at roughly 500 posts per month — and is off by default: a gezel can only draft posts until allowPublish is enabled on the binding, and every publish is individually consented through the daemon (night shift defers publishes to the outbox for daytime approval). Accounts linked before publishing existed must reconnect from the connector's settings to grant the new tweet.write scope. Requires your own X developer app: set GEZEL_X_CLIENT_ID (and GEZEL_X_CLIENT_SECRET for confidential apps) to enable linking."
66
92
  }