@synap-core/workspace-templates 0.11.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 (249) 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 +189 -900
  57. package/dist/index.js +24654 -21639
  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 +41 -27
  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.template.test.ts +25 -19
  139. package/src/base.yaml +95 -39
  140. package/src/blockchain-ecosystem.yaml +1 -0
  141. package/src/brand-library.yaml +525 -14
  142. package/src/builder-dev-fold.template.test.ts +113 -0
  143. package/src/builder.yaml +798 -92
  144. package/src/business-model-catalog-description.test.ts +47 -0
  145. package/src/business-model-grp.template.test.ts +214 -0
  146. package/src/business-model.yaml +598 -0
  147. package/src/catalog-safety.test.ts +198 -0
  148. package/src/catalog.template-items.test.ts +75 -0
  149. package/src/catalog.ts +85 -2
  150. package/src/civic-france.yaml +102 -0
  151. package/src/concept-guard.test.ts +28 -8
  152. package/src/concept-guard.ts +2 -2
  153. package/src/content-creation.template.test.ts +52 -0
  154. package/src/content-creation.yaml +192 -0
  155. package/src/content-os.edit-loop.template.test.ts +210 -0
  156. package/src/content-os.publish-due-posts.template.test.ts +261 -0
  157. package/src/content-os.template.test.ts +322 -0
  158. package/src/content-os.video-takes.template.test.ts +338 -0
  159. package/src/content-os.work-a-take.template.test.ts +125 -0
  160. package/src/content-os.yaml +1830 -39
  161. package/src/content-studio-retired.template.test.ts +82 -0
  162. package/src/corpus-flow-validity.test.ts +4 -5
  163. package/src/crm-absorbs-outreach.template.test.ts +117 -0
  164. package/src/crm-build-plan.template.test.ts +128 -0
  165. package/src/crm-lists.template.test.ts +96 -0
  166. package/src/crm-messages-cadence.template.test.ts +164 -0
  167. package/src/crm-one-send-door.template.test.ts +65 -0
  168. package/src/crm.yaml +1456 -181
  169. package/src/decisions-wave.template.test.ts +186 -0
  170. package/src/define.ts +130 -17
  171. package/src/design-to-implementation.template.test.ts +94 -0
  172. package/src/dist-freshness.test.ts +97 -0
  173. package/src/domain-template-type.test.ts +75 -0
  174. package/src/ecosystem.yaml +27 -66
  175. package/src/enterprise-os.yaml +10 -134
  176. package/src/facecam-edit.audio-studio.test.ts +188 -0
  177. package/src/fal-generate-image.test.ts +200 -0
  178. package/src/fal-media-lab-pod-files.test.ts +180 -0
  179. package/src/finance.yaml +254 -1
  180. package/src/foundation.yaml +130 -28
  181. package/src/frame-host-modules.d.mts +6 -0
  182. package/src/frame-host-modules.mjs +58 -0
  183. package/src/generate.mjs +9 -3
  184. package/src/github-merge.test.ts +210 -0
  185. package/src/grants.yaml +57 -17
  186. package/src/hr.yaml +39 -6
  187. package/src/index.ts +36 -1
  188. package/src/intent.test.ts +161 -0
  189. package/src/internal-runbook.yaml +19 -1
  190. package/src/jev.test.ts +356 -0
  191. package/src/legal.yaml +33 -1
  192. package/src/life-os.yaml +65 -240
  193. package/src/lifecycle-coverage.test.ts +124 -0
  194. package/src/lifecycle-declarations.test.ts +252 -0
  195. package/src/lifecycle-presets.test.ts +468 -0
  196. package/src/lifecycle-presets.ts +494 -0
  197. package/src/lifecycle-stuck.test.ts +46 -0
  198. package/src/lifecycle-waits.parity.test.ts +319 -0
  199. package/src/marketing.yaml +108 -4
  200. package/src/meetings-knowledge.yaml +18 -0
  201. package/src/networking.yaml +23 -2
  202. package/src/operations.yaml +304 -4
  203. package/src/pipeline-views.template.test.ts +88 -0
  204. package/src/playbook-goal-placeholders.test.ts +57 -0
  205. package/src/postiz-contract.test.ts +134 -0
  206. package/src/presentation-declarations.test.ts +174 -0
  207. package/src/process-contract.validate.test.ts +352 -0
  208. package/src/project-management.yaml +39 -740
  209. package/src/publish-body.d.mts +29 -0
  210. package/src/publish-body.mjs +74 -0
  211. package/src/publish-body.test.ts +74 -0
  212. package/src/publish-official-lib.test.ts +176 -0
  213. package/src/replicate-rvm.test.ts +504 -0
  214. package/src/research-base.yaml +119 -20
  215. package/src/research-decision.yaml +60 -3
  216. package/src/reserved-profile-slugs.test.ts +83 -0
  217. package/src/retired-goal-kinds.test.ts +58 -0
  218. package/src/role-reference-and-step-domain.test.ts +192 -0
  219. package/src/seed-obligation.test.ts +176 -0
  220. package/src/send-message-maps.test.ts +227 -0
  221. package/src/shared-role-ssot.test.ts +42 -1
  222. package/src/ship-a-change.template.test.ts +177 -0
  223. package/src/skills.test.ts +170 -0
  224. package/src/social.yaml +85 -27
  225. package/src/space-packages.freshness.test.ts +97 -0
  226. package/src/space-packages.generated.ts +40 -0
  227. package/src/space-packages.test.ts +125 -0
  228. package/src/space-packages.ts +73 -0
  229. package/src/start.test.ts +188 -0
  230. package/src/task-intents.test.ts +293 -0
  231. package/src/templates.ts +21639 -21522
  232. package/src/types.ts +443 -60
  233. package/src/unipile-publish-post.test.ts +162 -0
  234. package/src/validate-flow.drift.test.ts +2 -3
  235. package/src/validate-flow.ts +32 -2
  236. package/src/validate-onboarding.test.ts +59 -0
  237. package/src/validate.test.ts +156 -3
  238. package/src/validate.ts +1384 -7
  239. package/src/wire-core.ts +231 -0
  240. package/src/wire.test.ts +159 -0
  241. package/src/wire.ts +107 -0
  242. package/src/__fixtures__/report-automation-flow.json +0 -393
  243. package/src/agent-fleet.yaml +0 -1645
  244. package/src/business-developer.template.test.ts +0 -351
  245. package/src/business-developer.yaml +0 -860
  246. package/src/content-studio.yaml +0 -361
  247. package/src/dev-dashboard.yaml +0 -990
  248. package/src/outreach-comms.yaml +0 -486
  249. package/src/the-arch.yaml +0 -201
@@ -0,0 +1,158 @@
1
+ {
2
+ "apiVersion": "synap/v1",
3
+ "kind": "capability",
4
+ "meta": {
5
+ "slug": "fal-media-lab",
6
+ "name": "fal.ai — Media Lab",
7
+ "description": "Create and edit images with Nano Banana 2 and turn images into video with Seedance 2. Each output is saved as a bookmark with its prompt and model after approval. fal-hosted links can expire, so keep what you need. Needs a fal.ai API key.",
8
+ "tags": [
9
+ "capability"
10
+ ],
11
+ "isPublic": true,
12
+ "syncByDefault": true
13
+ },
14
+ "spec": {
15
+ "key": "fal-media-lab",
16
+ "name": "Media Lab — fal.ai",
17
+ "description": "A governed generative-media workbench backed by one fal.ai API key. It creates and reference-edits images with Nano Banana 2, submits Seedance 2 image-to-video jobs to fal's durable queue, and records each request or finished provider-hosted output as a proposal-gated bookmark with prompt, model, role and provenance. This is deliberately a Phase 1 connector: it does not download media into pod-owned storage, transcode derivatives, receive webhooks automatically, or provision third-party 3D providers such as Tripo. fal output URLs can expire; retain approved media in owned storage before relying on it in production.",
18
+ "params": [
19
+ {
20
+ "name": "falApiKey",
21
+ "label": "fal.ai API key",
22
+ "type": "text",
23
+ "required": true,
24
+ "description": "A fal.ai API key from https://fal.ai/dashboard/keys. It is encrypted in this pod's vault and is never included in generated media records."
25
+ }
26
+ ],
27
+ "vault": [
28
+ {
29
+ "ref": "falApiKeySecret",
30
+ "name": "{{name}} fal.ai API key",
31
+ "value": "{{falApiKey}}",
32
+ "type": "api_key",
33
+ "description": "Credential for the Media Lab's fal.run and queue.fal.run tools."
34
+ }
35
+ ],
36
+ "tools": [
37
+ {
38
+ "name": "fal_media_run",
39
+ "kind": "api",
40
+ "description": "Authenticated access to fal.ai's synchronous model API. Use for Nano Banana 2 image generation and reference-image editing.",
41
+ "credentialRef": "falApiKeySecret",
42
+ "executor": "is-agent",
43
+ "config": {
44
+ "baseUrl": "https://fal.run",
45
+ "auth": {
46
+ "in": "header",
47
+ "name": "Authorization",
48
+ "prefix": "Key "
49
+ }
50
+ }
51
+ },
52
+ {
53
+ "name": "fal_media_queue",
54
+ "kind": "api",
55
+ "description": "Authenticated access to fal.ai's persistent queue API. Use for asynchronous Seedance 2 video submission, status and result retrieval.",
56
+ "credentialRef": "falApiKeySecret",
57
+ "executor": "is-agent",
58
+ "config": {
59
+ "baseUrl": "https://queue.fal.run",
60
+ "auth": {
61
+ "in": "header",
62
+ "name": "Authorization",
63
+ "prefix": "Key "
64
+ }
65
+ }
66
+ }
67
+ ],
68
+ "skills": [
69
+ {
70
+ "name": "fal_generate_art_directed_image",
71
+ "intent": "generate_media",
72
+ "kind": "code",
73
+ "scope": "pod",
74
+ "description": "Generate an art-directed still with fal.ai Nano Banana 2, then propose the provider-hosted output as a media bookmark. Args: { prompt: string, aspectRatio?: string, resolution?: string, role?: string }. Use a role such as hero, transition, background, texture or reference so the record remains useful later.",
75
+ "requires": [
76
+ "fal_media_run"
77
+ ],
78
+ "executionMode": "async",
79
+ "timeoutSeconds": 120,
80
+ "parameters": {
81
+ "prompt": "string",
82
+ "aspectRatio": "string?",
83
+ "resolution": "string?",
84
+ "role": "string?"
85
+ },
86
+ "code": "const prompt = String(args?.prompt ?? '').trim();\nif (!prompt) throw new Error('fal_generate_art_directed_image: prompt is required');\nconst aspectRatio = String(args?.aspectRatio ?? 'auto').trim() || 'auto';\nconst resolution = String(args?.resolution ?? '1K').trim() || '1K';\nconst role = String(args?.role ?? 'reference').trim() || 'reference';\nconst res = await callProvider('fal_media_run', 'POST', '/fal-ai/nano-banana-2', { prompt, num_images: 1, aspect_ratio: aspectRatio, output_format: 'png', resolution, limit_generations: true, enable_web_search: false });\nif (res?.proposed === true) return { success: false, proposed: true, proposalId: res?.proposalId ?? null, prompt };\nconst ok = typeof res?.status === 'number' && res.status >= 200 && res.status < 300;\nconst body = res?.body ?? res;\nconst image = Array.isArray(body?.images) ? body.images[0] : null;\nconst imageUrl = image?.url ?? null;\nif (!ok || !imageUrl) throw new Error('fal_generate_art_directed_image: generation failed or returned no image (status ' + (res?.status ?? 'unknown') + ')');\nawait propose.entity({ profileSlug: 'bookmark', title: 'Media image: ' + (prompt.length > 76 ? prompt.slice(0, 73) + '...' : prompt), properties: { url: imageUrl, mediaType: 'image', mediaRole: role, prompt, model: 'fal-ai/nano-banana-2', provenance: 'connector', provider: 'fal.ai', providerHosted: true, contentType: image?.content_type ?? 'image/png' }, reasoning: 'Recorded the generated image with its prompt, role and provider provenance for review.' });\nreturn { success: true, proposed: false, imageUrl, prompt, role, model: 'fal-ai/nano-banana-2' };"
87
+ },
88
+ {
89
+ "name": "fal_edit_reference_image",
90
+ "intent": "generate_media",
91
+ "kind": "code",
92
+ "scope": "pod",
93
+ "description": "Edit one or more public reference-image URLs with Nano Banana 2, then propose the result as a media bookmark. Args: { prompt: string, imageUrls?: string, images?: pod file entity ids (signed by the pod at run time), aspectRatio?: string, resolution?: string, role?: string }. imageUrls is a comma- or newline-separated list of public image URLs; give at least one image through imageUrls or images; preserve a real reference and change one controlled variable at a time.",
94
+ "requires": [
95
+ "fal_media_run"
96
+ ],
97
+ "executionMode": "async",
98
+ "timeoutSeconds": 120,
99
+ "parameters": {
100
+ "prompt": "string",
101
+ "imageUrls": "string?",
102
+ "images": "file[]:url?",
103
+ "aspectRatio": "string?",
104
+ "resolution": "string?",
105
+ "role": "string?"
106
+ },
107
+ "code": "const prompt = String(args?.prompt ?? '').trim();\nif (!prompt) throw new Error('fal_edit_reference_image: prompt is required');\nconst imageUrls = String(args?.imageUrls ?? '').split(/[\\n,]/).map((value) => value.trim()).filter((value) => /^https?:\\/\\//.test(value));\n// `images` are POD FILES (declared \"file[]:url?\", backend #469): the caller passes\n// file entity ids and the pod swaps each for a fresh signed URL at run time (6 h\n// for this async verb). Those URLs are sent to fal and never stored or echoed.\nlet podImages = [];\nif (args?.images !== undefined && args?.images !== null) {\n if (!Array.isArray(args.images)) throw new Error('fal_edit_reference_image: images must be a list of pod file ids');\n podImages = args.images.map((u) => String(u ?? '').trim()).filter((u) => u.length > 0);\n for (const u of podImages) {\n if (u.length > 4096 || !/^https:\\/\\/[^\\s\\/?#@]+([\\/?#][^\\s]*)?$/i.test(u)) {\n throw new Error('fal_edit_reference_image: images did not arrive as signed pod file URLs; pass pod file entity ids');\n }\n }\n}\nconst references = imageUrls.concat(podImages);\nif (references.length === 0) throw new Error('fal_edit_reference_image: give at least one reference image: imageUrls (public http(s) URLs) or images (pod file ids)');\nconst aspectRatio = String(args?.aspectRatio ?? 'auto').trim() || 'auto';\nconst resolution = String(args?.resolution ?? '1K').trim() || '1K';\nconst role = String(args?.role ?? 'reference').trim() || 'reference';\nconst res = await callProvider('fal_media_run', 'POST', '/fal-ai/nano-banana-2/edit', { prompt, image_urls: references.slice(0, 8), num_images: 1, aspect_ratio: aspectRatio, output_format: 'png', resolution, limit_generations: true, enable_web_search: false });\nif (res?.proposed === true) return { success: false, proposed: true, proposalId: res?.proposalId ?? null, prompt };\nconst ok = typeof res?.status === 'number' && res.status >= 200 && res.status < 300;\nconst body = res?.body ?? res;\nconst image = Array.isArray(body?.images) ? body.images[0] : null;\nconst imageUrl = image?.url ?? null;\nif (!ok || !imageUrl) throw new Error('fal_edit_reference_image: edit failed or returned no image (status ' + (res?.status ?? 'unknown') + ')');\nawait propose.entity({ profileSlug: 'bookmark', title: 'Media edit: ' + (prompt.length > 78 ? prompt.slice(0, 75) + '...' : prompt), properties: { url: imageUrl, mediaType: 'image', mediaRole: role, prompt, model: 'fal-ai/nano-banana-2/edit', provenance: 'connector', provider: 'fal.ai', providerHosted: true, referenceUrls: imageUrls, referencePodFiles: podImages.length || null, contentType: image?.content_type ?? 'image/png' }, reasoning: 'Recorded the reference-image edit with its source URLs and prompt for review.' });\nreturn { success: true, proposed: false, imageUrl, prompt, role, referenceUrls: imageUrls, referencePodFiles: podImages.length, model: 'fal-ai/nano-banana-2/edit' };"
108
+ },
109
+ {
110
+ "name": "fal_submit_seedance_video",
111
+ "intent": "generate_media",
112
+ "kind": "code",
113
+ "scope": "pod",
114
+ "description": "Submit a Seedance 2 image-to-video job to fal.ai's durable queue and propose a pending job record. Args: { prompt: string, imageUrl?: string OR image?: a pod file entity id (the first frame; exactly one), endImageUrl?: string OR endImage?: a pod file entity id (the pod signs pod files at run time), duration?: string, aspectRatio?: string, resolution?: string, role?: string, generateAudio?: string }. Then use fal_record_seedance_video with the returned requestId once the queue completes.",
115
+ "requires": [
116
+ "fal_media_queue"
117
+ ],
118
+ "executionMode": "async",
119
+ "timeoutSeconds": 60,
120
+ "parameters": {
121
+ "prompt": "string",
122
+ "imageUrl": "string?",
123
+ "image": "file:url?",
124
+ "endImageUrl": "string?",
125
+ "endImage": "file:url?",
126
+ "duration": "string?",
127
+ "aspectRatio": "string?",
128
+ "resolution": "string?",
129
+ "role": "string?",
130
+ "generateAudio": "string?"
131
+ },
132
+ "code": "const prompt = String(args?.prompt ?? '').trim();\nconst imageUrl = String(args?.imageUrl ?? '').trim();\nif (!prompt) throw new Error('fal_submit_seedance_video: prompt is required');\n// image / endImage are POD FILES (declared \"file:url?\", backend #469): the pod\n// swaps each file entity id for a fresh signed URL at run time (6 h for this\n// async verb, so the queue can fetch it later). Never stored or echoed.\nconst SIGNED = /^https:\\/\\/[^\\s\\/?#@]+([\\/?#][^\\s]*)?$/i;\nconst podFile = (key) => {\n const v = args?.[key] === undefined || args?.[key] === null ? '' : String(args[key]).trim();\n if (v && (v.length > 4096 || !SIGNED.test(v))) throw new Error('fal_submit_seedance_video: ' + key + ' did not arrive as a signed pod file URL; pass a pod file entity id');\n return v;\n};\nconst image = podFile('image');\nconst endImage = podFile('endImage');\nif (imageUrl && image) throw new Error('fal_submit_seedance_video: pass imageUrl or image, not both');\nif (!imageUrl && !image) throw new Error('fal_submit_seedance_video: the first frame is required: imageUrl (a public http(s) URL) or image (a pod file id)');\nif (imageUrl && !/^https?:\\/\\//.test(imageUrl)) throw new Error('fal_submit_seedance_video: imageUrl must be a public http(s) URL');\nconst endImageUrl = String(args?.endImageUrl ?? '').trim();\nif (endImageUrl && endImage) throw new Error('fal_submit_seedance_video: pass endImageUrl or endImage, not both');\nif (endImageUrl && !/^https?:\\/\\//.test(endImageUrl)) throw new Error('fal_submit_seedance_video: endImageUrl must be a public http(s) URL when provided');\nconst duration = String(args?.duration ?? 'auto').trim() || 'auto';\nconst aspectRatio = String(args?.aspectRatio ?? 'auto').trim() || 'auto';\nconst resolution = String(args?.resolution ?? '720p').trim() || '720p';\nconst role = String(args?.role ?? 'transition').trim() || 'transition';\nconst generateAudio = String(args?.generateAudio ?? 'true').trim().toLowerCase() !== 'false';\nconst payload = { prompt, image_url: imageUrl || image, duration, aspect_ratio: aspectRatio, resolution, generate_audio: generateAudio, bitrate_mode: 'high' };\nif (endImageUrl || endImage) payload.end_image_url = endImageUrl || endImage;\nconst res = await callProvider('fal_media_queue', 'POST', '/bytedance/seedance-2.0/image-to-video', payload);\nif (res?.proposed === true) return { success: false, proposed: true, proposalId: res?.proposalId ?? null, prompt };\nconst ok = typeof res?.status === 'number' && res.status >= 200 && res.status < 300;\nconst body = res?.body ?? res;\nconst requestId = body?.request_id ?? null;\nconst statusUrl = body?.status_url ?? null;\nconst responseUrl = body?.response_url ?? null;\nif (!ok || !requestId) throw new Error('fal_submit_seedance_video: queue submission failed or returned no request_id (status ' + (res?.status ?? 'unknown') + ')');\nawait propose.entity({ profileSlug: 'bookmark', title: 'Media video job: ' + (prompt.length > 72 ? prompt.slice(0, 69) + '...' : prompt), properties: { url: statusUrl ?? responseUrl ?? 'https://queue.fal.run/bytedance/seedance-2.0/image-to-video/requests/' + encodeURIComponent(requestId) + '/status', mediaType: 'video-job', mediaRole: role, prompt, model: 'bytedance/seedance-2.0/image-to-video', provenance: 'connector', provider: 'fal.ai', requestId, status: 'IN_QUEUE', imageUrl: imageUrl || null, endImageUrl: endImageUrl || null, imageFromPod: !!image, endImageFromPod: !!endImage, duration, aspectRatio, resolution, generateAudio, providerHosted: true }, reasoning: 'Recorded the queued Seedance video request so its status and final result can be reviewed later.' });\nreturn { success: true, proposed: false, requestId, statusUrl, responseUrl, prompt, role, model: 'bytedance/seedance-2.0/image-to-video' };"
133
+ },
134
+ {
135
+ "name": "fal_record_seedance_video",
136
+ "intent": "capture_into_pod",
137
+ "kind": "code",
138
+ "scope": "pod",
139
+ "description": "Check a Seedance 2 queue request and, only when complete, propose its provider-hosted video as a media bookmark. Args: { requestId: string, role?: string }. If the result is still queued or running, it returns status without creating a duplicate record.",
140
+ "requires": [
141
+ "fal_media_queue"
142
+ ],
143
+ "executionMode": "async",
144
+ "timeoutSeconds": 60,
145
+ "parameters": {
146
+ "requestId": "string",
147
+ "role": "string?"
148
+ },
149
+ "code": "const requestId = String(args?.requestId ?? '').trim();\nif (!requestId) throw new Error('fal_record_seedance_video: requestId is required');\nconst role = String(args?.role ?? 'transition').trim() || 'transition';\nconst modelPath = '/bytedance/seedance-2.0/image-to-video/requests/' + encodeURIComponent(requestId);\nconst statusRes = await callProvider('fal_media_queue', 'GET', modelPath + '/status?logs=1');\nif (statusRes?.proposed === true) return { success: false, proposed: true, proposalId: statusRes?.proposalId ?? null, requestId };\nconst statusOk = typeof statusRes?.status === 'number' && statusRes.status >= 200 && statusRes.status < 300;\nconst statusBody = statusRes?.body ?? statusRes;\nif (!statusOk) throw new Error('fal_record_seedance_video: status lookup failed (status ' + (statusRes?.status ?? 'unknown') + ')');\nif (statusBody?.status !== 'COMPLETED') return { success: true, complete: false, requestId, status: statusBody?.status ?? 'UNKNOWN', queuePosition: statusBody?.queue_position ?? null, error: statusBody?.error ?? null };\nif (statusBody?.error) throw new Error('fal_record_seedance_video: fal completed with error: ' + statusBody.error);\nconst resultRes = await callProvider('fal_media_queue', 'GET', modelPath);\nconst resultOk = typeof resultRes?.status === 'number' && resultRes.status >= 200 && resultRes.status < 300;\nconst body = resultRes?.body ?? resultRes;\nconst video = body?.video ?? null;\nconst videoUrl = video?.url ?? null;\nif (!resultOk || !videoUrl) throw new Error('fal_record_seedance_video: completed request returned no video (status ' + (resultRes?.status ?? 'unknown') + ')');\nawait propose.entity({ profileSlug: 'bookmark', title: 'Media video: Seedance ' + requestId.slice(0, 8), properties: { url: videoUrl, mediaType: 'video', mediaRole: role, model: 'bytedance/seedance-2.0/image-to-video', provenance: 'connector', provider: 'fal.ai', requestId, seed: body?.seed ?? null, providerHosted: true, contentType: video?.content_type ?? 'video/mp4', fileName: video?.file_name ?? null, fileSize: video?.file_size ?? null }, reasoning: 'Recorded the completed Seedance video with its model, request ID and provider provenance for review.' });\nreturn { success: true, complete: true, proposed: false, requestId, videoUrl, role, seed: body?.seed ?? null, model: 'bytedance/seedance-2.0/image-to-video' };"
150
+ }
151
+ ],
152
+ "provides": [
153
+ "generate_media",
154
+ "capture_into_pod"
155
+ ]
156
+ },
157
+ "dependencies": []
158
+ }
@@ -0,0 +1,191 @@
1
+ {
2
+ "apiVersion": "synap/v1",
3
+ "kind": "capability",
4
+ "meta": {
5
+ "slug": "fireflies",
6
+ "name": "Fireflies.ai Meetings",
7
+ "description": "Search and read your Fireflies.ai transcripts, and import a meeting as a summary note, attendees and action-item tasks after your approval. New transcripts can also arrive automatically. Needs a Fireflies API key.",
8
+ "tags": [
9
+ "capability"
10
+ ],
11
+ "isPublic": true,
12
+ "syncByDefault": true
13
+ },
14
+ "spec": {
15
+ "key": "fireflies",
16
+ "name": "Fireflies.ai Meetings",
17
+ "description": "Import Fireflies.ai meeting recordings into Synap as an entity graph. Fireflies exposes a single GraphQL endpoint (https://api.fireflies.ai/graphql) authenticated with `Authorization: Bearer <key>`. Stores the API key in the vault, registers one HTTP API tool (vault-generic) whose baseUrl IS the GraphQL endpoint, and seeds skills: DECLARATIVE GraphQL read verbs (fireflies_search, fireflies_get_transcript, fireflies_list_recent_transcripts) and a proposal-gated code import skill. The declarative verbs use the `transport:\"graphql\"` provider-verb shape (POST { query, variables } to the /graphql path, unwrap `data`, surface `errors[]` as a provider error) — no hand-rolled payload.errors/payload.data boilerplate. The import skill stays a code skill because it orchestrates an ENTITY-GRAPH proposal (a note for the summary + a person per attendee + a task per action item) that a declarative HTTP verb cannot express. 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 as the Bearer token. PRECONDITION: a Fireflies API key (Fireflies dashboard -> Settings -> Developer Settings -> API Key). Pass it as `firefliesApiKey`. INBOUND WEBHOOK (transcripts -> channel/message substrate): Fireflies fires a single `Transcription complete` event with a minimal payload {meetingId, eventType, clientReferenceId} to POST /api/webhooks/fireflies/:token; the pod verifies it, enqueues a fetch-then-ingest job that calls fireflies_get_transcript and lands the transcript as a channel MESSAGE via recordInboundMessage (queryable by the AI). fireflies_list_recent_transcripts backs a backfill poller that recovers missed webhooks. The webhook config is provisioned at install/config time (NOT template-seeded) under `tools.metadata.fireflies.webhook = { token, secretVaultRef?, workspaceId?, ownerUserId?, seen:{} }` and backfill under `tools.metadata.fireflies.backfill = { enabled }`; the `:token` selects+authorizes the config (unknown -> 404), and when `secretVaultRef` is set the `x-hub-signature` header (SHA-256 HMAC of the raw body, `sha256=<hex>`, vault-resolved secret) is verified fail-closed.",
18
+ "params": [
19
+ {
20
+ "name": "firefliesApiKey",
21
+ "label": "Fireflies API key",
22
+ "type": "text",
23
+ "required": true,
24
+ "description": "The Fireflies.ai API key (dashboard -> Settings -> Developer Settings -> API Key). Used as the Bearer token. Stored server-encrypted in the vault; never persisted in the template."
25
+ }
26
+ ],
27
+ "vault": [
28
+ {
29
+ "ref": "firefliesApiKeySecret",
30
+ "name": "{{name}} Fireflies API key",
31
+ "value": "{{firefliesApiKey}}",
32
+ "type": "api_key",
33
+ "description": "Fireflies.ai API key (Bearer token) for the fireflies connector."
34
+ }
35
+ ],
36
+ "tools": [
37
+ {
38
+ "name": "fireflies",
39
+ "kind": "api",
40
+ "description": "Authenticated HTTP access to the Fireflies.ai GraphQL API. 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. baseUrl IS the single GraphQL endpoint — skills POST { query, variables } with an empty path.",
41
+ "credentialRef": "firefliesApiKeySecret",
42
+ "executor": "is-agent",
43
+ "config": {
44
+ "baseUrl": "https://api.fireflies.ai",
45
+ "auth": {
46
+ "in": "header",
47
+ "name": "Authorization",
48
+ "prefix": "Bearer "
49
+ }
50
+ }
51
+ }
52
+ ],
53
+ "skills": [
54
+ {
55
+ "name": "fireflies_import_last_meeting",
56
+ "intent": "capture_into_pod",
57
+ "kind": "code",
58
+ "scope": "pod",
59
+ "description": "Imports the most recent Fireflies meeting and proposes the full entity graph (proposal-gated): a note for the meeting summary, a person per attendee (deduped by email), and a task per action item. No args. Returns { success, meetingTitle, attendees, actionItems }.",
60
+ "requires": [
61
+ "fireflies"
62
+ ],
63
+ "executionMode": "async",
64
+ "timeoutSeconds": 90,
65
+ "parameters": {},
66
+ "code": "// Import the most recent Fireflies meeting and propose the entity graph.\n// Executor wraps this body as (async (args, context) => { <code> }); use args\n// directly and `return` the result. callProvider(tool, method, path, body?)\n// runs the required tool ('fireflies') through the dispatcher: it injects the\n// vaulted API key as Authorization: Bearer <key> and POSTs to the tool's\n// config.baseUrl (= https://api.fireflies.ai/graphql). Path is EMPTY because the\n// baseUrl IS the single GraphQL endpoint; the body is { query, variables }.\nconst gql = async (query, variables) => {\n const res = await callProvider('fireflies', 'POST', '/graphql', { query: query, variables: variables || {} });\n const payload = res && res.body !== undefined ? res.body : res;\n if (payload && Array.isArray(payload.errors) && payload.errors.length) {\n throw new Error('Fireflies GraphQL error: ' + String((payload.errors[0] && payload.errors[0].message) || 'unknown'));\n }\n return (payload && payload.data) || {};\n};\n\n// 1) Most recent transcript id.\nconst listData = await gql('query { transcripts(mine: true, limit: 1) { id } }');\nconst list = (listData && Array.isArray(listData.transcripts)) ? listData.transcripts : [];\nif (!list.length) return { success: false, reason: 'no transcripts found' };\nconst transcriptId = String((list[0] && list[0].id) || '');\nif (!transcriptId) return { success: false, reason: 'most recent transcript has no id' };\n\n// 2) Full transcript detail.\nconst detailData = await gql(\n 'query Transcript($id: String!) { transcript(id: $id) { id title date duration meeting_link transcript_url meeting_attendees { name email } summary { overview action_items keywords topics_discussed } } }',\n { id: transcriptId }\n);\nconst t = (detailData && detailData.transcript) || {};\nconst title = String(t.title || 'Untitled meeting');\nconst summary = (t.summary && typeof t.summary === 'object') ? t.summary : {};\nconst overview = String(summary.overview || '');\n\n// Meeting summary -> note.\nawait propose.entity({\n profileSlug: 'note',\n title: title,\n properties: {\n date: t.date != null ? String(t.date) : null,\n durationMin: t.duration != null ? Number(t.duration) : null,\n meetingLink: t.meeting_link != null ? String(t.meeting_link) : null,\n provenance: 'connector', provider: 'fireflies',\n firefliesId: String(t.id || transcriptId),\n summary: overview,\n transcriptUrl: t.transcript_url != null ? String(t.transcript_url) : null\n },\n reasoning: 'Imported meeting summary from Fireflies.'\n});\n\n// Attendees -> person (dedupe by email, then by lowercased name).\nconst attendees = Array.isArray(t.meeting_attendees) ? t.meeting_attendees : [];\nconst seen = {};\nlet attendeeCount = 0;\nfor (const a of attendees) {\n const email = (a && a.email != null) ? String(a.email).trim() : '';\n const name = (a && a.name != null) ? String(a.name).trim() : '';\n if (!email && !name) continue;\n const dedupeKey = email ? email.toLowerCase() : name.toLowerCase();\n if (seen[dedupeKey]) continue;\n seen[dedupeKey] = true;\n await propose.entity({\n profileSlug: 'person',\n title: name || email,\n properties: { email: email || null, provenance: 'connector', provider: 'fireflies' },\n reasoning: 'Attendee of the imported Fireflies meeting: ' + title\n });\n attendeeCount++;\n}\n\n// action_items is ONE markdown STRING (not an array) -> split by newline.\nconst rawActions = (summary.action_items != null) ? String(summary.action_items) : '';\nconst lines = rawActions.split('\\n').map((l) => String(l).trim()).filter((l) => l.length > 0);\nlet actionCount = 0;\nfor (const line of lines) {\n await propose.entity({\n profileSlug: 'task',\n title: line,\n properties: { provenance: 'connector', provider: 'fireflies', fromMeeting: title },\n reasoning: 'Action item extracted from the imported Fireflies meeting: ' + title\n });\n actionCount++;\n}\n\nreturn { success: true, meetingTitle: title, attendees: attendeeCount, actionItems: actionCount };\n"
67
+ },
68
+ {
69
+ "name": "fireflies_search",
70
+ "intent": "search_external",
71
+ "kind": "declarative",
72
+ "scope": "pod",
73
+ "description": "Searches recent Fireflies transcripts by keyword and returns a list of { id, title, date } (read; returns inline). Args: { query: string }.",
74
+ "requires": [
75
+ "fireflies"
76
+ ],
77
+ "executionMode": "sync",
78
+ "timeoutSeconds": 60,
79
+ "parameters": {
80
+ "query": "string"
81
+ },
82
+ "providerSpec": {
83
+ "tool": "fireflies",
84
+ "method": "POST",
85
+ "pathTemplate": "/graphql",
86
+ "transport": "graphql",
87
+ "graphql": {
88
+ "query": "query Search($keyword: String) { transcripts(mine: true, limit: 10, keyword: $keyword) { id title date } }",
89
+ "variables": {
90
+ "keyword": "{{query}}"
91
+ },
92
+ "operation": "query",
93
+ "dataPath": "data"
94
+ },
95
+ "responseShape": {
96
+ "collectionPath": "transcripts",
97
+ "collectionAs": "results",
98
+ "item": {
99
+ "id": "id",
100
+ "title": "title",
101
+ "date": "date"
102
+ },
103
+ "scalar": {
104
+ "query": "{{query}}",
105
+ "count": "@count"
106
+ }
107
+ }
108
+ }
109
+ },
110
+ {
111
+ "name": "fireflies_get_transcript",
112
+ "intent": "fetch_record",
113
+ "kind": "declarative",
114
+ "scope": "pod",
115
+ "description": "Fetches ONE Fireflies transcript by id — full transcript sentences, participants, title, date, summary. Backs the inbound-webhook ingestion worker that lands a completed meeting as a channel message. Args: { id: string }. Returns { transcript, meetingId } where `transcript` is the full object (incl. sentences[] and meeting_attendees[]).",
116
+ "requires": [
117
+ "fireflies"
118
+ ],
119
+ "executionMode": "sync",
120
+ "timeoutSeconds": 60,
121
+ "parameters": {
122
+ "id": "string"
123
+ },
124
+ "providerSpec": {
125
+ "tool": "fireflies",
126
+ "method": "POST",
127
+ "pathTemplate": "/graphql",
128
+ "transport": "graphql",
129
+ "graphql": {
130
+ "query": "query Transcript($id: String!) { transcript(id: $id) { id title date duration meeting_link transcript_url meeting_attendees { name email displayName } summary { overview action_items keywords } sentences { speaker_name text } } }",
131
+ "variables": {
132
+ "id": "{{id}}"
133
+ },
134
+ "operation": "query",
135
+ "dataPath": "data"
136
+ },
137
+ "responseShape": {
138
+ "scalar": {
139
+ "transcript": "transcript",
140
+ "meetingId": "transcript.id"
141
+ }
142
+ }
143
+ }
144
+ },
145
+ {
146
+ "name": "fireflies_list_recent_transcripts",
147
+ "intent": "list_records",
148
+ "kind": "declarative",
149
+ "scope": "pod",
150
+ "description": "Lists the most recent Fireflies transcripts (id, title, date). Backs the backfill poller that recovers meetings whose completion webhook was missed. No args. Returns { results: [{ id, title, date }], count }.",
151
+ "requires": [
152
+ "fireflies"
153
+ ],
154
+ "executionMode": "sync",
155
+ "timeoutSeconds": 60,
156
+ "parameters": {},
157
+ "providerSpec": {
158
+ "tool": "fireflies",
159
+ "method": "POST",
160
+ "pathTemplate": "/graphql",
161
+ "transport": "graphql",
162
+ "graphql": {
163
+ "query": "query Recent { transcripts(mine: true, limit: 50) { id title date } }",
164
+ "variables": {},
165
+ "operation": "query",
166
+ "dataPath": "data"
167
+ },
168
+ "responseShape": {
169
+ "collectionPath": "transcripts",
170
+ "collectionAs": "results",
171
+ "item": {
172
+ "id": "id",
173
+ "title": "title",
174
+ "date": "date"
175
+ },
176
+ "scalar": {
177
+ "count": "@count"
178
+ }
179
+ }
180
+ }
181
+ }
182
+ ],
183
+ "provides": [
184
+ "capture_into_pod",
185
+ "search_external",
186
+ "fetch_record",
187
+ "list_records"
188
+ ]
189
+ },
190
+ "dependencies": []
191
+ }
@@ -0,0 +1,114 @@
1
+ {
2
+ "apiVersion": "synap/v1",
3
+ "kind": "capability",
4
+ "meta": {
5
+ "slug": "freellmapi",
6
+ "name": "FreeLLMAPI Gateway",
7
+ "description": "Connects a self-hosted FreeLLMAPI gateway so agents can see which free-tier models are available, provider health, usage and routing. Changing the routing strategy needs your approval. Needs a running FreeLLMAPI instance and its API key.",
8
+ "tags": [
9
+ "capability"
10
+ ],
11
+ "isPublic": true,
12
+ "syncByDefault": true
13
+ },
14
+ "spec": {
15
+ "key": "freellmapi",
16
+ "name": "FreeLLMAPI — Free-Tier LLM Gateway",
17
+ "description": "Connects a self-hosted FreeLLMAPI gateway (pooled free-tier LLM quota behind one OpenAI-compatible endpoint) to this pod as a governed MCP server. Agents get live introspection — which models are available and tool-capable, per-provider health and cooldowns, usage, routing — as INLINE read tools. Changing the gateway's routing strategy is a WRITE and goes through review. PRECONDITION: a running FreeLLMAPI instance (e.g. `eve add freellmapi`) and its unified API key.",
18
+ "params": [
19
+ {
20
+ "name": "baseUrl",
21
+ "label": "Gateway URL",
22
+ "type": "text",
23
+ "required": true,
24
+ "description": "Base URL of the FreeLLMAPI gateway WITHOUT a trailing path, reachable from the intelligence service — e.g. https://llm.example.com. The MCP endpoint is <baseUrl>/mcp."
25
+ },
26
+ {
27
+ "name": "apiKey",
28
+ "label": "Unified API key",
29
+ "type": "text",
30
+ "required": true,
31
+ "description": "The gateway's unified key (freellmapi-…). Stored server-encrypted as a pod-wide shared key; used only as the MCP transport's Authorization header, never shown to agents."
32
+ }
33
+ ],
34
+ "vault": [
35
+ {
36
+ "ref": "unifiedKey",
37
+ "name": "FreeLLMAPI unified key",
38
+ "value": "{{apiKey}}",
39
+ "type": "api_key",
40
+ "podWide": true,
41
+ "description": "Service credential for the FreeLLMAPI gateway's /mcp endpoint."
42
+ }
43
+ ],
44
+ "mcpServers": [
45
+ {
46
+ "slug": "freellmapi",
47
+ "name": "FreeLLMAPI",
48
+ "description": "Free-tier LLM gateway introspection: models, provider health and cooldowns, usage, routing, cache and compression stats.",
49
+ "transport": "http",
50
+ "url": "{{baseUrl}}/mcp",
51
+ "enabled": true,
52
+ "auth": {
53
+ "credentialRef": "unifiedKey",
54
+ "header": "Authorization",
55
+ "prefix": "Bearer "
56
+ },
57
+ "toolPolicy": {
58
+ "default": "governed",
59
+ "inline": [
60
+ "list_models",
61
+ "provider_health",
62
+ "usage_summary",
63
+ "routing_info",
64
+ "cache_stats",
65
+ "compression_stats"
66
+ ]
67
+ },
68
+ "metadata": {
69
+ "provider": "freellmapi",
70
+ "docs": "https://github.com/tashfeenahmed/freellmapi",
71
+ "note": "Allowlist of READS, default governed: a write tool added upstream is gated automatically rather than silently inline."
72
+ }
73
+ }
74
+ ],
75
+ "tools": [
76
+ {
77
+ "name": "freellmapi_mcp",
78
+ "kind": "mcp",
79
+ "description": "Governed gateway to the FreeLLMAPI MCP server. The pod routes an agent's call to a non-inline tool (set_routing_strategy) through this row, where the capability gate turns it into a proposal. Path = MCP tool name; body = tool arguments.",
80
+ "credentialRef": "mcp://freellmapi",
81
+ "executor": "is-agent",
82
+ "config": {
83
+ "serverSlug": "freellmapi"
84
+ },
85
+ "metadata": {
86
+ "provider": "freellmapi",
87
+ "mcpTools": [
88
+ "list_models",
89
+ "provider_health",
90
+ "usage_summary",
91
+ "routing_info",
92
+ "cache_stats",
93
+ "compression_stats",
94
+ "set_routing_strategy"
95
+ ],
96
+ "governedTools": [
97
+ "set_routing_strategy"
98
+ ]
99
+ }
100
+ }
101
+ ],
102
+ "skills": [
103
+ {
104
+ "name": "freellmapi-gateway-advisor",
105
+ "kind": "instruction",
106
+ "scope": "pod",
107
+ "description": "When and how to use the FreeLLMAPI gateway's MCP tools. Load when a model/provider choice depends on free-tier availability, a provider looks rate-limited, or the user asks about LLM usage or cost.",
108
+ "code": "# FreeLLMAPI gateway\n\nFreeLLMAPI pools free-tier quota from many providers behind one OpenAI-compatible endpoint. On this pod it is registered as a LOW-PRIORITY fallback provider, so it serves turns only when paid providers fail.\n\n## Read tools (run immediately)\n\n| Tool | Use |\n|---|---|\n| `list_models` | Which models the router can serve, context windows, and which support tool calls |\n| `provider_health` | Per-provider key status (healthy / rate_limited / invalid), active cooldowns |\n| `usage_summary` | Requests, tokens, success rate over 24h / 7d / 30d |\n| `routing_info` | Current routing strategy and top-scored models |\n| `cache_stats` / `compression_stats` | Token savings |\n\n## Write tool (needs the user's approval)\n\n`set_routing_strategy` — one of `priority`, `balanced`, `smartest`, `fastest`, `reliable`, `custom`. Calling it files a proposal; it changes nothing until approved. Say what you proposed and why; do not retry it.\n\n## Rules\n- Check `provider_health` before blaming a model for a failure.\n- Never claim a model supports tools without `list_models` saying so.\n- Report numbers from `usage_summary` as measured, with the window."
109
+ }
110
+ ],
111
+ "provides": []
112
+ },
113
+ "dependencies": []
114
+ }
@@ -0,0 +1,92 @@
1
+ {
2
+ "apiVersion": "synap/v1",
3
+ "kind": "capability",
4
+ "meta": {
5
+ "slug": "generic-apikey",
6
+ "name": "Custom API Connector (template)",
7
+ "description": "A starting point for connecting any API that uses an API key: stores the key, registers the API and adds a skill that fetches records and proposes them as entities. Set the base URL, key and target entity type when you install.",
8
+ "tags": [
9
+ "capability"
10
+ ],
11
+ "isPublic": true,
12
+ "syncByDefault": true
13
+ },
14
+ "spec": {
15
+ "key": "generic-apikey",
16
+ "name": "Generic API-Key Connector",
17
+ "description": "Provider-neutral demonstration of the capability-template shape: stores one API key in the vault, registers one HTTP API tool that authenticates with it, and seeds one code skill that fetches from the provider and proposes the results as entities. Customize via the {{baseUrl}}, {{apiKey}}, and {{entityProfile}} params — no provider is hardcoded.",
18
+ "params": [
19
+ {
20
+ "name": "apiKey",
21
+ "label": "API key",
22
+ "type": "text",
23
+ "required": true,
24
+ "description": "The bearer API key for the provider. Stored server-encrypted in the vault; never persisted in the template."
25
+ },
26
+ {
27
+ "name": "baseUrl",
28
+ "label": "Base URL",
29
+ "type": "text",
30
+ "required": true,
31
+ "description": "The provider's API base URL, e.g. https://api.example.com/v1"
32
+ },
33
+ {
34
+ "name": "entityProfile",
35
+ "label": "Target entity profile",
36
+ "type": "text",
37
+ "required": false,
38
+ "default": "note",
39
+ "description": "The profileSlug the skill proposes fetched records as."
40
+ }
41
+ ],
42
+ "vault": [
43
+ {
44
+ "ref": "apiKeySecret",
45
+ "name": "{{name}} API key",
46
+ "value": "{{apiKey}}",
47
+ "type": "api_key",
48
+ "description": "API key for the generic-apikey connector."
49
+ }
50
+ ],
51
+ "tools": [
52
+ {
53
+ "name": "generic_api",
54
+ "kind": "api",
55
+ "description": "Authenticated HTTP access to the provider API.",
56
+ "credentialRef": "apiKeySecret",
57
+ "executor": "is-agent",
58
+ "config": {
59
+ "baseUrl": "{{baseUrl}}",
60
+ "auth": {
61
+ "in": "header",
62
+ "name": "Authorization",
63
+ "prefix": "Bearer "
64
+ }
65
+ }
66
+ }
67
+ ],
68
+ "skills": [
69
+ {
70
+ "name": "{{name}} fetch-and-propose",
71
+ "intent": "capture_into_pod",
72
+ "kind": "code",
73
+ "scope": "pod",
74
+ "description": "Fetches records from the provider API and proposes each as an entity (proposal-gated). Calls the required tool by name via callProvider('generic_api', ...).",
75
+ "requires": [
76
+ "generic_api"
77
+ ],
78
+ "executionMode": "async",
79
+ "timeoutSeconds": 60,
80
+ "parameters": {
81
+ "path": "string?",
82
+ "entityProfile": "string?"
83
+ },
84
+ "code": "// Generic fetch -> propose loop. The skill body is a STATEMENT body (no `export\n// default`); the executor wraps it as (async function(args, context){ <code> }).\n// callProvider is POSITIONAL: callProvider(provider, method, path, body?) where\n// `provider` is the required tool's NAME ('generic_api') — the dispatcher resolves\n// it to the tool's credentialRef server-side and injects the vaulted API key. No\n// providerRef arg needed. propose.entity REQUIRES `title` (not `name`) and routes\n// the write through checkPermissionOrPropose.\nconst path = args.path || '/records';\nconst profileSlug = args.entityProfile || 'note';\nconst res = await callProvider('generic_api', 'GET', path);\nconst data = res && res.body !== undefined ? res.body : res;\nconst records = Array.isArray(data)\n ? data\n : (data && Array.isArray(data.data) ? data.data : (data && data.records) || []);\nlet proposed = 0;\nfor (const record of records) {\n await propose.entity({\n profileSlug,\n title: String((record && (record.name || record.title || record.id)) || 'record'),\n properties: record,\n });\n proposed++;\n}\nreturn { proposed };\n"
85
+ }
86
+ ],
87
+ "provides": [
88
+ "capture_into_pod"
89
+ ]
90
+ },
91
+ "dependencies": []
92
+ }