agentgui 1.0.382 → 1.0.384
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/acp-queries.js +22 -2
- package/package.json +1 -1
- package/server.js +363 -365
package/acp-queries.js
CHANGED
|
@@ -144,8 +144,28 @@ export function createACPQueries(db, prep) {
|
|
|
144
144
|
const ths = rows.map(r => ({ thread_id: r.id, created_at: iso(r.created_at), updated_at: iso(r.updated_at), metadata: jp(r.metadata), status: r.status || 'idle' }));
|
|
145
145
|
return { threads: ths, total: tot, limit, offset, hasMore: offset + limit < tot };
|
|
146
146
|
},
|
|
147
|
-
searchAgents(flt = {}) {
|
|
148
|
-
|
|
147
|
+
searchAgents(agents, flt = {}) {
|
|
148
|
+
const { name, version, capabilities, limit = 50, offset = 0 } = flt;
|
|
149
|
+
let results = agents;
|
|
150
|
+
if (name) {
|
|
151
|
+
const n = name.toLowerCase();
|
|
152
|
+
results = results.filter(a => a.name.toLowerCase().includes(n) || a.id.toLowerCase().includes(n));
|
|
153
|
+
}
|
|
154
|
+
if (capabilities) {
|
|
155
|
+
results = results.filter(a => {
|
|
156
|
+
const desc = this.getAgentDescriptor ? this.getAgentDescriptor(a.id) : null;
|
|
157
|
+
if (!desc) return false;
|
|
158
|
+
const caps = desc.specs?.capabilities || {};
|
|
159
|
+
if (capabilities.streaming !== undefined && !caps.streaming) return false;
|
|
160
|
+
if (capabilities.threads !== undefined && caps.threads !== capabilities.threads) return false;
|
|
161
|
+
if (capabilities.interrupts !== undefined && caps.interrupts !== capabilities.interrupts) return false;
|
|
162
|
+
return true;
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
const total = results.length;
|
|
166
|
+
const paginated = results.slice(offset, offset + limit);
|
|
167
|
+
const agents_list = paginated.map(a => ({ agent_id: a.id, name: a.name, version: version || '1.0.0', path: a.path }));
|
|
168
|
+
return { agents: agents_list, total, limit, offset, hasMore: offset + limit < total };
|
|
149
169
|
},
|
|
150
170
|
searchRuns(flt = {}) {
|
|
151
171
|
const { agent_id, thread_id, status, limit = 50, offset = 0 } = flt;
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -340,6 +340,8 @@ function discoverAgents() {
|
|
|
340
340
|
|
|
341
341
|
const discoveredAgents = discoverAgents();
|
|
342
342
|
initializeDescriptors(discoveredAgents);
|
|
343
|
+
const acpQueries = createACPQueries(db, prepare);
|
|
344
|
+
acpQueries.getAgentDescriptor = getAgentDescriptor;
|
|
343
345
|
|
|
344
346
|
const modelCache = new Map();
|
|
345
347
|
|
|
@@ -1444,9 +1446,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
1444
1446
|
return;
|
|
1445
1447
|
}
|
|
1446
1448
|
|
|
1447
|
-
const
|
|
1448
|
-
if (
|
|
1449
|
-
|
|
1449
|
+
const oldRunByIdMatch = pathOnly.match(/^\/api\/runs\/([^/]+)$/);
|
|
1450
|
+
if (oldRunByIdMatch) {
|
|
1451
|
+
const runId = oldRunByIdMatch[1];
|
|
1450
1452
|
const session = queries.getSession(runId);
|
|
1451
1453
|
|
|
1452
1454
|
if (!session) {
|
|
@@ -1511,9 +1513,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
1511
1513
|
}
|
|
1512
1514
|
}
|
|
1513
1515
|
|
|
1514
|
-
const
|
|
1515
|
-
if (
|
|
1516
|
-
const runId =
|
|
1516
|
+
const oldRunCancelMatch = pathOnly.match(/^\/api\/runs\/([^/]+)\/cancel$/);
|
|
1517
|
+
if (oldRunCancelMatch && req.method === 'POST') {
|
|
1518
|
+
const runId = oldRunCancelMatch[1];
|
|
1517
1519
|
const session = queries.getSession(runId);
|
|
1518
1520
|
|
|
1519
1521
|
if (!session) {
|
|
@@ -1767,34 +1769,10 @@ const server = http.createServer(async (req, res) => {
|
|
|
1767
1769
|
return;
|
|
1768
1770
|
}
|
|
1769
1771
|
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
let parsed = {};
|
|
1775
|
-
try { parsed = body ? JSON.parse(body) : {}; } catch {}
|
|
1776
|
-
|
|
1777
|
-
const { query } = parsed;
|
|
1778
|
-
let results = discoveredAgents;
|
|
1779
|
-
|
|
1780
|
-
if (query) {
|
|
1781
|
-
const q = query.toLowerCase();
|
|
1782
|
-
results = discoveredAgents.filter(a =>
|
|
1783
|
-
a.name.toLowerCase().includes(q) ||
|
|
1784
|
-
a.id.toLowerCase().includes(q) ||
|
|
1785
|
-
(a.description && a.description.toLowerCase().includes(q))
|
|
1786
|
-
);
|
|
1787
|
-
}
|
|
1788
|
-
|
|
1789
|
-
const agents = results.map(a => ({
|
|
1790
|
-
id: a.id,
|
|
1791
|
-
name: a.name,
|
|
1792
|
-
description: a.description || '',
|
|
1793
|
-
icon: a.icon || null,
|
|
1794
|
-
status: 'available'
|
|
1795
|
-
}));
|
|
1796
|
-
|
|
1797
|
-
sendJSON(req, res, 200, agents);
|
|
1772
|
+
if (pathOnly === '/api/agents/search' && req.method === 'POST') {
|
|
1773
|
+
const body = await parseBody(req);
|
|
1774
|
+
const result = acpQueries.searchAgents(discoveredAgents, body);
|
|
1775
|
+
sendJSON(req, res, 200, result);
|
|
1798
1776
|
return;
|
|
1799
1777
|
}
|
|
1800
1778
|
|
|
@@ -1915,6 +1893,214 @@ const server = http.createServer(async (req, res) => {
|
|
|
1915
1893
|
return;
|
|
1916
1894
|
}
|
|
1917
1895
|
|
|
1896
|
+
if (pathOnly === '/api/runs' && req.method === 'POST') {
|
|
1897
|
+
const body = await parseBody(req);
|
|
1898
|
+
const { agent_id, input, config, webhook_url } = body;
|
|
1899
|
+
if (!agent_id) {
|
|
1900
|
+
sendJSON(req, res, 422, { error: 'agent_id is required' });
|
|
1901
|
+
return;
|
|
1902
|
+
}
|
|
1903
|
+
const agent = discoveredAgents.find(a => a.id === agent_id);
|
|
1904
|
+
if (!agent) {
|
|
1905
|
+
sendJSON(req, res, 404, { error: 'Agent not found' });
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
const run = acpQueries.createRun(agent_id, null, input, config, webhook_url);
|
|
1909
|
+
sendJSON(req, res, 201, run);
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
if (pathOnly === '/api/runs/search' && req.method === 'POST') {
|
|
1914
|
+
const body = await parseBody(req);
|
|
1915
|
+
const result = acpQueries.searchRuns(body);
|
|
1916
|
+
sendJSON(req, res, 200, result);
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
if (pathOnly === '/api/runs/stream' && req.method === 'POST') {
|
|
1921
|
+
const body = await parseBody(req);
|
|
1922
|
+
const { agent_id, input, config } = body;
|
|
1923
|
+
if (!agent_id) {
|
|
1924
|
+
sendJSON(req, res, 422, { error: 'agent_id is required' });
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
const agent = discoveredAgents.find(a => a.id === agent_id);
|
|
1928
|
+
if (!agent) {
|
|
1929
|
+
sendJSON(req, res, 404, { error: 'Agent not found' });
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
const run = acpQueries.createRun(agent_id, null, input, config);
|
|
1933
|
+
res.writeHead(200, {
|
|
1934
|
+
'Content-Type': 'text/event-stream',
|
|
1935
|
+
'Cache-Control': 'no-cache',
|
|
1936
|
+
'Connection': 'keep-alive'
|
|
1937
|
+
});
|
|
1938
|
+
res.write('data: ' + JSON.stringify({ type: 'run_created', run_id: run.run_id }) + '\n\n');
|
|
1939
|
+
const eventHandler = (eventData) => {
|
|
1940
|
+
if (eventData.sessionId === run.run_id || eventData.conversationId === run.thread_id) {
|
|
1941
|
+
res.write('data: ' + JSON.stringify(eventData) + '\n\n');
|
|
1942
|
+
}
|
|
1943
|
+
};
|
|
1944
|
+
const cleanup = () => {
|
|
1945
|
+
res.end();
|
|
1946
|
+
};
|
|
1947
|
+
req.on('close', cleanup);
|
|
1948
|
+
const statelessThreadId = acpQueries.getRun(run.run_id)?.thread_id;
|
|
1949
|
+
if (statelessThreadId) {
|
|
1950
|
+
const conv = queries.getConversation(statelessThreadId);
|
|
1951
|
+
if (conv && input?.content) {
|
|
1952
|
+
runClaudeWithStreaming(agent_id, statelessThreadId, input.content, config?.model || null).catch(() => {});
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
if (pathOnly === '/api/runs/wait' && req.method === 'POST') {
|
|
1959
|
+
const body = await parseBody(req);
|
|
1960
|
+
const { agent_id, input, config } = body;
|
|
1961
|
+
if (!agent_id) {
|
|
1962
|
+
sendJSON(req, res, 422, { error: 'agent_id is required' });
|
|
1963
|
+
return;
|
|
1964
|
+
}
|
|
1965
|
+
const agent = discoveredAgents.find(a => a.id === agent_id);
|
|
1966
|
+
if (!agent) {
|
|
1967
|
+
sendJSON(req, res, 404, { error: 'Agent not found' });
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
const run = acpQueries.createRun(agent_id, null, input, config);
|
|
1971
|
+
const statelessThreadId = acpQueries.getRun(run.run_id)?.thread_id;
|
|
1972
|
+
if (statelessThreadId && input?.content) {
|
|
1973
|
+
try {
|
|
1974
|
+
await runClaudeWithStreaming(agent_id, statelessThreadId, input.content, config?.model || null);
|
|
1975
|
+
const finalRun = acpQueries.getRun(run.run_id);
|
|
1976
|
+
sendJSON(req, res, 200, finalRun);
|
|
1977
|
+
} catch (err) {
|
|
1978
|
+
acpQueries.updateRunStatus(run.run_id, 'error');
|
|
1979
|
+
sendJSON(req, res, 500, { error: err.message });
|
|
1980
|
+
}
|
|
1981
|
+
} else {
|
|
1982
|
+
sendJSON(req, res, 200, run);
|
|
1983
|
+
}
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
const oldRunByIdMatch1 = pathOnly.match(/^\/api\/runs\/([^/]+)$/);
|
|
1988
|
+
if (oldRunByIdMatch1) {
|
|
1989
|
+
const runId = oldRunByIdMatch1[1];
|
|
1990
|
+
|
|
1991
|
+
if (req.method === 'GET') {
|
|
1992
|
+
const run = acpQueries.getRun(runId);
|
|
1993
|
+
if (!run) {
|
|
1994
|
+
sendJSON(req, res, 404, { error: 'Run not found' });
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
sendJSON(req, res, 200, run);
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
if (req.method === 'POST') {
|
|
2002
|
+
const body = await parseBody(req);
|
|
2003
|
+
const run = acpQueries.getRun(runId);
|
|
2004
|
+
if (!run) {
|
|
2005
|
+
sendJSON(req, res, 404, { error: 'Run not found' });
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
if (run.status !== 'pending') {
|
|
2009
|
+
sendJSON(req, res, 409, { error: 'Run is not resumable' });
|
|
2010
|
+
return;
|
|
2011
|
+
}
|
|
2012
|
+
if (body.input?.content && run.thread_id) {
|
|
2013
|
+
runClaudeWithStreaming(run.agent_id, run.thread_id, body.input.content, null).catch(() => {});
|
|
2014
|
+
}
|
|
2015
|
+
sendJSON(req, res, 200, run);
|
|
2016
|
+
return;
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
if (req.method === 'DELETE') {
|
|
2020
|
+
try {
|
|
2021
|
+
acpQueries.deleteRun(runId);
|
|
2022
|
+
res.writeHead(204);
|
|
2023
|
+
res.end();
|
|
2024
|
+
} catch (err) {
|
|
2025
|
+
sendJSON(req, res, 404, { error: 'Run not found' });
|
|
2026
|
+
}
|
|
2027
|
+
return;
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
const runWaitMatch = pathOnly.match(/^\/api\/runs\/([^/]+)\/wait$/);
|
|
2032
|
+
if (runWaitMatch && req.method === 'GET') {
|
|
2033
|
+
const runId = runWaitMatch[1];
|
|
2034
|
+
const run = acpQueries.getRun(runId);
|
|
2035
|
+
if (!run) {
|
|
2036
|
+
sendJSON(req, res, 404, { error: 'Run not found' });
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
2039
|
+
const startTime = Date.now();
|
|
2040
|
+
const pollInterval = setInterval(() => {
|
|
2041
|
+
const currentRun = acpQueries.getRun(runId);
|
|
2042
|
+
if (!currentRun || ['success', 'error', 'cancelled'].includes(currentRun.status) || (Date.now() - startTime) > 30000) {
|
|
2043
|
+
clearInterval(pollInterval);
|
|
2044
|
+
sendJSON(req, res, 200, currentRun || run);
|
|
2045
|
+
}
|
|
2046
|
+
}, 500);
|
|
2047
|
+
req.on('close', () => clearInterval(pollInterval));
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
const runStreamMatch = pathOnly.match(/^\/api\/runs\/([^/]+)\/stream$/);
|
|
2052
|
+
if (runStreamMatch && req.method === 'GET') {
|
|
2053
|
+
const runId = runStreamMatch[1];
|
|
2054
|
+
const run = acpQueries.getRun(runId);
|
|
2055
|
+
if (!run) {
|
|
2056
|
+
sendJSON(req, res, 404, { error: 'Run not found' });
|
|
2057
|
+
return;
|
|
2058
|
+
}
|
|
2059
|
+
res.writeHead(200, {
|
|
2060
|
+
'Content-Type': 'text/event-stream',
|
|
2061
|
+
'Cache-Control': 'no-cache',
|
|
2062
|
+
'Connection': 'keep-alive'
|
|
2063
|
+
});
|
|
2064
|
+
res.write('data: ' + JSON.stringify({ type: 'joined', run_id: runId }) + '\n\n');
|
|
2065
|
+
const eventHandler = (eventData) => {
|
|
2066
|
+
if (eventData.sessionId === runId || eventData.conversationId === run.thread_id) {
|
|
2067
|
+
res.write('data: ' + JSON.stringify(eventData) + '\n\n');
|
|
2068
|
+
}
|
|
2069
|
+
};
|
|
2070
|
+
const cleanup = () => {
|
|
2071
|
+
res.end();
|
|
2072
|
+
};
|
|
2073
|
+
req.on('close', cleanup);
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
const oldRunCancelMatch1 = pathOnly.match(/^\/api\/runs\/([^/]+)\/cancel$/);
|
|
2078
|
+
if (oldRunCancelMatch1 && req.method === 'POST') {
|
|
2079
|
+
const runId = oldRunCancelMatch1[1];
|
|
2080
|
+
try {
|
|
2081
|
+
const run = acpQueries.cancelRun(runId);
|
|
2082
|
+
const execution = activeExecutions.get(run.thread_id);
|
|
2083
|
+
if (execution?.process) {
|
|
2084
|
+
execution.process.kill('SIGTERM');
|
|
2085
|
+
setTimeout(() => {
|
|
2086
|
+
if (execution.process && !execution.process.killed) {
|
|
2087
|
+
execution.process.kill('SIGKILL');
|
|
2088
|
+
}
|
|
2089
|
+
}, 5000);
|
|
2090
|
+
}
|
|
2091
|
+
sendJSON(req, res, 200, run);
|
|
2092
|
+
} catch (err) {
|
|
2093
|
+
if (err.message === 'Run not found') {
|
|
2094
|
+
sendJSON(req, res, 404, { error: err.message });
|
|
2095
|
+
} else if (err.message.includes('already completed')) {
|
|
2096
|
+
sendJSON(req, res, 409, { error: err.message });
|
|
2097
|
+
} else {
|
|
2098
|
+
sendJSON(req, res, 500, { error: err.message });
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
2103
|
+
|
|
1918
2104
|
if (pathOnly === '/api/gemini-oauth/start' && req.method === 'POST') {
|
|
1919
2105
|
try {
|
|
1920
2106
|
const result = await startGeminiOAuth(req);
|
|
@@ -2139,336 +2325,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
2139
2325
|
return;
|
|
2140
2326
|
}
|
|
2141
2327
|
|
|
2142
|
-
const threadsMatch = pathOnly.match(/^\/api\/threads$/);
|
|
2143
|
-
if (threadsMatch && req.method === 'POST') {
|
|
2144
|
-
let body = '';
|
|
2145
|
-
for await (const chunk of req) { body += chunk; }
|
|
2146
|
-
let parsed = {};
|
|
2147
|
-
try { parsed = body ? JSON.parse(body) : {}; } catch {}
|
|
2148
|
-
|
|
2149
|
-
const thread = queries.createConversation(parsed.agentId || 'claude-code', parsed.title || 'New Thread', parsed.workingDirectory || STARTUP_CWD);
|
|
2150
|
-
sendJSON(req, res, 200, {
|
|
2151
|
-
id: thread.id,
|
|
2152
|
-
agentId: thread.agentId,
|
|
2153
|
-
title: thread.title,
|
|
2154
|
-
created_at: thread.created_at,
|
|
2155
|
-
status: thread.status,
|
|
2156
|
-
state: null
|
|
2157
|
-
});
|
|
2158
|
-
return;
|
|
2159
|
-
}
|
|
2160
|
-
|
|
2161
|
-
const threadsSearchMatch = pathOnly.match(/^\/api\/threads\/search$/);
|
|
2162
|
-
if (threadsSearchMatch && req.method === 'POST') {
|
|
2163
|
-
const conversations = queries.getConversations();
|
|
2164
|
-
const threads = conversations.map(c => ({
|
|
2165
|
-
id: c.id,
|
|
2166
|
-
agentId: c.agentId,
|
|
2167
|
-
title: c.title,
|
|
2168
|
-
created_at: c.created_at,
|
|
2169
|
-
updated_at: c.updated_at,
|
|
2170
|
-
status: c.status,
|
|
2171
|
-
state: null
|
|
2172
|
-
}));
|
|
2173
|
-
sendJSON(req, res, 200, threads);
|
|
2174
|
-
return;
|
|
2175
|
-
}
|
|
2176
|
-
|
|
2177
|
-
const threadByIdMatch = pathOnly.match(/^\/api\/threads\/([^/]+)$/);
|
|
2178
|
-
if (threadByIdMatch) {
|
|
2179
|
-
const threadId = threadByIdMatch[1];
|
|
2180
|
-
const conv = queries.getConversation(threadId);
|
|
2181
|
-
|
|
2182
|
-
if (!conv) {
|
|
2183
|
-
sendJSON(req, res, 404, { error: 'Thread not found' });
|
|
2184
|
-
return;
|
|
2185
|
-
}
|
|
2186
|
-
|
|
2187
|
-
if (req.method === 'GET') {
|
|
2188
|
-
sendJSON(req, res, 200, {
|
|
2189
|
-
id: conv.id,
|
|
2190
|
-
agentId: conv.agentId,
|
|
2191
|
-
title: conv.title,
|
|
2192
|
-
created_at: conv.created_at,
|
|
2193
|
-
updated_at: conv.updated_at,
|
|
2194
|
-
status: conv.status,
|
|
2195
|
-
state: null
|
|
2196
|
-
});
|
|
2197
|
-
return;
|
|
2198
|
-
}
|
|
2199
|
-
|
|
2200
|
-
if (req.method === 'DELETE') {
|
|
2201
|
-
const activeEntry = activeExecutions.get(threadId);
|
|
2202
|
-
if (activeEntry) {
|
|
2203
|
-
sendJSON(req, res, 409, { error: 'Thread has an active run, cannot delete' });
|
|
2204
|
-
return;
|
|
2205
|
-
}
|
|
2206
|
-
queries.deleteConversation(threadId);
|
|
2207
|
-
sendJSON(req, res, 204, {});
|
|
2208
|
-
return;
|
|
2209
|
-
}
|
|
2210
|
-
|
|
2211
|
-
if (req.method === 'PATCH') {
|
|
2212
|
-
let body = '';
|
|
2213
|
-
for await (const chunk of req) { body += chunk; }
|
|
2214
|
-
let parsed = {};
|
|
2215
|
-
try { parsed = body ? JSON.parse(body) : {}; } catch {}
|
|
2216
|
-
|
|
2217
|
-
const updates = {};
|
|
2218
|
-
if (parsed.title !== undefined) updates.title = parsed.title;
|
|
2219
|
-
if (parsed.state !== undefined) updates.state = parsed.state;
|
|
2220
|
-
|
|
2221
|
-
if (Object.keys(updates).length > 0) {
|
|
2222
|
-
queries.updateConversation(threadId, updates);
|
|
2223
|
-
}
|
|
2224
|
-
|
|
2225
|
-
const updated = queries.getConversation(threadId);
|
|
2226
|
-
sendJSON(req, res, 200, {
|
|
2227
|
-
id: updated.id,
|
|
2228
|
-
agentId: updated.agentId,
|
|
2229
|
-
title: updated.title,
|
|
2230
|
-
created_at: updated.created_at,
|
|
2231
|
-
updated_at: updated.updated_at,
|
|
2232
|
-
status: updated.status,
|
|
2233
|
-
state: updated.state
|
|
2234
|
-
});
|
|
2235
|
-
return;
|
|
2236
|
-
}
|
|
2237
|
-
}
|
|
2238
|
-
|
|
2239
|
-
const threadHistoryMatch = pathOnly.match(/^\/api\/threads\/([^/]+)\/history$/);
|
|
2240
|
-
if (threadHistoryMatch && req.method === 'GET') {
|
|
2241
|
-
const threadId = threadHistoryMatch[1];
|
|
2242
|
-
const conv = queries.getConversation(threadId);
|
|
2243
|
-
|
|
2244
|
-
if (!conv) {
|
|
2245
|
-
sendJSON(req, res, 404, { error: 'Thread not found' });
|
|
2246
|
-
return;
|
|
2247
|
-
}
|
|
2248
|
-
|
|
2249
|
-
const limit = parseInt(new URL(req.url, 'http://localhost').searchParams.get('limit') || '10', 10);
|
|
2250
|
-
const sessions = queries.getSessionsByConversation(threadId, limit);
|
|
2251
|
-
|
|
2252
|
-
const history = sessions.map(s => ({
|
|
2253
|
-
checkpoint: s.id,
|
|
2254
|
-
state: null,
|
|
2255
|
-
created_at: s.started_at,
|
|
2256
|
-
runId: s.id,
|
|
2257
|
-
status: s.status
|
|
2258
|
-
})).reverse();
|
|
2259
|
-
|
|
2260
|
-
sendJSON(req, res, 200, history);
|
|
2261
|
-
return;
|
|
2262
|
-
}
|
|
2263
|
-
|
|
2264
|
-
const threadCopyMatch = pathOnly.match(/^\/api\/threads\/([^/]+)\/copy$/);
|
|
2265
|
-
if (threadCopyMatch && req.method === 'POST') {
|
|
2266
|
-
const threadId = threadCopyMatch[1];
|
|
2267
|
-
const original = queries.getConversation(threadId);
|
|
2268
|
-
|
|
2269
|
-
if (!original) {
|
|
2270
|
-
sendJSON(req, res, 404, { error: 'Thread not found' });
|
|
2271
|
-
return;
|
|
2272
|
-
}
|
|
2273
|
-
|
|
2274
|
-
const newThread = queries.createConversation(original.agentId, original.title + ' (copy)', original.workingDirectory);
|
|
2275
|
-
|
|
2276
|
-
const messages = queries.getMessages(threadId, 1000, 0);
|
|
2277
|
-
for (const msg of messages) {
|
|
2278
|
-
queries.createMessage(newThread.id, msg.role, msg.content);
|
|
2279
|
-
}
|
|
2280
|
-
|
|
2281
|
-
sendJSON(req, res, 200, {
|
|
2282
|
-
id: newThread.id,
|
|
2283
|
-
agentId: newThread.agentId,
|
|
2284
|
-
title: newThread.title,
|
|
2285
|
-
created_at: newThread.created_at,
|
|
2286
|
-
status: newThread.status,
|
|
2287
|
-
state: null
|
|
2288
|
-
});
|
|
2289
|
-
return;
|
|
2290
|
-
}
|
|
2291
|
-
|
|
2292
|
-
const threadRunsMatch = pathOnly.match(/^\/api\/threads\/([^/]+)\/runs$/);
|
|
2293
|
-
if (threadRunsMatch) {
|
|
2294
|
-
const threadId = threadRunsMatch[1];
|
|
2295
|
-
const conv = queries.getConversation(threadId);
|
|
2296
|
-
|
|
2297
|
-
if (!conv) {
|
|
2298
|
-
sendJSON(req, res, 404, { error: 'Thread not found' });
|
|
2299
|
-
return;
|
|
2300
|
-
}
|
|
2301
|
-
|
|
2302
|
-
if (req.method === 'GET') {
|
|
2303
|
-
const limit = parseInt(new URL(req.url, 'http://localhost').searchParams.get('limit') || '10', 10);
|
|
2304
|
-
const offset = parseInt(new URL(req.url, 'http://localhost').searchParams.get('offset') || '0', 10);
|
|
2305
|
-
const sessions = queries.getSessionsByConversation(threadId, limit, offset);
|
|
2306
|
-
|
|
2307
|
-
const runs = sessions.map(s => ({
|
|
2308
|
-
id: s.id,
|
|
2309
|
-
threadId: s.conversationId,
|
|
2310
|
-
status: s.status,
|
|
2311
|
-
started_at: s.started_at,
|
|
2312
|
-
completed_at: s.completed_at,
|
|
2313
|
-
agentId: s.agentId,
|
|
2314
|
-
input: null,
|
|
2315
|
-
output: null
|
|
2316
|
-
}));
|
|
2317
|
-
|
|
2318
|
-
sendJSON(req, res, 200, runs);
|
|
2319
|
-
return;
|
|
2320
|
-
}
|
|
2321
|
-
|
|
2322
|
-
if (req.method === 'POST') {
|
|
2323
|
-
const activeEntry = activeExecutions.get(threadId);
|
|
2324
|
-
if (activeEntry) {
|
|
2325
|
-
sendJSON(req, res, 409, { error: 'Thread already has an active run' });
|
|
2326
|
-
return;
|
|
2327
|
-
}
|
|
2328
|
-
|
|
2329
|
-
let body = '';
|
|
2330
|
-
for await (const chunk of req) { body += chunk; }
|
|
2331
|
-
let parsed = {};
|
|
2332
|
-
try { parsed = body ? JSON.parse(body) : {}; } catch {}
|
|
2333
|
-
|
|
2334
|
-
const { input, agentId, webhook } = parsed;
|
|
2335
|
-
if (!input) {
|
|
2336
|
-
sendJSON(req, res, 400, { error: 'Missing input in request body' });
|
|
2337
|
-
return;
|
|
2338
|
-
}
|
|
2339
|
-
|
|
2340
|
-
const resolvedAgentId = agentId || conv.agentId || 'claude-code';
|
|
2341
|
-
const resolvedModel = parsed.model || conv.model || null;
|
|
2342
|
-
const cwd = conv.workingDirectory || STARTUP_CWD;
|
|
2343
|
-
|
|
2344
|
-
const session = queries.createSession(threadId, resolvedAgentId, 'pending');
|
|
2345
|
-
const message = queries.createMessage(threadId, 'user', typeof input === 'string' ? input : JSON.stringify(input));
|
|
2346
|
-
|
|
2347
|
-
processMessageWithStreaming(threadId, message.id, session.id, typeof input === 'string' ? input : JSON.stringify(input), resolvedAgentId, resolvedModel);
|
|
2348
|
-
|
|
2349
|
-
sendJSON(req, res, 200, {
|
|
2350
|
-
id: session.id,
|
|
2351
|
-
threadId: threadId,
|
|
2352
|
-
status: 'pending',
|
|
2353
|
-
started_at: session.started_at,
|
|
2354
|
-
agentId: resolvedAgentId
|
|
2355
|
-
});
|
|
2356
|
-
return;
|
|
2357
|
-
}
|
|
2358
|
-
}
|
|
2359
|
-
|
|
2360
|
-
const threadRunByIdMatch = pathOnly.match(/^\/api\/threads\/([^/]+)\/runs\/([^/]+)$/);
|
|
2361
|
-
if (threadRunByIdMatch) {
|
|
2362
|
-
const threadId = threadRunByIdMatch[1];
|
|
2363
|
-
const runId = threadRunByIdMatch[2];
|
|
2364
|
-
const session = queries.getSession(runId);
|
|
2365
|
-
|
|
2366
|
-
if (!session || session.conversationId !== threadId) {
|
|
2367
|
-
sendJSON(req, res, 404, { error: 'Run not found' });
|
|
2368
|
-
return;
|
|
2369
|
-
}
|
|
2370
|
-
|
|
2371
|
-
if (req.method === 'GET') {
|
|
2372
|
-
sendJSON(req, res, 200, {
|
|
2373
|
-
id: session.id,
|
|
2374
|
-
threadId: session.conversationId,
|
|
2375
|
-
status: session.status,
|
|
2376
|
-
started_at: session.started_at,
|
|
2377
|
-
completed_at: session.completed_at,
|
|
2378
|
-
agentId: session.agentId,
|
|
2379
|
-
input: null,
|
|
2380
|
-
output: null
|
|
2381
|
-
});
|
|
2382
|
-
return;
|
|
2383
|
-
}
|
|
2384
|
-
|
|
2385
|
-
if (req.method === 'DELETE') {
|
|
2386
|
-
queries.deleteSession(runId);
|
|
2387
|
-
sendJSON(req, res, 204, {});
|
|
2388
|
-
return;
|
|
2389
|
-
}
|
|
2390
|
-
|
|
2391
|
-
if (req.method === 'POST') {
|
|
2392
|
-
if (session.status !== 'interrupted') {
|
|
2393
|
-
sendJSON(req, res, 409, { error: 'Can only resume interrupted runs' });
|
|
2394
|
-
return;
|
|
2395
|
-
}
|
|
2396
|
-
|
|
2397
|
-
let body = '';
|
|
2398
|
-
for await (const chunk of req) { body += chunk; }
|
|
2399
|
-
let parsed = {};
|
|
2400
|
-
try { parsed = body ? JSON.parse(body) : {}; } catch {}
|
|
2401
|
-
|
|
2402
|
-
const { input } = parsed;
|
|
2403
|
-
if (!input) {
|
|
2404
|
-
sendJSON(req, res, 400, { error: 'Missing input in request body' });
|
|
2405
|
-
return;
|
|
2406
|
-
}
|
|
2407
|
-
|
|
2408
|
-
const conv = queries.getConversation(threadId);
|
|
2409
|
-
const resolvedAgentId = session.agentId || conv.agentId || 'claude-code';
|
|
2410
|
-
const resolvedModel = conv?.model || null;
|
|
2411
|
-
const cwd = conv?.workingDirectory || STARTUP_CWD;
|
|
2412
|
-
|
|
2413
|
-
queries.updateSession(runId, { status: 'pending' });
|
|
2414
|
-
|
|
2415
|
-
const message = queries.createMessage(threadId, 'user', typeof input === 'string' ? input : JSON.stringify(input));
|
|
2416
|
-
|
|
2417
|
-
processMessageWithStreaming(threadId, message.id, runId, typeof input === 'string' ? input : JSON.stringify(input), resolvedAgentId, resolvedModel);
|
|
2418
|
-
|
|
2419
|
-
sendJSON(req, res, 200, {
|
|
2420
|
-
id: session.id,
|
|
2421
|
-
threadId: threadId,
|
|
2422
|
-
status: 'pending',
|
|
2423
|
-
started_at: session.started_at,
|
|
2424
|
-
agentId: resolvedAgentId
|
|
2425
|
-
});
|
|
2426
|
-
return;
|
|
2427
|
-
}
|
|
2428
|
-
}
|
|
2429
|
-
|
|
2430
|
-
const threadRunCancelMatch = pathOnly.match(/^\/api\/threads\/([^/]+)\/runs\/([^/]+)\/cancel$/);
|
|
2431
|
-
if (threadRunCancelMatch && req.method === 'POST') {
|
|
2432
|
-
const threadId = threadRunCancelMatch[1];
|
|
2433
|
-
const runId = threadRunCancelMatch[2];
|
|
2434
|
-
const session = queries.getSession(runId);
|
|
2435
|
-
|
|
2436
|
-
if (!session || session.conversationId !== threadId) {
|
|
2437
|
-
sendJSON(req, res, 404, { error: 'Run not found' });
|
|
2438
|
-
return;
|
|
2439
|
-
}
|
|
2440
|
-
|
|
2441
|
-
const entry = activeExecutions.get(threadId);
|
|
2442
|
-
|
|
2443
|
-
if (entry && entry.sessionId === runId) {
|
|
2444
|
-
const { pid } = entry;
|
|
2445
|
-
if (pid) {
|
|
2446
|
-
try {
|
|
2447
|
-
process.kill(-pid, 'SIGKILL');
|
|
2448
|
-
} catch {
|
|
2449
|
-
try {
|
|
2450
|
-
process.kill(pid, 'SIGKILL');
|
|
2451
|
-
} catch (e) {}
|
|
2452
|
-
}
|
|
2453
|
-
}
|
|
2454
|
-
}
|
|
2455
|
-
|
|
2456
|
-
queries.updateSession(runId, { status: 'interrupted', completed_at: Date.now() });
|
|
2457
|
-
queries.setIsStreaming(threadId, false);
|
|
2458
|
-
activeExecutions.delete(threadId);
|
|
2459
|
-
|
|
2460
|
-
broadcastSync({
|
|
2461
|
-
type: 'streaming_complete',
|
|
2462
|
-
sessionId: runId,
|
|
2463
|
-
conversationId: threadId,
|
|
2464
|
-
interrupted: true,
|
|
2465
|
-
timestamp: Date.now()
|
|
2466
|
-
});
|
|
2467
|
-
|
|
2468
|
-
sendJSON(req, res, 204, {});
|
|
2469
|
-
return;
|
|
2470
|
-
}
|
|
2471
|
-
|
|
2472
2328
|
if (pathOnly === '/api/stt' && req.method === 'POST') {
|
|
2473
2329
|
try {
|
|
2474
2330
|
const chunks = [];
|
|
@@ -2734,7 +2590,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
2734
2590
|
if (pathOnly === '/api/git/push' && req.method === 'POST') {
|
|
2735
2591
|
try {
|
|
2736
2592
|
const isWindows = os.platform() === 'win32';
|
|
2737
|
-
const gitCommand = isWindows
|
|
2593
|
+
const gitCommand = isWindows
|
|
2738
2594
|
? 'git add -A & git commit -m "Auto-commit" & git push'
|
|
2739
2595
|
: 'git add -A && git commit -m "Auto-commit" && git push';
|
|
2740
2596
|
execSync(gitCommand, { encoding: 'utf-8', cwd: STARTUP_CWD, shell: isWindows });
|
|
@@ -2745,6 +2601,148 @@ const server = http.createServer(async (req, res) => {
|
|
|
2745
2601
|
return;
|
|
2746
2602
|
}
|
|
2747
2603
|
|
|
2604
|
+
// ============================================================
|
|
2605
|
+
// THREAD API ENDPOINTS (ACP v0.2.3)
|
|
2606
|
+
// ============================================================
|
|
2607
|
+
|
|
2608
|
+
// POST /threads - Create empty thread
|
|
2609
|
+
if (pathOnly === '/api/threads' && req.method === 'POST') {
|
|
2610
|
+
try {
|
|
2611
|
+
const body = await parseBody(req);
|
|
2612
|
+
const metadata = body.metadata || {};
|
|
2613
|
+
const thread = queries.createThread(metadata);
|
|
2614
|
+
sendJSON(req, res, 201, thread);
|
|
2615
|
+
} catch (err) {
|
|
2616
|
+
sendJSON(req, res, 422, { error: err.message, type: 'validation_error' });
|
|
2617
|
+
}
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
// POST /threads/search - Search threads
|
|
2622
|
+
if (pathOnly === '/api/threads/search' && req.method === 'POST') {
|
|
2623
|
+
try {
|
|
2624
|
+
const body = await parseBody(req);
|
|
2625
|
+
const result = queries.searchThreads(body);
|
|
2626
|
+
sendJSON(req, res, 200, result);
|
|
2627
|
+
} catch (err) {
|
|
2628
|
+
sendJSON(req, res, 422, { error: err.message, type: 'validation_error' });
|
|
2629
|
+
}
|
|
2630
|
+
return;
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
// GET /threads/{thread_id} - Get thread by ID
|
|
2634
|
+
const acpThreadMatch = pathOnly.match(/^\/api\/threads\/([a-f0-9-]{36})$/);
|
|
2635
|
+
if (acpThreadMatch && req.method === 'GET') {
|
|
2636
|
+
const threadId = threadByIdMatch[1];
|
|
2637
|
+
try {
|
|
2638
|
+
const thread = queries.getThread(threadId);
|
|
2639
|
+
if (!thread) {
|
|
2640
|
+
sendJSON(req, res, 404, { error: 'Thread not found', type: 'not_found' });
|
|
2641
|
+
} else {
|
|
2642
|
+
sendJSON(req, res, 200, thread);
|
|
2643
|
+
}
|
|
2644
|
+
} catch (err) {
|
|
2645
|
+
sendJSON(req, res, 500, { error: err.message, type: 'internal_error' });
|
|
2646
|
+
}
|
|
2647
|
+
return;
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
// PATCH /threads/{thread_id} - Update thread metadata
|
|
2651
|
+
if (acpThreadMatch && req.method === 'PATCH') {
|
|
2652
|
+
const threadId = threadByIdMatch[1];
|
|
2653
|
+
try {
|
|
2654
|
+
const body = await parseBody(req);
|
|
2655
|
+
const thread = queries.patchThread(threadId, body);
|
|
2656
|
+
sendJSON(req, res, 200, thread);
|
|
2657
|
+
} catch (err) {
|
|
2658
|
+
if (err.message.includes('not found')) {
|
|
2659
|
+
sendJSON(req, res, 404, { error: err.message, type: 'not_found' });
|
|
2660
|
+
} else {
|
|
2661
|
+
sendJSON(req, res, 422, { error: err.message, type: 'validation_error' });
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
return;
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
// DELETE /threads/{thread_id} - Delete thread (fail if pending runs exist)
|
|
2668
|
+
if (acpThreadMatch && req.method === 'DELETE') {
|
|
2669
|
+
const threadId = threadByIdMatch[1];
|
|
2670
|
+
try {
|
|
2671
|
+
queries.deleteThread(threadId);
|
|
2672
|
+
res.writeHead(204);
|
|
2673
|
+
res.end();
|
|
2674
|
+
} catch (err) {
|
|
2675
|
+
if (err.message.includes('not found')) {
|
|
2676
|
+
sendJSON(req, res, 404, { error: err.message, type: 'not_found' });
|
|
2677
|
+
} else if (err.message.includes('pending runs')) {
|
|
2678
|
+
sendJSON(req, res, 409, { error: err.message, type: 'conflict' });
|
|
2679
|
+
} else {
|
|
2680
|
+
sendJSON(req, res, 500, { error: err.message, type: 'internal_error' });
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
return;
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
// GET /threads/{thread_id}/history - Get thread state history with pagination
|
|
2687
|
+
const threadHistoryMatch = pathOnly.match(/^\/api\/threads\/([a-f0-9-]{36})\/history$/);
|
|
2688
|
+
if (threadHistoryMatch && req.method === 'GET') {
|
|
2689
|
+
const threadId = threadHistoryMatch[1];
|
|
2690
|
+
try {
|
|
2691
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
2692
|
+
const limit = parseInt(url.searchParams.get('limit') || '50', 10);
|
|
2693
|
+
const before = url.searchParams.get('before') || null;
|
|
2694
|
+
|
|
2695
|
+
// Convert 'before' cursor to offset if needed
|
|
2696
|
+
let offset = 0;
|
|
2697
|
+
if (before) {
|
|
2698
|
+
// Simple cursor-based pagination: before is an offset value
|
|
2699
|
+
offset = parseInt(before, 10);
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
const result = queries.getThreadHistory(threadId, limit, offset);
|
|
2703
|
+
|
|
2704
|
+
// Generate next_cursor if there are more results
|
|
2705
|
+
const response = {
|
|
2706
|
+
states: result.states,
|
|
2707
|
+
next_cursor: result.hasMore ? String(offset + limit) : null
|
|
2708
|
+
};
|
|
2709
|
+
|
|
2710
|
+
sendJSON(req, res, 200, response);
|
|
2711
|
+
} catch (err) {
|
|
2712
|
+
if (err.message.includes('not found')) {
|
|
2713
|
+
sendJSON(req, res, 404, { error: err.message, type: 'not_found' });
|
|
2714
|
+
} else {
|
|
2715
|
+
sendJSON(req, res, 422, { error: err.message, type: 'validation_error' });
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
return;
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
// POST /threads/{thread_id}/copy - Copy thread with all states/checkpoints
|
|
2722
|
+
const threadCopyMatch = pathOnly.match(/^\/api\/threads\/([a-f0-9-]{36})\/copy$/);
|
|
2723
|
+
if (threadCopyMatch && req.method === 'POST') {
|
|
2724
|
+
const sourceThreadId = threadCopyMatch[1];
|
|
2725
|
+
try {
|
|
2726
|
+
const body = await parseBody(req);
|
|
2727
|
+
const newThread = queries.copyThread(sourceThreadId);
|
|
2728
|
+
|
|
2729
|
+
// Update metadata if provided
|
|
2730
|
+
if (body.metadata) {
|
|
2731
|
+
const updated = queries.patchThread(newThread.thread_id, { metadata: body.metadata });
|
|
2732
|
+
sendJSON(req, res, 201, updated);
|
|
2733
|
+
} else {
|
|
2734
|
+
sendJSON(req, res, 201, newThread);
|
|
2735
|
+
}
|
|
2736
|
+
} catch (err) {
|
|
2737
|
+
if (err.message.includes('not found')) {
|
|
2738
|
+
sendJSON(req, res, 404, { error: err.message, type: 'not_found' });
|
|
2739
|
+
} else {
|
|
2740
|
+
sendJSON(req, res, 500, { error: err.message, type: 'internal_error' });
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
return;
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2748
2746
|
if (routePath.startsWith('/api/image/')) {
|
|
2749
2747
|
const imagePath = routePath.slice('/api/image/'.length);
|
|
2750
2748
|
const decodedPath = decodeURIComponent(imagePath);
|