@cremini/skillpack 1.0.2

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.
@@ -0,0 +1,41 @@
1
+ # Skillapp
2
+
3
+ ## Quick Start
4
+
5
+ ### macOS / Linux
6
+
7
+ ```bash
8
+ chmod +x start.sh
9
+ ./start.sh
10
+ ```
11
+
12
+ ### Windows
13
+
14
+ Double-click `start.bat` or run:
15
+
16
+ ```cmd
17
+ start.bat
18
+ ```
19
+
20
+ After the server starts, your browser opens [http://127.0.0.1:26313](http://127.0.0.1:26313) automatically.
21
+
22
+ By default, the server only listens on `127.0.0.1` so the API key you enter stays on the local machine and is not exposed to your LAN.
23
+
24
+ ## First Use
25
+
26
+ 1. Enter your OpenAI or Anthropic API key in the left sidebar
27
+ 2. Type a message in the chat box to start a conversation
28
+ 3. Click a prompt example on the welcome screen to prefill the input
29
+
30
+ ## Requirements
31
+
32
+ - Node.js >= 20
33
+
34
+ ## Environment Variables
35
+
36
+ | Variable | Description |
37
+ | ------------------- | ------------------------------------------------------- |
38
+ | `OPENAI_API_KEY` | OpenAI API key, optional if you set it in the web UI |
39
+ | `ANTHROPIC_API_KEY` | Anthropic API key, optional if you set it in the web UI |
40
+ | `HOST` | Bind address, defaults to `127.0.0.1` |
41
+ | `PORT` | Server port, defaults to `26313` |
@@ -0,0 +1,15 @@
1
+ @echo off
2
+ cd /d "%~dp0"
3
+
4
+ echo.
5
+ echo Starting Skills Pack...
6
+ echo.
7
+
8
+ if not exist "server\node_modules" (
9
+ echo Installing dependencies...
10
+ cd server && npm ci --omit=dev && cd ..
11
+ echo.
12
+ )
13
+
14
+ rem Start the server (port detection and browser launch are handled by server\index.js)
15
+ cd server && node index.js
@@ -0,0 +1,22 @@
1
+ #!/bin/bash
2
+ cd "$(dirname "$0")"
3
+
4
+ # Read the pack name
5
+ PACK_NAME="Skills Pack"
6
+ if [ -f "app.json" ] && command -v node &> /dev/null; then
7
+ PACK_NAME=$(node -e "console.log(JSON.parse(require('fs').readFileSync('app.json','utf-8')).name)" 2>/dev/null || echo "Skills Pack")
8
+ fi
9
+
10
+ echo ""
11
+ echo " Starting ${PACK_NAME}..."
12
+ echo ""
13
+
14
+ # Install dependencies
15
+ if [ ! -d "server/node_modules" ]; then
16
+ echo " Installing dependencies..."
17
+ cd server && npm install --omit=dev && cd ..
18
+ echo ""
19
+ fi
20
+
21
+ # Start the server
22
+ cd server && node index.js
@@ -0,0 +1,161 @@
1
+ import path from "node:path";
2
+ import {
3
+ AuthStorage,
4
+ createAgentSession,
5
+ ModelRegistry,
6
+ SessionManager,
7
+ DefaultResourceLoader,
8
+ } from "@mariozechner/pi-coding-agent";
9
+
10
+ const DEBUG = true;
11
+
12
+ const log = (...args) => DEBUG && console.log(...args);
13
+ const write = (data) => DEBUG && process.stdout.write(data);
14
+
15
+ /**
16
+ * Handle incoming WebSocket connection using pi-coding-agent
17
+ * @param {import("ws").WebSocket} ws
18
+ * @param {object} options
19
+ * @param {string} options.apiKey - OpenAI API Key
20
+ * @param {string} options.rootDir - Pack root directory
21
+ */
22
+ export async function handleWsConnection(
23
+ ws,
24
+ { apiKey, rootDir, provider = "openai", modelId = "gpt-5.4" },
25
+ ) {
26
+ try {
27
+ // Create an in-memory auth storage to avoid touching disk
28
+ const authStorage = AuthStorage.inMemory({
29
+ [provider]: { type: "api_key", key: apiKey },
30
+ });
31
+ authStorage.setRuntimeApiKey(provider, apiKey);
32
+
33
+ const modelRegistry = new ModelRegistry(authStorage);
34
+ const model = modelRegistry.find(provider, modelId);
35
+
36
+ const sessionManager = SessionManager.inMemory();
37
+
38
+ const skillsPath = path.resolve(rootDir, "skills");
39
+ log(`[ChatProxy] Loading additional skills from: ${skillsPath}`);
40
+
41
+ const resourceLoader = new DefaultResourceLoader({
42
+ cwd: rootDir,
43
+ additionalSkillPaths: [skillsPath], // 手动加载 rootDir/skills
44
+ });
45
+ await resourceLoader.reload();
46
+
47
+ const { session } = await createAgentSession({
48
+ cwd: rootDir, // Allow pi-coding-agent to find skills in this pack's directory
49
+ authStorage,
50
+ modelRegistry,
51
+ sessionManager,
52
+ resourceLoader,
53
+ model,
54
+ });
55
+
56
+ // Stream agent events to the WebSocket
57
+ session.subscribe((event) => {
58
+ switch (event.type) {
59
+ case "agent_start":
60
+ log("\n=== [PI-CODING-AGENT SESSION START] ===");
61
+ log("System Prompt:\n", session.systemPrompt);
62
+ log("========================================\n");
63
+ ws.send(JSON.stringify({ type: "agent_start" }));
64
+ break;
65
+
66
+ case "message_start":
67
+ log(`\n--- [Message Start: ${event.message?.role}] ---`);
68
+ if (event.message?.role === "user") {
69
+ log(JSON.stringify(event.message.content, null, 2));
70
+ }
71
+ ws.send(
72
+ JSON.stringify({
73
+ type: "message_start",
74
+ role: event.message?.role,
75
+ }),
76
+ );
77
+ break;
78
+
79
+ case "message_update":
80
+ if (event.assistantMessageEvent?.type === "text_delta") {
81
+ write(event.assistantMessageEvent.delta);
82
+ ws.send(
83
+ JSON.stringify({
84
+ type: "text_delta",
85
+ delta: event.assistantMessageEvent.delta,
86
+ }),
87
+ );
88
+ } else if (event.assistantMessageEvent?.type === "thinking_delta") {
89
+ ws.send(
90
+ JSON.stringify({
91
+ type: "thinking_delta",
92
+ delta: event.assistantMessageEvent.delta,
93
+ }),
94
+ );
95
+ }
96
+ break;
97
+
98
+ case "message_end":
99
+ log(`\n--- [Message End: ${event.message?.role}] ---`);
100
+ ws.send(
101
+ JSON.stringify({
102
+ type: "message_end",
103
+ role: event.message?.role,
104
+ }),
105
+ );
106
+ break;
107
+
108
+ case "tool_execution_start":
109
+ log(`\n>>> [Tool Execution Start: ${event.toolName}] >>>`);
110
+ log("Args:", JSON.stringify(event.args, null, 2));
111
+ ws.send(
112
+ JSON.stringify({
113
+ type: "tool_start",
114
+ toolName: event.toolName,
115
+ toolInput: event.args,
116
+ }),
117
+ );
118
+ break;
119
+
120
+ case "tool_execution_end":
121
+ log(`<<< [Tool Execution End: ${event.toolName}] <<<`);
122
+ log(`Error: ${event.isError ? "Yes" : "No"}`);
123
+ ws.send(
124
+ JSON.stringify({
125
+ type: "tool_end",
126
+ toolName: event.toolName,
127
+ isError: event.isError,
128
+ result: event.result,
129
+ }),
130
+ );
131
+ break;
132
+
133
+ case "agent_end":
134
+ log("\n=== [PI-CODING-AGENT SESSION END] ===\n");
135
+ ws.send(JSON.stringify({ type: "agent_end" }));
136
+ break;
137
+ }
138
+ });
139
+
140
+ // Listen for incoming messages from the frontend
141
+ ws.on("message", async (data) => {
142
+ try {
143
+ const payload = JSON.parse(data.toString());
144
+ if (payload.text) {
145
+ // Send prompt to the agent, the session will handle message history natively
146
+ await session.prompt(payload.text);
147
+ ws.send(JSON.stringify({ done: true }));
148
+ }
149
+ } catch (err) {
150
+ ws.send(JSON.stringify({ error: String(err) }));
151
+ }
152
+ });
153
+
154
+ ws.on("close", () => {
155
+ session.dispose();
156
+ });
157
+ } catch (err) {
158
+ ws.send(JSON.stringify({ error: String(err) }));
159
+ ws.close();
160
+ }
161
+ }
@@ -0,0 +1,59 @@
1
+ import express from "express";
2
+ import path from "node:path";
3
+ import fs from "node:fs";
4
+ import { fileURLToPath } from "node:url";
5
+ import { createServer } from "node:http";
6
+ import { exec } from "node:child_process";
7
+ import { registerRoutes } from "./routes.js";
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+
11
+ const rootDir = process.env.PACK_ROOT || path.join(__dirname, "..");
12
+
13
+ const webDir = fs.existsSync(path.join(rootDir, "web"))
14
+ ? path.join(rootDir, "web")
15
+ : path.join(__dirname, "..", "web");
16
+
17
+ const app = express();
18
+ app.use(express.json());
19
+
20
+ // Static file serving
21
+ app.use(express.static(webDir));
22
+
23
+ const server = createServer(app);
24
+
25
+ // Register API routes
26
+ registerRoutes(app, server, rootDir);
27
+
28
+ const HOST = process.env.HOST || "127.0.0.1";
29
+ const DEFAULT_PORT = 26313;
30
+
31
+ function tryListen(port) {
32
+ server.listen(port, HOST, () => {
33
+ const url = `http://${HOST}:${port}`;
34
+ console.log(`\n Skills Pack Server`);
35
+ console.log(` Running at ${url}\n`);
36
+
37
+ // Open the browser automatically
38
+ const cmd =
39
+ process.platform === "darwin"
40
+ ? `open ${url}`
41
+ : process.platform === "win32"
42
+ ? `start ${url}`
43
+ : `xdg-open ${url}`;
44
+ exec(cmd, () => {});
45
+ });
46
+
47
+ server.once("error", (err) => {
48
+ if (err.code === "EADDRINUSE") {
49
+ console.log(` Port ${port} is in use, trying ${port + 1}...`);
50
+ server.close();
51
+ tryListen(port + 1);
52
+ } else {
53
+ throw err;
54
+ }
55
+ });
56
+ }
57
+
58
+ const startPort = Number(process.env.PORT) || DEFAULT_PORT;
59
+ tryListen(startPort);
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "skillpack-server",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "start": "node index.js"
7
+ },
8
+ "dependencies": {
9
+ "express": "^5.1.0",
10
+ "@mariozechner/pi-coding-agent": "^0.57.1",
11
+ "ws": "^8.19.0"
12
+ }
13
+ }
@@ -0,0 +1,104 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { WebSocketServer } from "ws";
4
+
5
+ import { handleWsConnection } from "./chat-proxy.js";
6
+
7
+ /**
8
+ * Read the app.json config.
9
+ * @param {string} rootDir
10
+ */
11
+ function getPackConfig(rootDir) {
12
+ const raw = fs.readFileSync(path.join(rootDir, "app.json"), "utf-8");
13
+ return JSON.parse(raw);
14
+ }
15
+
16
+ /**
17
+ * Register all API routes.
18
+ * @param {import("express").Express} app
19
+ * @param {import("node:http").Server} server
20
+ * @param {string} rootDir - Root directory containing app.json and skills/
21
+ */
22
+ export function registerRoutes(app, server, rootDir) {
23
+ // API key and provider are stored in runtime memory
24
+ let apiKey = "";
25
+ let currentProvider = "openai";
26
+
27
+ if (process.env.OPENAI_API_KEY) {
28
+ apiKey = process.env.OPENAI_API_KEY;
29
+ currentProvider = "openai";
30
+ } else if (process.env.ANTHROPIC_API_KEY) {
31
+ apiKey = process.env.ANTHROPIC_API_KEY;
32
+ currentProvider = "anthropic";
33
+ }
34
+
35
+ // Get pack config
36
+ app.get("/api/config", (req, res) => {
37
+ const config = getPackConfig(rootDir);
38
+ res.json({
39
+ name: config.name,
40
+ description: config.description,
41
+ prompts: config.prompts || [],
42
+ skills: config.skills || [],
43
+ hasApiKey: !!apiKey,
44
+ provider: currentProvider,
45
+ });
46
+ });
47
+
48
+ // Get skills list
49
+ app.get("/api/skills", (req, res) => {
50
+ const config = getPackConfig(rootDir);
51
+ res.json(config.skills || []);
52
+ });
53
+
54
+ // Set API key
55
+ app.post("/api/config/key", (req, res) => {
56
+ const { key, provider } = req.body;
57
+ if (!key) {
58
+ return res.status(400).json({ error: "API key is required" });
59
+ }
60
+ apiKey = key;
61
+ if (provider) {
62
+ currentProvider = provider;
63
+ }
64
+ res.json({ success: true, provider: currentProvider });
65
+ });
66
+
67
+ // WebSocket chat service
68
+ const wss = new WebSocketServer({ noServer: true });
69
+
70
+ server.on("upgrade", (request, socket, head) => {
71
+ if (request.url.startsWith("/api/chat")) {
72
+ wss.handleUpgrade(request, socket, head, (ws) => {
73
+ wss.emit("connection", ws, request);
74
+ });
75
+ } else {
76
+ socket.destroy();
77
+ }
78
+ });
79
+
80
+ wss.on("connection", (ws, request) => {
81
+ const url = new URL(
82
+ request.url,
83
+ `http://${request.headers.host || "127.0.0.1"}`,
84
+ );
85
+ const reqProvider = url.searchParams.get("provider") || currentProvider;
86
+
87
+ if (!apiKey) {
88
+ ws.send(JSON.stringify({ error: "Please set an API key first" }));
89
+ ws.close();
90
+ return;
91
+ }
92
+
93
+ const config = getPackConfig(rootDir);
94
+
95
+ const modelId = reqProvider === "anthropic" ? "claude-opus-4-6" : "gpt-5.4";
96
+
97
+ handleWsConnection(ws, { apiKey, rootDir, provider: reqProvider, modelId });
98
+ });
99
+
100
+ // Clear session
101
+ app.delete("/api/chat", (req, res) => {
102
+ res.json({ success: true });
103
+ });
104
+ }
@@ -0,0 +1,31 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * Recursively load the contents of all SKILL.md files under skills/.
6
+ * @param {string} rootDir - Root directory containing skills/
7
+ * @returns {string[]} Array of SKILL.md file contents
8
+ */
9
+ export function loadSkillContents(rootDir) {
10
+ const skillsDir = path.join(rootDir, "skills");
11
+ const contents = [];
12
+
13
+ if (!fs.existsSync(skillsDir)) {
14
+ return contents;
15
+ }
16
+
17
+ function walk(dir) {
18
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
19
+ for (const entry of entries) {
20
+ const full = path.join(dir, entry.name);
21
+ if (entry.isDirectory()) {
22
+ walk(full);
23
+ } else if (entry.name === "SKILL.md") {
24
+ contents.push(fs.readFileSync(full, "utf-8"));
25
+ }
26
+ }
27
+ }
28
+
29
+ walk(skillsDir);
30
+ return contents;
31
+ }