@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.
Files changed (64) hide show
  1. package/dist/adapters/agent-runtime.js +4 -0
  2. package/dist/adapters/codex-function-tool-bridge.js +9 -1
  3. package/dist/adapters/git.js +13 -2
  4. package/dist/adapters/hermes-runtime.js +711 -108
  5. package/dist/adapters/langchain-runtime.js +384 -21
  6. package/dist/adapters/oh-my-pi-runtime.js +858 -0
  7. package/dist/adapters/open-agent-sdk-runtime.js +202 -5
  8. package/dist/adapters/open-agent-sdk-runtime.test.js +35 -0
  9. package/dist/agents/agent-message-parts.js +52 -2
  10. package/dist/agents/issue-task-controller.js +4 -2
  11. package/dist/agents/title-generator-agent.js +120 -0
  12. package/dist/app.js +59 -6
  13. package/dist/routes/agent-sse.js +70 -7
  14. package/dist/routes/channel.js +23 -2
  15. package/dist/routes/chat-run.js +191 -0
  16. package/dist/routes/chat.js +142 -0
  17. package/dist/routes/command.js +16 -0
  18. package/dist/routes/data.js +189 -0
  19. package/dist/routes/git.js +10 -1
  20. package/dist/routes/import.js +199 -0
  21. package/dist/routes/issue.js +16 -4
  22. package/dist/routes/plugin.js +69 -0
  23. package/dist/routes/version.js +2 -1
  24. package/dist/routes/workflow-hook.js +71 -0
  25. package/dist/routes/workflow.js +282 -7
  26. package/dist/routes/workspace.js +13 -4
  27. package/dist/services/agent.js +123 -36
  28. package/dist/services/ai-text.js +185 -0
  29. package/dist/services/builtin-tools/index.js +1 -0
  30. package/dist/services/builtin-tools/workflow-editor-tools.js +509 -0
  31. package/dist/services/builtin-tools/workflow-exec-tools.js +320 -0
  32. package/dist/services/chat.js +134 -0
  33. package/dist/services/command-process-manager.js +16 -0
  34. package/dist/services/execution-manager.js +1346 -0
  35. package/dist/services/generated-title.js +59 -0
  36. package/dist/services/gitignore.js +22 -18
  37. package/dist/services/interaction-manager.js +114 -0
  38. package/dist/services/issue-retry.js +25 -0
  39. package/dist/services/output-style.js +8 -1
  40. package/dist/services/plugin.js +257 -0
  41. package/dist/services/prompt-template.js +8 -1
  42. package/dist/services/pty.js +20 -0
  43. package/dist/services/search.js +16 -6
  44. package/dist/services/skill.js +2 -0
  45. package/dist/services/version.js +17 -7
  46. package/dist/services/workflow-command-runner.js +5 -4
  47. package/dist/services/workflow-trigger-service.js +137 -0
  48. package/dist/services/workflow.js +199 -36
  49. package/dist/storage/agent-store.js +8 -0
  50. package/dist/storage/chat-store.js +151 -0
  51. package/dist/storage/database-store.js +6 -0
  52. package/dist/storage/json-store.js +2 -1
  53. package/dist/storage/kanban-store.js +6 -0
  54. package/dist/storage/workflow-store.js +386 -22
  55. package/dist/ws/agent-prompt.js +6 -2
  56. package/dist/ws/agent-runner.js +29 -18
  57. package/dist/ws/chat-handler.js +89 -0
  58. package/dist/ws/connection-manager.js +49 -2
  59. package/dist/ws/execution-channels.js +88 -0
  60. package/dist/ws/handler.js +32 -5
  61. package/dist/ws/message-parts.js +130 -12
  62. package/dist/ws/terminal-handler.js +26 -0
  63. package/package.json +12 -1
  64. 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
@@ -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
@@ -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
- if (!title) {
22
- res.status(400).json({ error: 'title is required' });
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: 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
@@ -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 isDev = !process.env.NODE_ENV || process.env.NODE_ENV === 'development';
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