@agent-spaces/server 0.3.67 → 0.4.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/dist/adapters/agent-runtime.js +4 -0
- package/dist/adapters/codex-function-tool-bridge.js +9 -1
- package/dist/adapters/git.js +13 -2
- package/dist/adapters/hermes-runtime.js +711 -108
- package/dist/adapters/langchain-runtime.js +384 -21
- package/dist/adapters/oh-my-pi-runtime.js +858 -0
- package/dist/adapters/open-agent-sdk-runtime.js +202 -5
- package/dist/adapters/open-agent-sdk-runtime.test.js +35 -0
- package/dist/agents/agent-message-parts.js +52 -2
- package/dist/agents/issue-task-controller.js +4 -2
- package/dist/agents/title-generator-agent.js +120 -0
- package/dist/app.js +59 -6
- package/dist/routes/agent-sse.js +70 -7
- package/dist/routes/channel.js +23 -2
- package/dist/routes/chat-run.js +191 -0
- package/dist/routes/chat.js +142 -0
- package/dist/routes/command.js +16 -0
- package/dist/routes/data.js +189 -0
- package/dist/routes/git.js +10 -1
- package/dist/routes/import.js +199 -0
- package/dist/routes/issue.js +16 -4
- package/dist/routes/plugin.js +69 -0
- package/dist/routes/version.js +2 -1
- package/dist/routes/workflow-hook.js +71 -0
- package/dist/routes/workflow.js +282 -7
- package/dist/routes/workspace.js +13 -4
- package/dist/services/agent.js +123 -36
- package/dist/services/ai-text.js +185 -0
- package/dist/services/builtin-tools/index.js +1 -0
- package/dist/services/builtin-tools/workflow-editor-tools.js +509 -0
- package/dist/services/builtin-tools/workflow-exec-tools.js +320 -0
- package/dist/services/chat.js +134 -0
- package/dist/services/command-process-manager.js +16 -0
- package/dist/services/execution-manager.js +1346 -0
- package/dist/services/generated-title.js +59 -0
- package/dist/services/gitignore.js +22 -18
- package/dist/services/interaction-manager.js +114 -0
- package/dist/services/issue-retry.js +25 -0
- package/dist/services/output-style.js +8 -1
- package/dist/services/plugin.js +257 -0
- package/dist/services/prompt-template.js +8 -1
- package/dist/services/pty.js +20 -0
- package/dist/services/search.js +16 -6
- package/dist/services/skill.js +2 -0
- package/dist/services/version.js +17 -7
- package/dist/services/workflow-command-runner.js +5 -4
- package/dist/services/workflow-trigger-service.js +137 -0
- package/dist/services/workflow.js +199 -36
- package/dist/storage/agent-store.js +8 -0
- package/dist/storage/chat-store.js +151 -0
- package/dist/storage/database-store.js +6 -0
- package/dist/storage/json-store.js +2 -1
- package/dist/storage/kanban-store.js +6 -0
- package/dist/storage/workflow-store.js +386 -22
- package/dist/ws/agent-prompt.js +6 -2
- package/dist/ws/agent-runner.js +29 -18
- package/dist/ws/chat-handler.js +89 -0
- package/dist/ws/connection-manager.js +49 -2
- package/dist/ws/execution-channels.js +88 -0
- package/dist/ws/handler.js +32 -5
- package/dist/ws/message-parts.js +130 -12
- package/dist/ws/terminal-handler.js +26 -0
- package/package.json +12 -1
- package/public/avatars/user.jpg +0 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { existsSync, statSync, rmSync, cpSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
import archiver from 'archiver';
|
|
7
|
+
import AdmZip from 'adm-zip';
|
|
8
|
+
import multer from 'multer';
|
|
9
|
+
import { getDataDir } from '../storage/json-store.js';
|
|
10
|
+
import * as agentStore from '../storage/agent-store.js';
|
|
11
|
+
import * as databaseStore from '../storage/database-store.js';
|
|
12
|
+
import * as kanbanStore from '../storage/kanban-store.js';
|
|
13
|
+
const router = Router();
|
|
14
|
+
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 500 * 1024 * 1024 } });
|
|
15
|
+
const CATEGORIES = {
|
|
16
|
+
'auth': { path: 'auth.json', label: 'Authentication', group: 'config' },
|
|
17
|
+
'user-settings': { path: 'user-settings.json', label: 'User Settings', group: 'config' },
|
|
18
|
+
'robot-accounts': { path: 'robot-accounts.json', label: 'Robot Accounts', group: 'config' },
|
|
19
|
+
'speech-recognition': { path: 'speech-recognition.json', label: 'Speech Recognition', group: 'config' },
|
|
20
|
+
'llm': { path: 'llm', label: 'LLM Models & Providers', group: 'ai' },
|
|
21
|
+
'agents': { path: 'agents', label: 'Agent Usage', group: 'content' },
|
|
22
|
+
'database': { path: 'database', label: 'Document Database', group: 'content' },
|
|
23
|
+
'kanban': { path: 'kanban', label: 'Kanban Boards', group: 'content' },
|
|
24
|
+
'output-styles': { path: 'output-styles', label: 'Output Styles', group: 'customization' },
|
|
25
|
+
'prompt-templates': { path: 'prompt-templates', label: 'Prompt Templates', group: 'customization' },
|
|
26
|
+
'subscriptions': { path: 'subscriptions', label: 'Subscriptions', group: 'billing' },
|
|
27
|
+
'skills': { path: 'skills', label: 'Skills', group: 'customization' },
|
|
28
|
+
'mcps': { path: 'mcps', label: 'MCP Servers', group: 'customization' },
|
|
29
|
+
'agent-templates': { path: 'agent-templates', label: 'Agent Templates', group: 'customization' },
|
|
30
|
+
'workflows': { path: 'workflows', label: 'Workflows', group: 'content' },
|
|
31
|
+
};
|
|
32
|
+
// GET /api/data/export — stream zip backup
|
|
33
|
+
router.get('/export', (_req, res) => {
|
|
34
|
+
const dataDir = getDataDir();
|
|
35
|
+
const timestamp = new Date().toISOString().slice(0, 10);
|
|
36
|
+
res.setHeader('Content-Type', 'application/zip');
|
|
37
|
+
res.setHeader('Content-Disposition', `attachment; filename="agent-spaces-backup-${timestamp}.zip"`);
|
|
38
|
+
const archive = archiver('zip', { zlib: { level: 6 } });
|
|
39
|
+
archive.on('error', (err) => {
|
|
40
|
+
console.error('[data-export] archive error:', err);
|
|
41
|
+
if (!res.headersSent) {
|
|
42
|
+
res.status(500).json({ error: 'Failed to create archive' });
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
res.end();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
archive.pipe(res);
|
|
49
|
+
for (const { path: relPath } of Object.values(CATEGORIES)) {
|
|
50
|
+
const fullPath = join(dataDir, relPath);
|
|
51
|
+
if (!existsSync(fullPath))
|
|
52
|
+
continue;
|
|
53
|
+
const stat = statSync(fullPath);
|
|
54
|
+
if (stat.isDirectory()) {
|
|
55
|
+
archive.directory(fullPath, relPath);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
archive.file(fullPath, { name: relPath });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
archive.finalize();
|
|
62
|
+
});
|
|
63
|
+
// POST /api/data/import/preview — upload zip, extract, return categories
|
|
64
|
+
router.post('/import/preview', upload.single('file'), (req, res) => {
|
|
65
|
+
if (!req.file?.buffer) {
|
|
66
|
+
res.status(400).json({ error: 'No file uploaded' });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const zip = new AdmZip(req.file.buffer);
|
|
71
|
+
const sessionId = randomUUID();
|
|
72
|
+
const tempDir = join(tmpdir(), `agent-spaces-import-${sessionId}`);
|
|
73
|
+
zip.extractAllTo(tempDir, true);
|
|
74
|
+
const found = [];
|
|
75
|
+
for (const [key, { path: relPath, label, group }] of Object.entries(CATEGORIES)) {
|
|
76
|
+
const fullPath = join(tempDir, relPath);
|
|
77
|
+
if (!existsSync(fullPath))
|
|
78
|
+
continue;
|
|
79
|
+
const stat = statSync(fullPath);
|
|
80
|
+
const isDir = stat.isDirectory();
|
|
81
|
+
found.push({
|
|
82
|
+
key,
|
|
83
|
+
label,
|
|
84
|
+
group,
|
|
85
|
+
size: isDir ? getDirSize(fullPath) : stat.size,
|
|
86
|
+
type: isDir ? 'directory' : 'file',
|
|
87
|
+
details: isDir ? `${countFiles(fullPath)} files` : formatSize(stat.size),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
activeImportSessions.set(sessionId, { tempDir, expiresAt: Date.now() + 30 * 60 * 1000 });
|
|
91
|
+
res.json({ sessionId, categories: found });
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
res.status(500).json({ error: e.message });
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
// POST /api/data/import/execute — restore selected categories
|
|
98
|
+
router.post('/import/execute', (req, res) => {
|
|
99
|
+
const { sessionId, categories: selectedCategories } = req.body;
|
|
100
|
+
if (!sessionId || !selectedCategories?.length) {
|
|
101
|
+
res.status(400).json({ error: 'sessionId and categories are required' });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const session = activeImportSessions.get(sessionId);
|
|
105
|
+
if (!session || Date.now() > session.expiresAt) {
|
|
106
|
+
res.status(410).json({ error: 'Import session expired. Please upload again.' });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const dataDir = getDataDir();
|
|
110
|
+
const results = {};
|
|
111
|
+
for (const categoryKey of selectedCategories) {
|
|
112
|
+
const category = CATEGORIES[categoryKey];
|
|
113
|
+
if (!category) {
|
|
114
|
+
results[categoryKey] = 'skipped';
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const srcPath = join(session.tempDir, category.path);
|
|
119
|
+
const destPath = join(dataDir, category.path);
|
|
120
|
+
if (!existsSync(srcPath)) {
|
|
121
|
+
results[categoryKey] = 'skipped';
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
// Close SQLite connections before overwriting
|
|
125
|
+
if (categoryKey === 'agents')
|
|
126
|
+
agentStore.closeDb();
|
|
127
|
+
else if (categoryKey === 'database')
|
|
128
|
+
databaseStore.closeDb();
|
|
129
|
+
else if (categoryKey === 'kanban')
|
|
130
|
+
kanbanStore.closeDb();
|
|
131
|
+
if (statSync(srcPath).isDirectory()) {
|
|
132
|
+
if (existsSync(destPath))
|
|
133
|
+
rmSync(destPath, { recursive: true, force: true });
|
|
134
|
+
cpSync(srcPath, destPath, { recursive: true });
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
cpSync(srcPath, destPath);
|
|
138
|
+
}
|
|
139
|
+
results[categoryKey] = 'ok';
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
console.error(`[data-import] error importing ${categoryKey}:`, e);
|
|
143
|
+
results[categoryKey] = 'error';
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Cleanup
|
|
147
|
+
rmSync(session.tempDir, { recursive: true, force: true });
|
|
148
|
+
activeImportSessions.delete(sessionId);
|
|
149
|
+
res.json({ results });
|
|
150
|
+
});
|
|
151
|
+
// --- In-memory import sessions with auto-cleanup ---
|
|
152
|
+
const activeImportSessions = new Map();
|
|
153
|
+
setInterval(() => {
|
|
154
|
+
const now = Date.now();
|
|
155
|
+
for (const [id, session] of activeImportSessions) {
|
|
156
|
+
if (now > session.expiresAt) {
|
|
157
|
+
rmSync(session.tempDir, { recursive: true, force: true });
|
|
158
|
+
activeImportSessions.delete(id);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}, 10 * 60 * 1000).unref();
|
|
162
|
+
// --- Helpers ---
|
|
163
|
+
function listFilesRecursive(dir) {
|
|
164
|
+
const results = [];
|
|
165
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
166
|
+
if (entry.isDirectory()) {
|
|
167
|
+
results.push(...listFilesRecursive(join(dir, entry.name)));
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
results.push(join(dir, entry.name));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return results;
|
|
174
|
+
}
|
|
175
|
+
function getDirSize(dir) {
|
|
176
|
+
return listFilesRecursive(dir).reduce((sum, f) => sum + statSync(f).size, 0);
|
|
177
|
+
}
|
|
178
|
+
function countFiles(dir) {
|
|
179
|
+
return listFilesRecursive(dir).length;
|
|
180
|
+
}
|
|
181
|
+
function formatSize(bytes) {
|
|
182
|
+
if (bytes < 1024)
|
|
183
|
+
return `${bytes} B`;
|
|
184
|
+
if (bytes < 1024 * 1024)
|
|
185
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
186
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
187
|
+
}
|
|
188
|
+
export default router;
|
|
189
|
+
//# sourceMappingURL=data.js.map
|
package/dist/routes/git.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
|
-
import { gitStatus, gitDiff, gitLog, gitCommit, gitDiscard, gitDiscardAll, gitBranches, gitCheckout, gitInit, gitGenerateCommitMsg, gitPush, gitPull, gitFetch, gitGetRemotes, gitAddRemote, gitCheckoutDetached, gitCherryPick, gitCreateBranch, gitDeleteBranch, gitCreateTag, gitCommitDiff, gitGetRemoteUrl, gitMergeBase, gitGetConfig, gitSetConfig, gitStage, gitUnstage, gitResolveFile } from '../adapters/git.js';
|
|
2
|
+
import { gitStatus, gitDiff, gitLog, gitCommit, gitDiscard, gitDiscardAll, gitBranches, gitCheckout, gitInit, gitGenerateCommitMsg, gitPush, gitPull, gitFetch, gitGetRemotes, gitAddRemote, gitCheckoutDetached, gitCherryPick, gitCreateBranch, gitDeleteBranch, gitCreateTag, gitCommitDiff, gitGetRemoteUrl, gitMergeBase, gitGetConfig, gitSetConfig, gitStage, gitUnstage, gitResolveFile, gitReset } from '../adapters/git.js';
|
|
3
3
|
import { logGitOperation, getGitOperations } from '../services/git-operation-log.js';
|
|
4
4
|
const router = Router({ mergeParams: true });
|
|
5
5
|
function withLog(operation, inputFn, handler) {
|
|
@@ -201,5 +201,14 @@ router.post('/config', withLog('config.set', (req) => ({ name: req.body?.name, e
|
|
|
201
201
|
router.get('/operations', (req, res) => {
|
|
202
202
|
res.json(getGitOperations(req.params.id));
|
|
203
203
|
});
|
|
204
|
+
router.post('/reset', withLog('reset', (req) => ({ commitHash: req.body?.commitHash, mode: req.body?.mode }), async (req, res) => {
|
|
205
|
+
const { commitHash, mode } = req.body;
|
|
206
|
+
if (!commitHash) {
|
|
207
|
+
res.status(400).json({ error: 'commitHash is required' });
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
await gitReset(req.params.id, commitHash, mode || 'mixed');
|
|
211
|
+
res.json({ ok: true });
|
|
212
|
+
}));
|
|
204
213
|
export default router;
|
|
205
214
|
//# sourceMappingURL=git.js.map
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import * as llmStore from '../storage/llm-store.js';
|
|
6
|
+
import { importSkillsBatch } from '../services/skill.js';
|
|
7
|
+
import { importMcps } from '../services/mcp.js';
|
|
8
|
+
const router = Router();
|
|
9
|
+
const CC_SWITCH_DIR = join(homedir(), '.cc-switch');
|
|
10
|
+
const CONFIG_FILE = join(CC_SWITCH_DIR, 'config.json.migrated');
|
|
11
|
+
const SKILLS_DIR = join(CC_SWITCH_DIR, 'skills');
|
|
12
|
+
const MODEL_ENV_KEYS = [
|
|
13
|
+
'ANTHROPIC_MODEL',
|
|
14
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
15
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
16
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
17
|
+
];
|
|
18
|
+
function extractModels(env) {
|
|
19
|
+
const seen = new Set();
|
|
20
|
+
for (const key of MODEL_ENV_KEYS) {
|
|
21
|
+
const val = env[key];
|
|
22
|
+
if (val && !seen.has(val))
|
|
23
|
+
seen.add(val);
|
|
24
|
+
}
|
|
25
|
+
return [...seen];
|
|
26
|
+
}
|
|
27
|
+
function parseProviders(config) {
|
|
28
|
+
const results = [];
|
|
29
|
+
const sections = ['claude', 'codex', 'gemini'];
|
|
30
|
+
const envMappings = {
|
|
31
|
+
claude: { base: 'ANTHROPIC_BASE_URL', key: 'ANTHROPIC_AUTH_TOKEN' },
|
|
32
|
+
codex: { base: 'CODEX_BASE_URL', key: 'OPENAI_API_KEY' },
|
|
33
|
+
gemini: { base: 'GOOGLE_GEMINI_BASE_URL', key: 'GEMINI_API_KEY' },
|
|
34
|
+
};
|
|
35
|
+
for (const section of sections) {
|
|
36
|
+
const providers = config[section]?.providers;
|
|
37
|
+
if (!providers)
|
|
38
|
+
continue;
|
|
39
|
+
const { base: baseKey, key: apiKeyKey } = envMappings[section];
|
|
40
|
+
for (const [id, p] of Object.entries(providers)) {
|
|
41
|
+
const env = p.settingsConfig?.env || {};
|
|
42
|
+
let apiBase = env[baseKey] || '';
|
|
43
|
+
let apiKey = env[apiKeyKey] || '';
|
|
44
|
+
if (section === 'codex') {
|
|
45
|
+
apiKey = apiKey || p.settingsConfig?.auth?.OPENAI_API_KEY || '';
|
|
46
|
+
if (!apiBase) {
|
|
47
|
+
const configStr = p.settingsConfig?.config || '';
|
|
48
|
+
const match = configStr.match(/base_url\s*=\s*"([^"]+)"/);
|
|
49
|
+
if (match)
|
|
50
|
+
apiBase = match[1];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (section === 'claude') {
|
|
54
|
+
apiKey = apiKey || env.ANTHROPIC_API_KEY || '';
|
|
55
|
+
}
|
|
56
|
+
if (!apiKey && !apiBase)
|
|
57
|
+
continue;
|
|
58
|
+
results.push({
|
|
59
|
+
sourceId: id,
|
|
60
|
+
name: p.name || id,
|
|
61
|
+
apiBase,
|
|
62
|
+
apiKey,
|
|
63
|
+
source: section,
|
|
64
|
+
websiteUrl: p.websiteUrl,
|
|
65
|
+
category: p.category,
|
|
66
|
+
models: extractModels(env),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return results;
|
|
71
|
+
}
|
|
72
|
+
function parseSkills() {
|
|
73
|
+
if (!existsSync(SKILLS_DIR))
|
|
74
|
+
return [];
|
|
75
|
+
const results = [];
|
|
76
|
+
const folders = readdirSync(SKILLS_DIR).filter(f => {
|
|
77
|
+
const p = join(SKILLS_DIR, f);
|
|
78
|
+
return statSync(p).isDirectory() && !f.startsWith('_');
|
|
79
|
+
});
|
|
80
|
+
for (const folder of folders) {
|
|
81
|
+
const skillFile = join(SKILLS_DIR, folder, 'SKILL.md');
|
|
82
|
+
if (!existsSync(skillFile))
|
|
83
|
+
continue;
|
|
84
|
+
const content = readFileSync(skillFile, 'utf-8');
|
|
85
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
86
|
+
let description = '';
|
|
87
|
+
if (fmMatch) {
|
|
88
|
+
const descMatch = fmMatch[1].match(/description:\s*["']?(.+?)["']?\s*$/m);
|
|
89
|
+
if (descMatch)
|
|
90
|
+
description = descMatch[1];
|
|
91
|
+
}
|
|
92
|
+
results.push({ name: folder, path: skillFile, description });
|
|
93
|
+
}
|
|
94
|
+
return results;
|
|
95
|
+
}
|
|
96
|
+
function parseMcps(config) {
|
|
97
|
+
const servers = config.mcp?.servers;
|
|
98
|
+
if (!servers)
|
|
99
|
+
return [];
|
|
100
|
+
const results = [];
|
|
101
|
+
for (const [, s] of Object.entries(servers)) {
|
|
102
|
+
if (!s.server?.command)
|
|
103
|
+
continue;
|
|
104
|
+
results.push({
|
|
105
|
+
name: s.name || s.id,
|
|
106
|
+
command: s.server.command,
|
|
107
|
+
args: s.server.args || [],
|
|
108
|
+
env: s.server.env || {},
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return results;
|
|
112
|
+
}
|
|
113
|
+
// GET /api/import/cc-switch/preview
|
|
114
|
+
router.get('/cc-switch/preview', (_req, res) => {
|
|
115
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
116
|
+
res.json({ providers: [], skills: [], mcps: [], error: 'cc-switch config not found' });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const raw = readFileSync(CONFIG_FILE, 'utf-8');
|
|
121
|
+
const config = JSON.parse(raw);
|
|
122
|
+
const providers = parseProviders(config);
|
|
123
|
+
const skills = parseSkills();
|
|
124
|
+
const mcps = parseMcps(config);
|
|
125
|
+
res.json({ providers, skills, mcps });
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
res.status(500).json({ error: e.message });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
// POST /api/import/cc-switch/execute
|
|
132
|
+
router.post('/cc-switch/execute', (req, res) => {
|
|
133
|
+
const { providers: selectedProviders, skills: selectedSkills, mcps: selectedMcps } = req.body;
|
|
134
|
+
const results = { providers: [], models: [], skills: [], mcps: [] };
|
|
135
|
+
// Import providers + models
|
|
136
|
+
if (selectedProviders?.length) {
|
|
137
|
+
const existingProviders = llmStore.listProviders();
|
|
138
|
+
const existingModels = llmStore.listModels();
|
|
139
|
+
for (const p of selectedProviders) {
|
|
140
|
+
if (!p.name)
|
|
141
|
+
continue;
|
|
142
|
+
// skip if provider with same name already exists
|
|
143
|
+
let provider = existingProviders.find(ep => ep.name === p.name);
|
|
144
|
+
if (!provider) {
|
|
145
|
+
provider = llmStore.createProvider({
|
|
146
|
+
name: p.name,
|
|
147
|
+
apiBase: p.apiBase || '',
|
|
148
|
+
apiKey: p.apiKey || '',
|
|
149
|
+
});
|
|
150
|
+
results.providers.push(p.name);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
results.providers.push(`${p.name} (skipped: exists)`);
|
|
154
|
+
}
|
|
155
|
+
// import associated models (deduplicated)
|
|
156
|
+
for (const modelId of p.models) {
|
|
157
|
+
if (existingModels.some(m => m.modelId === modelId && m.provider === provider.id))
|
|
158
|
+
continue;
|
|
159
|
+
llmStore.createModel({
|
|
160
|
+
modelId,
|
|
161
|
+
name: modelId,
|
|
162
|
+
provider: provider.id,
|
|
163
|
+
thinkingEnabled: true,
|
|
164
|
+
thinkingEffort: 'medium',
|
|
165
|
+
vision: false,
|
|
166
|
+
reasoning: false,
|
|
167
|
+
embedding: false,
|
|
168
|
+
});
|
|
169
|
+
results.models.push(modelId);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Import skills
|
|
174
|
+
if (selectedSkills?.length) {
|
|
175
|
+
const items = [];
|
|
176
|
+
for (const s of selectedSkills) {
|
|
177
|
+
const skillFile = join(SKILLS_DIR, s.name, 'SKILL.md');
|
|
178
|
+
if (!existsSync(skillFile))
|
|
179
|
+
continue;
|
|
180
|
+
items.push({ name: s.name, content: readFileSync(skillFile, 'utf-8') });
|
|
181
|
+
}
|
|
182
|
+
if (items.length) {
|
|
183
|
+
const imported = importSkillsBatch(items);
|
|
184
|
+
results.skills = imported.map(s => s.name);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Import MCP servers
|
|
188
|
+
if (selectedMcps?.length) {
|
|
189
|
+
const mcpServers = {};
|
|
190
|
+
for (const m of selectedMcps) {
|
|
191
|
+
mcpServers[m.name] = { command: m.command, args: m.args, env: m.env };
|
|
192
|
+
}
|
|
193
|
+
const imported = importMcps(JSON.stringify(mcpServers));
|
|
194
|
+
results.mcps = imported.map(m => m.name);
|
|
195
|
+
}
|
|
196
|
+
res.json(results);
|
|
197
|
+
});
|
|
198
|
+
export default router;
|
|
199
|
+
//# sourceMappingURL=import.js.map
|
package/dist/routes/issue.js
CHANGED
|
@@ -10,6 +10,7 @@ import { retryIssue } from '../services/issue-retry.js';
|
|
|
10
10
|
import { hasActiveIssueAutomation, runIssueAutomation } from '../agents/issue-agent-runner.js';
|
|
11
11
|
import { continueIssueTaskScheduling, stopIssueAutomation } from '../agents/issue-task-controller.js';
|
|
12
12
|
import * as workflowService from '../services/workflow.js';
|
|
13
|
+
import { scheduleIssueTitleGeneration } from '../services/generated-title.js';
|
|
13
14
|
const router = Router({ mergeParams: true });
|
|
14
15
|
router.get('/', (req, res) => {
|
|
15
16
|
const status = req.query.status;
|
|
@@ -18,13 +19,15 @@ router.get('/', (req, res) => {
|
|
|
18
19
|
});
|
|
19
20
|
router.post('/', (req, res) => {
|
|
20
21
|
const { title, description, members, workflowId } = req.body;
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
const trimmedTitle = typeof title === 'string' ? title.trim() : '';
|
|
23
|
+
const trimmedDescription = typeof description === 'string' ? description.trim() : '';
|
|
24
|
+
if (!trimmedTitle && !trimmedDescription) {
|
|
25
|
+
res.status(400).json({ error: 'title or description is required' });
|
|
23
26
|
return;
|
|
24
27
|
}
|
|
25
28
|
const issue = issueService.create(req.params.id, {
|
|
26
|
-
title,
|
|
27
|
-
description:
|
|
29
|
+
title: trimmedTitle,
|
|
30
|
+
description: trimmedDescription,
|
|
28
31
|
members: mergeWorkflowMembers(members, workflowId),
|
|
29
32
|
workflowId,
|
|
30
33
|
});
|
|
@@ -33,6 +36,15 @@ router.post('/', (req, res) => {
|
|
|
33
36
|
broadcastToWorkspace(req.params.id, 'channel.updated', channel);
|
|
34
37
|
broadcastToWorkspace(req.params.id, 'issue.created', issue);
|
|
35
38
|
res.status(201).json(issue);
|
|
39
|
+
if (!trimmedTitle) {
|
|
40
|
+
scheduleIssueTitleGeneration({
|
|
41
|
+
workspaceId: req.params.id,
|
|
42
|
+
issueId: issue.id,
|
|
43
|
+
requirement: trimmedDescription,
|
|
44
|
+
description: trimmedDescription,
|
|
45
|
+
broadcast: (event, data) => broadcastToWorkspace(req.params.id, event, data),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
36
48
|
});
|
|
37
49
|
router.get('/:issueId', (req, res) => {
|
|
38
50
|
const issue = issueService.getById(req.params.id, req.params.issueId);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import * as pluginService from '../services/plugin.js';
|
|
3
|
+
const router = Router();
|
|
4
|
+
router.get('/', (_req, res) => {
|
|
5
|
+
try {
|
|
6
|
+
res.json(pluginService.listPlugins());
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
res.status(500).json({ error: error.message });
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
router.get('/workflow', (_req, res) => {
|
|
13
|
+
try {
|
|
14
|
+
res.json(pluginService.listWorkflowPlugins());
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
res.status(500).json({ error: error.message });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
router.post('/store/:pluginId/install', (req, res) => {
|
|
21
|
+
try {
|
|
22
|
+
res.json(pluginService.installTemplatePlugin(req.params.pluginId));
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
res.status(400).json({ error: error.message });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
router.post('/:pluginId/enable', (req, res) => {
|
|
29
|
+
try {
|
|
30
|
+
res.json(pluginService.setPluginEnabled(req.params.pluginId, true));
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
res.status(400).json({ error: error.message });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
router.post('/:pluginId/disable', (req, res) => {
|
|
37
|
+
try {
|
|
38
|
+
res.json(pluginService.setPluginEnabled(req.params.pluginId, false));
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
res.status(400).json({ error: error.message });
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
router.get('/:pluginId/config', (req, res) => {
|
|
45
|
+
try {
|
|
46
|
+
res.json(pluginService.getPluginConfig(req.params.pluginId));
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
res.status(400).json({ error: error.message });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
router.put('/:pluginId/config', (req, res) => {
|
|
53
|
+
try {
|
|
54
|
+
res.json(pluginService.savePluginConfig(req.params.pluginId, req.body || {}));
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
res.status(400).json({ success: false, error: error.message });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
router.get('/:pluginId/workflow-nodes', (req, res) => {
|
|
61
|
+
try {
|
|
62
|
+
res.json({ pluginId: req.params.pluginId, nodes: pluginService.getWorkflowNodes(req.params.pluginId) });
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
res.status(400).json({ error: error.message });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
export default router;
|
|
69
|
+
//# sourceMappingURL=plugin.js.map
|
package/dist/routes/version.js
CHANGED
|
@@ -5,7 +5,8 @@ import { tmpdir } from 'node:os';
|
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { getLocalVersion, getCachedLatest, fetchLatestVersion, isNewerVersion } from '../services/version.js';
|
|
7
7
|
const router = Router();
|
|
8
|
-
const
|
|
8
|
+
const isSourceRuntime = /[\\/]src[\\/]routes$/.test(import.meta.dirname);
|
|
9
|
+
const isDev = process.env.NODE_ENV === 'development' || (!process.env.NODE_ENV && isSourceRuntime);
|
|
9
10
|
let isUpdating = false;
|
|
10
11
|
// GET /version — cached version info (openPaths in auth.ts)
|
|
11
12
|
router.get('/version', async (_req, res) => {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Workflow webhook hook handler — SSE streaming of execution results
|
|
2
|
+
import { Router } from 'express';
|
|
3
|
+
const SSE_TIMEOUT_MS = 5 * 60_000;
|
|
4
|
+
export function createWorkflowHookRouter(triggerService, executionManager) {
|
|
5
|
+
const router = Router();
|
|
6
|
+
router.post('/hook/:hookName', (req, res) => {
|
|
7
|
+
const hookName = req.params.hookName;
|
|
8
|
+
const bindings = triggerService.getHookBindings(hookName);
|
|
9
|
+
if (bindings.length === 0) {
|
|
10
|
+
res.status(404).json({ error: `No workflows bound to hook "${hookName}"` });
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const body = req.body || {};
|
|
14
|
+
let targets = bindings;
|
|
15
|
+
if (body.workflowId) {
|
|
16
|
+
targets = bindings.filter(b => b.workflowId === body.workflowId);
|
|
17
|
+
if (targets.length === 0) {
|
|
18
|
+
res.status(404).json({ error: `Workflow ${body.workflowId} not bound to hook "${hookName}"` });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
res.writeHead(200, {
|
|
23
|
+
'Content-Type': 'text/event-stream',
|
|
24
|
+
'Cache-Control': 'no-cache',
|
|
25
|
+
'Connection': 'keep-alive',
|
|
26
|
+
'X-Accel-Buffering': 'no',
|
|
27
|
+
});
|
|
28
|
+
let completed = 0;
|
|
29
|
+
let closed = false;
|
|
30
|
+
const sse = (event, payload) => {
|
|
31
|
+
if (closed)
|
|
32
|
+
return;
|
|
33
|
+
res.write(`event: ${event}\ndata: ${JSON.stringify(payload)}\n\n`);
|
|
34
|
+
};
|
|
35
|
+
const timeoutId = setTimeout(() => {
|
|
36
|
+
if (!closed) {
|
|
37
|
+
sse('timeout', { message: 'SSE timeout, closing' });
|
|
38
|
+
res.write('event: done\ndata: {}\n\n');
|
|
39
|
+
closed = true;
|
|
40
|
+
res.end();
|
|
41
|
+
}
|
|
42
|
+
}, SSE_TIMEOUT_MS);
|
|
43
|
+
res.on('close', () => {
|
|
44
|
+
closed = true;
|
|
45
|
+
clearTimeout(timeoutId);
|
|
46
|
+
});
|
|
47
|
+
const total = targets.length;
|
|
48
|
+
for (const binding of targets) {
|
|
49
|
+
executionManager.execute({ workflowId: binding.workflowId, input: body.input || {} }, '__hook__', (channel, payload) => sse(channel, payload)).then(() => {
|
|
50
|
+
completed++;
|
|
51
|
+
if (completed === total && !closed) {
|
|
52
|
+
clearTimeout(timeoutId);
|
|
53
|
+
res.write('event: done\ndata: {}\n\n');
|
|
54
|
+
closed = true;
|
|
55
|
+
res.end();
|
|
56
|
+
}
|
|
57
|
+
}).catch((err) => {
|
|
58
|
+
sse('workflow:error', { workflowId: binding.workflowId, error: err.message });
|
|
59
|
+
completed++;
|
|
60
|
+
if (completed === total && !closed) {
|
|
61
|
+
clearTimeout(timeoutId);
|
|
62
|
+
res.write('event: done\ndata: {}\n\n');
|
|
63
|
+
closed = true;
|
|
64
|
+
res.end();
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
return router;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=workflow-hook.js.map
|