chat-glass 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lindow Consulting Ltd
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,71 @@
1
+ # chat-glass
2
+
3
+ Visual thinking companion for Claude Code.
4
+
5
+ chat-glass gives Claude Code the ability to create rich, interactive visualizations -- diagrams, charts, comparisons, flowcharts -- and display them in a live-reloading browser viewer.
6
+
7
+ ## What it does
8
+
9
+ When Claude Code needs to explain something visually, it delegates to the chat-glass agent. The agent writes a self-contained HTML file, and chat-glass serves it in a browser viewer with a gallery of past visualizations. Everything updates in real time via WebSocket.
10
+
11
+ ## Quick start
12
+
13
+ ```bash
14
+ npm install -g chat-glass
15
+ chat-glass install-agent
16
+ ```
17
+
18
+ That's it. The agent is now available in Claude Code. When a visual explanation would help, Claude will automatically use it.
19
+
20
+ To install the agent for only the current project:
21
+
22
+ ```bash
23
+ chat-glass install-agent --local
24
+ ```
25
+
26
+ ## How it works
27
+
28
+ 1. Claude Code decides a visual would help and delegates to the chat-glass subagent
29
+ 2. The subagent writes a self-contained HTML file to `.chat-glass/pages/`
30
+ 3. The subagent runs `chat-glass show`, which starts a local server (or reloads an existing one)
31
+ 4. The browser viewer updates in real time via WebSocket
32
+ 5. A gallery strip lets you browse all past visualizations
33
+
34
+ ## Commands
35
+
36
+ | Command | Description |
37
+ |---------|-------------|
38
+ | `chat-glass show` | Open the visualization viewer (starts server if needed) |
39
+ | `chat-glass list` | List saved visualizations |
40
+ | `chat-glass clean` | Delete all saved pages |
41
+ | `chat-glass install-agent` | Install the chat-glass agent file globally |
42
+ | `chat-glass install-agent --local` | Install the agent for the current project only |
43
+ | `chat-glass --help` | Show help |
44
+
45
+ ## Configuration
46
+
47
+ chat-glass stores data in a `.chat-glass/` directory in your project root:
48
+
49
+ ```
50
+ .chat-glass/
51
+ config.json # Server state (port, pid, timestamps)
52
+ pages/ # Generated HTML visualizations
53
+ latest.html # Symlink to most recent page
54
+ ```
55
+
56
+ The server listens on `127.0.0.1` on a port in the range 3737-3747 and shuts down automatically after 30 minutes of inactivity.
57
+
58
+ ## For contributors
59
+
60
+ ```bash
61
+ git clone https://github.com/tfLConsulting/chat-glass.git
62
+ cd chat-glass
63
+ npm install
64
+ npm test
65
+ ```
66
+
67
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for development guidelines.
68
+
69
+ ## License
70
+
71
+ [MIT](LICENSE)
@@ -0,0 +1,109 @@
1
+ ---
2
+ name: chat-glass
3
+ description: >
4
+ Use this agent whenever a visual explanation would be clearer than text.
5
+ Comparisons, architectures, flows, data, relationships, tradeoffs,
6
+ layouts, timelines, hierarchies — any time showing is better than telling.
7
+ Be proactive: if something would benefit from a visual, use this without
8
+ being asked.
9
+ tools: Write, Bash
10
+ ---
11
+
12
+ You are a visualization agent. You receive a plain-English description of something to visualize and produce a single self-contained HTML file.
13
+
14
+ ## Output rules
15
+
16
+ 1. Generate exactly one HTML file — inline all CSS and JS, no external stylesheets or scripts except CDN libraries.
17
+ 2. Write the file to `.chat-glass/pages/{timestamp}.html` where timestamp is the current time formatted as `YYYY-MM-DDTHH-mm-ss` (filesystem-safe ISO). Create the directory if it does not exist.
18
+ 3. Run `chat-glass show` via Bash to open/reload the viewer.
19
+ 4. Return a single sentence describing what you visualized. Nothing more.
20
+ 5. **Never** return HTML, SVG, or code in your response to the main agent.
21
+ 6. If `chat-glass show` outputs a screenshot path, include it in your response
22
+ as: "Screenshot: /path/to/screenshot.png" — this lets the calling context
23
+ see what was rendered.
24
+
25
+ ## HTML file requirements
26
+
27
+ - Must be fully self-contained and renderable by opening the file directly
28
+ - Must include a descriptive `<title>` tag (the gallery uses this)
29
+ - Target viewport: approximately 1200 x 800 pixels
30
+ - No scrolling required — content should fit in view
31
+
32
+ ## CDN libraries you may use
33
+
34
+ Pick whichever fits the visualization best:
35
+
36
+ - **Tailwind CSS** — utility styling via `<script src="https://cdn.tailwindcss.com"></script>`
37
+ - **Mermaid** — diagrams, flowcharts, sequence diagrams, gantt charts
38
+ - **D3.js** — custom data visualizations, force graphs, trees
39
+ - **Chart.js** — bar, line, pie, radar, and other standard charts
40
+ - **Prism / Highlight.js** — syntax-highlighted code blocks
41
+ - **KaTeX** — mathematical notation
42
+ - **Three.js** — 3D visualizations
43
+ - Any other CDN library that fits the task
44
+
45
+ ## Style guide
46
+
47
+ ### Theme
48
+
49
+ - Background: `#1a1a2e` (deep blue-black)
50
+ - Text: `#e0e0e0`
51
+ - Accent palette — rotate through these: `#00d4ff`, `#ff6b6b`, `#4ecdc4`, `#ffe66d`, `#a29bfe`
52
+
53
+ ### Typography
54
+
55
+ - Body: system font stack (`-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif`)
56
+ - Headings/labels that need character: pull a single Google Font via CDN — vary it per visualization
57
+ - Monospace: `'SF Mono', 'Fira Code', 'Cascadia Code', monospace`
58
+
59
+ ### Layout principles
60
+
61
+ - Generous whitespace — let the content breathe
62
+ - Content centered with max-width ~1000px
63
+ - Use CSS grid/flexbox for layout
64
+ - No scrolling if possible — fit the viewport
65
+ - No unnecessary chrome or decoration
66
+
67
+ ### Diagram style
68
+
69
+ - Prefer organic SVG feel over rigid boxes
70
+ - Rounded corners, subtle shadows, curved connecting lines
71
+ - Use color to encode meaning, not decoration
72
+
73
+ ### Comparison style
74
+
75
+ - Side-by-side cards with clear headers and key points
76
+ - Color-coded borders for pros/cons or categories
77
+ - Highlight differences with accent colors
78
+
79
+ ### Flow / process style
80
+
81
+ - Nodes as rounded rectangles
82
+ - Edges as curved SVG paths with arrowheads
83
+ - Subtle load animation if it aids understanding
84
+
85
+ ### Data visualization style
86
+
87
+ - Clean axes, minimal gridlines
88
+ - Direct labels preferred over legends
89
+ - Accent colors for data series
90
+ - No chartjunk — every pixel should inform
91
+
92
+ ## Timestamp generation
93
+
94
+ Use this Bash one-liner to generate the filename timestamp:
95
+ ```bash
96
+ date +%Y-%m-%dT%H-%M-%S
97
+ ```
98
+
99
+ Or in JavaScript:
100
+ ```javascript
101
+ new Date().toISOString().replace(/:/g, '-').replace(/\..+/, '')
102
+ ```
103
+
104
+ ## Example workflow
105
+
106
+ 1. Receive: "Show the request lifecycle through our middleware stack"
107
+ 2. Write: `.chat-glass/pages/2025-06-15T14-30-22.html` containing a styled flowchart
108
+ 3. Run: `chat-glass show` — output includes the server URL and optionally a screenshot path
109
+ 4. Reply: "Created a flowchart showing the request lifecycle through the middleware stack. Screenshot: /path/to/project/.chat-glass/pages/screenshot-1718459422000.png"
package/bin/cli.js ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+
3
+ import show from "../src/commands/show.js";
4
+ import installAgent from "../src/commands/install-agent.js";
5
+ import list from "../src/commands/list.js";
6
+ import clean from "../src/commands/clean.js";
7
+
8
+ const commands = {
9
+ show,
10
+ "install-agent": installAgent,
11
+ list,
12
+ clean,
13
+ };
14
+
15
+ function printUsage() {
16
+ console.log(`chat-glass — Visual thinking companion for Claude Code
17
+
18
+ Usage: chat-glass <command>
19
+
20
+ Commands:
21
+ show Open the visualization viewer
22
+ install-agent Install the chat-glass agent file
23
+ list List saved visualizations
24
+ clean Delete all saved pages
25
+
26
+ Options:
27
+ --help Show this help message`);
28
+ }
29
+
30
+ const args = process.argv.slice(2);
31
+ const command = args[0];
32
+
33
+ if (!command || command === "--help" || command === "-h") {
34
+ printUsage();
35
+ process.exit(0);
36
+ }
37
+
38
+ const handler = commands[command];
39
+
40
+ if (!handler) {
41
+ console.error(`Unknown command: ${command}\n`);
42
+ printUsage();
43
+ process.exit(1);
44
+ }
45
+
46
+ try {
47
+ await handler(args.slice(1));
48
+ } catch (err) {
49
+ console.error(`Error: ${err.message}`);
50
+ process.exit(1);
51
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "chat-glass",
3
+ "version": "1.0.0",
4
+ "description": "Visual thinking companion for Claude Code",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "chat-glass": "./bin/cli.js"
9
+ },
10
+ "scripts": {
11
+ "test": "vitest run",
12
+ "test:coverage": "vitest run --coverage",
13
+ "dev": "node bin/cli.js install-agent --local --dev"
14
+ },
15
+ "files": [
16
+ "src",
17
+ "bin",
18
+ "agent"
19
+ ],
20
+ "engines": {
21
+ "node": ">=18"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/tfLConsulting/chat-glass.git"
26
+ },
27
+ "homepage": "https://github.com/tfLConsulting/chat-glass",
28
+ "bugs": {
29
+ "url": "https://github.com/tfLConsulting/chat-glass/issues"
30
+ },
31
+ "keywords": [
32
+ "claude",
33
+ "claude-code",
34
+ "visualization",
35
+ "ai",
36
+ "developer-tools"
37
+ ],
38
+ "license": "MIT",
39
+ "devDependencies": {
40
+ "@vitest/coverage-v8": "^4.0.18",
41
+ "vitest": "^4.0.18"
42
+ },
43
+ "dependencies": {
44
+ "open": "^11.0.0",
45
+ "puppeteer-core": "^24.0.0",
46
+ "ws": "^8.19.0"
47
+ }
48
+ }
@@ -0,0 +1,33 @@
1
+ import { readdir, unlink } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { pagesDir, getProjectDir } from "../utils/paths.js";
4
+
5
+ export default async function clean() {
6
+ const dir = pagesDir(getProjectDir());
7
+ let files;
8
+ try {
9
+ files = await readdir(dir);
10
+ } catch {
11
+ console.log("No pages directory found.");
12
+ return;
13
+ }
14
+
15
+ const targets = files.filter((f) => f.endsWith(".html"));
16
+
17
+ if (targets.length === 0) {
18
+ console.log("No pages to clean.");
19
+ return;
20
+ }
21
+
22
+ let removed = 0;
23
+ for (const filename of targets) {
24
+ try {
25
+ await unlink(join(dir, filename));
26
+ removed++;
27
+ } catch {
28
+ // skip files that can't be removed
29
+ }
30
+ }
31
+
32
+ console.log(`Removed ${removed} page(s).`);
33
+ }
@@ -0,0 +1,64 @@
1
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
2
+ import { join, dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { homedir } from "node:os";
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+ const PACKAGE_ROOT = resolve(__dirname, "..", "..");
9
+ const AGENT_SOURCE = join(PACKAGE_ROOT, "agent", "chat-glass.md");
10
+ const CLI_PATH = join(PACKAGE_ROOT, "bin", "cli.js");
11
+
12
+ export default async function installAgent(args = []) {
13
+ const isLocal = args.includes("--local");
14
+ const isDev = args.includes("--dev");
15
+
16
+ const destDir = isLocal
17
+ ? join(process.cwd(), ".claude", "agents")
18
+ : join(homedir(), ".claude", "agents");
19
+
20
+ const destPath = join(destDir, "chat-glass.md");
21
+
22
+ // Read source agent file
23
+ let source;
24
+ try {
25
+ source = await readFile(AGENT_SOURCE, "utf8");
26
+ } catch {
27
+ console.error("Could not read agent file from package. Is chat-glass installed correctly?");
28
+ process.exit(1);
29
+ }
30
+
31
+ // Dev mode: rewrite `chat-glass` commands to use the local checkout
32
+ if (isDev) {
33
+ const localCmd = `node ${CLI_PATH}`;
34
+ source = source.replace(/`chat-glass show`/g, `\`${localCmd} show\``);
35
+ source = source.replace(/`chat-glass /g, `\`${localCmd} `);
36
+ source = source.replace(
37
+ /Run: `node .* show`/,
38
+ `Run: \`${localCmd} show\``
39
+ );
40
+ }
41
+
42
+ // Check if destination already exists and is identical
43
+ try {
44
+ const existing = await readFile(destPath, "utf8");
45
+ if (existing === source) {
46
+ console.log("Agent already up to date.");
47
+ return;
48
+ }
49
+ } catch {
50
+ // File doesn't exist yet — that's fine
51
+ }
52
+
53
+ // Write agent file
54
+ await mkdir(destDir, { recursive: true });
55
+ await writeFile(destPath, source, "utf8");
56
+
57
+ if (isDev) {
58
+ console.log(`Agent installed (dev mode) — commands point to ${CLI_PATH}`);
59
+ } else if (isLocal) {
60
+ console.log("Agent installed to .claude/agents/chat-glass.md — available in this project");
61
+ } else {
62
+ console.log("Agent installed to ~/.claude/agents/chat-glass.md — available in all projects");
63
+ }
64
+ }
@@ -0,0 +1,42 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { pagesDir, getProjectDir } from "../utils/paths.js";
4
+
5
+ function extractTitle(html) {
6
+ const match = html.match(/<title>(.*?)<\/title>/i);
7
+ return match ? match[1] : null;
8
+ }
9
+
10
+ export default async function list() {
11
+ const dir = pagesDir(getProjectDir());
12
+ let files;
13
+ try {
14
+ files = await readdir(dir);
15
+ } catch {
16
+ console.log("No pages directory found.");
17
+ return;
18
+ }
19
+
20
+ const htmlFiles = files
21
+ .filter((f) => f.endsWith(".html") && f !== "latest.html")
22
+ .sort((a, b) => b.localeCompare(a));
23
+
24
+ if (htmlFiles.length === 0) {
25
+ console.log("No pages found.");
26
+ return;
27
+ }
28
+
29
+ for (const filename of htmlFiles) {
30
+ let title = null;
31
+ try {
32
+ const content = await readFile(join(dir, filename), "utf8");
33
+ title = extractTitle(content);
34
+ } catch {
35
+ // skip unreadable files
36
+ }
37
+ const display = title ? `${filename} ${title}` : filename;
38
+ console.log(display);
39
+ }
40
+
41
+ console.log(`\n${htmlFiles.length} page(s)`);
42
+ }
@@ -0,0 +1,215 @@
1
+ import { spawn } from "node:child_process";
2
+ import { readdir, symlink, unlink } from "node:fs/promises";
3
+ import { resolve, dirname } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { readConfig, ensureDirs, writeConfig } from "../utils/config.js";
6
+ import { pagesDir, latestPath, getProjectDir } from "../utils/paths.js";
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+ const SERVER_ENTRY = resolve(__dirname, "..", "server", "start.js");
11
+
12
+ const HEALTH_CHECK_TIMEOUT_MS = 2000;
13
+ const SERVER_START_TIMEOUT_MS = 5000;
14
+ const READY_POLL_INTERVAL_MS = 200;
15
+ const READY_MAX_ATTEMPTS = 15;
16
+
17
+ async function isServerAlive(port) {
18
+ try {
19
+ const controller = new AbortController();
20
+ const timeout = setTimeout(() => controller.abort(), HEALTH_CHECK_TIMEOUT_MS);
21
+ const res = await fetch(`http://127.0.0.1:${port}/health`, {
22
+ signal: controller.signal,
23
+ });
24
+ clearTimeout(timeout);
25
+ return res.ok;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ function isProcessAlive(pid) {
32
+ try {
33
+ process.kill(pid, 0);
34
+ return true;
35
+ } catch {
36
+ return false;
37
+ }
38
+ }
39
+
40
+ async function updateLatestSymlink(projectDir) {
41
+ const dir = pagesDir(projectDir);
42
+ let files;
43
+ try {
44
+ files = await readdir(dir);
45
+ } catch {
46
+ return;
47
+ }
48
+
49
+ const htmlFiles = files
50
+ .filter((f) => f.endsWith(".html") && f !== "latest.html")
51
+ .sort();
52
+
53
+ if (htmlFiles.length === 0) return;
54
+
55
+ const newest = htmlFiles[htmlFiles.length - 1];
56
+ const linkPath = latestPath(projectDir);
57
+
58
+ // Remove existing symlink/file
59
+ try {
60
+ await unlink(linkPath);
61
+ } catch {
62
+ // doesn't exist yet
63
+ }
64
+
65
+ await symlink(newest, linkPath);
66
+ }
67
+
68
+ async function spawnServer(projectDir) {
69
+ const child = spawn(process.execPath, [SERVER_ENTRY, projectDir], {
70
+ detached: true,
71
+ stdio: ["ignore", "pipe", "pipe"],
72
+ });
73
+
74
+ // Read the port from stdout
75
+ let stdout = "";
76
+ let stderr = "";
77
+
78
+ // Wait for the server to write its port, or fail
79
+ const port = await new Promise((resolvePort, reject) => {
80
+ const timeout = setTimeout(() => {
81
+ reject(new Error("Server failed to start within 5 seconds"));
82
+ }, SERVER_START_TIMEOUT_MS);
83
+
84
+ child.stdout.on("data", (chunk) => {
85
+ stdout += chunk.toString();
86
+ const parsed = parseInt(stdout.trim(), 10);
87
+ if (parsed > 0) {
88
+ clearTimeout(timeout);
89
+ resolvePort(parsed);
90
+ }
91
+ });
92
+
93
+ child.stderr.on("data", (chunk) => {
94
+ stderr += chunk.toString();
95
+ });
96
+
97
+ child.on("error", (err) => {
98
+ clearTimeout(timeout);
99
+ reject(err);
100
+ });
101
+
102
+ child.on("exit", (code) => {
103
+ if (code !== null && code !== 0) {
104
+ clearTimeout(timeout);
105
+ reject(new Error(stderr.trim() || `Server exited with code ${code}`));
106
+ }
107
+ });
108
+ });
109
+
110
+ // Detach — let the server run independently
111
+ child.unref();
112
+ child.stdout.destroy();
113
+ child.stderr.destroy();
114
+
115
+ return port;
116
+ }
117
+
118
+ async function waitForReady(port, maxAttempts = READY_MAX_ATTEMPTS) {
119
+ for (let i = 0; i < maxAttempts; i++) {
120
+ if (await isServerAlive(port)) return true;
121
+ await new Promise((r) => setTimeout(r, READY_POLL_INTERVAL_MS));
122
+ }
123
+ return false;
124
+ }
125
+
126
+ export async function show(projectDir, { openBrowser = true } = {}) {
127
+ // 1. Ensure directories exist
128
+ await ensureDirs(projectDir);
129
+
130
+ // 2. Update latest.html symlink
131
+ await updateLatestSymlink(projectDir);
132
+
133
+ // 3. Check if server is already running
134
+ const config = await readConfig(projectDir);
135
+ let port = config.port;
136
+ let isNew = false;
137
+
138
+ let alive = false;
139
+ if (port && config.pid) {
140
+ // Check if the process is still alive and responding
141
+ if (isProcessAlive(config.pid)) {
142
+ alive = await isServerAlive(port);
143
+ }
144
+ // Stale config — process is dead
145
+ if (!alive) {
146
+ port = null;
147
+ }
148
+ }
149
+
150
+ if (!alive) {
151
+ // 4. Start server as background process
152
+ port = await spawnServer(projectDir);
153
+
154
+ // 5. Wait for server to be ready
155
+ const ready = await waitForReady(port);
156
+ if (!ready) {
157
+ throw new Error("Server started but failed to respond");
158
+ }
159
+
160
+ isNew = true;
161
+ }
162
+
163
+ // 6. Open browser on first start
164
+ if (isNew && openBrowser) {
165
+ try {
166
+ const open = (await import("open")).default;
167
+ await open(`http://localhost:${port}`);
168
+ } catch {
169
+ // Non-fatal — headless environments may not have a browser
170
+ }
171
+
172
+ // Track that browser was opened
173
+ const currentConfig = await readConfig(projectDir);
174
+ await writeConfig(projectDir, { ...currentConfig, browserOpened: true });
175
+ }
176
+
177
+ // 7. Ping reload
178
+ try {
179
+ await fetch(`http://127.0.0.1:${port}/reload`);
180
+ } catch {
181
+ // Non-fatal
182
+ }
183
+
184
+ // 8. Attempt screenshot capture
185
+ let screenshot = null;
186
+ try {
187
+ const { captureScreenshot } = await import("../utils/screenshot.js");
188
+ const outputFile = resolve(pagesDir(projectDir), `screenshot-${Date.now()}.png`);
189
+ screenshot = await captureScreenshot(port, outputFile);
190
+ } catch {
191
+ // Non-fatal — screenshot is a bonus
192
+ }
193
+
194
+ return { port, isNew, screenshot };
195
+ }
196
+
197
+ export default async function showCommand() {
198
+ try {
199
+ const projectDir = getProjectDir();
200
+ const { port, screenshot } = await show(projectDir);
201
+ console.log(`chat-glass running on http://localhost:${port}`);
202
+ if (screenshot) {
203
+ console.log(`screenshot: ${screenshot}`);
204
+ }
205
+ } catch (err) {
206
+ if (err.message.includes("No free port")) {
207
+ console.error(
208
+ "All ports 3737-3747 in use. Close some chat-glass instances."
209
+ );
210
+ } else {
211
+ console.error(`Failed to start server: ${err.message}`);
212
+ }
213
+ process.exit(1);
214
+ }
215
+ }
package/src/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { startServer } from "./server/index.js";
2
+ export { findFreePort } from "./utils/port.js";
3
+ export { readConfig, writeConfig, ensureDirs } from "./utils/config.js";
4
+ export { pagesDir, configPath, latestPath, getProjectDir } from "./utils/paths.js";