agentstudio 0.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 (115) hide show
  1. package/.env +15 -0
  2. package/README.md +85 -0
  3. package/dist/bin/agentstudio.d.ts +3 -0
  4. package/dist/bin/agentstudio.d.ts.map +1 -0
  5. package/dist/bin/agentstudio.js +141 -0
  6. package/dist/bin/agentstudio.js.map +1 -0
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +87 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/middleware/auth.d.ts +7 -0
  12. package/dist/middleware/auth.d.ts.map +1 -0
  13. package/dist/middleware/auth.js +21 -0
  14. package/dist/middleware/auth.js.map +1 -0
  15. package/dist/routes/agents.d.ts +4 -0
  16. package/dist/routes/agents.d.ts.map +1 -0
  17. package/dist/routes/agents.js +804 -0
  18. package/dist/routes/agents.js.map +1 -0
  19. package/dist/routes/auth.d.ts +4 -0
  20. package/dist/routes/auth.d.ts.map +1 -0
  21. package/dist/routes/auth.js +60 -0
  22. package/dist/routes/auth.js.map +1 -0
  23. package/dist/routes/files.d.ts +4 -0
  24. package/dist/routes/files.d.ts.map +1 -0
  25. package/dist/routes/files.js +301 -0
  26. package/dist/routes/files.js.map +1 -0
  27. package/dist/routes/mcp.d.ts +4 -0
  28. package/dist/routes/mcp.d.ts.map +1 -0
  29. package/dist/routes/mcp.js +652 -0
  30. package/dist/routes/mcp.js.map +1 -0
  31. package/dist/routes/media.d.ts +5 -0
  32. package/dist/routes/media.d.ts.map +1 -0
  33. package/dist/routes/media.js +117 -0
  34. package/dist/routes/media.js.map +1 -0
  35. package/dist/routes/slides.d.ts +4 -0
  36. package/dist/routes/slides.d.ts.map +1 -0
  37. package/dist/routes/slides.js +146 -0
  38. package/dist/routes/slides.js.map +1 -0
  39. package/dist/services/claudeSession.d.ts +83 -0
  40. package/dist/services/claudeSession.d.ts.map +1 -0
  41. package/dist/services/claudeSession.js +255 -0
  42. package/dist/services/claudeSession.js.map +1 -0
  43. package/dist/services/messageQueue.d.ts +31 -0
  44. package/dist/services/messageQueue.d.ts.map +1 -0
  45. package/dist/services/messageQueue.js +67 -0
  46. package/dist/services/messageQueue.js.map +1 -0
  47. package/dist/services/sessionManager.d.ts +132 -0
  48. package/dist/services/sessionManager.d.ts.map +1 -0
  49. package/dist/services/sessionManager.js +439 -0
  50. package/dist/services/sessionManager.js.map +1 -0
  51. package/dist/types/claude-history.d.ts +48 -0
  52. package/dist/types/claude-history.d.ts.map +1 -0
  53. package/dist/types/claude-history.js +2 -0
  54. package/dist/types/claude-history.js.map +1 -0
  55. package/dist/types/claude-versions.d.ts +31 -0
  56. package/dist/types/claude-versions.d.ts.map +1 -0
  57. package/dist/types/claude-versions.js +2 -0
  58. package/dist/types/claude-versions.js.map +1 -0
  59. package/dist/types/commands.d.ts +32 -0
  60. package/dist/types/commands.d.ts.map +1 -0
  61. package/dist/types/commands.js +2 -0
  62. package/dist/types/commands.js.map +1 -0
  63. package/dist/types/index.d.ts +81 -0
  64. package/dist/types/index.d.ts.map +1 -0
  65. package/dist/types/index.js +150 -0
  66. package/dist/types/index.js.map +1 -0
  67. package/dist/types/subagents.d.ts +88 -0
  68. package/dist/types/subagents.d.ts.map +1 -0
  69. package/dist/types/subagents.js +2 -0
  70. package/dist/types/subagents.js.map +1 -0
  71. package/dist/utils/agentStorage.d.ts +19 -0
  72. package/dist/utils/agentStorage.d.ts.map +1 -0
  73. package/dist/utils/agentStorage.js +110 -0
  74. package/dist/utils/agentStorage.js.map +1 -0
  75. package/dist/utils/claudeVersionStorage.d.ts +33 -0
  76. package/dist/utils/claudeVersionStorage.d.ts.map +1 -0
  77. package/dist/utils/claudeVersionStorage.js +168 -0
  78. package/dist/utils/claudeVersionStorage.js.map +1 -0
  79. package/dist/utils/jwt.d.ts +15 -0
  80. package/dist/utils/jwt.d.ts.map +1 -0
  81. package/dist/utils/jwt.js +28 -0
  82. package/dist/utils/jwt.js.map +1 -0
  83. package/dist/utils/projectMetadataStorage.d.ts +21 -0
  84. package/dist/utils/projectMetadataStorage.d.ts.map +1 -0
  85. package/dist/utils/projectMetadataStorage.js +68 -0
  86. package/dist/utils/projectMetadataStorage.js.map +1 -0
  87. package/frontend/dist/index.html +86 -0
  88. package/package.json +66 -0
  89. package/src/bin/agentstudio.ts +161 -0
  90. package/src/index.ts +100 -0
  91. package/src/middleware/auth.ts +26 -0
  92. package/src/routes/agents.ts +885 -0
  93. package/src/routes/auth.ts +73 -0
  94. package/src/routes/commands.ts.bak +441 -0
  95. package/src/routes/files.ts +352 -0
  96. package/src/routes/mcp.ts +751 -0
  97. package/src/routes/media.ts +140 -0
  98. package/src/routes/projects.ts.bak +601 -0
  99. package/src/routes/sessions.ts.bak +809 -0
  100. package/src/routes/settings.ts.bak +718 -0
  101. package/src/routes/slides.ts +170 -0
  102. package/src/routes/subagents.ts.bak +364 -0
  103. package/src/services/claudeSession.ts +293 -0
  104. package/src/services/messageQueue.ts +71 -0
  105. package/src/services/sessionManager.ts +532 -0
  106. package/src/types/claude-history.ts +50 -0
  107. package/src/types/claude-versions.ts +33 -0
  108. package/src/types/commands.ts +35 -0
  109. package/src/types/index.ts +248 -0
  110. package/src/types/subagents.ts +106 -0
  111. package/src/utils/agentStorage.ts +126 -0
  112. package/src/utils/claudeVersionStorage.ts +199 -0
  113. package/src/utils/jwt.ts +36 -0
  114. package/src/utils/projectMetadataStorage.ts +86 -0
  115. package/tsconfig.json +26 -0
@@ -0,0 +1,73 @@
1
+ import express, { Request, Response, Router } from 'express';
2
+ import { generateToken, verifyToken } from '../utils/jwt.js';
3
+
4
+ const router: Router = express.Router();
5
+
6
+ const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'admin123';
7
+
8
+ /**
9
+ * POST /api/auth/login
10
+ * Authenticate with password and return JWT token
11
+ */
12
+ router.post('/login', (req: Request, res: Response) => {
13
+ const { password } = req.body;
14
+
15
+ if (!password) {
16
+ res.status(400).json({ error: 'Password is required' });
17
+ return;
18
+ }
19
+
20
+ if (password !== ADMIN_PASSWORD) {
21
+ res.status(401).json({ error: 'Invalid password' });
22
+ return;
23
+ }
24
+
25
+ // Generate JWT token
26
+ const token = generateToken();
27
+
28
+ res.json({
29
+ success: true,
30
+ token,
31
+ message: 'Login successful',
32
+ });
33
+ });
34
+
35
+ /**
36
+ * POST /api/auth/verify
37
+ * Verify if a token is valid
38
+ */
39
+ router.post('/verify', (req: Request, res: Response) => {
40
+ const { token } = req.body;
41
+
42
+ if (!token) {
43
+ res.status(400).json({ error: 'Token is required' });
44
+ return;
45
+ }
46
+
47
+ const payload = verifyToken(token);
48
+
49
+ if (!payload) {
50
+ res.status(401).json({ valid: false, error: 'Invalid or expired token' });
51
+ return;
52
+ }
53
+
54
+ res.json({
55
+ valid: true,
56
+ payload,
57
+ });
58
+ });
59
+
60
+ /**
61
+ * POST /api/auth/logout
62
+ * Logout endpoint (client-side token removal)
63
+ */
64
+ router.post('/logout', (req: Request, res: Response) => {
65
+ // With JWT, logout is primarily handled client-side by removing the token
66
+ // This endpoint is provided for consistency and future extensibility
67
+ res.json({
68
+ success: true,
69
+ message: 'Logout successful',
70
+ });
71
+ });
72
+
73
+ export default router;
@@ -0,0 +1,441 @@
1
+ import express, { Router } from 'express';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { promisify } from 'util';
5
+ import matter from 'gray-matter';
6
+ import { SlashCommand, SlashCommandCreate, SlashCommandUpdate, SlashCommandFilter } from '../types/commands.js';
7
+
8
+ const router: Router = express.Router();
9
+ const readdir = promisify(fs.readdir);
10
+ const readFile = promisify(fs.readFile);
11
+ const writeFile = promisify(fs.writeFile);
12
+ const mkdir = promisify(fs.mkdir);
13
+ const unlink = promisify(fs.unlink);
14
+ const stat = promisify(fs.stat);
15
+
16
+ // Get project commands directory (.claude/commands)
17
+ const getProjectCommandsDir = (projectPath?: string) => {
18
+ if (projectPath) {
19
+ return path.join(projectPath, '.claude', 'commands');
20
+ }
21
+ return path.join(process.cwd(), '..', '.claude', 'commands');
22
+ };
23
+
24
+ // Get user commands directory (~/.claude/commands)
25
+ const getUserCommandsDir = () => path.join(process.env.HOME || process.env.USERPROFILE || '', '.claude', 'commands');
26
+
27
+ // Ensure directory exists
28
+ async function ensureDir(dirPath: string) {
29
+ try {
30
+ await mkdir(dirPath, { recursive: true });
31
+ } catch (error) {
32
+ // Directory already exists
33
+ }
34
+ }
35
+
36
+ // Parse command file content
37
+ function parseCommandContent(content: string): { frontmatter: any; body: string } {
38
+ try {
39
+ const parsed = matter(content);
40
+ return {
41
+ frontmatter: parsed.data,
42
+ body: parsed.content.trim()
43
+ };
44
+ } catch {
45
+ return {
46
+ frontmatter: {},
47
+ body: content
48
+ };
49
+ }
50
+ }
51
+
52
+ // Format command content with frontmatter
53
+ function formatCommandContent(command: SlashCommandCreate | SlashCommandUpdate, existingContent?: string): string {
54
+ let frontmatter: any = {};
55
+
56
+ if (existingContent) {
57
+ const parsed = parseCommandContent(existingContent);
58
+ frontmatter = parsed.frontmatter;
59
+ }
60
+
61
+ // Update frontmatter with new values
62
+ if (command.description) frontmatter.description = command.description;
63
+ if (command.argumentHint) frontmatter['argument-hint'] = command.argumentHint;
64
+ if (command.allowedTools) frontmatter['allowed-tools'] = command.allowedTools.join(', ');
65
+ if (command.model) frontmatter.model = command.model;
66
+ if ('namespace' in command && command.namespace !== undefined) frontmatter.namespace = command.namespace;
67
+
68
+ // Build content
69
+ let content = '';
70
+ if (Object.keys(frontmatter).length > 0) {
71
+ content += '---\n';
72
+ for (const [key, value] of Object.entries(frontmatter)) {
73
+ // Quote values that contain special YAML characters
74
+ const shouldQuote = typeof value === 'string' && (/[[\]{}:>|*&!%@`]/.test(value) || value.includes('#') || value.trim() !== value);
75
+ const formattedValue = shouldQuote ? `"${value.replace(/"/g, '\\"')}"` : value;
76
+ content += `${key}: ${formattedValue}\n`;
77
+ }
78
+ content += '---\n\n';
79
+ }
80
+
81
+ if ('content' in command && command.content) {
82
+ content += command.content;
83
+ } else if (existingContent) {
84
+ const parsed = parseCommandContent(existingContent);
85
+ content += parsed.body;
86
+ }
87
+
88
+ return content;
89
+ }
90
+
91
+ // Scan commands in directory
92
+ async function scanCommands(dirPath: string, scope: 'project' | 'user'): Promise<SlashCommand[]> {
93
+ try {
94
+ await ensureDir(dirPath);
95
+ const commands: SlashCommand[] = [];
96
+
97
+ async function scanDirectory(currentDir: string, namespace?: string) {
98
+ const items = await readdir(currentDir, { withFileTypes: true });
99
+
100
+ for (const item of items) {
101
+ const itemPath = path.join(currentDir, item.name);
102
+
103
+ if (item.isDirectory()) {
104
+ const subNamespace = namespace ? `${namespace}/${item.name}` : item.name;
105
+ await scanDirectory(itemPath, subNamespace);
106
+ } else if (item.name.endsWith('.md')) {
107
+ const commandName = item.name.replace('.md', '');
108
+ const content = await readFile(itemPath, 'utf-8');
109
+ const parsed = parseCommandContent(content);
110
+ const stats = await stat(itemPath);
111
+
112
+ commands.push({
113
+ id: `${scope}:${namespace ? namespace + '/' : ''}${commandName}`,
114
+ name: commandName,
115
+ description: parsed.frontmatter.description || parsed.body.split('\n')[0] || '',
116
+ content: parsed.body,
117
+ scope,
118
+ namespace,
119
+ argumentHint: parsed.frontmatter['argument-hint'],
120
+ allowedTools: parsed.frontmatter['allowed-tools'] ?
121
+ parsed.frontmatter['allowed-tools'].split(',').map((s: string) => s.trim()) : undefined,
122
+ model: parsed.frontmatter.model,
123
+ createdAt: stats.birthtime,
124
+ updatedAt: stats.mtime
125
+ });
126
+ }
127
+ }
128
+ }
129
+
130
+ await scanDirectory(dirPath);
131
+ return commands;
132
+ } catch (error) {
133
+ console.error(`Error scanning commands in ${dirPath}:`, error);
134
+ return [];
135
+ }
136
+ }
137
+
138
+ // GET /api/commands - List all commands
139
+ router.get('/', async (req, res) => {
140
+ try {
141
+ const filter: SlashCommandFilter = {
142
+ scope: req.query.scope as any || 'all',
143
+ namespace: req.query.namespace as string,
144
+ search: req.query.search as string
145
+ };
146
+ const projectPath = req.query.projectPath as string;
147
+
148
+ let commands: SlashCommand[] = [];
149
+
150
+ // Scan project commands
151
+ if (filter.scope === 'all' || filter.scope === 'project') {
152
+ const projectCommands = await scanCommands(getProjectCommandsDir(projectPath), 'project');
153
+ commands.push(...projectCommands);
154
+ }
155
+
156
+ // Scan user commands
157
+ if (filter.scope === 'all' || filter.scope === 'user') {
158
+ const userCommands = await scanCommands(getUserCommandsDir(), 'user');
159
+ commands.push(...userCommands);
160
+ }
161
+
162
+ // Apply filters
163
+ if (filter.namespace) {
164
+ commands = commands.filter(cmd => cmd.namespace === filter.namespace);
165
+ }
166
+
167
+ if (filter.search) {
168
+ const searchLower = filter.search.toLowerCase();
169
+ // Remove leading '/' if present, as it's not part of the actual command name
170
+ const cleanSearch = searchLower.startsWith('/') ? searchLower.slice(1) : searchLower;
171
+
172
+ commands = commands.filter(cmd => {
173
+ // Basic field matching
174
+ const basicMatch = cmd.name.toLowerCase().includes(cleanSearch) ||
175
+ cmd.description.toLowerCase().includes(cleanSearch) ||
176
+ cmd.content.toLowerCase().includes(cleanSearch) ||
177
+ (cmd.namespace && cmd.namespace.toLowerCase().includes(cleanSearch));
178
+
179
+ // Special handling for namespace pattern matching (e.g., "code:" should match "code:testcmd")
180
+ if (cleanSearch.endsWith(':') && cmd.namespace) {
181
+ const namespacePrefix = cleanSearch.slice(0, -1); // Remove the trailing ':'
182
+ if (cmd.namespace.toLowerCase() === namespacePrefix) {
183
+ return true;
184
+ }
185
+ }
186
+
187
+ // Full namespace:name pattern matching (e.g., "code:test" should match "code:testcmd")
188
+ if (cleanSearch.includes(':') && cmd.namespace) {
189
+ const fullDisplayName = `${cmd.namespace.toLowerCase()}:${cmd.name.toLowerCase()}`;
190
+ if (fullDisplayName.includes(cleanSearch)) {
191
+ return true;
192
+ }
193
+ }
194
+
195
+ return basicMatch;
196
+ });
197
+ }
198
+
199
+ // Sort by scope (project first) then by name
200
+ commands.sort((a, b) => {
201
+ if (a.scope !== b.scope) {
202
+ return a.scope === 'project' ? -1 : 1;
203
+ }
204
+ return a.name.localeCompare(b.name);
205
+ });
206
+
207
+ res.json(commands);
208
+ } catch (error) {
209
+ console.error('Error listing commands:', error);
210
+ res.status(500).json({ error: 'Failed to list commands' });
211
+ }
212
+ });
213
+
214
+ // GET /api/commands/:id - Get specific command
215
+ router.get('/:id', async (req, res) => {
216
+ try {
217
+ const { id } = req.params;
218
+ const projectPath = req.query.projectPath as string;
219
+ const [scope, ...nameParts] = id.split(':');
220
+ const fullName = nameParts.join(':');
221
+
222
+ if (!['project', 'user'].includes(scope)) {
223
+ return res.status(400).json({ error: 'Invalid command scope' });
224
+ }
225
+
226
+ const baseDir = scope === 'project' ? getProjectCommandsDir(projectPath) : getUserCommandsDir();
227
+ const filePath = path.join(baseDir, fullName + '.md');
228
+
229
+ try {
230
+ const content = await readFile(filePath, 'utf-8');
231
+ const parsed = parseCommandContent(content);
232
+ const stats = await stat(filePath);
233
+
234
+ const pathParts = fullName.split('/');
235
+ const commandName = pathParts.pop()!;
236
+ const namespace = pathParts.length > 0 ? pathParts.join('/') : undefined;
237
+
238
+ const command: SlashCommand = {
239
+ id,
240
+ name: commandName,
241
+ description: parsed.frontmatter.description || parsed.body.split('\n')[0] || '',
242
+ content: parsed.body,
243
+ scope: scope as 'project' | 'user',
244
+ namespace,
245
+ argumentHint: parsed.frontmatter['argument-hint'],
246
+ allowedTools: parsed.frontmatter['allowed-tools'] ?
247
+ parsed.frontmatter['allowed-tools'].split(',').map((s: string) => s.trim()) : undefined,
248
+ model: parsed.frontmatter.model,
249
+ createdAt: stats.birthtime,
250
+ updatedAt: stats.mtime
251
+ };
252
+
253
+ res.json(command);
254
+ } catch (error) {
255
+ res.status(404).json({ error: 'Command not found' });
256
+ }
257
+ } catch (error) {
258
+ console.error('Error getting command:', error);
259
+ res.status(500).json({ error: 'Failed to get command' });
260
+ }
261
+ });
262
+
263
+ // POST /api/commands - Create new command
264
+ router.post('/', async (req, res) => {
265
+ try {
266
+ const commandData: SlashCommandCreate = req.body;
267
+ const projectPath = req.query.projectPath as string;
268
+
269
+ if (!commandData.name || !commandData.content || !commandData.scope) {
270
+ return res.status(400).json({ error: 'Missing required fields: name, content, scope' });
271
+ }
272
+
273
+ if (!['project', 'user'].includes(commandData.scope)) {
274
+ return res.status(400).json({ error: 'Invalid scope. Must be "project" or "user"' });
275
+ }
276
+
277
+ const baseDir = commandData.scope === 'project' ? getProjectCommandsDir(projectPath) : getUserCommandsDir();
278
+ const fileName = commandData.namespace
279
+ ? path.join(commandData.namespace, commandData.name + '.md')
280
+ : commandData.name + '.md';
281
+ const filePath = path.join(baseDir, fileName);
282
+
283
+ // Check if command already exists
284
+ try {
285
+ await stat(filePath);
286
+ return res.status(409).json({ error: 'Command already exists' });
287
+ } catch {
288
+ // Command doesn't exist, continue
289
+ }
290
+
291
+ // Ensure directory exists
292
+ await ensureDir(path.dirname(filePath));
293
+
294
+ // Format and write content
295
+ const content = formatCommandContent(commandData);
296
+ await writeFile(filePath, content, 'utf-8');
297
+
298
+ // Return created command
299
+ const stats = await stat(filePath);
300
+ const command: SlashCommand = {
301
+ id: `${commandData.scope}:${commandData.namespace ? commandData.namespace + '/' : ''}${commandData.name}`,
302
+ name: commandData.name,
303
+ description: commandData.description || '',
304
+ content: commandData.content,
305
+ scope: commandData.scope,
306
+ namespace: commandData.namespace,
307
+ argumentHint: commandData.argumentHint,
308
+ allowedTools: commandData.allowedTools,
309
+ model: commandData.model,
310
+ createdAt: stats.birthtime,
311
+ updatedAt: stats.mtime
312
+ };
313
+
314
+ res.status(201).json(command);
315
+ } catch (error) {
316
+ console.error('Error creating command:', error);
317
+ res.status(500).json({ error: 'Failed to create command' });
318
+ }
319
+ });
320
+
321
+ // PUT /api/commands/:id - Update command
322
+ router.put('/:id', async (req, res) => {
323
+ try {
324
+ const { id } = req.params;
325
+ const updateData: SlashCommandUpdate = req.body;
326
+ const projectPath = req.query.projectPath as string;
327
+ const [scope, ...nameParts] = id.split(':');
328
+ const fullName = nameParts.join(':');
329
+
330
+ if (!['project', 'user'].includes(scope)) {
331
+ return res.status(400).json({ error: 'Invalid command scope' });
332
+ }
333
+
334
+ const baseDir = scope === 'project' ? getProjectCommandsDir(projectPath) : getUserCommandsDir();
335
+ const oldFilePath = path.join(baseDir, fullName + '.md');
336
+
337
+ try {
338
+ // Read existing content
339
+ const existingContent = await readFile(oldFilePath, 'utf-8');
340
+
341
+ // Parse existing command to get current namespace and name
342
+ const pathParts = fullName.split('/');
343
+ const commandName = pathParts.pop()!;
344
+ const currentNamespace = pathParts.length > 0 ? pathParts.join('/') : undefined;
345
+
346
+ // Determine new namespace (from update data or keep current)
347
+ const newNamespace = updateData.namespace !== undefined ? updateData.namespace || undefined : currentNamespace;
348
+
349
+ // Format updated content
350
+ const content = formatCommandContent(updateData, existingContent);
351
+
352
+ // Check if namespace changed - if so, we need to move the file
353
+ let newFilePath = oldFilePath;
354
+ let newId = id;
355
+
356
+ if (newNamespace !== currentNamespace) {
357
+ const newFileName = newNamespace
358
+ ? path.join(newNamespace, commandName + '.md')
359
+ : commandName + '.md';
360
+ newFilePath = path.join(baseDir, newFileName);
361
+ newId = `${scope}:${newNamespace ? newNamespace + '/' : ''}${commandName}`;
362
+
363
+ // Ensure new directory exists
364
+ await ensureDir(path.dirname(newFilePath));
365
+
366
+ // Check if target file already exists
367
+ try {
368
+ await stat(newFilePath);
369
+ if (newFilePath !== oldFilePath) {
370
+ return res.status(409).json({ error: 'A command with this namespace and name already exists' });
371
+ }
372
+ } catch {
373
+ // File doesn't exist, good to proceed
374
+ }
375
+ }
376
+
377
+ // Write to new location
378
+ await writeFile(newFilePath, content, 'utf-8');
379
+
380
+ // If file location changed, remove old file
381
+ if (newFilePath !== oldFilePath) {
382
+ await unlink(oldFilePath);
383
+ }
384
+
385
+ // Return updated command
386
+ const parsed = parseCommandContent(content);
387
+ const stats = await stat(newFilePath);
388
+
389
+ const command: SlashCommand = {
390
+ id: newId,
391
+ name: commandName,
392
+ description: parsed.frontmatter.description || parsed.body.split('\n')[0] || '',
393
+ content: parsed.body,
394
+ scope: scope as 'project' | 'user',
395
+ namespace: newNamespace,
396
+ argumentHint: parsed.frontmatter['argument-hint'],
397
+ allowedTools: parsed.frontmatter['allowed-tools'] ?
398
+ parsed.frontmatter['allowed-tools'].split(',').map((s: string) => s.trim()) : undefined,
399
+ model: parsed.frontmatter.model,
400
+ createdAt: stats.birthtime,
401
+ updatedAt: stats.mtime
402
+ };
403
+
404
+ res.json(command);
405
+ } catch (error) {
406
+ res.status(404).json({ error: 'Command not found' });
407
+ }
408
+ } catch (error) {
409
+ console.error('Error updating command:', error);
410
+ res.status(500).json({ error: 'Failed to update command' });
411
+ }
412
+ });
413
+
414
+ // DELETE /api/commands/:id - Delete command
415
+ router.delete('/:id', async (req, res) => {
416
+ try {
417
+ const { id } = req.params;
418
+ const projectPath = req.query.projectPath as string;
419
+ const [scope, ...nameParts] = id.split(':');
420
+ const fullName = nameParts.join(':');
421
+
422
+ if (!['project', 'user'].includes(scope)) {
423
+ return res.status(400).json({ error: 'Invalid command scope' });
424
+ }
425
+
426
+ const baseDir = scope === 'project' ? getProjectCommandsDir(projectPath) : getUserCommandsDir();
427
+ const filePath = path.join(baseDir, fullName + '.md');
428
+
429
+ try {
430
+ await unlink(filePath);
431
+ res.status(204).send();
432
+ } catch (error) {
433
+ res.status(404).json({ error: 'Command not found' });
434
+ }
435
+ } catch (error) {
436
+ console.error('Error deleting command:', error);
437
+ res.status(500).json({ error: 'Failed to delete command' });
438
+ }
439
+ });
440
+
441
+ export default router;