@helloleo/plugins 0.2.29 → 0.2.31-beta.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 (76) hide show
  1. package/dist/cli.js +12533 -0
  2. package/dist/index.js +2135 -2033
  3. package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/bun.lock +2 -2
  4. package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/bunfig.toml +1 -1
  5. package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/package.json +1 -1
  6. package/dist/templates/tanstack-start-2026-09-29/src/lib/proxy.server.ts +204 -0
  7. package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/vite.config.ts +5 -10
  8. package/package.json +2 -2
  9. package/dist/templates/tanstack-start-2026-09-11/src/lib/proxy.ts +0 -182
  10. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/components.json +0 -0
  11. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/drizzle/meta/_journal.json +0 -0
  12. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/drizzle.config.ts +0 -0
  13. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/error-component.tsx +0 -0
  14. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/accordion.tsx +0 -0
  15. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/alert-dialog.tsx +0 -0
  16. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/alert.tsx +0 -0
  17. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/aspect-ratio.tsx +0 -0
  18. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/avatar.tsx +0 -0
  19. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/badge.tsx +0 -0
  20. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/breadcrumb.tsx +0 -0
  21. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/button.tsx +0 -0
  22. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/calendar.tsx +0 -0
  23. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/card.tsx +0 -0
  24. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/carousel.tsx +0 -0
  25. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/chart.tsx +0 -0
  26. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/checkbox.tsx +0 -0
  27. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/collapsible.tsx +0 -0
  28. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/command.tsx +0 -0
  29. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/context-menu.tsx +0 -0
  30. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/dialog.tsx +0 -0
  31. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/drawer.tsx +0 -0
  32. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/dropdown-menu.tsx +0 -0
  33. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/form.tsx +0 -0
  34. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/hover-card.tsx +0 -0
  35. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/input-otp.tsx +0 -0
  36. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/input.tsx +0 -0
  37. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/label.tsx +0 -0
  38. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/menubar.tsx +0 -0
  39. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/navigation-menu.tsx +0 -0
  40. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/pagination.tsx +0 -0
  41. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/popover.tsx +0 -0
  42. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/progress.tsx +0 -0
  43. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/radio-group.tsx +0 -0
  44. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/resizable.tsx +0 -0
  45. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/scroll-area.tsx +0 -0
  46. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/select.tsx +0 -0
  47. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/separator.tsx +0 -0
  48. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/sheet.tsx +0 -0
  49. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/sidebar.tsx +0 -0
  50. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/skeleton.tsx +0 -0
  51. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/slider.tsx +0 -0
  52. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/sonner.tsx +0 -0
  53. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/switch.tsx +0 -0
  54. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/table.tsx +0 -0
  55. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/tabs.tsx +0 -0
  56. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/textarea.tsx +0 -0
  57. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/toggle-group.tsx +0 -0
  58. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/toggle.tsx +0 -0
  59. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/tooltip.tsx +0 -0
  60. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/db/index.ts +0 -0
  61. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/db/schema.ts +0 -0
  62. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/global.d.ts +0 -0
  63. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/hooks/use-mobile.tsx +0 -0
  64. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/lib/cache.ts +0 -0
  65. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/lib/kv.ts +0 -0
  66. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/lib/storage.ts +0 -0
  67. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/lib/utils.ts +0 -0
  68. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/routeTree.gen.ts +0 -0
  69. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/router.tsx +0 -0
  70. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/routes/README.md +0 -0
  71. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/routes/__root.tsx +0 -0
  72. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/routes/api/health.ts +0 -0
  73. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/routes/index.tsx +0 -0
  74. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/server.ts +0 -0
  75. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/styles.css +0 -0
  76. /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/tsconfig.json +0 -0
@@ -62,7 +62,7 @@
62
62
  "devDependencies": {
63
63
  "@cloudflare/vite-plugin": "1.54.4",
64
64
  "@cloudflare/workers-types": "5.20260908.1",
65
- "@helloleo/vite-config": "0.1.6",
65
+ "@helloleo/vite-config": "0.1.7",
66
66
  "@libsql/client": "0.17.4",
67
67
  "@tailwindcss/typography": "0.5.20",
68
68
  "@tanstack/router-cli": "1.167.34",
@@ -206,7 +206,7 @@
206
206
 
207
207
  "@helloleo/runtime": ["@helloleo/runtime@0.1.6", "", { "dependencies": { "aws4fetch": "^1.0.20" }, "peerDependencies": { "@libsql/client": "*", "drizzle-orm": "^0.45.2" }, "optionalPeers": ["@libsql/client"] }, "sha512-oH5v5cZ0xMB8/IOg3VsJ2OpoG9Bod6D8UXjR2IGjFKAwx3vj62phit+3daJ9Wwpn3EuX4+BHs84swLfsOW3reg=="],
208
208
 
209
- "@helloleo/vite-config": ["@helloleo/vite-config@0.1.6", "", { "peerDependencies": { "@cloudflare/vite-plugin": "*", "@tailwindcss/vite": "*", "@tanstack/react-start": "*", "@tanstack/router-plugin": "*", "@vitejs/plugin-react": "*", "vite": "*" } }, "sha512-W/B7vXXo/4qTRvisDv379fkVyoaiKu2JJ+T7+iA45PPJfct1i78pZQTomEy36V2u8beS3F7ZImYogePjfCUqSw=="],
209
+ "@helloleo/vite-config": ["@helloleo/vite-config@0.1.7", "", { "peerDependencies": { "@cloudflare/vite-plugin": "*", "@tailwindcss/vite": "*", "@tanstack/react-start": "*", "@tanstack/router-plugin": "*", "@vitejs/plugin-react": "*", "vite": "*" } }, "sha512-couH35PuS5qN11tYvEJBl/ccdoP0Zec3KaUwc5vMGeihNd82k2bRqIAZGEGqAF2YJPjGr74mK/0vglCGDIT8Aw=="],
210
210
 
211
211
  "@hookform/resolvers": ["@hookform/resolvers@5.9.1", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "@sinclair/typebox": ">=0.25.24", "@standard-schema/spec": "^1.0.0", "@typeschema/main": ">=0.13.7", "@vinejs/vine": "^2.0.0 || ^3.0.0 || ^4.0.0", "ajv": "^8.12.0", "ajv-errors": "^3.0.0", "ajv-formats": "^2.1.1", "arktype": "^2.0.0", "ata-validator": "^1.2.0", "class-transformer": ">=0.4.0", "class-validator": ">=0.12.0", "computed-types": "^1.0.0", "effect": "^3.10.3", "fluentvalidation-ts": "^3.0.0", "fp-ts": "^2.7.0", "io-ts": "^2.0.0", "joi": "^17.0.0 || ^18.0.0", "nope-validator": ">=0.12.0", "react-hook-form": "^7.55.0", "superstruct": ">=0.12.0", "typanion": "^3.3.2", "valibot": ">=0.31.0 || ^1.0.0-beta.4 || ^1.0.0-rc", "vest": ">=6.0.0", "yup": "^1.0.0", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@sinclair/typebox", "@standard-schema/spec", "@typeschema/main", "@vinejs/vine", "ajv", "ajv-errors", "ajv-formats", "arktype", "ata-validator", "class-transformer", "class-validator", "computed-types", "effect", "fluentvalidation-ts", "fp-ts", "io-ts", "joi", "nope-validator", "superstruct", "typanion", "valibot", "vest", "yup", "zod"] }, "sha512-7b7vsbraJxKgjVSA1Nur9tLwj539WGJUBLA7QNvXnFoT2pM5Z7G+6rlukk4B2/QrTZy6huRtH6wKeESPKuIr6w=="],
212
212
 
@@ -8,4 +8,4 @@
8
8
  # install the runtime it was just scaffolded against.
9
9
  [install]
10
10
  minimumReleaseAge = 259200
11
- minimumReleaseAgeExcludes = ["@helloleo/runtime", "@helloleo/plugins"]
11
+ minimumReleaseAgeExcludes = ["@helloleo/runtime", "@helloleo/vite-config", "@helloleo/plugins"]
@@ -72,7 +72,7 @@
72
72
  "devDependencies": {
73
73
  "@cloudflare/vite-plugin": "1.54.4",
74
74
  "@cloudflare/workers-types": "5.20260908.1",
75
- "@helloleo/vite-config": "0.1.6",
75
+ "@helloleo/vite-config": "0.1.7",
76
76
  "@libsql/client": "0.17.4",
77
77
  "@tailwindcss/typography": "0.5.20",
78
78
  "drizzle-kit": "0.31.10",
@@ -0,0 +1,204 @@
1
+ import '@tanstack/react-start/server-only'
2
+ import { env } from '@helloleo/runtime'
3
+
4
+ /*
5
+ * HelloLeo Integration Proxy: server-side helper for connected integrations.
6
+ *
7
+ * All third-party APIs (Odoo, Airtable, HubSpot, Stripe, Notion, Gladia,
8
+ * Microsoft 365, Klaviyo, Supabase, DimoMaint, etc.) are reachable through
9
+ * this single proxy. The proxy holds the credentials the user configured in
10
+ * Project Settings -- you NEVER pass an integration's API key yourself, and you
11
+ * NEVER call third-party hosts directly.
12
+ *
13
+ * SERVER-ONLY. Every call carries this project's HELLOLEO_API_KEY, which only
14
+ * exists on the server. Importing this file from browser code fails the build
15
+ * (the import above, plus the .server.ts name). Call it from a server route
16
+ * under src/routes/api/ and fetch that route from the page with react-query:
17
+ *
18
+ * // src/routes/api/products.ts
19
+ * import { createFileRoute } from '@tanstack/react-router'
20
+ * import { callIntegration } from '#/lib/proxy.server'
21
+ *
22
+ * export const Route = createFileRoute('/api/products')({
23
+ * server: {
24
+ * handlers: {
25
+ * GET: async () => {
26
+ * const r = await callIntegration({
27
+ * integration: 'odoo',
28
+ * endpoint: '/jsonrpc',
29
+ * body: {
30
+ * model: 'product.template',
31
+ * method: 'search_read',
32
+ * args: [[['type', '=', 'consu']]],
33
+ * kwargs: { fields: ['name', 'list_price'], limit: 50 },
34
+ * },
35
+ * })
36
+ * if (!r.success) return Response.json({ error: r.data }, { status: 502 })
37
+ * return Response.json(r.data?.result ?? [])
38
+ * },
39
+ * },
40
+ * },
41
+ * })
42
+ *
43
+ * One route per operation the app needs. NEVER write a route that forwards
44
+ * `integration`, `endpoint`, `model`, `method` or a raw body from the request
45
+ * to callIntegration: that hands every visitor the user's full integration
46
+ * access, with this project's key attached.
47
+ *
48
+ * Integration slugs:
49
+ * Use the exact slug from the Enabled MCP list with the "pipedream-"
50
+ * prefix removed. e.g. "pipedream-gorgias_oauth" -> "gorgias_oauth".
51
+ * Native integrations (odoo, airtable) keep their plain name.
52
+ *
53
+ * Response shape (always wrapped):
54
+ * { success: boolean, status: number, data: <provider payload>, docs: string }
55
+ *
56
+ * - `status` is the UPSTREAM status, not the proxy's.
57
+ * - `success` is false whenever that status is >= 400.
58
+ * - `data` is null when the upstream returned no body.
59
+ * - `docs` links the proxy's own reference.
60
+ *
61
+ * Unwrap "data" by provider:
62
+ * odoo -> data.result
63
+ * airtable -> data.records
64
+ * notion -> data.results
65
+ * microsoft 365 -> data.value
66
+ * klaviyo -> data.data
67
+ * supabase / dimomaint -> data
68
+ *
69
+ * A 401 with code "api_key_missing" or "api_key_invalid" comes from the
70
+ * proxy itself: the call ran without this project's key. Its `llm` field
71
+ * says what to change.
72
+ *
73
+ * Pagination: put query params directly in the endpoint string,
74
+ * e.g. endpoint: '/api/tickets?cursor=xyz&per_page=100'.
75
+ * Read pagination metadata from the response to build the next call.
76
+ * Never .filter() large lists in the page -- paginate in the route.
77
+ *
78
+ * Sending files:
79
+ * Most APIs take a file as base64 inside the normal JSON body. Odoo is the
80
+ * common case: ir.attachment.create with the bytes in `datas` and BOTH
81
+ * `res_model` and `res_id` set, or the file is stored but appears on no
82
+ * record. Nothing special is needed for those.
83
+ *
84
+ * Some upload endpoints accept only multipart/form-data and answer JSON with
85
+ * HTTP 415. For those set bodyEncoding: 'multipart' (see uploadFile below).
86
+ * Never set a Content-Type header for multipart, the proxy lets fetch
87
+ * generate the boundary. Not accepted for odoo, which has no multipart API.
88
+ *
89
+ * Either way the browser POSTs the file to one of the app's own routes as
90
+ * FormData; the route reads it with `await request.formData()` and encodes
91
+ * it with fileToBase64 below.
92
+ *
93
+ * Secrets for non-integration APIs (e.g. raw OpenAI): store them in the
94
+ * project's Variables and read them via `env` from @helloleo/runtime, in
95
+ * server code only.
96
+ */
97
+
98
+ export const INTEGRATION_PROXY_URL = '{{LEO_INTEGRATION_PROXY_URL}}'
99
+
100
+ // This project's key, issued by HelloLeo per target and injected into the
101
+ // server env: the preview holds one value, the published app another. The
102
+ // proxy reads which one it got to pick the integration connection, so the
103
+ // published app talks to production and the preview to development with no
104
+ // flag to set. Not in @helloleo/runtime's RuntimeEnv, hence the local shape.
105
+ const platformEnv = env as unknown as { HELLOLEO_API_KEY?: string }
106
+
107
+ function proxyHeaders(): Record<string, string> {
108
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' }
109
+ const key = platformEnv.HELLOLEO_API_KEY
110
+ if (key) headers.Authorization = `Bearer ${key}`
111
+ return headers
112
+ }
113
+
114
+ export interface MultipartFile {
115
+ /** Form field name the upstream expects, e.g. 'content' or 'file'. */
116
+ name: string
117
+ filename: string
118
+ contentType?: string
119
+ /** File bytes, base64-encoded (no data: prefix). */
120
+ dataBase64: string
121
+ }
122
+
123
+ export interface MultipartBody {
124
+ fields?: Record<string, string>
125
+ files?: MultipartFile[]
126
+ }
127
+
128
+ export interface ProxyRequest {
129
+ integration: string
130
+ endpoint: string
131
+ method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
132
+ body?: unknown
133
+ headers?: Record<string, string>
134
+ /** Omit for JSON. 'multipart' when the upstream upload endpoint rejects JSON with 415. */
135
+ bodyEncoding?: 'json' | 'multipart'
136
+ }
137
+
138
+ export interface ProxyResponse<T = unknown> {
139
+ success: boolean
140
+ /** The UPSTREAM status, not the proxy's. */
141
+ status: number
142
+ /** Null when the upstream returned no body. */
143
+ data: T | null
144
+ /** The proxy's own reference, on every response. */
145
+ docs: string
146
+ }
147
+
148
+ export async function callIntegration<T = unknown>(
149
+ req: ProxyRequest,
150
+ ): Promise<ProxyResponse<T>> {
151
+ if (req.integration.toLowerCase() === 'netsuite') {
152
+ throw new Error(
153
+ "NetSuite is not proxied -- call it from this app's own /auth/netsuite + " +
154
+ "/api server routes with the signed-in user's session, not callIntegration().",
155
+ )
156
+ }
157
+ const res = await fetch(INTEGRATION_PROXY_URL, {
158
+ method: 'POST',
159
+ headers: proxyHeaders(),
160
+ body: JSON.stringify({ method: 'POST', ...req }),
161
+ })
162
+ return res.json()
163
+ }
164
+
165
+ /**
166
+ * Upload a file to an integration whose endpoint requires multipart/form-data
167
+ * (Zoho WorkDrive and similar). For Odoo, do NOT use this -- create an
168
+ * ir.attachment with the bytes base64-encoded in `datas` instead.
169
+ */
170
+ export async function uploadFile<T = unknown>(
171
+ req: Omit<ProxyRequest, 'body' | 'bodyEncoding'> & { body: MultipartBody },
172
+ ): Promise<ProxyResponse<T>> {
173
+ return callIntegration<T>({ method: 'POST', ...req, bodyEncoding: 'multipart' })
174
+ }
175
+
176
+ export async function fileToBase64(file: Blob): Promise<string> {
177
+ const bytes = new Uint8Array(await file.arrayBuffer())
178
+ let binary = ''
179
+ for (let i = 0; i < bytes.length; i += 1) binary += String.fromCharCode(bytes[i])
180
+ return btoa(binary)
181
+ }
182
+
183
+ /* Example -- multipart upload (Zoho WorkDrive), inside a POST route handler:
184
+ *
185
+ * POST: async ({ request }) => {
186
+ * const form = await request.formData()
187
+ * const file = form.get('file') as File
188
+ * const r = await uploadFile({
189
+ * integration: 'zoho_workdrive',
190
+ * endpoint: '/api/v1/upload',
191
+ * body: {
192
+ * fields: { parent_id: FOLDER_ID, 'override-name-exist': 'true' },
193
+ * files: [{
194
+ * name: 'content',
195
+ * filename: file.name,
196
+ * contentType: file.type,
197
+ * dataBase64: await fileToBase64(file),
198
+ * }],
199
+ * },
200
+ * })
201
+ * if (!r.success) return Response.json({ error: r.data }, { status: 502 })
202
+ * return Response.json(r.data)
203
+ * }
204
+ */
@@ -4,14 +4,9 @@ import { defineConfig } from '@helloleo/vite-config'
4
4
  // aliases `cloudflare:workers` -> @helloleo/runtime/cf-shim (libsql .dev.db for
5
5
  // D1+KV, local disk for R2) and runs plain Vite SSR; at build it adds
6
6
  // @cloudflare/vite-plugin to emit a workerd Worker bound to wrangler.jsonc. It
7
- // also binds host:true/port:5173 itself (>=0.1.2).
7
+ // also binds host:true/port:5173 itself (>=0.1.2) and blocks *.server.* files
8
+ // from the client bundle (>=0.1.7).
8
9
  //
9
- // The one thing the wrapper doesn't set: HMR over the HelloLeo preview proxy,
10
- // which terminates HTTPS on :443 — so point the HMR client at wss://…:443.
11
- export default defineConfig({
12
- vite: {
13
- server: {
14
- hmr: { clientPort: 443, protocol: 'wss' },
15
- },
16
- },
17
- })
10
+ // No HMR override: with no clientPort set, the Vite client connects back to the
11
+ // page's own origin, which is wss://…:443 behind the preview proxy.
12
+ export default defineConfig()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helloleo/plugins",
3
- "version": "0.2.29",
3
+ "version": "0.2.31-beta.0",
4
4
  "description": "OpenCode plugins for HelloLeo",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -15,7 +15,7 @@
15
15
  "access": "public"
16
16
  },
17
17
  "scripts": {
18
- "build": "rm -rf dist && bun build src/index.ts --outdir dist --target bun --format esm && bun run scripts/copy-templates.ts",
18
+ "build": "rm -rf dist && bun build src/index.ts src/cli.ts --outdir dist --target bun --format esm && bun run scripts/copy-templates.ts",
19
19
  "format": "bunx --bun @biomejs/biome format --write src",
20
20
  "lint": "bunx --bun @biomejs/biome lint --write src",
21
21
  "check": "bunx --bun @biomejs/biome check --write src",
@@ -1,182 +0,0 @@
1
- /*
2
- * HelloLeo Integration Proxy: runtime helper for connected integrations.
3
- *
4
- * All third-party APIs (Odoo, Airtable, HubSpot, Stripe, Notion, Gladia,
5
- * Microsoft 365, Klaviyo, Supabase, DimoMaint, etc.) are reachable through
6
- * this single proxy. The proxy handles auth and CORS using credentials the
7
- * user already configured in Project Settings -- you NEVER pass an API key
8
- * from frontend code, and you NEVER call third-party hosts directly.
9
- *
10
- * Two modes:
11
- * A) Frontend-only project -> import callIntegration() from this file.
12
- * B) HelloLeo Cloud project -> route via Worker fetch(env.INTEGRATION_PROXY).
13
- * Use Cloud whenever the project needs backend logic, automations,
14
- * webhooks, scheduled jobs, or AI agents. Keep secrets server-side.
15
- *
16
- * Integration slugs:
17
- * Use the exact slug from the Enabled MCP list with the "pipedream-"
18
- * prefix removed. e.g. "pipedream-gorgias_oauth" -> "gorgias_oauth".
19
- * Native integrations (odoo, airtable) keep their plain name.
20
- *
21
- * Response shape (always wrapped):
22
- * { success: boolean, status: number, data: <provider payload>, docs: string }
23
- *
24
- * - `status` is the UPSTREAM status, not the proxy's.
25
- * - `success` is false whenever that status is >= 400.
26
- * - `data` is null when the upstream returned no body.
27
- * - `docs` links the proxy's own reference.
28
- *
29
- * Unwrap "data" by provider:
30
- * odoo -> data.result
31
- * airtable -> data.records
32
- * notion -> data.results
33
- * microsoft 365 -> data.value
34
- * klaviyo -> data.data
35
- * supabase / dimomaint -> data
36
- *
37
- * Pagination: put query params directly in the endpoint string,
38
- * e.g. endpoint: '/api/tickets?cursor=xyz&per_page=100'.
39
- * Read pagination metadata from the response to build the next call.
40
- * Never .filter() large lists in the frontend -- paginate server-side.
41
- *
42
- * Sending files:
43
- * Most APIs take a file as base64 inside the normal JSON body. Odoo is the
44
- * common case: ir.attachment.create with the bytes in `datas` and BOTH
45
- * `res_model` and `res_id` set, or the file is stored but appears on no
46
- * record. Nothing special is needed for those.
47
- *
48
- * Some upload endpoints accept only multipart/form-data and answer JSON with
49
- * HTTP 415. For those set bodyEncoding: 'multipart' (see uploadFile below).
50
- * Never set a Content-Type header for multipart, the proxy lets fetch
51
- * generate the boundary. Not accepted for odoo, which has no multipart API.
52
- *
53
- * Secrets for non-integration APIs (e.g. raw OpenAI): store them in
54
- * HelloLeo Cloud -> Settings -> Cloud -> Secrets, and read via env.SECRET_NAME
55
- * from a Worker. Never embed secrets in frontend code.
56
- */
57
-
58
- export const INTEGRATION_PROXY_URL = '{{LEO_INTEGRATION_PROXY_URL}}';
59
-
60
- // Multi-environment integrations: the published HelloLeo Cloud Worker has
61
- // HELLOLEO_ENV=production injected at publish. Forwarding `?env=production`
62
- // makes the proxy resolve credentials from the project's PRODUCTION integration
63
- // connection (e.g. your production Odoo) instead of the development one, so a
64
- // published app talks to production while the preview keeps talking to staging.
65
- //
66
- // Reads the flag from the server runtime (Cloudflare Worker / Node) when
67
- // available; in the browser it's absent and we send no param — the proxy then
68
- // derives the environment from the request origin, so client-side calls from
69
- // the published app still resolve to production. Server-side calls (SSR / Cloud
70
- // Worker) carry no browser origin, which is exactly why the explicit param
71
- // matters there.
72
- function proxyUrl(): string {
73
- // Read via globalThis only (no bare `process` reference) so this compiles in
74
- // any scaffolded project regardless of whether node types are present.
75
- const g = globalThis as {
76
- HELLOLEO_ENV?: string;
77
- process?: { env?: Record<string, string | undefined> };
78
- };
79
- const env = g.HELLOLEO_ENV ?? g.process?.env?.HELLOLEO_ENV;
80
- return env === 'production' ? `${INTEGRATION_PROXY_URL}?env=production` : INTEGRATION_PROXY_URL;
81
- }
82
-
83
- export interface MultipartFile {
84
- /** Form field name the upstream expects, e.g. 'content' or 'file'. */
85
- name: string;
86
- filename: string;
87
- contentType?: string;
88
- /** File bytes, base64-encoded (no data: prefix). */
89
- dataBase64: string;
90
- }
91
-
92
- export interface MultipartBody {
93
- fields?: Record<string, string>;
94
- files?: MultipartFile[];
95
- }
96
-
97
- export interface ProxyRequest {
98
- integration: string;
99
- endpoint: string;
100
- method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
101
- body?: unknown;
102
- headers?: Record<string, string>;
103
- /** Omit for JSON. 'multipart' when the upstream upload endpoint rejects JSON with 415. */
104
- bodyEncoding?: 'json' | 'multipart';
105
- }
106
-
107
- export interface ProxyResponse<T = unknown> {
108
- success: boolean;
109
- /** The UPSTREAM status, not the proxy's. */
110
- status: number;
111
- /** Null when the upstream returned no body. */
112
- data: T | null;
113
- /** The proxy's own reference, on every response. */
114
- docs: string;
115
- }
116
-
117
- export async function callIntegration<T = unknown>(
118
- req: ProxyRequest,
119
- ): Promise<ProxyResponse<T>> {
120
- if (req.integration.toLowerCase() === 'netsuite') {
121
- throw new Error(
122
- 'NetSuite is not proxied -- call it from this app\'s own /auth/netsuite + ' +
123
- '/api server routes with the signed-in user\'s session, not callIntegration().',
124
- );
125
- }
126
- const res = await fetch(proxyUrl(), {
127
- method: 'POST',
128
- headers: { 'Content-Type': 'application/json' },
129
- body: JSON.stringify({ method: 'POST', ...req }),
130
- });
131
- return res.json();
132
- }
133
-
134
- /**
135
- * Upload a file to an integration whose endpoint requires multipart/form-data
136
- * (Zoho WorkDrive and similar). For Odoo, do NOT use this -- create an
137
- * ir.attachment with the bytes base64-encoded in `datas` instead.
138
- */
139
- export async function uploadFile<T = unknown>(
140
- req: Omit<ProxyRequest, 'body' | 'bodyEncoding'> & { body: MultipartBody },
141
- ): Promise<ProxyResponse<T>> {
142
- return callIntegration<T>({ method: 'POST', ...req, bodyEncoding: 'multipart' });
143
- }
144
-
145
- export async function fileToBase64(file: Blob): Promise<string> {
146
- const bytes = new Uint8Array(await file.arrayBuffer());
147
- let binary = '';
148
- for (let i = 0; i < bytes.length; i += 1) binary += String.fromCharCode(bytes[i]);
149
- return btoa(binary);
150
- }
151
-
152
- /* Example -- Odoo search_read:
153
- *
154
- * const r = await callIntegration({
155
- * integration: 'odoo',
156
- * endpoint: '/jsonrpc',
157
- * body: {
158
- * model: 'product.template',
159
- * method: 'search_read',
160
- * args: [[['type', '=', 'consu']]],
161
- * kwargs: { fields: ['name', 'list_price'], limit: 50 },
162
- * },
163
- * });
164
- * const products = r.data?.result ?? [];
165
- *
166
- * Example -- multipart upload (Zoho WorkDrive):
167
- *
168
- * const r = await uploadFile({
169
- * integration: 'zoho_workdrive',
170
- * endpoint: '/api/v1/upload',
171
- * body: {
172
- * fields: { parent_id: folderId, 'override-name-exist': 'true' },
173
- * files: [{
174
- * name: 'content',
175
- * filename: file.name,
176
- * contentType: file.type,
177
- * dataBase64: await fileToBase64(file),
178
- * }],
179
- * },
180
- * });
181
- * if (!r.success) throw new Error(String(r.data));
182
- */