pneuma-skills 0.2.1 → 0.5.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 CHANGED
@@ -126,7 +126,7 @@ pneuma-skills/
126
126
  │ ├── StreamingText.tsx # Streaming response display
127
127
  │ ├── ActivityIndicator.tsx # Thinking/tool progress indicator
128
128
  │ ├── PermissionBanner.tsx # Tool permission approval UI
129
- │ └── StatusBar.tsx # Connection status + session info
129
+ │ └── TopBar.tsx # Tab navigation + connection status
130
130
  ├── skill/
131
131
  │ └── doc/SKILL.md # Doc Mode skill prompt for Claude Code
132
132
  ├── docs/adr/ # Architecture Decision Records
package/bin/pneuma.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import { resolve, dirname, join } from "node:path";
10
- import { existsSync, copyFileSync, mkdirSync, readFileSync } from "node:fs";
10
+ import { existsSync, copyFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
11
11
  import * as readline from "node:readline";
12
12
  import { startServer } from "../server/index.js";
13
13
  import { CliLauncher } from "../server/cli-launcher.js";
@@ -16,6 +16,49 @@ import { startFileWatcher } from "../server/file-watcher.js";
16
16
 
17
17
  const PROJECT_ROOT = resolve(dirname(import.meta.path), "..");
18
18
 
19
+ // ── Session persistence ──────────────────────────────────────────────────────
20
+
21
+ interface PersistedSession {
22
+ sessionId: string;
23
+ cliSessionId?: string;
24
+ mode: string;
25
+ createdAt: number;
26
+ }
27
+
28
+ function loadSession(workspace: string): PersistedSession | null {
29
+ const filePath = join(workspace, ".pneuma", "session.json");
30
+ try {
31
+ const content = readFileSync(filePath, "utf-8");
32
+ return JSON.parse(content);
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ function saveSession(workspace: string, session: PersistedSession): void {
39
+ const dir = join(workspace, ".pneuma");
40
+ mkdirSync(dir, { recursive: true });
41
+ writeFileSync(join(dir, "session.json"), JSON.stringify(session, null, 2));
42
+ }
43
+
44
+ function loadHistory(workspace: string): unknown[] {
45
+ try {
46
+ const content = readFileSync(join(workspace, ".pneuma", "history.json"), "utf-8");
47
+ const data = JSON.parse(content);
48
+ return Array.isArray(data) ? data : [];
49
+ } catch {
50
+ return [];
51
+ }
52
+ }
53
+
54
+ function saveHistory(workspace: string, history: unknown[]): void {
55
+ const dir = join(workspace, ".pneuma");
56
+ mkdirSync(dir, { recursive: true });
57
+ writeFileSync(join(dir, "history.json"), JSON.stringify(history));
58
+ }
59
+
60
+ // ── CLI arg parsing ──────────────────────────────────────────────────────────
61
+
19
62
  function parseArgs(argv: string[]) {
20
63
  const args = argv.slice(2); // skip bun + script path
21
64
  let mode = "";
@@ -49,7 +92,31 @@ function ask(question: string): Promise<string> {
49
92
  });
50
93
  }
51
94
 
95
+ // ── Main ─────────────────────────────────────────────────────────────────────
96
+
97
+ function checkBunVersion() {
98
+ const MIN_BUN = "1.3.5"; // Required for Bun.spawn terminal (PTY) support
99
+ const current = typeof Bun !== "undefined" ? Bun.version : null;
100
+ if (!current) {
101
+ console.warn("[pneuma] Warning: Not running under Bun. Pneuma requires Bun >= " + MIN_BUN);
102
+ return;
103
+ }
104
+ const [curMajor, curMinor, curPatch] = current.split(".").map(Number);
105
+ const [minMajor, minMinor, minPatch] = MIN_BUN.split(".").map(Number);
106
+ const ok =
107
+ curMajor > minMajor ||
108
+ (curMajor === minMajor && curMinor > minMinor) ||
109
+ (curMajor === minMajor && curMinor === minMinor && curPatch >= minPatch);
110
+ if (!ok) {
111
+ console.warn(
112
+ `[pneuma] Warning: Bun ${current} detected, but >= ${MIN_BUN} is required.` +
113
+ ` Terminal features may not work. Run \`bun upgrade\` to update.`
114
+ );
115
+ }
116
+ }
117
+
52
118
  async function main() {
119
+ checkBunVersion();
53
120
  const { mode, workspace, port, noOpen } = parseArgs(process.argv);
54
121
 
55
122
  if (!mode || mode !== "doc") {
@@ -113,21 +180,100 @@ async function main() {
113
180
  ...(isDev ? {} : { distDir }),
114
181
  });
115
182
 
116
- // 4. Launch CLI
183
+ // 4. Launch CLI (with session resume if available)
117
184
  const launcher = new CliLauncher(actualPort);
118
185
 
119
- // When the CLI reports its internal session_id, store it
186
+ // When the CLI reports its internal session_id, persist it
120
187
  wsBridge.onCLISessionIdReceived((sessionId, cliSessionId) => {
121
188
  launcher.setCLISessionId(sessionId, cliSessionId);
189
+ // Persist to .pneuma/session.json
190
+ const persisted = loadSession(workspace);
191
+ if (persisted && persisted.sessionId === sessionId) {
192
+ persisted.cliSessionId = cliSessionId;
193
+ saveSession(workspace, persisted);
194
+ console.log(`[pneuma] Saved cliSessionId for resume: ${cliSessionId}`);
195
+ }
122
196
  });
123
197
 
198
+ // Check for existing session to resume
199
+ const existing = loadSession(workspace);
200
+ let resuming = false;
201
+
124
202
  const session = launcher.launch({
125
203
  cwd: workspace,
126
204
  permissionMode: "bypassPermissions",
205
+ // Reuse sessionId for stable WS routing
206
+ ...(existing?.cliSessionId ? {
207
+ sessionId: existing.sessionId,
208
+ resumeSessionId: existing.cliSessionId,
209
+ } : {}),
210
+ });
211
+
212
+ if (existing?.cliSessionId) {
213
+ resuming = true;
214
+ console.log(`[pneuma] Resuming session: ${existing.cliSessionId}`);
215
+ }
216
+
217
+ // Persist session info
218
+ saveSession(workspace, {
219
+ sessionId: session.sessionId,
220
+ cliSessionId: existing?.cliSessionId,
221
+ mode,
222
+ createdAt: existing?.createdAt || Date.now(),
127
223
  });
128
224
 
129
225
  console.log(`[pneuma] CLI session started: ${session.sessionId}`);
130
226
 
227
+ // Auto-greeting for fresh sessions — sends a hidden prompt so Claude greets the user
228
+ if (!resuming) {
229
+ const greeting = "The user just opened the Pneuma document editor workspace. Briefly greet them and let them know you're ready to help edit and create documents. Keep it to 1-2 sentences.";
230
+ wsBridge.injectGreeting(session.sessionId, greeting);
231
+ console.log("[pneuma] Sent auto-greeting for fresh session");
232
+ }
233
+
234
+ // Load persisted message history into WsBridge
235
+ const savedHistory = loadHistory(workspace);
236
+ if (savedHistory.length > 0) {
237
+ wsBridge.loadMessageHistory(session.sessionId, savedHistory as any);
238
+ console.log(`[pneuma] Restored ${savedHistory.length} messages from history`);
239
+ }
240
+
241
+ // Periodically persist message history (debounced — every 5s)
242
+ const historyInterval = setInterval(() => {
243
+ const history = wsBridge.getMessageHistory(session.sessionId);
244
+ if (history.length > 0) {
245
+ saveHistory(workspace, history);
246
+ }
247
+ }, 5_000);
248
+
249
+ // Handle CLI exit: surface errors + clear stale resume state
250
+ launcher.onSessionExited((exitedId, exitCode) => {
251
+ // Broadcast CLI errors to browser
252
+ if (exitCode !== 0 && exitCode !== 143 /* SIGTERM = normal shutdown */) {
253
+ let errorMsg: string;
254
+ if (exitCode === 127) {
255
+ errorMsg = "Claude Code CLI not found. Please install it: https://docs.anthropic.com/claude-code";
256
+ } else {
257
+ errorMsg = `Claude Code exited unexpectedly (code ${exitCode}). Check CLI installation and subscription status.`;
258
+ }
259
+ wsBridge.broadcastToSession(exitedId, { type: "error", message: errorMsg });
260
+ }
261
+
262
+ // If resume fails (CLI exits quickly), clear cliSessionId from persistence
263
+ if (exitedId === session.sessionId && resuming) {
264
+ const info = launcher.getSession(exitedId);
265
+ if (info && !info.cliSessionId) {
266
+ // Resume failed, cliSessionId was cleared by launcher
267
+ const persisted = loadSession(workspace);
268
+ if (persisted) {
269
+ persisted.cliSessionId = undefined;
270
+ saveSession(workspace, persisted);
271
+ console.log("[pneuma] Resume failed, cleared cliSessionId. Restart for fresh session.");
272
+ }
273
+ }
274
+ }
275
+ });
276
+
131
277
  // 5. Start file watcher
132
278
  startFileWatcher(workspace, (files) => {
133
279
  wsBridge.broadcastToSession(session.sessionId, {
@@ -187,6 +333,13 @@ async function main() {
187
333
  // Graceful shutdown
188
334
  const shutdown = async () => {
189
335
  console.log("\n[pneuma] Shutting down...");
336
+ clearInterval(historyInterval);
337
+ // Final history save
338
+ const history = wsBridge.getMessageHistory(session.sessionId);
339
+ if (history.length > 0) {
340
+ saveHistory(workspace, history);
341
+ console.log(`[pneuma] Saved ${history.length} messages to history`);
342
+ }
190
343
  viteProc?.kill();
191
344
  await launcher.killAll();
192
345
  server.stop(true);