pneuma-skills 0.2.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 +21 -0
- package/README.md +185 -0
- package/bin/pneuma.ts +202 -0
- package/dist/assets/index-BperbaZp.js +87 -0
- package/dist/assets/index-CpzBc9ts.css +1 -0
- package/dist/index.html +13 -0
- package/index.html +12 -0
- package/package.json +48 -0
- package/server/cli-launcher.ts +243 -0
- package/server/file-watcher.ts +76 -0
- package/server/index.ts +154 -0
- package/server/path-resolver.ts +131 -0
- package/server/session-types.ts +343 -0
- package/server/skill-installer.ts +73 -0
- package/server/ws-bridge-browser.ts +129 -0
- package/server/ws-bridge-controls.ts +51 -0
- package/server/ws-bridge-replay.ts +57 -0
- package/server/ws-bridge-types.ts +64 -0
- package/server/ws-bridge.ts +674 -0
- package/skill/doc/SKILL.md +23 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pandazki
|
|
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,185 @@
|
|
|
1
|
+
# Pneuma Skills
|
|
2
|
+
|
|
3
|
+
**Let Code Agents do WYSIWYG editing on HTML-based content.**
|
|
4
|
+
|
|
5
|
+
> **"pneuma"** — Greek *pneuma*, meaning soul, breath, life force.
|
|
6
|
+
|
|
7
|
+
Pneuma Skills is a framework that connects a Code Agent (like Claude Code) to a browser-based editor, giving users a real-time WYSIWYG experience for AI-assisted content editing. The agent edits files on disk; Pneuma watches for changes and streams a live preview to the browser alongside a chat interface.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Pneuma Skills = Content Mode x Code Agent Backend x Editor Shell
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Demo
|
|
14
|
+
|
|
15
|
+
Currently ships with **Doc Mode** — a markdown editing environment where Claude Code edits `.md` files and you see the rendered result in real-time.
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
┌─────────────────────────────┬──────────────────────────┐
|
|
19
|
+
│ │ Chat with Claude Code │
|
|
20
|
+
│ Live Markdown Preview │ │
|
|
21
|
+
│ │ > Add a features section│
|
|
22
|
+
│ # My Document │ │
|
|
23
|
+
│ ## Features │ [Thinking... 3s] │
|
|
24
|
+
│ - Real-time preview │ │
|
|
25
|
+
│ - GFM support │ ✎ Edit README.md │
|
|
26
|
+
│ - Image rendering │ ✎ Write hero.png │
|
|
27
|
+
│ │ │
|
|
28
|
+
├─────────────────────────────┴──────────────────────────┤
|
|
29
|
+
│ ● Connected session:abc123 $0.02 3 turns │
|
|
30
|
+
└────────────────────────────────────────────────────────┘
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Prerequisites
|
|
34
|
+
|
|
35
|
+
- [Bun](https://bun.sh) >= 1.0
|
|
36
|
+
- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and authenticated (`claude` command available in PATH)
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Clone and install
|
|
42
|
+
git clone https://github.com/pandazki/pneuma-skills.git
|
|
43
|
+
cd pneuma-skills
|
|
44
|
+
bun install
|
|
45
|
+
|
|
46
|
+
# Start Doc Mode on a workspace directory
|
|
47
|
+
bun bin/pneuma.ts doc --workspace ~/my-notes
|
|
48
|
+
|
|
49
|
+
# Or use the current directory
|
|
50
|
+
bun bin/pneuma.ts doc
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This will:
|
|
54
|
+
|
|
55
|
+
1. Install a skill prompt into `<workspace>/.claude/skills/`
|
|
56
|
+
2. Start the Pneuma server (default port 17996)
|
|
57
|
+
3. Spawn a Claude Code CLI session connected via WebSocket
|
|
58
|
+
4. Open your browser with the editor UI
|
|
59
|
+
|
|
60
|
+
## CLI Usage
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
pneuma <mode> [options]
|
|
64
|
+
|
|
65
|
+
Modes:
|
|
66
|
+
doc Markdown document editing mode
|
|
67
|
+
|
|
68
|
+
Options:
|
|
69
|
+
--workspace <path> Target workspace directory (default: current directory)
|
|
70
|
+
--port <number> Server port (default: 17996)
|
|
71
|
+
--no-open Don't auto-open the browser
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Architecture
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
Browser (React) Pneuma Server (Bun + Hono) Claude Code CLI
|
|
78
|
+
┌──────────────┐ JSON/WS ┌─────────────────────┐ NDJSON/WS ┌──────────────┐
|
|
79
|
+
│ Chat Panel │◄──────────────►│ │◄───────────────►│ │
|
|
80
|
+
│ Live Preview │ │ WebSocket Bridge │ │ claude │
|
|
81
|
+
│ Permissions │ │ File Watcher │ │ --sdk-url │
|
|
82
|
+
│ Status Bar │ │ Content Server │ │ │
|
|
83
|
+
└──────────────┘ └─────────────────────┘ └──────────────┘
|
|
84
|
+
│
|
|
85
|
+
watches disk
|
|
86
|
+
│
|
|
87
|
+
┌─────────────┐
|
|
88
|
+
│ Workspace │
|
|
89
|
+
│ *.md files │
|
|
90
|
+
└─────────────┘
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The server maintains dual WebSocket channels:
|
|
94
|
+
- **Browser channel** (`/ws/browser/:sessionId`) — JSON messages for the React UI
|
|
95
|
+
- **CLI channel** (`/ws/cli/:sessionId`) — NDJSON messages for Claude Code's `--sdk-url` protocol
|
|
96
|
+
|
|
97
|
+
When Claude Code edits files, chokidar detects the changes and pushes updated content to the browser for live preview.
|
|
98
|
+
|
|
99
|
+
## Project Structure
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
pneuma-skills/
|
|
103
|
+
├── bin/pneuma.ts # CLI entry point
|
|
104
|
+
├── server/
|
|
105
|
+
│ ├── index.ts # Hono HTTP server + content API
|
|
106
|
+
│ ├── ws-bridge.ts # Dual WebSocket bridge (browser <-> CLI)
|
|
107
|
+
│ ├── cli-launcher.ts # Claude Code process spawner
|
|
108
|
+
│ ├── file-watcher.ts # chokidar-based file watcher
|
|
109
|
+
│ └── skill-installer.ts # Copies skill prompts to workspace
|
|
110
|
+
├── src/
|
|
111
|
+
│ ├── App.tsx # Root layout (resizable panels)
|
|
112
|
+
│ ├── main.tsx # React entry
|
|
113
|
+
│ ├── store.ts # Zustand state management
|
|
114
|
+
│ ├── ws.ts # Browser WebSocket client
|
|
115
|
+
│ ├── types.ts # Shared TypeScript types
|
|
116
|
+
│ └── components/
|
|
117
|
+
│ ├── ChatPanel.tsx # Chat message feed
|
|
118
|
+
│ ├── ChatInput.tsx # Message input box
|
|
119
|
+
│ ├── MarkdownPreview.tsx # Live markdown renderer
|
|
120
|
+
│ ├── MessageBubble.tsx # Rich message rendering (text, tools, thinking)
|
|
121
|
+
│ ├── ToolBlock.tsx # Expandable tool call cards
|
|
122
|
+
│ ├── StreamingText.tsx # Streaming response display
|
|
123
|
+
│ ├── ActivityIndicator.tsx # Thinking/tool progress indicator
|
|
124
|
+
│ ├── PermissionBanner.tsx # Tool permission approval UI
|
|
125
|
+
│ └── StatusBar.tsx # Connection status + session info
|
|
126
|
+
├── skill/
|
|
127
|
+
│ └── doc/SKILL.md # Doc Mode skill prompt for Claude Code
|
|
128
|
+
├── docs/adr/ # Architecture Decision Records
|
|
129
|
+
└── draft.md # Full requirements document (Chinese)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Tech Stack
|
|
133
|
+
|
|
134
|
+
| Layer | Technology |
|
|
135
|
+
|-------|-----------|
|
|
136
|
+
| Runtime | [Bun](https://bun.sh) |
|
|
137
|
+
| Server | [Hono](https://hono.dev) |
|
|
138
|
+
| Frontend | React 19 + [Vite](https://vite.dev) 6 |
|
|
139
|
+
| Styling | [Tailwind CSS](https://tailwindcss.com) 4 + Typography plugin |
|
|
140
|
+
| State | [Zustand](https://zustand.docs.pmnd.rs) 5 |
|
|
141
|
+
| Markdown | [react-markdown](https://github.com/remarkjs/react-markdown) + remark-gfm |
|
|
142
|
+
| File Watching | [chokidar](https://github.com/paulmillr/chokidar) 4 |
|
|
143
|
+
| Agent | [Claude Code](https://docs.anthropic.com/en/docs/claude-code) via `--sdk-url` |
|
|
144
|
+
|
|
145
|
+
## How It Works
|
|
146
|
+
|
|
147
|
+
1. **Skill Installation** — Pneuma copies a mode-specific skill prompt (e.g. `skill/doc/SKILL.md`) into the workspace's `.claude/skills/` directory. Claude Code natively discovers and loads skills from this location.
|
|
148
|
+
|
|
149
|
+
2. **Agent Spawning** — The CLI launcher spawns `claude --sdk-url ws://localhost:17007/ws/cli/<sessionId>` which connects Claude Code's streaming output to the Pneuma server.
|
|
150
|
+
|
|
151
|
+
3. **Message Bridge** — The WebSocket bridge translates between the browser's JSON protocol and Claude Code's NDJSON protocol, handling message routing, permission flows, and event replay.
|
|
152
|
+
|
|
153
|
+
4. **Live Preview** — When Claude Code writes or edits files, chokidar detects changes and pushes updated content to all connected browsers via WebSocket.
|
|
154
|
+
|
|
155
|
+
5. **Rich Chat UI** — The browser renders Claude Code's full output: streaming text with markdown, expandable tool call cards, collapsible thinking blocks, and an activity indicator.
|
|
156
|
+
|
|
157
|
+
## Content Mode System
|
|
158
|
+
|
|
159
|
+
Pneuma is designed to be extensible with different content modes. Each mode defines:
|
|
160
|
+
|
|
161
|
+
- **Renderer** — How to render the content in the browser
|
|
162
|
+
- **Skill** — Domain-specific prompt for the Code Agent
|
|
163
|
+
- **File Convention** — How content is organized on disk
|
|
164
|
+
- **Navigator** — Structural navigation (outline, page list, etc.)
|
|
165
|
+
|
|
166
|
+
Currently implemented: **Doc Mode** (markdown). Future modes could include slides, mindmaps, canvas, and more.
|
|
167
|
+
|
|
168
|
+
## Roadmap
|
|
169
|
+
|
|
170
|
+
- [x] Doc Mode MVP — Markdown WYSIWYG editing
|
|
171
|
+
- [x] Element selection — Click to select and instruct edits on specific elements
|
|
172
|
+
- [ ] Slide Mode — Presentation editing with page navigation
|
|
173
|
+
- [ ] Session persistence — Resume previous editing sessions
|
|
174
|
+
- [ ] Multiple agent backends — Codex CLI, custom agents
|
|
175
|
+
- [ ] Production build — Single binary distribution
|
|
176
|
+
|
|
177
|
+
## Acknowledgements
|
|
178
|
+
|
|
179
|
+
This project's WebSocket bridge, NDJSON protocol handling, and chat UI rendering are heavily ~~inspired by~~ copied from [Companion](https://github.com/The-Vibe-Company/companion) by The Vibe Company. To be honest, the entire technical approach was basically Claude Code reading Companion's source code and reproducing it here. We stand on the shoulders of giants — or more accurately, we asked an AI to stand on their shoulders for us.
|
|
180
|
+
|
|
181
|
+
Thank you Companion for figuring out the undocumented `--sdk-url` protocol so we didn't have to.
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
[MIT](LICENSE)
|
package/bin/pneuma.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Pneuma Skills CLI entry point.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* pneuma doc --workspace /path/to/project [--port 17996] [--no-open]
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { resolve, dirname, join } from "node:path";
|
|
10
|
+
import { existsSync, copyFileSync, mkdirSync, readFileSync } from "node:fs";
|
|
11
|
+
import * as readline from "node:readline";
|
|
12
|
+
import { startServer } from "../server/index.js";
|
|
13
|
+
import { CliLauncher } from "../server/cli-launcher.js";
|
|
14
|
+
import { installSkill } from "../server/skill-installer.js";
|
|
15
|
+
import { startFileWatcher } from "../server/file-watcher.js";
|
|
16
|
+
|
|
17
|
+
const PROJECT_ROOT = resolve(dirname(import.meta.path), "..");
|
|
18
|
+
|
|
19
|
+
function parseArgs(argv: string[]) {
|
|
20
|
+
const args = argv.slice(2); // skip bun + script path
|
|
21
|
+
let mode = "";
|
|
22
|
+
let workspace = process.cwd();
|
|
23
|
+
let port = 0; // 0 = auto-detect based on mode
|
|
24
|
+
let noOpen = false;
|
|
25
|
+
|
|
26
|
+
for (let i = 0; i < args.length; i++) {
|
|
27
|
+
const arg = args[i];
|
|
28
|
+
if (arg === "--workspace" && i + 1 < args.length) {
|
|
29
|
+
workspace = args[++i];
|
|
30
|
+
} else if (arg === "--port" && i + 1 < args.length) {
|
|
31
|
+
port = Number(args[++i]);
|
|
32
|
+
} else if (arg === "--no-open") {
|
|
33
|
+
noOpen = true;
|
|
34
|
+
} else if (!arg.startsWith("--")) {
|
|
35
|
+
mode = arg;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { mode, workspace: resolve(workspace), port, noOpen };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function ask(question: string): Promise<string> {
|
|
43
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
rl.question(question, (answer) => {
|
|
46
|
+
rl.close();
|
|
47
|
+
resolve(answer.trim());
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function main() {
|
|
53
|
+
const { mode, workspace, port, noOpen } = parseArgs(process.argv);
|
|
54
|
+
|
|
55
|
+
if (!mode || mode !== "doc") {
|
|
56
|
+
console.log("Usage: pneuma doc --workspace /path/to/project [--port 17996] [--no-open]");
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!existsSync(workspace)) {
|
|
61
|
+
const answer = await ask(`Workspace does not exist: ${workspace}\nCreate it? [Y/n] `);
|
|
62
|
+
if (answer.toLowerCase() === "n") {
|
|
63
|
+
console.log("[pneuma] Aborted.");
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
mkdirSync(workspace, { recursive: true });
|
|
67
|
+
console.log(`[pneuma] Created workspace: ${workspace}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log(`[pneuma] Mode: ${mode}`);
|
|
71
|
+
console.log(`[pneuma] Workspace: ${workspace}`);
|
|
72
|
+
|
|
73
|
+
// 1. Install skill + inject CLAUDE.md
|
|
74
|
+
console.log("[pneuma] Installing skill and preparing environment...");
|
|
75
|
+
installSkill(workspace);
|
|
76
|
+
|
|
77
|
+
// 1.5 Seed default content if workspace has no meaningful markdown files
|
|
78
|
+
const contentFiles = Array.from(
|
|
79
|
+
new Bun.Glob("**/*.md").scanSync({ cwd: workspace, absolute: false })
|
|
80
|
+
).filter((f) => f !== "CLAUDE.md" && !f.startsWith(".claude/"));
|
|
81
|
+
|
|
82
|
+
const hasContent = contentFiles.some((f) => {
|
|
83
|
+
try {
|
|
84
|
+
return readFileSync(join(workspace, f), "utf-8").trim().length > 0;
|
|
85
|
+
} catch { return false; }
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (!hasContent) {
|
|
89
|
+
const readmeSrc = join(PROJECT_ROOT, "README.md");
|
|
90
|
+
if (existsSync(readmeSrc)) {
|
|
91
|
+
copyFileSync(readmeSrc, join(workspace, "README.md"));
|
|
92
|
+
console.log("[pneuma] Seeded workspace with default README.md");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 2. Detect dev vs production mode
|
|
97
|
+
const distDir = resolve(PROJECT_ROOT, "dist");
|
|
98
|
+
const isDev = !existsSync(join(distDir, "index.html"));
|
|
99
|
+
|
|
100
|
+
if (isDev) {
|
|
101
|
+
console.log("[pneuma] Development mode (serving via Vite)");
|
|
102
|
+
} else {
|
|
103
|
+
console.log("[pneuma] Production mode (serving built assets)");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 3. Start server
|
|
107
|
+
// Dev mode: backend on 17007, Vite on 17996 (user-facing)
|
|
108
|
+
// Prod mode: backend on 17996 (serves everything)
|
|
109
|
+
const serverPort = port || (isDev ? 17007 : 17996);
|
|
110
|
+
const { server, wsBridge, port: actualPort } = startServer({
|
|
111
|
+
port: serverPort,
|
|
112
|
+
workspace,
|
|
113
|
+
...(isDev ? {} : { distDir }),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// 4. Launch CLI
|
|
117
|
+
const launcher = new CliLauncher(actualPort);
|
|
118
|
+
|
|
119
|
+
// When the CLI reports its internal session_id, store it
|
|
120
|
+
wsBridge.onCLISessionIdReceived((sessionId, cliSessionId) => {
|
|
121
|
+
launcher.setCLISessionId(sessionId, cliSessionId);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const session = launcher.launch({
|
|
125
|
+
cwd: workspace,
|
|
126
|
+
permissionMode: "bypassPermissions",
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
console.log(`[pneuma] CLI session started: ${session.sessionId}`);
|
|
130
|
+
|
|
131
|
+
// 5. Start file watcher
|
|
132
|
+
startFileWatcher(workspace, (files) => {
|
|
133
|
+
wsBridge.broadcastToSession(session.sessionId, {
|
|
134
|
+
type: "content_update",
|
|
135
|
+
files,
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// 6. Frontend serving
|
|
140
|
+
let viteProc: ReturnType<typeof Bun.spawn> | null = null;
|
|
141
|
+
let browserPort = actualPort;
|
|
142
|
+
|
|
143
|
+
if (isDev) {
|
|
144
|
+
// Dev mode: start Vite dev server
|
|
145
|
+
const VITE_PORT = 17996;
|
|
146
|
+
console.log(`[pneuma] Starting Vite dev server on port ${VITE_PORT}...`);
|
|
147
|
+
viteProc = Bun.spawn(
|
|
148
|
+
["bunx", "vite", "--port", String(VITE_PORT), "--strictPort"],
|
|
149
|
+
{
|
|
150
|
+
cwd: PROJECT_ROOT,
|
|
151
|
+
stdout: "pipe",
|
|
152
|
+
stderr: "pipe",
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const pipeViteOutput = async (stream: ReadableStream<Uint8Array>) => {
|
|
157
|
+
const reader = stream.getReader();
|
|
158
|
+
const decoder = new TextDecoder();
|
|
159
|
+
while (true) {
|
|
160
|
+
const { done, value } = await reader.read();
|
|
161
|
+
if (done) break;
|
|
162
|
+
const text = decoder.decode(value, { stream: true });
|
|
163
|
+
for (const line of text.split("\n")) {
|
|
164
|
+
if (line.trim()) console.log(`[vite] ${line}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
if (viteProc.stdout && typeof viteProc.stdout !== "number") pipeViteOutput(viteProc.stdout);
|
|
169
|
+
if (viteProc.stderr && typeof viteProc.stderr !== "number") pipeViteOutput(viteProc.stderr);
|
|
170
|
+
browserPort = VITE_PORT;
|
|
171
|
+
// Wait for Vite to start
|
|
172
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 7. Open browser
|
|
176
|
+
if (!noOpen) {
|
|
177
|
+
const url = `http://localhost:${browserPort}?session=${session.sessionId}`;
|
|
178
|
+
console.log(`[pneuma] Opening browser: ${url}`);
|
|
179
|
+
try {
|
|
180
|
+
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
181
|
+
Bun.spawn([opener, url], { stdout: "ignore", stderr: "ignore" });
|
|
182
|
+
} catch {
|
|
183
|
+
console.log(`[pneuma] Could not open browser. Visit: ${url}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Graceful shutdown
|
|
188
|
+
const shutdown = async () => {
|
|
189
|
+
console.log("\n[pneuma] Shutting down...");
|
|
190
|
+
viteProc?.kill();
|
|
191
|
+
await launcher.killAll();
|
|
192
|
+
server.stop(true);
|
|
193
|
+
process.exit(0);
|
|
194
|
+
};
|
|
195
|
+
process.on("SIGTERM", shutdown);
|
|
196
|
+
process.on("SIGINT", shutdown);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
main().catch((err) => {
|
|
200
|
+
console.error("[pneuma] Fatal error:", err);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
});
|