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.
Files changed (3) hide show
  1. package/README.md +17 -9
  2. package/bin/pneuma.ts +109 -48
  3. package/package.json +7 -6
package/README.md CHANGED
@@ -1,15 +1,21 @@
1
- # Pneuma Skills
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
- **An extensible delivery platform for filesystem-based Agent capabilities.**
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
- # Run directly (no install needed)
44
- bunx pneuma-skills doc --workspace ~/my-notes
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 readline from "node:readline";
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
- console.log("[pneuma] Configuring mode parameters...");
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 ask(`${param.label}${suffix} [${param.defaultValue}]: `);
139
- if (answer === "") {
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
- console.warn("[pneuma] Warning: Not running under Bun. Pneuma requires Bun >= " + MIN_BUN);
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
- console.warn(
168
- `[pneuma] Warning: Bun ${current} detected, but >= ${MIN_BUN} is required.` +
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
- console.log(
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
- console.error(`[pneuma] Failed to resolve mode "${mode}": ${msg}`);
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
- console.log(`[pneuma] External mode "${resolved.name}" loaded from ${resolved.path}`);
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
- console.error(`[pneuma] Failed to load mode "${modeName}": ${msg}`);
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 answer = await ask(`Workspace does not exist: ${workspace}\nCreate it? [Y/n] `);
226
- if (answer.toLowerCase() === "n") {
227
- console.log("[pneuma] Aborted.");
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
- console.log(`[pneuma] Created workspace: ${workspace}`);
291
+ p.log.success(`Created workspace: ${workspace}`);
232
292
  }
233
293
 
234
- console.log(`[pneuma] Mode: ${manifest.displayName} (${modeName})`);
235
- console.log(`[pneuma] Workspace: ${workspace}`);
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
- console.log("[pneuma] Loaded init params from .pneuma/config.json");
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
- console.log("[pneuma] Saved init params to .pneuma/config.json");
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 answer = await ask(
269
- `[pneuma] Existing skills found at ${skillTarget}\n` +
270
- ` Replace with latest version? [Y/n] `,
271
- );
272
- if (answer.toLowerCase() === "n") {
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
- console.log("[pneuma] Installing skill and preparing environment...");
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
- console.log(`[pneuma] Seeded workspace with ${dst}`);
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
- console.log("[pneuma] Development mode (serving via Vite)");
384
+ p.log.info("Development mode (serving via Vite)");
323
385
  } else {
324
- console.log("[pneuma] Production mode (serving built assets)");
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
- console.log(`[pneuma] Resuming session: ${existing.agentSessionId}`);
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
- console.log(`[pneuma] Agent session started: ${session.sessionId}`);
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
- console.log(`[pneuma] Starting Vite dev server on port ${VITE_PORT}...`);
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
- console.log(`[pneuma] Opening browser: ${url}`);
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
- console.log(`[pneuma] Could not open browser. Visit: ${url}`);
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
- console.error("[pneuma] Fatal error:", err);
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.7.2",
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
  }