@oro.ad/nuxt-claude-devtools 1.0.7 → 1.1.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 +149 -13
- package/dist/client/200.html +1 -1
- package/dist/client/404.html +1 -1
- package/dist/client/_nuxt/{BpkYThsl.js → BRCY8pHC.js} +1 -1
- package/dist/client/_nuxt/BVHVIm9H.js +12 -0
- package/dist/client/_nuxt/BZrcCMrf.js +1 -0
- package/dist/client/_nuxt/BbEuL4Z6.js +1 -0
- package/dist/client/_nuxt/BmjlsnUc.js +1 -0
- package/dist/client/_nuxt/D2NL8Xro.js +7 -0
- package/dist/client/_nuxt/D9qGFoJm.js +4 -0
- package/dist/client/_nuxt/DImlDIT-.js +59 -0
- package/dist/client/_nuxt/{B1H6wO_D.js → DV075BoS.js} +1 -1
- package/dist/client/_nuxt/{D-z88P1l.js → DYNukx3V.js} +1 -1
- package/dist/client/_nuxt/Dbw96V2H.js +7 -0
- package/dist/client/_nuxt/FllXIyfS.js +8 -0
- package/dist/client/_nuxt/MarkdownContent.WwTYmYZK.css +1 -0
- package/dist/client/_nuxt/TvBJGid1.js +1 -0
- package/dist/client/_nuxt/XJ4dJUK2.js +1 -0
- package/dist/client/_nuxt/builds/latest.json +1 -1
- package/dist/client/_nuxt/builds/meta/e8ae4dbb-462d-47a2-9aa2-50bed9498ab2.json +1 -0
- package/dist/client/_nuxt/e7kgpy_n.js +4 -0
- package/dist/client/_nuxt/entry.DwDQaFYc.css +1 -0
- package/dist/client/_nuxt/index.Bomb3OYy.css +1 -0
- package/dist/client/agents/index.html +1 -0
- package/dist/client/commands/index.html +1 -0
- package/dist/client/docs/index.html +1 -0
- package/dist/client/index.html +1 -1
- package/dist/client/mcp/index.html +1 -1
- package/dist/client/skills/index.html +1 -0
- package/dist/module.json +1 -1
- package/dist/runtime/server/agents-manager.d.ts +31 -0
- package/dist/runtime/server/agents-manager.js +193 -0
- package/dist/runtime/server/claude-session.d.ts +15 -0
- package/dist/runtime/server/claude-session.js +456 -5
- package/dist/runtime/server/commands-manager.d.ts +24 -0
- package/dist/runtime/server/commands-manager.js +132 -0
- package/dist/runtime/server/docs-manager.d.ts +48 -0
- package/dist/runtime/server/docs-manager.js +189 -0
- package/dist/runtime/server/history-manager.d.ts +24 -0
- package/dist/runtime/server/history-manager.js +184 -0
- package/dist/runtime/server/skills-manager.d.ts +36 -0
- package/dist/runtime/server/skills-manager.js +210 -0
- package/dist/runtime/types.d.ts +156 -0
- package/dist/runtime/types.js +0 -0
- package/package.json +16 -1
- package/dist/client/_nuxt/C2ORx7Gd.js +0 -1
- package/dist/client/_nuxt/CfGtRVGd.js +0 -4
- package/dist/client/_nuxt/DJn_CTvm.js +0 -1
- package/dist/client/_nuxt/EMyRkg8p.js +0 -1
- package/dist/client/_nuxt/PGt8fA_Y.js +0 -1
- package/dist/client/_nuxt/builds/meta/88c99fa0-10e0-4015-a61a-e0c6d7a01859.json +0 -1
- package/dist/client/_nuxt/entry.Ci1n7Rlt.css +0 -1
|
@@ -1,18 +1,44 @@
|
|
|
1
1
|
import { execSync, spawn } from "node:child_process";
|
|
2
2
|
import { createLogger } from "../logger.js";
|
|
3
|
+
import { AgentsManager } from "./agents-manager.js";
|
|
4
|
+
import { CommandsManager } from "./commands-manager.js";
|
|
5
|
+
import { DocsManager } from "./docs-manager.js";
|
|
6
|
+
import { HistoryManager } from "./history-manager.js";
|
|
7
|
+
import { SkillsManager } from "./skills-manager.js";
|
|
3
8
|
const log = createLogger("session", { timestamp: true });
|
|
4
9
|
function getErrorMessage(error) {
|
|
5
10
|
if (error instanceof Error) return error.message;
|
|
6
11
|
return String(error);
|
|
7
12
|
}
|
|
13
|
+
function generateId() {
|
|
14
|
+
return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
15
|
+
}
|
|
8
16
|
export const SOCKET_PATH = "/__claude_devtools_socket";
|
|
9
17
|
export class ClaudeSession {
|
|
10
18
|
config;
|
|
11
19
|
io = null;
|
|
12
20
|
isProcessing = false;
|
|
13
21
|
continueSession = false;
|
|
22
|
+
historyManager;
|
|
23
|
+
docsManager;
|
|
24
|
+
commandsManager;
|
|
25
|
+
agentsManager;
|
|
26
|
+
skillsManager;
|
|
27
|
+
// Claude CLI session ID (in-memory only, lost on hot-reload)
|
|
28
|
+
claudeSessionId = null;
|
|
29
|
+
// Stream parsing state
|
|
30
|
+
parseBuffer = "";
|
|
31
|
+
currentContentBlocks = [];
|
|
32
|
+
currentMessageId = "";
|
|
33
|
+
currentModel = "";
|
|
34
|
+
accumulatedText = "";
|
|
14
35
|
constructor(config) {
|
|
15
36
|
this.config = config;
|
|
37
|
+
this.historyManager = new HistoryManager(config.rootDir);
|
|
38
|
+
this.docsManager = new DocsManager(config.rootDir);
|
|
39
|
+
this.commandsManager = new CommandsManager(config.rootDir);
|
|
40
|
+
this.agentsManager = new AgentsManager(config.rootDir);
|
|
41
|
+
this.skillsManager = new SkillsManager(config.rootDir);
|
|
16
42
|
}
|
|
17
43
|
attachSocketIO(io) {
|
|
18
44
|
this.io = io;
|
|
@@ -23,6 +49,8 @@ export class ClaudeSession {
|
|
|
23
49
|
active: true,
|
|
24
50
|
processing: this.isProcessing
|
|
25
51
|
});
|
|
52
|
+
const conversation = this.historyManager.getActiveConversation();
|
|
53
|
+
socket.emit("history:loaded", conversation);
|
|
26
54
|
socket.on("message:send", (message) => {
|
|
27
55
|
log("Message received", { length: message.length, preview: message.substring(0, 100) });
|
|
28
56
|
this.sendMessage(message);
|
|
@@ -30,7 +58,37 @@ export class ClaudeSession {
|
|
|
30
58
|
socket.on("session:reset", () => {
|
|
31
59
|
log("Resetting session (new conversation)");
|
|
32
60
|
this.continueSession = false;
|
|
61
|
+
this.claudeSessionId = null;
|
|
62
|
+
const conversation2 = this.historyManager.resetSession();
|
|
33
63
|
this.io?.emit("session:status", { active: true, processing: false });
|
|
64
|
+
this.io?.emit("history:loaded", conversation2);
|
|
65
|
+
});
|
|
66
|
+
socket.on("history:load", () => {
|
|
67
|
+
const conversation2 = this.historyManager.getActiveConversation();
|
|
68
|
+
socket.emit("history:loaded", conversation2);
|
|
69
|
+
});
|
|
70
|
+
socket.on("history:list", () => {
|
|
71
|
+
const conversations = this.historyManager.getConversations();
|
|
72
|
+
socket.emit("history:list", conversations);
|
|
73
|
+
});
|
|
74
|
+
socket.on("history:switch", (id) => {
|
|
75
|
+
const conversation2 = this.historyManager.setActiveConversation(id);
|
|
76
|
+
if (conversation2) {
|
|
77
|
+
this.continueSession = false;
|
|
78
|
+
this.claudeSessionId = conversation2.claudeSessionId || null;
|
|
79
|
+
log("Switched to conversation", {
|
|
80
|
+
id: conversation2.id,
|
|
81
|
+
claudeSessionId: this.claudeSessionId,
|
|
82
|
+
messageCount: conversation2.messages.length
|
|
83
|
+
});
|
|
84
|
+
socket.emit("history:switched", conversation2);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
socket.on("history:delete", (id) => {
|
|
88
|
+
const success = this.historyManager.deleteConversation(id);
|
|
89
|
+
socket.emit("history:deleted", { id, success });
|
|
90
|
+
const conversations = this.historyManager.getConversations();
|
|
91
|
+
socket.emit("history:list", conversations);
|
|
34
92
|
});
|
|
35
93
|
socket.on("mcp:list", () => {
|
|
36
94
|
log("MCP list requested");
|
|
@@ -45,6 +103,192 @@ export class ClaudeSession {
|
|
|
45
103
|
log("MCP remove requested", data);
|
|
46
104
|
this.removeMcpServer(data, socket);
|
|
47
105
|
});
|
|
106
|
+
socket.on("docs:list", () => {
|
|
107
|
+
log("Docs list requested");
|
|
108
|
+
const files = this.docsManager.getDocFiles();
|
|
109
|
+
socket.emit("docs:list", files);
|
|
110
|
+
});
|
|
111
|
+
socket.on("docs:get", (path) => {
|
|
112
|
+
log("Docs get requested", { path });
|
|
113
|
+
const file = this.docsManager.getDocFile(path);
|
|
114
|
+
socket.emit("docs:file", file);
|
|
115
|
+
});
|
|
116
|
+
socket.on("docs:save", (data) => {
|
|
117
|
+
log("Docs save requested", { path: data.path });
|
|
118
|
+
try {
|
|
119
|
+
const file = this.docsManager.saveDocFile(data.path, data.content);
|
|
120
|
+
socket.emit("docs:saved", { success: true, file });
|
|
121
|
+
const files = this.docsManager.getDocFiles();
|
|
122
|
+
socket.emit("docs:list", files);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
socket.emit("docs:saved", { success: false, error: String(error) });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
socket.on("docs:delete", (path) => {
|
|
128
|
+
log("Docs delete requested", { path });
|
|
129
|
+
const success = this.docsManager.deleteDocFile(path);
|
|
130
|
+
socket.emit("docs:deleted", { path, success });
|
|
131
|
+
const files = this.docsManager.getDocFiles();
|
|
132
|
+
socket.emit("docs:list", files);
|
|
133
|
+
});
|
|
134
|
+
socket.on("claudemd:get", () => {
|
|
135
|
+
log("CLAUDE.md get requested");
|
|
136
|
+
const data = this.docsManager.getClaudeMd();
|
|
137
|
+
socket.emit("claudemd:data", data);
|
|
138
|
+
});
|
|
139
|
+
socket.on("claudemd:save", (content) => {
|
|
140
|
+
log("CLAUDE.md save requested");
|
|
141
|
+
try {
|
|
142
|
+
const data = this.docsManager.saveClaudeMd(content);
|
|
143
|
+
socket.emit("claudemd:saved", { success: true, ...data });
|
|
144
|
+
} catch (error) {
|
|
145
|
+
socket.emit("claudemd:saved", { success: false, error: String(error) });
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
socket.on("llms:list", () => {
|
|
149
|
+
log("LLMS list requested");
|
|
150
|
+
const sources = this.docsManager.getLlmsSources();
|
|
151
|
+
socket.emit("llms:list", sources);
|
|
152
|
+
});
|
|
153
|
+
socket.on("llms:add", (data) => {
|
|
154
|
+
log("LLMS add requested", { url: data.url });
|
|
155
|
+
try {
|
|
156
|
+
const source = this.docsManager.addLlmsSource(data.url, data.title, data.description);
|
|
157
|
+
socket.emit("llms:added", { success: true, source });
|
|
158
|
+
const sources = this.docsManager.getLlmsSources();
|
|
159
|
+
socket.emit("llms:list", sources);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
socket.emit("llms:added", { success: false, error: String(error) });
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
socket.on("llms:remove", (url) => {
|
|
165
|
+
log("LLMS remove requested", { url });
|
|
166
|
+
const success = this.docsManager.removeLlmsSource(url);
|
|
167
|
+
socket.emit("llms:removed", { url, success });
|
|
168
|
+
const sources = this.docsManager.getLlmsSources();
|
|
169
|
+
socket.emit("llms:list", sources);
|
|
170
|
+
});
|
|
171
|
+
socket.on("llms:update", (data) => {
|
|
172
|
+
log("LLMS update requested", { url: data.url });
|
|
173
|
+
const source = this.docsManager.updateLlmsSource(data.url, {
|
|
174
|
+
title: data.title,
|
|
175
|
+
description: data.description
|
|
176
|
+
});
|
|
177
|
+
socket.emit("llms:updated", { success: !!source, source });
|
|
178
|
+
});
|
|
179
|
+
socket.on("commands:list", () => {
|
|
180
|
+
log("Commands list requested");
|
|
181
|
+
const commands = this.commandsManager.getCommands();
|
|
182
|
+
socket.emit("commands:list", commands);
|
|
183
|
+
});
|
|
184
|
+
socket.on("commands:get", (name) => {
|
|
185
|
+
log("Command get requested", { name });
|
|
186
|
+
const command = this.commandsManager.getCommand(name);
|
|
187
|
+
socket.emit("commands:data", command);
|
|
188
|
+
});
|
|
189
|
+
socket.on("commands:save", (data) => {
|
|
190
|
+
log("Command save requested", { name: data.name });
|
|
191
|
+
try {
|
|
192
|
+
const command = this.commandsManager.saveCommand(data.name, data.content, {
|
|
193
|
+
description: data.description,
|
|
194
|
+
allowedTools: data.allowedTools,
|
|
195
|
+
disableModelInvocation: data.disableModelInvocation
|
|
196
|
+
});
|
|
197
|
+
socket.emit("commands:saved", { success: true, command });
|
|
198
|
+
const commands = this.commandsManager.getCommands();
|
|
199
|
+
socket.emit("commands:list", commands);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
socket.emit("commands:saved", { success: false, error: String(error) });
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
socket.on("commands:delete", (name) => {
|
|
205
|
+
log("Command delete requested", { name });
|
|
206
|
+
const success = this.commandsManager.deleteCommand(name);
|
|
207
|
+
socket.emit("commands:deleted", { name, success });
|
|
208
|
+
const commands = this.commandsManager.getCommands();
|
|
209
|
+
socket.emit("commands:list", commands);
|
|
210
|
+
});
|
|
211
|
+
socket.on("agents:list", () => {
|
|
212
|
+
log("Agents list requested");
|
|
213
|
+
const agents = this.agentsManager.getAgents();
|
|
214
|
+
socket.emit("agents:list", agents);
|
|
215
|
+
});
|
|
216
|
+
socket.on("agents:get", (name) => {
|
|
217
|
+
log("Agent get requested", { name });
|
|
218
|
+
const agent = this.agentsManager.getAgent(name);
|
|
219
|
+
socket.emit("agents:data", agent);
|
|
220
|
+
});
|
|
221
|
+
socket.on("agents:save", (data) => {
|
|
222
|
+
log("Agent save requested", { name: data.name });
|
|
223
|
+
try {
|
|
224
|
+
const agent = this.agentsManager.saveAgent({
|
|
225
|
+
name: data.name,
|
|
226
|
+
description: data.description,
|
|
227
|
+
prompt: data.prompt,
|
|
228
|
+
model: data.model,
|
|
229
|
+
tools: data.tools,
|
|
230
|
+
disallowedTools: data.disallowedTools,
|
|
231
|
+
permissionMode: data.permissionMode,
|
|
232
|
+
skills: data.skills
|
|
233
|
+
});
|
|
234
|
+
socket.emit("agents:saved", { success: true, agent });
|
|
235
|
+
const agents = this.agentsManager.getAgents();
|
|
236
|
+
socket.emit("agents:list", agents);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
socket.emit("agents:saved", { success: false, error: String(error) });
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
socket.on("agents:delete", (name) => {
|
|
242
|
+
log("Agent delete requested", { name });
|
|
243
|
+
const success = this.agentsManager.deleteAgent(name);
|
|
244
|
+
socket.emit("agents:deleted", { name, success });
|
|
245
|
+
const agents = this.agentsManager.getAgents();
|
|
246
|
+
socket.emit("agents:list", agents);
|
|
247
|
+
});
|
|
248
|
+
socket.on("skills:list", () => {
|
|
249
|
+
log("Skills list requested");
|
|
250
|
+
const skills = this.skillsManager.getSkills();
|
|
251
|
+
socket.emit("skills:list", skills);
|
|
252
|
+
});
|
|
253
|
+
socket.on("skills:get", (name) => {
|
|
254
|
+
log("Skill get requested", { name });
|
|
255
|
+
const skill = this.skillsManager.getSkill(name);
|
|
256
|
+
socket.emit("skills:get", skill);
|
|
257
|
+
});
|
|
258
|
+
socket.on("skills:save", (data) => {
|
|
259
|
+
log("Skill save requested", { name: data.name });
|
|
260
|
+
try {
|
|
261
|
+
const skill = this.skillsManager.saveSkill({
|
|
262
|
+
name: data.name,
|
|
263
|
+
description: data.description,
|
|
264
|
+
content: data.content,
|
|
265
|
+
argumentHint: data.argumentHint,
|
|
266
|
+
disableModelInvocation: data.disableModelInvocation,
|
|
267
|
+
userInvocable: data.userInvocable,
|
|
268
|
+
allowedTools: data.allowedTools,
|
|
269
|
+
model: data.model,
|
|
270
|
+
context: data.context,
|
|
271
|
+
agent: data.agent
|
|
272
|
+
});
|
|
273
|
+
socket.emit("skills:saved", { success: true, skill });
|
|
274
|
+
const skills = this.skillsManager.getSkills();
|
|
275
|
+
socket.emit("skills:list", skills);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
socket.emit("skills:saved", { success: false, error: getErrorMessage(error) });
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
socket.on("skills:names", () => {
|
|
281
|
+
log("Skill names requested");
|
|
282
|
+
const names = this.skillsManager.getSkillNames();
|
|
283
|
+
socket.emit("skills:names", names);
|
|
284
|
+
});
|
|
285
|
+
socket.on("skills:delete", (name) => {
|
|
286
|
+
log("Skill delete requested", { name });
|
|
287
|
+
const success = this.skillsManager.deleteSkill(name);
|
|
288
|
+
socket.emit("skills:deleted", { name, success });
|
|
289
|
+
const skills = this.skillsManager.getSkills();
|
|
290
|
+
socket.emit("skills:list", skills);
|
|
291
|
+
});
|
|
48
292
|
socket.on("disconnect", () => {
|
|
49
293
|
log("Client disconnected", { socketId: socket.id });
|
|
50
294
|
});
|
|
@@ -53,21 +297,197 @@ export class ClaudeSession {
|
|
|
53
297
|
destroy() {
|
|
54
298
|
this.io?.close();
|
|
55
299
|
}
|
|
300
|
+
resetStreamState() {
|
|
301
|
+
this.parseBuffer = "";
|
|
302
|
+
this.currentContentBlocks = [];
|
|
303
|
+
this.currentMessageId = generateId();
|
|
304
|
+
this.currentModel = "";
|
|
305
|
+
this.accumulatedText = "";
|
|
306
|
+
}
|
|
307
|
+
buildSystemPrompt() {
|
|
308
|
+
const sections = [];
|
|
309
|
+
const llmsSources = this.docsManager.getLlmsSources();
|
|
310
|
+
if (llmsSources.length > 0) {
|
|
311
|
+
sections.push("=== EXTERNAL DOCUMENTATION SOURCES ===");
|
|
312
|
+
sections.push("The following llms.txt sources are configured for this project.");
|
|
313
|
+
sections.push("You can fetch these URLs to get documentation context when needed:");
|
|
314
|
+
sections.push("");
|
|
315
|
+
for (const source of llmsSources) {
|
|
316
|
+
const title = source.title || source.domain;
|
|
317
|
+
const desc = source.description ? ` - ${source.description}` : "";
|
|
318
|
+
sections.push(`- ${title}${desc}`);
|
|
319
|
+
sections.push(` URL: ${source.url}`);
|
|
320
|
+
}
|
|
321
|
+
sections.push("");
|
|
322
|
+
}
|
|
323
|
+
if (this.historyManager.hasHistoryForRecovery()) {
|
|
324
|
+
const historyPrompt = this.historyManager.formatHistoryForSystemPrompt();
|
|
325
|
+
if (historyPrompt) {
|
|
326
|
+
sections.push(historyPrompt);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (sections.length === 0) {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
return sections.join("\n");
|
|
333
|
+
}
|
|
334
|
+
parseStreamChunk(data) {
|
|
335
|
+
this.parseBuffer += data;
|
|
336
|
+
const events = [];
|
|
337
|
+
const lines = this.parseBuffer.split("\n");
|
|
338
|
+
this.parseBuffer = lines.pop() || "";
|
|
339
|
+
for (const line of lines) {
|
|
340
|
+
if (line.trim()) {
|
|
341
|
+
try {
|
|
342
|
+
const event = JSON.parse(line);
|
|
343
|
+
events.push(event);
|
|
344
|
+
} catch (e) {
|
|
345
|
+
log("Failed to parse stream event", { line: line.substring(0, 100), error: e });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return events;
|
|
350
|
+
}
|
|
351
|
+
processStreamEvent(event) {
|
|
352
|
+
switch (event.type) {
|
|
353
|
+
case "system":
|
|
354
|
+
log("System event", { subtype: event.subtype });
|
|
355
|
+
break;
|
|
356
|
+
case "assistant": {
|
|
357
|
+
const assistantEvent = event;
|
|
358
|
+
this.currentMessageId = assistantEvent.message.id;
|
|
359
|
+
this.currentModel = assistantEvent.message.model;
|
|
360
|
+
for (const block of assistantEvent.message.content) {
|
|
361
|
+
if (block.type === "text" && block.text) {
|
|
362
|
+
const textBlock = {
|
|
363
|
+
type: "text",
|
|
364
|
+
text: block.text
|
|
365
|
+
};
|
|
366
|
+
this.currentContentBlocks.push(textBlock);
|
|
367
|
+
this.accumulatedText += block.text;
|
|
368
|
+
this.io?.emit("output:chunk", block.text);
|
|
369
|
+
this.io?.emit("stream:text_delta", {
|
|
370
|
+
index: this.currentContentBlocks.length - 1,
|
|
371
|
+
text: block.text
|
|
372
|
+
});
|
|
373
|
+
} else if (block.type === "tool_use" && block.id && block.name) {
|
|
374
|
+
const toolBlock = {
|
|
375
|
+
type: "tool_use",
|
|
376
|
+
id: block.id,
|
|
377
|
+
name: block.name,
|
|
378
|
+
input: block.input || {}
|
|
379
|
+
};
|
|
380
|
+
this.currentContentBlocks.push(toolBlock);
|
|
381
|
+
this.io?.emit("stream:tool_use", {
|
|
382
|
+
id: block.id,
|
|
383
|
+
name: block.name,
|
|
384
|
+
input: block.input || {}
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
case "tool_use": {
|
|
391
|
+
const toolEvent = event;
|
|
392
|
+
const toolBlock = {
|
|
393
|
+
type: "tool_use",
|
|
394
|
+
id: toolEvent.tool_use_id,
|
|
395
|
+
name: toolEvent.name,
|
|
396
|
+
input: toolEvent.input
|
|
397
|
+
};
|
|
398
|
+
this.currentContentBlocks.push(toolBlock);
|
|
399
|
+
this.io?.emit("stream:tool_use", {
|
|
400
|
+
id: toolEvent.tool_use_id,
|
|
401
|
+
name: toolEvent.name,
|
|
402
|
+
input: toolEvent.input
|
|
403
|
+
});
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
case "tool_result": {
|
|
407
|
+
const resultEvent = event;
|
|
408
|
+
this.currentContentBlocks.push({
|
|
409
|
+
type: "tool_result",
|
|
410
|
+
tool_use_id: resultEvent.tool_use_id,
|
|
411
|
+
content: resultEvent.content,
|
|
412
|
+
is_error: resultEvent.is_error
|
|
413
|
+
});
|
|
414
|
+
this.io?.emit("stream:tool_result", {
|
|
415
|
+
tool_use_id: resultEvent.tool_use_id,
|
|
416
|
+
name: resultEvent.name,
|
|
417
|
+
content: resultEvent.content,
|
|
418
|
+
is_error: resultEvent.is_error
|
|
419
|
+
});
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
case "result": {
|
|
423
|
+
const resultEvent = event;
|
|
424
|
+
log("Result event", {
|
|
425
|
+
subtype: resultEvent.subtype,
|
|
426
|
+
cost: resultEvent.cost_usd,
|
|
427
|
+
duration: resultEvent.duration_ms,
|
|
428
|
+
session_id: resultEvent.session_id
|
|
429
|
+
});
|
|
430
|
+
if (resultEvent.session_id) {
|
|
431
|
+
this.claudeSessionId = resultEvent.session_id;
|
|
432
|
+
this.historyManager.setClaudeSessionId(resultEvent.session_id);
|
|
433
|
+
log("Saved Claude session ID", { sessionId: resultEvent.session_id });
|
|
434
|
+
}
|
|
435
|
+
this.io?.emit("stream:result", {
|
|
436
|
+
subtype: resultEvent.subtype,
|
|
437
|
+
result: resultEvent.result,
|
|
438
|
+
error: resultEvent.error,
|
|
439
|
+
session_id: resultEvent.session_id,
|
|
440
|
+
cost_usd: resultEvent.cost_usd,
|
|
441
|
+
duration_ms: resultEvent.duration_ms,
|
|
442
|
+
is_error: resultEvent.is_error
|
|
443
|
+
});
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
default:
|
|
447
|
+
log("Unknown event type", { type: event.type });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
56
450
|
sendMessage(message) {
|
|
57
451
|
if (this.isProcessing) {
|
|
58
452
|
log("Already processing, ignoring message");
|
|
59
453
|
return;
|
|
60
454
|
}
|
|
61
455
|
this.isProcessing = true;
|
|
456
|
+
this.resetStreamState();
|
|
62
457
|
this.io?.emit("session:status", { active: true, processing: true });
|
|
458
|
+
const userMessage = {
|
|
459
|
+
id: generateId(),
|
|
460
|
+
role: "user",
|
|
461
|
+
content: message,
|
|
462
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
463
|
+
};
|
|
464
|
+
this.historyManager.addMessage(userMessage);
|
|
465
|
+
this.io?.emit("stream:message_start", {
|
|
466
|
+
id: this.currentMessageId
|
|
467
|
+
});
|
|
63
468
|
const args = [
|
|
64
469
|
...this.config.args,
|
|
65
470
|
"-p",
|
|
66
471
|
message,
|
|
472
|
+
"--output-format",
|
|
473
|
+
"stream-json",
|
|
474
|
+
"--verbose",
|
|
67
475
|
"--dangerously-skip-permissions"
|
|
68
476
|
];
|
|
69
|
-
|
|
70
|
-
|
|
477
|
+
const storedSessionId = this.claudeSessionId || this.historyManager.getClaudeSessionId();
|
|
478
|
+
if (storedSessionId) {
|
|
479
|
+
args.push("--resume", storedSessionId);
|
|
480
|
+
log("Resuming Claude session", { sessionId: storedSessionId });
|
|
481
|
+
} else {
|
|
482
|
+
const systemPrompt = this.buildSystemPrompt();
|
|
483
|
+
if (systemPrompt) {
|
|
484
|
+
args.push("--system-prompt", systemPrompt);
|
|
485
|
+
log("Using system prompt with project context", {
|
|
486
|
+
promptLength: systemPrompt.length
|
|
487
|
+
});
|
|
488
|
+
} else if (this.continueSession) {
|
|
489
|
+
args.push("--continue");
|
|
490
|
+
}
|
|
71
491
|
}
|
|
72
492
|
log("Spawning Claude process", { command: this.config.command, args, cwd: this.config.rootDir });
|
|
73
493
|
const child = spawn(this.config.command, args, {
|
|
@@ -83,7 +503,10 @@ export class ClaudeSession {
|
|
|
83
503
|
child.stdout?.on("data", (data) => {
|
|
84
504
|
const chunk = data.toString();
|
|
85
505
|
log("stdout chunk", { length: chunk.length });
|
|
86
|
-
this.
|
|
506
|
+
const events = this.parseStreamChunk(chunk);
|
|
507
|
+
for (const event of events) {
|
|
508
|
+
this.processStreamEvent(event);
|
|
509
|
+
}
|
|
87
510
|
});
|
|
88
511
|
child.stderr?.on("data", (data) => {
|
|
89
512
|
const chunk = data.toString();
|
|
@@ -95,18 +518,46 @@ export class ClaudeSession {
|
|
|
95
518
|
this.isProcessing = false;
|
|
96
519
|
this.io?.emit("session:status", { active: true, processing: false });
|
|
97
520
|
});
|
|
521
|
+
child.stdin?.on("error", (error) => {
|
|
522
|
+
log("stdin error (process may have exited)", { error: error.message });
|
|
523
|
+
});
|
|
98
524
|
child.on("close", (code) => {
|
|
99
525
|
log("Process closed", { exitCode: code });
|
|
100
526
|
this.isProcessing = false;
|
|
101
527
|
if (code === 0) {
|
|
102
528
|
this.continueSession = true;
|
|
529
|
+
const assistantMessage = {
|
|
530
|
+
id: this.currentMessageId,
|
|
531
|
+
role: "assistant",
|
|
532
|
+
content: this.accumulatedText,
|
|
533
|
+
contentBlocks: this.currentContentBlocks.length > 0 ? [...this.currentContentBlocks] : void 0,
|
|
534
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
535
|
+
model: this.currentModel
|
|
536
|
+
};
|
|
537
|
+
this.historyManager.addMessage(assistantMessage);
|
|
538
|
+
this.io?.emit("stream:message_complete", {
|
|
539
|
+
id: this.currentMessageId,
|
|
540
|
+
model: this.currentModel,
|
|
541
|
+
content: this.accumulatedText,
|
|
542
|
+
contentBlocks: this.currentContentBlocks
|
|
543
|
+
});
|
|
103
544
|
this.io?.emit("output:complete");
|
|
104
545
|
} else {
|
|
105
|
-
|
|
546
|
+
const sessionIdWasUsed = this.claudeSessionId || this.historyManager.getClaudeSessionId();
|
|
547
|
+
if (sessionIdWasUsed) {
|
|
548
|
+
log("Clearing expired Claude session ID", { sessionId: sessionIdWasUsed });
|
|
549
|
+
this.claudeSessionId = null;
|
|
550
|
+
this.historyManager.setClaudeSessionId("");
|
|
551
|
+
}
|
|
552
|
+
this.io?.emit("session:error", `Process exited with code ${code}. Session may have expired - try sending the message again.`);
|
|
106
553
|
}
|
|
107
554
|
this.io?.emit("session:status", { active: true, processing: false });
|
|
108
555
|
});
|
|
109
|
-
|
|
556
|
+
try {
|
|
557
|
+
child.stdin?.end();
|
|
558
|
+
} catch (e) {
|
|
559
|
+
log("Error closing stdin", { error: e });
|
|
560
|
+
}
|
|
110
561
|
}
|
|
111
562
|
getMcpServers() {
|
|
112
563
|
const servers = [];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface SlashCommand {
|
|
2
|
+
name: string;
|
|
3
|
+
path: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
allowedTools?: string[];
|
|
6
|
+
disableModelInvocation?: boolean;
|
|
7
|
+
content: string;
|
|
8
|
+
rawContent: string;
|
|
9
|
+
updatedAt: string;
|
|
10
|
+
}
|
|
11
|
+
export declare class CommandsManager {
|
|
12
|
+
private commandsDir;
|
|
13
|
+
constructor(projectPath: string);
|
|
14
|
+
private parseFrontmatter;
|
|
15
|
+
private buildFrontmatter;
|
|
16
|
+
getCommands(): SlashCommand[];
|
|
17
|
+
getCommand(name: string): SlashCommand | null;
|
|
18
|
+
saveCommand(name: string, content: string, options?: {
|
|
19
|
+
description?: string;
|
|
20
|
+
allowedTools?: string[];
|
|
21
|
+
disableModelInvocation?: boolean;
|
|
22
|
+
}): SlashCommand;
|
|
23
|
+
deleteCommand(name: string): boolean;
|
|
24
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { createLogger } from "../logger.js";
|
|
4
|
+
const log = createLogger("commands", { timestamp: true });
|
|
5
|
+
export class CommandsManager {
|
|
6
|
+
commandsDir;
|
|
7
|
+
constructor(projectPath) {
|
|
8
|
+
this.commandsDir = join(projectPath, ".claude", "commands");
|
|
9
|
+
if (!existsSync(this.commandsDir)) {
|
|
10
|
+
mkdirSync(this.commandsDir, { recursive: true });
|
|
11
|
+
log("Created commands directory", { path: this.commandsDir });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
// Parse frontmatter from markdown content
|
|
15
|
+
parseFrontmatter(content) {
|
|
16
|
+
const frontmatterRegex = /^---\n((?:[^\n]*\n)*?)---\n([\s\S]*)$/;
|
|
17
|
+
const match = content.match(frontmatterRegex);
|
|
18
|
+
if (!match) {
|
|
19
|
+
return { frontmatter: {}, body: content };
|
|
20
|
+
}
|
|
21
|
+
const [, yaml, body] = match;
|
|
22
|
+
const frontmatter = {};
|
|
23
|
+
for (const line of yaml.split("\n")) {
|
|
24
|
+
const colonIndex = line.indexOf(":");
|
|
25
|
+
if (colonIndex === -1) continue;
|
|
26
|
+
const key = line.slice(0, colonIndex).trim();
|
|
27
|
+
const value = line.slice(colonIndex + 1).trim();
|
|
28
|
+
if (key === "description") {
|
|
29
|
+
frontmatter.description = value;
|
|
30
|
+
} else if (key === "allowed-tools") {
|
|
31
|
+
frontmatter["allowed-tools"] = value;
|
|
32
|
+
} else if (key === "disable-model-invocation") {
|
|
33
|
+
frontmatter["disable-model-invocation"] = value === "true";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { frontmatter, body: body.trim() };
|
|
37
|
+
}
|
|
38
|
+
// Build frontmatter string
|
|
39
|
+
buildFrontmatter(command) {
|
|
40
|
+
const lines = ["---"];
|
|
41
|
+
if (command.description) {
|
|
42
|
+
lines.push(`description: ${command.description}`);
|
|
43
|
+
}
|
|
44
|
+
if (command.allowedTools && command.allowedTools.length > 0) {
|
|
45
|
+
lines.push(`allowed-tools: ${command.allowedTools.join(", ")}`);
|
|
46
|
+
}
|
|
47
|
+
if (command.disableModelInvocation !== void 0) {
|
|
48
|
+
lines.push(`disable-model-invocation: ${command.disableModelInvocation}`);
|
|
49
|
+
}
|
|
50
|
+
lines.push("---");
|
|
51
|
+
return lines.join("\n");
|
|
52
|
+
}
|
|
53
|
+
// Get all slash commands
|
|
54
|
+
getCommands() {
|
|
55
|
+
const commands = [];
|
|
56
|
+
if (!existsSync(this.commandsDir)) return commands;
|
|
57
|
+
const entries = readdirSync(this.commandsDir);
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
if (!entry.endsWith(".md")) continue;
|
|
60
|
+
const fullPath = join(this.commandsDir, entry);
|
|
61
|
+
const stat = statSync(fullPath);
|
|
62
|
+
if (stat.isDirectory()) continue;
|
|
63
|
+
const rawContent = readFileSync(fullPath, "utf-8");
|
|
64
|
+
const { frontmatter, body } = this.parseFrontmatter(rawContent);
|
|
65
|
+
commands.push({
|
|
66
|
+
name: basename(entry, ".md"),
|
|
67
|
+
path: entry,
|
|
68
|
+
description: frontmatter.description,
|
|
69
|
+
allowedTools: frontmatter["allowed-tools"] ? frontmatter["allowed-tools"].split(",").map((s) => s.trim()) : void 0,
|
|
70
|
+
disableModelInvocation: frontmatter["disable-model-invocation"],
|
|
71
|
+
content: body,
|
|
72
|
+
rawContent,
|
|
73
|
+
updatedAt: stat.mtime.toISOString()
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return commands.sort((a, b) => a.name.localeCompare(b.name));
|
|
77
|
+
}
|
|
78
|
+
// Get single command
|
|
79
|
+
getCommand(name) {
|
|
80
|
+
const fileName = name.endsWith(".md") ? name : `${name}.md`;
|
|
81
|
+
const fullPath = join(this.commandsDir, fileName);
|
|
82
|
+
if (!existsSync(fullPath)) return null;
|
|
83
|
+
const stat = statSync(fullPath);
|
|
84
|
+
const rawContent = readFileSync(fullPath, "utf-8");
|
|
85
|
+
const { frontmatter, body } = this.parseFrontmatter(rawContent);
|
|
86
|
+
return {
|
|
87
|
+
name: basename(fileName, ".md"),
|
|
88
|
+
path: fileName,
|
|
89
|
+
description: frontmatter.description,
|
|
90
|
+
allowedTools: frontmatter["allowed-tools"] ? frontmatter["allowed-tools"].split(",").map((s) => s.trim()) : void 0,
|
|
91
|
+
disableModelInvocation: frontmatter["disable-model-invocation"],
|
|
92
|
+
content: body,
|
|
93
|
+
rawContent,
|
|
94
|
+
updatedAt: stat.mtime.toISOString()
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
// Create or update command
|
|
98
|
+
saveCommand(name, content, options) {
|
|
99
|
+
const safeName = name.replace(/[^\w-]/g, "-").toLowerCase();
|
|
100
|
+
const fileName = `${safeName}.md`;
|
|
101
|
+
const fullPath = join(this.commandsDir, fileName);
|
|
102
|
+
const frontmatter = this.buildFrontmatter({
|
|
103
|
+
description: options?.description,
|
|
104
|
+
allowedTools: options?.allowedTools,
|
|
105
|
+
disableModelInvocation: options?.disableModelInvocation
|
|
106
|
+
});
|
|
107
|
+
const rawContent = `${frontmatter}
|
|
108
|
+
|
|
109
|
+
${content}`;
|
|
110
|
+
writeFileSync(fullPath, rawContent, "utf-8");
|
|
111
|
+
log("Saved command", { name: safeName });
|
|
112
|
+
return {
|
|
113
|
+
name: safeName,
|
|
114
|
+
path: fileName,
|
|
115
|
+
description: options?.description,
|
|
116
|
+
allowedTools: options?.allowedTools,
|
|
117
|
+
disableModelInvocation: options?.disableModelInvocation,
|
|
118
|
+
content,
|
|
119
|
+
rawContent,
|
|
120
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
// Delete command
|
|
124
|
+
deleteCommand(name) {
|
|
125
|
+
const fileName = name.endsWith(".md") ? name : `${name}.md`;
|
|
126
|
+
const fullPath = join(this.commandsDir, fileName);
|
|
127
|
+
if (!existsSync(fullPath)) return false;
|
|
128
|
+
unlinkSync(fullPath);
|
|
129
|
+
log("Deleted command", { name });
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
}
|