@synap-core/workspace-templates 0.12.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/README.md +483 -0
  2. package/automations/bookmark-enrichment.automation.json +321 -0
  3. package/automations/github-pr-events.automation.json +505 -0
  4. package/automations/mail-feed.automation.json +86 -0
  5. package/automations/ship-a-change-checks-green.automation.json +106 -0
  6. package/automations/ship-a-change-checks-red.automation.json +106 -0
  7. package/automations/ship-a-change-deployed.automation.json +56 -0
  8. package/automations/ship-a-change-pr-merged.automation.json +110 -0
  9. package/automations/ship-a-change-pr-opened.automation.json +104 -0
  10. package/automations/url-bookmark-capture.automation.json +132 -0
  11. package/capabilities/agency-skills.capability.json +157 -0
  12. package/capabilities/apify.capability.json +265 -0
  13. package/capabilities/apollo.capability.json +120 -0
  14. package/capabilities/arch-backend.capability.json +97 -0
  15. package/capabilities/arch-client-intelligence.capability.json +401 -0
  16. package/capabilities/backblaze-b2.capability.json +205 -0
  17. package/capabilities/cal-com.capability.json +266 -0
  18. package/capabilities/canva.capability.json +274 -0
  19. package/capabilities/claude-code-routines.capability.json +172 -0
  20. package/capabilities/claude-managed-agents.capability.json +215 -0
  21. package/capabilities/client-comms-report.capability.json +246 -0
  22. package/capabilities/content-idea-to-production.capability.json +29 -0
  23. package/capabilities/datagouv.capability.json +315 -0
  24. package/capabilities/discord-bot.capability.json +157 -0
  25. package/capabilities/dropbox.capability.json +281 -0
  26. package/capabilities/exa.capability.json +109 -0
  27. package/capabilities/fal-ai.capability.json +83 -0
  28. package/capabilities/fal-media-lab.capability.json +158 -0
  29. package/capabilities/fireflies.capability.json +191 -0
  30. package/capabilities/freellmapi.capability.json +114 -0
  31. package/capabilities/generic-apikey.capability.json +92 -0
  32. package/capabilities/github-merge.capability.json +99 -0
  33. package/capabilities/github-read.capability.json +325 -0
  34. package/capabilities/governance-tighten-recommender.capability.json +81 -0
  35. package/capabilities/jev.capability.json +226 -0
  36. package/capabilities/lead-conversion-enrollment.capability.json +246 -0
  37. package/capabilities/mail-feed.capability.json +277 -0
  38. package/capabilities/mailgun.capability.json +52 -0
  39. package/capabilities/nango-google.capability.json +639 -0
  40. package/capabilities/notion.capability.json +417 -0
  41. package/capabilities/omniroute.capability.json +137 -0
  42. package/capabilities/posthog-analytics.capability.json +258 -0
  43. package/capabilities/postiz.capability.json +467 -0
  44. package/capabilities/proactive-digest.capability.json +190 -0
  45. package/capabilities/proton.capability.json +288 -0
  46. package/capabilities/replicate-rvm.capability.json +97 -0
  47. package/capabilities/research-methods.capability.json +57 -0
  48. package/capabilities/shopify.capability.json +395 -0
  49. package/capabilities/slack.capability.json +369 -0
  50. package/capabilities/stripe.capability.json +420 -0
  51. package/capabilities/telegram-bridge.capability.json +56 -0
  52. package/capabilities/treg.capability.json +102 -0
  53. package/capabilities/unipile-linkedin.capability.json +471 -0
  54. package/capabilities/vercel.capability.json +424 -0
  55. package/dist/define-CPS0n8BV.d.ts +1331 -0
  56. package/dist/index.d.ts +174 -900
  57. package/dist/index.js +26148 -23931
  58. package/dist/index.js.map +1 -1
  59. package/dist/wire.d.ts +222 -0
  60. package/dist/wire.js +77 -0
  61. package/dist/wire.js.map +1 -0
  62. package/package.json +27 -4
  63. package/packs/bookmark-views-pack.json +33 -0
  64. package/packs/crm-pack.json +76 -0
  65. package/packs/customer-profile.json +28 -0
  66. package/packs/deep-research.playbook.json +30 -0
  67. package/packs/event-views-pack.json +33 -0
  68. package/packs/habit-profile.json +27 -0
  69. package/packs/hr-pack.json +54 -0
  70. package/packs/invoice-profile.json +28 -0
  71. package/packs/knowledge-base-pack.json +56 -0
  72. package/packs/meeting-profile.json +29 -0
  73. package/packs/product-profile.json +30 -0
  74. package/packs/project-management-pack.json +49 -0
  75. package/packs/recipe-profile.json +29 -0
  76. package/packs/task-views-pack.json +39 -0
  77. package/retired.yaml +136 -0
  78. package/skills/app-factory-build-slice/SKILL.md +70 -0
  79. package/skills/app-factory-build-slice.catalog.json +17 -0
  80. package/skills/app-factory-design-canvas/SKILL.md +96 -0
  81. package/skills/app-factory-design-canvas.catalog.json +17 -0
  82. package/skills/app-factory-design-review/SKILL.md +99 -0
  83. package/skills/app-factory-design-review.catalog.json +18 -0
  84. package/skills/app-factory-research/SKILL.md +123 -0
  85. package/skills/app-factory-research.catalog.json +17 -0
  86. package/skills/bookmark-enrichment/SKILL.md +40 -0
  87. package/skills/carousel-format/carousel-format.skill.json +18 -0
  88. package/skills/collage-broll/collage-broll.skill.json +20 -0
  89. package/skills/content-taste/content-taste.skill.json +18 -0
  90. package/skills/creative-director/creative-director.skill.json +18 -0
  91. package/skills/design-critique/SKILL.md +135 -0
  92. package/skills/design-critique.catalog.json +12 -0
  93. package/skills/designing-with-real-components/SKILL.md +140 -0
  94. package/skills/designing-with-real-components.catalog.json +12 -0
  95. package/skills/edit-loop/edit-loop.skill.json +25 -0
  96. package/skills/facecam-edit/facecam-edit.skill.json +24 -0
  97. package/skills/mapping-the-server/SKILL.md +54 -0
  98. package/skills/math-explainer-manimgx/math-explainer-manimgx.skill.json +20 -0
  99. package/skills/motion-quality-gates/motion-quality-gates.skill.json +20 -0
  100. package/skills/open-design-edit/open-design-edit.skill.json +24 -0
  101. package/skills/reading-client-progress/SKILL.md +65 -0
  102. package/skills/routing-to-channels/SKILL.md +54 -0
  103. package/skills/stellar-scf-grants-advisor/ASSOCIATE-CHEATSHEET.md +133 -0
  104. package/skills/stellar-scf-grants-advisor/ASSOCIATE-ONBOARDING-GUIDE.md +761 -0
  105. package/skills/stellar-scf-grants-advisor/CLIENT-FAQ.md +181 -0
  106. package/skills/stellar-scf-grants-advisor/PARTNER-HANDOVER.md +130 -0
  107. package/skills/stellar-scf-grants-advisor/SKILL.md +102 -0
  108. package/skills/stellar-scf-grants-advisor/examples/README.md +43 -0
  109. package/skills/stellar-scf-grants-advisor/examples/chat-summary-templates.md +158 -0
  110. package/skills/stellar-scf-grants-advisor/examples/corpus-comparable-rows.md +136 -0
  111. package/skills/stellar-scf-grants-advisor/examples/q10-team-rewrites.md +109 -0
  112. package/skills/stellar-scf-grants-advisor/examples/q2-description-rewrites.md +88 -0
  113. package/skills/stellar-scf-grants-advisor/examples/q6-integration-rewrites.md +107 -0
  114. package/skills/stellar-scf-grants-advisor/examples/tranche-structures.md +176 -0
  115. package/skills/stellar-scf-grants-advisor/reference/01-methodology.md +96 -0
  116. package/skills/stellar-scf-grants-advisor/reference/02-scoring-framework.md +199 -0
  117. package/skills/stellar-scf-grants-advisor/reference/03-rejection-patterns.md +241 -0
  118. package/skills/stellar-scf-grants-advisor/reference/04-winner-patterns.md +120 -0
  119. package/skills/stellar-scf-grants-advisor/reference/05-comeback-patterns.md +92 -0
  120. package/skills/stellar-scf-grants-advisor/reference/06-budget-benchmarks.md +125 -0
  121. package/skills/stellar-scf-grants-advisor/reference/07-integration-list.md +147 -0
  122. package/skills/stellar-scf-grants-advisor/reference/08-abstract-fields.md +258 -0
  123. package/skills/stellar-scf-grants-advisor/reference/09-build-fields.md +259 -0
  124. package/skills/stellar-scf-grants-advisor/reference/10-corpus-winners.md +135 -0
  125. package/skills/stellar-scf-grants-advisor/reference/11-corpus-rejects.md +267 -0
  126. package/skills/stellar-scf-grants-advisor/reference/12-client-communication.md +131 -0
  127. package/skills/stellar-scf-grants-advisor/reference/13-excel-output-spec.md +199 -0
  128. package/skills/stellar-scf-grants-advisor/reference/14-worked-examples.md +160 -0
  129. package/skills/stellar-scf-grants-advisor/templates/abstract-review-builder.py +253 -0
  130. package/skills/stellar-scf-grants-advisor/templates/build-review-builder.py +213 -0
  131. package/skills/video-direction/video-direction.skill.json +20 -0
  132. package/skills/video-style-vocabulary/video-style-vocabulary.skill.json +20 -0
  133. package/skills/video-toolbox/video-toolbox.skill.json +20 -0
  134. package/src/__fixtures__/lifecycle-presets.flat-before.json +205 -0
  135. package/src/agency-os.yaml +122 -0
  136. package/src/agent-os-skill-slugs.test.ts +65 -0
  137. package/src/apify-social-reader.test.ts +317 -0
  138. package/src/base.yaml +85 -39
  139. package/src/blockchain-ecosystem.yaml +1 -0
  140. package/src/brand-library.yaml +525 -14
  141. package/src/builder-dev-fold.template.test.ts +113 -0
  142. package/src/builder.yaml +779 -91
  143. package/src/business-model-catalog-description.test.ts +47 -0
  144. package/src/business-model-grp.template.test.ts +214 -0
  145. package/src/business-model.yaml +296 -49
  146. package/src/catalog-safety.test.ts +198 -0
  147. package/src/catalog.template-items.test.ts +75 -0
  148. package/src/catalog.ts +66 -2
  149. package/src/concept-guard.test.ts +28 -8
  150. package/src/concept-guard.ts +2 -2
  151. package/src/content-creation.template.test.ts +52 -0
  152. package/src/content-creation.yaml +192 -0
  153. package/src/content-os.edit-loop.template.test.ts +210 -0
  154. package/src/content-os.publish-due-posts.template.test.ts +261 -0
  155. package/src/content-os.template.test.ts +322 -0
  156. package/src/content-os.video-takes.template.test.ts +338 -0
  157. package/src/content-os.work-a-take.template.test.ts +125 -0
  158. package/src/content-os.yaml +1830 -39
  159. package/src/content-studio-retired.template.test.ts +82 -0
  160. package/src/corpus-flow-validity.test.ts +4 -5
  161. package/src/crm-absorbs-outreach.template.test.ts +117 -0
  162. package/src/crm-build-plan.template.test.ts +128 -0
  163. package/src/crm-lists.template.test.ts +96 -0
  164. package/src/crm-messages-cadence.template.test.ts +164 -0
  165. package/src/crm-one-send-door.template.test.ts +65 -0
  166. package/src/crm.yaml +1456 -181
  167. package/src/decisions-wave.template.test.ts +186 -0
  168. package/src/define.ts +130 -17
  169. package/src/design-to-implementation.template.test.ts +94 -0
  170. package/src/domain-template-type.test.ts +75 -0
  171. package/src/ecosystem.yaml +27 -66
  172. package/src/enterprise-os.yaml +10 -134
  173. package/src/facecam-edit.audio-studio.test.ts +188 -0
  174. package/src/fal-generate-image.test.ts +200 -0
  175. package/src/fal-media-lab-pod-files.test.ts +180 -0
  176. package/src/finance.yaml +254 -1
  177. package/src/foundation.yaml +78 -38
  178. package/src/frame-host-modules.d.mts +6 -0
  179. package/src/frame-host-modules.mjs +58 -0
  180. package/src/generate.mjs +9 -3
  181. package/src/github-merge.test.ts +210 -0
  182. package/src/grants.yaml +46 -17
  183. package/src/hr.yaml +39 -6
  184. package/src/index.ts +36 -1
  185. package/src/intent.test.ts +161 -0
  186. package/src/internal-runbook.yaml +19 -1
  187. package/src/jev.test.ts +356 -0
  188. package/src/legal.yaml +33 -1
  189. package/src/life-os.yaml +51 -240
  190. package/src/lifecycle-coverage.test.ts +124 -0
  191. package/src/lifecycle-declarations.test.ts +252 -0
  192. package/src/lifecycle-presets.test.ts +468 -0
  193. package/src/lifecycle-presets.ts +494 -0
  194. package/src/lifecycle-stuck.test.ts +46 -0
  195. package/src/lifecycle-waits.parity.test.ts +319 -0
  196. package/src/marketing.yaml +108 -4
  197. package/src/meetings-knowledge.yaml +18 -0
  198. package/src/networking.yaml +23 -2
  199. package/src/operations.yaml +304 -4
  200. package/src/pipeline-views.template.test.ts +88 -0
  201. package/src/playbook-goal-placeholders.test.ts +57 -0
  202. package/src/postiz-contract.test.ts +134 -0
  203. package/src/presentation-declarations.test.ts +174 -0
  204. package/src/process-contract.validate.test.ts +352 -0
  205. package/src/project-management.yaml +39 -740
  206. package/src/publish-body.d.mts +29 -0
  207. package/src/publish-body.mjs +74 -0
  208. package/src/publish-body.test.ts +74 -0
  209. package/src/publish-official-lib.test.ts +176 -0
  210. package/src/replicate-rvm.test.ts +504 -0
  211. package/src/research-base.yaml +119 -20
  212. package/src/research-decision.yaml +60 -3
  213. package/src/reserved-profile-slugs.test.ts +83 -0
  214. package/src/retired-goal-kinds.test.ts +58 -0
  215. package/src/role-reference-and-step-domain.test.ts +192 -0
  216. package/src/seed-obligation.test.ts +1 -6
  217. package/src/send-message-maps.test.ts +227 -0
  218. package/src/shared-role-ssot.test.ts +40 -2
  219. package/src/ship-a-change.template.test.ts +177 -0
  220. package/src/skills.test.ts +170 -0
  221. package/src/social.yaml +85 -27
  222. package/src/space-packages.freshness.test.ts +97 -0
  223. package/src/space-packages.generated.ts +40 -0
  224. package/src/space-packages.test.ts +125 -0
  225. package/src/space-packages.ts +73 -0
  226. package/src/start.test.ts +188 -0
  227. package/src/task-intents.test.ts +293 -0
  228. package/src/templates.ts +20851 -21496
  229. package/src/types.ts +443 -60
  230. package/src/unipile-publish-post.test.ts +162 -0
  231. package/src/validate-flow.drift.test.ts +2 -3
  232. package/src/validate-flow.ts +22 -2
  233. package/src/validate-onboarding.test.ts +59 -0
  234. package/src/validate.test.ts +71 -4
  235. package/src/validate.ts +1335 -7
  236. package/src/wire-core.ts +231 -0
  237. package/src/wire.test.ts +159 -0
  238. package/src/wire.ts +107 -0
  239. package/src/agent-fleet.yaml +0 -1645
  240. package/src/business-developer.template.test.ts +0 -351
  241. package/src/business-developer.yaml +0 -860
  242. package/src/content-studio.yaml +0 -361
  243. package/src/dev-dashboard.yaml +0 -990
  244. package/src/outreach-comms.yaml +0 -486
  245. package/src/the-arch.yaml +0 -201
@@ -0,0 +1,205 @@
1
+ {
2
+ "apiVersion": "synap/v1",
3
+ "kind": "capability",
4
+ "meta": {
5
+ "slug": "backblaze-b2",
6
+ "name": "Backblaze B2 — Backup Storage",
7
+ "description": "Set up and check the B2 bucket that holds your pods' backups: list buckets and keys, check a bucket against the backup policy, see storage per pod and whether each pod's backups are recent. Creating or changing the bucket waits for your approval. Needs a B2 application key.",
8
+ "tags": [
9
+ "capability",
10
+ "backup",
11
+ "storage",
12
+ "backblaze",
13
+ "opt-in"
14
+ ],
15
+ "isPublic": true,
16
+ "syncByDefault": false
17
+ },
18
+ "spec": {
19
+ "key": "backblaze-b2",
20
+ "name": "Backblaze B2 — Backup Storage",
21
+ "description": "Set up and audit Backblaze B2 backup storage for Synap pods. Pod backups go to ONE versioned, private B2 bucket; each pod writes under pods/<podId>/ with its own key that cannot delete. READ verbs (run without approval): list buckets, read a bucket's settings against the backup policy, list keys, storage used per pods/<podId>/ prefix, and whether a pod's backups are present and recent (newest restic snapshot). WRITE verb (always proposal-gated): create or configure THE backup bucket — private, keep all versions, lifecycle rule {fileNamePrefix:\"pods/\", daysFromHidingToDeleting:30, daysFromUploadingToHiding:null}, Object Lock off. OUT OF SCOPE ON PURPOSE: deleting buckets, files or keys, and minting per-pod keys. Per-pod keys are minted ONLY by the control-plane backup vault (one key-minting door), so this capability never holds writeKeys or deleteKeys. Auth: signs in with b2_authorize_account (HTTP Basic keyID:applicationKey) and calls the account's own apiUrl with the returned token, cached up to 24h and refreshed on expiry. PRECONDITION: a B2 application key with exactly these capabilities: listBuckets, writeBuckets, readBucketRetentions, listFiles, listKeys — all buckets (no bucket or prefix restriction). Not needed: readBuckets (S3-API only), writeKeys (no verb mints keys). readBucketRetentions is what lets the Object Lock state be read.",
22
+ "params": [
23
+ {
24
+ "name": "b2Credential",
25
+ "label": "B2 application key (keyID:applicationKey)",
26
+ "type": "text",
27
+ "required": true,
28
+ "description": "Your B2 application key as keyID:applicationKey — the keyID, a colon, then the applicationKey (B2 shows the applicationKey only once, when the key is created). Capabilities: listBuckets, writeBuckets, readBucketRetentions, listFiles, listKeys. Stored server-encrypted in the vault; never persisted in the template."
29
+ }
30
+ ],
31
+ "vault": [
32
+ {
33
+ "ref": "b2CredentialSecret",
34
+ "name": "{{name}} B2 application key",
35
+ "value": "{{b2Credential}}",
36
+ "type": "api_key",
37
+ "description": "B2 application key for the b2_api connector, stored as keyID:applicationKey. Used only to sign in (b2_authorize_account); the token it returns is held in pod memory, never stored."
38
+ }
39
+ ],
40
+ "tools": [
41
+ {
42
+ "name": "b2_api",
43
+ "kind": "api",
44
+ "description": "Authenticated HTTP access to the Backblaze B2 native API v4. Signs in with b2_authorize_account (HTTP Basic keyID:applicationKey), then sends each call to the account's apiUrl with the returned authorizationToken; re-signs in once on expired_auth_token / bad_auth_token. accountId comes from the sign-in, filled into any request body that declares it. Its credentialRef is remapped to the runtime vault://<id> at apply time.",
45
+ "credentialRef": "b2CredentialSecret",
46
+ "executor": "is-agent",
47
+ "config": {
48
+ "auth": {
49
+ "type": "session",
50
+ "signIn": {
51
+ "url": "https://api.backblazeb2.com/b2api/v4/b2_authorize_account",
52
+ "method": "GET",
53
+ "credential": "basic"
54
+ },
55
+ "token": {
56
+ "path": "authorizationToken",
57
+ "header": "Authorization",
58
+ "prefix": "",
59
+ "ttlSeconds": 86400
60
+ },
61
+ "baseUrlFrom": "apiInfo.storageApi.apiUrl",
62
+ "bodyFrom": {
63
+ "accountId": "accountId"
64
+ },
65
+ "reauthOn": {
66
+ "status": 401,
67
+ "codePath": "code",
68
+ "codes": [
69
+ "expired_auth_token",
70
+ "bad_auth_token"
71
+ ]
72
+ }
73
+ }
74
+ }
75
+ }
76
+ ],
77
+ "skills": [
78
+ {
79
+ "name": "b2_list_buckets",
80
+ "intent": "list_records",
81
+ "kind": "code",
82
+ "scope": "pod",
83
+ "description": "Lists the B2 account's buckets (name, id, type, lifecycle rules, revision). Read-only, no side effects. Args: { bucketName?: string (only that bucket) }.",
84
+ "requires": [
85
+ "b2_api"
86
+ ],
87
+ "executionMode": "sync",
88
+ "timeoutSeconds": 30,
89
+ "parameters": {
90
+ "bucketName": "string?"
91
+ },
92
+ "metadata": {
93
+ "readOnly": true
94
+ },
95
+ "code": "// Executor wraps this body as (async (args, context) => { <code> }); use args\n// directly and `return`. Calling the b2_api tool (a POST to /b2api/v4/<op>)\n// signs in to B2 for you (auth.type \"session\": b2_authorize_account, token\n// cached), calls the per-account apiUrl, and fills `accountId` into a body that\n// declares it (accountId: null) — B2 rejects it on ops that do not take it.\n// B2 native API v4 — every operation is a POST with a JSON body.\nconst b2 = async (op, body) => {\n const res = await callProvider('b2_api', 'POST', `/b2api/v4/${op}`, body || {});\n if (res && res.proposed === true) return { proposed: true, proposalId: res.proposalId || null };\n return (res && res.body) || {};\n};\nconst body = { accountId: null };\nconst name = String(args?.bucketName ?? '').trim();\nif (name) body.bucketName = name;\nconst res = await b2('b2_list_buckets', body);\nconst buckets = (Array.isArray(res.buckets) ? res.buckets : []).map((b) => ({\n bucketName: b.bucketName,\n bucketId: b.bucketId,\n bucketType: b.bucketType,\n lifecycleRules: Array.isArray(b.lifecycleRules) ? b.lifecycleRules : [],\n revision: b.revision ?? null,\n}));\nreturn { count: buckets.length, buckets };\n"
96
+ },
97
+ {
98
+ "name": "b2_get_bucket_settings",
99
+ "intent": "fetch_record",
100
+ "kind": "code",
101
+ "scope": "pod",
102
+ "description": "Reads one bucket's settings — type (private/public), versioning (B2 keeps every version; lifecycle rules decide purging), lifecycle rules, Object Lock, default encryption — and checks them against the backup policy: private, ONE lifecycle rule {fileNamePrefix:\"pods/\", daysFromHidingToDeleting:30, daysFromUploadingToHiding:null}, Object Lock off. Read-only. Object Lock reads as \"unreadable\" when the key lacks readBucketRetentions. Args: { bucketName: string }.",
103
+ "requires": [
104
+ "b2_api"
105
+ ],
106
+ "executionMode": "sync",
107
+ "timeoutSeconds": 30,
108
+ "parameters": {
109
+ "bucketName": "string"
110
+ },
111
+ "metadata": {
112
+ "readOnly": true
113
+ },
114
+ "code": "// Executor wraps this body as (async (args, context) => { <code> }); use args\n// directly and `return`. Calling the b2_api tool (a POST to /b2api/v4/<op>)\n// signs in to B2 for you (auth.type \"session\": b2_authorize_account, token\n// cached), calls the per-account apiUrl, and fills `accountId` into a body that\n// declares it (accountId: null) — B2 rejects it on ops that do not take it.\n// B2 native API v4 — every operation is a POST with a JSON body.\nconst b2 = async (op, body) => {\n const res = await callProvider('b2_api', 'POST', `/b2api/v4/${op}`, body || {});\n if (res && res.proposed === true) return { proposed: true, proposalId: res.proposalId || null };\n return (res && res.body) || {};\n};\nconst BACKUP_RULE = { fileNamePrefix: 'pods/', daysFromHidingToDeleting: 30, daysFromUploadingToHiding: null };\nconst isBackupRule = (r) => !!r && r.fileNamePrefix === 'pods/' && r.daysFromHidingToDeleting === 30 && (r.daysFromUploadingToHiding === null || r.daysFromUploadingToHiding === undefined);\nconst objectLockOf = (b) => {\n const fl = b && b.fileLockConfiguration;\n if (!fl || fl.isClientAuthorizedToRead !== true) return 'unreadable';\n return fl.value && fl.value.isFileLockEnabled === true ? 'on' : 'off';\n};\nconst policyOf = (b) => {\n const rules = Array.isArray(b.lifecycleRules) ? b.lifecycleRules : [];\n const podRules = rules.filter((r) => r && r.fileNamePrefix === 'pods/');\n const objectLock = objectLockOf(b);\n const check = {\n private: b.bucketType === 'allPrivate',\n lifecycleRule: podRules.length === 1 && isBackupRule(podRules[0]),\n objectLock,\n };\n return { ...check, compliant: check.private && check.lifecycleRule && objectLock === 'off' };\n};\nconst bucketName = String(args?.bucketName ?? '').trim();\nif (!/^[A-Za-z0-9][A-Za-z0-9.-]{4,61}[A-Za-z0-9]$/.test(bucketName)) {\n throw new Error('bucketName is required: 6-63 letters, digits, hyphens or periods');\n}\nconst listed = await b2('b2_list_buckets', { accountId: null, bucketName });\nconst bucket = (Array.isArray(listed.buckets) ? listed.buckets : []).find((b) => b && b.bucketName === bucketName) || null;\nif (!bucket) return { found: false, bucketName };\nreturn {\n found: true,\n bucketName,\n bucketId: bucket.bucketId,\n bucketType: bucket.bucketType,\n // B2 keeps every version of a file by default; there is no versioning switch.\n // Old versions are only removed by a lifecycle rule.\n versioning: 'keep-all-versions (B2 default; lifecycle rules decide purging)',\n lifecycleRules: Array.isArray(bucket.lifecycleRules) ? bucket.lifecycleRules : [],\n objectLock: objectLockOf(bucket),\n fileLockConfiguration: bucket.fileLockConfiguration ?? null,\n defaultServerSideEncryption: bucket.defaultServerSideEncryption ?? null,\n options: bucket.options ?? [],\n revision: bucket.revision ?? null,\n backupPolicy: { expected: { bucketType: 'allPrivate', lifecycleRule: BACKUP_RULE, objectLock: 'off' }, ...policyOf(bucket) },\n};\n"
115
+ },
116
+ {
117
+ "name": "b2_list_keys",
118
+ "intent": "list_records",
119
+ "kind": "code",
120
+ "scope": "pod",
121
+ "description": "Lists the account's application keys — id, name, capabilities, bucket and namePrefix restrictions, expiry. Never returns a key's secret (B2 does not). Use it to audit that each pod key is scoped to pods/<podId>/ and has no deleteFiles. Read-only. Args: { maxKeyCount?: number (default 100, max 1000), startApplicationKeyId?: string (next page) }.",
122
+ "requires": [
123
+ "b2_api"
124
+ ],
125
+ "executionMode": "sync",
126
+ "timeoutSeconds": 30,
127
+ "parameters": {
128
+ "maxKeyCount": "number?",
129
+ "startApplicationKeyId": "string?"
130
+ },
131
+ "metadata": {
132
+ "readOnly": true
133
+ },
134
+ "code": "// Executor wraps this body as (async (args, context) => { <code> }); use args\n// directly and `return`. Calling the b2_api tool (a POST to /b2api/v4/<op>)\n// signs in to B2 for you (auth.type \"session\": b2_authorize_account, token\n// cached), calls the per-account apiUrl, and fills `accountId` into a body that\n// declares it (accountId: null) — B2 rejects it on ops that do not take it.\n// B2 native API v4 — every operation is a POST with a JSON body.\nconst b2 = async (op, body) => {\n const res = await callProvider('b2_api', 'POST', `/b2api/v4/${op}`, body || {});\n if (res && res.proposed === true) return { proposed: true, proposalId: res.proposalId || null };\n return (res && res.body) || {};\n};\nconst n = Number(args?.maxKeyCount);\nconst maxKeyCount = Number.isFinite(n) ? Math.max(1, Math.min(1000, Math.trunc(n))) : 100;\nconst body = { accountId: null, maxKeyCount };\nconst start = String(args?.startApplicationKeyId ?? '').trim();\nif (start) body.startApplicationKeyId = start;\nconst res = await b2('b2_list_keys', body);\n// b2_list_keys never returns a key's secret — only its id and restrictions.\nconst keys = (Array.isArray(res.keys) ? res.keys : []).map((k) => ({\n applicationKeyId: k.applicationKeyId,\n keyName: k.keyName,\n capabilities: k.capabilities ?? [],\n bucketIds: k.bucketIds ?? (k.bucketId ? [k.bucketId] : null),\n namePrefix: k.namePrefix ?? null,\n expirationTimestamp: k.expirationTimestamp ?? null,\n}));\nreturn { count: keys.length, keys, nextApplicationKeyId: res.nextApplicationKeyId ?? null };\n"
135
+ },
136
+ {
137
+ "name": "b2_storage_by_pod",
138
+ "intent": "fetch_record",
139
+ "kind": "code",
140
+ "scope": "pod",
141
+ "description": "Storage used under pods/<podId>/ in a bucket: total bytes of every stored version, version count and hide markers. Without podId, sums every pods/<id>/ folder. Read-only, but it pages through file versions (B2 Class C calls, 1000 versions per page; maxPages caps the scan and `truncated` says when it stopped early). Args: { bucketName: string, podId?: string, maxPages?: number (default 20, max 100) }.",
142
+ "requires": [
143
+ "b2_api"
144
+ ],
145
+ "executionMode": "sync",
146
+ "timeoutSeconds": 120,
147
+ "parameters": {
148
+ "bucketName": "string",
149
+ "podId": "string?",
150
+ "maxPages": "number?"
151
+ },
152
+ "metadata": {
153
+ "readOnly": true
154
+ },
155
+ "code": "// Executor wraps this body as (async (args, context) => { <code> }); use args\n// directly and `return`. Calling the b2_api tool (a POST to /b2api/v4/<op>)\n// signs in to B2 for you (auth.type \"session\": b2_authorize_account, token\n// cached), calls the per-account apiUrl, and fills `accountId` into a body that\n// declares it (accountId: null) — B2 rejects it on ops that do not take it.\n// B2 native API v4 — every operation is a POST with a JSON body.\nconst b2 = async (op, body) => {\n const res = await callProvider('b2_api', 'POST', `/b2api/v4/${op}`, body || {});\n if (res && res.proposed === true) return { proposed: true, proposalId: res.proposalId || null };\n return (res && res.body) || {};\n};\nconst podIdOf = (v) => {\n const id = String(v ?? '').trim();\n if (!/^[A-Za-z0-9-]{1,64}$/.test(id)) throw new Error('podId must be letters, digits or hyphens (a pod id), never a path');\n return id;\n};\nconst bucketName = String(args?.bucketName ?? '').trim();\nif (!/^[A-Za-z0-9][A-Za-z0-9.-]{4,61}[A-Za-z0-9]$/.test(bucketName)) {\n throw new Error('bucketName is required: 6-63 letters, digits, hyphens or periods');\n}\nconst listed = await b2('b2_list_buckets', { accountId: null, bucketName });\nconst bucket = (Array.isArray(listed.buckets) ? listed.buckets : []).find((b) => b && b.bucketName === bucketName) || null;\nif (!bucket) throw new Error(`bucket \"${bucketName}\" not found`);\nconst m = Number(args?.maxPages);\nlet budget = Number.isFinite(m) ? Math.max(1, Math.min(100, Math.trunc(m))) : 20;\nconst sumPrefix = async (prefix) => {\n let bytes = 0, versions = 0, hidden = 0, startFileName = null, startFileId = null, truncated = false;\n for (;;) {\n if (budget <= 0) { truncated = true; break; }\n budget -= 1;\n const body = { bucketId: bucket.bucketId, prefix, maxFileCount: 1000 };\n if (startFileName) { body.startFileName = startFileName; if (startFileId) body.startFileId = startFileId; }\n const page = await b2('b2_list_file_versions', body);\n for (const f of (Array.isArray(page.files) ? page.files : [])) {\n if (f.action === 'upload') { bytes += Number(f.contentLength) || 0; versions += 1; }\n else if (f.action === 'hide') hidden += 1;\n }\n if (!page.nextFileName) break;\n startFileName = page.nextFileName;\n startFileId = page.nextFileId || null;\n }\n return { prefix, bytes, versions, hiddenMarkers: hidden, truncated };\n};\nlet podIds;\nif (args?.podId) {\n podIds = [podIdOf(args.podId)];\n} else {\n podIds = [];\n let startFileName = null;\n for (;;) {\n budget -= 1;\n const body = { bucketId: bucket.bucketId, prefix: 'pods/', delimiter: '/', maxFileCount: 1000 };\n if (startFileName) body.startFileName = startFileName;\n const page = await b2('b2_list_file_names', body);\n for (const f of (Array.isArray(page.files) ? page.files : [])) {\n const mm = /^pods\\/([^/]+)\\/$/.exec(String(f.fileName || ''));\n if (f.action === 'folder' && mm) podIds.push(mm[1]);\n }\n if (!page.nextFileName || budget <= 0) break;\n startFileName = page.nextFileName;\n }\n}\nconst pods = [];\nfor (const id of podIds) pods.push({ podId: id, ...(await sumPrefix(`pods/${id}/`)) });\nconst totalBytes = pods.reduce((a, p) => a + p.bytes, 0);\nreturn { bucketName, pods, totalBytes, truncated: pods.some((p) => p.truncated) };\n"
156
+ },
157
+ {
158
+ "name": "b2_backup_freshness",
159
+ "intent": "fetch_record",
160
+ "kind": "code",
161
+ "scope": "pod",
162
+ "description": "Are this pod's backups present and recent? Lists the restic snapshot files under pods/<podId>/snapshots/ and reports the newest upload time, its age in hours, and recent = age <= maxAgeHours. Read-only. Args: { bucketName: string, podId: string, maxAgeHours?: number (default 26) }.",
163
+ "requires": [
164
+ "b2_api"
165
+ ],
166
+ "executionMode": "sync",
167
+ "timeoutSeconds": 60,
168
+ "parameters": {
169
+ "bucketName": "string",
170
+ "podId": "string",
171
+ "maxAgeHours": "number?"
172
+ },
173
+ "metadata": {
174
+ "readOnly": true
175
+ },
176
+ "code": "// Executor wraps this body as (async (args, context) => { <code> }); use args\n// directly and `return`. Calling the b2_api tool (a POST to /b2api/v4/<op>)\n// signs in to B2 for you (auth.type \"session\": b2_authorize_account, token\n// cached), calls the per-account apiUrl, and fills `accountId` into a body that\n// declares it (accountId: null) — B2 rejects it on ops that do not take it.\n// B2 native API v4 — every operation is a POST with a JSON body.\nconst b2 = async (op, body) => {\n const res = await callProvider('b2_api', 'POST', `/b2api/v4/${op}`, body || {});\n if (res && res.proposed === true) return { proposed: true, proposalId: res.proposalId || null };\n return (res && res.body) || {};\n};\nconst podIdOf = (v) => {\n const id = String(v ?? '').trim();\n if (!/^[A-Za-z0-9-]{1,64}$/.test(id)) throw new Error('podId must be letters, digits or hyphens (a pod id), never a path');\n return id;\n};\nconst bucketName = String(args?.bucketName ?? '').trim();\nif (!/^[A-Za-z0-9][A-Za-z0-9.-]{4,61}[A-Za-z0-9]$/.test(bucketName)) {\n throw new Error('bucketName is required: 6-63 letters, digits, hyphens or periods');\n}\nconst listed = await b2('b2_list_buckets', { accountId: null, bucketName });\nconst bucket = (Array.isArray(listed.buckets) ? listed.buckets : []).find((b) => b && b.bucketName === bucketName) || null;\nif (!bucket) throw new Error(`bucket \"${bucketName}\" not found`);\nconst podId = podIdOf(args?.podId);\nconst h = Number(args?.maxAgeHours);\nconst maxAgeHours = Number.isFinite(h) && h > 0 ? h : 26;\n// restic writes one file per snapshot under <repo>/snapshots/; the pod's repo is\n// b2:<bucket>:pods/<podId> (CP backup vault), so this lists ONLY snapshot\n// files — never the pack data. b2_list_file_names returns the latest visible\n// version of each name (hidden files are excluded).\nconst prefix = `pods/${podId}/snapshots/`;\nlet newest = null, count = 0, startFileName = null, pages = 0, truncated = false;\nfor (;;) {\n if (pages >= 10) { truncated = true; break; }\n pages += 1;\n const body = { bucketId: bucket.bucketId, prefix, maxFileCount: 1000 };\n if (startFileName) body.startFileName = startFileName;\n const page = await b2('b2_list_file_names', body);\n for (const f of (Array.isArray(page.files) ? page.files : [])) {\n if (f.action !== 'upload') continue;\n count += 1;\n if (!newest || Number(f.uploadTimestamp) > Number(newest.uploadTimestamp)) newest = f;\n }\n if (!page.nextFileName) break;\n startFileName = page.nextFileName;\n}\nconst newestAt = newest ? new Date(Number(newest.uploadTimestamp)).toISOString() : null;\nconst ageHours = newest ? Math.round(((Date.now() - Number(newest.uploadTimestamp)) / 3600000) * 10) / 10 : null;\nreturn {\n podId, bucketName, prefix,\n present: count > 0,\n snapshotCount: count,\n newestSnapshotAt: newestAt,\n ageHours,\n maxAgeHours,\n recent: ageHours !== null && ageHours <= maxAgeHours,\n truncated,\n};\n"
177
+ },
178
+ {
179
+ "name": "b2_ensure_backup_bucket",
180
+ "intent": "manage_file",
181
+ "kind": "code",
182
+ "scope": "pod",
183
+ "description": "Creates THE backup bucket, or brings an existing one to the backup policy: private (allPrivate), B2's default keep-all-versions, ONE lifecycle rule {fileNamePrefix:\"pods/\", daysFromHidingToDeleting:30, daysFromUploadingToHiding:null} (other rules are kept), Object Lock off. Changes the B2 account, so it is NOT read-only: an agent's run waits for your approval. Refuses a bucket that already has Object Lock on (B2 cannot turn it off). Never deletes anything. Args: { bucketName: string }.",
184
+ "requires": [
185
+ "b2_api"
186
+ ],
187
+ "executionMode": "sync",
188
+ "timeoutSeconds": 60,
189
+ "parameters": {
190
+ "bucketName": "string"
191
+ },
192
+ "metadata": {
193
+ "verbType": "write"
194
+ },
195
+ "code": "// Executor wraps this body as (async (args, context) => { <code> }); use args\n// directly and `return`. Calling the b2_api tool (a POST to /b2api/v4/<op>)\n// signs in to B2 for you (auth.type \"session\": b2_authorize_account, token\n// cached), calls the per-account apiUrl, and fills `accountId` into a body that\n// declares it (accountId: null) — B2 rejects it on ops that do not take it.\n// B2 native API v4 — every operation is a POST with a JSON body.\nconst b2 = async (op, body) => {\n const res = await callProvider('b2_api', 'POST', `/b2api/v4/${op}`, body || {});\n if (res && res.proposed === true) return { proposed: true, proposalId: res.proposalId || null };\n return (res && res.body) || {};\n};\nconst BACKUP_RULE = { fileNamePrefix: 'pods/', daysFromHidingToDeleting: 30, daysFromUploadingToHiding: null };\nconst isBackupRule = (r) => !!r && r.fileNamePrefix === 'pods/' && r.daysFromHidingToDeleting === 30 && (r.daysFromUploadingToHiding === null || r.daysFromUploadingToHiding === undefined);\nconst objectLockOf = (b) => {\n const fl = b && b.fileLockConfiguration;\n if (!fl || fl.isClientAuthorizedToRead !== true) return 'unreadable';\n return fl.value && fl.value.isFileLockEnabled === true ? 'on' : 'off';\n};\nconst policyOf = (b) => {\n const rules = Array.isArray(b.lifecycleRules) ? b.lifecycleRules : [];\n const podRules = rules.filter((r) => r && r.fileNamePrefix === 'pods/');\n const objectLock = objectLockOf(b);\n const check = {\n private: b.bucketType === 'allPrivate',\n lifecycleRule: podRules.length === 1 && isBackupRule(podRules[0]),\n objectLock,\n };\n return { ...check, compliant: check.private && check.lifecycleRule && objectLock === 'off' };\n};\nconst bucketName = String(args?.bucketName ?? '').trim();\nif (!/^[A-Za-z0-9][A-Za-z0-9.-]{4,61}[A-Za-z0-9]$/.test(bucketName)) {\n throw new Error('bucketName is required: 6-63 letters, digits, hyphens or periods');\n}\nconst listed = await b2('b2_list_buckets', { accountId: null, bucketName });\nconst bucket = (Array.isArray(listed.buckets) ? listed.buckets : []).find((b) => b && b.bucketName === bucketName) || null;\nif (!bucket) {\n const created = await b2('b2_create_bucket', {\n accountId: null,\n bucketName,\n bucketType: 'allPrivate',\n lifecycleRules: [BACKUP_RULE],\n fileLockEnabled: false,\n });\n if (created.proposed) return { proposed: true, proposalId: created.proposalId, bucketName };\n return { action: 'created', bucketName, bucketId: created.bucketId ?? null, backupPolicy: policyOf(created) };\n}\nif (objectLockOf(bucket) === 'on') {\n throw new Error(`bucket \"${bucketName}\" has Object Lock enabled, and B2 cannot turn it off. Restic cannot remove its own lock files under Object Lock: use a new bucket without it.`);\n}\nconst before = policyOf(bucket);\nif (before.private && before.lifecycleRule) {\n return { action: 'unchanged', bucketName, bucketId: bucket.bucketId, backupPolicy: before };\n}\n// Keep every rule that is not about pods/; replace the pods/ rule(s) with THE rule.\nconst others = (Array.isArray(bucket.lifecycleRules) ? bucket.lifecycleRules : []).filter((r) => !r || r.fileNamePrefix !== 'pods/');\nconst body = { accountId: null, bucketId: bucket.bucketId, bucketType: 'allPrivate', lifecycleRules: [...others, BACKUP_RULE] };\nif (typeof bucket.revision === 'number') body.ifRevisionIs = bucket.revision;\nconst updated = await b2('b2_update_bucket', body);\nif (updated.proposed) return { proposed: true, proposalId: updated.proposalId, bucketName };\nreturn { action: 'updated', bucketName, bucketId: bucket.bucketId, before, backupPolicy: policyOf(updated), keptRules: others };\n"
196
+ }
197
+ ],
198
+ "provides": [
199
+ "list_records",
200
+ "fetch_record",
201
+ "manage_file"
202
+ ]
203
+ },
204
+ "dependencies": []
205
+ }
@@ -0,0 +1,266 @@
1
+ {
2
+ "apiVersion": "synap/v1",
3
+ "kind": "capability",
4
+ "meta": {
5
+ "slug": "cal-com",
6
+ "name": "Cal.com Scheduling",
7
+ "description": "Check availability, list event types and bookings, and create or cancel Cal.com bookings from Synap. New bookings and cancellations follow your approval rules. Needs a Cal.com API key.",
8
+ "tags": [
9
+ "capability"
10
+ ],
11
+ "isPublic": true,
12
+ "syncByDefault": true
13
+ },
14
+ "spec": {
15
+ "key": "cal-com",
16
+ "name": "Cal.com Scheduling",
17
+ "description": "Scheduling via the Cal.com API v2 (base https://api.cal.com/v2) using API-key auth. Stores the Cal.com API key in the vault, registers one HTTP API tool that authenticates with `Authorization: Bearer <cal_live_key>`, and seeds read + write scheduling skills. Reads (cal_list_bookings, cal_get_slots, cal_list_event_types, cal_me) are `declarative` skills that return data inline; writes (cal_create_booking, cal_cancel_booking) are proposal-gated `code` skills that route through the capability-execution gate. GOTCHA: every Cal.com v2 endpoint requires a per-endpoint `cal-api-version` header (e.g. /bookings + /event-types + /me => 2024-08-13, /slots => 2024-09-04); provider skills set it via providerSpec.headers and code skills pass it via callProvider's opts.headers. Rides on the shipped `vault://` executor: the applier remaps the tool's template-local vault ref to a runtime `vault://<id>`, and the dispatcher resolves the tool by NAME server-side and injects the vaulted key. PRECONDITION: a Cal.com account with an API key (Settings -> Developer -> API Keys, a `cal_live_...` token). Pass it as `calApiKey`.",
18
+ "params": [
19
+ {
20
+ "name": "calApiKey",
21
+ "label": "Cal.com API key",
22
+ "type": "text",
23
+ "required": true,
24
+ "description": "The Cal.com API key (Settings -> Developer -> API Keys, a `cal_live_...` token). Stored server-encrypted in the vault; never persisted in the template."
25
+ }
26
+ ],
27
+ "vault": [
28
+ {
29
+ "ref": "calApiKeySecret",
30
+ "name": "{{name}} Cal.com API key",
31
+ "value": "{{calApiKey}}",
32
+ "type": "api_key",
33
+ "description": "Cal.com API v2 key for the cal_com connector."
34
+ }
35
+ ],
36
+ "tools": [
37
+ {
38
+ "name": "cal_com",
39
+ "kind": "api",
40
+ "description": "Authenticated HTTP access to the Cal.com API v2. The applier injects the vaulted API key into the Authorization header as `Bearer <key>`; its credentialRef is remapped to the runtime vault://<id> at apply time. NOTE: each v2 endpoint also needs a per-endpoint `cal-api-version` header, supplied per-skill (providerSpec.headers for reads, callProvider opts.headers for writes).",
41
+ "credentialRef": "calApiKeySecret",
42
+ "executor": "is-agent",
43
+ "config": {
44
+ "baseUrl": "https://api.cal.com/v2",
45
+ "auth": {
46
+ "in": "header",
47
+ "name": "Authorization",
48
+ "prefix": "Bearer "
49
+ }
50
+ }
51
+ }
52
+ ],
53
+ "skills": [
54
+ {
55
+ "name": "cal_list_bookings",
56
+ "intent": "list_records",
57
+ "kind": "declarative",
58
+ "scope": "pod",
59
+ "description": "Lists upcoming Cal.com bookings (uid, title, start, end, status, location, meeting/video URL, attendees). No args.",
60
+ "requires": [
61
+ "cal_com"
62
+ ],
63
+ "executionMode": "sync",
64
+ "timeoutSeconds": 60,
65
+ "parameters": {},
66
+ "providerSpec": {
67
+ "tool": "cal_com",
68
+ "method": "GET",
69
+ "pathTemplate": "/bookings",
70
+ "headers": {
71
+ "cal-api-version": "2024-08-13"
72
+ },
73
+ "query": {
74
+ "status": "upcoming"
75
+ },
76
+ "responseShape": {
77
+ "collectionPath": "data",
78
+ "collectionAs": "bookings",
79
+ "item": {
80
+ "id": "id",
81
+ "uid": "uid",
82
+ "title": "title",
83
+ "start": "start",
84
+ "end": "end",
85
+ "status": "status",
86
+ "location": "location",
87
+ "meetingUrl": "meetingUrl",
88
+ "attendees": "attendees"
89
+ },
90
+ "scalar": {
91
+ "count": "@count"
92
+ }
93
+ }
94
+ }
95
+ },
96
+ {
97
+ "name": "cal_get_booking",
98
+ "intent": "fetch_record",
99
+ "kind": "declarative",
100
+ "scope": "pod",
101
+ "description": "Fetches full detail for ONE Cal.com booking by uid — attendees, start/end, status, location, meeting/video URL, and the booking-form responses. Args: { uid: string }.",
102
+ "requires": [
103
+ "cal_com"
104
+ ],
105
+ "executionMode": "sync",
106
+ "timeoutSeconds": 60,
107
+ "parameters": {
108
+ "uid": "string"
109
+ },
110
+ "providerSpec": {
111
+ "tool": "cal_com",
112
+ "method": "GET",
113
+ "pathTemplate": "/bookings/{{uid}}",
114
+ "headers": {
115
+ "cal-api-version": "2024-08-13"
116
+ },
117
+ "paramMapping": {
118
+ "uid": {
119
+ "encode": "uri"
120
+ }
121
+ },
122
+ "responseShape": {
123
+ "scalar": {
124
+ "id": "data.id",
125
+ "uid": "data.uid",
126
+ "title": "data.title",
127
+ "start": "data.start",
128
+ "end": "data.end",
129
+ "status": "data.status",
130
+ "location": "data.location",
131
+ "meetingUrl": "data.meetingUrl",
132
+ "attendees": "data.attendees",
133
+ "responses": "data.responses"
134
+ }
135
+ }
136
+ }
137
+ },
138
+ {
139
+ "name": "cal_get_slots",
140
+ "intent": "list_records",
141
+ "kind": "declarative",
142
+ "scope": "pod",
143
+ "description": "Returns available booking slots for an event type within a date range. Args: { eventTypeId: number|string, start: string (YYYY-MM-DD or RFC3339), end: string, timeZone?: string (default 'UTC') }.",
144
+ "requires": [
145
+ "cal_com"
146
+ ],
147
+ "executionMode": "sync",
148
+ "timeoutSeconds": 60,
149
+ "parameters": {
150
+ "eventTypeId": "string",
151
+ "start": "string",
152
+ "end": "string",
153
+ "timeZone": "string"
154
+ },
155
+ "providerSpec": {
156
+ "tool": "cal_com",
157
+ "method": "GET",
158
+ "pathTemplate": "/slots",
159
+ "headers": {
160
+ "cal-api-version": "2024-09-04"
161
+ },
162
+ "query": {
163
+ "eventTypeId": "{{eventTypeId}}",
164
+ "start": "{{start}}",
165
+ "end": "{{end}}",
166
+ "timeZone": "{{timeZone}}"
167
+ },
168
+ "paramMapping": {
169
+ "timeZone": {
170
+ "default": "UTC"
171
+ }
172
+ }
173
+ }
174
+ },
175
+ {
176
+ "name": "cal_list_event_types",
177
+ "intent": "list_records",
178
+ "kind": "declarative",
179
+ "scope": "pod",
180
+ "description": "Lists the connected user's Cal.com event types. No args.",
181
+ "requires": [
182
+ "cal_com"
183
+ ],
184
+ "executionMode": "sync",
185
+ "timeoutSeconds": 60,
186
+ "parameters": {},
187
+ "providerSpec": {
188
+ "tool": "cal_com",
189
+ "method": "GET",
190
+ "pathTemplate": "/event-types",
191
+ "headers": {
192
+ "cal-api-version": "2024-08-13"
193
+ },
194
+ "responseShape": {
195
+ "collectionPath": "data",
196
+ "collectionAs": "eventTypes"
197
+ }
198
+ }
199
+ },
200
+ {
201
+ "name": "cal_me",
202
+ "intent": "fetch_record",
203
+ "kind": "declarative",
204
+ "scope": "pod",
205
+ "description": "Returns the authenticated Cal.com user profile. No args.",
206
+ "requires": [
207
+ "cal_com"
208
+ ],
209
+ "executionMode": "sync",
210
+ "timeoutSeconds": 60,
211
+ "parameters": {},
212
+ "providerSpec": {
213
+ "tool": "cal_com",
214
+ "method": "GET",
215
+ "pathTemplate": "/me",
216
+ "headers": {
217
+ "cal-api-version": "2024-08-13"
218
+ }
219
+ }
220
+ },
221
+ {
222
+ "name": "cal_create_booking",
223
+ "intent": "schedule_event",
224
+ "kind": "code",
225
+ "scope": "pod",
226
+ "description": "Creates a Cal.com booking and notes it as an event entity (proposal-gated). Args: { start: string (RFC3339), eventTypeId: number|string, name: string, email: string, timeZone?: string (default 'UTC') }.",
227
+ "requires": [
228
+ "cal_com"
229
+ ],
230
+ "executionMode": "async",
231
+ "timeoutSeconds": 60,
232
+ "parameters": {
233
+ "start": "string",
234
+ "eventTypeId": "string",
235
+ "name": "string",
236
+ "email": "string",
237
+ "timeZone": "string?"
238
+ },
239
+ "code": "// Create a Cal.com booking (side-effecting write). Executor wraps this body as\n// (async (args, context) => { <code> }); use args directly and `return`.\n// callProvider(tool, method, path, body?, opts?) resolves the cal_com tool by\n// NAME, injects the vaulted key as Authorization: Bearer <key>, and uses the\n// tool's config.baseUrl = https://api.cal.com/v2. Cal.com v2 requires a\n// per-endpoint version header — passed via opts.headers ('cal-api-version').\nconst start = String(args?.start ?? '').trim();\nif (!start) throw new Error('cal_create_booking: start is required (RFC3339, e.g. 2026-07-01T15:00:00Z)');\nconst eventTypeRaw = args?.eventTypeId;\nif (eventTypeRaw === undefined || eventTypeRaw === null || String(eventTypeRaw).trim() === '') throw new Error('cal_create_booking: eventTypeId is required');\nconst eventTypeId = Number(eventTypeRaw);\nif (!Number.isFinite(eventTypeId)) throw new Error('cal_create_booking: eventTypeId must be numeric');\nconst name = String(args?.name ?? '').trim();\nif (!name) throw new Error('cal_create_booking: name (attendee) is required');\nconst email = String(args?.email ?? '').trim();\nif (!email) throw new Error('cal_create_booking: email (attendee) is required');\nconst timeZone = String(args?.timeZone ?? 'UTC').trim() || 'UTC';\n\nconst payload = { start, eventTypeId, attendee: { name, timeZone, email } };\nconst res = await callProvider('cal_com', 'POST', '/bookings', payload, { headers: { 'cal-api-version': '2024-08-13' } });\n// The capability-execution gate may return a PROPOSAL envelope ({ proposed,\n// proposalId }) instead of executing — that proposal IS the record; return early\n// WITHOUT also filing an event (no double-proposal).\nif (res?.proposed === true) {\n return { success: false, proposed: true, proposalId: res?.proposalId ?? null, start, email };\n}\nconst ok = typeof res?.status === 'number' && res.status >= 200 && res.status < 300;\nconst body = res?.body ?? res;\nconst data = body?.data ?? body ?? {};\nconst uid = data?.uid ?? null;\nconst title = data?.title ?? ('Booking with ' + name);\nconst startDate = data?.start ?? start;\nconst endDate = data?.end ?? null;\nconst calendarLink = data?.meetingUrl ?? data?.location ?? null;\n\n// Shape the event to match event-sync's normalizeEntity (startDate/endDate/\n// calendarLink), so the booking mirrors to Discord like any other event.\nawait propose.entity({\n profileSlug: 'event',\n title: title,\n properties: { startDate, endDate, calendarLink, provenance: 'connector', provider: 'cal.com', calBookingUid: uid },\n reasoning: 'Recorded the Cal.com booking for the activity timeline.'\n});\nreturn { success: ok, proposed: false, uid };"
240
+ },
241
+ {
242
+ "name": "cal_cancel_booking",
243
+ "intent": "schedule_event",
244
+ "kind": "code",
245
+ "scope": "pod",
246
+ "description": "Cancels a Cal.com booking by uid and notes the cancellation (proposal-gated). Args: { uid: string, cancellationReason?: string }.",
247
+ "requires": [
248
+ "cal_com"
249
+ ],
250
+ "executionMode": "async",
251
+ "timeoutSeconds": 60,
252
+ "parameters": {
253
+ "uid": "string",
254
+ "cancellationReason": "string"
255
+ },
256
+ "code": "// Cancel a Cal.com booking (side-effecting write). callProvider resolves the\n// cal_com tool by NAME and injects the vaulted key; opts.headers carries the\n// required per-endpoint version header.\nconst uid = String(args?.uid ?? '').trim();\nif (!uid) throw new Error('cal_cancel_booking: uid is required');\nconst cancellationReason = String(args?.cancellationReason ?? '').trim();\n\nconst res = await callProvider('cal_com', 'POST', '/bookings/' + encodeURIComponent(uid) + '/cancel', { cancellationReason }, { headers: { 'cal-api-version': '2024-08-13' } });\nif (res?.proposed === true) {\n return { success: false, proposed: true, proposalId: res?.proposalId ?? null, uid };\n}\nconst ok = typeof res?.status === 'number' && res.status >= 200 && res.status < 300;\n\nawait propose.entity({\n profileSlug: 'event',\n title: 'Cal.com booking ' + (ok ? 'cancelled' : 'cancel attempted') + ': ' + uid,\n properties: { action: 'cal_cancel_booking', calBookingUid: uid, cancellationReason, provenance: 'connector', provider: 'cal.com', success: ok },\n reasoning: 'Recorded the Cal.com booking cancellation for the activity timeline.'\n});\nreturn { success: ok, proposed: false, uid };"
257
+ }
258
+ ],
259
+ "provides": [
260
+ "list_records",
261
+ "fetch_record",
262
+ "schedule_event"
263
+ ]
264
+ },
265
+ "dependencies": []
266
+ }