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,170 @@
|
|
|
1
|
+
import express, { Router } from 'express';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { join, dirname, resolve, relative } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = dirname(__filename);
|
|
10
|
+
|
|
11
|
+
const router: Router = express.Router();
|
|
12
|
+
|
|
13
|
+
// Get working directory (project root)
|
|
14
|
+
const getWorkingDir = () => {
|
|
15
|
+
return resolve(__dirname, '../../..');
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Validation schemas
|
|
19
|
+
const ReadFileSchema = z.object({
|
|
20
|
+
path: z.string()
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const ReadFilesSchema = z.object({
|
|
24
|
+
paths: z.array(z.string())
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const WriteFileSchema = z.object({
|
|
28
|
+
path: z.string(),
|
|
29
|
+
content: z.string()
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Helper function to resolve and validate file path
|
|
33
|
+
const resolveSafePath = (filePath: string): string => {
|
|
34
|
+
const workingDir = getWorkingDir();
|
|
35
|
+
const resolvedPath = resolve(workingDir, filePath);
|
|
36
|
+
|
|
37
|
+
// Ensure the path is within the working directory for security
|
|
38
|
+
const relativePath = relative(workingDir, resolvedPath);
|
|
39
|
+
if (relativePath.startsWith('..') || resolve(workingDir, relativePath) !== resolvedPath) {
|
|
40
|
+
throw new Error('Path is outside working directory');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return resolvedPath;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// GET /api/files/read - Read a single file
|
|
47
|
+
router.get('/read', async (req, res) => {
|
|
48
|
+
try {
|
|
49
|
+
const { path } = req.query;
|
|
50
|
+
|
|
51
|
+
if (!path || typeof path !== 'string') {
|
|
52
|
+
return res.status(400).json({ error: 'File path is required' });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const fullPath = resolveSafePath(path);
|
|
56
|
+
|
|
57
|
+
if (!existsSync(fullPath)) {
|
|
58
|
+
return res.status(404).json({ error: 'File not found' });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
62
|
+
|
|
63
|
+
res.json({
|
|
64
|
+
path,
|
|
65
|
+
content,
|
|
66
|
+
exists: true
|
|
67
|
+
});
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error('Error reading file:', error);
|
|
70
|
+
if (error instanceof Error && error.message === 'Path is outside working directory') {
|
|
71
|
+
return res.status(403).json({ error: 'Access denied' });
|
|
72
|
+
}
|
|
73
|
+
res.status(500).json({ error: 'Failed to read file' });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// POST /api/files/read-multiple - Read multiple files
|
|
78
|
+
router.post('/read-multiple', async (req, res) => {
|
|
79
|
+
try {
|
|
80
|
+
const validation = ReadFilesSchema.safeParse(req.body);
|
|
81
|
+
if (!validation.success) {
|
|
82
|
+
return res.status(400).json({ error: 'Invalid request body', details: validation.error });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const { paths } = validation.data;
|
|
86
|
+
|
|
87
|
+
const results = await Promise.allSettled(
|
|
88
|
+
paths.map(async (path) => {
|
|
89
|
+
try {
|
|
90
|
+
const fullPath = resolveSafePath(path);
|
|
91
|
+
const exists = existsSync(fullPath);
|
|
92
|
+
|
|
93
|
+
if (!exists) {
|
|
94
|
+
return {
|
|
95
|
+
path,
|
|
96
|
+
content: null,
|
|
97
|
+
exists: false,
|
|
98
|
+
error: 'File not found'
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
103
|
+
return {
|
|
104
|
+
path,
|
|
105
|
+
content,
|
|
106
|
+
exists: true
|
|
107
|
+
};
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return {
|
|
110
|
+
path,
|
|
111
|
+
content: null,
|
|
112
|
+
exists: false,
|
|
113
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const files = results.map((result, index) => {
|
|
120
|
+
if (result.status === 'fulfilled') {
|
|
121
|
+
return result.value;
|
|
122
|
+
} else {
|
|
123
|
+
return {
|
|
124
|
+
path: paths[index],
|
|
125
|
+
content: null,
|
|
126
|
+
exists: false,
|
|
127
|
+
error: result.reason
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
res.json({ files });
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error('Error reading files:', error);
|
|
135
|
+
res.status(500).json({ error: 'Failed to read files' });
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// PUT /api/files/write - Write to a single file
|
|
140
|
+
router.put('/write', async (req, res) => {
|
|
141
|
+
try {
|
|
142
|
+
const validation = WriteFileSchema.safeParse(req.body);
|
|
143
|
+
if (!validation.success) {
|
|
144
|
+
return res.status(400).json({ error: 'Invalid request body', details: validation.error });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const { path, content } = validation.data;
|
|
148
|
+
const fullPath = resolveSafePath(path);
|
|
149
|
+
|
|
150
|
+
// Ensure directory exists
|
|
151
|
+
await fs.ensureDir(dirname(fullPath));
|
|
152
|
+
|
|
153
|
+
// Write the file
|
|
154
|
+
await fs.writeFile(fullPath, content, 'utf-8');
|
|
155
|
+
|
|
156
|
+
res.json({
|
|
157
|
+
success: true,
|
|
158
|
+
message: 'File written successfully',
|
|
159
|
+
path
|
|
160
|
+
});
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error('Error writing file:', error);
|
|
163
|
+
if (error instanceof Error && error.message === 'Path is outside working directory') {
|
|
164
|
+
return res.status(403).json({ error: 'Access denied' });
|
|
165
|
+
}
|
|
166
|
+
res.status(500).json({ error: 'Failed to write file' });
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
export default router;
|
|
@@ -0,0 +1,364 @@
|
|
|
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 { Subagent, SubagentCreate, SubagentUpdate, SubagentFilter } from '../types/subagents.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 user subagents directory (~/.claude/agents)
|
|
17
|
+
const getUserSubagentsDir = () => path.join(process.env.HOME || process.env.USERPROFILE || '', '.claude', 'agents');
|
|
18
|
+
|
|
19
|
+
// Get project subagents directory (.claude/agents)
|
|
20
|
+
const getProjectSubagentsDir = (projectPath?: string) => {
|
|
21
|
+
if (projectPath) {
|
|
22
|
+
return path.join(projectPath, '.claude', 'agents');
|
|
23
|
+
}
|
|
24
|
+
return path.join(process.cwd(), '..', '.claude', 'agents');
|
|
25
|
+
};
|
|
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 subagent file content
|
|
37
|
+
function parseSubagentContent(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 subagent content with frontmatter
|
|
53
|
+
function formatSubagentContent(subagent: SubagentCreate | SubagentUpdate, existingContent?: string): string {
|
|
54
|
+
let frontmatter: any = {};
|
|
55
|
+
|
|
56
|
+
if (existingContent) {
|
|
57
|
+
const parsed = parseSubagentContent(existingContent);
|
|
58
|
+
frontmatter = parsed.frontmatter;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Update frontmatter with new values
|
|
62
|
+
if ('name' in subagent && subagent.name) frontmatter.name = subagent.name;
|
|
63
|
+
if (subagent.description) frontmatter.description = subagent.description;
|
|
64
|
+
if (subagent.tools && subagent.tools.length > 0) {
|
|
65
|
+
frontmatter.tools = subagent.tools.join(', ');
|
|
66
|
+
}
|
|
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
|
+
content += `${key}: ${value}\n`;
|
|
74
|
+
}
|
|
75
|
+
content += '---\n\n';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if ('content' in subagent && subagent.content) {
|
|
79
|
+
content += subagent.content;
|
|
80
|
+
} else if (existingContent) {
|
|
81
|
+
const parsed = parseSubagentContent(existingContent);
|
|
82
|
+
content += parsed.body;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return content;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Scan subagents in directory
|
|
89
|
+
async function scanSubagents(dirPath: string): Promise<Subagent[]> {
|
|
90
|
+
try {
|
|
91
|
+
await ensureDir(dirPath);
|
|
92
|
+
const subagents: Subagent[] = [];
|
|
93
|
+
const items = await readdir(dirPath, { withFileTypes: true });
|
|
94
|
+
|
|
95
|
+
for (const item of items) {
|
|
96
|
+
if (item.isFile() && item.name.endsWith('.md')) {
|
|
97
|
+
const subagentName = item.name.replace('.md', '');
|
|
98
|
+
const itemPath = path.join(dirPath, item.name);
|
|
99
|
+
const content = await readFile(itemPath, 'utf-8');
|
|
100
|
+
const parsed = parseSubagentContent(content);
|
|
101
|
+
const stats = await stat(itemPath);
|
|
102
|
+
|
|
103
|
+
subagents.push({
|
|
104
|
+
id: `user:${subagentName}`,
|
|
105
|
+
name: parsed.frontmatter.name || subagentName,
|
|
106
|
+
description: parsed.frontmatter.description || '',
|
|
107
|
+
content: parsed.body,
|
|
108
|
+
scope: 'user',
|
|
109
|
+
tools: parsed.frontmatter.tools ?
|
|
110
|
+
parsed.frontmatter.tools.split(',').map((s: string) => s.trim()) : undefined,
|
|
111
|
+
createdAt: stats.birthtime,
|
|
112
|
+
updatedAt: stats.mtime
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return subagents;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error(`Error scanning subagents in ${dirPath}:`, error);
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// GET /api/subagents - List all subagents
|
|
125
|
+
router.get('/', async (req, res) => {
|
|
126
|
+
try {
|
|
127
|
+
const filter: SubagentFilter = {
|
|
128
|
+
search: req.query.search as string
|
|
129
|
+
};
|
|
130
|
+
const projectPath = req.query.projectPath as string;
|
|
131
|
+
|
|
132
|
+
let subagents;
|
|
133
|
+
if (projectPath) {
|
|
134
|
+
// Get project-specific subagents
|
|
135
|
+
subagents = await scanSubagents(getProjectSubagentsDir(projectPath));
|
|
136
|
+
} else {
|
|
137
|
+
// Get user subagents
|
|
138
|
+
subagents = await scanSubagents(getUserSubagentsDir());
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Apply search filter
|
|
142
|
+
if (filter.search) {
|
|
143
|
+
const searchLower = filter.search.toLowerCase();
|
|
144
|
+
subagents = subagents.filter(subagent =>
|
|
145
|
+
subagent.name.toLowerCase().includes(searchLower) ||
|
|
146
|
+
subagent.description.toLowerCase().includes(searchLower) ||
|
|
147
|
+
subagent.content.toLowerCase().includes(searchLower)
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Sort by name
|
|
152
|
+
subagents.sort((a, b) => a.name.localeCompare(b.name));
|
|
153
|
+
|
|
154
|
+
res.json(subagents);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
console.error('Error listing subagents:', error);
|
|
157
|
+
res.status(500).json({ error: 'Failed to list subagents' });
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// GET /api/subagents/:id - Get specific subagent
|
|
162
|
+
router.get('/:id', async (req, res) => {
|
|
163
|
+
try {
|
|
164
|
+
const { id } = req.params;
|
|
165
|
+
const [scope, name] = id.split(':');
|
|
166
|
+
|
|
167
|
+
if (scope !== 'user') {
|
|
168
|
+
return res.status(400).json({ error: 'Invalid subagent scope' });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const dirPath = getUserSubagentsDir();
|
|
172
|
+
const filePath = path.join(dirPath, name + '.md');
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const content = await readFile(filePath, 'utf-8');
|
|
176
|
+
const parsed = parseSubagentContent(content);
|
|
177
|
+
const stats = await stat(filePath);
|
|
178
|
+
|
|
179
|
+
const subagent: Subagent = {
|
|
180
|
+
id,
|
|
181
|
+
name: parsed.frontmatter.name || name,
|
|
182
|
+
description: parsed.frontmatter.description || '',
|
|
183
|
+
content: parsed.body,
|
|
184
|
+
scope: 'user',
|
|
185
|
+
tools: parsed.frontmatter.tools ?
|
|
186
|
+
parsed.frontmatter.tools.split(',').map((s: string) => s.trim()) : undefined,
|
|
187
|
+
createdAt: stats.birthtime,
|
|
188
|
+
updatedAt: stats.mtime
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
res.json(subagent);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
res.status(404).json({ error: 'Subagent not found' });
|
|
194
|
+
}
|
|
195
|
+
} catch (error) {
|
|
196
|
+
console.error('Error getting subagent:', error);
|
|
197
|
+
res.status(500).json({ error: 'Failed to get subagent' });
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// POST /api/subagents - Create new subagent
|
|
202
|
+
router.post('/', async (req, res) => {
|
|
203
|
+
try {
|
|
204
|
+
const subagentData: SubagentCreate = req.body;
|
|
205
|
+
|
|
206
|
+
if (!subagentData.name || !subagentData.description || !subagentData.content) {
|
|
207
|
+
return res.status(400).json({ error: 'Missing required fields: name, description, content' });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!['user', 'project'].includes(subagentData.scope)) {
|
|
211
|
+
return res.status(400).json({ error: 'Invalid scope. Must be "user" or "project"' });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Validate name format (lowercase letters and hyphens only)
|
|
215
|
+
const nameRegex = /^[a-z0-9-]+$/;
|
|
216
|
+
if (!nameRegex.test(subagentData.name)) {
|
|
217
|
+
return res.status(400).json({ error: 'Name must contain only lowercase letters, numbers, and hyphens' });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const projectPath = req.query.projectPath as string;
|
|
221
|
+
let dirPath: string;
|
|
222
|
+
|
|
223
|
+
if (subagentData.scope === 'project') {
|
|
224
|
+
if (!projectPath) {
|
|
225
|
+
return res.status(400).json({ error: 'Project path is required for project scope' });
|
|
226
|
+
}
|
|
227
|
+
dirPath = getProjectSubagentsDir(projectPath);
|
|
228
|
+
} else {
|
|
229
|
+
dirPath = getUserSubagentsDir();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const filePath = path.join(dirPath, subagentData.name + '.md');
|
|
233
|
+
|
|
234
|
+
// Check if subagent already exists
|
|
235
|
+
try {
|
|
236
|
+
await stat(filePath);
|
|
237
|
+
return res.status(409).json({ error: 'Subagent already exists' });
|
|
238
|
+
} catch {
|
|
239
|
+
// Subagent doesn't exist, continue
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Ensure directory exists
|
|
243
|
+
await ensureDir(dirPath);
|
|
244
|
+
|
|
245
|
+
// Format and write content
|
|
246
|
+
const content = formatSubagentContent(subagentData);
|
|
247
|
+
await writeFile(filePath, content, 'utf-8');
|
|
248
|
+
|
|
249
|
+
// Return created subagent
|
|
250
|
+
const stats = await stat(filePath);
|
|
251
|
+
const subagent: Subagent = {
|
|
252
|
+
id: `${subagentData.scope}:${subagentData.name}`,
|
|
253
|
+
name: subagentData.name,
|
|
254
|
+
description: subagentData.description,
|
|
255
|
+
content: subagentData.content,
|
|
256
|
+
scope: subagentData.scope,
|
|
257
|
+
tools: subagentData.tools,
|
|
258
|
+
createdAt: stats.birthtime,
|
|
259
|
+
updatedAt: stats.mtime
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
res.status(201).json(subagent);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
console.error('Error creating subagent:', error);
|
|
265
|
+
res.status(500).json({ error: 'Failed to create subagent' });
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// PUT /api/subagents/:id - Update subagent
|
|
270
|
+
router.put('/:id', async (req, res) => {
|
|
271
|
+
try {
|
|
272
|
+
const { id } = req.params;
|
|
273
|
+
const updateData: SubagentUpdate = req.body;
|
|
274
|
+
const [scope, name] = id.split(':');
|
|
275
|
+
|
|
276
|
+
if (!['user', 'project'].includes(scope)) {
|
|
277
|
+
return res.status(400).json({ error: 'Invalid subagent scope' });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const projectPath = req.query.projectPath as string;
|
|
281
|
+
let dirPath: string;
|
|
282
|
+
|
|
283
|
+
if (scope === 'project') {
|
|
284
|
+
if (!projectPath) {
|
|
285
|
+
return res.status(400).json({ error: 'Project path is required for project scope' });
|
|
286
|
+
}
|
|
287
|
+
dirPath = getProjectSubagentsDir(projectPath);
|
|
288
|
+
} else {
|
|
289
|
+
dirPath = getUserSubagentsDir();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const filePath = path.join(dirPath, name + '.md');
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
// Read existing content
|
|
296
|
+
const existingContent = await readFile(filePath, 'utf-8');
|
|
297
|
+
|
|
298
|
+
// Format updated content
|
|
299
|
+
const content = formatSubagentContent(updateData, existingContent);
|
|
300
|
+
await writeFile(filePath, content, 'utf-8');
|
|
301
|
+
|
|
302
|
+
// Return updated subagent
|
|
303
|
+
const parsed = parseSubagentContent(content);
|
|
304
|
+
const stats = await stat(filePath);
|
|
305
|
+
|
|
306
|
+
const subagent: Subagent = {
|
|
307
|
+
id,
|
|
308
|
+
name: parsed.frontmatter.name || name,
|
|
309
|
+
description: parsed.frontmatter.description || '',
|
|
310
|
+
content: parsed.body,
|
|
311
|
+
scope: scope as 'user' | 'project',
|
|
312
|
+
tools: parsed.frontmatter.tools ?
|
|
313
|
+
parsed.frontmatter.tools.split(',').map((s: string) => s.trim()) : undefined,
|
|
314
|
+
createdAt: stats.birthtime,
|
|
315
|
+
updatedAt: stats.mtime
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
res.json(subagent);
|
|
319
|
+
} catch (error) {
|
|
320
|
+
res.status(404).json({ error: 'Subagent not found' });
|
|
321
|
+
}
|
|
322
|
+
} catch (error) {
|
|
323
|
+
console.error('Error updating subagent:', error);
|
|
324
|
+
res.status(500).json({ error: 'Failed to update subagent' });
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// DELETE /api/subagents/:id - Delete subagent
|
|
329
|
+
router.delete('/:id', async (req, res) => {
|
|
330
|
+
try {
|
|
331
|
+
const { id } = req.params;
|
|
332
|
+
const [scope, name] = id.split(':');
|
|
333
|
+
|
|
334
|
+
if (!['user', 'project'].includes(scope)) {
|
|
335
|
+
return res.status(400).json({ error: 'Invalid subagent scope' });
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const projectPath = req.query.projectPath as string;
|
|
339
|
+
let dirPath: string;
|
|
340
|
+
|
|
341
|
+
if (scope === 'project') {
|
|
342
|
+
if (!projectPath) {
|
|
343
|
+
return res.status(400).json({ error: 'Project path is required for project scope' });
|
|
344
|
+
}
|
|
345
|
+
dirPath = getProjectSubagentsDir(projectPath);
|
|
346
|
+
} else {
|
|
347
|
+
dirPath = getUserSubagentsDir();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const filePath = path.join(dirPath, name + '.md');
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
await unlink(filePath);
|
|
354
|
+
res.status(204).send();
|
|
355
|
+
} catch (error) {
|
|
356
|
+
res.status(404).json({ error: 'Subagent not found' });
|
|
357
|
+
}
|
|
358
|
+
} catch (error) {
|
|
359
|
+
console.error('Error deleting subagent:', error);
|
|
360
|
+
res.status(500).json({ error: 'Failed to delete subagent' });
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
export default router;
|