@intrect/openswarm 0.15.0 → 0.17.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/README.md +4 -3
- package/dist/adapters/agenticLoop.d.ts +2 -0
- package/dist/adapters/agenticLoop.d.ts.map +1 -1
- package/dist/adapters/agenticLoop.js +8 -5
- package/dist/adapters/agenticLoop.js.map +1 -1
- package/dist/adapters/codexResponses.d.ts.map +1 -1
- package/dist/adapters/codexResponses.js +1 -0
- package/dist/adapters/codexResponses.js.map +1 -1
- package/dist/adapters/gpt.d.ts.map +1 -1
- package/dist/adapters/gpt.js +1 -0
- package/dist/adapters/gpt.js.map +1 -1
- package/dist/adapters/local.d.ts.map +1 -1
- package/dist/adapters/local.js +1 -0
- package/dist/adapters/local.js.map +1 -1
- package/dist/adapters/openrouter.d.ts.map +1 -1
- package/dist/adapters/openrouter.js +1 -0
- package/dist/adapters/openrouter.js.map +1 -1
- package/dist/adapters/tools.d.ts +2 -0
- package/dist/adapters/tools.d.ts.map +1 -1
- package/dist/adapters/tools.js +7 -0
- package/dist/adapters/tools.js.map +1 -1
- package/dist/adapters/types.d.ts +2 -0
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/automation/autonomousRunner.d.ts +5 -5
- package/dist/automation/autonomousRunner.d.ts.map +1 -1
- package/dist/automation/autonomousRunner.js +7 -9
- package/dist/automation/autonomousRunner.js.map +1 -1
- package/dist/automation/ciWorker.d.ts.map +1 -1
- package/dist/automation/ciWorker.js +16 -11
- package/dist/automation/ciWorker.js.map +1 -1
- package/dist/automation/dailyReporter.d.ts.map +1 -1
- package/dist/automation/dailyReporter.js +7 -2
- package/dist/automation/dailyReporter.js.map +1 -1
- package/dist/automation/longRunningMonitor.d.ts.map +1 -1
- package/dist/automation/longRunningMonitor.js +17 -27
- package/dist/automation/longRunningMonitor.js.map +1 -1
- package/dist/automation/prProcessor.d.ts.map +1 -1
- package/dist/automation/prProcessor.js +103 -24
- package/dist/automation/prProcessor.js.map +1 -1
- package/dist/automation/runnerState.d.ts +0 -6
- package/dist/automation/runnerState.d.ts.map +1 -1
- package/dist/automation/runnerState.js +16 -31
- package/dist/automation/runnerState.js.map +1 -1
- package/dist/automation/runnerTypes.d.ts +0 -2
- package/dist/automation/runnerTypes.d.ts.map +1 -1
- package/dist/automation/scheduler.d.ts.map +1 -1
- package/dist/automation/scheduler.js +76 -76
- package/dist/automation/scheduler.js.map +1 -1
- package/dist/automation/taskSource.d.ts +1 -1
- package/dist/automation/taskSource.d.ts.map +1 -1
- package/dist/automation/taskSource.js +95 -8
- package/dist/automation/taskSource.js.map +1 -1
- package/dist/automation/workerAuditLog.d.ts.map +1 -1
- package/dist/automation/workerAuditLog.js +4 -1
- package/dist/automation/workerAuditLog.js.map +1 -1
- package/dist/cli/authHandler.d.ts.map +1 -1
- package/dist/cli/authHandler.js +10 -12
- package/dist/cli/authHandler.js.map +1 -1
- package/dist/cli/checkHandler.d.ts.map +1 -1
- package/dist/cli/checkHandler.js +12 -10
- package/dist/cli/checkHandler.js.map +1 -1
- package/dist/cli/daemon.d.ts.map +1 -1
- package/dist/cli/daemon.js +24 -9
- package/dist/cli/daemon.js.map +1 -1
- package/dist/cli/fixCommand.d.ts.map +1 -1
- package/dist/cli/fixCommand.js +10 -0
- package/dist/cli/fixCommand.js.map +1 -1
- package/dist/cli/initWizard.d.ts.map +1 -1
- package/dist/cli/initWizard.js +6 -1
- package/dist/cli/initWizard.js.map +1 -1
- package/dist/cli/projectHandler.d.ts.map +1 -1
- package/dist/cli/projectHandler.js +3 -2
- package/dist/cli/projectHandler.js.map +1 -1
- package/dist/cli/promptHandler.d.ts.map +1 -1
- package/dist/cli/promptHandler.js +21 -10
- package/dist/cli/promptHandler.js.map +1 -1
- package/dist/cli/reviewAudit.d.ts +2 -2
- package/dist/cli/reviewAudit.js +3 -3
- package/dist/cli/reviewAudit.js.map +1 -1
- package/dist/cli/reviewMaxCommand.d.ts.map +1 -1
- package/dist/cli/reviewMaxCommand.js +10 -2
- package/dist/cli/reviewMaxCommand.js.map +1 -1
- package/dist/cli/scheduleCommand.d.ts.map +1 -1
- package/dist/cli/scheduleCommand.js +20 -5
- package/dist/cli/scheduleCommand.js.map +1 -1
- package/dist/cli.js +15 -4
- package/dist/cli.js.map +1 -1
- package/dist/core/config.d.ts +33 -14
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +18 -3
- package/dist/core/config.js.map +1 -1
- package/dist/core/service.d.ts.map +1 -1
- package/dist/core/service.js +0 -2
- package/dist/core/service.js.map +1 -1
- package/dist/core/types.d.ts +0 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/discord/discordCore.d.ts.map +1 -1
- package/dist/discord/discordCore.js +20 -4
- package/dist/discord/discordCore.js.map +1 -1
- package/dist/discord/discordPair.js +12 -0
- package/dist/discord/discordPair.js.map +1 -1
- package/dist/github/github.d.ts +1 -1
- package/dist/github/github.d.ts.map +1 -1
- package/dist/github/github.js +41 -11
- package/dist/github/github.js.map +1 -1
- package/dist/index.js +15 -6
- package/dist/index.js.map +1 -1
- package/dist/issues/graphql/resolvers.d.ts +4 -4
- package/dist/issues/graphql/resolvers.d.ts.map +1 -1
- package/dist/issues/graphql/resolvers.js +43 -3
- package/dist/issues/graphql/resolvers.js.map +1 -1
- package/dist/issues/graphql/server.d.ts.map +1 -1
- package/dist/issues/graphql/server.js +76 -5
- package/dist/issues/graphql/server.js.map +1 -1
- package/dist/issues/graphql/typeDefs.d.ts +1 -1
- package/dist/issues/graphql/typeDefs.d.ts.map +1 -1
- package/dist/issues/graphql/typeDefs.js +0 -5
- package/dist/issues/graphql/typeDefs.js.map +1 -1
- package/dist/issues/issueBoardHtml.d.ts +1 -1
- package/dist/issues/issueBoardHtml.d.ts.map +1 -1
- package/dist/issues/issueBoardHtml.js +40 -20
- package/dist/issues/issueBoardHtml.js.map +1 -1
- package/dist/issues/linearBridge.d.ts +1 -1
- package/dist/issues/linearBridge.d.ts.map +1 -1
- package/dist/issues/linearBridge.js +14 -5
- package/dist/issues/linearBridge.js.map +1 -1
- package/dist/issues/memoryBridge.d.ts.map +1 -1
- package/dist/issues/memoryBridge.js +23 -11
- package/dist/issues/memoryBridge.js.map +1 -1
- package/dist/issues/schema.d.ts +3 -3
- package/dist/issues/sqliteStore.d.ts +3 -0
- package/dist/issues/sqliteStore.d.ts.map +1 -1
- package/dist/issues/sqliteStore.js +124 -19
- package/dist/issues/sqliteStore.js.map +1 -1
- package/dist/knowledge/analyzer.d.ts.map +1 -1
- package/dist/knowledge/analyzer.js +13 -1
- package/dist/knowledge/analyzer.js.map +1 -1
- package/dist/knowledge/graphqlExporter.d.ts.map +1 -1
- package/dist/knowledge/graphqlExporter.js +38 -9
- package/dist/knowledge/graphqlExporter.js.map +1 -1
- package/dist/knowledge/scanner.d.ts.map +1 -1
- package/dist/knowledge/scanner.js +69 -26
- package/dist/knowledge/scanner.js.map +1 -1
- package/dist/linear/linear.d.ts.map +1 -1
- package/dist/linear/linear.js +11 -11
- package/dist/linear/linear.js.map +1 -1
- package/dist/linear/projectUpdater.js +12 -2
- package/dist/linear/projectUpdater.js.map +1 -1
- package/dist/locale/en.d.ts.map +1 -1
- package/dist/locale/en.js +4 -0
- package/dist/locale/en.js.map +1 -1
- package/dist/locale/index.d.ts +7 -2
- package/dist/locale/index.d.ts.map +1 -1
- package/dist/locale/index.js.map +1 -1
- package/dist/locale/ko.d.ts.map +1 -1
- package/dist/locale/ko.js +4 -0
- package/dist/locale/ko.js.map +1 -1
- package/dist/locale/prompts/en.d.ts.map +1 -1
- package/dist/locale/prompts/en.js +111 -42
- package/dist/locale/prompts/en.js.map +1 -1
- package/dist/locale/prompts/ko.d.ts.map +1 -1
- package/dist/locale/prompts/ko.js +111 -42
- package/dist/locale/prompts/ko.js.map +1 -1
- package/dist/locale/types.d.ts +4 -0
- package/dist/locale/types.d.ts.map +1 -1
- package/dist/mcp/mcpClient.d.ts.map +1 -1
- package/dist/mcp/mcpClient.js +68 -6
- package/dist/mcp/mcpClient.js.map +1 -1
- package/dist/mcp/memoryServer.js +3 -0
- package/dist/mcp/memoryServer.js.map +1 -1
- package/dist/memory/codex.d.ts.map +1 -1
- package/dist/memory/codex.js +3 -2
- package/dist/memory/codex.js.map +1 -1
- package/dist/memory/compaction.d.ts.map +1 -1
- package/dist/memory/compaction.js +36 -6
- package/dist/memory/compaction.js.map +1 -1
- package/dist/memory/memoryCore.d.ts +1 -0
- package/dist/memory/memoryCore.d.ts.map +1 -1
- package/dist/memory/memoryCore.js +43 -25
- package/dist/memory/memoryCore.js.map +1 -1
- package/dist/memory/memoryOps.d.ts.map +1 -1
- package/dist/memory/memoryOps.js +54 -58
- package/dist/memory/memoryOps.js.map +1 -1
- package/dist/notify/notifier.d.ts.map +1 -1
- package/dist/notify/notifier.js +31 -7
- package/dist/notify/notifier.js.map +1 -1
- package/dist/orchestration/conflictDetector.d.ts.map +1 -1
- package/dist/orchestration/conflictDetector.js +15 -4
- package/dist/orchestration/conflictDetector.js.map +1 -1
- package/dist/orchestration/decisionEngine.d.ts +32 -2
- package/dist/orchestration/decisionEngine.d.ts.map +1 -1
- package/dist/orchestration/decisionEngine.js +80 -41
- package/dist/orchestration/decisionEngine.js.map +1 -1
- package/dist/orchestration/taskParser.d.ts.map +1 -1
- package/dist/orchestration/taskParser.js +21 -3
- package/dist/orchestration/taskParser.js.map +1 -1
- package/dist/orchestration/taskScheduler.d.ts.map +1 -1
- package/dist/orchestration/taskScheduler.js +47 -19
- package/dist/orchestration/taskScheduler.js.map +1 -1
- package/dist/orchestration/workflow.d.ts.map +1 -1
- package/dist/orchestration/workflow.js +22 -5
- package/dist/orchestration/workflow.js.map +1 -1
- package/dist/registry/bsDetector.js +2 -2
- package/dist/registry/bsDetector.js.map +1 -1
- package/dist/registry/entityScanner.d.ts.map +1 -1
- package/dist/registry/entityScanner.js +25 -6
- package/dist/registry/entityScanner.js.map +1 -1
- package/dist/registry/graphql/resolvers.d.ts +23 -8
- package/dist/registry/graphql/resolvers.d.ts.map +1 -1
- package/dist/registry/graphql/resolvers.js +126 -32
- package/dist/registry/graphql/resolvers.js.map +1 -1
- package/dist/registry/graphql/typeDefs.d.ts +1 -1
- package/dist/registry/graphql/typeDefs.d.ts.map +1 -1
- package/dist/registry/graphql/typeDefs.js +9 -8
- package/dist/registry/graphql/typeDefs.js.map +1 -1
- package/dist/registry/issueBridge.d.ts +1 -1
- package/dist/registry/issueBridge.d.ts.map +1 -1
- package/dist/registry/issueBridge.js +2 -2
- package/dist/registry/issueBridge.js.map +1 -1
- package/dist/registry/sqliteStore.d.ts +2 -2
- package/dist/registry/sqliteStore.d.ts.map +1 -1
- package/dist/registry/sqliteStore.js +24 -10
- package/dist/registry/sqliteStore.js.map +1 -1
- package/dist/runners/cliRunner.d.ts.map +1 -1
- package/dist/runners/cliRunner.js +42 -19
- package/dist/runners/cliRunner.js.map +1 -1
- package/dist/support/apiCache.d.ts +15 -1
- package/dist/support/apiCache.d.ts.map +1 -1
- package/dist/support/apiCache.js +42 -7
- package/dist/support/apiCache.js.map +1 -1
- package/dist/support/chatBackend.d.ts.map +1 -1
- package/dist/support/chatBackend.js +2 -3
- package/dist/support/chatBackend.js.map +1 -1
- package/dist/support/chatSession.d.ts.map +1 -1
- package/dist/support/chatSession.js +21 -3
- package/dist/support/chatSession.js.map +1 -1
- package/dist/support/dashboardHtml.d.ts +1 -1
- package/dist/support/dashboardHtml.d.ts.map +1 -1
- package/dist/support/dashboardHtml.js +19 -14
- package/dist/support/dashboardHtml.js.map +1 -1
- package/dist/support/delete-beliefs.d.ts +3 -1
- package/dist/support/delete-beliefs.d.ts.map +1 -1
- package/dist/support/delete-beliefs.js +18 -7
- package/dist/support/delete-beliefs.js.map +1 -1
- package/dist/support/editParser.d.ts.map +1 -1
- package/dist/support/editParser.js +46 -12
- package/dist/support/editParser.js.map +1 -1
- package/dist/support/gitTracker.d.ts +2 -2
- package/dist/support/gitTracker.d.ts.map +1 -1
- package/dist/support/gitTracker.js +10 -5
- package/dist/support/gitTracker.js.map +1 -1
- package/dist/support/planner.d.ts.map +1 -1
- package/dist/support/planner.js +1 -0
- package/dist/support/planner.js.map +1 -1
- package/dist/support/projectMapper.d.ts.map +1 -1
- package/dist/support/projectMapper.js +21 -16
- package/dist/support/projectMapper.js.map +1 -1
- package/dist/support/rateLimiter.d.ts.map +1 -1
- package/dist/support/rateLimiter.js +3 -0
- package/dist/support/rateLimiter.js.map +1 -1
- package/dist/support/rollback.d.ts.map +1 -1
- package/dist/support/rollback.js +57 -8
- package/dist/support/rollback.js.map +1 -1
- package/dist/support/stuckDetector.d.ts.map +1 -1
- package/dist/support/stuckDetector.js +8 -6
- package/dist/support/stuckDetector.js.map +1 -1
- package/dist/support/timeWindow.d.ts +1 -1
- package/dist/support/timeWindow.d.ts.map +1 -1
- package/dist/support/timeWindow.js +18 -7
- package/dist/support/timeWindow.js.map +1 -1
- package/dist/support/web.d.ts.map +1 -1
- package/dist/support/web.js +872 -791
- package/dist/support/web.js.map +1 -1
- package/dist/support/workflowLinear.d.ts.map +1 -1
- package/dist/support/workflowLinear.js +8 -6
- package/dist/support/workflowLinear.js.map +1 -1
- package/dist/support/worktreeManager.d.ts.map +1 -1
- package/dist/support/worktreeManager.js +33 -9
- package/dist/support/worktreeManager.js.map +1 -1
- package/dist/taskState/store.d.ts +7 -2
- package/dist/taskState/store.d.ts.map +1 -1
- package/dist/taskState/store.js +77 -21
- package/dist/taskState/store.js.map +1 -1
- package/dist/telemetry/telemetry.d.ts.map +1 -1
- package/dist/telemetry/telemetry.js +18 -4
- package/dist/telemetry/telemetry.js.map +1 -1
- package/dist/tui/components/ChatInput.d.ts +1 -0
- package/dist/tui/components/ChatInput.d.ts.map +1 -1
- package/dist/tui/components/ChatInput.js +14 -1
- package/dist/tui/components/ChatInput.js.map +1 -1
- package/dist/tui/components/SubagentTree.d.ts.map +1 -1
- package/dist/tui/components/SubagentTree.js +14 -2
- package/dist/tui/components/SubagentTree.js.map +1 -1
- package/dist/tui/hooks/useMonitor.d.ts.map +1 -1
- package/dist/tui/hooks/useMonitor.js +5 -0
- package/dist/tui/hooks/useMonitor.js.map +1 -1
- package/dist/tui/hooks/usePipelineEvents.d.ts.map +1 -1
- package/dist/tui/hooks/usePipelineEvents.js +12 -2
- package/dist/tui/hooks/usePipelineEvents.js.map +1 -1
- package/dist/tui/markdown.d.ts.map +1 -1
- package/dist/tui/markdown.js +11 -3
- package/dist/tui/markdown.js.map +1 -1
- package/dist/tui/monitorApi.d.ts.map +1 -1
- package/dist/tui/monitorApi.js +21 -0
- package/dist/tui/monitorApi.js.map +1 -1
- package/dist/tui/monitorRows.d.ts +2 -2
- package/dist/tui/monitorRows.d.ts.map +1 -1
- package/dist/tui/monitorRows.js +7 -1
- package/dist/tui/monitorRows.js.map +1 -1
- package/dist/tui/panels/ChatPanel.d.ts.map +1 -1
- package/dist/tui/panels/ChatPanel.js +30 -3
- package/dist/tui/panels/ChatPanel.js.map +1 -1
- package/dist/tui/sse.d.ts +1 -0
- package/dist/tui/sse.d.ts.map +1 -1
- package/dist/tui/sse.js +6 -1
- package/dist/tui/sse.js.map +1 -1
- package/dist/tui/subagentTree.d.ts.map +1 -1
- package/dist/tui/subagentTree.js +16 -5
- package/dist/tui/subagentTree.js.map +1 -1
- package/dist/tui/tabs.d.ts.map +1 -1
- package/dist/tui/tabs.js +7 -1
- package/dist/tui/tabs.js.map +1 -1
- package/package.json +1 -1
package/dist/support/web.js
CHANGED
|
@@ -27,6 +27,14 @@ import { ISSUE_BOARD_HTML } from '../issues/issueBoardHtml.js';
|
|
|
27
27
|
import { createSubIssuesWithDependencies, getTaskSource } from '../automation/runnerExecution.js';
|
|
28
28
|
let server = null;
|
|
29
29
|
let runnerRef;
|
|
30
|
+
const MAX_REQUEST_BODY_BYTES = 1024 * 1024;
|
|
31
|
+
class HttpError extends Error {
|
|
32
|
+
statusCode;
|
|
33
|
+
constructor(statusCode, message) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.statusCode = statusCode;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
30
38
|
// CORS origin allowlist — hostname-strict match (no substring/prefix pitfalls)
|
|
31
39
|
function isAllowedOrigin(origin) {
|
|
32
40
|
let url;
|
|
@@ -61,6 +69,38 @@ function safeErrorMessage(err) {
|
|
|
61
69
|
}
|
|
62
70
|
return 'Internal error';
|
|
63
71
|
}
|
|
72
|
+
function isLoopbackAddress(address) {
|
|
73
|
+
return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1';
|
|
74
|
+
}
|
|
75
|
+
function extractBearerToken(header) {
|
|
76
|
+
if (!header)
|
|
77
|
+
return null;
|
|
78
|
+
// Linear-time parse (no regex): 'Bearer' + one space/tab + token. A
|
|
79
|
+
// backtracking /^Bearer\s+(.+)$/ is polynomial on adversarial whitespace runs.
|
|
80
|
+
const prefix = header.slice(0, 7).toLowerCase();
|
|
81
|
+
if (prefix !== 'bearer ' && prefix !== 'bearer\t')
|
|
82
|
+
return null;
|
|
83
|
+
return header.slice(7).trim() || null;
|
|
84
|
+
}
|
|
85
|
+
function isAuthorizedMutation(req) {
|
|
86
|
+
if (isLoopbackAddress(req.socket.remoteAddress))
|
|
87
|
+
return true;
|
|
88
|
+
const configuredToken = process.env.OPENSWARM_WEB_TOKEN?.trim();
|
|
89
|
+
if (!configuredToken)
|
|
90
|
+
return false;
|
|
91
|
+
const presentedToken = extractBearerToken(req.headers.authorization) ||
|
|
92
|
+
(Array.isArray(req.headers['x-openswarm-token'])
|
|
93
|
+
? req.headers['x-openswarm-token'][0]
|
|
94
|
+
: req.headers['x-openswarm-token']);
|
|
95
|
+
return presentedToken === configuredToken;
|
|
96
|
+
}
|
|
97
|
+
function isMutatingApiRequest(pathname, method) {
|
|
98
|
+
return pathname.startsWith('/api/') && ['DELETE', 'PATCH', 'POST', 'PUT'].includes(method ?? '');
|
|
99
|
+
}
|
|
100
|
+
function writeJson(res, statusCode, body) {
|
|
101
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
102
|
+
res.end(JSON.stringify(body));
|
|
103
|
+
}
|
|
64
104
|
const execTasks = new Map();
|
|
65
105
|
function cleanupExecTask(taskId) {
|
|
66
106
|
setTimeout(() => { execTasks.delete(taskId); }, 3600000); // 1 hour
|
|
@@ -244,10 +284,34 @@ export function setWebRunner(runner) {
|
|
|
244
284
|
}
|
|
245
285
|
// Read POST body helper
|
|
246
286
|
function readBody(req) {
|
|
247
|
-
return new Promise((resolve) => {
|
|
287
|
+
return new Promise((resolve, reject) => {
|
|
248
288
|
let data = '';
|
|
249
|
-
|
|
250
|
-
|
|
289
|
+
let totalBytes = 0;
|
|
290
|
+
let settled = false;
|
|
291
|
+
const fail = (statusCode, message) => {
|
|
292
|
+
if (settled)
|
|
293
|
+
return;
|
|
294
|
+
settled = true;
|
|
295
|
+
reject(new HttpError(statusCode, message));
|
|
296
|
+
};
|
|
297
|
+
req.on('data', (chunk) => {
|
|
298
|
+
if (settled)
|
|
299
|
+
return;
|
|
300
|
+
totalBytes += chunk.length;
|
|
301
|
+
if (totalBytes > MAX_REQUEST_BODY_BYTES) {
|
|
302
|
+
fail(413, 'Request body too large');
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
data += chunk.toString('utf-8');
|
|
306
|
+
});
|
|
307
|
+
req.on('end', () => {
|
|
308
|
+
if (settled)
|
|
309
|
+
return;
|
|
310
|
+
settled = true;
|
|
311
|
+
resolve(data);
|
|
312
|
+
});
|
|
313
|
+
req.on('aborted', () => fail(400, 'Request body aborted'));
|
|
314
|
+
req.on('error', () => fail(400, 'Request body error'));
|
|
251
315
|
});
|
|
252
316
|
}
|
|
253
317
|
// Start web server
|
|
@@ -258,890 +322,907 @@ export async function startWebServer(port = 3847) {
|
|
|
258
322
|
}
|
|
259
323
|
return new Promise((resolve, reject) => {
|
|
260
324
|
server = createServer(async (req, res) => {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
await handleGraphQL(req, res);
|
|
271
|
-
// ---- Issue Board ----
|
|
272
|
-
}
|
|
273
|
-
else if (url === '/issues') {
|
|
274
|
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
275
|
-
res.end(ISSUE_BOARD_HTML);
|
|
276
|
-
// ---- Dashboard ----
|
|
277
|
-
}
|
|
278
|
-
else if (url === '/' || url === '/index.html') {
|
|
279
|
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
280
|
-
res.end(DASHBOARD_HTML);
|
|
281
|
-
// ---- SSE stream ----
|
|
282
|
-
}
|
|
283
|
-
else if (url === '/api/events') {
|
|
284
|
-
const skipReplay = req.url?.includes('skipReplay=1') ?? false;
|
|
285
|
-
res.writeHead(200, {
|
|
286
|
-
'Content-Type': 'text/event-stream',
|
|
287
|
-
'Cache-Control': 'no-cache',
|
|
288
|
-
'Connection': 'keep-alive',
|
|
289
|
-
});
|
|
290
|
-
res.write(':connected\n\n');
|
|
291
|
-
addSSEClient(res, skipReplay);
|
|
292
|
-
// ---- Stats ----
|
|
293
|
-
}
|
|
294
|
-
else if (url === '/api/stats') {
|
|
295
|
-
const stats = runnerRef?.getStats();
|
|
296
|
-
const state = runnerRef?.getState();
|
|
297
|
-
const adapters = runnerRef?.getAdapterSummary();
|
|
298
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
299
|
-
res.end(JSON.stringify({
|
|
300
|
-
runningTasks: stats?.schedulerStats?.running ?? 0,
|
|
301
|
-
queuedTasks: stats?.schedulerStats?.queued ?? 0,
|
|
302
|
-
completedToday: stats?.schedulerStats?.completed ?? 0,
|
|
303
|
-
uptime: state?.startedAt ? Date.now() - state.startedAt : 0,
|
|
304
|
-
isRunning: stats?.isRunning ?? false,
|
|
305
|
-
sseClients: getActiveSSECount(),
|
|
306
|
-
adapters,
|
|
307
|
-
turboMode: stats?.turboMode ?? false,
|
|
308
|
-
turboExpiresAt: stats?.turboExpiresAt ?? null,
|
|
309
|
-
dailyPace: stats?.dailyPace ?? null,
|
|
310
|
-
}));
|
|
311
|
-
// ---- Tasks ----
|
|
312
|
-
}
|
|
313
|
-
else if (url === '/api/tasks') {
|
|
314
|
-
const running = runnerRef?.getRunningTasks() ?? [];
|
|
315
|
-
const queued = runnerRef?.getQueuedTasks() ?? [];
|
|
316
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
317
|
-
res.end(JSON.stringify({ running, queued }));
|
|
318
|
-
// ---- Pipeline GET (detailed pipeline stages) ----
|
|
319
|
-
}
|
|
320
|
-
else if (url === '/api/pipeline') {
|
|
321
|
-
const stages = getStageBuffer();
|
|
322
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
323
|
-
res.end(JSON.stringify({ stages }));
|
|
324
|
-
// ---- Rate Limiter Metrics GET ----
|
|
325
|
-
}
|
|
326
|
-
else if (url === '/api/rate-limits') {
|
|
327
|
-
const metrics = getRateLimiterMetrics();
|
|
328
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
329
|
-
res.end(JSON.stringify(metrics));
|
|
330
|
-
// ---- Projects GET (pinned + active projects) ----
|
|
331
|
-
}
|
|
332
|
-
else if (url === '/api/projects' && req.method === 'GET') {
|
|
333
|
-
const enabledPaths = new Set(runnerRef?.getEnabledProjects() ?? []);
|
|
334
|
-
const taskInfo = runnerRef?.getProjectsInfo() ?? [];
|
|
335
|
-
const byPath = new Map(taskInfo.filter(p => p.path).map(p => [p.path, p]));
|
|
336
|
-
// Fallback: match by project name (for tasks not yet executed → path not cached)
|
|
337
|
-
const byName = new Map(taskInfo.map(p => [p.name, p]));
|
|
338
|
-
// Start with pinned projects
|
|
339
|
-
const allPaths = new Set(pinnedProjects);
|
|
340
|
-
// Auto-include enabled projects and projects with active tasks
|
|
341
|
-
for (const path of enabledPaths)
|
|
342
|
-
allPaths.add(path);
|
|
343
|
-
for (const info of taskInfo) {
|
|
344
|
-
if (info.path && (info.running.length > 0 || info.queued.length > 0)) {
|
|
345
|
-
allPaths.add(info.path);
|
|
346
|
-
}
|
|
325
|
+
try {
|
|
326
|
+
const requestUrl = new URL(req.url ?? '/', 'http://localhost');
|
|
327
|
+
const url = requestUrl.pathname;
|
|
328
|
+
// CORS: allow localhost, Tauri webview, and Tailscale network
|
|
329
|
+
const origin = req.headers.origin;
|
|
330
|
+
if (origin && isAllowedOrigin(origin)) {
|
|
331
|
+
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
332
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE');
|
|
333
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-OpenSwarm-Token');
|
|
347
334
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
335
|
+
if (req.method === 'OPTIONS') {
|
|
336
|
+
res.writeHead(204);
|
|
337
|
+
res.end();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (isMutatingApiRequest(url, req.method) && !isAuthorizedMutation(req)) {
|
|
341
|
+
writeJson(res, 403, { error: 'Forbidden' });
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
// ---- GraphQL API (이슈 트래커) ----
|
|
345
|
+
if (isGraphQLRequest(req.url)) {
|
|
346
|
+
await handleGraphQL(req, res);
|
|
347
|
+
// ---- Issue Board ----
|
|
348
|
+
}
|
|
349
|
+
else if (url === '/issues') {
|
|
350
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
351
|
+
res.end(ISSUE_BOARD_HTML);
|
|
352
|
+
// ---- Dashboard ----
|
|
353
|
+
}
|
|
354
|
+
else if (url === '/' || url === '/index.html') {
|
|
355
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
356
|
+
res.end(DASHBOARD_HTML);
|
|
357
|
+
// ---- SSE stream ----
|
|
358
|
+
}
|
|
359
|
+
else if (url === '/api/events') {
|
|
360
|
+
const skipReplay = req.url?.includes('skipReplay=1') ?? false;
|
|
361
|
+
res.writeHead(200, {
|
|
362
|
+
'Content-Type': 'text/event-stream',
|
|
363
|
+
'Cache-Control': 'no-cache',
|
|
364
|
+
'Connection': 'keep-alive',
|
|
365
|
+
});
|
|
366
|
+
res.write(':connected\n\n');
|
|
367
|
+
addSSEClient(res, skipReplay);
|
|
368
|
+
// ---- Stats ----
|
|
369
|
+
}
|
|
370
|
+
else if (url === '/api/stats') {
|
|
371
|
+
const stats = runnerRef?.getStats();
|
|
372
|
+
const state = runnerRef?.getState();
|
|
373
|
+
const adapters = runnerRef?.getAdapterSummary();
|
|
382
374
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
383
|
-
res.end(JSON.stringify(
|
|
375
|
+
res.end(JSON.stringify({
|
|
376
|
+
runningTasks: stats?.schedulerStats?.running ?? 0,
|
|
377
|
+
queuedTasks: stats?.schedulerStats?.queued ?? 0,
|
|
378
|
+
completedToday: stats?.schedulerStats?.completed ?? 0,
|
|
379
|
+
uptime: state?.startedAt ? Date.now() - state.startedAt : 0,
|
|
380
|
+
isRunning: stats?.isRunning ?? false,
|
|
381
|
+
sseClients: getActiveSSECount(),
|
|
382
|
+
adapters,
|
|
383
|
+
turboMode: stats?.turboMode ?? false,
|
|
384
|
+
turboExpiresAt: stats?.turboExpiresAt ?? null,
|
|
385
|
+
dailyPace: stats?.dailyPace ?? null,
|
|
386
|
+
}));
|
|
387
|
+
// ---- Tasks ----
|
|
384
388
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
389
|
+
else if (url === '/api/tasks') {
|
|
390
|
+
const running = runnerRef?.getRunningTasks() ?? [];
|
|
391
|
+
const queued = runnerRef?.getQueuedTasks() ?? [];
|
|
392
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
393
|
+
res.end(JSON.stringify({ running, queued }));
|
|
394
|
+
// ---- Pipeline GET (detailed pipeline stages) ----
|
|
388
395
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
else if (url === '/api/projects/pin' && req.method === 'POST') {
|
|
392
|
-
const body = await readBody(req);
|
|
393
|
-
try {
|
|
394
|
-
const { projectPath } = JSON.parse(body);
|
|
395
|
-
if (typeof projectPath === 'string' && projectPath) {
|
|
396
|
-
pinnedProjects.add(projectPath);
|
|
397
|
-
// R6: an explicit pin is a deliberate re-enable — clear the denylist so it isn't
|
|
398
|
-
// skipped again by setWebRunner on the next restart.
|
|
399
|
-
removedConfigPaths.delete(projectPath);
|
|
400
|
-
saveReposConfig();
|
|
401
|
-
// Seed path cache so Linear project name matches immediately
|
|
402
|
-
const name = projectPath.split('/').pop();
|
|
403
|
-
if (name && runnerRef)
|
|
404
|
-
runnerRef.registerProjectPath(name, projectPath);
|
|
405
|
-
}
|
|
396
|
+
else if (url === '/api/pipeline') {
|
|
397
|
+
const stages = getStageBuffer();
|
|
406
398
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
407
|
-
res.end(JSON.stringify({
|
|
399
|
+
res.end(JSON.stringify({ stages }));
|
|
400
|
+
// ---- Rate Limiter Metrics GET ----
|
|
408
401
|
}
|
|
409
|
-
|
|
410
|
-
|
|
402
|
+
else if (url === '/api/rate-limits') {
|
|
403
|
+
const metrics = getRateLimiterMetrics();
|
|
404
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
405
|
+
res.end(JSON.stringify(metrics));
|
|
406
|
+
// ---- Projects GET (pinned + active projects) ----
|
|
411
407
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
408
|
+
else if (url === '/api/projects' && req.method === 'GET') {
|
|
409
|
+
const enabledPaths = new Set(runnerRef?.getEnabledProjects() ?? []);
|
|
410
|
+
const taskInfo = runnerRef?.getProjectsInfo() ?? [];
|
|
411
|
+
const byPath = new Map(taskInfo.filter(p => p.path).map(p => [p.path, p]));
|
|
412
|
+
// Fallback: match by project name (for tasks not yet executed → path not cached)
|
|
413
|
+
const byName = new Map(taskInfo.map(p => [p.name, p]));
|
|
414
|
+
// Start with pinned projects
|
|
415
|
+
const allPaths = new Set(pinnedProjects);
|
|
416
|
+
// Auto-include enabled projects and projects with active tasks
|
|
417
|
+
for (const path of enabledPaths)
|
|
418
|
+
allPaths.add(path);
|
|
419
|
+
for (const info of taskInfo) {
|
|
420
|
+
if (info.path && (info.running.length > 0 || info.queued.length > 0)) {
|
|
421
|
+
allPaths.add(info.path);
|
|
422
|
+
}
|
|
423
423
|
}
|
|
424
|
+
const result = await Promise.all(Array.from(allPaths).map(async (p) => {
|
|
425
|
+
const dirName = p.split('/').pop() ?? p;
|
|
426
|
+
const info = byPath.get(p) ?? byName.get(dirName);
|
|
427
|
+
const gitInfo = await getProjectGitInfo(p);
|
|
428
|
+
return {
|
|
429
|
+
path: p,
|
|
430
|
+
name: dirName,
|
|
431
|
+
enabled: enabledPaths.has(p),
|
|
432
|
+
pinned: pinnedProjects.has(p),
|
|
433
|
+
running: info?.running ?? [],
|
|
434
|
+
queued: info?.queued ?? [],
|
|
435
|
+
pending: info?.pending ?? [],
|
|
436
|
+
git: gitInfo.git,
|
|
437
|
+
prs: gitInfo.prs,
|
|
438
|
+
};
|
|
439
|
+
}));
|
|
440
|
+
result.sort((a, b) => {
|
|
441
|
+
const aActive = a.running.length + a.queued.length + a.pending.length;
|
|
442
|
+
const bActive = b.running.length + b.queued.length + b.pending.length;
|
|
443
|
+
if (aActive !== bActive)
|
|
444
|
+
return bActive - aActive;
|
|
445
|
+
return a.name.localeCompare(b.name);
|
|
446
|
+
});
|
|
424
447
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
425
|
-
res.end(JSON.stringify(
|
|
448
|
+
res.end(JSON.stringify(result));
|
|
449
|
+
// ---- Local projects for picker ----
|
|
426
450
|
}
|
|
427
|
-
|
|
428
|
-
|
|
451
|
+
else if (url === '/api/local-projects' && req.method === 'GET') {
|
|
452
|
+
const configPaths = runnerRef?.getAllowedProjects() ?? [];
|
|
453
|
+
const allBasePaths = [...new Set([...configPaths, ...customBasePaths])];
|
|
454
|
+
try {
|
|
455
|
+
const locals = await scanLocalProjects(allBasePaths);
|
|
456
|
+
const SKIP = ['/node_modules/', '/.git/', '/dist/', '/build/', '/__pycache__/', '/venv/', '/.venv/'];
|
|
457
|
+
const filtered = locals.filter(l => !SKIP.some(s => l.path.includes(s)));
|
|
458
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
459
|
+
res.end(JSON.stringify(filtered.map(l => ({ path: l.path, name: l.name, pinned: pinnedProjects.has(l.path) }))));
|
|
460
|
+
}
|
|
461
|
+
catch (e) {
|
|
462
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
463
|
+
res.end(JSON.stringify({ error: safeErrorMessage(e) }));
|
|
464
|
+
}
|
|
465
|
+
// ---- Pin project ----
|
|
429
466
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
removedConfigPaths.delete(projectPath);
|
|
439
|
-
|
|
467
|
+
else if (url === '/api/projects/pin' && req.method === 'POST') {
|
|
468
|
+
const body = await readBody(req);
|
|
469
|
+
try {
|
|
470
|
+
const { projectPath } = JSON.parse(body);
|
|
471
|
+
if (typeof projectPath === 'string' && projectPath) {
|
|
472
|
+
pinnedProjects.add(projectPath);
|
|
473
|
+
// R6: an explicit pin is a deliberate re-enable — clear the denylist so it isn't
|
|
474
|
+
// skipped again by setWebRunner on the next restart.
|
|
475
|
+
removedConfigPaths.delete(projectPath);
|
|
476
|
+
saveReposConfig();
|
|
477
|
+
// Seed path cache so Linear project name matches immediately
|
|
478
|
+
const name = projectPath.split('/').pop();
|
|
479
|
+
if (name && runnerRef)
|
|
480
|
+
runnerRef.registerProjectPath(name, projectPath);
|
|
440
481
|
}
|
|
441
|
-
|
|
482
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
483
|
+
res.end(JSON.stringify({ ok: true }));
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
res.writeHead(400).end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
487
|
+
}
|
|
488
|
+
// ---- Unpin project ----
|
|
489
|
+
}
|
|
490
|
+
else if (url === '/api/projects/unpin' && req.method === 'POST') {
|
|
491
|
+
const body = await readBody(req);
|
|
492
|
+
try {
|
|
493
|
+
const { projectPath } = JSON.parse(body);
|
|
494
|
+
if (typeof projectPath === 'string') {
|
|
495
|
+
pinnedProjects.delete(projectPath);
|
|
496
|
+
// Also disable the project so it doesn't reappear via enabledPaths
|
|
442
497
|
runnerRef?.disableProject(projectPath);
|
|
498
|
+
saveReposConfig();
|
|
443
499
|
}
|
|
444
|
-
|
|
445
|
-
|
|
500
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
501
|
+
res.end(JSON.stringify({ ok: true }));
|
|
446
502
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
452
|
-
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
503
|
+
catch {
|
|
504
|
+
res.writeHead(400).end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
505
|
+
}
|
|
506
|
+
// ---- Projects Toggle ----
|
|
453
507
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
508
|
+
else if (url === '/api/projects/toggle' && req.method === 'POST') {
|
|
509
|
+
const body = await readBody(req);
|
|
510
|
+
try {
|
|
511
|
+
const { projectPath, enabled } = JSON.parse(body);
|
|
512
|
+
if (typeof projectPath === 'string' && typeof enabled === 'boolean') {
|
|
513
|
+
if (enabled) {
|
|
514
|
+
removedConfigPaths.delete(projectPath); // R6: explicit enable clears the denylist
|
|
515
|
+
runnerRef?.enableProject(projectPath);
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
runnerRef?.disableProject(projectPath);
|
|
519
|
+
}
|
|
520
|
+
saveReposConfig();
|
|
521
|
+
broadcastEvent({ type: 'project:toggled', data: { projectPath, enabled } });
|
|
522
|
+
}
|
|
523
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
524
|
+
res.end(JSON.stringify({ ok: true }));
|
|
525
|
+
}
|
|
526
|
+
catch {
|
|
461
527
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
462
|
-
res.end(JSON.stringify({ error: '
|
|
463
|
-
return;
|
|
528
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
464
529
|
}
|
|
465
|
-
//
|
|
466
|
-
const linearModule = await import('../linear/index.js');
|
|
467
|
-
await linearModule.updateIssueState(issueId, 'Todo');
|
|
468
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
469
|
-
res.end(JSON.stringify({ ok: true }));
|
|
470
|
-
}
|
|
471
|
-
catch (error) {
|
|
472
|
-
console.error('[Web] Failed to move issue to Todo:', error);
|
|
473
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
474
|
-
res.end(JSON.stringify({ error: safeErrorMessage(error) }));
|
|
530
|
+
// ---- Move Issue to Todo ----
|
|
475
531
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
// so the dashboard's provider buttons (incl. claude / codex-responses) never
|
|
491
|
-
// drift out of sync with what's actually registered.
|
|
492
|
-
if (!isKnownAdapter(provider)) {
|
|
493
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
494
|
-
res.end(JSON.stringify({ error: `Invalid provider "${provider}". Valid: ${listAdapterNames().join(', ')}` }));
|
|
495
|
-
return;
|
|
532
|
+
else if (url === '/api/issue/move-to-todo' && req.method === 'POST') {
|
|
533
|
+
const body = await readBody(req);
|
|
534
|
+
try {
|
|
535
|
+
const { issueId } = JSON.parse(body);
|
|
536
|
+
if (!issueId) {
|
|
537
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
538
|
+
res.end(JSON.stringify({ error: 'Missing issueId' }));
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
// Import linear dynamically to avoid circular deps
|
|
542
|
+
const linearModule = await import('../linear/index.js');
|
|
543
|
+
await linearModule.updateIssueState(issueId, 'Todo');
|
|
544
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
545
|
+
res.end(JSON.stringify({ ok: true }));
|
|
496
546
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
504
|
-
res.end(JSON.stringify({ ok: true, provider }));
|
|
547
|
+
catch (error) {
|
|
548
|
+
console.error('[Web] Failed to move issue to Todo:', error);
|
|
549
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
550
|
+
res.end(JSON.stringify({ error: safeErrorMessage(error) }));
|
|
551
|
+
}
|
|
552
|
+
// ---- Heartbeat (manual trigger) ----
|
|
505
553
|
}
|
|
506
|
-
|
|
507
|
-
res.writeHead(
|
|
508
|
-
res.end(JSON.stringify({
|
|
554
|
+
else if (url === '/api/heartbeat' && req.method === 'POST') {
|
|
555
|
+
res.writeHead(202, { 'Content-Type': 'application/json' });
|
|
556
|
+
res.end(JSON.stringify({ ok: true }));
|
|
557
|
+
// Non-blocking heartbeat
|
|
558
|
+
runnerRef?.heartbeat().catch((e) => console.error('[Web] Heartbeat error:', e));
|
|
559
|
+
// ---- Provider toggle ----
|
|
509
560
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
561
|
+
else if (url === '/api/provider' && req.method === 'POST') {
|
|
562
|
+
const body = await readBody(req);
|
|
563
|
+
try {
|
|
564
|
+
const { provider } = JSON.parse(body);
|
|
565
|
+
// Validate against the live adapter registry rather than a hardcoded list,
|
|
566
|
+
// so the dashboard's provider buttons (incl. claude / codex-responses) never
|
|
567
|
+
// drift out of sync with what's actually registered.
|
|
568
|
+
if (!isKnownAdapter(provider)) {
|
|
569
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
570
|
+
res.end(JSON.stringify({ error: `Invalid provider "${provider}". Valid: ${listAdapterNames().join(', ')}` }));
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
setDefaultAdapter(provider);
|
|
574
|
+
runnerRef?.switchProvider(provider);
|
|
575
|
+
broadcastEvent({
|
|
576
|
+
type: 'log',
|
|
577
|
+
data: { taskId: 'system', stage: 'provider', line: `Provider switched to ${provider}` },
|
|
578
|
+
});
|
|
579
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
580
|
+
res.end(JSON.stringify({ ok: true, provider }));
|
|
581
|
+
}
|
|
582
|
+
catch {
|
|
517
583
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
518
|
-
res.end(JSON.stringify({ error: '
|
|
519
|
-
return;
|
|
584
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
520
585
|
}
|
|
521
|
-
|
|
522
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
523
|
-
res.end(JSON.stringify({ ok: true, turboMode: enabled }));
|
|
586
|
+
// ---- Turbo Mode Toggle ----
|
|
524
587
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
588
|
+
else if (url === '/api/turbo' && req.method === 'POST') {
|
|
589
|
+
const body = await readBody(req);
|
|
590
|
+
try {
|
|
591
|
+
const { enabled } = JSON.parse(body);
|
|
592
|
+
if (typeof enabled !== 'boolean') {
|
|
593
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
594
|
+
res.end(JSON.stringify({ error: 'enabled must be boolean' }));
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
runnerRef?.setTurboMode(enabled);
|
|
598
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
599
|
+
res.end(JSON.stringify({ ok: true, turboMode: enabled }));
|
|
600
|
+
}
|
|
601
|
+
catch {
|
|
602
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
603
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
604
|
+
}
|
|
605
|
+
// ---- PR Processor Status ----
|
|
528
606
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
607
|
+
else if (url === '/api/pr-processor-status' && req.method === 'GET') {
|
|
608
|
+
try {
|
|
609
|
+
const { getPRProcessor } = await import('../core/service.js');
|
|
610
|
+
const processor = getPRProcessor();
|
|
611
|
+
const status = processor ? processor.getStatus() : null;
|
|
612
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
613
|
+
res.end(JSON.stringify(status));
|
|
614
|
+
}
|
|
615
|
+
catch (error) {
|
|
616
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
617
|
+
res.end(JSON.stringify({ error: safeErrorMessage(error) }));
|
|
618
|
+
}
|
|
619
|
+
// ---- Trigger PR Processor ----
|
|
538
620
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
621
|
+
else if (url === '/api/trigger-pr-processor' && req.method === 'POST') {
|
|
622
|
+
try {
|
|
623
|
+
const { getPRProcessor } = await import('../core/service.js');
|
|
624
|
+
const processor = getPRProcessor();
|
|
625
|
+
if (!processor) {
|
|
626
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
627
|
+
res.end(JSON.stringify({ error: 'PR Processor not initialized' }));
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
res.writeHead(202, { 'Content-Type': 'application/json' });
|
|
631
|
+
res.end(JSON.stringify({ ok: true }));
|
|
632
|
+
// Non-blocking PR processing
|
|
633
|
+
processor.processPRs().catch((e) => console.error('[Web] PR Processor error:', e));
|
|
634
|
+
}
|
|
635
|
+
catch (error) {
|
|
636
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
637
|
+
res.end(JSON.stringify({ error: safeErrorMessage(error) }));
|
|
638
|
+
}
|
|
639
|
+
// ---- CI Worker Status ----
|
|
542
640
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
if (!processor) {
|
|
550
|
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
551
|
-
res.end(JSON.stringify({ error: 'PR Processor not initialized' }));
|
|
552
|
-
return;
|
|
641
|
+
else if (url === '/api/ci-worker-status' && req.method === 'GET') {
|
|
642
|
+
try {
|
|
643
|
+
const { getCIWorkerStatus } = await import('../automation/ciWorker.js');
|
|
644
|
+
const status = getCIWorkerStatus();
|
|
645
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
646
|
+
res.end(JSON.stringify(status));
|
|
553
647
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
648
|
+
catch (error) {
|
|
649
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
650
|
+
res.end(JSON.stringify({ error: safeErrorMessage(error) }));
|
|
651
|
+
}
|
|
652
|
+
// ---- Stuck/Failed Issues ----
|
|
558
653
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
654
|
+
else if (url === '/api/stuck-issues' && req.method === 'GET') {
|
|
655
|
+
try {
|
|
656
|
+
const linearModule = await import('../linear/index.js');
|
|
657
|
+
const result = await linearModule.getStuckIssues();
|
|
658
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
659
|
+
res.end(JSON.stringify(result));
|
|
660
|
+
}
|
|
661
|
+
catch (error) {
|
|
662
|
+
console.error('[Web] Failed to fetch stuck issues:', error);
|
|
663
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
664
|
+
res.end(JSON.stringify({ error: safeErrorMessage(error) }));
|
|
665
|
+
}
|
|
666
|
+
// ---- Chat history ----
|
|
562
667
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
668
|
+
else if (url === '/api/chat/history' && req.method === 'GET') {
|
|
669
|
+
const buf = getChatBuffer();
|
|
670
|
+
const history = buf
|
|
671
|
+
.filter((ev) => ev.type === 'chat:user' || ev.type === 'chat:agent')
|
|
672
|
+
.map(ev => ({
|
|
673
|
+
role: ev.type === 'chat:user' ? 'user' : 'agent',
|
|
674
|
+
text: ev.data.text,
|
|
675
|
+
ts: ev.data.ts,
|
|
676
|
+
}));
|
|
569
677
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
570
|
-
res.end(JSON.stringify(
|
|
678
|
+
res.end(JSON.stringify(history.slice(-50)));
|
|
679
|
+
// ---- Log buffer snapshot ----
|
|
571
680
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
// ---- Stuck/Failed Issues ----
|
|
577
|
-
}
|
|
578
|
-
else if (url === '/api/stuck-issues' && req.method === 'GET') {
|
|
579
|
-
try {
|
|
580
|
-
const linearModule = await import('../linear/index.js');
|
|
581
|
-
const result = await linearModule.getStuckIssues();
|
|
681
|
+
else if (url === '/api/logs' && req.method === 'GET') {
|
|
682
|
+
// 성능 최적화: 최근 200개 로그만 반환 (응답 시간 단축)
|
|
683
|
+
const allLogs = getLogBuffer();
|
|
684
|
+
const recentLogs = allLogs.slice(-200);
|
|
582
685
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
583
|
-
res.end(JSON.stringify(
|
|
584
|
-
|
|
585
|
-
catch (error) {
|
|
586
|
-
console.error('[Web] Failed to fetch stuck issues:', error);
|
|
587
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
588
|
-
res.end(JSON.stringify({ error: safeErrorMessage(error) }));
|
|
589
|
-
}
|
|
590
|
-
// ---- Chat history ----
|
|
591
|
-
}
|
|
592
|
-
else if (url === '/api/chat/history' && req.method === 'GET') {
|
|
593
|
-
const buf = getChatBuffer();
|
|
594
|
-
const history = buf
|
|
595
|
-
.filter((ev) => ev.type === 'chat:user' || ev.type === 'chat:agent')
|
|
596
|
-
.map(ev => ({
|
|
597
|
-
role: ev.type === 'chat:user' ? 'user' : 'agent',
|
|
598
|
-
text: ev.data.text,
|
|
599
|
-
ts: ev.data.ts,
|
|
600
|
-
}));
|
|
601
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
602
|
-
res.end(JSON.stringify(history.slice(-50)));
|
|
603
|
-
// ---- Log buffer snapshot ----
|
|
604
|
-
}
|
|
605
|
-
else if (url === '/api/logs' && req.method === 'GET') {
|
|
606
|
-
// 성능 최적화: 최근 200개 로그만 반환 (응답 시간 단축)
|
|
607
|
-
const allLogs = getLogBuffer();
|
|
608
|
-
const recentLogs = allLogs.slice(-200);
|
|
609
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
610
|
-
res.end(JSON.stringify(recentLogs));
|
|
611
|
-
// ---- Stage buffer snapshot ----
|
|
612
|
-
}
|
|
613
|
-
else if (url === '/api/stages' && req.method === 'GET') {
|
|
614
|
-
// 성능 최적화: 최근 100개 스테이지만 반환 (응답 시간 단축)
|
|
615
|
-
const allStages = getStageBuffer();
|
|
616
|
-
const recentStages = allStages.slice(-100);
|
|
617
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
618
|
-
res.end(JSON.stringify(recentStages));
|
|
619
|
-
// ---- Chat message ----
|
|
620
|
-
}
|
|
621
|
-
else if (url === '/api/chat' && req.method === 'POST') {
|
|
622
|
-
const body = await readBody(req);
|
|
623
|
-
let message;
|
|
624
|
-
try {
|
|
625
|
-
message = JSON.parse(body).message?.trim();
|
|
626
|
-
}
|
|
627
|
-
catch {
|
|
628
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
629
|
-
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
630
|
-
return;
|
|
631
|
-
}
|
|
632
|
-
if (!message) {
|
|
633
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
634
|
-
res.end(JSON.stringify({ error: 'Empty message' }));
|
|
635
|
-
return;
|
|
686
|
+
res.end(JSON.stringify(recentLogs));
|
|
687
|
+
// ---- Stage buffer snapshot ----
|
|
636
688
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
const enabled = projects.filter(p => p.enabled).length;
|
|
642
|
-
const state = runnerRef?.getState();
|
|
643
|
-
const uptimeSec = state?.startedAt ? Math.floor((Date.now() - state.startedAt) / 1000) : 0;
|
|
644
|
-
// 1. Short-term memory: recent chat buffer
|
|
645
|
-
const chatBuf = getChatBuffer()
|
|
646
|
-
.filter((ev) => ev.type === 'chat:user' || ev.type === 'chat:agent');
|
|
647
|
-
const recentHistory = chatBuf.slice(-11, -1);
|
|
648
|
-
const historyBlock = recentHistory.length > 0
|
|
649
|
-
? recentHistory.map(m => (m.type === 'chat:user' ? 'User' : 'OpenSwarm') + ': ' + m.data.text).join('\n\n')
|
|
650
|
-
: '';
|
|
651
|
-
// 2. Long-term memory: semantic search (shared with Discord)
|
|
652
|
-
const memories = await memory.searchMemory(message, {
|
|
653
|
-
types: ['journal'],
|
|
654
|
-
repo: 'chat', // Shared repo for both Discord and Dashboard
|
|
655
|
-
limit: 5,
|
|
656
|
-
minSimilarity: 0.4,
|
|
657
|
-
minTrust: 0.5,
|
|
658
|
-
});
|
|
659
|
-
const memoryContext = memories.length > 0
|
|
660
|
-
? '## Relevant Past Discussions\n' + memories.map(m => `- ${m.content.replace(/^Q: |^A: /g, '')}`).join('\n')
|
|
661
|
-
: '';
|
|
662
|
-
const provider = runnerRef?.getAdapterSummary().defaultAdapter ?? 'codex';
|
|
663
|
-
const model = runnerRef?.getAdapterSummary().worker?.model ?? getDefaultChatModel(provider);
|
|
664
|
-
const contextPrompt = [
|
|
665
|
-
'You are OpenSwarm, an autonomous code development supervisor.',
|
|
666
|
-
'You manage a fleet of coding agents that autonomously work on Linear issues.',
|
|
667
|
-
`Current chat provider: ${provider}`,
|
|
668
|
-
`Current chat model: ${model}`,
|
|
669
|
-
'',
|
|
670
|
-
'Current system status:',
|
|
671
|
-
'- Running tasks: ' + (stats?.schedulerStats?.running ?? 0),
|
|
672
|
-
'- Queued tasks: ' + (stats?.schedulerStats?.queued ?? 0),
|
|
673
|
-
'- Completed today: ' + (stats?.schedulerStats?.completed ?? 0),
|
|
674
|
-
'- Active repos: ' + enabled + '/' + projects.length,
|
|
675
|
-
'- Uptime: ' + uptimeSec + 's',
|
|
676
|
-
'',
|
|
677
|
-
...(historyBlock ? [
|
|
678
|
-
'Conversation history (most recent first):',
|
|
679
|
-
historyBlock,
|
|
680
|
-
'',
|
|
681
|
-
] : []),
|
|
682
|
-
...(memoryContext ? [memoryContext, ''] : []),
|
|
683
|
-
'Answer the user concisely and helpfully in the same language they use. Use the status data above if relevant.',
|
|
684
|
-
'',
|
|
685
|
-
'User: ' + message,
|
|
686
|
-
].join('\n');
|
|
687
|
-
const result = await runChatCompletion({
|
|
688
|
-
prompt: contextPrompt,
|
|
689
|
-
provider,
|
|
690
|
-
model,
|
|
691
|
-
cwd: process.cwd(),
|
|
692
|
-
timeoutMs: 180000,
|
|
693
|
-
}).catch((error) => ({
|
|
694
|
-
response: `[Error: ${error.message}]`,
|
|
695
|
-
provider,
|
|
696
|
-
model,
|
|
697
|
-
cost: undefined,
|
|
698
|
-
tokens: undefined,
|
|
699
|
-
}));
|
|
700
|
-
const response = result.response;
|
|
701
|
-
if (result.cost !== undefined) {
|
|
702
|
-
console.log(`[Web Chat] Cost: ${formatCost({
|
|
703
|
-
costUsd: result.cost,
|
|
704
|
-
inputTokens: result.tokens ?? 0,
|
|
705
|
-
outputTokens: 0,
|
|
706
|
-
cacheReadTokens: 0,
|
|
707
|
-
cacheCreationTokens: 0,
|
|
708
|
-
durationMs: 0,
|
|
709
|
-
model: result.model,
|
|
710
|
-
})}`);
|
|
711
|
-
}
|
|
712
|
-
broadcastEvent({ type: 'chat:agent', data: { text: response, ts: Date.now() } });
|
|
713
|
-
// 3. Save conversation to long-term memory
|
|
714
|
-
await memory.saveConversation('dashboard', // channelId (fixed for dashboard)
|
|
715
|
-
'dashboard-user', // userId
|
|
716
|
-
'User', // userName
|
|
717
|
-
message, response);
|
|
718
|
-
console.log(`[Dashboard Chat] Saved to memory (${message.length} + ${response.length} chars)`);
|
|
719
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
720
|
-
res.end(JSON.stringify({ ok: true, response, provider: result.provider, model: result.model }));
|
|
721
|
-
// ---- Service control: status ----
|
|
722
|
-
}
|
|
723
|
-
else if (url === '/api/service/status' && req.method === 'GET') {
|
|
724
|
-
try {
|
|
725
|
-
const result = await new Promise((resolve) => {
|
|
726
|
-
execFile('systemctl', ['--user', 'is-active', 'openswarm'], (_err, stdout) => {
|
|
727
|
-
resolve(stdout.trim());
|
|
728
|
-
});
|
|
729
|
-
});
|
|
689
|
+
else if (url === '/api/stages' && req.method === 'GET') {
|
|
690
|
+
// 성능 최적화: 최근 100개 스테이지만 반환 (응답 시간 단축)
|
|
691
|
+
const allStages = getStageBuffer();
|
|
692
|
+
const recentStages = allStages.slice(-100);
|
|
730
693
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
731
|
-
res.end(JSON.stringify(
|
|
732
|
-
|
|
733
|
-
catch {
|
|
734
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
735
|
-
res.end(JSON.stringify({ status: 'unknown' }));
|
|
694
|
+
res.end(JSON.stringify(recentStages));
|
|
695
|
+
// ---- Chat message ----
|
|
736
696
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
697
|
+
else if (url === '/api/chat' && req.method === 'POST') {
|
|
698
|
+
const body = await readBody(req);
|
|
699
|
+
let message;
|
|
700
|
+
try {
|
|
701
|
+
message = JSON.parse(body).message?.trim();
|
|
702
|
+
}
|
|
703
|
+
catch {
|
|
704
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
705
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
if (!message) {
|
|
709
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
710
|
+
res.end(JSON.stringify({ error: 'Empty message' }));
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
broadcastEvent({ type: 'chat:user', data: { text: message, ts: Date.now() } });
|
|
714
|
+
// Build context-aware prompt (including previous conversation)
|
|
715
|
+
const stats = runnerRef?.getStats();
|
|
716
|
+
const projects = runnerRef?.getProjectsInfo() ?? [];
|
|
717
|
+
const enabled = projects.filter(p => p.enabled).length;
|
|
718
|
+
const state = runnerRef?.getState();
|
|
719
|
+
const uptimeSec = state?.startedAt ? Math.floor((Date.now() - state.startedAt) / 1000) : 0;
|
|
720
|
+
// 1. Short-term memory: recent chat buffer
|
|
721
|
+
const chatBuf = getChatBuffer()
|
|
722
|
+
.filter((ev) => ev.type === 'chat:user' || ev.type === 'chat:agent');
|
|
723
|
+
const recentHistory = chatBuf.slice(-11, -1);
|
|
724
|
+
const historyBlock = recentHistory.length > 0
|
|
725
|
+
? recentHistory.map(m => (m.type === 'chat:user' ? 'User' : 'OpenSwarm') + ': ' + m.data.text).join('\n\n')
|
|
726
|
+
: '';
|
|
727
|
+
// 2. Long-term memory: semantic search (shared with Discord)
|
|
728
|
+
const memories = await memory.searchMemory(message, {
|
|
729
|
+
types: ['journal'],
|
|
730
|
+
repo: 'chat', // Shared repo for both Discord and Dashboard
|
|
731
|
+
limit: 5,
|
|
732
|
+
minSimilarity: 0.4,
|
|
733
|
+
minTrust: 0.5,
|
|
748
734
|
});
|
|
735
|
+
const memoryContext = memories.length > 0
|
|
736
|
+
? '## Relevant Past Discussions\n' + memories.map(m => `- ${m.content.replace(/^Q: |^A: /g, '')}`).join('\n')
|
|
737
|
+
: '';
|
|
738
|
+
const provider = runnerRef?.getAdapterSummary().defaultAdapter ?? 'codex';
|
|
739
|
+
const model = runnerRef?.getAdapterSummary().worker?.model ?? getDefaultChatModel(provider);
|
|
740
|
+
const contextPrompt = [
|
|
741
|
+
'You are OpenSwarm, an autonomous code development supervisor.',
|
|
742
|
+
'You manage a fleet of coding agents that autonomously work on Linear issues.',
|
|
743
|
+
`Current chat provider: ${provider}`,
|
|
744
|
+
`Current chat model: ${model}`,
|
|
745
|
+
'',
|
|
746
|
+
'Current system status:',
|
|
747
|
+
'- Running tasks: ' + (stats?.schedulerStats?.running ?? 0),
|
|
748
|
+
'- Queued tasks: ' + (stats?.schedulerStats?.queued ?? 0),
|
|
749
|
+
'- Completed today: ' + (stats?.schedulerStats?.completed ?? 0),
|
|
750
|
+
'- Active repos: ' + enabled + '/' + projects.length,
|
|
751
|
+
'- Uptime: ' + uptimeSec + 's',
|
|
752
|
+
'',
|
|
753
|
+
...(historyBlock ? [
|
|
754
|
+
'Conversation history (most recent first):',
|
|
755
|
+
historyBlock,
|
|
756
|
+
'',
|
|
757
|
+
] : []),
|
|
758
|
+
...(memoryContext ? [memoryContext, ''] : []),
|
|
759
|
+
'Answer the user concisely and helpfully in the same language they use. Use the status data above if relevant.',
|
|
760
|
+
'',
|
|
761
|
+
'User: ' + message,
|
|
762
|
+
].join('\n');
|
|
763
|
+
const result = await runChatCompletion({
|
|
764
|
+
prompt: contextPrompt,
|
|
765
|
+
provider,
|
|
766
|
+
model,
|
|
767
|
+
cwd: process.cwd(),
|
|
768
|
+
timeoutMs: 180000,
|
|
769
|
+
}).catch((error) => ({
|
|
770
|
+
response: `[Error: ${error.message}]`,
|
|
771
|
+
provider,
|
|
772
|
+
model,
|
|
773
|
+
cost: undefined,
|
|
774
|
+
tokens: undefined,
|
|
775
|
+
}));
|
|
776
|
+
const response = result.response;
|
|
777
|
+
if (result.cost !== undefined) {
|
|
778
|
+
console.log(`[Web Chat] Cost: ${formatCost({
|
|
779
|
+
costUsd: result.cost,
|
|
780
|
+
inputTokens: result.tokens ?? 0,
|
|
781
|
+
outputTokens: 0,
|
|
782
|
+
cacheReadTokens: 0,
|
|
783
|
+
cacheCreationTokens: 0,
|
|
784
|
+
durationMs: 0,
|
|
785
|
+
model: result.model,
|
|
786
|
+
})}`);
|
|
787
|
+
}
|
|
788
|
+
broadcastEvent({ type: 'chat:agent', data: { text: response, ts: Date.now() } });
|
|
789
|
+
// 3. Save conversation to long-term memory
|
|
790
|
+
await memory.saveConversation('dashboard', // channelId (fixed for dashboard)
|
|
791
|
+
'dashboard-user', // userId
|
|
792
|
+
'User', // userName
|
|
793
|
+
message, response);
|
|
794
|
+
console.log(`[Dashboard Chat] Saved to memory (${message.length} + ${response.length} chars)`);
|
|
749
795
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
750
|
-
res.end(JSON.stringify({ ok: true }));
|
|
796
|
+
res.end(JSON.stringify({ ok: true, response, provider: result.provider, model: result.model }));
|
|
797
|
+
// ---- Service control: status ----
|
|
751
798
|
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
else if (url === '/api/service/restart' && req.method === 'POST') {
|
|
759
|
-
try {
|
|
760
|
-
await new Promise((resolve, reject) => {
|
|
761
|
-
execFile('systemctl', ['--user', 'restart', 'openswarm'], (err) => {
|
|
762
|
-
if (err)
|
|
763
|
-
reject(err);
|
|
764
|
-
else
|
|
765
|
-
resolve();
|
|
799
|
+
else if (url === '/api/service/status' && req.method === 'GET') {
|
|
800
|
+
try {
|
|
801
|
+
const result = await new Promise((resolve) => {
|
|
802
|
+
execFile('systemctl', ['--user', 'is-active', 'openswarm'], (_err, stdout) => {
|
|
803
|
+
resolve(stdout.trim());
|
|
804
|
+
});
|
|
766
805
|
});
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
// ---- Knowledge Graph: project health ----
|
|
776
|
-
}
|
|
777
|
-
else if (url.startsWith('/api/knowledge/') && req.method === 'GET') {
|
|
778
|
-
const projectSlug = url.replace('/api/knowledge/', '').split('?')[0];
|
|
779
|
-
if (!projectSlug) {
|
|
780
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
781
|
-
res.end(JSON.stringify({ error: 'Missing project slug' }));
|
|
806
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
807
|
+
res.end(JSON.stringify({ status: result }));
|
|
808
|
+
}
|
|
809
|
+
catch {
|
|
810
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
811
|
+
res.end(JSON.stringify({ status: 'unknown' }));
|
|
812
|
+
}
|
|
813
|
+
// ---- Service control: stop ----
|
|
782
814
|
}
|
|
783
|
-
else {
|
|
815
|
+
else if (url === '/api/service/stop' && req.method === 'POST') {
|
|
784
816
|
try {
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
scannedAt: graph.scannedAt,
|
|
796
|
-
nodeCount: graph.nodeCount,
|
|
797
|
-
edgeCount: graph.edgeCount,
|
|
798
|
-
...health,
|
|
799
|
-
}));
|
|
800
|
-
}
|
|
817
|
+
await new Promise((resolve, reject) => {
|
|
818
|
+
execFile('systemctl', ['--user', 'stop', 'openswarm'], (err) => {
|
|
819
|
+
if (err)
|
|
820
|
+
reject(err);
|
|
821
|
+
else
|
|
822
|
+
resolve();
|
|
823
|
+
});
|
|
824
|
+
});
|
|
825
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
826
|
+
res.end(JSON.stringify({ ok: true }));
|
|
801
827
|
}
|
|
802
828
|
catch (e) {
|
|
803
829
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
804
830
|
res.end(JSON.stringify({ error: safeErrorMessage(e) }));
|
|
805
831
|
}
|
|
832
|
+
// ---- Service control: restart ----
|
|
806
833
|
}
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
if (graph) {
|
|
816
|
-
result.push({
|
|
817
|
-
slug,
|
|
818
|
-
nodeCount: graph.nodeCount,
|
|
819
|
-
edgeCount: graph.edgeCount,
|
|
820
|
-
scannedAt: graph.scannedAt,
|
|
821
|
-
summary: graph.buildSummary(),
|
|
834
|
+
else if (url === '/api/service/restart' && req.method === 'POST') {
|
|
835
|
+
try {
|
|
836
|
+
await new Promise((resolve, reject) => {
|
|
837
|
+
execFile('systemctl', ['--user', 'restart', 'openswarm'], (err) => {
|
|
838
|
+
if (err)
|
|
839
|
+
reject(err);
|
|
840
|
+
else
|
|
841
|
+
resolve();
|
|
822
842
|
});
|
|
823
|
-
}
|
|
843
|
+
});
|
|
844
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
845
|
+
res.end(JSON.stringify({ ok: true }));
|
|
824
846
|
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
res.end(JSON.stringify({ error: safeErrorMessage(e) }));
|
|
847
|
+
catch (e) {
|
|
848
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
849
|
+
res.end(JSON.stringify({ error: safeErrorMessage(e) }));
|
|
850
|
+
}
|
|
851
|
+
// ---- Knowledge Graph: project health ----
|
|
831
852
|
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
const body = await readBody(req);
|
|
836
|
-
try {
|
|
837
|
-
const { projectPath } = JSON.parse(body);
|
|
838
|
-
if (!projectPath) {
|
|
853
|
+
else if (url.startsWith('/api/knowledge/') && req.method === 'GET') {
|
|
854
|
+
const projectSlug = url.replace('/api/knowledge/', '').split('?')[0];
|
|
855
|
+
if (!projectSlug) {
|
|
839
856
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
840
|
-
res.end(JSON.stringify({ error: 'Missing
|
|
857
|
+
res.end(JSON.stringify({ error: 'Missing project slug' }));
|
|
841
858
|
}
|
|
842
859
|
else {
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
860
|
+
try {
|
|
861
|
+
const graph = await getGraph(projectSlug);
|
|
862
|
+
if (!graph) {
|
|
863
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
864
|
+
res.end(JSON.stringify({ error: 'Graph not found. Run a scan first.' }));
|
|
865
|
+
}
|
|
866
|
+
else {
|
|
867
|
+
const health = getProjectHealth(graph);
|
|
868
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
869
|
+
res.end(JSON.stringify({
|
|
870
|
+
slug: projectSlug,
|
|
871
|
+
scannedAt: graph.scannedAt,
|
|
872
|
+
nodeCount: graph.nodeCount,
|
|
873
|
+
edgeCount: graph.edgeCount,
|
|
874
|
+
...health,
|
|
875
|
+
}));
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
catch (e) {
|
|
879
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
880
|
+
res.end(JSON.stringify({ error: safeErrorMessage(e) }));
|
|
881
|
+
}
|
|
848
882
|
}
|
|
883
|
+
// ---- Knowledge Graph: list all ----
|
|
849
884
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
885
|
+
else if (url === '/api/knowledge' && req.method === 'GET') {
|
|
886
|
+
try {
|
|
887
|
+
const slugs = await listGraphs();
|
|
888
|
+
const result = [];
|
|
889
|
+
for (const slug of slugs) {
|
|
890
|
+
const graph = await getGraph(slug);
|
|
891
|
+
if (graph) {
|
|
892
|
+
result.push({
|
|
893
|
+
slug,
|
|
894
|
+
nodeCount: graph.nodeCount,
|
|
895
|
+
edgeCount: graph.edgeCount,
|
|
896
|
+
scannedAt: graph.scannedAt,
|
|
897
|
+
summary: graph.buildSummary(),
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
902
|
+
res.end(JSON.stringify(result));
|
|
903
|
+
}
|
|
904
|
+
catch (e) {
|
|
905
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
906
|
+
res.end(JSON.stringify({ error: safeErrorMessage(e) }));
|
|
907
|
+
}
|
|
908
|
+
// ---- Knowledge Graph: trigger scan ----
|
|
853
909
|
}
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
try {
|
|
872
|
-
const config = JSON.parse(body);
|
|
873
|
-
if (!config.id ||
|
|
874
|
-
!config.name ||
|
|
875
|
-
!Array.isArray(config.checkCommand) ||
|
|
876
|
-
config.checkCommand.length === 0 ||
|
|
877
|
-
!config.completionCheck) {
|
|
910
|
+
else if (url === '/api/knowledge/scan' && req.method === 'POST') {
|
|
911
|
+
const body = await readBody(req);
|
|
912
|
+
try {
|
|
913
|
+
const { projectPath } = JSON.parse(body);
|
|
914
|
+
if (!projectPath) {
|
|
915
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
916
|
+
res.end(JSON.stringify({ error: 'Missing projectPath' }));
|
|
917
|
+
}
|
|
918
|
+
else {
|
|
919
|
+
const resolvedPath = projectPath.replace('~', homedir());
|
|
920
|
+
res.writeHead(202, { 'Content-Type': 'application/json' });
|
|
921
|
+
res.end(JSON.stringify({ ok: true, slug: toProjectSlug(resolvedPath) }));
|
|
922
|
+
// Non-blocking scan
|
|
923
|
+
scanAndCache(resolvedPath, { force: true }).catch(e => console.error('[Web] Knowledge scan error:', e));
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
catch {
|
|
878
927
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
879
|
-
res.end(JSON.stringify({
|
|
880
|
-
error: 'Missing or invalid required fields: id, name, checkCommand (string[]), completionCheck',
|
|
881
|
-
}));
|
|
882
|
-
return;
|
|
928
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
883
929
|
}
|
|
884
|
-
|
|
885
|
-
res.writeHead(201, { 'Content-Type': 'application/json' });
|
|
886
|
-
res.end(JSON.stringify(monitor));
|
|
930
|
+
// ---- Pipeline history (time-ordered) ----
|
|
887
931
|
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
const monitorId = url.replace('/api/monitors/', '');
|
|
896
|
-
const deleted = unregisterMonitor(monitorId);
|
|
897
|
-
res.writeHead(deleted ? 200 : 404, { 'Content-Type': 'application/json' });
|
|
898
|
-
res.end(JSON.stringify({ ok: deleted }));
|
|
899
|
-
// ---- Processes: list ----
|
|
900
|
-
}
|
|
901
|
-
else if (url === '/api/processes' && req.method === 'GET') {
|
|
902
|
-
// Spawned CLI subprocesses (PID-tracked) + in-process pipeline tasks. Native
|
|
903
|
-
// adapters (codex-responses/openrouter/local) run the worker/reviewer in-process
|
|
904
|
-
// with no child PID, so without the pipeline entries the panel looks empty even
|
|
905
|
-
// while tasks are actively running.
|
|
906
|
-
const subprocs = getAllProcesses().map((p) => ({ ...p, kind: 'subprocess', id: String(p.pid) }));
|
|
907
|
-
const pipelines = (runnerRef?.getRunningPipelines() ?? []).map((t) => ({
|
|
908
|
-
kind: 'pipeline',
|
|
909
|
-
id: t.id,
|
|
910
|
-
pid: null,
|
|
911
|
-
taskId: t.issue ?? t.title,
|
|
912
|
-
project: t.project,
|
|
913
|
-
stage: t.stage ?? 'running',
|
|
914
|
-
projectPath: t.projectPath,
|
|
915
|
-
spawnedAt: t.startedAt,
|
|
916
|
-
lastActivityAt: t.startedAt,
|
|
917
|
-
}));
|
|
918
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
919
|
-
res.end(JSON.stringify([...pipelines, ...subprocs]));
|
|
920
|
-
// ---- Processes: kill (PID) or cancel (pipeline task id) ----
|
|
921
|
-
}
|
|
922
|
-
else if (url.startsWith('/api/processes/') && req.method === 'DELETE') {
|
|
923
|
-
const idStr = decodeURIComponent(url.replace('/api/processes/', ''));
|
|
924
|
-
const pid = parseInt(idStr, 10);
|
|
925
|
-
if (!isNaN(pid) && String(pid) === idStr) {
|
|
926
|
-
const killed = await killProcess(pid);
|
|
927
|
-
res.writeHead(killed ? 200 : 404, { 'Content-Type': 'application/json' });
|
|
928
|
-
res.end(JSON.stringify({ ok: killed }));
|
|
932
|
+
else if (url === '/api/pipeline/history' && req.method === 'GET') {
|
|
933
|
+
const limitParam = (req.url?.split('?')[1] || '').match(/limit=(\d+)/);
|
|
934
|
+
const limit = limitParam ? Math.min(Number(limitParam[1]), 100) : 50;
|
|
935
|
+
const history = runnerRef?.getPipelineHistory(limit) ?? [];
|
|
936
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
937
|
+
res.end(JSON.stringify(history));
|
|
938
|
+
// ---- Monitors: list ----
|
|
929
939
|
}
|
|
930
|
-
else {
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
res.end(JSON.stringify({ ok: cancelled }));
|
|
940
|
+
else if (url === '/api/monitors' && req.method === 'GET') {
|
|
941
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
942
|
+
res.end(JSON.stringify(getActiveMonitors()));
|
|
943
|
+
// ---- Monitors: register ----
|
|
935
944
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
955
|
-
res.end(JSON.stringify({ error: 'Missing path' }));
|
|
945
|
+
else if (url === '/api/monitors' && req.method === 'POST') {
|
|
946
|
+
const body = await readBody(req);
|
|
947
|
+
try {
|
|
948
|
+
const config = JSON.parse(body);
|
|
949
|
+
if (!config.id ||
|
|
950
|
+
!config.name ||
|
|
951
|
+
!Array.isArray(config.checkCommand) ||
|
|
952
|
+
config.checkCommand.length === 0 ||
|
|
953
|
+
!config.completionCheck) {
|
|
954
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
955
|
+
res.end(JSON.stringify({
|
|
956
|
+
error: 'Missing or invalid required fields: id, name, checkCommand (string[]), completionCheck',
|
|
957
|
+
}));
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
const monitor = registerMonitor(config);
|
|
961
|
+
res.writeHead(201, { 'Content-Type': 'application/json' });
|
|
962
|
+
res.end(JSON.stringify(monitor));
|
|
956
963
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
// Update runner's allowedProjects with merged list
|
|
961
|
-
const configPaths = runnerRef?.getAllowedProjects() ?? [];
|
|
962
|
-
const merged = [...new Set([...configPaths, ...customBasePaths])];
|
|
963
|
-
runnerRef?.updateAllowedProjects(merged);
|
|
964
|
-
saveReposConfig();
|
|
965
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
966
|
-
res.end(JSON.stringify({ ok: true }));
|
|
964
|
+
catch (e) {
|
|
965
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
966
|
+
res.end(JSON.stringify({ error: safeErrorMessage(e) }));
|
|
967
967
|
}
|
|
968
|
+
// ---- Monitors: delete ----
|
|
968
969
|
}
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
970
|
+
else if (url.startsWith('/api/monitors/') && req.method === 'DELETE') {
|
|
971
|
+
const monitorId = url.replace('/api/monitors/', '');
|
|
972
|
+
const deleted = unregisterMonitor(monitorId);
|
|
973
|
+
res.writeHead(deleted ? 200 : 404, { 'Content-Type': 'application/json' });
|
|
974
|
+
res.end(JSON.stringify({ ok: deleted }));
|
|
975
|
+
// ---- Processes: list ----
|
|
972
976
|
}
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
977
|
+
else if (url === '/api/processes' && req.method === 'GET') {
|
|
978
|
+
// Spawned CLI subprocesses (PID-tracked) + in-process pipeline tasks. Native
|
|
979
|
+
// adapters (codex-responses/openrouter/local) run the worker/reviewer in-process
|
|
980
|
+
// with no child PID, so without the pipeline entries the panel looks empty even
|
|
981
|
+
// while tasks are actively running.
|
|
982
|
+
const subprocs = getAllProcesses().map((p) => ({ ...p, kind: 'subprocess', id: String(p.pid) }));
|
|
983
|
+
const pipelines = (runnerRef?.getRunningPipelines() ?? []).map((t) => ({
|
|
984
|
+
kind: 'pipeline',
|
|
985
|
+
id: t.id,
|
|
986
|
+
pid: null,
|
|
987
|
+
taskId: t.issue ?? t.title,
|
|
988
|
+
project: t.project,
|
|
989
|
+
stage: t.stage ?? 'running',
|
|
990
|
+
projectPath: t.projectPath,
|
|
991
|
+
spawnedAt: t.startedAt,
|
|
992
|
+
lastActivityAt: t.startedAt,
|
|
993
|
+
}));
|
|
994
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
995
|
+
res.end(JSON.stringify([...pipelines, ...subprocs]));
|
|
996
|
+
// ---- Processes: kill (PID) or cancel (pipeline task id) ----
|
|
985
997
|
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
}
|
|
994
|
-
else if (url.startsWith('/api/fs/list') && req.method === 'GET') {
|
|
995
|
-
try {
|
|
996
|
-
const qs = url.split('?')[1] ?? '';
|
|
997
|
-
const params = new URLSearchParams(qs);
|
|
998
|
-
const requested = params.get('path')?.trim();
|
|
999
|
-
const startPath = requested && requested.length > 0
|
|
1000
|
-
? requested
|
|
1001
|
-
: homedir();
|
|
1002
|
-
const expanded = startPath.startsWith('~')
|
|
1003
|
-
? join(homedir(), startPath.slice(1))
|
|
1004
|
-
: startPath;
|
|
1005
|
-
const absolute = resolvePath(expanded);
|
|
1006
|
-
const st = await stat(absolute);
|
|
1007
|
-
if (!st.isDirectory()) {
|
|
1008
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1009
|
-
res.end(JSON.stringify({ error: 'Not a directory', path: absolute }));
|
|
1010
|
-
return;
|
|
998
|
+
else if (url.startsWith('/api/processes/') && req.method === 'DELETE') {
|
|
999
|
+
const idStr = decodeURIComponent(url.replace('/api/processes/', ''));
|
|
1000
|
+
const pid = parseInt(idStr, 10);
|
|
1001
|
+
if (!isNaN(pid) && String(pid) === idStr) {
|
|
1002
|
+
const killed = await killProcess(pid);
|
|
1003
|
+
res.writeHead(killed ? 200 : 404, { 'Content-Type': 'application/json' });
|
|
1004
|
+
res.end(JSON.stringify({ ok: killed }));
|
|
1011
1005
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
.
|
|
1016
|
-
.
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1006
|
+
else {
|
|
1007
|
+
// Non-numeric id → in-process pipeline task: abort it (and its adapter call).
|
|
1008
|
+
const cancelled = runnerRef?.cancelTask(idStr) ?? false;
|
|
1009
|
+
res.writeHead(cancelled ? 200 : 404, { 'Content-Type': 'application/json' });
|
|
1010
|
+
res.end(JSON.stringify({ ok: cancelled }));
|
|
1011
|
+
}
|
|
1012
|
+
// ---- Scan Paths: list ----
|
|
1013
|
+
}
|
|
1014
|
+
else if (url === '/api/scan-paths' && req.method === 'GET') {
|
|
1015
|
+
// removedConfigPaths에 있는 경로는 UI에 표시하지 않음
|
|
1016
|
+
const allConfigPaths = runnerRef?.getAllowedProjects() ?? [];
|
|
1017
|
+
const configPaths = allConfigPaths.filter(p => !removedConfigPaths.has(p));
|
|
1022
1018
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1023
1019
|
res.end(JSON.stringify({
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
name: basename(absolute) || absolute,
|
|
1027
|
-
entries,
|
|
1020
|
+
configPaths,
|
|
1021
|
+
customPaths: Array.from(customBasePaths),
|
|
1028
1022
|
}));
|
|
1023
|
+
// ---- Scan Paths: add ----
|
|
1029
1024
|
}
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1025
|
+
else if (url === '/api/scan-paths' && req.method === 'POST') {
|
|
1026
|
+
const body = await readBody(req);
|
|
1027
|
+
try {
|
|
1028
|
+
const { path: newPath } = JSON.parse(body);
|
|
1029
|
+
if (typeof newPath !== 'string' || !newPath.trim()) {
|
|
1030
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1031
|
+
res.end(JSON.stringify({ error: 'Missing path' }));
|
|
1032
|
+
}
|
|
1033
|
+
else {
|
|
1034
|
+
customBasePaths.add(newPath.trim());
|
|
1035
|
+
invalidateProjectCache();
|
|
1036
|
+
// Update runner's allowedProjects with merged list
|
|
1037
|
+
const configPaths = runnerRef?.getAllowedProjects() ?? [];
|
|
1038
|
+
const merged = [...new Set([...configPaths, ...customBasePaths])];
|
|
1039
|
+
runnerRef?.updateAllowedProjects(merged);
|
|
1040
|
+
saveReposConfig();
|
|
1041
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1042
|
+
res.end(JSON.stringify({ ok: true }));
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
catch {
|
|
1047
1046
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1048
|
-
res.end(JSON.stringify({ error: '
|
|
1049
|
-
return;
|
|
1047
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
1050
1048
|
}
|
|
1051
|
-
|
|
1052
|
-
res.writeHead(202, { 'Content-Type': 'application/json' });
|
|
1053
|
-
res.end(JSON.stringify({ taskId, status: 'queued' }));
|
|
1049
|
+
// ---- Scan Paths: remove ----
|
|
1054
1050
|
}
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1051
|
+
else if (url.startsWith('/api/scan-paths/') && req.method === 'DELETE') {
|
|
1052
|
+
const encodedPath = url.replace('/api/scan-paths/', '');
|
|
1053
|
+
const decodedPath = decodeURIComponent(encodedPath);
|
|
1054
|
+
// customPaths에서 제거
|
|
1055
|
+
customBasePaths.delete(decodedPath);
|
|
1056
|
+
// configPaths에서도 제거: removedConfigPaths에 기록하고 runner에서 즉시 반영
|
|
1057
|
+
const allConfigPaths = runnerRef?.getAllowedProjects() ?? [];
|
|
1058
|
+
if (allConfigPaths.includes(decodedPath)) {
|
|
1059
|
+
removedConfigPaths.add(decodedPath);
|
|
1060
|
+
runnerRef?.updateAllowedProjects(allConfigPaths.filter(p => p !== decodedPath));
|
|
1061
|
+
}
|
|
1062
|
+
invalidateProjectCache();
|
|
1063
|
+
saveReposConfig();
|
|
1064
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1065
|
+
res.end(JSON.stringify({ ok: true }));
|
|
1066
|
+
// ---- Filesystem browse (folder picker) ----
|
|
1067
|
+
// GET /api/fs/list?path=<absolute or ~/...>
|
|
1068
|
+
// Returns: { path, parent, entries: [{name, isDir}] } — dotfiles excluded, dirs first.
|
|
1058
1069
|
}
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1070
|
+
else if (url.startsWith('/api/fs/list') && req.method === 'GET') {
|
|
1071
|
+
try {
|
|
1072
|
+
const requested = requestUrl.searchParams.get('path')?.trim();
|
|
1073
|
+
const startPath = requested && requested.length > 0
|
|
1074
|
+
? requested
|
|
1075
|
+
: homedir();
|
|
1076
|
+
const expanded = startPath.startsWith('~')
|
|
1077
|
+
? join(homedir(), startPath.slice(1))
|
|
1078
|
+
: startPath;
|
|
1079
|
+
const absolute = resolvePath(expanded);
|
|
1080
|
+
const st = await stat(absolute);
|
|
1081
|
+
if (!st.isDirectory()) {
|
|
1082
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1083
|
+
res.end(JSON.stringify({ error: 'Not a directory', path: absolute }));
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
const raw = await readdir(absolute, { withFileTypes: true });
|
|
1087
|
+
const entries = raw
|
|
1088
|
+
.filter((d) => !d.name.startsWith('.'))
|
|
1089
|
+
.map((d) => ({ name: d.name, isDir: d.isDirectory() }))
|
|
1090
|
+
.sort((a, b) => {
|
|
1091
|
+
if (a.isDir !== b.isDir)
|
|
1092
|
+
return a.isDir ? -1 : 1;
|
|
1093
|
+
return a.name.localeCompare(b.name);
|
|
1094
|
+
});
|
|
1095
|
+
const parent = dirname(absolute);
|
|
1096
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1097
|
+
res.end(JSON.stringify({
|
|
1098
|
+
path: absolute,
|
|
1099
|
+
parent: parent === absolute ? null : parent,
|
|
1100
|
+
name: basename(absolute) || absolute,
|
|
1101
|
+
entries,
|
|
1102
|
+
}));
|
|
1103
|
+
}
|
|
1104
|
+
catch (e) {
|
|
1105
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1106
|
+
res.end(JSON.stringify({ error: safeErrorMessage(e) }));
|
|
1107
|
+
}
|
|
1108
|
+
// ---- Discord history (legacy) ----
|
|
1109
|
+
}
|
|
1110
|
+
else if (url === '/api/history') {
|
|
1111
|
+
const history = await getChatHistory();
|
|
1112
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1113
|
+
res.end(JSON.stringify(history));
|
|
1114
|
+
// ---- Exec: submit task ----
|
|
1115
|
+
}
|
|
1116
|
+
else if (url === '/api/exec' && req.method === 'POST') {
|
|
1117
|
+
const body = await readBody(req);
|
|
1118
|
+
try {
|
|
1119
|
+
const { prompt, projectPath, pipeline, workerOnly, model } = JSON.parse(body);
|
|
1120
|
+
if (!prompt?.trim()) {
|
|
1121
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1122
|
+
res.end(JSON.stringify({ error: 'Missing prompt' }));
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
const taskId = startExecTask(prompt, { projectPath, pipeline, workerOnly, model });
|
|
1126
|
+
res.writeHead(202, { 'Content-Type': 'application/json' });
|
|
1127
|
+
res.end(JSON.stringify({ taskId, status: 'queued' }));
|
|
1128
|
+
}
|
|
1129
|
+
catch {
|
|
1066
1130
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1067
|
-
res.end(JSON.stringify({ error: '
|
|
1068
|
-
return;
|
|
1131
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
1069
1132
|
}
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
if (source) {
|
|
1080
|
-
const parent = await source.createTask(goal, `Planned via the \`/plan\` cockpit.\n\n${tasks.length} sub-task(s) dispatched.`);
|
|
1081
|
-
if ('error' in parent) {
|
|
1082
|
-
res.writeHead(502, { 'Content-Type': 'application/json' });
|
|
1083
|
-
res.end(JSON.stringify({ error: `Task source: ${parent.error}` }));
|
|
1133
|
+
// ---- Plan dispatch: TUI /plan cockpit → daemon loop ----
|
|
1134
|
+
}
|
|
1135
|
+
else if (url === '/api/plan/dispatch' && req.method === 'POST') {
|
|
1136
|
+
const body = await readBody(req);
|
|
1137
|
+
try {
|
|
1138
|
+
const { goal, projectPath, subTasks } = JSON.parse(body);
|
|
1139
|
+
if (!goal?.trim()) {
|
|
1140
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1141
|
+
res.end(JSON.stringify({ error: 'Missing goal' }));
|
|
1084
1142
|
return;
|
|
1085
1143
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1144
|
+
const resolvedPath = projectPath ?? process.cwd();
|
|
1145
|
+
const tasks = Array.isArray(subTasks) ? subTasks : [];
|
|
1146
|
+
const triggerHeartbeat = () => {
|
|
1147
|
+
runnerRef?.heartbeat().catch((e) => console.error('[Web] plan heartbeat error:', e));
|
|
1148
|
+
};
|
|
1149
|
+
// Path A — a task source is registered (Linear OR local SQLite): create a
|
|
1150
|
+
// parent issue + dependency-wired sub-issues (reusing the autonomous
|
|
1151
|
+
// engine, which routes through the same source), then heartbeat.
|
|
1152
|
+
const source = getTaskSource();
|
|
1153
|
+
if (source) {
|
|
1154
|
+
const parent = await source.createTask(goal, `Planned via the \`/plan\` cockpit.\n\n${tasks.length} sub-task(s) dispatched.`);
|
|
1155
|
+
if ('error' in parent) {
|
|
1156
|
+
res.writeHead(502, { 'Content-Type': 'application/json' });
|
|
1157
|
+
res.end(JSON.stringify({ error: `Task source: ${parent.error}` }));
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
if (tasks.length === 0) {
|
|
1161
|
+
// Planner saw no decomposition — run the goal itself as one task.
|
|
1162
|
+
await source.updateState(parent.id, 'Todo').catch(() => { });
|
|
1163
|
+
triggerHeartbeat();
|
|
1164
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1165
|
+
res.end(JSON.stringify({
|
|
1166
|
+
mode: source.kind,
|
|
1167
|
+
parentIssue: { id: parent.id, identifier: parent.identifier },
|
|
1168
|
+
subIssues: [],
|
|
1169
|
+
}));
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
const totalMinutes = tasks.reduce((sum, t) => sum + (t.estimatedMinutes || 0), 0);
|
|
1173
|
+
await createSubIssuesWithDependencies(parent.id, { title: goal }, tasks, totalMinutes, { reportToDiscord: () => { }, scheduleNextHeartbeat: triggerHeartbeat }, parent.id, 20);
|
|
1090
1174
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1091
1175
|
res.end(JSON.stringify({
|
|
1092
1176
|
mode: source.kind,
|
|
1093
1177
|
parentIssue: { id: parent.id, identifier: parent.identifier },
|
|
1094
|
-
subIssues: [],
|
|
1095
1178
|
}));
|
|
1096
1179
|
return;
|
|
1097
1180
|
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1181
|
+
// Path B (fallback) — no Linear: run each sub-task via the exec pipeline.
|
|
1182
|
+
const items = tasks.length > 0
|
|
1183
|
+
? tasks
|
|
1184
|
+
: [{ title: goal, description: goal, estimatedMinutes: 0, priority: 3 }];
|
|
1185
|
+
const taskIds = items.map((st) => startExecTask(`${st.title}\n\n${st.description ?? ''}`.trim(), {
|
|
1186
|
+
projectPath: resolvedPath,
|
|
1187
|
+
pipeline: true,
|
|
1188
|
+
}));
|
|
1189
|
+
res.writeHead(202, { 'Content-Type': 'application/json' });
|
|
1190
|
+
res.end(JSON.stringify({ mode: 'exec', taskIds }));
|
|
1191
|
+
}
|
|
1192
|
+
catch (err) {
|
|
1193
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1194
|
+
res.end(JSON.stringify({ error: err instanceof Error ? err.message : 'Invalid JSON' }));
|
|
1195
|
+
}
|
|
1196
|
+
// ---- Exec: task status ----
|
|
1197
|
+
}
|
|
1198
|
+
else if (url.startsWith('/api/exec/') && req.method === 'GET') {
|
|
1199
|
+
const taskId = url.replace('/api/exec/', '');
|
|
1200
|
+
const entry = execTasks.get(taskId);
|
|
1201
|
+
if (!entry) {
|
|
1202
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1203
|
+
res.end(JSON.stringify({ error: 'Task not found' }));
|
|
1204
|
+
}
|
|
1205
|
+
else {
|
|
1100
1206
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1101
1207
|
res.end(JSON.stringify({
|
|
1102
|
-
|
|
1103
|
-
|
|
1208
|
+
taskId: entry.taskId,
|
|
1209
|
+
status: entry.status,
|
|
1210
|
+
currentStage: entry.currentStage,
|
|
1211
|
+
result: entry.result,
|
|
1212
|
+
error: entry.error,
|
|
1104
1213
|
}));
|
|
1105
|
-
return;
|
|
1106
1214
|
}
|
|
1107
|
-
// Path B (fallback) — no Linear: run each sub-task via the exec pipeline.
|
|
1108
|
-
const items = tasks.length > 0
|
|
1109
|
-
? tasks
|
|
1110
|
-
: [{ title: goal, description: goal, estimatedMinutes: 0, priority: 3 }];
|
|
1111
|
-
const taskIds = items.map((st) => startExecTask(`${st.title}\n\n${st.description ?? ''}`.trim(), {
|
|
1112
|
-
projectPath: resolvedPath,
|
|
1113
|
-
pipeline: true,
|
|
1114
|
-
}));
|
|
1115
|
-
res.writeHead(202, { 'Content-Type': 'application/json' });
|
|
1116
|
-
res.end(JSON.stringify({ mode: 'exec', taskIds }));
|
|
1117
|
-
}
|
|
1118
|
-
catch (err) {
|
|
1119
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1120
|
-
res.end(JSON.stringify({ error: err instanceof Error ? err.message : 'Invalid JSON' }));
|
|
1121
|
-
}
|
|
1122
|
-
// ---- Exec: task status ----
|
|
1123
|
-
}
|
|
1124
|
-
else if (url.startsWith('/api/exec/') && req.method === 'GET') {
|
|
1125
|
-
const taskId = url.replace('/api/exec/', '');
|
|
1126
|
-
const entry = execTasks.get(taskId);
|
|
1127
|
-
if (!entry) {
|
|
1128
|
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1129
|
-
res.end(JSON.stringify({ error: 'Task not found' }));
|
|
1130
1215
|
}
|
|
1131
1216
|
else {
|
|
1132
|
-
res.writeHead(
|
|
1133
|
-
res.end(
|
|
1134
|
-
taskId: entry.taskId,
|
|
1135
|
-
status: entry.status,
|
|
1136
|
-
currentStage: entry.currentStage,
|
|
1137
|
-
result: entry.result,
|
|
1138
|
-
error: entry.error,
|
|
1139
|
-
}));
|
|
1217
|
+
res.writeHead(404);
|
|
1218
|
+
res.end('Not Found');
|
|
1140
1219
|
}
|
|
1141
1220
|
}
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
res
|
|
1221
|
+
catch (err) {
|
|
1222
|
+
const statusCode = err instanceof HttpError ? err.statusCode : 500;
|
|
1223
|
+
writeJson(res, statusCode, {
|
|
1224
|
+
error: err instanceof HttpError ? err.message : safeErrorMessage(err),
|
|
1225
|
+
});
|
|
1145
1226
|
}
|
|
1146
1227
|
});
|
|
1147
1228
|
server.on('error', (err) => {
|