@cremini/skillpack 1.0.8 → 1.0.9-im.1

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 (33) hide show
  1. package/README.md +3 -1
  2. package/dist/cli.js +45 -11
  3. package/package.json +3 -2
  4. package/runtime/server/dist/adapters/markdown.js +74 -0
  5. package/runtime/server/dist/adapters/markdown.js.map +1 -0
  6. package/runtime/server/dist/adapters/slack.js +369 -0
  7. package/runtime/server/dist/adapters/slack.js.map +1 -0
  8. package/runtime/server/dist/adapters/telegram.js +199 -0
  9. package/runtime/server/dist/adapters/telegram.js.map +1 -0
  10. package/runtime/server/dist/adapters/types.js +2 -0
  11. package/runtime/server/dist/adapters/types.js.map +1 -0
  12. package/runtime/server/dist/adapters/web.js +168 -0
  13. package/runtime/server/dist/adapters/web.js.map +1 -0
  14. package/runtime/server/dist/agent.js +219 -0
  15. package/runtime/server/dist/agent.js.map +1 -0
  16. package/runtime/server/dist/config.js +73 -0
  17. package/runtime/server/dist/config.js.map +1 -0
  18. package/runtime/server/dist/index.js +122 -0
  19. package/runtime/server/dist/index.js.map +1 -0
  20. package/runtime/server/package-lock.json +2768 -121
  21. package/runtime/server/package.json +13 -3
  22. package/runtime/start.bat +2 -2
  23. package/runtime/start.sh +1 -1
  24. package/runtime/web/index.html +58 -15
  25. package/runtime/web/js/api.js +13 -0
  26. package/runtime/web/{app.js → js/chat.js} +126 -193
  27. package/runtime/web/js/config.js +15 -0
  28. package/runtime/web/js/main.js +47 -0
  29. package/runtime/web/js/settings.js +132 -0
  30. package/runtime/web/styles.css +171 -0
  31. package/runtime/server/chat-proxy.js +0 -161
  32. package/runtime/server/index.js +0 -63
  33. package/runtime/server/routes.js +0 -104
@@ -0,0 +1,47 @@
1
+ import { state, loadConfig } from "./config.js";
2
+ import { initSettings } from "./settings.js";
3
+ import { initChat, showWelcome } from "./chat.js";
4
+
5
+ async function init() {
6
+ try {
7
+ const config = await loadConfig();
8
+
9
+ // Set Pack Name & Description
10
+ const elName = document.getElementById("pack-name");
11
+ const elDesc = document.getElementById("pack-desc");
12
+ if (elName) elName.textContent = config.name || "Skills Pack";
13
+ if (elDesc) elDesc.textContent = config.description || "";
14
+ document.title = config.name || "Skills Pack";
15
+
16
+ // Set Sidebar Skills list
17
+ const skillsList = document.getElementById("skills-list");
18
+ if (skillsList && config.skills) {
19
+ skillsList.innerHTML = config.skills
20
+ .map(
21
+ (s) =>
22
+ `<li><div class="skill-name">${s.name}</div><div class="skill-desc">${s.description}</div></li>`,
23
+ )
24
+ .join("");
25
+ }
26
+
27
+ // Pre-fill prompt if exactly one
28
+ if (config.prompts && config.prompts.length === 1) {
29
+ const input = document.getElementById("user-input");
30
+ if (input) {
31
+ input.value = config.prompts[0];
32
+ input.style.height = "auto";
33
+ input.style.height = Math.min(input.scrollHeight, 120) + "px";
34
+ }
35
+ }
36
+
37
+ showWelcome(config);
38
+ } catch (err) {
39
+ console.error("Initialization Failed:", err);
40
+ }
41
+
42
+ initSettings();
43
+ initChat();
44
+ }
45
+
46
+ // Start application
47
+ init();
@@ -0,0 +1,132 @@
1
+ import { state } from "./config.js";
2
+ import { saveConfigData } from "./api.js";
3
+
4
+ // DOM Elements
5
+ let dialog;
6
+ let settingsBtn;
7
+ let closeBtn;
8
+ let saveBtn;
9
+ let providerSelect;
10
+ let apiKeyInput;
11
+ let telegramTokenInput;
12
+ let slackBotTokenInput;
13
+ let slackAppTokenInput;
14
+ let keyStatus;
15
+
16
+ export function initSettings() {
17
+ dialog = document.getElementById("settings-dialog");
18
+ settingsBtn = document.getElementById("open-settings-btn");
19
+ closeBtn = document.getElementById("close-settings-btn");
20
+ saveBtn = document.getElementById("save-settings-btn");
21
+
22
+ providerSelect = document.getElementById("provider-select");
23
+ apiKeyInput = document.getElementById("api-key-input");
24
+ telegramTokenInput = document.getElementById("telegram-token-input");
25
+ slackBotTokenInput = document.getElementById("slack-bot-token-input");
26
+ slackAppTokenInput = document.getElementById("slack-app-token-input");
27
+ keyStatus = document.getElementById("key-status");
28
+
29
+ if (!dialog) return;
30
+
31
+ // Open/Close dialog
32
+ if (settingsBtn) {
33
+ settingsBtn.addEventListener("click", () => {
34
+ populateForm();
35
+ dialog.showModal();
36
+ });
37
+ }
38
+
39
+ if (closeBtn) {
40
+ closeBtn.addEventListener("click", () => {
41
+ dialog.close();
42
+ keyStatus.textContent = ""; // clear status on close
43
+ });
44
+ }
45
+
46
+ // Save Settings
47
+ if (saveBtn) {
48
+ saveBtn.addEventListener("click", handleSave);
49
+ }
50
+
51
+ // Placeholder logic
52
+ if (providerSelect) {
53
+ providerSelect.addEventListener("change", updatePlaceholder);
54
+ }
55
+ }
56
+
57
+ function updatePlaceholder() {
58
+ const p = providerSelect.value;
59
+ if (p === "openai") apiKeyInput.placeholder = "sk-proj-...";
60
+ else if (p === "anthropic") apiKeyInput.placeholder = "sk-ant-api03-...";
61
+ else apiKeyInput.placeholder = "sk-...";
62
+ }
63
+
64
+ function populateForm() {
65
+ const config = state.config;
66
+ if (!config) return;
67
+
68
+ if (config.hasApiKey) {
69
+ keyStatus.textContent = "API key configured";
70
+ keyStatus.className = "status-text success";
71
+ } else {
72
+ keyStatus.textContent = "";
73
+ }
74
+
75
+ if (config.provider) {
76
+ providerSelect.value = config.provider;
77
+ }
78
+ updatePlaceholder();
79
+
80
+ const adapters = config.adapters || {};
81
+ if (adapters.telegram && adapters.telegram.token) {
82
+ telegramTokenInput.value = adapters.telegram.token;
83
+ } else {
84
+ telegramTokenInput.value = "";
85
+ }
86
+
87
+ if (adapters.slack) {
88
+ slackBotTokenInput.value = adapters.slack.botToken || "";
89
+ slackAppTokenInput.value = adapters.slack.appToken || "";
90
+ } else {
91
+ slackBotTokenInput.value = "";
92
+ slackAppTokenInput.value = "";
93
+ }
94
+ }
95
+
96
+ async function handleSave() {
97
+ const key = apiKeyInput.value.trim();
98
+ const provider = providerSelect.value;
99
+ const telegramToken = telegramTokenInput.value.trim();
100
+ const slackBotToken = slackBotTokenInput.value.trim();
101
+ const slackAppToken = slackAppTokenInput.value.trim();
102
+
103
+ const adapters = {};
104
+ if (telegramToken) adapters.telegram = { token: telegramToken };
105
+ if (slackBotToken || slackAppToken) {
106
+ adapters.slack = {
107
+ botToken: slackBotToken || undefined,
108
+ appToken: slackAppToken || undefined
109
+ };
110
+ }
111
+
112
+ const updates = { provider, adapters };
113
+ if (key) {
114
+ updates.key = key;
115
+ }
116
+
117
+ try {
118
+ const res = await saveConfigData(updates);
119
+ keyStatus.textContent = "Settings saved";
120
+ keyStatus.className = "status-text success";
121
+ apiKeyInput.value = ""; // clear after save
122
+
123
+ // Update local config
124
+ state.config.provider = res.provider;
125
+ state.config.adapters = res.adapters;
126
+ if (key) state.config.hasApiKey = true;
127
+
128
+ } catch (err) {
129
+ keyStatus.textContent = "Save failed: " + err.message;
130
+ keyStatus.className = "status-text error";
131
+ }
132
+ }
@@ -772,3 +772,174 @@ body {
772
772
  .thinking-content p:last-child {
773
773
  margin-bottom: 0;
774
774
  }
775
+
776
+ /* ---- Settings Modal ---- */
777
+ .settings-trigger-btn {
778
+ display: flex;
779
+ align-items: center;
780
+ justify-content: center;
781
+ gap: 8px;
782
+ width: 100%;
783
+ padding: 12px;
784
+ background: var(--bg-tertiary);
785
+ border: 1px solid var(--border-color);
786
+ border-radius: var(--radius-sm);
787
+ color: var(--text-primary);
788
+ font-weight: 500;
789
+ cursor: pointer;
790
+ transition: all 0.2s ease;
791
+ }
792
+ .settings-trigger-btn:hover {
793
+ background: var(--bg-secondary);
794
+ border-color: var(--accent);
795
+ box-shadow: 0 4px 12px rgba(0,0,0,0.03);
796
+ transform: translateY(-1px);
797
+ }
798
+
799
+ .settings-modal {
800
+ margin: auto;
801
+ padding: 0;
802
+ border: 1px solid rgba(255, 255, 255, 0.1);
803
+ border-radius: 16px;
804
+ background: var(--bg-secondary);
805
+ width: 520px;
806
+ max-width: 90vw;
807
+ box-shadow: 0 20px 40px rgba(0,0,0,0.1), 0 0 0 1px var(--border-color);
808
+ font-family: inherit;
809
+ color: var(--text-primary);
810
+ opacity: 0;
811
+ transform: scale(0.95) translateY(10px);
812
+ transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
813
+ }
814
+ .settings-modal[open] {
815
+ opacity: 1;
816
+ transform: scale(1) translateY(0);
817
+ }
818
+ .settings-modal::backdrop {
819
+ background: rgba(0, 0, 0, 0.2);
820
+ backdrop-filter: blur(4px);
821
+ -webkit-backdrop-filter: blur(4px);
822
+ opacity: 0;
823
+ transition: opacity 0.3s ease;
824
+ }
825
+ .settings-modal[open]::backdrop {
826
+ opacity: 1;
827
+ }
828
+
829
+ .settings-modal-header {
830
+ display: flex;
831
+ justify-content: space-between;
832
+ align-items: center;
833
+ padding: 24px 24px 12px 24px;
834
+ background: transparent;
835
+ border-top-left-radius: 16px;
836
+ border-top-right-radius: 16px;
837
+ }
838
+ .settings-modal-header h2 { font-size: 18px; font-weight: 600; margin: 0; color: var(--text-primary); letter-spacing: -0.01em; }
839
+ .close-btn {
840
+ background: var(--bg-tertiary);
841
+ border: 1px solid var(--border-color);
842
+ border-radius: 8px;
843
+ width: 32px;
844
+ height: 32px;
845
+ display: flex;
846
+ align-items: center;
847
+ justify-content: center;
848
+ font-size: 18px;
849
+ cursor: pointer;
850
+ color: var(--text-secondary);
851
+ transition: all 0.2s ease;
852
+ }
853
+ .close-btn:hover {
854
+ background: var(--bg-primary);
855
+ color: var(--text-primary);
856
+ border-color: var(--text-secondary);
857
+ }
858
+
859
+ .settings-sections {
860
+ padding: 24px;
861
+ max-height: calc(85vh - 140px);
862
+ overflow-y: auto;
863
+ }
864
+ .settings-section {
865
+ margin-bottom: 28px;
866
+ padding-bottom: 28px;
867
+ border-bottom: 1px solid var(--border-color);
868
+ }
869
+ .settings-section:last-child {
870
+ margin-bottom: 0;
871
+ padding-bottom: 0;
872
+ border-bottom: none;
873
+ }
874
+ .section-title {
875
+ font-size: 14px;
876
+ font-weight: 600;
877
+ color: var(--text-primary);
878
+ margin-top: 0;
879
+ margin-bottom: 16px;
880
+ text-transform: uppercase;
881
+ letter-spacing: 0.5px;
882
+ opacity: 0.8;
883
+ }
884
+
885
+ .form-group { margin-bottom: 16px; }
886
+ .form-group:last-child { margin-bottom: 0; }
887
+ .form-group label { display: block; font-size: 13px; font-weight: 500; margin-bottom: 8px; color: var(--text-primary); }
888
+ .form-input {
889
+ width: 100%;
890
+ padding: 12px 14px;
891
+ background: var(--bg-tertiary);
892
+ border: 1px solid var(--border-color);
893
+ border-radius: var(--radius-sm);
894
+ font-size: 14px;
895
+ outline: none;
896
+ color: var(--text-primary);
897
+ transition: all 0.2s ease;
898
+ }
899
+ .form-input:focus {
900
+ border-color: var(--accent);
901
+ background: var(--bg-secondary);
902
+ box-shadow: 0 0 0 3px var(--accent-glow);
903
+ }
904
+
905
+ /* Specific to provider-select in Settings Modal to match form-input */
906
+ .settings-modal .provider-select-wrapper select {
907
+ padding: 12px 14px;
908
+ font-size: 14px;
909
+ transition: all 0.2s ease;
910
+ }
911
+ .settings-modal .provider-select-wrapper select:focus {
912
+ border-color: var(--accent);
913
+ background: var(--bg-secondary);
914
+ box-shadow: 0 0 0 3px var(--accent-glow);
915
+ }
916
+
917
+ .settings-modal-footer {
918
+ padding: 16px 24px 24px 24px;
919
+ background: transparent;
920
+ border-bottom-left-radius: 16px;
921
+ border-bottom-right-radius: 16px;
922
+ display: flex;
923
+ justify-content: flex-end;
924
+ align-items: center;
925
+ }
926
+ .primary-btn {
927
+ padding: 10px 20px;
928
+ background: var(--accent);
929
+ color: #fff;
930
+ border: none;
931
+ border-radius: var(--radius-sm);
932
+ font-weight: 500;
933
+ font-size: 14px;
934
+ cursor: pointer;
935
+ transition: all 0.2s ease;
936
+ }
937
+ .primary-btn:hover {
938
+ background: var(--accent-hover);
939
+ box-shadow: 0 4px 12px var(--accent-glow);
940
+ transform: translateY(-1px);
941
+ }
942
+ .primary-btn:active {
943
+ transform: translateY(0);
944
+ }
945
+
@@ -1,161 +0,0 @@
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
- }
@@ -1,63 +0,0 @@
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
- server.once("listening", () => {
32
- const address = server.address();
33
- const actualPort = typeof address === "string" ? address : address.port;
34
- const url = `http://${HOST}:${actualPort}`;
35
- console.log(`\n Skills Pack Server`);
36
- console.log(` Running at ${url}\n`);
37
-
38
- // Open the browser automatically
39
- const cmd =
40
- process.platform === "darwin"
41
- ? `open ${url}`
42
- : process.platform === "win32"
43
- ? `start ${url}`
44
- : `xdg-open ${url}`;
45
- exec(cmd, () => {});
46
- });
47
-
48
- function tryListen(port) {
49
- server.listen(port, HOST);
50
-
51
- server.once("error", (err) => {
52
- if (err.code === "EADDRINUSE") {
53
- console.log(` Port ${port} is in use, trying ${port + 1}...`);
54
- server.close();
55
- tryListen(port + 1);
56
- } else {
57
- throw err;
58
- }
59
- });
60
- }
61
-
62
- const startPort = Number(process.env.PORT) || DEFAULT_PORT;
63
- tryListen(startPort);
@@ -1,104 +0,0 @@
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 skillpack.json config.
9
- * @param {string} rootDir
10
- */
11
- function getPackConfig(rootDir) {
12
- const raw = fs.readFileSync(path.join(rootDir, "skillpack.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 skillpack.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
- }