pneuma-skills 0.3.0 → 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/README.md +96 -68
- package/bin/pneuma.ts +158 -51
- package/dist/assets/EditorPanel-DX1afRsK.js +31 -0
- package/dist/assets/TerminalPanel-0rj3ZSfU.js +39 -0
- package/dist/assets/TerminalPanel-6GBZ9nXN.css +32 -0
- package/dist/assets/index-6USW7crt.css +1 -0
- package/dist/assets/index-BfpOUEAv.js +95 -0
- package/dist/assets/manifest-D0B-XRFy.js +10 -0
- package/dist/assets/pneuma-mode-BDeNbInz.js +6 -0
- package/dist/index.html +2 -2
- package/package.json +14 -2
- package/server/file-watcher.ts +57 -11
- package/server/index.ts +288 -4
- package/server/session-types.ts +17 -2
- package/server/skill-installer.ts +23 -26
- package/server/terminal-manager.ts +171 -0
- package/server/ws-bridge-types.ts +6 -1
- package/server/ws-bridge.ts +158 -13
- package/dist/assets/index-C3xcHnxM.css +0 -1
- package/dist/assets/index-MpOcQtLI.js +0 -90
- package/server/cli-launcher.ts +0 -262
- package/skill/doc/SKILL.md +0 -23
package/README.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# Pneuma Skills
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**An extensible delivery platform for filesystem-based Agent capabilities.**
|
|
4
4
|
|
|
5
5
|
> **"pneuma"** — Greek *pneuma*, meaning soul, breath, life force.
|
|
6
6
|
|
|
7
|
-
Pneuma
|
|
7
|
+
Pneuma fills the last mile between Code Agents and users: agents edit files on disk, Pneuma watches for changes and streams a live WYSIWYG preview alongside a full chat interface. Everything is driven by three pluggable contracts — bring your own Mode, Viewer, or Agent backend.
|
|
8
8
|
|
|
9
9
|
```
|
|
10
|
-
|
|
10
|
+
ModeManifest(skill + viewer + agent_config) × AgentBackend × RuntimeShell
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
## Demo
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Ships with **Doc Mode** — a markdown editing environment where Claude Code edits `.md` files and you see the rendered result in real-time.
|
|
16
16
|
|
|
17
17
|
```
|
|
18
18
|
┌─────────────────────────────┬──────────────────────────┐
|
|
@@ -25,6 +25,8 @@ Currently ships with **Doc Mode** — a markdown editing environment where Claud
|
|
|
25
25
|
│ - GFM support │ ✎ Edit README.md │
|
|
26
26
|
│ - Image rendering │ ✎ Write hero.png │
|
|
27
27
|
│ │ │
|
|
28
|
+
├─────────────────────────────┼──────────────────────────┤
|
|
29
|
+
│ view / edit / select │ Chat │ Context │ Term │
|
|
28
30
|
├─────────────────────────────┴──────────────────────────┤
|
|
29
31
|
│ ● Connected session:abc123 $0.02 3 turns │
|
|
30
32
|
└────────────────────────────────────────────────────────┘
|
|
@@ -32,7 +34,7 @@ Currently ships with **Doc Mode** — a markdown editing environment where Claud
|
|
|
32
34
|
|
|
33
35
|
## Prerequisites
|
|
34
36
|
|
|
35
|
-
- [Bun](https://bun.sh) >= 1.
|
|
37
|
+
- [Bun](https://bun.sh) >= 1.3.5 (required for PTY terminal support)
|
|
36
38
|
- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and authenticated (`claude` command available in PATH)
|
|
37
39
|
|
|
38
40
|
## Quick Start
|
|
@@ -56,7 +58,7 @@ bun run dev doc --workspace ~/my-notes
|
|
|
56
58
|
|
|
57
59
|
This will:
|
|
58
60
|
|
|
59
|
-
1.
|
|
61
|
+
1. Load the Doc Mode manifest and install its skill prompt into `<workspace>/.claude/skills/`
|
|
60
62
|
2. Start the Pneuma server on `http://localhost:17996`
|
|
61
63
|
3. Spawn a Claude Code CLI session connected via WebSocket
|
|
62
64
|
4. Open your browser with the editor UI
|
|
@@ -77,21 +79,26 @@ Options:
|
|
|
77
79
|
|
|
78
80
|
## Architecture
|
|
79
81
|
|
|
82
|
+
Pneuma is organized in four layers, each with a clear contract boundary:
|
|
83
|
+
|
|
80
84
|
```
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
│
|
|
84
|
-
│
|
|
85
|
-
|
|
86
|
-
│
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
85
|
+
┌─────────────────────────────────────────────────────────┐
|
|
86
|
+
│ Layer 4: Mode Protocol │
|
|
87
|
+
│ ModeManifest — "what capability, what config, what UI" │
|
|
88
|
+
│ modes/doc/pneuma-mode.ts │
|
|
89
|
+
├─────────────────────────────────────────────────────────┤
|
|
90
|
+
│ Layer 3: Content Viewer │
|
|
91
|
+
│ ViewerContract — "how to render, select, update" │
|
|
92
|
+
│ modes/doc/components/DocPreview.tsx │
|
|
93
|
+
├─────────────────────────────────────────────────────────┤
|
|
94
|
+
│ Layer 2: Agent Bridge │
|
|
95
|
+
│ AgentBackend — "how to launch, communicate, lifecycle" │
|
|
96
|
+
│ backends/claude-code/ │
|
|
97
|
+
├─────────────────────────────────────────────────────────┤
|
|
98
|
+
│ Layer 1: Runtime Shell │
|
|
99
|
+
│ WS Bridge, HTTP, File Watcher, Session, Frontend │
|
|
100
|
+
│ server/ + src/ │
|
|
101
|
+
└─────────────────────────────────────────────────────────┘
|
|
95
102
|
```
|
|
96
103
|
|
|
97
104
|
The server maintains dual WebSocket channels:
|
|
@@ -100,83 +107,104 @@ The server maintains dual WebSocket channels:
|
|
|
100
107
|
|
|
101
108
|
When Claude Code edits files, chokidar detects the changes and pushes updated content to the browser for live preview.
|
|
102
109
|
|
|
110
|
+
## Three Core Contracts
|
|
111
|
+
|
|
112
|
+
| Contract | Responsibility | Extend to... |
|
|
113
|
+
|----------|---------------|-------------|
|
|
114
|
+
| **ModeManifest** | Declares skill, viewer config, agent preferences, init seeds | Add new modes (slide, mindmap, canvas) |
|
|
115
|
+
| **ViewerContract** | Preview component, context extraction, update strategy | Custom renderers (iframe, D3, Monaco) |
|
|
116
|
+
| **AgentBackend** | Launch, resume, kill, capability declaration | Other agents (Codex, Aider) |
|
|
117
|
+
|
|
118
|
+
Contracts are defined in `core/types/` with 42 tests in `core/__tests__/`.
|
|
119
|
+
|
|
103
120
|
## Project Structure
|
|
104
121
|
|
|
105
122
|
```
|
|
106
123
|
pneuma-skills/
|
|
107
|
-
├── bin/pneuma.ts # CLI entry
|
|
124
|
+
├── bin/pneuma.ts # CLI entry — orchestrates mode + agent + server
|
|
125
|
+
├── core/
|
|
126
|
+
│ ├── types/ # Contract definitions
|
|
127
|
+
│ │ ├── mode-manifest.ts # ModeManifest, SkillConfig, ViewerConfig
|
|
128
|
+
│ │ ├── viewer-contract.ts # ViewerContract, ViewerPreviewProps
|
|
129
|
+
│ │ ├── agent-backend.ts # AgentBackend, AgentCapabilities
|
|
130
|
+
│ │ ├── mode-definition.ts # ModeDefinition (manifest + viewer)
|
|
131
|
+
│ │ └── index.ts # Re-exports
|
|
132
|
+
│ ├── mode-loader.ts # Dynamic mode discovery and loading
|
|
133
|
+
│ └── __tests__/ # 42 contract tests
|
|
134
|
+
├── modes/
|
|
135
|
+
│ └── doc/
|
|
136
|
+
│ ├── pneuma-mode.ts # Doc Mode definition (manifest + viewer)
|
|
137
|
+
│ ├── skill/SKILL.md # Skill prompt for Claude Code
|
|
138
|
+
│ └── components/
|
|
139
|
+
│ └── DocPreview.tsx # Markdown preview with select/edit modes
|
|
140
|
+
├── backends/
|
|
141
|
+
│ └── claude-code/
|
|
142
|
+
│ ├── index.ts # ClaudeCodeBackend implements AgentBackend
|
|
143
|
+
│ └── cli-launcher.ts # Process spawner (Bun.spawn + --sdk-url)
|
|
108
144
|
├── server/
|
|
109
145
|
│ ├── index.ts # Hono HTTP server + content API
|
|
110
|
-
│ ├── ws-bridge.ts # Dual WebSocket bridge (browser
|
|
111
|
-
│ ├──
|
|
112
|
-
│ ├── file-watcher.ts # chokidar-
|
|
113
|
-
│
|
|
146
|
+
│ ├── ws-bridge.ts # Dual WebSocket bridge (browser ↔ CLI)
|
|
147
|
+
│ ├── ws-bridge-*.ts # Controls, replay, browser handlers, types
|
|
148
|
+
│ ├── file-watcher.ts # chokidar watcher (manifest-driven patterns)
|
|
149
|
+
│ ├── skill-installer.ts # Copies skill prompts (manifest-driven)
|
|
150
|
+
│ └── terminal-manager.ts # PTY terminal sessions
|
|
114
151
|
├── src/
|
|
115
|
-
│ ├── App.tsx # Root layout (
|
|
116
|
-
│ ├──
|
|
117
|
-
│ ├── store.ts # Zustand state management
|
|
152
|
+
│ ├── App.tsx # Root layout (dynamic viewer from store)
|
|
153
|
+
│ ├── store.ts # Zustand state (session, messages, viewer)
|
|
118
154
|
│ ├── ws.ts # Browser WebSocket client
|
|
119
|
-
│ ├── types.ts # Shared TypeScript types
|
|
120
155
|
│ └── components/
|
|
121
156
|
│ ├── ChatPanel.tsx # Chat message feed
|
|
122
|
-
│ ├── ChatInput.tsx # Message
|
|
123
|
-
│ ├──
|
|
124
|
-
│ ├──
|
|
157
|
+
│ ├── ChatInput.tsx # Message composer with image upload
|
|
158
|
+
│ ├── MessageBubble.tsx # Rich messages (markdown, tools, thinking, context card)
|
|
159
|
+
│ ├── ContextPanel.tsx # Session stats, tasks, MCP servers, tools
|
|
160
|
+
│ ├── TerminalPanel.tsx # Integrated xterm.js terminal
|
|
125
161
|
│ ├── ToolBlock.tsx # Expandable tool call cards
|
|
126
|
-
│ ├──
|
|
127
|
-
│
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
├──
|
|
131
|
-
|
|
132
|
-
├── docs/adr/ # Architecture Decision Records
|
|
133
|
-
└── draft.md # Full requirements document (Chinese)
|
|
162
|
+
│ ├── PermissionBanner.tsx # Tool permission approval UI
|
|
163
|
+
│ └── TopBar.tsx # Tabs (Chat/Context/Terminal) + status
|
|
164
|
+
└── docs/
|
|
165
|
+
├── architecture-review-v1.md # Architecture review & v1.0 blueprint
|
|
166
|
+
├── design/phase1-internal-decoupling.md # Detailed design doc
|
|
167
|
+
└── adr/ # Architecture Decision Records (1-11)
|
|
134
168
|
```
|
|
135
169
|
|
|
136
170
|
## Tech Stack
|
|
137
171
|
|
|
138
172
|
| Layer | Technology |
|
|
139
173
|
|-------|-----------|
|
|
140
|
-
| Runtime | [Bun](https://bun.sh) |
|
|
174
|
+
| Runtime | [Bun](https://bun.sh) >= 1.3.5 |
|
|
141
175
|
| Server | [Hono](https://hono.dev) |
|
|
142
176
|
| Frontend | React 19 + [Vite](https://vite.dev) 6 |
|
|
143
|
-
| Styling | [Tailwind CSS](https://tailwindcss.com) 4
|
|
177
|
+
| Styling | [Tailwind CSS](https://tailwindcss.com) 4 |
|
|
144
178
|
| State | [Zustand](https://zustand.docs.pmnd.rs) 5 |
|
|
145
179
|
| Markdown | [react-markdown](https://github.com/remarkjs/react-markdown) + remark-gfm |
|
|
180
|
+
| Terminal | [xterm.js](https://xtermjs.org) + Bun native PTY |
|
|
146
181
|
| File Watching | [chokidar](https://github.com/paulmillr/chokidar) 4 |
|
|
147
182
|
| Agent | [Claude Code](https://docs.anthropic.com/en/docs/claude-code) via `--sdk-url` |
|
|
148
183
|
|
|
149
|
-
##
|
|
150
|
-
|
|
151
|
-
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.
|
|
152
|
-
|
|
153
|
-
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.
|
|
154
|
-
|
|
155
|
-
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.
|
|
156
|
-
|
|
157
|
-
4. **Live Preview** — When Claude Code writes or edits files, chokidar detects changes and pushes updated content to all connected browsers via WebSocket.
|
|
158
|
-
|
|
159
|
-
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.
|
|
160
|
-
|
|
161
|
-
## Content Mode System
|
|
162
|
-
|
|
163
|
-
Pneuma is designed to be extensible with different content modes. Each mode defines:
|
|
164
|
-
|
|
165
|
-
- **Renderer** — How to render the content in the browser
|
|
166
|
-
- **Skill** — Domain-specific prompt for the Code Agent
|
|
167
|
-
- **File Convention** — How content is organized on disk
|
|
168
|
-
- **Navigator** — Structural navigation (outline, page list, etc.)
|
|
184
|
+
## Features
|
|
169
185
|
|
|
170
|
-
|
|
186
|
+
- **Live WYSIWYG preview** — Agent edits files, you see rendered results instantly
|
|
187
|
+
- **Element selection** — Click any block to select it, then instruct changes on that specific element
|
|
188
|
+
- **Inline editing** — Edit content directly in the preview (edit mode)
|
|
189
|
+
- **Rich chat UI** — Streaming text, expandable tool calls, collapsible thinking, context visualization
|
|
190
|
+
- **Integrated terminal** — Full PTY terminal with xterm.js
|
|
191
|
+
- **Session management** — Persist and resume sessions across restarts
|
|
192
|
+
- **Permission control** — Review and approve/deny tool use requests
|
|
193
|
+
- **Task tracking** — Visualize Claude's TodoWrite/TaskCreate progress
|
|
194
|
+
- **Background processes** — Monitor long-running background commands
|
|
195
|
+
- **Context visualization** — Rich `/context` card with category breakdown and stacked bar
|
|
196
|
+
- **Image upload** — Drag & drop or paste images into chat
|
|
171
197
|
|
|
172
198
|
## Roadmap
|
|
173
199
|
|
|
174
200
|
- [x] Doc Mode MVP — Markdown WYSIWYG editing
|
|
175
|
-
- [x] Element selection
|
|
176
|
-
- [
|
|
177
|
-
- [
|
|
178
|
-
- [
|
|
179
|
-
- [
|
|
201
|
+
- [x] Element selection & inline editing
|
|
202
|
+
- [x] Session persistence & resume
|
|
203
|
+
- [x] Terminal, tasks, context panel
|
|
204
|
+
- [x] v1.0 contract architecture (ModeManifest, ViewerContract, AgentBackend)
|
|
205
|
+
- [ ] Slide Mode — Presentation editing with iframe preview (v1.1)
|
|
206
|
+
- [ ] Remote mode loading — `pneuma --mode github:user/repo` (v1.x)
|
|
207
|
+
- [ ] Additional agent backends — Codex CLI, custom agents (v1.x)
|
|
180
208
|
|
|
181
209
|
## Acknowledgements
|
|
182
210
|
|
package/bin/pneuma.ts
CHANGED
|
@@ -3,16 +3,20 @@
|
|
|
3
3
|
* Pneuma Skills CLI entry point.
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
|
-
* pneuma
|
|
6
|
+
* pneuma <mode> --workspace /path/to/project [--port 17996] [--no-open]
|
|
7
|
+
*
|
|
8
|
+
* Driven by ModeManifest + AgentBackend — no hardcoded mode knowledge.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
import { resolve, dirname, join } from "node:path";
|
|
10
12
|
import { existsSync, copyFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
13
|
import * as readline from "node:readline";
|
|
12
14
|
import { startServer } from "../server/index.js";
|
|
13
|
-
import {
|
|
15
|
+
import { ClaudeCodeBackend } from "../backends/claude-code/index.js";
|
|
14
16
|
import { installSkill } from "../server/skill-installer.js";
|
|
15
17
|
import { startFileWatcher } from "../server/file-watcher.js";
|
|
18
|
+
import { loadModeManifest, listModes } from "../core/mode-loader.js";
|
|
19
|
+
import type { ModeManifest } from "../core/types/mode-manifest.js";
|
|
16
20
|
|
|
17
21
|
const PROJECT_ROOT = resolve(dirname(import.meta.path), "..");
|
|
18
22
|
|
|
@@ -20,7 +24,8 @@ const PROJECT_ROOT = resolve(dirname(import.meta.path), "..");
|
|
|
20
24
|
|
|
21
25
|
interface PersistedSession {
|
|
22
26
|
sessionId: string;
|
|
23
|
-
|
|
27
|
+
/** Agent's internal session ID (e.g. Claude Code's --resume ID) */
|
|
28
|
+
agentSessionId?: string;
|
|
24
29
|
mode: string;
|
|
25
30
|
createdAt: number;
|
|
26
31
|
}
|
|
@@ -29,7 +34,13 @@ function loadSession(workspace: string): PersistedSession | null {
|
|
|
29
34
|
const filePath = join(workspace, ".pneuma", "session.json");
|
|
30
35
|
try {
|
|
31
36
|
const content = readFileSync(filePath, "utf-8");
|
|
32
|
-
|
|
37
|
+
const data = JSON.parse(content);
|
|
38
|
+
// Backward compat: rename cliSessionId → agentSessionId
|
|
39
|
+
if (data.cliSessionId && !data.agentSessionId) {
|
|
40
|
+
data.agentSessionId = data.cliSessionId;
|
|
41
|
+
delete data.cliSessionId;
|
|
42
|
+
}
|
|
43
|
+
return data;
|
|
33
44
|
} catch {
|
|
34
45
|
return null;
|
|
35
46
|
}
|
|
@@ -41,6 +52,22 @@ function saveSession(workspace: string, session: PersistedSession): void {
|
|
|
41
52
|
writeFileSync(join(dir, "session.json"), JSON.stringify(session, null, 2));
|
|
42
53
|
}
|
|
43
54
|
|
|
55
|
+
function loadHistory(workspace: string): unknown[] {
|
|
56
|
+
try {
|
|
57
|
+
const content = readFileSync(join(workspace, ".pneuma", "history.json"), "utf-8");
|
|
58
|
+
const data = JSON.parse(content);
|
|
59
|
+
return Array.isArray(data) ? data : [];
|
|
60
|
+
} catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function saveHistory(workspace: string, history: unknown[]): void {
|
|
66
|
+
const dir = join(workspace, ".pneuma");
|
|
67
|
+
mkdirSync(dir, { recursive: true });
|
|
68
|
+
writeFileSync(join(dir, "history.json"), JSON.stringify(history));
|
|
69
|
+
}
|
|
70
|
+
|
|
44
71
|
// ── CLI arg parsing ──────────────────────────────────────────────────────────
|
|
45
72
|
|
|
46
73
|
function parseArgs(argv: string[]) {
|
|
@@ -78,11 +105,45 @@ function ask(question: string): Promise<string> {
|
|
|
78
105
|
|
|
79
106
|
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
80
107
|
|
|
108
|
+
function checkBunVersion() {
|
|
109
|
+
const MIN_BUN = "1.3.5"; // Required for Bun.spawn terminal (PTY) support
|
|
110
|
+
const current = typeof Bun !== "undefined" ? Bun.version : null;
|
|
111
|
+
if (!current) {
|
|
112
|
+
console.warn("[pneuma] Warning: Not running under Bun. Pneuma requires Bun >= " + MIN_BUN);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const [curMajor, curMinor, curPatch] = current.split(".").map(Number);
|
|
116
|
+
const [minMajor, minMinor, minPatch] = MIN_BUN.split(".").map(Number);
|
|
117
|
+
const ok =
|
|
118
|
+
curMajor > minMajor ||
|
|
119
|
+
(curMajor === minMajor && curMinor > minMinor) ||
|
|
120
|
+
(curMajor === minMajor && curMinor === minMinor && curPatch >= minPatch);
|
|
121
|
+
if (!ok) {
|
|
122
|
+
console.warn(
|
|
123
|
+
`[pneuma] Warning: Bun ${current} detected, but >= ${MIN_BUN} is required.` +
|
|
124
|
+
` Terminal features may not work. Run \`bun upgrade\` to update.`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
81
129
|
async function main() {
|
|
130
|
+
checkBunVersion();
|
|
82
131
|
const { mode, workspace, port, noOpen } = parseArgs(process.argv);
|
|
83
132
|
|
|
84
|
-
|
|
85
|
-
|
|
133
|
+
// Validate mode
|
|
134
|
+
const availableModes = listModes();
|
|
135
|
+
if (!mode || !availableModes.includes(mode)) {
|
|
136
|
+
const modeList = availableModes.join(" | ");
|
|
137
|
+
console.log(`Usage: pneuma <${modeList}> --workspace /path/to/project [--port 17996] [--no-open]`);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Load mode manifest (no React deps — backend safe)
|
|
142
|
+
let manifest: ModeManifest;
|
|
143
|
+
try {
|
|
144
|
+
manifest = await loadModeManifest(mode);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.error(`[pneuma] Failed to load mode "${mode}":`, err);
|
|
86
147
|
process.exit(1);
|
|
87
148
|
}
|
|
88
149
|
|
|
@@ -96,29 +157,35 @@ async function main() {
|
|
|
96
157
|
console.log(`[pneuma] Created workspace: ${workspace}`);
|
|
97
158
|
}
|
|
98
159
|
|
|
99
|
-
console.log(`[pneuma] Mode: ${mode}`);
|
|
160
|
+
console.log(`[pneuma] Mode: ${manifest.displayName} (${mode})`);
|
|
100
161
|
console.log(`[pneuma] Workspace: ${workspace}`);
|
|
101
162
|
|
|
102
|
-
// 1. Install skill + inject CLAUDE.md
|
|
163
|
+
// 1. Install skill + inject CLAUDE.md (driven by manifest)
|
|
103
164
|
console.log("[pneuma] Installing skill and preparing environment...");
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
165
|
+
const modeSourceDir = resolve(PROJECT_ROOT, "modes", mode);
|
|
166
|
+
installSkill(workspace, manifest.skill, modeSourceDir);
|
|
167
|
+
|
|
168
|
+
// 1.5 Seed default content if workspace has no meaningful files
|
|
169
|
+
if (manifest.init && manifest.init.contentCheckPattern) {
|
|
170
|
+
const checkPattern = manifest.init.contentCheckPattern;
|
|
171
|
+
const contentFiles = Array.from(
|
|
172
|
+
new Bun.Glob(checkPattern).scanSync({ cwd: workspace, absolute: false })
|
|
173
|
+
).filter((f) => f !== "CLAUDE.md" && !f.startsWith(".claude/"));
|
|
174
|
+
|
|
175
|
+
const hasContent = contentFiles.some((f) => {
|
|
176
|
+
try {
|
|
177
|
+
return readFileSync(join(workspace, f), "utf-8").trim().length > 0;
|
|
178
|
+
} catch { return false; }
|
|
179
|
+
});
|
|
116
180
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
181
|
+
if (!hasContent && manifest.init.seedFiles) {
|
|
182
|
+
for (const [src, dst] of Object.entries(manifest.init.seedFiles)) {
|
|
183
|
+
const srcPath = join(PROJECT_ROOT, src);
|
|
184
|
+
if (existsSync(srcPath)) {
|
|
185
|
+
copyFileSync(srcPath, join(workspace, dst));
|
|
186
|
+
console.log(`[pneuma] Seeded workspace with ${dst}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
122
189
|
}
|
|
123
190
|
}
|
|
124
191
|
|
|
@@ -142,18 +209,18 @@ async function main() {
|
|
|
142
209
|
...(isDev ? {} : { distDir }),
|
|
143
210
|
});
|
|
144
211
|
|
|
145
|
-
// 4. Launch
|
|
146
|
-
const
|
|
212
|
+
// 4. Launch Agent backend (driven by manifest)
|
|
213
|
+
const backend = new ClaudeCodeBackend(actualPort);
|
|
147
214
|
|
|
148
215
|
// When the CLI reports its internal session_id, persist it
|
|
149
|
-
wsBridge.onCLISessionIdReceived((sessionId,
|
|
150
|
-
|
|
216
|
+
wsBridge.onCLISessionIdReceived((sessionId, agentSessionId) => {
|
|
217
|
+
backend.setAgentSessionId(sessionId, agentSessionId);
|
|
151
218
|
// Persist to .pneuma/session.json
|
|
152
219
|
const persisted = loadSession(workspace);
|
|
153
220
|
if (persisted && persisted.sessionId === sessionId) {
|
|
154
|
-
persisted.
|
|
221
|
+
persisted.agentSessionId = agentSessionId;
|
|
155
222
|
saveSession(workspace, persisted);
|
|
156
|
-
console.log(`[pneuma] Saved
|
|
223
|
+
console.log(`[pneuma] Saved agentSessionId for resume: ${agentSessionId}`);
|
|
157
224
|
}
|
|
158
225
|
});
|
|
159
226
|
|
|
@@ -161,49 +228,82 @@ async function main() {
|
|
|
161
228
|
const existing = loadSession(workspace);
|
|
162
229
|
let resuming = false;
|
|
163
230
|
|
|
164
|
-
const
|
|
231
|
+
const permissionMode = manifest.agent?.permissionMode;
|
|
232
|
+
const session = backend.launch({
|
|
165
233
|
cwd: workspace,
|
|
166
|
-
permissionMode
|
|
234
|
+
permissionMode,
|
|
167
235
|
// Reuse sessionId for stable WS routing
|
|
168
|
-
...(existing?.
|
|
236
|
+
...(existing?.agentSessionId ? {
|
|
169
237
|
sessionId: existing.sessionId,
|
|
170
|
-
resumeSessionId: existing.
|
|
238
|
+
resumeSessionId: existing.agentSessionId,
|
|
171
239
|
} : {}),
|
|
172
240
|
});
|
|
173
241
|
|
|
174
|
-
if (existing?.
|
|
242
|
+
if (existing?.agentSessionId) {
|
|
175
243
|
resuming = true;
|
|
176
|
-
console.log(`[pneuma] Resuming session: ${existing.
|
|
244
|
+
console.log(`[pneuma] Resuming session: ${existing.agentSessionId}`);
|
|
177
245
|
}
|
|
178
246
|
|
|
179
247
|
// Persist session info
|
|
180
248
|
saveSession(workspace, {
|
|
181
249
|
sessionId: session.sessionId,
|
|
182
|
-
|
|
250
|
+
agentSessionId: existing?.agentSessionId,
|
|
183
251
|
mode,
|
|
184
252
|
createdAt: existing?.createdAt || Date.now(),
|
|
185
253
|
});
|
|
186
254
|
|
|
187
|
-
console.log(`[pneuma]
|
|
255
|
+
console.log(`[pneuma] Agent session started: ${session.sessionId}`);
|
|
188
256
|
|
|
189
|
-
//
|
|
190
|
-
|
|
257
|
+
// Auto-greeting for fresh sessions (driven by manifest)
|
|
258
|
+
if (!resuming && manifest.agent?.greeting) {
|
|
259
|
+
wsBridge.injectGreeting(session.sessionId, manifest.agent.greeting);
|
|
260
|
+
console.log("[pneuma] Sent auto-greeting for fresh session");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Load persisted message history into WsBridge
|
|
264
|
+
const savedHistory = loadHistory(workspace);
|
|
265
|
+
if (savedHistory.length > 0) {
|
|
266
|
+
wsBridge.loadMessageHistory(session.sessionId, savedHistory as any);
|
|
267
|
+
console.log(`[pneuma] Restored ${savedHistory.length} messages from history`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Periodically persist message history (debounced — every 5s)
|
|
271
|
+
const historyInterval = setInterval(() => {
|
|
272
|
+
const history = wsBridge.getMessageHistory(session.sessionId);
|
|
273
|
+
if (history.length > 0) {
|
|
274
|
+
saveHistory(workspace, history);
|
|
275
|
+
}
|
|
276
|
+
}, 5_000);
|
|
277
|
+
|
|
278
|
+
// Handle Agent exit: surface errors + clear stale resume state
|
|
279
|
+
backend.onSessionExited((exitedId, exitCode) => {
|
|
280
|
+
// Broadcast Agent errors to browser
|
|
281
|
+
if (exitCode !== 0 && exitCode !== 143 /* SIGTERM = normal shutdown */) {
|
|
282
|
+
let errorMsg: string;
|
|
283
|
+
if (exitCode === 127) {
|
|
284
|
+
errorMsg = "Claude Code CLI not found. Please install it: https://docs.anthropic.com/claude-code";
|
|
285
|
+
} else {
|
|
286
|
+
errorMsg = `Claude Code exited unexpectedly (code ${exitCode}). Check CLI installation and subscription status.`;
|
|
287
|
+
}
|
|
288
|
+
wsBridge.broadcastToSession(exitedId, { type: "error", message: errorMsg });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// If resume fails (Agent exits quickly), clear agentSessionId from persistence
|
|
191
292
|
if (exitedId === session.sessionId && resuming) {
|
|
192
|
-
const info =
|
|
193
|
-
if (info && !info.
|
|
194
|
-
// Resume failed, cliSessionId was cleared by launcher
|
|
293
|
+
const info = backend.getSession(exitedId);
|
|
294
|
+
if (info && !info.agentSessionId) {
|
|
195
295
|
const persisted = loadSession(workspace);
|
|
196
296
|
if (persisted) {
|
|
197
|
-
persisted.
|
|
297
|
+
persisted.agentSessionId = undefined;
|
|
198
298
|
saveSession(workspace, persisted);
|
|
199
|
-
console.log("[pneuma] Resume failed, cleared
|
|
299
|
+
console.log("[pneuma] Resume failed, cleared agentSessionId. Restart for fresh session.");
|
|
200
300
|
}
|
|
201
301
|
}
|
|
202
302
|
}
|
|
203
303
|
});
|
|
204
304
|
|
|
205
|
-
// 5. Start file watcher
|
|
206
|
-
startFileWatcher(workspace, (files) => {
|
|
305
|
+
// 5. Start file watcher (driven by manifest)
|
|
306
|
+
startFileWatcher(workspace, manifest.viewer, (files) => {
|
|
207
307
|
wsBridge.broadcastToSession(session.sessionId, {
|
|
208
308
|
type: "content_update",
|
|
209
309
|
files,
|
|
@@ -246,9 +346,9 @@ async function main() {
|
|
|
246
346
|
await new Promise((r) => setTimeout(r, 2000));
|
|
247
347
|
}
|
|
248
348
|
|
|
249
|
-
// 7. Open browser
|
|
349
|
+
// 7. Open browser (include mode in URL for frontend)
|
|
250
350
|
if (!noOpen) {
|
|
251
|
-
const url = `http://localhost:${browserPort}?session=${session.sessionId}`;
|
|
351
|
+
const url = `http://localhost:${browserPort}?session=${session.sessionId}&mode=${mode}`;
|
|
252
352
|
console.log(`[pneuma] Opening browser: ${url}`);
|
|
253
353
|
try {
|
|
254
354
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
@@ -261,8 +361,15 @@ async function main() {
|
|
|
261
361
|
// Graceful shutdown
|
|
262
362
|
const shutdown = async () => {
|
|
263
363
|
console.log("\n[pneuma] Shutting down...");
|
|
364
|
+
clearInterval(historyInterval);
|
|
365
|
+
// Final history save
|
|
366
|
+
const history = wsBridge.getMessageHistory(session.sessionId);
|
|
367
|
+
if (history.length > 0) {
|
|
368
|
+
saveHistory(workspace, history);
|
|
369
|
+
console.log(`[pneuma] Saved ${history.length} messages to history`);
|
|
370
|
+
}
|
|
264
371
|
viteProc?.kill();
|
|
265
|
-
await
|
|
372
|
+
await backend.killAll();
|
|
266
373
|
server.stop(true);
|
|
267
374
|
process.exit(0);
|
|
268
375
|
};
|