potion-kit 0.0.1

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/.env.example ADDED
@@ -0,0 +1,20 @@
1
+ # Potion-kit LLM config (copy to .env and set your keys)
2
+ # See https://ai-sdk.dev for provider setup.
3
+
4
+ # Provider: openai | anthropic
5
+ POTION_KIT_PROVIDER=openai
6
+
7
+ # Model id (optional; must be a chat model, not completion-only)
8
+ # OpenAI: gpt-5.2, gpt-4o, gpt-3.5-turbo
9
+ # Anthropic: claude-sonnet-4-5, claude-3-7-sonnet-latest
10
+ # POTION_KIT_MODEL=gpt-5.2
11
+
12
+ # API key for the chosen provider (set one)
13
+ OPENAI_API_KEY=
14
+ # ANTHROPIC_API_KEY=
15
+
16
+ # Optional: single key for any provider (fallback if OPENAI_API_KEY/ANTHROPIC_API_KEY not set)
17
+ # POTION_KIT_API_KEY=
18
+
19
+ # Optional: custom base URL (e.g. OpenAI-compatible proxy, LiteLLM, local model)
20
+ # POTION_KIT_BASE_URL=
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Husky installer: run only when in a git repo (e.g. clone + npm install).
3
+ * Skip when installed from npm registry (no .git), in CI, or in production.
4
+ * See https://typicode.github.io/husky/how-to.html#ci-server-and-docker
5
+ */
6
+ import { existsSync } from "node:fs";
7
+ import { resolve, dirname } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ const packageRoot = resolve(__dirname, "..");
12
+ const hasGit = existsSync(resolve(packageRoot, ".git"));
13
+
14
+ if (
15
+ !hasGit ||
16
+ process.env.CI === "true" ||
17
+ process.env.NODE_ENV === "production"
18
+ ) {
19
+ process.exit(0);
20
+ }
21
+
22
+ const { default: husky } = await import("husky");
23
+ console.log(husky());
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 potion-kit contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,166 @@
1
+ # Potion Kit
2
+
3
+ CLI to build websites with **HaroldJS** (haroldjs.com) and **UIPotion** (uipotion.com): static sites with Handlebars, Markdown, and SCSS only. Chat with the AI to design and build your site; the model uses the UIPotion catalog and HaroldJS conventions and can only suggest components from real specs.
4
+
5
+ > **Note:** potion-kit is actively developed — we’re improving and optimizing it over time. For the best experience we recommend the **newest OpenAI or Anthropic models**; they handle the outcome quality best. Extensive use consumes API tokens and costs depend on your provider’s pricing. By using the tool you accept it as-is; only you decide whether and how much to use it. We hope you enjoy building with it.
6
+
7
+ ## Commands
8
+
9
+ - **`potion-kit chat`** or **`potion-kit`** (default) — Interactive chat.
10
+ - **`potion-kit chat "message"`** — Send one message and exit (one-shot).
11
+ - **`potion-kit clear`** — Clear chat history for this project (next chat starts a new conversation).
12
+
13
+ ---
14
+
15
+ ## Usage (from npm)
16
+
17
+ Use potion-kit as an installed CLI: run it from any directory where you have a `.env` with your LLM API key. No need to clone this repo.
18
+
19
+ ### Install
20
+
21
+ **Option A — npx (no install):**
22
+
23
+ ```bash
24
+ npx potion-kit chat
25
+ ```
26
+
27
+ **Option B — global install:**
28
+
29
+ ```bash
30
+ npm install -g potion-kit
31
+ potion-kit chat
32
+ ```
33
+
34
+ ### Run in your project (or empty directory)
35
+
36
+ 1. **Go to the directory** where you want to work (new site or existing project). It can be an empty folder.
37
+
38
+ ```bash
39
+ mkdir my-site && cd my-site
40
+ ```
41
+
42
+ 2. **Create a `.env` file** in that directory with your LLM provider and API key. Minimal contents:
43
+
44
+ ```env
45
+ POTION_KIT_PROVIDER=openai
46
+ OPENAI_API_KEY=sk-your-key-here
47
+ ```
48
+
49
+ For Anthropic use `POTION_KIT_PROVIDER=anthropic` and `ANTHROPIC_API_KEY=...`. See [.env variables](#env-variables) for all options and [.env and security](#env-and-security).
50
+
51
+ 3. **Run potion-kit** from that same directory:
52
+
53
+ ```bash
54
+ npx potion-kit chat
55
+ # or, if installed globally:
56
+ potion-kit chat
57
+ ```
58
+
59
+ The tool reads `.env` from the **current working directory**, so always run `potion-kit` from the directory that contains your `.env` (and where you want the AI to read/write files).
60
+
61
+ ### Usage examples
62
+
63
+ **Interactive chat (recommended):**
64
+
65
+ ```bash
66
+ cd my-project
67
+ npx potion-kit chat
68
+ # or: potion-kit chat
69
+ ```
70
+
71
+ Type your message at the `You:` prompt, press Enter; the model replies. Type `exit`, `quit`, or `q` (or Ctrl+C) to quit. Your conversation is saved for the next run.
72
+
73
+ **One-shot (single message then exit):**
74
+
75
+ ```bash
76
+ npx potion-kit chat "I want a blog with a header and footer"
77
+ npx potion-kit chat "Add the navbar potion to the layout"
78
+ ```
79
+
80
+ **Start a new conversation** (clear history for this directory):
81
+
82
+ ```bash
83
+ npx potion-kit clear
84
+ npx potion-kit chat "Let's build a docs site"
85
+ ```
86
+
87
+ ### Config
88
+
89
+ Config is loaded in this order (later overrides earlier):
90
+
91
+ 1. **`.env` in the current working directory** (where you run potion-kit).
92
+ 2. **Environment variables** (e.g. `OPENAI_API_KEY`, `POTION_KIT_PROVIDER`).
93
+ 3. **`~/.potion-kit/config.json`** — provider and model only; **do not put API keys there**.
94
+
95
+ #### .env variables
96
+
97
+ | Variable | Required | Description |
98
+ |----------|----------|-------------|
99
+ | `POTION_KIT_PROVIDER` | yes | `openai` or `anthropic` |
100
+ | `OPENAI_API_KEY` or `ANTHROPIC_API_KEY` | one required | API key for the chosen provider |
101
+ | `POTION_KIT_MODEL` | no | Chat model id (defaults: `gpt-5.2` / `claude-sonnet-4-5`). Must be a **chat** model. |
102
+ | `POTION_KIT_API_KEY` | no | Fallback key if provider-specific key is not set |
103
+ | `POTION_KIT_BASE_URL` | no | Custom base URL (e.g. OpenAI proxy, LiteLLM) |
104
+
105
+ **Minimal `.env`:**
106
+
107
+ ```env
108
+ POTION_KIT_PROVIDER=openai
109
+ OPENAI_API_KEY=sk-your-key-here
110
+ ```
111
+
112
+ **With optional model:**
113
+
114
+ ```env
115
+ POTION_KIT_PROVIDER=openai
116
+ POTION_KIT_MODEL=gpt-5.2
117
+ OPENAI_API_KEY=sk-your-key-here
118
+ ```
119
+
120
+ #### .env and security
121
+
122
+ - **potion-kit never sends `.env` or API keys to the model.** Keys are read only by the CLI and used for authentication with the LLM provider (OpenAI/Anthropic). They are not included in the system prompt, chat history, or any message content sent to the model. The only way a key could appear in the conversation is if you paste it yourself in a chat message — so don’t.
123
+ - **Never commit `.env`.** It contains secrets. Add `.env` to your `.gitignore`. If the AI scaffolds a project for you, ensure `.env` is in that project’s `.gitignore` too.
124
+ - **Never put API keys in `~/.potion-kit/config.json`.** That file is for provider and model only. Use `.env` or environment variables for keys.
125
+ - **Never paste API keys in logs, issues, or chat.** If you paste a key into a chat message, it becomes part of the conversation and history.
126
+ - **Where to put `.env`:** In the directory from which you run `potion-kit` (usually your project root). One `.env` per project.
127
+
128
+ ### Chat history
129
+
130
+ Conversation is stored in **`.potion-kit/chat-history.json`** in the directory where you run `potion-kit chat`. The model uses it for context on the next run. Add `.potion-kit/` to `.gitignore` if you don’t want to commit chat history. Use `potion-kit clear` to reset history for that project.
131
+
132
+ ### Legal
133
+
134
+ potion-kit uses [UIPotion](https://uipotion.com) specifications and catalog. **By using potion-kit you are using UIPotion’s service and agree to the [UIPotion legal disclaimer and privacy policy](https://uipotion.com/legal).** That page covers disclaimers on AI-generated code, liability, and user responsibility. Please read it before use.
135
+
136
+ ---
137
+
138
+ ## Development (potion-kit repo)
139
+
140
+ For contributing to potion-kit or running from source.
141
+
142
+ ### Setup
143
+
144
+ ```bash
145
+ git clone <repo>
146
+ cd potion-kit
147
+ npm install
148
+ npm run build
149
+ ```
150
+
151
+ ### Run locally
152
+
153
+ ```bash
154
+ node dist/index.js --help
155
+ node dist/index.js chat
156
+ ```
157
+
158
+ Put a `.env` in the repo (or in a test directory) and run from there. Copy `.env.example` to `.env` and set your API key.
159
+
160
+ ### Scripts
161
+
162
+ - **`npm run build`** — Compile TypeScript to `dist/`.
163
+ - **`npm run lint`** — ESLint on `src/`. `npm run lint:fix` to auto-fix.
164
+ - **`npm run format`** — Prettier on `src/**/*.ts`. `npm run format:check` to only check.
165
+ - **`npm run typecheck`** — `tsc --noEmit`.
166
+ - **`npm run test`** — Run tests (Node built-in test runner; tests live in `test/`).
@@ -0,0 +1,4 @@
1
+ {
2
+ "provider": "openai",
3
+ "model": "gpt-5.2"
4
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Chat client using the Vercel AI SDK (https://ai-sdk.dev).
3
+ * Config drives which provider we use; tools are built-in (search_potions, get_potion_spec, get_harold_project_info, fetch_doc_page, write_project_file).
4
+ */
5
+ import { generateText } from "ai";
6
+ import { createAnthropic } from "@ai-sdk/anthropic";
7
+ import { createOpenAI } from "@ai-sdk/openai";
8
+ import { createPotionKitTools } from "./tools.js";
9
+ const REQUEST_TIMEOUT_MS = 300_000; // 5 minutes (multi-step tool use can be slow)
10
+ const MAX_STEPS = 16; // tool rounds (search, spec, read, write) plus a final text reply; scaffold can need many writes
11
+ /**
12
+ * Create a chat that uses the AI SDK with the configured provider.
13
+ * send(messages) uses the first message as system if role is 'system', rest as messages.
14
+ * Tools (search_potions, get_potion_spec, get_harold_project_info, read_project_file, fetch_doc_page, write_project_file) are always available; multi-step so the model can call tools then reply.
15
+ */
16
+ export function createChat(config, options = {}) {
17
+ const { onProgress, progressMessageBuilder, onError } = options;
18
+ const model = config.provider === "openai"
19
+ ? createOpenAI({ apiKey: config.apiKey, baseURL: config.baseUrl })(config.model)
20
+ : createAnthropic({ apiKey: config.apiKey })(config.model);
21
+ const tools = createPotionKitTools();
22
+ let stepCount = 0;
23
+ return {
24
+ async send(messages) {
25
+ stepCount = 0;
26
+ const system = messages.find((m) => m.role === "system")?.content;
27
+ const conversation = messages.filter((m) => m.role !== "system");
28
+ const controller = new AbortController();
29
+ const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
30
+ try {
31
+ const result = await generateText({
32
+ model,
33
+ system: system ?? undefined,
34
+ messages: conversation,
35
+ tools,
36
+ maxSteps: MAX_STEPS,
37
+ maxTokens: 16_384, // per-step output limit; "length" finish = hit this before replying
38
+ abortSignal: controller.signal,
39
+ onStepFinish: (stepResult) => {
40
+ stepCount++;
41
+ if (onProgress) {
42
+ const rawNames = stepResult.toolCalls?.map((t) => t.toolName) ?? [];
43
+ const uniqueNames = [...new Set(rawNames)];
44
+ const message = progressMessageBuilder
45
+ ? progressMessageBuilder(stepCount, MAX_STEPS, uniqueNames)
46
+ : `Step ${stepCount} (of up to ${MAX_STEPS})${uniqueNames.length ? ` — ${uniqueNames.join(", ")}` : " — Thinking"}…`;
47
+ onProgress(message);
48
+ }
49
+ },
50
+ });
51
+ const text = result.text?.trim() ?? "";
52
+ if (text)
53
+ return text;
54
+ const fromSteps = result.steps
55
+ .map((s) => s.text?.trim())
56
+ .filter(Boolean);
57
+ if (fromSteps.length) {
58
+ const combined = fromSteps.join("\n\n");
59
+ if (result.finishReason === "length") {
60
+ return (combined +
61
+ "\n\n[Reply was cut off by length limit; files may still have been created.]");
62
+ }
63
+ return combined;
64
+ }
65
+ // No text in any step: step limit or length; give a helpful fallback so the user isn't left with nothing
66
+ const stepsUsed = result.steps.length;
67
+ const hitStepLimit = result.finishReason === "tool-calls" || stepsUsed >= MAX_STEPS;
68
+ if (onError) {
69
+ onError(hitStepLimit
70
+ ? `Step limit reached (${stepsUsed} steps). The assistant may have created or updated files but didn't get to send a final message. Check your project and ask again if you want a summary or more changes.`
71
+ : `potion-kit: model returned no text (finishReason: ${result.finishReason}, steps: ${stepsUsed}). Try asking again.`);
72
+ }
73
+ else {
74
+ console.error(hitStepLimit
75
+ ? `Step limit reached (${stepsUsed} steps). Check your project for changes.`
76
+ : `potion-kit: model returned no text (finishReason: ${result.finishReason}, steps: ${stepsUsed}). Try asking again.`);
77
+ }
78
+ if (result.finishReason === "length") {
79
+ return "The reply was cut off by the output length limit, but any file creation in earlier steps should have completed. Check your project for new files (e.g. under src/). You can run the build and ask for follow-up changes.";
80
+ }
81
+ if (hitStepLimit) {
82
+ return "I hit the step limit while working on your project, so I didn't get to send a final message. Check your project for any files I created or updated (e.g. under src/). Run `npm run build` or `npm start` to try the site, and ask again if you want a summary or more changes.";
83
+ }
84
+ return "";
85
+ }
86
+ catch (err) {
87
+ if (err instanceof Error && err.name === "AbortError") {
88
+ throw new Error(`Request timed out after ${REQUEST_TIMEOUT_MS / 60_000} minutes. Try again or use a shorter message.`);
89
+ }
90
+ throw err;
91
+ }
92
+ finally {
93
+ clearTimeout(timeoutId);
94
+ }
95
+ },
96
+ };
97
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Canonical "what the static site stack is and how it works" (HaroldJS).
3
+ * Injected into the system prompt so the model has real knowledge
4
+ */
5
+ import { npmPackageLatestUrl } from "../endpoints.js";
6
+ import { getJson } from "../remote.js";
7
+ /** Fetch the latest harold-scripts version from npm; fallback to "latest" on error. */
8
+ export async function getHaroldScriptsLatestVersion() {
9
+ const data = await getJson(npmPackageLatestUrl("harold-scripts"), {
10
+ timeoutMs: 5000,
11
+ });
12
+ if (!data || typeof data.version !== "string")
13
+ return "latest";
14
+ return `^${data.version}`;
15
+ }
16
+ const HAROLD_CONTEXT_TEMPLATE = `
17
+ ## STATIC SITE STACK (how it works)
18
+
19
+ This is the only stack you use. All projects follow this structure and these conventions.
20
+
21
+ ### Config (project root: .haroldrc.json or package.json)
22
+ - mdFilesDirName: directory for markdown files (default "posts"); also used in URLs (/posts/name).
23
+ - mdFilesLayoutsDirName: directory for markdown layouts (default "blog-layouts").
24
+ - outputDirName: build output directory (default "build").
25
+ - hostDirName: optional subpath when hosting in a subdirectory.
26
+ - minifyHtml, minifyCss: optional (default true).
27
+
28
+ ### Source layout (under src/)
29
+ - pages/ — Handlebars pages (index.hbs, about.hbs). Output as .html at site root.
30
+ - partials/ — Handlebars partials; include with {{> partialName}} or {{> head title="Page"}}. No underscore prefix in filenames (head.hbs, footer.hbs).
31
+ - posts/ (or name from mdFilesDirName) — Markdown files. Output as .html under that path. Subdirs allowed; structure preserved in URLs.
32
+ - blog-layouts/ (or from mdFilesLayoutsDirName) — Layouts for markdown; referenced in front matter as layout: 'layout-name'.
33
+ - styles/ — SCSS/CSS. When scaffolding from zero use one main.scss only (no @import/@use). If the project already has multiple SCSS files or partials, keep that structure. harold-scripts compiles .scss/.css files that do not start with _.
34
+ - assets/ — Images, JS, fonts; copied to build.
35
+ - jsonData/ — Auto-generated (e.g. posts.json); do not hand-edit.
36
+ - statics/ — Files copied to output root (robots.txt, manifest.json).
37
+
38
+ ### Markdown front matter (required)
39
+ - layout — layout template name (string).
40
+ - title — page title (string).
41
+ - publicationDate — must be a valid YYYY-MM-DD string (e.g. 2025-01-15). Invalid or wrong-format dates break formatDate and the build.
42
+ Optional: excerpt, tags (array), coverImage, and any custom fields (all passed to the layout). Use LF line endings; CRLF can break parsing.
43
+
44
+ ### Handlebars helpers (always use these)
45
+ - {{relativePath 'path'}} — for ALL links and assets (so subdirectory hosting works). Never use absolute or root paths.
46
+ - {{formatDate date=publicationDate format='dd mmmm yyyy'}} — dates. The date= value must be a valid date string (YYYY-MM-DD only), e.g. publicationDate from front matter or a literal like '2025-01-15'. Never use date='now' or any non-date string (it causes Invalid date). For copyright year use date='2025-01-01' format='yyyy'. Format options: dd, d, mmmm, mmm, mm, yyyy.
47
+ - {{postsList perPageLimit=5 currentPage=1 byTagName="tag" className="..." dateFormat="dd mmmm yyyy" noTags= false noExcerpt= false noDate= false}} — render post lists from jsonData.
48
+ - {{responsiveImg src="..." alt="..." width="..." height="..." loading="lazy"}} — responsive images.
49
+ - {{hostDirName}} — for subdirectory-aware output (e.g. data-hrld-root="{{hostDirName}}").
50
+ - Partials: {{> head}}, {{> footer}}, {{> partialName param="value"}}.
51
+
52
+ ### Pages (.hbs)
53
+ - No DOCTYPE/html in page files (added by layout).
54
+ - Always use {{> partialName}} for header/footer and {{relativePath '...'}} for links and assets.
55
+ - Content in layout: {{{content}}} (triple braces for HTML from markdown).
56
+
57
+ ### Build process
58
+ 1. Prepare directories (build/, build/posts/).
59
+ 2. Copy assets (src/assets/ → build/assets/).
60
+ 3. Register Handlebars helpers and partials.
61
+ 4. Generate posts: Markdown + front matter → layout → HTML in build/posts/.
62
+ 5. Generate styles: SCSS → CSS → build/styles/.
63
+ 6. Generate pages: src/pages/*.hbs → HTML at build root.
64
+ 7. Copy jsonData and statics to build.
65
+ Commands: npm run build (full build), npm start (build + dev server + watch, e.g. localhost:3000). These require package.json with harold-scripts (see scaffold below).
66
+
67
+ ### Complete project scaffold (when the site is new or missing root setup)
68
+ If get_harold_project_info returns found: false, or the user has only src/ without a working build, create the full project so they can run npm install && npm run build.
69
+
70
+ **1. package.json (project root)** — Required. Must include:
71
+ - "scripts": { "build": "harold-scripts build", "start": "harold-scripts start" }
72
+ - "devDependencies": { "harold-scripts": "<version>" } — always use the newest version (injected below).
73
+ - "harold": { "mdFilesDirName": "posts", "mdFilesLayoutsDirName": "blog-layouts", "outputDirName": "build", "minifyHtml": true, "minifyCss": true }
74
+ - "name", "version", "private": true as needed. Example:
75
+ {"name":"my-site","version":"1.0.0","private":true,"scripts":{"build":"harold-scripts build","start":"harold-scripts start"},"devDependencies":{"harold-scripts":"__HAROLD_SCRIPTS_VERSION__"},"harold":{"mdFilesDirName":"posts","mdFilesLayoutsDirName":"blog-layouts","outputDirName":"build","minifyHtml":true,"minifyCss":true}}
76
+
77
+ **2. .gitignore (project root)** — Recommended. Include: build/, node_modules/, .env, .potion-kit/
78
+
79
+ **3. File structure (when starting from nothing)** — Keep the standard layout. Do not use @import or @use in SCSS.
80
+
81
+ - **src/styles/** — Create a single file main.scss with all styles in it. Do not add @import or @use; write plain SCSS/CSS only. No partials (_variables.scss etc.) when scaffolding from zero.
82
+ - **src/partials/** — At least head.hbs and footer.hbs (pages use {{> head}} {{> footer}}).
83
+ - **src/pages/** — At least index.hbs.
84
+ - **src/assets/** — Can be empty; create a .gitkeep or one file if the dir must exist.
85
+ - If the site will have a blog: src/posts/, src/blog-layouts/ with at least one layout.
86
+
87
+ **4. SCSS rule (scaffolding only)** — When starting from zero files (harold not implemented yet), use one main.scss only: no @import, no @use. Put all styles in that single file so the build works. Once the user has split SCSS into multiple files or uses partials/@import/@use, accept that structure and do not force a single file.
88
+
89
+ After creating package.json, tell the user to run: npm install && npm run build (or npm start for dev server).
90
+
91
+ ### Conventions and don'ts
92
+ - File naming: kebab-case for posts/pages; partials without _ prefix. When scaffolding from zero use one main.scss only; if the project already uses multiple SCSS files or @import/@use, keep that structure.
93
+ - Always use relativePath for href/src. Include excerpt and tags for SEO. Semantic HTML and a11y from UIPotion specs.
94
+ - Do not edit files in build/ (generated). Do not skip required front matter. Do not use absolute paths. When talking to users, you may mention HaroldJS (haroldjs.com) and that the UI is based on UIPotion specs (uipotion.com).
95
+ `;
96
+ /** Harold context with the latest harold-scripts version injected for scaffold. */
97
+ export async function getHaroldContext() {
98
+ const version = await getHaroldScriptsLatestVersion();
99
+ return HAROLD_CONTEXT_TEMPLATE.replace(/__HAROLD_SCRIPTS_VERSION__/g, version);
100
+ }
@@ -0,0 +1,2 @@
1
+ export { getHaroldContext, getHaroldScriptsLatestVersion } from "./harold.js";
2
+ export { fetchPotionsIndex, formatPotionsCatalog, getPotionsCatalogText, } from "./potions-catalog.js";
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Fetch the UIPotion index and return a text catalog so the model knows
3
+ * what potions exist and when to reach for them. Injected into the system prompt.
4
+ */
5
+ import { potionsIndexUrl } from "../endpoints.js";
6
+ import { getJson } from "../remote.js";
7
+ /**
8
+ * Fetch potions-index.json from UIPotion. Returns null on network/parse error.
9
+ */
10
+ export async function fetchPotionsIndex() {
11
+ return getJson(potionsIndexUrl);
12
+ }
13
+ const CATEGORY_ORDER = ["layouts", "components", "features", "patterns", "tooling"];
14
+ /**
15
+ * Build a text catalog from the potions index for injection into the system prompt.
16
+ * Grouped by category so the model knows what's available and when to use each.
17
+ */
18
+ export function formatPotionsCatalog(index) {
19
+ const potions = index.potions ?? [];
20
+ if (potions.length === 0) {
21
+ return "No UIPotion catalog available. Use search_potions and get_potion_spec tools to discover guides.";
22
+ }
23
+ const byCategory = new Map();
24
+ for (const p of potions) {
25
+ const cat = (p.category ?? "other").toLowerCase();
26
+ if (!byCategory.has(cat))
27
+ byCategory.set(cat, []);
28
+ byCategory.get(cat).push(p);
29
+ }
30
+ const lines = [
31
+ "## UI POTIONS (component and layout guides)",
32
+ "",
33
+ "You have access to these UIPotion guides. Use them when the user asks for a layout, component, feature, or pattern. When you need the full spec to generate code, call get_potion_spec(category, id) with the category and id below.",
34
+ "",
35
+ ];
36
+ for (const cat of CATEGORY_ORDER) {
37
+ const list = byCategory.get(cat);
38
+ if (!list?.length)
39
+ continue;
40
+ lines.push(`### ${cat}`);
41
+ for (const p of list) {
42
+ const excerpt = p.excerpt
43
+ ? ` — ${p.excerpt.slice(0, 80)}${p.excerpt.length > 80 ? "..." : ""}`
44
+ : "";
45
+ lines.push(`- **${p.id}**: ${p.name}${excerpt}`);
46
+ }
47
+ lines.push("");
48
+ }
49
+ const other = byCategory.get("other");
50
+ if (other?.length) {
51
+ lines.push("### other");
52
+ for (const p of other) {
53
+ lines.push(`- **${p.id}**: ${p.name}`);
54
+ }
55
+ lines.push("");
56
+ }
57
+ lines.push("When to reach for them: use **layouts** for full-page structure (dashboard, landing, docs); **components** for reusable UI (buttons, nav, cards, forms); **features** for complete flows (pricing, auth); **patterns** for interaction/design patterns; **tooling** for dev tooling. Always call get_potion_spec(category, id) to fetch the full JSON guide before generating code from a potion.");
58
+ return lines.join("\n");
59
+ }
60
+ /**
61
+ * Fetch the index and return the formatted catalog string for the system prompt.
62
+ * Use this when starting a chat session; cache the result if you want to avoid refetching.
63
+ */
64
+ export async function getPotionsCatalogText() {
65
+ const index = await fetchPotionsIndex();
66
+ if (!index) {
67
+ return "UIPotion catalog could not be loaded. Use search_potions and get_potion_spec tools to discover and fetch guides.";
68
+ }
69
+ return formatPotionsCatalog(index);
70
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * All remote endpoints used by potion-kit (npm, UIPotion, doc allowlist).
3
+ * Single source of truth for URLs and allowlisted hosts.
4
+ */
5
+ /** npm registry base (no trailing slash). */
6
+ export const NPM_REGISTRY_BASE = "https://registry.npmjs.org";
7
+ /** UIPotion site base (no trailing slash). */
8
+ export const UIPOTION_BASE = "https://uipotion.com";
9
+ /** HaroldJS doc site hostnames (for fetch_doc_page allowlist). */
10
+ export const DOC_ALLOWED_HOSTS = new Set([
11
+ "haroldjs.com",
12
+ "www.haroldjs.com",
13
+ "uipotion.com",
14
+ "www.uipotion.com",
15
+ ]);
16
+ /** URL for a package's "latest" version in the npm registry. */
17
+ export function npmPackageLatestUrl(packageName) {
18
+ return `${NPM_REGISTRY_BASE}/${packageName}/latest`;
19
+ }
20
+ /** URL for the UIPotion potions index JSON. */
21
+ export const potionsIndexUrl = `${UIPOTION_BASE}/potions-index.json`;
22
+ /** URL for a single potion spec JSON (category + id). */
23
+ export function potionSpecUrl(category, id) {
24
+ return `${UIPOTION_BASE}/potions/${category}/${id}.json`;
25
+ }
26
+ /** Whether a URL is allowed for fetch_doc_page (haroldjs.com or uipotion.com only). */
27
+ export function isDocUrlAllowed(url) {
28
+ try {
29
+ const host = new URL(url).hostname.toLowerCase();
30
+ return DOC_ALLOWED_HOSTS.has(host);
31
+ }
32
+ catch {
33
+ return false;
34
+ }
35
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Fetch a doc page or jsonData/posts.json from allowlisted domains only
3
+ * (haroldjs.com, uipotion.com). HaroldJS sites always generate jsonData/posts.json
4
+ * with the doc index (fileName, title, excerpt, etc.). The model can fetch that
5
+ * first, then open specific pages. Used as a fallback when info isn't in context
6
+ * or Potion specs.
7
+ */
8
+ import { DOC_ALLOWED_HOSTS } from "./endpoints.js";
9
+ import { get } from "./remote.js";
10
+ const MAX_CHARS = 14_000;
11
+ /**
12
+ * Strip HTML to rough plain text: remove script/style, then tags, collapse whitespace.
13
+ */
14
+ function htmlToText(html) {
15
+ return html
16
+ .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
17
+ .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "")
18
+ .replace(/<[^>]+>/g, " ")
19
+ .replace(/&nbsp;/g, " ")
20
+ .replace(/&amp;/g, "&")
21
+ .replace(/&lt;/g, "<")
22
+ .replace(/&gt;/g, ">")
23
+ .replace(/&quot;/g, '"')
24
+ .replace(/\s+/g, " ")
25
+ .trim();
26
+ }
27
+ export async function fetchDocPage(url) {
28
+ let parsed;
29
+ try {
30
+ parsed = new URL(url);
31
+ }
32
+ catch {
33
+ return { ok: false, error: "Invalid URL" };
34
+ }
35
+ if (!DOC_ALLOWED_HOSTS.has(parsed.hostname.toLowerCase())) {
36
+ return {
37
+ ok: false,
38
+ error: `URL must be from haroldjs.com or uipotion.com. Got: ${parsed.hostname}`,
39
+ };
40
+ }
41
+ try {
42
+ const res = await get(url);
43
+ if (!res.ok) {
44
+ return { ok: false, error: `HTTP ${res.status}` };
45
+ }
46
+ const contentType = res.headers.get("content-type") ?? "";
47
+ const isJson = parsed.pathname.endsWith("/jsonData/posts.json") || contentType.includes("application/json");
48
+ const raw = await res.text();
49
+ let text;
50
+ let title;
51
+ if (isJson) {
52
+ try {
53
+ const data = JSON.parse(raw);
54
+ text = JSON.stringify(data, null, 2);
55
+ }
56
+ catch {
57
+ text = raw;
58
+ }
59
+ }
60
+ else {
61
+ text = htmlToText(raw);
62
+ const titleMatch = raw.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
63
+ if (titleMatch)
64
+ title = htmlToText(titleMatch[1]);
65
+ }
66
+ const truncated = text.length > MAX_CHARS ? text.slice(0, MAX_CHARS) + "\n...[truncated]" : text;
67
+ return { ok: true, title, text: truncated };
68
+ }
69
+ catch (e) {
70
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
71
+ }
72
+ }