@helloleo/plugins 0.1.10 → 0.1.12
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 +537 -498
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/tools/inspect-http.ts +9 -16
- package/src/tools/scaffold.ts +52 -104
- package/src/tools/{scaffold-netsuite.ts → setup-netsuite.ts} +97 -12
- package/src/utils/try-catch.ts +0 -22
package/dist/index.js
CHANGED
|
@@ -12332,10 +12332,10 @@ function tool(input) {
|
|
|
12332
12332
|
tool.schema = exports_external;
|
|
12333
12333
|
// src/tools/inspect-http.ts
|
|
12334
12334
|
var z = tool.schema;
|
|
12335
|
-
var
|
|
12336
|
-
|
|
12337
|
-
".
|
|
12338
|
-
".
|
|
12335
|
+
var ALLOWED_BASE_HOSTS = [
|
|
12336
|
+
"helloleo.dev",
|
|
12337
|
+
"helloleo.app",
|
|
12338
|
+
"augustin-d60.workers.dev"
|
|
12339
12339
|
];
|
|
12340
12340
|
var DEFAULT_TIMEOUT_MS = 15000;
|
|
12341
12341
|
var MAX_TIMEOUT_MS = 30000;
|
|
@@ -12343,7 +12343,7 @@ var MAX_RESPONSE_BYTES = 1e6;
|
|
|
12343
12343
|
var METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
12344
12344
|
var METHODS_AUTO_JSON = new Set(["POST", "PUT", "PATCH"]);
|
|
12345
12345
|
var argsShape = {
|
|
12346
|
-
url: z.string().describe("Fully-qualified URL to request. Must be on the HelloLeo whitelist " + "(
|
|
12346
|
+
url: z.string().describe("Fully-qualified URL to request. Must be on the HelloLeo whitelist " + "(**.helloleo.dev, **.helloleo.app, **.augustin-d60.workers.dev)."),
|
|
12347
12347
|
method: z.enum(METHODS).default("GET").describe(`HTTP method (${METHODS.join(", ")}). Defaults to GET.`),
|
|
12348
12348
|
body: z.string().optional().describe("Request body as a string. Not valid for GET. Sent with " + "Content-Type: application/json on POST/PUT/PATCH unless a " + "different Content-Type is supplied via headers."),
|
|
12349
12349
|
headers: z.record(z.string(), z.string()).default({}).describe("Optional additional request headers as key/value pairs (e.g. Authorization)."),
|
|
@@ -12351,7 +12351,7 @@ var argsShape = {
|
|
|
12351
12351
|
};
|
|
12352
12352
|
var argsSchema = z.object(argsShape);
|
|
12353
12353
|
var inspectHttp = tool({
|
|
12354
|
-
description: "Send an HTTP request to a HelloLeo endpoint and return the status code, " + "response headers, and response body. Use this to inspect the live behaviour " + "of HelloLeo workers, the production API, or preview deployments so you can " + "self-debug instead of operating blind. "
|
|
12354
|
+
description: "Send an HTTP request to a HelloLeo endpoint and return the status code, " + "response headers, and response body. Use this to inspect the live behaviour " + "of HelloLeo workers, the production API, or preview deployments so you can " + "self-debug instead of operating blind. ",
|
|
12355
12355
|
args: argsShape,
|
|
12356
12356
|
async execute(rawArgs, context) {
|
|
12357
12357
|
const args = argsSchema.parse(rawArgs);
|
|
@@ -12437,17 +12437,12 @@ function parseTargetUrl(raw) {
|
|
|
12437
12437
|
return url2;
|
|
12438
12438
|
}
|
|
12439
12439
|
function allowedHostsHuman() {
|
|
12440
|
-
return
|
|
12441
|
-
...ALLOWED_EXACT_HOSTS,
|
|
12442
|
-
...ALLOWED_HOST_SUFFIXES.map((s) => `*${s}`)
|
|
12443
|
-
].join(", ");
|
|
12440
|
+
return ALLOWED_BASE_HOSTS.map((base) => `**.${base}`).join(", ");
|
|
12444
12441
|
}
|
|
12445
12442
|
function isAllowedHost(hostname3) {
|
|
12446
12443
|
const host = hostname3.toLowerCase().replace(/\.$/, "");
|
|
12447
|
-
|
|
12448
|
-
|
|
12449
|
-
for (const suffix of ALLOWED_HOST_SUFFIXES) {
|
|
12450
|
-
if (host.endsWith(suffix) && host.length > suffix.length)
|
|
12444
|
+
for (const base of ALLOWED_BASE_HOSTS) {
|
|
12445
|
+
if (host === base || host.endsWith(`.${base}`))
|
|
12451
12446
|
return true;
|
|
12452
12447
|
}
|
|
12453
12448
|
return false;
|
|
@@ -12544,74 +12539,493 @@ var RemovePlugin = async () => {
|
|
|
12544
12539
|
// src/tools/scaffold.ts
|
|
12545
12540
|
import { mkdir } from "fs/promises";
|
|
12546
12541
|
import path2 from "path";
|
|
12542
|
+
function packageJson(name) {
|
|
12543
|
+
return JSON.stringify({
|
|
12544
|
+
name,
|
|
12545
|
+
private: true,
|
|
12546
|
+
version: "0.0.0",
|
|
12547
|
+
type: "module",
|
|
12548
|
+
scripts: {
|
|
12549
|
+
dev: "vite",
|
|
12550
|
+
start: "vite",
|
|
12551
|
+
build: "vite build",
|
|
12552
|
+
preview: "vite preview"
|
|
12553
|
+
},
|
|
12554
|
+
dependencies: {
|
|
12555
|
+
react: "^18.3.1",
|
|
12556
|
+
"react-dom": "^18.3.1",
|
|
12557
|
+
clsx: "^2.1.1",
|
|
12558
|
+
"tailwind-merge": "^2.6.0",
|
|
12559
|
+
"lucide-react": "^1.14.0"
|
|
12560
|
+
},
|
|
12561
|
+
devDependencies: {
|
|
12562
|
+
"@types/react": "^18.3.12",
|
|
12563
|
+
"@types/react-dom": "^18.3.1",
|
|
12564
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
12565
|
+
autoprefixer: "^10.4.20",
|
|
12566
|
+
postcss: "^8.4.49",
|
|
12567
|
+
tailwindcss: "^3.4.17",
|
|
12568
|
+
typescript: "^5.6.3",
|
|
12569
|
+
vite: "^6.0.5"
|
|
12570
|
+
}
|
|
12571
|
+
}, null, 2);
|
|
12572
|
+
}
|
|
12573
|
+
function indexHtml(fontFamily) {
|
|
12574
|
+
const fontParam = fontFamily.replace(/\s+/g, "+");
|
|
12575
|
+
return `<!DOCTYPE html>
|
|
12576
|
+
<html lang="en">
|
|
12577
|
+
<head>
|
|
12578
|
+
<meta charset="UTF-8" />
|
|
12579
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
12580
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
12581
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
12582
|
+
<link href="https://fonts.googleapis.com/css2?family=${fontParam}:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
12583
|
+
<title>App</title>
|
|
12584
|
+
</head>
|
|
12585
|
+
<body>
|
|
12586
|
+
<div id="root"></div>
|
|
12587
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12588
|
+
</body>
|
|
12589
|
+
</html>`;
|
|
12590
|
+
}
|
|
12591
|
+
function viteConfig() {
|
|
12592
|
+
return `import { defineConfig } from 'vite'
|
|
12593
|
+
import react from '@vitejs/plugin-react'
|
|
12594
|
+
import path from 'path'
|
|
12547
12595
|
|
|
12548
|
-
|
|
12549
|
-
|
|
12596
|
+
export default defineConfig({
|
|
12597
|
+
plugins: [react()],
|
|
12598
|
+
server: {
|
|
12599
|
+
allowedHosts: true,
|
|
12600
|
+
},
|
|
12601
|
+
resolve: {
|
|
12602
|
+
alias: {
|
|
12603
|
+
'@': path.resolve(__dirname, './src'),
|
|
12604
|
+
},
|
|
12605
|
+
},
|
|
12606
|
+
})`;
|
|
12607
|
+
}
|
|
12608
|
+
function tailwindConfig() {
|
|
12609
|
+
return `export default {
|
|
12610
|
+
darkMode: ["class"],
|
|
12611
|
+
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
|
12612
|
+
theme: {
|
|
12613
|
+
extend: {
|
|
12614
|
+
colors: {
|
|
12615
|
+
background: "hsl(var(--background))",
|
|
12616
|
+
foreground: "hsl(var(--foreground))",
|
|
12617
|
+
card: { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))" },
|
|
12618
|
+
primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))" },
|
|
12619
|
+
secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))" },
|
|
12620
|
+
muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))" },
|
|
12621
|
+
accent: { DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))" },
|
|
12622
|
+
destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))" },
|
|
12623
|
+
success: { DEFAULT: "hsl(var(--success))", foreground: "hsl(var(--success-foreground))" },
|
|
12624
|
+
warning: { DEFAULT: "hsl(var(--warning))", foreground: "hsl(var(--warning-foreground))" },
|
|
12625
|
+
border: "hsl(var(--border))",
|
|
12626
|
+
input: "hsl(var(--input))",
|
|
12627
|
+
ring: "hsl(var(--ring))",
|
|
12628
|
+
},
|
|
12629
|
+
fontFamily: {
|
|
12630
|
+
sans: "var(--font-sans)",
|
|
12631
|
+
heading: "var(--font-heading)",
|
|
12632
|
+
},
|
|
12633
|
+
borderRadius: {
|
|
12634
|
+
lg: "var(--radius)",
|
|
12635
|
+
md: "calc(var(--radius) - 2px)",
|
|
12636
|
+
sm: "calc(var(--radius) - 4px)",
|
|
12637
|
+
},
|
|
12638
|
+
borderWidth: {
|
|
12639
|
+
DEFAULT: "var(--border-width)",
|
|
12640
|
+
},
|
|
12641
|
+
boxShadow: {
|
|
12642
|
+
DEFAULT: "var(--shadow)",
|
|
12643
|
+
lg: "var(--shadow-lg)",
|
|
12644
|
+
},
|
|
12645
|
+
},
|
|
12646
|
+
},
|
|
12647
|
+
plugins: [],
|
|
12648
|
+
}`;
|
|
12649
|
+
}
|
|
12650
|
+
function postcssConfig() {
|
|
12651
|
+
return `export default {
|
|
12652
|
+
plugins: {
|
|
12653
|
+
tailwindcss: {},
|
|
12654
|
+
autoprefixer: {},
|
|
12655
|
+
},
|
|
12656
|
+
}`;
|
|
12657
|
+
}
|
|
12658
|
+
function indexCss(primaryColor, fontFamily, borderRadius) {
|
|
12659
|
+
const fontValue = `'${fontFamily}', ui-sans-serif, system-ui, sans-serif`;
|
|
12550
12660
|
return `/*
|
|
12551
|
-
*
|
|
12661
|
+
* HelloLeo Design System -- READ BEFORE STYLING.
|
|
12552
12662
|
*
|
|
12553
|
-
*
|
|
12554
|
-
*
|
|
12555
|
-
*
|
|
12556
|
-
*
|
|
12557
|
-
*
|
|
12558
|
-
* the browser. Cookies are NOT used -- frontend \u2194 Worker are cross-site
|
|
12559
|
-
* (e.g. *.helloleo.dev <-> *.workers.dev) and Chrome partitions / drops
|
|
12560
|
-
* third-party cookies in the HelloLeo preview iframe.
|
|
12663
|
+
* The CSS variables below are the single source of truth for the project's
|
|
12664
|
+
* design tokens. They power the no-code Design panel in the HelloLeo webapp:
|
|
12665
|
+
* the user edits colors / fonts / radius / shadows visually, and the panel
|
|
12666
|
+
* writes back to THIS file. Keeping the conventions intact is what unlocks
|
|
12667
|
+
* that round-trip.
|
|
12561
12668
|
*
|
|
12562
|
-
*
|
|
12563
|
-
*
|
|
12564
|
-
*
|
|
12565
|
-
*
|
|
12566
|
-
*
|
|
12567
|
-
*
|
|
12568
|
-
*
|
|
12569
|
-
*
|
|
12570
|
-
*
|
|
12571
|
-
*
|
|
12669
|
+
* Conventions (do not break):
|
|
12670
|
+
* - All colors stored as raw HSL triplets (no hsl() wrapper, no #hex).
|
|
12671
|
+
* Tailwind classes wrap them via hsl(var(--token)). Never inline #hex
|
|
12672
|
+
* in components -- always use design tokens (bg-primary, text-foreground,
|
|
12673
|
+
* border-border, etc.).
|
|
12674
|
+
* - --primary / --secondary / --accent / --destructive / --success /
|
|
12675
|
+
* --warning each have a paired -foreground for text on that surface.
|
|
12676
|
+
* - --radius drives lg/md/sm via calc() in tailwind.config.js. Edit here,
|
|
12677
|
+
* not in component classes.
|
|
12678
|
+
* - --font-sans for body, --font-heading for headings. Both load from the
|
|
12679
|
+
* Google Fonts <link> in index.html.
|
|
12680
|
+
* - .dark variants mirror :root one-for-one. When adding a token, add it
|
|
12681
|
+
* to BOTH blocks or dark mode breaks.
|
|
12572
12682
|
*
|
|
12573
|
-
*
|
|
12574
|
-
*
|
|
12575
|
-
*
|
|
12683
|
+
* UI primitives (Button, Card, Input, Modal, Table, Badge, Alert, Spinner)
|
|
12684
|
+
* MUST live in src/components/ui/ and consume these tokens via Tailwind
|
|
12685
|
+
* classes -- never hardcoded colors. One responsibility per file, <50 lines
|
|
12686
|
+
* per component.
|
|
12576
12687
|
*/
|
|
12577
12688
|
|
|
12578
|
-
|
|
12579
|
-
|
|
12689
|
+
@tailwind base;
|
|
12690
|
+
@tailwind components;
|
|
12691
|
+
@tailwind utilities;
|
|
12580
12692
|
|
|
12581
|
-
|
|
12582
|
-
|
|
12583
|
-
|
|
12584
|
-
|
|
12585
|
-
|
|
12586
|
-
|
|
12587
|
-
|
|
12588
|
-
|
|
12589
|
-
|
|
12590
|
-
|
|
12591
|
-
|
|
12592
|
-
|
|
12593
|
-
|
|
12594
|
-
|
|
12693
|
+
@layer base {
|
|
12694
|
+
:root {
|
|
12695
|
+
--background: 0 0% 100%;
|
|
12696
|
+
--foreground: 222.2 84% 4.9%;
|
|
12697
|
+
--card: 0 0% 100%;
|
|
12698
|
+
--card-foreground: 222.2 84% 4.9%;
|
|
12699
|
+
--primary: ${primaryColor};
|
|
12700
|
+
--primary-foreground: 210 40% 98%;
|
|
12701
|
+
--secondary: 210 40% 96.1%;
|
|
12702
|
+
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
12703
|
+
--muted: 210 40% 96.1%;
|
|
12704
|
+
--muted-foreground: 215.4 16.3% 46.9%;
|
|
12705
|
+
--accent: 210 40% 96.1%;
|
|
12706
|
+
--accent-foreground: 222.2 47.4% 11.2%;
|
|
12707
|
+
--destructive: 0 84.2% 60.2%;
|
|
12708
|
+
--destructive-foreground: 210 40% 98%;
|
|
12709
|
+
--success: 142 76% 36%;
|
|
12710
|
+
--success-foreground: 210 40% 98%;
|
|
12711
|
+
--warning: 38 92% 50%;
|
|
12712
|
+
--warning-foreground: 222.2 47.4% 11.2%;
|
|
12713
|
+
--border: 214.3 31.8% 91.4%;
|
|
12714
|
+
--input: 214.3 31.8% 91.4%;
|
|
12715
|
+
--ring: 221.2 83.2% 53.3%;
|
|
12716
|
+
--radius: ${borderRadius};
|
|
12717
|
+
--border-width: 1px;
|
|
12718
|
+
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
|
12719
|
+
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|
12720
|
+
--font-sans: ${fontValue};
|
|
12721
|
+
--font-heading: ${fontValue};
|
|
12595
12722
|
}
|
|
12596
|
-
})();
|
|
12597
12723
|
|
|
12598
|
-
|
|
12599
|
-
|
|
12724
|
+
.dark {
|
|
12725
|
+
--background: 222.2 84% 4.9%;
|
|
12726
|
+
--foreground: 210 40% 98%;
|
|
12727
|
+
--card: 222.2 84% 4.9%;
|
|
12728
|
+
--card-foreground: 210 40% 98%;
|
|
12729
|
+
--primary: 217.2 91.2% 59.8%;
|
|
12730
|
+
--primary-foreground: 222.2 47.4% 11.2%;
|
|
12731
|
+
--secondary: 217.2 32.6% 17.5%;
|
|
12732
|
+
--secondary-foreground: 210 40% 98%;
|
|
12733
|
+
--muted: 217.2 32.6% 17.5%;
|
|
12734
|
+
--muted-foreground: 215 20.2% 65.1%;
|
|
12735
|
+
--accent: 217.2 32.6% 17.5%;
|
|
12736
|
+
--accent-foreground: 210 40% 98%;
|
|
12737
|
+
--destructive: 0 62.8% 30.6%;
|
|
12738
|
+
--destructive-foreground: 210 40% 98%;
|
|
12739
|
+
--success: 142 76% 36%;
|
|
12740
|
+
--success-foreground: 210 40% 98%;
|
|
12741
|
+
--warning: 38 92% 50%;
|
|
12742
|
+
--warning-foreground: 222.2 47.4% 11.2%;
|
|
12743
|
+
--border: 217.2 32.6% 17.5%;
|
|
12744
|
+
--input: 217.2 32.6% 17.5%;
|
|
12745
|
+
--ring: 224.3 76.3% 48%;
|
|
12746
|
+
--border-width: 1px;
|
|
12747
|
+
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.3), 0 1px 2px -1px rgb(0 0 0 / 0.3);
|
|
12748
|
+
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.3), 0 4px 6px -4px rgb(0 0 0 / 0.3);
|
|
12749
|
+
--font-sans: ${fontValue};
|
|
12750
|
+
--font-heading: ${fontValue};
|
|
12751
|
+
}
|
|
12600
12752
|
}
|
|
12601
12753
|
|
|
12602
|
-
|
|
12603
|
-
|
|
12604
|
-
|
|
12605
|
-
|
|
12606
|
-
|
|
12607
|
-
}
|
|
12608
|
-
|
|
12609
|
-
|
|
12610
|
-
|
|
12611
|
-
|
|
12612
|
-
|
|
12613
|
-
|
|
12614
|
-
|
|
12754
|
+
body {
|
|
12755
|
+
@apply bg-background text-foreground font-sans;
|
|
12756
|
+
}`;
|
|
12757
|
+
}
|
|
12758
|
+
function utilsTs() {
|
|
12759
|
+
return `import { clsx, type ClassValue } from 'clsx'
|
|
12760
|
+
import { twMerge } from 'tailwind-merge'
|
|
12761
|
+
|
|
12762
|
+
export function cn(...inputs: ClassValue[]) {
|
|
12763
|
+
return twMerge(clsx(inputs))
|
|
12764
|
+
}`;
|
|
12765
|
+
}
|
|
12766
|
+
function proxyTs() {
|
|
12767
|
+
return `/*
|
|
12768
|
+
* HelloLeo Integration Proxy -- runtime helper for connected integrations.
|
|
12769
|
+
*
|
|
12770
|
+
* All third-party APIs (Odoo, Airtable, HubSpot, Stripe, Notion, Gladia,
|
|
12771
|
+
* Microsoft 365, Klaviyo, Supabase, DimoMaint, etc.) are reachable through
|
|
12772
|
+
* this single proxy. The proxy handles auth and CORS using credentials the
|
|
12773
|
+
* user already configured in Project Settings -- you NEVER pass an API key
|
|
12774
|
+
* from frontend code, and you NEVER call third-party hosts directly.
|
|
12775
|
+
*
|
|
12776
|
+
* Two modes:
|
|
12777
|
+
* A) Frontend-only project -> import callIntegration() from this file.
|
|
12778
|
+
* B) HelloLeo Cloud project -> route via Worker fetch(env.INTEGRATION_PROXY).
|
|
12779
|
+
* Use Cloud whenever the project needs backend logic, automations,
|
|
12780
|
+
* webhooks, scheduled jobs, or AI agents. Keep secrets server-side.
|
|
12781
|
+
*
|
|
12782
|
+
* Integration slugs:
|
|
12783
|
+
* Use the exact slug from the Enabled MCP list with the "pipedream-"
|
|
12784
|
+
* prefix removed. e.g. "pipedream-gorgias_oauth" -> "gorgias_oauth".
|
|
12785
|
+
* Native integrations (odoo, airtable) keep their plain name.
|
|
12786
|
+
*
|
|
12787
|
+
* Response shape (always wrapped):
|
|
12788
|
+
* { success: boolean, status: number, data: <provider payload> }
|
|
12789
|
+
*
|
|
12790
|
+
* Unwrap "data" by provider:
|
|
12791
|
+
* odoo -> data.result
|
|
12792
|
+
* airtable -> data.records
|
|
12793
|
+
* notion -> data.results
|
|
12794
|
+
* microsoft 365 -> data.value
|
|
12795
|
+
* klaviyo -> data.data
|
|
12796
|
+
* supabase / dimomaint -> data
|
|
12797
|
+
*
|
|
12798
|
+
* Pagination: put query params directly in the endpoint string,
|
|
12799
|
+
* e.g. endpoint: '/api/tickets?cursor=xyz&per_page=100'.
|
|
12800
|
+
* Read pagination metadata from the response to build the next call.
|
|
12801
|
+
* Never .filter() large lists in the frontend -- paginate server-side.
|
|
12802
|
+
*
|
|
12803
|
+
* Secrets for non-integration APIs (e.g. raw OpenAI): store them in
|
|
12804
|
+
* HelloLeo Cloud -> Settings -> Cloud -> Secrets, and read via env.SECRET_NAME
|
|
12805
|
+
* from a Worker. Never embed secrets in frontend code.
|
|
12806
|
+
*/
|
|
12807
|
+
|
|
12808
|
+
const PROJECT_UUID = import.meta.env.VITE_PROJECT_UUID ?? '';
|
|
12809
|
+
export const INTEGRATION_PROXY_URL =
|
|
12810
|
+
\`https://api.app.helloleo.dev/api/integration-proxy/\${PROJECT_UUID}\`;
|
|
12811
|
+
|
|
12812
|
+
export interface ProxyRequest {
|
|
12813
|
+
integration: string;
|
|
12814
|
+
endpoint: string;
|
|
12815
|
+
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
12816
|
+
body?: unknown;
|
|
12817
|
+
headers?: Record<string, string>;
|
|
12818
|
+
}
|
|
12819
|
+
|
|
12820
|
+
export interface ProxyResponse<T = unknown> {
|
|
12821
|
+
success: boolean;
|
|
12822
|
+
status: number;
|
|
12823
|
+
data: T;
|
|
12824
|
+
}
|
|
12825
|
+
|
|
12826
|
+
export async function callIntegration<T = unknown>(
|
|
12827
|
+
req: ProxyRequest,
|
|
12828
|
+
): Promise<ProxyResponse<T>> {
|
|
12829
|
+
const res = await fetch(INTEGRATION_PROXY_URL, {
|
|
12830
|
+
method: 'POST',
|
|
12831
|
+
headers: { 'Content-Type': 'application/json' },
|
|
12832
|
+
body: JSON.stringify({ method: 'POST', ...req }),
|
|
12833
|
+
});
|
|
12834
|
+
return res.json();
|
|
12835
|
+
}
|
|
12836
|
+
|
|
12837
|
+
/* Example -- Odoo search_read:
|
|
12838
|
+
*
|
|
12839
|
+
* const r = await callIntegration({
|
|
12840
|
+
* integration: 'odoo',
|
|
12841
|
+
* endpoint: '/jsonrpc',
|
|
12842
|
+
* body: {
|
|
12843
|
+
* model: 'product.template',
|
|
12844
|
+
* method: 'search_read',
|
|
12845
|
+
* args: [[['type', '=', 'consu']]],
|
|
12846
|
+
* kwargs: { fields: ['name', 'list_price'], limit: 50 },
|
|
12847
|
+
* },
|
|
12848
|
+
* });
|
|
12849
|
+
* const products = r.data?.result ?? [];
|
|
12850
|
+
*/
|
|
12851
|
+
`;
|
|
12852
|
+
}
|
|
12853
|
+
function hexToHslTriplet(hex3) {
|
|
12854
|
+
let h = hex3.trim().replace(/^#/, "");
|
|
12855
|
+
if (h.length === 3) {
|
|
12856
|
+
h = h.split("").map((c) => c + c).join("");
|
|
12857
|
+
}
|
|
12858
|
+
if (!/^[0-9a-fA-F]{6}$/.test(h))
|
|
12859
|
+
return "";
|
|
12860
|
+
const r = Number.parseInt(h.slice(0, 2), 16) / 255;
|
|
12861
|
+
const g = Number.parseInt(h.slice(2, 4), 16) / 255;
|
|
12862
|
+
const b = Number.parseInt(h.slice(4, 6), 16) / 255;
|
|
12863
|
+
const max = Math.max(r, g, b);
|
|
12864
|
+
const min = Math.min(r, g, b);
|
|
12865
|
+
const l = (max + min) / 2;
|
|
12866
|
+
let s = 0;
|
|
12867
|
+
let hue = 0;
|
|
12868
|
+
if (max !== min) {
|
|
12869
|
+
const d = max - min;
|
|
12870
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
12871
|
+
switch (max) {
|
|
12872
|
+
case r:
|
|
12873
|
+
hue = (g - b) / d + (g < b ? 6 : 0);
|
|
12874
|
+
break;
|
|
12875
|
+
case g:
|
|
12876
|
+
hue = (b - r) / d + 2;
|
|
12877
|
+
break;
|
|
12878
|
+
default:
|
|
12879
|
+
hue = (r - g) / d + 4;
|
|
12880
|
+
}
|
|
12881
|
+
hue *= 60;
|
|
12882
|
+
}
|
|
12883
|
+
return `${Math.round(hue)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`;
|
|
12884
|
+
}
|
|
12885
|
+
function normalizePrimaryColor(value) {
|
|
12886
|
+
const trimmed = value.trim();
|
|
12887
|
+
if (/^(#|[0-9a-fA-F]{3}$|[0-9a-fA-F]{6}$)/.test(trimmed)) {
|
|
12888
|
+
const hsl = hexToHslTriplet(trimmed);
|
|
12889
|
+
if (hsl)
|
|
12890
|
+
return hsl;
|
|
12891
|
+
}
|
|
12892
|
+
return trimmed;
|
|
12893
|
+
}
|
|
12894
|
+
function mainTsx() {
|
|
12895
|
+
return `import React from 'react'
|
|
12896
|
+
import ReactDOM from 'react-dom/client'
|
|
12897
|
+
import App from './App'
|
|
12898
|
+
import './index.css'
|
|
12899
|
+
|
|
12900
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
12901
|
+
<React.StrictMode>
|
|
12902
|
+
<App />
|
|
12903
|
+
</React.StrictMode>,
|
|
12904
|
+
)`;
|
|
12905
|
+
}
|
|
12906
|
+
async function writeBaseScaffold(opts, dir) {
|
|
12907
|
+
const name = opts.name ?? "leo-project";
|
|
12908
|
+
const primary = normalizePrimaryColor(opts.primaryColor ?? "221.2 83.2% 53.3%");
|
|
12909
|
+
const font = opts.fontFamily ?? "Inter";
|
|
12910
|
+
const radius = opts.borderRadius ?? "0.5rem";
|
|
12911
|
+
await mkdir(path2.join(dir, "src", "lib"), { recursive: true });
|
|
12912
|
+
const files = [
|
|
12913
|
+
["package.json", packageJson(name)],
|
|
12914
|
+
["index.html", indexHtml(font)],
|
|
12915
|
+
["vite.config.js", viteConfig()],
|
|
12916
|
+
["tailwind.config.js", tailwindConfig()],
|
|
12917
|
+
["postcss.config.js", postcssConfig()],
|
|
12918
|
+
[path2.join("src", "index.css"), indexCss(primary, font, radius)],
|
|
12919
|
+
[path2.join("src", "lib", "utils.ts"), utilsTs()],
|
|
12920
|
+
[path2.join("src", "lib", "proxy.ts"), proxyTs()],
|
|
12921
|
+
[path2.join("src", "main.tsx"), mainTsx()]
|
|
12922
|
+
];
|
|
12923
|
+
const created = [];
|
|
12924
|
+
for (const [filePath, content] of files) {
|
|
12925
|
+
await Bun.write(path2.join(dir, filePath), content);
|
|
12926
|
+
created.push(filePath);
|
|
12927
|
+
}
|
|
12928
|
+
return created;
|
|
12929
|
+
}
|
|
12930
|
+
var scaffold = tool({
|
|
12931
|
+
description: "Scaffold a new React + Vite + Tailwind CSS project with the HelloLeo design system. " + "Creates all mandatory boilerplate: package.json, index.html (with Google Fonts), " + "vite.config.js, tailwind.config.js, postcss.config.js, src/index.css (CSS variables " + "light+dark), src/lib/utils.ts (cn helper), src/lib/proxy.ts, and src/main.tsx. " + "Call this FIRST for any new project before writing App.tsx and components. " + "For NetSuite projects (NetSuite MCP in the live list), call `setup_netsuite` " + "INSTEAD -- it bundles base scaffold + OAuth wiring in one call. Do not call both.",
|
|
12932
|
+
args: {
|
|
12933
|
+
name: tool.schema.string().optional().describe("Project name for package.json (default: leo-project)"),
|
|
12934
|
+
primaryColor: tool.schema.string().optional().describe("Primary brand color. Prefer raw HSL triplet (no hsl() wrapper, no # prefix), " + 'e.g. "221.2 83.2% 53.3%". Hex like "#3b82f6" is also accepted and will ' + "be auto-converted. Default: blue."),
|
|
12935
|
+
fontFamily: tool.schema.string().optional().describe('Google Font family name, e.g. "Inter", "Poppins" (default: Inter)'),
|
|
12936
|
+
borderRadius: tool.schema.string().optional().describe('Base border radius, e.g. "0.5rem", "0.75rem" (default: 0.5rem)')
|
|
12937
|
+
},
|
|
12938
|
+
async execute(args, context) {
|
|
12939
|
+
const created = await writeBaseScaffold(args, context.directory);
|
|
12940
|
+
return [
|
|
12941
|
+
`Scaffolded ${created.length} files:`,
|
|
12942
|
+
...created.map((f) => ` - ${f}`),
|
|
12943
|
+
"",
|
|
12944
|
+
"Next: create src/App.tsx with your application code.",
|
|
12945
|
+
"Design system reference: read the header comment in src/index.css",
|
|
12946
|
+
" before adding tokens or styling primitives.",
|
|
12947
|
+
"Integration calls: use callIntegration() from src/lib/proxy.ts.",
|
|
12948
|
+
" Header comment in that file documents slugs, response unwrapping,",
|
|
12949
|
+
" pagination, and frontend-only vs HelloLeo Cloud routing.",
|
|
12950
|
+
"Update package.json with any extra packages you need after scaffolding."
|
|
12951
|
+
].join(`
|
|
12952
|
+
`);
|
|
12953
|
+
}
|
|
12954
|
+
});
|
|
12955
|
+
var ScaffoldPlugin = async () => {
|
|
12956
|
+
return {
|
|
12957
|
+
tool: { scaffold }
|
|
12958
|
+
};
|
|
12959
|
+
};
|
|
12960
|
+
// src/tools/setup-netsuite.ts
|
|
12961
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
12962
|
+
import path3 from "path";
|
|
12963
|
+
function netsuiteTs() {
|
|
12964
|
+
return `/*
|
|
12965
|
+
* NetSuite OAuth client helper (bearer-token-in-fragment).
|
|
12966
|
+
*
|
|
12967
|
+
* Two NetSuite Integration Records back this:
|
|
12968
|
+
* 1. MCP (scope: mcp) -- used by HelloLeo at build time.
|
|
12969
|
+
* 2. Runtime (scope: rest_webservices) -- used here at sign-in time.
|
|
12970
|
+
* Runtime credentials live as HelloLeo Cloud secrets on the project's Worker
|
|
12971
|
+
* (env.NETSUITE_ACCOUNT_ID / _CLIENT_ID / _CLIENT_SECRET). They never reach
|
|
12972
|
+
* the browser. Cookies are NOT used -- frontend \u2194 Worker are cross-site
|
|
12973
|
+
* (e.g. *.helloleo.dev <-> *.workers.dev) and Chrome partitions / drops
|
|
12974
|
+
* third-party cookies in the HelloLeo preview iframe.
|
|
12975
|
+
*
|
|
12976
|
+
* Flow:
|
|
12977
|
+
* 1. User clicks "Sign in with NetSuite".
|
|
12978
|
+
* 2. Browser navigates to the Worker /auth/netsuite/start.
|
|
12979
|
+
* 3. Worker redirects to NetSuite's OAuth UI with PKCE.
|
|
12980
|
+
* 4. NetSuite redirects back to the Worker /auth/netsuite/callback.
|
|
12981
|
+
* 5. Worker exchanges the code, mints a session id, then redirects to
|
|
12982
|
+
* <return_to>#nssid=<sid>. URL fragments stay in the browser only.
|
|
12983
|
+
* 6. This module reads the fragment on load, stashes the sid in
|
|
12984
|
+
* sessionStorage, and sends it as Authorization: Bearer on every
|
|
12985
|
+
* Worker call.
|
|
12986
|
+
*
|
|
12987
|
+
* NEVER use a hardcoded password gate. NEVER use sessionStorage as the
|
|
12988
|
+
* authoritative auth state -- the only acceptable check is /api/me on the
|
|
12989
|
+
* Worker. sessionStorage here is just a cache of the bearer token.
|
|
12990
|
+
*/
|
|
12991
|
+
|
|
12992
|
+
export const WORKER_URL = import.meta.env.VITE_WORKER_URL ?? '';
|
|
12993
|
+
const TOKEN_KEY = 'nssid';
|
|
12994
|
+
|
|
12995
|
+
// Capture-and-strip the #nssid=... fragment as early as possible so URLs
|
|
12996
|
+
// shown to the user stay clean. Runs once at module load.
|
|
12997
|
+
(function bootstrapTokenFromHash() {
|
|
12998
|
+
if (typeof window === 'undefined') return;
|
|
12999
|
+
const hash = window.location.hash.replace(/^#/, '');
|
|
13000
|
+
if (!hash) return;
|
|
13001
|
+
const params = new URLSearchParams(hash);
|
|
13002
|
+
const sid = params.get(TOKEN_KEY);
|
|
13003
|
+
if (sid) {
|
|
13004
|
+
try { sessionStorage.setItem(TOKEN_KEY, sid); } catch {}
|
|
13005
|
+
params.delete(TOKEN_KEY);
|
|
13006
|
+
const remaining = params.toString();
|
|
13007
|
+
const newHash = remaining ? '#' + remaining : '';
|
|
13008
|
+
window.history.replaceState(null, '', window.location.pathname + window.location.search + newHash);
|
|
13009
|
+
}
|
|
13010
|
+
})();
|
|
13011
|
+
|
|
13012
|
+
export function getSessionToken(): string | null {
|
|
13013
|
+
try { return sessionStorage.getItem(TOKEN_KEY); } catch { return null; }
|
|
13014
|
+
}
|
|
13015
|
+
|
|
13016
|
+
export function signInWithNetSuite(returnTo: string = window.location.href) {
|
|
13017
|
+
if (!WORKER_URL) {
|
|
13018
|
+
throw new Error(
|
|
13019
|
+
'VITE_WORKER_URL is not set. HelloLeo Cloud must be provisioned before sign-in works.',
|
|
13020
|
+
);
|
|
13021
|
+
}
|
|
13022
|
+
const url = new URL(\`\${WORKER_URL.replace(/\\/$/, '')}/auth/netsuite/start\`);
|
|
13023
|
+
url.searchParams.set('return_to', returnTo);
|
|
13024
|
+
// The app usually renders inside the HelloLeo preview iframe. NetSuite's
|
|
13025
|
+
// OAuth page sets X-Frame-Options: DENY, so navigating the iframe to the
|
|
13026
|
+
// Worker (which 302s to NetSuite) ends up loading a page the browser
|
|
13027
|
+
// refuses to render -- looks like "nothing happens" to the user. Navigate
|
|
13028
|
+
// the top frame instead so the OAuth flow runs at the top level and the
|
|
12615
13029
|
// callback redirect lands on the same top frame, which then reads the
|
|
12616
13030
|
// #nssid= fragment. Fallback to same-frame navigation if the top window
|
|
12617
13031
|
// is cross-origin and can't be reassigned.
|
|
@@ -12799,448 +13213,73 @@ export default function App() {
|
|
|
12799
13213
|
}
|
|
12800
13214
|
`;
|
|
12801
13215
|
}
|
|
12802
|
-
|
|
12803
|
-
|
|
12804
|
-
|
|
12805
|
-
|
|
12806
|
-
name,
|
|
12807
|
-
private: true,
|
|
12808
|
-
version: "0.0.0",
|
|
12809
|
-
type: "module",
|
|
12810
|
-
scripts: {
|
|
12811
|
-
dev: "vite",
|
|
12812
|
-
start: "vite",
|
|
12813
|
-
build: "vite build",
|
|
12814
|
-
preview: "vite preview"
|
|
12815
|
-
},
|
|
12816
|
-
dependencies: {
|
|
12817
|
-
react: "^18.3.1",
|
|
12818
|
-
"react-dom": "^18.3.1",
|
|
12819
|
-
clsx: "^2.1.1",
|
|
12820
|
-
"tailwind-merge": "^2.6.0",
|
|
12821
|
-
"lucide-react": "^1.14.0"
|
|
12822
|
-
},
|
|
12823
|
-
devDependencies: {
|
|
12824
|
-
"@types/react": "^18.3.12",
|
|
12825
|
-
"@types/react-dom": "^18.3.1",
|
|
12826
|
-
"@vitejs/plugin-react": "^4.3.4",
|
|
12827
|
-
autoprefixer: "^10.4.20",
|
|
12828
|
-
postcss: "^8.4.49",
|
|
12829
|
-
tailwindcss: "^3.4.17",
|
|
12830
|
-
typescript: "^5.6.3",
|
|
12831
|
-
vite: "^6.0.5"
|
|
12832
|
-
}
|
|
12833
|
-
}, null, 2);
|
|
12834
|
-
}
|
|
12835
|
-
function indexHtml(fontFamily) {
|
|
12836
|
-
const fontParam = fontFamily.replace(/\s+/g, "+");
|
|
12837
|
-
return `<!DOCTYPE html>
|
|
12838
|
-
<html lang="en">
|
|
12839
|
-
<head>
|
|
12840
|
-
<meta charset="UTF-8" />
|
|
12841
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
12842
|
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
12843
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
12844
|
-
<link href="https://fonts.googleapis.com/css2?family=${fontParam}:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
12845
|
-
<title>App</title>
|
|
12846
|
-
</head>
|
|
12847
|
-
<body>
|
|
12848
|
-
<div id="root"></div>
|
|
12849
|
-
<script type="module" src="/src/main.tsx"></script>
|
|
12850
|
-
</body>
|
|
12851
|
-
</html>`;
|
|
12852
|
-
}
|
|
12853
|
-
function viteConfig() {
|
|
12854
|
-
return `import { defineConfig } from 'vite'
|
|
12855
|
-
import react from '@vitejs/plugin-react'
|
|
12856
|
-
import path from 'path'
|
|
12857
|
-
|
|
12858
|
-
export default defineConfig({
|
|
12859
|
-
plugins: [react()],
|
|
12860
|
-
server: {
|
|
12861
|
-
allowedHosts: true,
|
|
12862
|
-
},
|
|
12863
|
-
resolve: {
|
|
12864
|
-
alias: {
|
|
12865
|
-
'@': path.resolve(__dirname, './src'),
|
|
12866
|
-
},
|
|
12867
|
-
},
|
|
12868
|
-
})`;
|
|
12869
|
-
}
|
|
12870
|
-
function tailwindConfig() {
|
|
12871
|
-
return `export default {
|
|
12872
|
-
darkMode: ["class"],
|
|
12873
|
-
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
|
12874
|
-
theme: {
|
|
12875
|
-
extend: {
|
|
12876
|
-
colors: {
|
|
12877
|
-
background: "hsl(var(--background))",
|
|
12878
|
-
foreground: "hsl(var(--foreground))",
|
|
12879
|
-
card: { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))" },
|
|
12880
|
-
primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))" },
|
|
12881
|
-
secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))" },
|
|
12882
|
-
muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))" },
|
|
12883
|
-
accent: { DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))" },
|
|
12884
|
-
destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))" },
|
|
12885
|
-
success: { DEFAULT: "hsl(var(--success))", foreground: "hsl(var(--success-foreground))" },
|
|
12886
|
-
warning: { DEFAULT: "hsl(var(--warning))", foreground: "hsl(var(--warning-foreground))" },
|
|
12887
|
-
border: "hsl(var(--border))",
|
|
12888
|
-
input: "hsl(var(--input))",
|
|
12889
|
-
ring: "hsl(var(--ring))",
|
|
12890
|
-
},
|
|
12891
|
-
fontFamily: {
|
|
12892
|
-
sans: "var(--font-sans)",
|
|
12893
|
-
heading: "var(--font-heading)",
|
|
12894
|
-
},
|
|
12895
|
-
borderRadius: {
|
|
12896
|
-
lg: "var(--radius)",
|
|
12897
|
-
md: "calc(var(--radius) - 2px)",
|
|
12898
|
-
sm: "calc(var(--radius) - 4px)",
|
|
12899
|
-
},
|
|
12900
|
-
borderWidth: {
|
|
12901
|
-
DEFAULT: "var(--border-width)",
|
|
12902
|
-
},
|
|
12903
|
-
boxShadow: {
|
|
12904
|
-
DEFAULT: "var(--shadow)",
|
|
12905
|
-
lg: "var(--shadow-lg)",
|
|
12906
|
-
},
|
|
12907
|
-
},
|
|
12908
|
-
},
|
|
12909
|
-
plugins: [],
|
|
12910
|
-
}`;
|
|
12911
|
-
}
|
|
12912
|
-
function postcssConfig() {
|
|
12913
|
-
return `export default {
|
|
12914
|
-
plugins: {
|
|
12915
|
-
tailwindcss: {},
|
|
12916
|
-
autoprefixer: {},
|
|
12917
|
-
},
|
|
12918
|
-
}`;
|
|
12919
|
-
}
|
|
12920
|
-
function indexCss(primaryColor, fontFamily, borderRadius) {
|
|
12921
|
-
const fontValue = `'${fontFamily}', ui-sans-serif, system-ui, sans-serif`;
|
|
12922
|
-
return `/*
|
|
12923
|
-
* HelloLeo Design System -- READ BEFORE STYLING.
|
|
12924
|
-
*
|
|
12925
|
-
* The CSS variables below are the single source of truth for the project's
|
|
12926
|
-
* design tokens. They power the no-code Design panel in the HelloLeo webapp:
|
|
12927
|
-
* the user edits colors / fonts / radius / shadows visually, and the panel
|
|
12928
|
-
* writes back to THIS file. Keeping the conventions intact is what unlocks
|
|
12929
|
-
* that round-trip.
|
|
12930
|
-
*
|
|
12931
|
-
* Conventions (do not break):
|
|
12932
|
-
* - All colors stored as raw HSL triplets (no hsl() wrapper, no #hex).
|
|
12933
|
-
* Tailwind classes wrap them via hsl(var(--token)). Never inline #hex
|
|
12934
|
-
* in components -- always use design tokens (bg-primary, text-foreground,
|
|
12935
|
-
* border-border, etc.).
|
|
12936
|
-
* - --primary / --secondary / --accent / --destructive / --success /
|
|
12937
|
-
* --warning each have a paired -foreground for text on that surface.
|
|
12938
|
-
* - --radius drives lg/md/sm via calc() in tailwind.config.js. Edit here,
|
|
12939
|
-
* not in component classes.
|
|
12940
|
-
* - --font-sans for body, --font-heading for headings. Both load from the
|
|
12941
|
-
* Google Fonts <link> in index.html.
|
|
12942
|
-
* - .dark variants mirror :root one-for-one. When adding a token, add it
|
|
12943
|
-
* to BOTH blocks or dark mode breaks.
|
|
12944
|
-
*
|
|
12945
|
-
* UI primitives (Button, Card, Input, Modal, Table, Badge, Alert, Spinner)
|
|
12946
|
-
* MUST live in src/components/ui/ and consume these tokens via Tailwind
|
|
12947
|
-
* classes -- never hardcoded colors. One responsibility per file, <50 lines
|
|
12948
|
-
* per component.
|
|
12949
|
-
*/
|
|
12950
|
-
|
|
12951
|
-
@tailwind base;
|
|
12952
|
-
@tailwind components;
|
|
12953
|
-
@tailwind utilities;
|
|
12954
|
-
|
|
12955
|
-
@layer base {
|
|
12956
|
-
:root {
|
|
12957
|
-
--background: 0 0% 100%;
|
|
12958
|
-
--foreground: 222.2 84% 4.9%;
|
|
12959
|
-
--card: 0 0% 100%;
|
|
12960
|
-
--card-foreground: 222.2 84% 4.9%;
|
|
12961
|
-
--primary: ${primaryColor};
|
|
12962
|
-
--primary-foreground: 210 40% 98%;
|
|
12963
|
-
--secondary: 210 40% 96.1%;
|
|
12964
|
-
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
12965
|
-
--muted: 210 40% 96.1%;
|
|
12966
|
-
--muted-foreground: 215.4 16.3% 46.9%;
|
|
12967
|
-
--accent: 210 40% 96.1%;
|
|
12968
|
-
--accent-foreground: 222.2 47.4% 11.2%;
|
|
12969
|
-
--destructive: 0 84.2% 60.2%;
|
|
12970
|
-
--destructive-foreground: 210 40% 98%;
|
|
12971
|
-
--success: 142 76% 36%;
|
|
12972
|
-
--success-foreground: 210 40% 98%;
|
|
12973
|
-
--warning: 38 92% 50%;
|
|
12974
|
-
--warning-foreground: 222.2 47.4% 11.2%;
|
|
12975
|
-
--border: 214.3 31.8% 91.4%;
|
|
12976
|
-
--input: 214.3 31.8% 91.4%;
|
|
12977
|
-
--ring: 221.2 83.2% 53.3%;
|
|
12978
|
-
--radius: ${borderRadius};
|
|
12979
|
-
--border-width: 1px;
|
|
12980
|
-
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
|
12981
|
-
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|
12982
|
-
--font-sans: ${fontValue};
|
|
12983
|
-
--font-heading: ${fontValue};
|
|
12984
|
-
}
|
|
12985
|
-
|
|
12986
|
-
.dark {
|
|
12987
|
-
--background: 222.2 84% 4.9%;
|
|
12988
|
-
--foreground: 210 40% 98%;
|
|
12989
|
-
--card: 222.2 84% 4.9%;
|
|
12990
|
-
--card-foreground: 210 40% 98%;
|
|
12991
|
-
--primary: 217.2 91.2% 59.8%;
|
|
12992
|
-
--primary-foreground: 222.2 47.4% 11.2%;
|
|
12993
|
-
--secondary: 217.2 32.6% 17.5%;
|
|
12994
|
-
--secondary-foreground: 210 40% 98%;
|
|
12995
|
-
--muted: 217.2 32.6% 17.5%;
|
|
12996
|
-
--muted-foreground: 215 20.2% 65.1%;
|
|
12997
|
-
--accent: 217.2 32.6% 17.5%;
|
|
12998
|
-
--accent-foreground: 210 40% 98%;
|
|
12999
|
-
--destructive: 0 62.8% 30.6%;
|
|
13000
|
-
--destructive-foreground: 210 40% 98%;
|
|
13001
|
-
--success: 142 76% 36%;
|
|
13002
|
-
--success-foreground: 210 40% 98%;
|
|
13003
|
-
--warning: 38 92% 50%;
|
|
13004
|
-
--warning-foreground: 222.2 47.4% 11.2%;
|
|
13005
|
-
--border: 217.2 32.6% 17.5%;
|
|
13006
|
-
--input: 217.2 32.6% 17.5%;
|
|
13007
|
-
--ring: 224.3 76.3% 48%;
|
|
13008
|
-
--border-width: 1px;
|
|
13009
|
-
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.3), 0 1px 2px -1px rgb(0 0 0 / 0.3);
|
|
13010
|
-
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.3), 0 4px 6px -4px rgb(0 0 0 / 0.3);
|
|
13011
|
-
--font-sans: ${fontValue};
|
|
13012
|
-
--font-heading: ${fontValue};
|
|
13013
|
-
}
|
|
13014
|
-
}
|
|
13015
|
-
|
|
13016
|
-
body {
|
|
13017
|
-
@apply bg-background text-foreground font-sans;
|
|
13018
|
-
}`;
|
|
13019
|
-
}
|
|
13020
|
-
function utilsTs() {
|
|
13021
|
-
return `import { clsx, type ClassValue } from 'clsx'
|
|
13022
|
-
import { twMerge } from 'tailwind-merge'
|
|
13023
|
-
|
|
13024
|
-
export function cn(...inputs: ClassValue[]) {
|
|
13025
|
-
return twMerge(clsx(inputs))
|
|
13026
|
-
}`;
|
|
13027
|
-
}
|
|
13028
|
-
function proxyTs() {
|
|
13029
|
-
return `/*
|
|
13030
|
-
* HelloLeo Integration Proxy -- runtime helper for connected integrations.
|
|
13031
|
-
*
|
|
13032
|
-
* All third-party APIs (Odoo, Airtable, HubSpot, Stripe, Notion, Gladia,
|
|
13033
|
-
* Microsoft 365, Klaviyo, Supabase, DimoMaint, etc.) are reachable through
|
|
13034
|
-
* this single proxy. The proxy handles auth and CORS using credentials the
|
|
13035
|
-
* user already configured in Project Settings -- you NEVER pass an API key
|
|
13036
|
-
* from frontend code, and you NEVER call third-party hosts directly.
|
|
13037
|
-
*
|
|
13038
|
-
* Two modes:
|
|
13039
|
-
* A) Frontend-only project -> import callIntegration() from this file.
|
|
13040
|
-
* B) HelloLeo Cloud project -> route via Worker fetch(env.INTEGRATION_PROXY).
|
|
13041
|
-
* Use Cloud whenever the project needs backend logic, automations,
|
|
13042
|
-
* webhooks, scheduled jobs, or AI agents. Keep secrets server-side.
|
|
13043
|
-
*
|
|
13044
|
-
* Integration slugs:
|
|
13045
|
-
* Use the exact slug from the Enabled MCP list with the "pipedream-"
|
|
13046
|
-
* prefix removed. e.g. "pipedream-gorgias_oauth" -> "gorgias_oauth".
|
|
13047
|
-
* Native integrations (odoo, airtable) keep their plain name.
|
|
13048
|
-
*
|
|
13049
|
-
* Response shape (always wrapped):
|
|
13050
|
-
* { success: boolean, status: number, data: <provider payload> }
|
|
13051
|
-
*
|
|
13052
|
-
* Unwrap "data" by provider:
|
|
13053
|
-
* odoo -> data.result
|
|
13054
|
-
* airtable -> data.records
|
|
13055
|
-
* notion -> data.results
|
|
13056
|
-
* microsoft 365 -> data.value
|
|
13057
|
-
* klaviyo -> data.data
|
|
13058
|
-
* supabase / dimomaint -> data
|
|
13059
|
-
*
|
|
13060
|
-
* Pagination: put query params directly in the endpoint string,
|
|
13061
|
-
* e.g. endpoint: '/api/tickets?cursor=xyz&per_page=100'.
|
|
13062
|
-
* Read pagination metadata from the response to build the next call.
|
|
13063
|
-
* Never .filter() large lists in the frontend -- paginate server-side.
|
|
13064
|
-
*
|
|
13065
|
-
* Secrets for non-integration APIs (e.g. raw OpenAI): store them in
|
|
13066
|
-
* HelloLeo Cloud -> Settings -> Cloud -> Secrets, and read via env.SECRET_NAME
|
|
13067
|
-
* from a Worker. Never embed secrets in frontend code.
|
|
13068
|
-
*/
|
|
13069
|
-
|
|
13070
|
-
const PROJECT_UUID = import.meta.env.VITE_PROJECT_UUID ?? '';
|
|
13071
|
-
export const INTEGRATION_PROXY_URL =
|
|
13072
|
-
\`https://api.app.helloleo.dev/api/integration-proxy/\${PROJECT_UUID}\`;
|
|
13073
|
-
|
|
13074
|
-
export interface ProxyRequest {
|
|
13075
|
-
integration: string;
|
|
13076
|
-
endpoint: string;
|
|
13077
|
-
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
13078
|
-
body?: unknown;
|
|
13079
|
-
headers?: Record<string, string>;
|
|
13080
|
-
}
|
|
13081
|
-
|
|
13082
|
-
export interface ProxyResponse<T = unknown> {
|
|
13083
|
-
success: boolean;
|
|
13084
|
-
status: number;
|
|
13085
|
-
data: T;
|
|
13086
|
-
}
|
|
13087
|
-
|
|
13088
|
-
export async function callIntegration<T = unknown>(
|
|
13089
|
-
req: ProxyRequest,
|
|
13090
|
-
): Promise<ProxyResponse<T>> {
|
|
13091
|
-
const res = await fetch(INTEGRATION_PROXY_URL, {
|
|
13092
|
-
method: 'POST',
|
|
13093
|
-
headers: { 'Content-Type': 'application/json' },
|
|
13094
|
-
body: JSON.stringify({ method: 'POST', ...req }),
|
|
13095
|
-
});
|
|
13096
|
-
return res.json();
|
|
13097
|
-
}
|
|
13098
|
-
|
|
13099
|
-
/* Example -- Odoo search_read:
|
|
13100
|
-
*
|
|
13101
|
-
* const r = await callIntegration({
|
|
13102
|
-
* integration: 'odoo',
|
|
13103
|
-
* endpoint: '/jsonrpc',
|
|
13104
|
-
* body: {
|
|
13105
|
-
* model: 'product.template',
|
|
13106
|
-
* method: 'search_read',
|
|
13107
|
-
* args: [[['type', '=', 'consu']]],
|
|
13108
|
-
* kwargs: { fields: ['name', 'list_price'], limit: 50 },
|
|
13109
|
-
* },
|
|
13110
|
-
* });
|
|
13111
|
-
* const products = r.data?.result ?? [];
|
|
13112
|
-
*/
|
|
13113
|
-
`;
|
|
13114
|
-
}
|
|
13115
|
-
function hexToHslTriplet(hex3) {
|
|
13116
|
-
let h = hex3.trim().replace(/^#/, "");
|
|
13117
|
-
if (h.length === 3) {
|
|
13118
|
-
h = h.split("").map((c) => c + c).join("");
|
|
13119
|
-
}
|
|
13120
|
-
if (!/^[0-9a-fA-F]{6}$/.test(h))
|
|
13121
|
-
return "";
|
|
13122
|
-
const r = Number.parseInt(h.slice(0, 2), 16) / 255;
|
|
13123
|
-
const g = Number.parseInt(h.slice(2, 4), 16) / 255;
|
|
13124
|
-
const b = Number.parseInt(h.slice(4, 6), 16) / 255;
|
|
13125
|
-
const max = Math.max(r, g, b);
|
|
13126
|
-
const min = Math.min(r, g, b);
|
|
13127
|
-
const l = (max + min) / 2;
|
|
13128
|
-
let s = 0;
|
|
13129
|
-
let hue = 0;
|
|
13130
|
-
if (max !== min) {
|
|
13131
|
-
const d = max - min;
|
|
13132
|
-
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
13133
|
-
switch (max) {
|
|
13134
|
-
case r:
|
|
13135
|
-
hue = (g - b) / d + (g < b ? 6 : 0);
|
|
13136
|
-
break;
|
|
13137
|
-
case g:
|
|
13138
|
-
hue = (b - r) / d + 2;
|
|
13139
|
-
break;
|
|
13140
|
-
default:
|
|
13141
|
-
hue = (r - g) / d + 4;
|
|
13142
|
-
}
|
|
13143
|
-
hue *= 60;
|
|
13144
|
-
}
|
|
13145
|
-
return `${Math.round(hue)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`;
|
|
13146
|
-
}
|
|
13147
|
-
function normalizePrimaryColor(value) {
|
|
13148
|
-
const trimmed = value.trim();
|
|
13149
|
-
if (/^(#|[0-9a-fA-F]{3}$|[0-9a-fA-F]{6}$)/.test(trimmed)) {
|
|
13150
|
-
const hsl = hexToHslTriplet(trimmed);
|
|
13151
|
-
if (hsl)
|
|
13152
|
-
return hsl;
|
|
13153
|
-
}
|
|
13154
|
-
return trimmed;
|
|
13155
|
-
}
|
|
13156
|
-
function mainTsx() {
|
|
13157
|
-
return `import React from 'react'
|
|
13158
|
-
import ReactDOM from 'react-dom/client'
|
|
13159
|
-
import App from './App'
|
|
13160
|
-
import './index.css'
|
|
13161
|
-
|
|
13162
|
-
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
13163
|
-
<React.StrictMode>
|
|
13164
|
-
<App />
|
|
13165
|
-
</React.StrictMode>,
|
|
13166
|
-
)`;
|
|
13167
|
-
}
|
|
13168
|
-
var scaffold = tool({
|
|
13169
|
-
description: "Scaffold a new React + Vite + Tailwind CSS project with the HelloLeo design system. " + "Creates all mandatory boilerplate: package.json, index.html (with Google Fonts), " + "vite.config.js, tailwind.config.js, postcss.config.js, src/index.css (CSS variables " + "light+dark), src/lib/utils.ts (cn helper), and src/main.tsx. " + "Call this FIRST for any new project before writing App.tsx and components.",
|
|
13170
|
-
args: {
|
|
13171
|
-
name: tool.schema.string().optional().describe("Project name for package.json (default: leo-project)"),
|
|
13172
|
-
primaryColor: tool.schema.string().optional().describe("Primary brand color. Prefer raw HSL triplet (no hsl() wrapper, no # prefix), " + 'e.g. "221.2 83.2% 53.3%". Hex like "#3b82f6" is also accepted and will ' + "be auto-converted. Default: blue."),
|
|
13173
|
-
fontFamily: tool.schema.string().optional().describe('Google Font family name, e.g. "Inter", "Poppins" (default: Inter)'),
|
|
13174
|
-
borderRadius: tool.schema.string().optional().describe('Base border radius, e.g. "0.5rem", "0.75rem" (default: 0.5rem)'),
|
|
13175
|
-
integrations: tool.schema.array(tool.schema.string()).optional().describe("Connected integration slugs for this project (live MCP list). " + 'Pass ["netsuite"] when the project uses NetSuite -- the scaffold ' + "will add src/lib/netsuite.ts (OAuth client helper), " + "src/components/auth/{SignInWithNetSuite,LoginPage,AuthGate}.tsx " + "and a src/App.tsx wrapped in <AuthGate>. OAuth is wired from " + "day one. Forbids hardcoded password gates -- the gate IS the " + "real flow against the project's Worker.")
|
|
13176
|
-
},
|
|
13177
|
-
async execute(args, context) {
|
|
13216
|
+
var setupNetsuite = tool({
|
|
13217
|
+
description: "Bootstrap a NetSuite-backed React + Vite + Tailwind project in one call. " + "Writes the full HelloLeo base scaffold AND the NetSuite OAuth wiring " + "(src/lib/netsuite.ts, src/components/auth/{SignInWithNetSuite,LoginPage,AuthGate}.tsx, " + "src/App.tsx wrapped in <AuthGate>). " + "Use this INSTEAD of `scaffold` for any project that needs NetSuite. " + "Uses default theme (Inter font, blue primary, 0.5rem radius) -- edit " + "src/index.css afterwards to customise. Real OAuth is wired from day " + "one -- never replace the gate with a password placeholder.",
|
|
13218
|
+
args: {},
|
|
13219
|
+
async execute(_args, context) {
|
|
13178
13220
|
const dir = context.directory;
|
|
13179
|
-
const
|
|
13180
|
-
|
|
13181
|
-
|
|
13182
|
-
|
|
13183
|
-
const
|
|
13184
|
-
|
|
13185
|
-
|
|
13186
|
-
|
|
13187
|
-
await mkdir(path2.join(dir, "src", "components", "auth"), {
|
|
13188
|
-
recursive: true
|
|
13189
|
-
});
|
|
13190
|
-
}
|
|
13191
|
-
const files = [
|
|
13192
|
-
["package.json", packageJson(name)],
|
|
13193
|
-
["index.html", indexHtml(font)],
|
|
13194
|
-
["vite.config.js", viteConfig()],
|
|
13195
|
-
["tailwind.config.js", tailwindConfig()],
|
|
13196
|
-
["postcss.config.js", postcssConfig()],
|
|
13197
|
-
[path2.join("src", "index.css"), indexCss(primary, font, radius)],
|
|
13198
|
-
[path2.join("src", "lib", "utils.ts"), utilsTs()],
|
|
13199
|
-
[path2.join("src", "lib", "proxy.ts"), proxyTs()],
|
|
13200
|
-
[path2.join("src", "main.tsx"), mainTsx()]
|
|
13201
|
-
];
|
|
13202
|
-
if (hasNetsuite) {
|
|
13203
|
-
files.push([path2.join("src", "lib", "netsuite.ts"), netsuiteTs()], [
|
|
13204
|
-
path2.join("src", "components", "auth", "SignInWithNetSuite.tsx"),
|
|
13221
|
+
const base = await writeBaseScaffold({}, dir);
|
|
13222
|
+
await mkdir2(path3.join(dir, "src", "components", "auth"), {
|
|
13223
|
+
recursive: true
|
|
13224
|
+
});
|
|
13225
|
+
const nsFiles = [
|
|
13226
|
+
[path3.join("src", "lib", "netsuite.ts"), netsuiteTs()],
|
|
13227
|
+
[
|
|
13228
|
+
path3.join("src", "components", "auth", "SignInWithNetSuite.tsx"),
|
|
13205
13229
|
signInButtonTsx()
|
|
13206
|
-
],
|
|
13207
|
-
|
|
13230
|
+
],
|
|
13231
|
+
[
|
|
13232
|
+
path3.join("src", "components", "auth", "LoginPage.tsx"),
|
|
13208
13233
|
netsuiteLoginPageTsx()
|
|
13209
|
-
],
|
|
13210
|
-
|
|
13234
|
+
],
|
|
13235
|
+
[
|
|
13236
|
+
path3.join("src", "components", "auth", "AuthGate.tsx"),
|
|
13211
13237
|
netsuiteAuthGateTsx()
|
|
13212
|
-
],
|
|
13213
|
-
|
|
13214
|
-
|
|
13215
|
-
|
|
13216
|
-
|
|
13217
|
-
|
|
13218
|
-
|
|
13219
|
-
|
|
13220
|
-
|
|
13221
|
-
|
|
13238
|
+
],
|
|
13239
|
+
[path3.join("src", "App.tsx"), netsuiteAppTsx()]
|
|
13240
|
+
];
|
|
13241
|
+
const nsCreated = [];
|
|
13242
|
+
for (const [filePath, content] of nsFiles) {
|
|
13243
|
+
await Bun.write(path3.join(dir, filePath), content);
|
|
13244
|
+
nsCreated.push(filePath);
|
|
13245
|
+
}
|
|
13246
|
+
const all = [...base, ...nsCreated];
|
|
13247
|
+
return [
|
|
13248
|
+
`Scaffolded ${all.length} files (base + NetSuite OAuth):`,
|
|
13249
|
+
...all.map((f) => ` - ${f}`),
|
|
13222
13250
|
"",
|
|
13223
|
-
"Next:
|
|
13251
|
+
"Next: build your dashboard INSIDE <AuthGate> in src/App.tsx.",
|
|
13224
13252
|
"Design system reference: read the header comment in src/index.css",
|
|
13225
13253
|
" before adding tokens or styling primitives.",
|
|
13226
|
-
"
|
|
13227
|
-
"
|
|
13228
|
-
"
|
|
13229
|
-
"
|
|
13230
|
-
|
|
13231
|
-
|
|
13232
|
-
|
|
13233
|
-
|
|
13234
|
-
|
|
13254
|
+
"",
|
|
13255
|
+
"NetSuite auth -- already wired:",
|
|
13256
|
+
" - App.tsx wraps everything in <AuthGate>. NEVER remove the gate",
|
|
13257
|
+
" or build a password placeholder -- real OAuth is the only login.",
|
|
13258
|
+
" - LoginPage.tsx + SignInWithNetSuite.tsx are the unauthenticated",
|
|
13259
|
+
" view. Customise the copy / imagery, keep <SignInWithNetSuite/>",
|
|
13260
|
+
" as the call-to-action.",
|
|
13261
|
+
" - signInWithNetSuite() in src/lib/netsuite.ts hits the Worker at",
|
|
13262
|
+
" /auth/netsuite/start. Deploy the Worker via cloudflare-backend",
|
|
13263
|
+
" MCP with routes:",
|
|
13264
|
+
" GET /auth/netsuite/start -- redirect to NetSuite OAuth",
|
|
13265
|
+
" GET /auth/netsuite/callback -- code -> token exchange",
|
|
13266
|
+
" GET /api/me -- current session",
|
|
13267
|
+
" POST /auth/netsuite/logout -- clear session",
|
|
13268
|
+
" Worker reads env.NETSUITE_ACCOUNT_ID, env.NETSUITE_CLIENT_ID,",
|
|
13269
|
+
" env.NETSUITE_CLIENT_SECRET (set by the runtime-setup wizard).",
|
|
13270
|
+
" - VITE_WORKER_URL must be set so the frontend knows where to",
|
|
13271
|
+
" send users. The HelloLeo Cloud provisioner seeds this."
|
|
13272
|
+
].join(`
|
|
13235
13273
|
`);
|
|
13236
13274
|
}
|
|
13237
13275
|
});
|
|
13238
|
-
var
|
|
13276
|
+
var SetupNetsuitePlugin = async () => {
|
|
13239
13277
|
return {
|
|
13240
|
-
tool: {
|
|
13278
|
+
tool: { setup_netsuite: setupNetsuite }
|
|
13241
13279
|
};
|
|
13242
13280
|
};
|
|
13243
13281
|
export {
|
|
13282
|
+
SetupNetsuitePlugin,
|
|
13244
13283
|
ScaffoldPlugin,
|
|
13245
13284
|
RemovePlugin,
|
|
13246
13285
|
InspectHttpPlugin
|