@kernel.chat/kbot 3.99.15 → 3.99.16

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/cli.js CHANGED
@@ -879,6 +879,42 @@ async function main() {
879
879
  printInfo(` ${result.skipped} skipped (existing user-authored files preserved)`);
880
880
  printInfo(` → ${result.destination}`);
881
881
  });
882
+ program
883
+ .command('design <brief...>')
884
+ .description('Local-first alternative to Claude Design. Reads your repo\'s design tokens and generates an HTML prototype applying your visual system.')
885
+ .option('-o, --out <path>', 'Output file path (default: ./design-output/<slug>.html)')
886
+ .option('-k, --kind <kind>', 'Artifact kind: deck | page | prototype | one-pager', 'one-pager')
887
+ .option('--pdf', 'Also render to PDF via Playwright')
888
+ .option('--open', 'Open in default browser after generation')
889
+ .action(async (briefWords, opts) => {
890
+ const { runDesign } = await import('./design.js');
891
+ const brief = briefWords.join(' ');
892
+ if (!brief) {
893
+ printError('Usage: kbot design "<brief>" [--kind deck|page|prototype|one-pager] [--pdf] [--open]');
894
+ process.exit(1);
895
+ }
896
+ printInfo(`Designing ${opts.kind || 'one-pager'} from brief: "${brief.slice(0, 60)}${brief.length > 60 ? '…' : ''}"`);
897
+ try {
898
+ const out = await runDesign({
899
+ brief,
900
+ out: opts.out,
901
+ kind: opts.kind || 'one-pager',
902
+ pdf: opts.pdf,
903
+ open: opts.open,
904
+ });
905
+ printSuccess(`Written ${out}`);
906
+ if (opts.pdf) {
907
+ const pdf = out.replace(/\.html?$/i, '.pdf');
908
+ printInfo(`PDF: ${pdf}`);
909
+ }
910
+ if (!opts.open)
911
+ printInfo(`Preview: open ${out}`);
912
+ }
913
+ catch (err) {
914
+ printError(`Design failed: ${String(err).slice(0, 200)}`);
915
+ process.exit(1);
916
+ }
917
+ });
882
918
  program
883
919
  .command('doctor')
884
920
  .description('Diagnose your kbot setup — check everything is working')
@@ -0,0 +1,22 @@
1
+ export interface DesignOptions {
2
+ /** The design brief — what to build */
3
+ brief: string;
4
+ /** Output file path (default: ./design-output/<slug>.html) */
5
+ out?: string;
6
+ /** Kind of artifact: 'deck' | 'page' | 'prototype' | 'one-pager' */
7
+ kind?: 'deck' | 'page' | 'prototype' | 'one-pager';
8
+ /** Also render to PDF via Playwright */
9
+ pdf?: boolean;
10
+ /** Open in browser after generation */
11
+ open?: boolean;
12
+ }
13
+ /**
14
+ * Discover the repo's design tokens by scanning common CSS / config files.
15
+ * Returns a deduplicated string block the agent can reference.
16
+ */
17
+ export declare function extractDesignTokens(projectRoot: string): string;
18
+ /** Build the prompt that the aesthete specialist will receive. */
19
+ export declare function buildDesignPrompt(opts: DesignOptions, tokens: string): string;
20
+ /** Run the design command. Returns the output file path. */
21
+ export declare function runDesign(opts: DesignOptions, projectRoot?: string): Promise<string>;
22
+ //# sourceMappingURL=design.d.ts.map
package/dist/design.js ADDED
@@ -0,0 +1,161 @@
1
+ // kbot design — local-first alternative to Claude Design.
2
+ //
3
+ // Reads the current repo's CSS design tokens, feeds them to the aesthete
4
+ // specialist along with the user's brief, writes an HTML prototype to disk
5
+ // that applies those tokens, and optionally renders it to PDF via Playwright.
6
+ //
7
+ // No subscription. No cloud. $0 with a local model. The generated HTML lives
8
+ // on your machine, your design tokens stay local, your prototype is yours.
9
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
10
+ import { join, resolve, dirname } from 'node:path';
11
+ import { homedir } from 'node:os';
12
+ /**
13
+ * Discover the repo's design tokens by scanning common CSS / config files.
14
+ * Returns a deduplicated string block the agent can reference.
15
+ */
16
+ export function extractDesignTokens(projectRoot) {
17
+ const candidates = [
18
+ 'src/index.css',
19
+ 'src/styles/tokens.css',
20
+ 'src/tokens.css',
21
+ 'app/globals.css',
22
+ 'styles/globals.css',
23
+ 'tailwind.config.ts',
24
+ 'tailwind.config.js',
25
+ 'src/theme.ts',
26
+ 'src/theme.css',
27
+ ];
28
+ const sections = [];
29
+ for (const rel of candidates) {
30
+ const path = join(projectRoot, rel);
31
+ if (!existsSync(path))
32
+ continue;
33
+ try {
34
+ const content = readFileSync(path, 'utf-8');
35
+ // Only grab the token-declaring sections, not full CSS
36
+ const customProps = content.match(/--[\w-]+\s*:\s*[^;}\n]+/g) ?? [];
37
+ const colorDecls = content.match(/#[0-9a-fA-F]{3,8}\b|rgb\([^)]+\)|hsl\([^)]+\)/g) ?? [];
38
+ const fontDecls = content.match(/font-family\s*:\s*[^;}\n]+/g) ?? [];
39
+ if (customProps.length > 0 || colorDecls.length > 0 || fontDecls.length > 0) {
40
+ const uniqueProps = [...new Set(customProps)].slice(0, 40);
41
+ const uniqueColors = [...new Set(colorDecls)].slice(0, 15);
42
+ const uniqueFonts = [...new Set(fontDecls)].slice(0, 5);
43
+ sections.push(`## Tokens from ${rel}\n\n${[
44
+ ...uniqueProps,
45
+ ...uniqueColors.map(c => `color: ${c}`),
46
+ ...uniqueFonts,
47
+ ].join('\n')}`);
48
+ }
49
+ }
50
+ catch { /* skip unreadable */ }
51
+ }
52
+ if (sections.length === 0) {
53
+ return '(no design tokens found — using sensible defaults)';
54
+ }
55
+ return sections.join('\n\n');
56
+ }
57
+ /** Build the prompt that the aesthete specialist will receive. */
58
+ export function buildDesignPrompt(opts, tokens) {
59
+ const kind = opts.kind ?? 'one-pager';
60
+ const kindGuide = {
61
+ 'deck': 'A pitch deck — multiple slides via <section class="slide"> blocks, one idea per slide, big typography, clear hierarchy. Include title, problem, solution, market, team, ask slides as baseline.',
62
+ 'page': 'A responsive landing page — hero, feature grid, social proof, CTA. Mobile-first.',
63
+ 'prototype': 'An interactive product prototype — clickable navigation, realistic fake data, hover states, at least one interactive element (modal, form, toggle).',
64
+ 'one-pager': 'A single-scroll one-pager — hero, three value props, visual diagram, conclusion. Print-friendly.',
65
+ };
66
+ return `You are the aesthete specialist producing a polished HTML artifact.
67
+
68
+ ## Brief
69
+ ${opts.brief}
70
+
71
+ ## Artifact kind: ${kind}
72
+ ${kindGuide[kind]}
73
+
74
+ ## Design tokens (USE THESE — respect the user's system)
75
+ ${tokens}
76
+
77
+ ## Requirements
78
+ 1. Output a SINGLE complete HTML file with inline <style>. No external deps.
79
+ 2. Use the design tokens above if provided (custom properties, colors, fonts).
80
+ 3. Mobile-first responsive — test at 375px, 768px, 1440px.
81
+ 4. Accessible: semantic HTML, aria-labels on interactive elements, color-contrast AA minimum.
82
+ 5. No JavaScript unless absolutely required for prototype interactivity.
83
+ 6. Typography system: clear scale, consistent rhythm, no orphan widows.
84
+ 7. If the user has Rubin tokens (--rubin-*), EB Garamond/Courier Prime, use them. Otherwise pick a tasteful system-font stack.
85
+ 8. Print to stdout ONLY the HTML, starting with <!DOCTYPE html>. No explanations, no markdown fences.`;
86
+ }
87
+ /** Run the design command. Returns the output file path. */
88
+ export async function runDesign(opts, projectRoot = process.cwd()) {
89
+ const tokens = extractDesignTokens(projectRoot);
90
+ const prompt = buildDesignPrompt(opts, tokens);
91
+ // Route through the agent with the aesthete specialist.
92
+ // skipPlanner: bypass the complexity-detector planner — design prompts are
93
+ // long-form creative, not multi-step tasks, so plan mode returns scaffolding
94
+ // instead of the HTML we need.
95
+ const { runAgent } = await import('./agent.js');
96
+ const response = await runAgent(prompt, {
97
+ agent: 'aesthete',
98
+ stream: false,
99
+ plan: false,
100
+ skipPlanner: true,
101
+ });
102
+ // Extract the HTML — model may wrap in markdown fences despite instructions
103
+ let html = response.content.trim();
104
+ const fenceMatch = html.match(/```(?:html)?\s*([\s\S]*?)```/);
105
+ if (fenceMatch)
106
+ html = fenceMatch[1].trim();
107
+ if (!html.toLowerCase().includes('<!doctype')) {
108
+ html = `<!DOCTYPE html>\n${html}`;
109
+ }
110
+ // Determine output path
111
+ const slug = opts.brief
112
+ .toLowerCase()
113
+ .replace(/[^a-z0-9\s-]/g, '')
114
+ .trim()
115
+ .replace(/\s+/g, '-')
116
+ .slice(0, 50) || 'design';
117
+ const outPath = opts.out
118
+ ? resolve(opts.out.startsWith('~/') ? join(homedir(), opts.out.slice(2)) : opts.out)
119
+ : resolve(projectRoot, 'design-output', `${slug}.html`);
120
+ mkdirSync(dirname(outPath), { recursive: true });
121
+ writeFileSync(outPath, html);
122
+ // Optional: render to PDF via Playwright
123
+ if (opts.pdf) {
124
+ try {
125
+ await renderToPdf(outPath);
126
+ }
127
+ catch (err) {
128
+ process.stderr.write(` (PDF render failed: ${String(err).slice(0, 80)})\n`);
129
+ }
130
+ }
131
+ // Optional: open in default browser
132
+ if (opts.open) {
133
+ try {
134
+ const { spawn } = await import('node:child_process');
135
+ const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
136
+ spawn(opener, [outPath], { detached: true, stdio: 'ignore' }).unref();
137
+ }
138
+ catch { /* non-critical */ }
139
+ }
140
+ return outPath;
141
+ }
142
+ /** Render HTML to PDF via Playwright if it's available. */
143
+ async function renderToPdf(htmlPath) {
144
+ const pdfPath = htmlPath.replace(/\.html?$/i, '.pdf');
145
+ // Dynamic import — Playwright is an optional peer dep
146
+ const playwright = await import('playwright').catch(() => null);
147
+ if (!playwright) {
148
+ throw new Error('playwright not installed; run `npm i -g playwright && npx playwright install chromium`');
149
+ }
150
+ const browser = await playwright.chromium.launch();
151
+ try {
152
+ const page = await browser.newPage();
153
+ await page.goto(`file://${htmlPath}`, { waitUntil: 'networkidle' });
154
+ await page.pdf({ path: pdfPath, format: 'A4', printBackground: true });
155
+ return pdfPath;
156
+ }
157
+ finally {
158
+ await browser.close();
159
+ }
160
+ }
161
+ //# sourceMappingURL=design.js.map
@@ -70,10 +70,71 @@ export function registerSearchTools() {
70
70
  }
71
71
  catch { /* skip */ }
72
72
  }
73
+ // Source 4: Brave Search (general web results, requires free API key)
74
+ // Sign up at https://api.search.brave.com — 2000 free queries/month.
75
+ const braveKey = process.env.BRAVE_API_KEY || process.env.BRAVE_SEARCH_API_KEY;
76
+ if (braveKey && parts.length < 2) {
77
+ try {
78
+ const encoded = encodeURIComponent(query);
79
+ const res = await fetch(`https://api.search.brave.com/res/v1/web/search?q=${encoded}&count=5`, {
80
+ headers: {
81
+ 'Accept': 'application/json',
82
+ 'X-Subscription-Token': braveKey,
83
+ },
84
+ signal: AbortSignal.timeout(8000),
85
+ });
86
+ if (res.ok) {
87
+ const data = await res.json();
88
+ const items = data.web?.results ?? [];
89
+ if (items.length > 0) {
90
+ parts.push('**Brave Search:**');
91
+ for (const item of items.slice(0, 5)) {
92
+ const desc = (item.description || '').replace(/<[^>]+>/g, '').slice(0, 200);
93
+ parts.push(`- [${item.title}](${item.url}) — ${desc}`);
94
+ }
95
+ }
96
+ }
97
+ }
98
+ catch { /* skip */ }
99
+ }
100
+ // Source 5: DuckDuckGo HTML search (fallback when instant answers empty).
101
+ // No API key required. Free but fragile to HTML changes.
102
+ if (parts.length === 0) {
103
+ try {
104
+ const encoded = encodeURIComponent(query);
105
+ const res = await fetch(`https://html.duckduckgo.com/html/?q=${encoded}`, {
106
+ headers: { 'User-Agent': 'Mozilla/5.0 (compatible; KBot/2.0)' },
107
+ signal: AbortSignal.timeout(8000),
108
+ });
109
+ if (res.ok) {
110
+ const html = await res.text();
111
+ // Extract result snippets — DuckDuckGo HTML structure
112
+ const resultPattern = /<a[^>]+class="result__a"[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>[\s\S]*?<a[^>]+class="result__snippet"[^>]*>([^<]+)</g;
113
+ const results = [];
114
+ let m;
115
+ while ((m = resultPattern.exec(html)) !== null && results.length < 5) {
116
+ const url = m[1].replace(/^\/\/duckduckgo\.com\/l\/\?uddg=/, '')
117
+ .split('&')[0];
118
+ const decoded = decodeURIComponent(url);
119
+ const title = m[2].trim();
120
+ const snippet = m[3].trim();
121
+ results.push(`- [${title}](${decoded}) — ${snippet}`);
122
+ }
123
+ if (results.length > 0) {
124
+ parts.push('**Web results:**');
125
+ parts.push(...results);
126
+ }
127
+ }
128
+ }
129
+ catch { /* skip */ }
130
+ }
73
131
  if (parts.length > 0) {
74
132
  return parts.join('\n\n');
75
133
  }
76
- return `No instant results for "${query}". Try:\n- url_fetch with a specific documentation URL\n- research tool for deeper investigation`;
134
+ const hint = braveKey
135
+ ? `\n- Check BRAVE_API_KEY is valid and has quota`
136
+ : `\n- Set BRAVE_API_KEY (free at api.search.brave.com, 2000/mo) for real web search`;
137
+ return `No results for "${query}". Try:\n- url_fetch with a specific documentation URL\n- research tool for deeper investigation${hint}`;
77
138
  },
78
139
  });
79
140
  registerTool({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kernel.chat/kbot",
3
- "version": "3.99.15",
3
+ "version": "3.99.16",
4
4
  "description": "Open-source terminal AI agent. 787+ tools, 35 agents, 20 providers. Dreams, learns, watches your system. Controls your phone. Fully local, fully sovereign. MIT.",
5
5
  "type": "module",
6
6
  "repository": {