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.
- package/.env +15 -0
- package/README.md +85 -0
- package/dist/bin/agentstudio.d.ts +3 -0
- package/dist/bin/agentstudio.d.ts.map +1 -0
- package/dist/bin/agentstudio.js +141 -0
- package/dist/bin/agentstudio.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +87 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/auth.d.ts +7 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +21 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/routes/agents.d.ts +4 -0
- package/dist/routes/agents.d.ts.map +1 -0
- package/dist/routes/agents.js +804 -0
- package/dist/routes/agents.js.map +1 -0
- package/dist/routes/auth.d.ts +4 -0
- package/dist/routes/auth.d.ts.map +1 -0
- package/dist/routes/auth.js +60 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/files.d.ts +4 -0
- package/dist/routes/files.d.ts.map +1 -0
- package/dist/routes/files.js +301 -0
- package/dist/routes/files.js.map +1 -0
- package/dist/routes/mcp.d.ts +4 -0
- package/dist/routes/mcp.d.ts.map +1 -0
- package/dist/routes/mcp.js +652 -0
- package/dist/routes/mcp.js.map +1 -0
- package/dist/routes/media.d.ts +5 -0
- package/dist/routes/media.d.ts.map +1 -0
- package/dist/routes/media.js +117 -0
- package/dist/routes/media.js.map +1 -0
- package/dist/routes/slides.d.ts +4 -0
- package/dist/routes/slides.d.ts.map +1 -0
- package/dist/routes/slides.js +146 -0
- package/dist/routes/slides.js.map +1 -0
- package/dist/services/claudeSession.d.ts +83 -0
- package/dist/services/claudeSession.d.ts.map +1 -0
- package/dist/services/claudeSession.js +255 -0
- package/dist/services/claudeSession.js.map +1 -0
- package/dist/services/messageQueue.d.ts +31 -0
- package/dist/services/messageQueue.d.ts.map +1 -0
- package/dist/services/messageQueue.js +67 -0
- package/dist/services/messageQueue.js.map +1 -0
- package/dist/services/sessionManager.d.ts +132 -0
- package/dist/services/sessionManager.d.ts.map +1 -0
- package/dist/services/sessionManager.js +439 -0
- package/dist/services/sessionManager.js.map +1 -0
- package/dist/types/claude-history.d.ts +48 -0
- package/dist/types/claude-history.d.ts.map +1 -0
- package/dist/types/claude-history.js +2 -0
- package/dist/types/claude-history.js.map +1 -0
- package/dist/types/claude-versions.d.ts +31 -0
- package/dist/types/claude-versions.d.ts.map +1 -0
- package/dist/types/claude-versions.js +2 -0
- package/dist/types/claude-versions.js.map +1 -0
- package/dist/types/commands.d.ts +32 -0
- package/dist/types/commands.d.ts.map +1 -0
- package/dist/types/commands.js +2 -0
- package/dist/types/commands.js.map +1 -0
- package/dist/types/index.d.ts +81 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +150 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/subagents.d.ts +88 -0
- package/dist/types/subagents.d.ts.map +1 -0
- package/dist/types/subagents.js +2 -0
- package/dist/types/subagents.js.map +1 -0
- package/dist/utils/agentStorage.d.ts +19 -0
- package/dist/utils/agentStorage.d.ts.map +1 -0
- package/dist/utils/agentStorage.js +110 -0
- package/dist/utils/agentStorage.js.map +1 -0
- package/dist/utils/claudeVersionStorage.d.ts +33 -0
- package/dist/utils/claudeVersionStorage.d.ts.map +1 -0
- package/dist/utils/claudeVersionStorage.js +168 -0
- package/dist/utils/claudeVersionStorage.js.map +1 -0
- package/dist/utils/jwt.d.ts +15 -0
- package/dist/utils/jwt.d.ts.map +1 -0
- package/dist/utils/jwt.js +28 -0
- package/dist/utils/jwt.js.map +1 -0
- package/dist/utils/projectMetadataStorage.d.ts +21 -0
- package/dist/utils/projectMetadataStorage.d.ts.map +1 -0
- package/dist/utils/projectMetadataStorage.js +68 -0
- package/dist/utils/projectMetadataStorage.js.map +1 -0
- package/frontend/dist/index.html +86 -0
- package/package.json +66 -0
- package/src/bin/agentstudio.ts +161 -0
- package/src/index.ts +100 -0
- package/src/middleware/auth.ts +26 -0
- package/src/routes/agents.ts +885 -0
- package/src/routes/auth.ts +73 -0
- package/src/routes/commands.ts.bak +441 -0
- package/src/routes/files.ts +352 -0
- package/src/routes/mcp.ts +751 -0
- package/src/routes/media.ts +140 -0
- package/src/routes/projects.ts.bak +601 -0
- package/src/routes/sessions.ts.bak +809 -0
- package/src/routes/settings.ts.bak +718 -0
- package/src/routes/slides.ts +170 -0
- package/src/routes/subagents.ts.bak +364 -0
- package/src/services/claudeSession.ts +293 -0
- package/src/services/messageQueue.ts +71 -0
- package/src/services/sessionManager.ts +532 -0
- package/src/types/claude-history.ts +50 -0
- package/src/types/claude-versions.ts +33 -0
- package/src/types/commands.ts +35 -0
- package/src/types/index.ts +248 -0
- package/src/types/subagents.ts +106 -0
- package/src/utils/agentStorage.ts +126 -0
- package/src/utils/claudeVersionStorage.ts +199 -0
- package/src/utils/jwt.ts +36 -0
- package/src/utils/projectMetadataStorage.ts +86 -0
- 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;
|