pneuma-skills 1.7.2 → 1.9.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 +17 -9
- package/bin/pneuma.ts +109 -48
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
<h1 align="center">Pneuma Skills</h1>
|
|
2
|
+
<p align="center"><strong>WYSIWYG Delivery Platform for Code Agents</strong></p>
|
|
3
|
+
<p align="center">Agents edit files. You see the result — live.</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.npmjs.com/package/pneuma-skills"><img src="https://img.shields.io/npm/v/pneuma-skills.svg" alt="npm version" /></a>
|
|
7
|
+
<a href="https://www.npmjs.com/package/pneuma-skills"><img src="https://img.shields.io/npm/dm/pneuma-skills.svg" alt="npm downloads" /></a>
|
|
8
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="MIT License" /></a>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<pre align="center">bunx pneuma-skills slide --workspace ./my-first-pneuma-slide</pre>
|
|
12
|
+
|
|
13
|
+
---
|
|
4
14
|
|
|
5
15
|
> **"pneuma"** — Greek *pneuma*, meaning soul, breath, life force.
|
|
6
16
|
|
|
7
17
|
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
18
|
|
|
9
|
-
```
|
|
10
|
-
ModeManifest(skill + viewer + agent_config) × AgentBackend × RuntimeShell
|
|
11
|
-
```
|
|
12
|
-
|
|
13
19
|
## Demo
|
|
14
20
|
|
|
15
21
|
Ships with **Doc Mode** (markdown editing), **Slide Mode** (presentation editing), and **Draw Mode** (Excalidraw whiteboard). Here's Doc Mode — Claude Code edits `.md` files and you see the rendered result in real-time:
|
|
@@ -40,10 +46,12 @@ Ships with **Doc Mode** (markdown editing), **Slide Mode** (presentation editing
|
|
|
40
46
|
## Quick Start
|
|
41
47
|
|
|
42
48
|
```bash
|
|
43
|
-
#
|
|
44
|
-
|
|
49
|
+
# Install Bun if you haven't: curl -fsSL https://bun.sh/install | bash
|
|
50
|
+
|
|
51
|
+
# Start with a fresh workspace (recommended)
|
|
52
|
+
bunx pneuma-skills slide --workspace ./my-first-pneuma-slide
|
|
45
53
|
|
|
46
|
-
# Or use the current directory
|
|
54
|
+
# Or use the current directory (files will be created here)
|
|
47
55
|
bunx pneuma-skills doc
|
|
48
56
|
```
|
|
49
57
|
|
package/bin/pneuma.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { resolve, dirname, join } from "node:path";
|
|
12
12
|
import { existsSync, copyFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
|
-
import * as
|
|
13
|
+
import * as p from "@clack/prompts";
|
|
14
14
|
import { startServer } from "../server/index.js";
|
|
15
15
|
import { ClaudeCodeBackend } from "../backends/claude-code/index.js";
|
|
16
16
|
import { installSkill } from "../server/skill-installer.js";
|
|
@@ -20,6 +20,7 @@ import type { ModeManifest } from "../core/types/mode-manifest.js";
|
|
|
20
20
|
import { applyTemplateParams } from "../server/skill-installer.js";
|
|
21
21
|
import { resolveMode as resolveModeSource, isExternalMode } from "../core/mode-resolver.js";
|
|
22
22
|
import type { ResolvedMode } from "../core/mode-resolver.js";
|
|
23
|
+
import { resolveBinary } from "../server/path-resolver.js";
|
|
23
24
|
|
|
24
25
|
const PROJECT_ROOT = resolve(dirname(import.meta.path), "..");
|
|
25
26
|
|
|
@@ -99,16 +100,6 @@ function parseArgs(argv: string[]) {
|
|
|
99
100
|
return { mode, workspace: resolve(workspace), port, noOpen, debug };
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
function ask(question: string): Promise<string> {
|
|
103
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
104
|
-
return new Promise((resolve) => {
|
|
105
|
-
rl.question(question, (answer) => {
|
|
106
|
-
rl.close();
|
|
107
|
-
resolve(answer.trim());
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
103
|
// ── Init params persistence ──────────────────────────────────────────────────
|
|
113
104
|
|
|
114
105
|
function loadConfig(workspace: string): Record<string, number | string> | null {
|
|
@@ -132,11 +123,19 @@ async function promptInitParams(manifest: ModeManifest): Promise<Record<string,
|
|
|
132
123
|
const initParams = manifest.init?.params;
|
|
133
124
|
if (!initParams || initParams.length === 0) return params;
|
|
134
125
|
|
|
135
|
-
|
|
126
|
+
p.log.step("Configuring mode parameters...");
|
|
136
127
|
for (const param of initParams) {
|
|
137
128
|
const suffix = param.description ? ` (${param.description})` : "";
|
|
138
|
-
const answer = await
|
|
139
|
-
|
|
129
|
+
const answer = await p.text({
|
|
130
|
+
message: `${param.label}${suffix}`,
|
|
131
|
+
placeholder: String(param.defaultValue),
|
|
132
|
+
defaultValue: String(param.defaultValue),
|
|
133
|
+
});
|
|
134
|
+
if (p.isCancel(answer)) {
|
|
135
|
+
p.cancel("Cancelled.");
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
|
138
|
+
if (answer === "" || answer === String(param.defaultValue)) {
|
|
140
139
|
params[param.name] = param.defaultValue;
|
|
141
140
|
} else if (param.type === "number") {
|
|
142
141
|
const num = Number(answer);
|
|
@@ -154,7 +153,7 @@ function checkBunVersion() {
|
|
|
154
153
|
const MIN_BUN = "1.3.5"; // Required for Bun.spawn terminal (PTY) support
|
|
155
154
|
const current = typeof Bun !== "undefined" ? Bun.version : null;
|
|
156
155
|
if (!current) {
|
|
157
|
-
|
|
156
|
+
p.log.warn("Not running under Bun. Pneuma requires Bun >= " + MIN_BUN);
|
|
158
157
|
return;
|
|
159
158
|
}
|
|
160
159
|
const [curMajor, curMinor, curPatch] = current.split(".").map(Number);
|
|
@@ -164,15 +163,70 @@ function checkBunVersion() {
|
|
|
164
163
|
(curMajor === minMajor && curMinor > minMinor) ||
|
|
165
164
|
(curMajor === minMajor && curMinor === minMinor && curPatch >= minPatch);
|
|
166
165
|
if (!ok) {
|
|
167
|
-
|
|
168
|
-
`
|
|
169
|
-
` Terminal features may not work. Run \`bun upgrade\` to update.`
|
|
166
|
+
p.log.warn(
|
|
167
|
+
`Bun ${current} detected, but >= ${MIN_BUN} is required. Terminal features may not work. Run \`bun upgrade\` to update.`
|
|
170
168
|
);
|
|
171
169
|
}
|
|
172
170
|
}
|
|
173
171
|
|
|
172
|
+
async function checkForUpdate(currentVersion: string) {
|
|
173
|
+
try {
|
|
174
|
+
const controller = new AbortController();
|
|
175
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
176
|
+
const res = await fetch("https://registry.npmjs.org/pneuma-skills/latest", {
|
|
177
|
+
signal: controller.signal,
|
|
178
|
+
});
|
|
179
|
+
clearTimeout(timeout);
|
|
180
|
+
if (!res.ok) return;
|
|
181
|
+
|
|
182
|
+
const { version: latest } = (await res.json()) as { version: string };
|
|
183
|
+
const [curMaj, curMin] = currentVersion.split(".").map(Number);
|
|
184
|
+
const [latMaj, latMin] = latest.split(".").map(Number);
|
|
185
|
+
|
|
186
|
+
// Only prompt when major or minor differs (skip patch-only differences)
|
|
187
|
+
if (curMaj === latMaj && curMin === latMin) return;
|
|
188
|
+
|
|
189
|
+
p.log.warn(`Update available: ${currentVersion} → ${latest}`);
|
|
190
|
+
const shouldUpdate = await p.confirm({
|
|
191
|
+
message: "Update to latest version?",
|
|
192
|
+
});
|
|
193
|
+
if (p.isCancel(shouldUpdate) || !shouldUpdate) return;
|
|
194
|
+
|
|
195
|
+
p.log.step(`Updating to pneuma-skills@${latest}...`);
|
|
196
|
+
const originalArgs = process.argv.slice(2);
|
|
197
|
+
const child = Bun.spawn(["bunx", `pneuma-skills@${latest}`, ...originalArgs], {
|
|
198
|
+
stdin: "inherit",
|
|
199
|
+
stdout: "inherit",
|
|
200
|
+
stderr: "inherit",
|
|
201
|
+
});
|
|
202
|
+
await child.exited;
|
|
203
|
+
process.exit(child.exitCode ?? 0);
|
|
204
|
+
} catch {
|
|
205
|
+
// Network error / timeout → silently skip
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function checkClaudeCode() {
|
|
210
|
+
const resolved = resolveBinary("claude");
|
|
211
|
+
if (!resolved) {
|
|
212
|
+
p.cancel(
|
|
213
|
+
"Claude Code CLI not found.\n" +
|
|
214
|
+
" Pneuma requires Claude Code to be installed and authenticated.\n" +
|
|
215
|
+
" Install it from: https://docs.anthropic.com/en/docs/claude-code"
|
|
216
|
+
);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
174
221
|
async function main() {
|
|
222
|
+
// Read version from package.json
|
|
223
|
+
const pkgPath = join(PROJECT_ROOT, "package.json");
|
|
224
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
225
|
+
|
|
226
|
+
p.intro(`pneuma-skills v${pkg.version}`);
|
|
227
|
+
|
|
175
228
|
checkBunVersion();
|
|
229
|
+
await checkForUpdate(pkg.version);
|
|
176
230
|
|
|
177
231
|
// Snapshot subcommand — intercept before mode validation
|
|
178
232
|
const rawArgs = process.argv.slice(2);
|
|
@@ -187,9 +241,10 @@ async function main() {
|
|
|
187
241
|
// Validate mode — support builtin names, local paths, and github: specifiers
|
|
188
242
|
if (!mode) {
|
|
189
243
|
const modeList = listBuiltinModes().join("|");
|
|
190
|
-
|
|
244
|
+
p.log.warn(
|
|
191
245
|
`Usage: pneuma <${modeList}|/path/to/mode|github:user/repo> [--workspace <path>] [--port <number>] [--no-open]`,
|
|
192
246
|
);
|
|
247
|
+
p.cancel("No mode specified.");
|
|
193
248
|
process.exit(1);
|
|
194
249
|
}
|
|
195
250
|
|
|
@@ -199,14 +254,14 @@ async function main() {
|
|
|
199
254
|
resolved = await resolveModeSource(mode, PROJECT_ROOT);
|
|
200
255
|
} catch (err) {
|
|
201
256
|
const msg = err instanceof Error ? err.message : String(err);
|
|
202
|
-
|
|
257
|
+
p.cancel(`Failed to resolve mode "${mode}": ${msg}`);
|
|
203
258
|
process.exit(1);
|
|
204
259
|
}
|
|
205
260
|
|
|
206
261
|
// For external modes, register them in the mode-loader before loading
|
|
207
262
|
if (resolved.type !== "builtin") {
|
|
208
263
|
registerExternalMode(resolved.name, resolved.path);
|
|
209
|
-
|
|
264
|
+
p.log.info(`External mode "${resolved.name}" loaded from ${resolved.path}`);
|
|
210
265
|
}
|
|
211
266
|
|
|
212
267
|
// Load mode manifest (no React deps — backend safe)
|
|
@@ -217,22 +272,27 @@ async function main() {
|
|
|
217
272
|
manifest = await loadModeManifest(modeName);
|
|
218
273
|
} catch (err) {
|
|
219
274
|
const msg = err instanceof Error ? err.message : String(err);
|
|
220
|
-
|
|
275
|
+
p.cancel(`Failed to load mode "${modeName}": ${msg}`);
|
|
221
276
|
process.exit(1);
|
|
222
277
|
}
|
|
223
278
|
|
|
279
|
+
// Verify Claude Code CLI is available before proceeding
|
|
280
|
+
checkClaudeCode();
|
|
281
|
+
|
|
224
282
|
if (!existsSync(workspace)) {
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
283
|
+
const shouldCreate = await p.confirm({
|
|
284
|
+
message: `Workspace does not exist: ${workspace}\n Create it?`,
|
|
285
|
+
});
|
|
286
|
+
if (p.isCancel(shouldCreate) || !shouldCreate) {
|
|
287
|
+
p.cancel("Cancelled.");
|
|
228
288
|
process.exit(0);
|
|
229
289
|
}
|
|
230
290
|
mkdirSync(workspace, { recursive: true });
|
|
231
|
-
|
|
291
|
+
p.log.success(`Created workspace: ${workspace}`);
|
|
232
292
|
}
|
|
233
293
|
|
|
234
|
-
|
|
235
|
-
|
|
294
|
+
p.log.info(`Mode: ${manifest.displayName} (${modeName})`);
|
|
295
|
+
p.log.info(`Workspace: ${workspace}`);
|
|
236
296
|
|
|
237
297
|
// 0.5 Resolve init params (interactive on first run, then cached)
|
|
238
298
|
let resolvedParams: Record<string, number | string> = {};
|
|
@@ -240,11 +300,11 @@ async function main() {
|
|
|
240
300
|
const cached = loadConfig(workspace);
|
|
241
301
|
if (cached) {
|
|
242
302
|
resolvedParams = cached;
|
|
243
|
-
|
|
303
|
+
p.log.step("Loaded init params from .pneuma/config.json");
|
|
244
304
|
} else {
|
|
245
305
|
resolvedParams = await promptInitParams(manifest);
|
|
246
306
|
saveConfig(workspace, resolvedParams);
|
|
247
|
-
|
|
307
|
+
p.log.step("Saved init params to .pneuma/config.json");
|
|
248
308
|
}
|
|
249
309
|
// Compute derived params (e.g. imageGenEnabled from API keys)
|
|
250
310
|
if (manifest.init.deriveParams) {
|
|
@@ -265,18 +325,20 @@ async function main() {
|
|
|
265
325
|
const effectiveApiPort = port || (isDev ? 17007 : 17996);
|
|
266
326
|
|
|
267
327
|
if (existsSync(skillTarget)) {
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
)
|
|
272
|
-
|
|
328
|
+
const shouldReplace = await p.confirm({
|
|
329
|
+
message: "Skill already exists. Replace with latest?",
|
|
330
|
+
});
|
|
331
|
+
if (p.isCancel(shouldReplace)) {
|
|
332
|
+
p.cancel("Cancelled.");
|
|
333
|
+
process.exit(0);
|
|
334
|
+
}
|
|
335
|
+
if (!shouldReplace) {
|
|
273
336
|
skipSkillInstall = true;
|
|
274
|
-
console.log("[pneuma] Keeping existing skills.");
|
|
275
337
|
}
|
|
276
338
|
}
|
|
277
339
|
|
|
278
340
|
if (!skipSkillInstall) {
|
|
279
|
-
|
|
341
|
+
p.log.step("Installing skill and preparing environment...");
|
|
280
342
|
installSkill(workspace, manifest.skill, modeSourceDir, resolvedParams, manifest.viewerApi, effectiveApiPort);
|
|
281
343
|
}
|
|
282
344
|
|
|
@@ -311,7 +373,7 @@ async function main() {
|
|
|
311
373
|
} else {
|
|
312
374
|
copyFileSync(srcPath, dstPath);
|
|
313
375
|
}
|
|
314
|
-
|
|
376
|
+
p.log.step(`Seeded workspace with ${dst}`);
|
|
315
377
|
}
|
|
316
378
|
}
|
|
317
379
|
}
|
|
@@ -319,9 +381,9 @@ async function main() {
|
|
|
319
381
|
|
|
320
382
|
// 2. Detect dev vs production mode (distDir, isDev computed earlier for port)
|
|
321
383
|
if (isDev) {
|
|
322
|
-
|
|
384
|
+
p.log.info("Development mode (serving via Vite)");
|
|
323
385
|
} else {
|
|
324
|
-
|
|
386
|
+
p.log.info("Production mode (serving built assets)");
|
|
325
387
|
}
|
|
326
388
|
|
|
327
389
|
// 3. Start server
|
|
@@ -372,7 +434,7 @@ async function main() {
|
|
|
372
434
|
|
|
373
435
|
if (existing?.agentSessionId) {
|
|
374
436
|
resuming = true;
|
|
375
|
-
|
|
437
|
+
p.log.info(`Resuming session: ${existing.agentSessionId}`);
|
|
376
438
|
}
|
|
377
439
|
|
|
378
440
|
// Persist session info
|
|
@@ -383,7 +445,7 @@ async function main() {
|
|
|
383
445
|
createdAt: existing?.createdAt || Date.now(),
|
|
384
446
|
});
|
|
385
447
|
|
|
386
|
-
|
|
448
|
+
p.log.info(`Agent session: ${session.sessionId}`);
|
|
387
449
|
|
|
388
450
|
// Auto-greeting for fresh sessions (driven by manifest)
|
|
389
451
|
if (!resuming && manifest.agent?.greeting) {
|
|
@@ -448,7 +510,7 @@ async function main() {
|
|
|
448
510
|
if (isDev) {
|
|
449
511
|
// Dev mode: start Vite dev server
|
|
450
512
|
const VITE_PORT = 17996;
|
|
451
|
-
|
|
513
|
+
p.log.step(`Starting Vite dev server on port ${VITE_PORT}...`);
|
|
452
514
|
|
|
453
515
|
// Pass external mode path to Vite via env var (for server.fs.allow)
|
|
454
516
|
const viteEnv: Record<string, string> = {
|
|
@@ -492,28 +554,27 @@ async function main() {
|
|
|
492
554
|
if (!noOpen) {
|
|
493
555
|
const debugParam = debug ? "&debug=1" : "";
|
|
494
556
|
const url = `http://localhost:${browserPort}?session=${session.sessionId}&mode=${modeName}${debugParam}`;
|
|
495
|
-
|
|
557
|
+
p.log.success(`Ready → ${url}`);
|
|
496
558
|
try {
|
|
497
559
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
498
560
|
Bun.spawn([opener, url], { stdout: "ignore", stderr: "ignore" });
|
|
499
561
|
} catch {
|
|
500
|
-
|
|
562
|
+
p.log.warn(`Could not open browser. Visit: ${url}`);
|
|
501
563
|
}
|
|
502
564
|
}
|
|
503
565
|
|
|
504
566
|
// Graceful shutdown
|
|
505
567
|
const shutdown = async () => {
|
|
506
|
-
console.log("\n[pneuma] Shutting down...");
|
|
507
568
|
clearInterval(historyInterval);
|
|
508
569
|
// Final history save
|
|
509
570
|
const history = wsBridge.getMessageHistory(session.sessionId);
|
|
510
571
|
if (history.length > 0) {
|
|
511
572
|
saveHistory(workspace, history);
|
|
512
|
-
console.log(`[pneuma] Saved ${history.length} messages to history`);
|
|
513
573
|
}
|
|
514
574
|
viteProc?.kill();
|
|
515
575
|
await backend.killAll();
|
|
516
576
|
server.stop(true);
|
|
577
|
+
p.outro("Goodbye!");
|
|
517
578
|
process.exit(0);
|
|
518
579
|
};
|
|
519
580
|
process.on("SIGTERM", shutdown);
|
|
@@ -521,6 +582,6 @@ async function main() {
|
|
|
521
582
|
}
|
|
522
583
|
|
|
523
584
|
main().catch((err) => {
|
|
524
|
-
|
|
585
|
+
p.cancel(`Fatal error: ${err instanceof Error ? err.message : String(err)}`);
|
|
525
586
|
process.exit(1);
|
|
526
587
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pneuma-skills",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Framework for Code Agents to do WYSIWYG editing on HTML-based content",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"prepublishOnly": "bun run build"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
+
"@clack/prompts": "^1.0.1",
|
|
39
40
|
"@codemirror/lang-css": "^6.3.1",
|
|
40
41
|
"@codemirror/lang-html": "^6.4.11",
|
|
41
42
|
"@codemirror/lang-javascript": "^6.2.4",
|
|
@@ -53,10 +54,13 @@
|
|
|
53
54
|
"chokidar": "^4.0.0",
|
|
54
55
|
"diff": "^8.0.3",
|
|
55
56
|
"hono": "^4.7.0",
|
|
57
|
+
"react": "^19.0.0",
|
|
58
|
+
"react-dom": "^19.0.0",
|
|
56
59
|
"react-markdown": "^10.1.0",
|
|
57
60
|
"react-resizable-panels": "^4.6.2",
|
|
58
61
|
"rehype-raw": "^7.0.0",
|
|
59
|
-
"remark-gfm": "^4.0.1"
|
|
62
|
+
"remark-gfm": "^4.0.1",
|
|
63
|
+
"zustand": "^5.0.0"
|
|
60
64
|
},
|
|
61
65
|
"devDependencies": {
|
|
62
66
|
"@tailwindcss/vite": "^4.0.0",
|
|
@@ -65,11 +69,8 @@
|
|
|
65
69
|
"@types/react": "^19.0.0",
|
|
66
70
|
"@types/react-dom": "^19.0.0",
|
|
67
71
|
"@vitejs/plugin-react": "^4.4.0",
|
|
68
|
-
"react": "^19.0.0",
|
|
69
|
-
"react-dom": "^19.0.0",
|
|
70
72
|
"tailwindcss": "^4.0.0",
|
|
71
73
|
"typescript": "^5.9.3",
|
|
72
|
-
"vite": "^6.3.0"
|
|
73
|
-
"zustand": "^5.0.0"
|
|
74
|
+
"vite": "^6.3.0"
|
|
74
75
|
}
|
|
75
76
|
}
|