@helloleo/plugins 0.2.26 → 0.2.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -21,7 +21,7 @@ var WRITE_TOOLS = new Set(["write", "edit", "apply_patch"]);
21
21
  var CHECKED = /\.(?:[cm]?tsx?|[cm]?jsx?)$/;
22
22
  var SKIPPED = /(?:^|[/\\])(?:node_modules|dist|build|\.wrangler|\.output)[/\\]/;
23
23
  var MAX_LINES = 6;
24
- var MAX_BYTES = 2000000;
24
+ var MAX_BYTES = 262144;
25
25
  function loaderFor(file) {
26
26
  const tsx = file.endsWith("x");
27
27
  return file.includes(".ts") ? tsx ? "tsx" : "ts" : tsx ? "jsx" : "js";
@@ -25009,9 +25009,8 @@ function resolveTemplateId() {
25009
25009
  function projectUuidFromDir(dir) {
25010
25010
  return path3.basename(dir);
25011
25011
  }
25012
- function integrationProxyUrl(projectUuid) {
25013
- const base = (process.env.PLUGINS_INTEGRATION_PROXY_URL || "https://api.app.helloleo.dev").replace(/\/+$/, "");
25014
- return `${base}/api/integration-proxy/${projectUuid}`;
25012
+ function integrationProxyBase() {
25013
+ return (process.env.PLUGINS_INTEGRATION_PROXY_URL || "https://api.app.helloleo.dev").replace(/\/+$/, "");
25015
25014
  }
25016
25015
  var scaffoldOptionsSchema = exports_external2.object({
25017
25016
  name: exports_external2.string().describe("Human-readable name for SEO, e.g. 'Shipping Platform'"),
@@ -25028,7 +25027,7 @@ async function writeBaseScaffold({ name, description }, dir) {
25028
25027
  LEO_SLUG: slugify(name),
25029
25028
  LEO_DESCRIPTION: description,
25030
25029
  LEO_PROJECT_UUID: projectUuid,
25031
- LEO_INTEGRATION_PROXY_URL: integrationProxyUrl(projectUuid)
25030
+ LEO_INTEGRATION_PROXY_URL: `${integrationProxyBase()}/api/integration-proxy/${projectUuid}`
25032
25031
  }, dir);
25033
25032
  await mkdir(path3.join(dir, ".leo"), { recursive: true });
25034
25033
  const leoPath = path3.join(".leo", "project.json");
@@ -44,9 +44,9 @@
44
44
  "@radix-ui/react-toggle-group": "^1.1.14",
45
45
  "@radix-ui/react-tooltip": "^1.2.11",
46
46
  "@tailwindcss/vite": "^4.3.2",
47
- "@tanstack/react-query": "^5.101.2",
47
+ "@tanstack/react-query": "^5.102.8",
48
48
  "@tanstack/react-router": "^1.170.17",
49
- "@tanstack/react-router-with-query": "^1.130.17",
49
+ "@tanstack/react-router-ssr-query": "^1.167.2",
50
50
  "@tanstack/react-start": "^1.168.27",
51
51
  "class-variance-authority": "^0.7.1",
52
52
  "clsx": "^2.1.1",
@@ -1,5 +1,5 @@
1
1
  /*
2
- * HelloLeo Integration Proxy -- runtime helper for connected integrations.
2
+ * HelloLeo Integration Proxy: runtime helper for connected integrations.
3
3
  *
4
4
  * All third-party APIs (Odoo, Airtable, HubSpot, Stripe, Notion, Gladia,
5
5
  * Microsoft 365, Klaviyo, Supabase, DimoMaint, etc.) are reachable through
@@ -19,7 +19,12 @@
19
19
  * Native integrations (odoo, airtable) keep their plain name.
20
20
  *
21
21
  * Response shape (always wrapped):
22
- * { success: boolean, status: number, data: <provider payload> }
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.
23
28
  *
24
29
  * Unwrap "data" by provider:
25
30
  * odoo -> data.result
@@ -34,6 +39,17 @@
34
39
  * Read pagination metadata from the response to build the next call.
35
40
  * Never .filter() large lists in the frontend -- paginate server-side.
36
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
+ *
37
53
  * Secrets for non-integration APIs (e.g. raw OpenAI): store them in
38
54
  * HelloLeo Cloud -> Settings -> Cloud -> Secrets, and read via env.SECRET_NAME
39
55
  * from a Worker. Never embed secrets in frontend code.
@@ -64,18 +80,38 @@ function proxyUrl(): string {
64
80
  return env === 'production' ? `${INTEGRATION_PROXY_URL}?env=production` : INTEGRATION_PROXY_URL;
65
81
  }
66
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
+
67
97
  export interface ProxyRequest {
68
98
  integration: string;
69
99
  endpoint: string;
70
100
  method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
71
101
  body?: unknown;
72
102
  headers?: Record<string, string>;
103
+ /** Omit for JSON. 'multipart' when the upstream upload endpoint rejects JSON with 415. */
104
+ bodyEncoding?: 'json' | 'multipart';
73
105
  }
74
106
 
75
107
  export interface ProxyResponse<T = unknown> {
76
108
  success: boolean;
109
+ /** The UPSTREAM status, not the proxy's. */
77
110
  status: number;
78
- data: T;
111
+ /** Null when the upstream returned no body. */
112
+ data: T | null;
113
+ /** The proxy's own reference, on every response. */
114
+ docs: string;
79
115
  }
80
116
 
81
117
  export async function callIntegration<T = unknown>(
@@ -95,6 +131,24 @@ export async function callIntegration<T = unknown>(
95
131
  return res.json();
96
132
  }
97
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
+
98
152
  /* Example -- Odoo search_read:
99
153
  *
100
154
  * const r = await callIntegration({
@@ -108,4 +162,21 @@ export async function callIntegration<T = unknown>(
108
162
  * },
109
163
  * });
110
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));
111
182
  */
@@ -1,31 +1,31 @@
1
1
  import { QueryClient } from '@tanstack/react-query'
2
2
  import { createRouter as createTanStackRouter } from '@tanstack/react-router'
3
- import { routerWithQueryClient } from '@tanstack/react-router-with-query'
3
+ import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'
4
4
  import { routeTree } from './routeTree.gen'
5
5
  import { DefaultErrorComponent } from '@/components/error-component'
6
6
 
7
- // TanStack Start calls getRouter() on both server and client. routerWithQueryClient
8
- // wires the QueryClient into the router context and handles SSR dehydration /
9
- // rehydration, so loaders can call context.queryClient.ensureQueryData() and the
10
- // client picks the cache up without refetching.
7
+ // TanStack Start calls getRouter() on both server and client.
8
+ // setupRouterSsrQueryIntegration wires the QueryClient into the router context
9
+ // and handles SSR dehydration / rehydration, so loaders can call
10
+ // context.queryClient.ensureQueryData() and the client picks the cache up
11
+ // without refetching.
11
12
  export function getRouter() {
12
13
  const queryClient = new QueryClient()
13
14
 
14
- const router = routerWithQueryClient(
15
- createTanStackRouter({
16
- routeTree,
17
- context: { queryClient },
18
- scrollRestoration: true,
19
- // Catches uncaught render/loader errors on any route. Routes can still
20
- // set their own `errorComponent` to override this.
21
- defaultErrorComponent: DefaultErrorComponent,
22
- defaultPreload: 'intent',
23
- // With React Query we never want loader data to be stale -- the loader runs
24
- // on every preload/visit and React Query owns caching from there.
25
- defaultPreloadStaleTime: 0,
26
- }),
27
- queryClient,
28
- )
15
+ const router = createTanStackRouter({
16
+ routeTree,
17
+ context: { queryClient },
18
+ scrollRestoration: true,
19
+ // Catches uncaught render/loader errors on any route. Routes can still
20
+ // set their own `errorComponent` to override this.
21
+ defaultErrorComponent: DefaultErrorComponent,
22
+ defaultPreload: 'intent',
23
+ // With React Query we never want loader data to be stale -- the loader runs
24
+ // on every preload/visit and React Query owns caching from there.
25
+ defaultPreloadStaleTime: 0,
26
+ })
27
+
28
+ setupRouterSsrQueryIntegration({ router, queryClient })
29
29
 
30
30
  return router
31
31
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helloleo/plugins",
3
- "version": "0.2.26",
3
+ "version": "0.2.28",
4
4
  "description": "OpenCode plugins for HelloLeo",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",