@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.
Files changed (52) hide show
  1. package/README.md +149 -13
  2. package/dist/client/200.html +1 -1
  3. package/dist/client/404.html +1 -1
  4. package/dist/client/_nuxt/{BpkYThsl.js → BRCY8pHC.js} +1 -1
  5. package/dist/client/_nuxt/BVHVIm9H.js +12 -0
  6. package/dist/client/_nuxt/BZrcCMrf.js +1 -0
  7. package/dist/client/_nuxt/BbEuL4Z6.js +1 -0
  8. package/dist/client/_nuxt/BmjlsnUc.js +1 -0
  9. package/dist/client/_nuxt/D2NL8Xro.js +7 -0
  10. package/dist/client/_nuxt/D9qGFoJm.js +4 -0
  11. package/dist/client/_nuxt/DImlDIT-.js +59 -0
  12. package/dist/client/_nuxt/{B1H6wO_D.js → DV075BoS.js} +1 -1
  13. package/dist/client/_nuxt/{D-z88P1l.js → DYNukx3V.js} +1 -1
  14. package/dist/client/_nuxt/Dbw96V2H.js +7 -0
  15. package/dist/client/_nuxt/FllXIyfS.js +8 -0
  16. package/dist/client/_nuxt/MarkdownContent.WwTYmYZK.css +1 -0
  17. package/dist/client/_nuxt/TvBJGid1.js +1 -0
  18. package/dist/client/_nuxt/XJ4dJUK2.js +1 -0
  19. package/dist/client/_nuxt/builds/latest.json +1 -1
  20. package/dist/client/_nuxt/builds/meta/e8ae4dbb-462d-47a2-9aa2-50bed9498ab2.json +1 -0
  21. package/dist/client/_nuxt/e7kgpy_n.js +4 -0
  22. package/dist/client/_nuxt/entry.DwDQaFYc.css +1 -0
  23. package/dist/client/_nuxt/index.Bomb3OYy.css +1 -0
  24. package/dist/client/agents/index.html +1 -0
  25. package/dist/client/commands/index.html +1 -0
  26. package/dist/client/docs/index.html +1 -0
  27. package/dist/client/index.html +1 -1
  28. package/dist/client/mcp/index.html +1 -1
  29. package/dist/client/skills/index.html +1 -0
  30. package/dist/module.json +1 -1
  31. package/dist/runtime/server/agents-manager.d.ts +31 -0
  32. package/dist/runtime/server/agents-manager.js +193 -0
  33. package/dist/runtime/server/claude-session.d.ts +15 -0
  34. package/dist/runtime/server/claude-session.js +456 -5
  35. package/dist/runtime/server/commands-manager.d.ts +24 -0
  36. package/dist/runtime/server/commands-manager.js +132 -0
  37. package/dist/runtime/server/docs-manager.d.ts +48 -0
  38. package/dist/runtime/server/docs-manager.js +189 -0
  39. package/dist/runtime/server/history-manager.d.ts +24 -0
  40. package/dist/runtime/server/history-manager.js +184 -0
  41. package/dist/runtime/server/skills-manager.d.ts +36 -0
  42. package/dist/runtime/server/skills-manager.js +210 -0
  43. package/dist/runtime/types.d.ts +156 -0
  44. package/dist/runtime/types.js +0 -0
  45. package/package.json +16 -1
  46. package/dist/client/_nuxt/C2ORx7Gd.js +0 -1
  47. package/dist/client/_nuxt/CfGtRVGd.js +0 -4
  48. package/dist/client/_nuxt/DJn_CTvm.js +0 -1
  49. package/dist/client/_nuxt/EMyRkg8p.js +0 -1
  50. package/dist/client/_nuxt/PGt8fA_Y.js +0 -1
  51. package/dist/client/_nuxt/builds/meta/88c99fa0-10e0-4015-a61a-e0c6d7a01859.json +0 -1
  52. 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
- if (this.continueSession) {
70
- args.push("--continue");
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.io?.emit("output:chunk", chunk);
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
- this.io?.emit("session:error", `Process exited with code ${code}`);
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
- child.stdin?.end();
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
+ }