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.
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Chat command: load config, build system prompt, send user message, print reply.
3
+ * Conversation is persisted in .potion-kit/chat-history.json (project-scoped) so
4
+ * you can chat over multiple runs and build the site iteratively.
5
+ * - With no args: interactive mode (readline loop; type "exit" or Ctrl+C to quit).
6
+ * - With a message: one-shot, then exit. Use `potion-kit clear` to start a new conversation.
7
+ * Exits with clear error if .env / API keys are missing.
8
+ */
9
+ import { existsSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { createInterface } from "node:readline";
12
+ import { loadLlmConfig } from "../config/index.js";
13
+ import { createChat } from "../ai/client.js";
14
+ import { getFullSystemPrompt } from "../ai/system-prompt.js";
15
+ import { readHistory, writeHistory } from "./chat-history.js";
16
+ import { cli, buildProgressMessage } from "../cli/formatting.js";
17
+ const DEFAULT_MESSAGE = "What can you help me build? I’d like to create a static site with Handlebars and the UIPotion components.";
18
+ const EXIT_COMMANDS = ["exit", "quit", "q"];
19
+ function formatChatError(err) {
20
+ const msg = err instanceof Error ? err.message : String(err);
21
+ if (/not a chat model|v1\/chat\/completions|v1\/completions/i.test(msg)) {
22
+ return (msg +
23
+ "\n\nUse a chat model (e.g. gpt-5.2, gpt-4o), not a completion-only model. Set POTION_KIT_MODEL in .env or ~/.potion-kit/config.json.");
24
+ }
25
+ return msg;
26
+ }
27
+ function printConfigError() {
28
+ const cwd = process.cwd();
29
+ const envPath = join(cwd, ".env");
30
+ const examplePath = join(cwd, ".env.example");
31
+ console.error(cli.error("potion-kit: missing LLM configuration.\n"));
32
+ console.error(cli.error("Create a .env file in this directory (or set env vars) with:"));
33
+ console.error(cli.error(" POTION_KIT_PROVIDER=openai # or anthropic"));
34
+ console.error(cli.error(" OPENAI_API_KEY=sk-... # if provider is openai"));
35
+ console.error(cli.error(" ANTHROPIC_API_KEY=... # if provider is anthropic\n"));
36
+ if (!existsSync(envPath)) {
37
+ console.error(cli.error(`No .env found in ${cwd}.`));
38
+ if (existsSync(examplePath)) {
39
+ console.error(cli.error("Copy .env.example to .env: cp .env.example .env"));
40
+ }
41
+ else {
42
+ console.error(cli.error("Add a .env file with the variables above."));
43
+ }
44
+ }
45
+ else {
46
+ console.error(cli.error(`Found .env in ${cwd}; check POTION_KIT_PROVIDER and the matching API key.`));
47
+ }
48
+ console.error("");
49
+ }
50
+ function buildMessages(systemPrompt, history, userMessage) {
51
+ const conversation = [
52
+ ...history.map((m) => ({ role: m.role, content: m.content })),
53
+ { role: "user", content: userMessage },
54
+ ];
55
+ return [{ role: "system", content: systemPrompt }, ...conversation];
56
+ }
57
+ export async function runChat(messageParts) {
58
+ const config = loadLlmConfig();
59
+ if (!config) {
60
+ printConfigError();
61
+ process.exit(1);
62
+ }
63
+ const cwd = process.cwd();
64
+ const hasMessage = messageParts.length > 0;
65
+ const userMessage = hasMessage ? messageParts.join(" ").trim() : "";
66
+ if (hasMessage) {
67
+ await runOneShot(cwd, config, userMessage);
68
+ return;
69
+ }
70
+ await runInteractive(cwd, config);
71
+ }
72
+ const DEFAULT_PROGRESS_TEXT = "Thinking… (may take a minute when using tools)";
73
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴"];
74
+ const SPIN_INTERVAL_MS = 80;
75
+ const LINE_PAD = 80; // width to clear when redrawing
76
+ function createProgressReporter() {
77
+ let intervalId = null;
78
+ let currentMessage = DEFAULT_PROGRESS_TEXT;
79
+ let frameIndex = 0;
80
+ function tick() {
81
+ const frame = SPINNER_FRAMES[frameIndex % SPINNER_FRAMES.length];
82
+ const line = cli.spinner(frame) + " " + cli.progress(currentMessage);
83
+ const pad = " ".repeat(Math.max(0, LINE_PAD - (frame.length + 1 + currentMessage.length)));
84
+ process.stdout.write("\r" + line + pad + "\r");
85
+ frameIndex += 1;
86
+ }
87
+ const HIDE_CURSOR = "\x1b[?25l";
88
+ const SHOW_CURSOR = "\x1b[?25h";
89
+ return {
90
+ start: () => {
91
+ currentMessage = DEFAULT_PROGRESS_TEXT;
92
+ frameIndex = 0;
93
+ process.stdout.write(HIDE_CURSOR);
94
+ intervalId = setInterval(tick, SPIN_INTERVAL_MS);
95
+ },
96
+ onProgress: (msg) => {
97
+ currentMessage = msg;
98
+ },
99
+ progressMessageBuilder: (step, max, toolNames) => buildProgressMessage(step, max, toolNames),
100
+ clear: () => {
101
+ if (intervalId !== null) {
102
+ clearInterval(intervalId);
103
+ intervalId = null;
104
+ }
105
+ process.stdout.write("\r\x1b[2K\r" + SHOW_CURSOR);
106
+ },
107
+ };
108
+ }
109
+ async function runOneShot(cwd, config, userMessage) {
110
+ const systemPrompt = await getFullSystemPrompt();
111
+ const progress = createProgressReporter();
112
+ const chat = createChat(config, {
113
+ onProgress: progress.onProgress,
114
+ progressMessageBuilder: progress.progressMessageBuilder,
115
+ onError: (msg) => console.error(cli.error(msg)),
116
+ });
117
+ const history = readHistory(cwd);
118
+ const message = userMessage || DEFAULT_MESSAGE;
119
+ const messages = buildMessages(systemPrompt, history, message);
120
+ try {
121
+ console.log(cli.user("You: ") + message);
122
+ progress.start();
123
+ try {
124
+ const reply = await chat.send(messages);
125
+ progress.clear();
126
+ const replyToSave = reply.trim() || "(No text reply from the model.)";
127
+ writeHistory(cwd, [
128
+ ...history,
129
+ { role: "user", content: message },
130
+ { role: "assistant", content: replyToSave },
131
+ ]);
132
+ if (reply.trim()) {
133
+ console.log("\n" + cli.separator());
134
+ console.log(cli.agentLabel("Potion-kit:") + "\n");
135
+ console.log(cli.agentReply(reply));
136
+ }
137
+ else {
138
+ console.log(cli.intro("The model didn't return any text this time (it may have only run tools). Try rephrasing your request."));
139
+ }
140
+ }
141
+ finally {
142
+ progress.clear();
143
+ }
144
+ }
145
+ catch (err) {
146
+ console.error(cli.error("potion-kit: chat failed: " + formatChatError(err)));
147
+ process.exit(1);
148
+ }
149
+ }
150
+ async function runInteractive(cwd, config) {
151
+ const systemPrompt = await getFullSystemPrompt();
152
+ const progress = createProgressReporter();
153
+ const chat = createChat(config, {
154
+ onProgress: progress.onProgress,
155
+ progressMessageBuilder: progress.progressMessageBuilder,
156
+ onError: (msg) => console.error(cli.error(msg)),
157
+ });
158
+ let history = readHistory(cwd);
159
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
160
+ function saveAndExit() {
161
+ writeHistory(cwd, history);
162
+ rl.close();
163
+ process.exit(0);
164
+ }
165
+ process.on("SIGINT", () => {
166
+ rl.close();
167
+ writeHistory(cwd, history);
168
+ process.exit(0);
169
+ });
170
+ console.log(cli.intro('Chat with the AI to build your site. Type "exit" or Ctrl+C to quit.\n'));
171
+ function prompt() {
172
+ rl.question(cli.user("You: "), async (line) => {
173
+ const input = line.trim();
174
+ if (!input || EXIT_COMMANDS.includes(input.toLowerCase())) {
175
+ saveAndExit();
176
+ return;
177
+ }
178
+ try {
179
+ const messages = buildMessages(systemPrompt, history, input);
180
+ progress.start();
181
+ try {
182
+ const reply = await chat.send(messages);
183
+ progress.clear();
184
+ const replyToSave = reply.trim() || "(No text reply from the model.)";
185
+ history = [
186
+ ...history,
187
+ { role: "user", content: input },
188
+ { role: "assistant", content: replyToSave },
189
+ ];
190
+ writeHistory(cwd, history);
191
+ if (reply.trim()) {
192
+ console.log("\n" + cli.separator());
193
+ console.log(cli.agentLabel("Potion-kit:") + "\n");
194
+ console.log(cli.agentReply(reply) + "\n");
195
+ }
196
+ else {
197
+ console.log("\n" +
198
+ cli.intro('The model didn\'t return any text this time (it may have only run tools). Try asking again or rephrase, e.g. "What were we building?" or "Summarize our plan."') +
199
+ "\n");
200
+ }
201
+ }
202
+ finally {
203
+ progress.clear();
204
+ }
205
+ }
206
+ catch (err) {
207
+ console.error(cli.error("potion-kit: chat failed: " + formatChatError(err)));
208
+ }
209
+ prompt();
210
+ });
211
+ }
212
+ prompt();
213
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Clear command: delete chat history for the current project so the next
3
+ * chat run starts a new conversation.
4
+ */
5
+ import { clearHistory } from "./chat-history.js";
6
+ export async function runClear() {
7
+ const cwd = process.cwd();
8
+ clearHistory(cwd);
9
+ console.log("Chat history cleared for this project. The next chat will start a new conversation.");
10
+ }
@@ -0,0 +1 @@
1
+ export { loadLlmConfig, CONFIG_DIR, CONFIG_FILE } from "./load.js";
@@ -0,0 +1,65 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { config as loadEnv } from "dotenv";
5
+ // Load .env from cwd (directory where the user ran potion-kit — works with global install).
6
+ // Does not override existing process.env, so exported vars (e.g. OPENAI_API_KEY) take precedence.
7
+ let envLoaded = false;
8
+ function ensureEnvLoaded() {
9
+ if (envLoaded)
10
+ return;
11
+ envLoaded = true;
12
+ loadEnv(); // dotenv default: path is process.cwd() + '/.env'
13
+ }
14
+ const CONFIG_DIR = join(homedir(), ".potion-kit");
15
+ const CONFIG_FILE = join(CONFIG_DIR, "config.json");
16
+ const PROVIDER_ENV = {
17
+ openai: "OPENAI_API_KEY",
18
+ anthropic: "ANTHROPIC_API_KEY",
19
+ };
20
+ const DEFAULT_MODELS = {
21
+ openai: "gpt-5.2",
22
+ anthropic: "claude-sonnet-4-5",
23
+ };
24
+ /**
25
+ * Load LLM config from:
26
+ * 1. .env in current working directory (if present)
27
+ * 2. Env vars: POTION_KIT_PROVIDER, POTION_KIT_MODEL, OPENAI_API_KEY / ANTHROPIC_API_KEY
28
+ * 3. Optional file: ~/.potion-kit/config.json (provider, model only; never put keys there).
29
+ * See config.example.json in this package.
30
+ */
31
+ export function loadLlmConfig() {
32
+ ensureEnvLoaded();
33
+ const provider = (process.env.POTION_KIT_PROVIDER ?? readConfigFile().provider);
34
+ const model = process.env.POTION_KIT_MODEL ?? readConfigFile().model;
35
+ if (!provider || !["openai", "anthropic"].includes(provider)) {
36
+ return null;
37
+ }
38
+ const apiKey = process.env[PROVIDER_ENV[provider]] ?? process.env.POTION_KIT_API_KEY;
39
+ if (!apiKey || typeof apiKey !== "string") {
40
+ return null;
41
+ }
42
+ return {
43
+ provider,
44
+ model: model ?? DEFAULT_MODELS[provider],
45
+ apiKey,
46
+ baseUrl: process.env.POTION_KIT_BASE_URL || undefined,
47
+ };
48
+ }
49
+ function readConfigFile() {
50
+ if (!existsSync(CONFIG_FILE)) {
51
+ return {};
52
+ }
53
+ try {
54
+ const raw = readFileSync(CONFIG_FILE, "utf-8");
55
+ const data = JSON.parse(raw);
56
+ return {
57
+ provider: data.provider,
58
+ model: data.model,
59
+ };
60
+ }
61
+ catch {
62
+ return {};
63
+ }
64
+ }
65
+ export { CONFIG_DIR, CONFIG_FILE };
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { Command } from "commander";
6
+ import { runChat } from "./commands/chat.js";
7
+ import { runClear } from "./commands/clear.js";
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ // Built output is dist/index.js → package.json is one level up
10
+ const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
11
+ const program = new Command();
12
+ program
13
+ .name("potion-kit")
14
+ .description("CLI to build static sites with Handlebars, Markdown, and SCSS")
15
+ .version(pkg.version ?? "0.0.0");
16
+ program
17
+ .command("chat [message...]", { isDefault: true })
18
+ .description("Chat with the AI (default command). Conversation is kept in .potion-kit/ so you can build the site over multiple turns.")
19
+ .action(async (messageParts) => {
20
+ await runChat(messageParts ?? []);
21
+ });
22
+ program
23
+ .command("clear")
24
+ .description("Clear chat history for this project (next chat starts a new conversation)")
25
+ .action(async () => {
26
+ await runClear();
27
+ });
28
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "potion-kit",
3
+ "version": "0.0.1",
4
+ "description": "CLI to build HaroldJS + UIPotion websites",
5
+ "author": "Julian Ćwirko <julian.io>",
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "bin": {
9
+ "potion-kit": "dist/index.js"
10
+ },
11
+ "scripts": {
12
+ "build": "rm -rf dist && tsc",
13
+ "start": "node dist/index.js",
14
+ "dev": "tsc --watch",
15
+ "prepare": "node .husky/install.mjs",
16
+ "prepublishOnly": "npm run build",
17
+ "test": "node --import tsx --test test/*.test.ts",
18
+ "typecheck": "tsc --noEmit",
19
+ "lint": "eslint src/ test/",
20
+ "lint:fix": "eslint src/ test/ --fix",
21
+ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
22
+ "format:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\"",
23
+ "pre-commit": "lint-staged && npm run typecheck"
24
+ },
25
+ "keywords": [
26
+ "harold",
27
+ "uipotion",
28
+ "static-site",
29
+ "cli",
30
+ "ui components"
31
+ ],
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/uiPotion/potion-kit.git"
36
+ },
37
+ "homepage": "https://github.com/uiPotion/potion-kit#readme",
38
+ "bugs": {
39
+ "url": "https://github.com/uiPotion/potion-kit/issues"
40
+ },
41
+ "files": [
42
+ "dist",
43
+ ".env.example",
44
+ "config.example.json",
45
+ ".husky/install.mjs"
46
+ ],
47
+ "lint-staged": {
48
+ "*.ts": [
49
+ "eslint --fix",
50
+ "prettier --write"
51
+ ]
52
+ },
53
+ "dependencies": {
54
+ "@ai-sdk/anthropic": "^1.0.0",
55
+ "@ai-sdk/openai": "^1.0.0",
56
+ "ai": "^4.0.0",
57
+ "chalk": "^5.6.2",
58
+ "commander": "^12.0.0",
59
+ "dotenv": "^16.4.0",
60
+ "zod": "^3.23.0"
61
+ },
62
+ "devDependencies": {
63
+ "@eslint/js": "^9.39.2",
64
+ "@types/node": "^20.0.0",
65
+ "eslint": "^9.39.2",
66
+ "eslint-config-prettier": "^10.1.8",
67
+ "eslint-plugin-prettier": "^5.5.5",
68
+ "husky": "^9.0.0",
69
+ "lint-staged": "^16.0.0",
70
+ "prettier": "^3.8.1",
71
+ "tsx": "^4.21.0",
72
+ "typescript": "^5.0.0",
73
+ "typescript-eslint": "^8.54.0"
74
+ },
75
+ "engines": {
76
+ "node": ">=20"
77
+ }
78
+ }