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 +1 -1
- package/bin/pneuma.ts +156 -3
- package/dist/assets/EditorPanel-CpYodpiX.js +31 -0
- package/dist/assets/TerminalPanel--aSGlXk2.js +39 -0
- package/dist/assets/TerminalPanel-6GBZ9nXN.css +32 -0
- package/dist/assets/index-DotlmM58.css +1 -0
- package/dist/assets/index-WbXrvKao.js +96 -0
- package/dist/index.html +2 -2
- package/package.json +14 -2
- package/server/cli-launcher.ts +20 -1
- package/server/file-watcher.ts +11 -1
- package/server/index.ts +288 -4
- package/server/session-types.ts +17 -2
- package/server/skill-installer.ts +22 -0
- package/server/terminal-manager.ts +171 -0
- package/server/ws-bridge-types.ts +6 -1
- package/server/ws-bridge.ts +139 -2
- package/dist/assets/index-Dl3AjGxu.js +0 -90
- package/dist/assets/index-DveJQfyt.css +0 -1
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
|
-
│ └──
|
|
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,
|
|
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);
|