@open-mercato/ai-assistant 0.4.2-canary-c02407ff85
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/AGENTS.md +1090 -0
- package/README.md +607 -0
- package/build.mjs +92 -0
- package/dist/di.js +8 -0
- package/dist/di.js.map +7 -0
- package/dist/frontend/components/CommandPalette/CommandFooter.js +80 -0
- package/dist/frontend/components/CommandPalette/CommandFooter.js.map +7 -0
- package/dist/frontend/components/CommandPalette/CommandHeader.js +53 -0
- package/dist/frontend/components/CommandPalette/CommandHeader.js.map +7 -0
- package/dist/frontend/components/CommandPalette/CommandInput.js +29 -0
- package/dist/frontend/components/CommandPalette/CommandInput.js.map +7 -0
- package/dist/frontend/components/CommandPalette/CommandItem.js +92 -0
- package/dist/frontend/components/CommandPalette/CommandItem.js.map +7 -0
- package/dist/frontend/components/CommandPalette/CommandPalette.js +244 -0
- package/dist/frontend/components/CommandPalette/CommandPalette.js.map +7 -0
- package/dist/frontend/components/CommandPalette/CommandPaletteProvider.js +42 -0
- package/dist/frontend/components/CommandPalette/CommandPaletteProvider.js.map +7 -0
- package/dist/frontend/components/CommandPalette/CommandPaletteWrapper.js +18 -0
- package/dist/frontend/components/CommandPalette/CommandPaletteWrapper.js.map +7 -0
- package/dist/frontend/components/CommandPalette/DebugPanel.js +215 -0
- package/dist/frontend/components/CommandPalette/DebugPanel.js.map +7 -0
- package/dist/frontend/components/CommandPalette/MessageBubble.js +64 -0
- package/dist/frontend/components/CommandPalette/MessageBubble.js.map +7 -0
- package/dist/frontend/components/CommandPalette/ToolCallConfirmation.js +91 -0
- package/dist/frontend/components/CommandPalette/ToolCallConfirmation.js.map +7 -0
- package/dist/frontend/components/CommandPalette/ToolCallDisplay.js +47 -0
- package/dist/frontend/components/CommandPalette/ToolCallDisplay.js.map +7 -0
- package/dist/frontend/components/CommandPalette/ToolChatPage.js +74 -0
- package/dist/frontend/components/CommandPalette/ToolChatPage.js.map +7 -0
- package/dist/frontend/components/CommandPalette/index.js +28 -0
- package/dist/frontend/components/CommandPalette/index.js.map +7 -0
- package/dist/frontend/constants.js +41 -0
- package/dist/frontend/constants.js.map +7 -0
- package/dist/frontend/hooks/index.js +13 -0
- package/dist/frontend/hooks/index.js.map +7 -0
- package/dist/frontend/hooks/useCommandPalette.js +1094 -0
- package/dist/frontend/hooks/useCommandPalette.js.map +7 -0
- package/dist/frontend/hooks/useMcpTools.js +66 -0
- package/dist/frontend/hooks/useMcpTools.js.map +7 -0
- package/dist/frontend/hooks/usePageContext.js +48 -0
- package/dist/frontend/hooks/usePageContext.js.map +7 -0
- package/dist/frontend/hooks/useRecentActions.js +56 -0
- package/dist/frontend/hooks/useRecentActions.js.map +7 -0
- package/dist/frontend/hooks/useRecentTools.js +55 -0
- package/dist/frontend/hooks/useRecentTools.js.map +7 -0
- package/dist/frontend/index.js +35 -0
- package/dist/frontend/index.js.map +7 -0
- package/dist/frontend/types.js +1 -0
- package/dist/frontend/types.js.map +7 -0
- package/dist/frontend/utils/index.js +7 -0
- package/dist/frontend/utils/index.js.map +7 -0
- package/dist/frontend/utils/toolMatcher.js +95 -0
- package/dist/frontend/utils/toolMatcher.js.map +7 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +7 -0
- package/dist/modules/ai_assistant/acl.js +14 -0
- package/dist/modules/ai_assistant/acl.js.map +7 -0
- package/dist/modules/ai_assistant/api/chat/route.js +152 -0
- package/dist/modules/ai_assistant/api/chat/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/health/route.js +27 -0
- package/dist/modules/ai_assistant/api/health/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/route/route.js +123 -0
- package/dist/modules/ai_assistant/api/route/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/settings/route.js +60 -0
- package/dist/modules/ai_assistant/api/settings/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/tools/execute/route.js +58 -0
- package/dist/modules/ai_assistant/api/tools/execute/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/tools/route.js +48 -0
- package/dist/modules/ai_assistant/api/tools/route.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/page.js +10 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/page.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/page.meta.js +28 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/page.meta.js.map +7 -0
- package/dist/modules/ai_assistant/cli.js +192 -0
- package/dist/modules/ai_assistant/cli.js.map +7 -0
- package/dist/modules/ai_assistant/di.js +11 -0
- package/dist/modules/ai_assistant/di.js.map +7 -0
- package/dist/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.js +257 -0
- package/dist/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.js.map +7 -0
- package/dist/modules/ai_assistant/index.js +13 -0
- package/dist/modules/ai_assistant/index.js.map +7 -0
- package/dist/modules/ai_assistant/lib/ai-sdk.js +13 -0
- package/dist/modules/ai_assistant/lib/ai-sdk.js.map +7 -0
- package/dist/modules/ai_assistant/lib/api-discovery-tools.js +249 -0
- package/dist/modules/ai_assistant/lib/api-discovery-tools.js.map +7 -0
- package/dist/modules/ai_assistant/lib/api-endpoint-index-config.js +177 -0
- package/dist/modules/ai_assistant/lib/api-endpoint-index-config.js.map +7 -0
- package/dist/modules/ai_assistant/lib/api-endpoint-index.js +210 -0
- package/dist/modules/ai_assistant/lib/api-endpoint-index.js.map +7 -0
- package/dist/modules/ai_assistant/lib/auth.js +87 -0
- package/dist/modules/ai_assistant/lib/auth.js.map +7 -0
- package/dist/modules/ai_assistant/lib/chat-config.js +117 -0
- package/dist/modules/ai_assistant/lib/chat-config.js.map +7 -0
- package/dist/modules/ai_assistant/lib/client-factory.js +60 -0
- package/dist/modules/ai_assistant/lib/client-factory.js.map +7 -0
- package/dist/modules/ai_assistant/lib/http-server.js +367 -0
- package/dist/modules/ai_assistant/lib/http-server.js.map +7 -0
- package/dist/modules/ai_assistant/lib/in-process-client.js +126 -0
- package/dist/modules/ai_assistant/lib/in-process-client.js.map +7 -0
- package/dist/modules/ai_assistant/lib/mcp-client.js +146 -0
- package/dist/modules/ai_assistant/lib/mcp-client.js.map +7 -0
- package/dist/modules/ai_assistant/lib/mcp-dev-server.js +283 -0
- package/dist/modules/ai_assistant/lib/mcp-dev-server.js.map +7 -0
- package/dist/modules/ai_assistant/lib/mcp-server-config.js +160 -0
- package/dist/modules/ai_assistant/lib/mcp-server-config.js.map +7 -0
- package/dist/modules/ai_assistant/lib/mcp-server.js +156 -0
- package/dist/modules/ai_assistant/lib/mcp-server.js.map +7 -0
- package/dist/modules/ai_assistant/lib/mcp-tool-adapter.js +44 -0
- package/dist/modules/ai_assistant/lib/mcp-tool-adapter.js.map +7 -0
- package/dist/modules/ai_assistant/lib/opencode-client.js +247 -0
- package/dist/modules/ai_assistant/lib/opencode-client.js.map +7 -0
- package/dist/modules/ai_assistant/lib/opencode-handlers.js +398 -0
- package/dist/modules/ai_assistant/lib/opencode-handlers.js.map +7 -0
- package/dist/modules/ai_assistant/lib/schema-utils.js +94 -0
- package/dist/modules/ai_assistant/lib/schema-utils.js.map +7 -0
- package/dist/modules/ai_assistant/lib/tool-executor.js +55 -0
- package/dist/modules/ai_assistant/lib/tool-executor.js.map +7 -0
- package/dist/modules/ai_assistant/lib/tool-index-config.js +125 -0
- package/dist/modules/ai_assistant/lib/tool-index-config.js.map +7 -0
- package/dist/modules/ai_assistant/lib/tool-loader.js +88 -0
- package/dist/modules/ai_assistant/lib/tool-loader.js.map +7 -0
- package/dist/modules/ai_assistant/lib/tool-registry.js +65 -0
- package/dist/modules/ai_assistant/lib/tool-registry.js.map +7 -0
- package/dist/modules/ai_assistant/lib/tool-search.js +192 -0
- package/dist/modules/ai_assistant/lib/tool-search.js.map +7 -0
- package/dist/modules/ai_assistant/lib/types.js +1 -0
- package/dist/modules/ai_assistant/lib/types.js.map +7 -0
- package/package.json +108 -0
- package/src/di.ts +11 -0
- package/src/frontend/components/CommandPalette/CommandFooter.tsx +113 -0
- package/src/frontend/components/CommandPalette/CommandHeader.tsx +76 -0
- package/src/frontend/components/CommandPalette/CommandInput.tsx +50 -0
- package/src/frontend/components/CommandPalette/CommandItem.tsx +111 -0
- package/src/frontend/components/CommandPalette/CommandPalette.tsx +276 -0
- package/src/frontend/components/CommandPalette/CommandPaletteProvider.tsx +60 -0
- package/src/frontend/components/CommandPalette/CommandPaletteWrapper.tsx +21 -0
- package/src/frontend/components/CommandPalette/DebugPanel.tsx +257 -0
- package/src/frontend/components/CommandPalette/MessageBubble.tsx +73 -0
- package/src/frontend/components/CommandPalette/ToolCallConfirmation.tsx +130 -0
- package/src/frontend/components/CommandPalette/ToolCallDisplay.tsx +57 -0
- package/src/frontend/components/CommandPalette/ToolChatPage.tsx +125 -0
- package/src/frontend/components/CommandPalette/index.ts +14 -0
- package/src/frontend/constants.ts +35 -0
- package/src/frontend/hooks/index.ts +5 -0
- package/src/frontend/hooks/useCommandPalette.ts +1389 -0
- package/src/frontend/hooks/useMcpTools.ts +73 -0
- package/src/frontend/hooks/usePageContext.ts +61 -0
- package/src/frontend/hooks/useRecentActions.ts +64 -0
- package/src/frontend/hooks/useRecentTools.ts +69 -0
- package/src/frontend/index.ts +39 -0
- package/src/frontend/types.ts +260 -0
- package/src/frontend/utils/index.ts +1 -0
- package/src/frontend/utils/toolMatcher.ts +127 -0
- package/src/index.ts +92 -0
- package/src/modules/ai_assistant/acl.ts +10 -0
- package/src/modules/ai_assistant/api/chat/route.ts +213 -0
- package/src/modules/ai_assistant/api/health/route.ts +30 -0
- package/src/modules/ai_assistant/api/route/route.ts +149 -0
- package/src/modules/ai_assistant/api/settings/route.ts +73 -0
- package/src/modules/ai_assistant/api/tools/execute/route.ts +71 -0
- package/src/modules/ai_assistant/api/tools/route.ts +57 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/page.meta.ts +26 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/page.tsx +12 -0
- package/src/modules/ai_assistant/cli.ts +233 -0
- package/src/modules/ai_assistant/di.ts +9 -0
- package/src/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.tsx +418 -0
- package/src/modules/ai_assistant/index.ts +11 -0
- package/src/modules/ai_assistant/lib/ai-sdk.ts +5 -0
- package/src/modules/ai_assistant/lib/api-discovery-tools.ts +334 -0
- package/src/modules/ai_assistant/lib/api-endpoint-index-config.ts +243 -0
- package/src/modules/ai_assistant/lib/api-endpoint-index.ts +381 -0
- package/src/modules/ai_assistant/lib/auth.ts +185 -0
- package/src/modules/ai_assistant/lib/chat-config.ts +152 -0
- package/src/modules/ai_assistant/lib/client-factory.ts +130 -0
- package/src/modules/ai_assistant/lib/http-server.ts +498 -0
- package/src/modules/ai_assistant/lib/in-process-client.ts +205 -0
- package/src/modules/ai_assistant/lib/mcp-client.ts +221 -0
- package/src/modules/ai_assistant/lib/mcp-dev-server.ts +373 -0
- package/src/modules/ai_assistant/lib/mcp-server-config.ts +287 -0
- package/src/modules/ai_assistant/lib/mcp-server.ts +214 -0
- package/src/modules/ai_assistant/lib/mcp-tool-adapter.ts +76 -0
- package/src/modules/ai_assistant/lib/opencode-client.ts +426 -0
- package/src/modules/ai_assistant/lib/opencode-handlers.ts +676 -0
- package/src/modules/ai_assistant/lib/schema-utils.ts +142 -0
- package/src/modules/ai_assistant/lib/tool-executor.ts +71 -0
- package/src/modules/ai_assistant/lib/tool-index-config.ts +178 -0
- package/src/modules/ai_assistant/lib/tool-loader.ts +149 -0
- package/src/modules/ai_assistant/lib/tool-registry.ts +114 -0
- package/src/modules/ai_assistant/lib/tool-search.ts +308 -0
- package/src/modules/ai_assistant/lib/types.ts +147 -0
- package/test-schema.ts +37 -0
- package/tsconfig.json +10 -0
- package/watch.mjs +6 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createOpenCodeClient
|
|
3
|
+
} from "./opencode-client.js";
|
|
4
|
+
let clientInstance = null;
|
|
5
|
+
function getClient() {
|
|
6
|
+
if (!clientInstance) {
|
|
7
|
+
clientInstance = createOpenCodeClient();
|
|
8
|
+
}
|
|
9
|
+
return clientInstance;
|
|
10
|
+
}
|
|
11
|
+
async function handleOpenCodeMessage(request) {
|
|
12
|
+
const client = getClient();
|
|
13
|
+
const { message, sessionId, model } = request;
|
|
14
|
+
if (!message) {
|
|
15
|
+
throw new Error("Message is required");
|
|
16
|
+
}
|
|
17
|
+
let session;
|
|
18
|
+
if (sessionId) {
|
|
19
|
+
session = await client.getSession(sessionId);
|
|
20
|
+
} else {
|
|
21
|
+
session = await client.createSession();
|
|
22
|
+
}
|
|
23
|
+
const result = await client.sendMessage(session.id, message, { model });
|
|
24
|
+
return {
|
|
25
|
+
sessionId: session.id,
|
|
26
|
+
result
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
async function handleOpenCodeHealth() {
|
|
30
|
+
const client = getClient();
|
|
31
|
+
const url = process.env.OPENCODE_URL ?? "http://localhost:4096";
|
|
32
|
+
let searchStatus = {
|
|
33
|
+
available: false,
|
|
34
|
+
driver: null
|
|
35
|
+
};
|
|
36
|
+
try {
|
|
37
|
+
const { createRequestContainer } = await import("@open-mercato/shared/lib/di/container");
|
|
38
|
+
const container = await createRequestContainer();
|
|
39
|
+
const searchService = container.resolve("searchService");
|
|
40
|
+
const available = searchService.isStrategyAvailable("fulltext");
|
|
41
|
+
searchStatus = {
|
|
42
|
+
available,
|
|
43
|
+
driver: available ? "meilisearch" : null
|
|
44
|
+
};
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const [health, mcp] = await Promise.all([client.health(), client.mcpStatus()]);
|
|
49
|
+
return {
|
|
50
|
+
status: "ok",
|
|
51
|
+
opencode: health,
|
|
52
|
+
mcp,
|
|
53
|
+
search: searchStatus,
|
|
54
|
+
url
|
|
55
|
+
};
|
|
56
|
+
} catch (error) {
|
|
57
|
+
return {
|
|
58
|
+
status: "error",
|
|
59
|
+
search: searchStatus,
|
|
60
|
+
message: error instanceof Error ? error.message : "OpenCode not reachable",
|
|
61
|
+
url
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function extractTextFromResponse(result) {
|
|
66
|
+
if (!result || typeof result !== "object") return null;
|
|
67
|
+
const message = result;
|
|
68
|
+
if (!message.parts) return null;
|
|
69
|
+
const textParts = message.parts.filter((p) => p.type === "text" && p.text);
|
|
70
|
+
return textParts.map((p) => p.text).join("\n") || null;
|
|
71
|
+
}
|
|
72
|
+
function extractAllPartsFromResponse(result) {
|
|
73
|
+
if (!result || typeof result !== "object") return [];
|
|
74
|
+
const message = result;
|
|
75
|
+
return message.parts || [];
|
|
76
|
+
}
|
|
77
|
+
function extractMetadataFromResponse(result) {
|
|
78
|
+
if (!result || typeof result !== "object") return null;
|
|
79
|
+
const message = result;
|
|
80
|
+
if (!message.info) return null;
|
|
81
|
+
return {
|
|
82
|
+
modelID: message.info.modelID,
|
|
83
|
+
providerID: message.info.providerID,
|
|
84
|
+
tokens: message.info.tokens,
|
|
85
|
+
timing: message.info.time
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
async function handleOpenCodeMessageStreaming(request, onEvent) {
|
|
89
|
+
const client = getClient();
|
|
90
|
+
const { message, sessionId, model } = request;
|
|
91
|
+
const startTime = Date.now();
|
|
92
|
+
if (!message) {
|
|
93
|
+
await onEvent({ type: "error", error: "Message is required" });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
let session;
|
|
98
|
+
if (sessionId) {
|
|
99
|
+
session = await client.getSession(sessionId);
|
|
100
|
+
} else {
|
|
101
|
+
session = await client.createSession();
|
|
102
|
+
}
|
|
103
|
+
const targetSessionId = session.id;
|
|
104
|
+
let unsubscribe = null;
|
|
105
|
+
let emittedThinking = false;
|
|
106
|
+
let wasBusy = false;
|
|
107
|
+
let resolved = false;
|
|
108
|
+
let lastActivityTime = Date.now();
|
|
109
|
+
let heartbeatInterval = null;
|
|
110
|
+
let lastMetadata = null;
|
|
111
|
+
const cleanup = () => {
|
|
112
|
+
if (heartbeatInterval) {
|
|
113
|
+
clearInterval(heartbeatInterval);
|
|
114
|
+
heartbeatInterval = null;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const eventPromise = new Promise((resolve, reject) => {
|
|
118
|
+
const timeout = setTimeout(() => {
|
|
119
|
+
cleanup();
|
|
120
|
+
unsubscribe?.();
|
|
121
|
+
reject(new Error("OpenCode request timed out"));
|
|
122
|
+
}, 3e5);
|
|
123
|
+
heartbeatInterval = setInterval(async () => {
|
|
124
|
+
if (resolved) return;
|
|
125
|
+
const idleTime = Date.now() - lastActivityTime;
|
|
126
|
+
if (idleTime >= 5e3 && wasBusy && !resolved) {
|
|
127
|
+
try {
|
|
128
|
+
const status = await client.getSessionStatus(targetSessionId);
|
|
129
|
+
if (status.status === "busy") {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (status.status === "waiting" && status.questionId) {
|
|
133
|
+
const questions2 = await client.getPendingQuestions();
|
|
134
|
+
const sessionQuestion2 = questions2.find((q) => q.id === status.questionId);
|
|
135
|
+
if (sessionQuestion2) {
|
|
136
|
+
await onEvent({ type: "question", question: sessionQuestion2 });
|
|
137
|
+
lastActivityTime = Date.now();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const questions = await client.getPendingQuestions();
|
|
142
|
+
const sessionQuestion = questions.find((q) => q.sessionID === targetSessionId);
|
|
143
|
+
if (sessionQuestion) {
|
|
144
|
+
await onEvent({ type: "question", question: sessionQuestion });
|
|
145
|
+
lastActivityTime = Date.now();
|
|
146
|
+
} else if (status.status === "idle") {
|
|
147
|
+
resolved = true;
|
|
148
|
+
try {
|
|
149
|
+
await onEvent({ type: "done", sessionId: targetSessionId });
|
|
150
|
+
} catch (err) {
|
|
151
|
+
console.error("[OpenCode SSE] Heartbeat: Failed to emit done event:", err);
|
|
152
|
+
}
|
|
153
|
+
cleanup();
|
|
154
|
+
clearTimeout(timeout);
|
|
155
|
+
unsubscribe?.();
|
|
156
|
+
resolve();
|
|
157
|
+
}
|
|
158
|
+
} catch (err) {
|
|
159
|
+
console.error("[OpenCode SSE] Heartbeat error:", err);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}, 1e3);
|
|
163
|
+
unsubscribe = client.subscribeToEvents(
|
|
164
|
+
async (sseEvent) => {
|
|
165
|
+
try {
|
|
166
|
+
const { type, properties } = sseEvent;
|
|
167
|
+
lastActivityTime = Date.now();
|
|
168
|
+
const eventSessionId = properties.sessionID || properties.info?.sessionID || properties.part?.sessionID || properties.question?.sessionID || properties.session?.id || properties.status?.sessionID;
|
|
169
|
+
if (eventSessionId && eventSessionId !== targetSessionId) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
switch (type) {
|
|
173
|
+
case "question.asked": {
|
|
174
|
+
await onEvent({ type: "debug", partType: "question-asked", data: properties });
|
|
175
|
+
const questionFromEvent = properties.question;
|
|
176
|
+
if (questionFromEvent && questionFromEvent.sessionID === targetSessionId) {
|
|
177
|
+
await onEvent({ type: "question", question: questionFromEvent });
|
|
178
|
+
} else {
|
|
179
|
+
const questions = await client.getPendingQuestions();
|
|
180
|
+
const sessionQuestion = questions.find((q) => q.sessionID === targetSessionId);
|
|
181
|
+
if (sessionQuestion) {
|
|
182
|
+
await onEvent({ type: "question", question: sessionQuestion });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
case "session.status": {
|
|
188
|
+
const status = properties.status;
|
|
189
|
+
if (status?.type === "busy") {
|
|
190
|
+
wasBusy = true;
|
|
191
|
+
if (!emittedThinking) {
|
|
192
|
+
emittedThinking = true;
|
|
193
|
+
await onEvent({ type: "thinking" });
|
|
194
|
+
}
|
|
195
|
+
} else if (status?.type === "waiting" && !resolved) {
|
|
196
|
+
const questions = await client.getPendingQuestions();
|
|
197
|
+
const sessionQuestion = status.questionId ? questions.find((q) => q.id === status.questionId) : questions.find((q) => q.sessionID === targetSessionId);
|
|
198
|
+
if (sessionQuestion) {
|
|
199
|
+
await onEvent({ type: "question", question: sessionQuestion });
|
|
200
|
+
lastActivityTime = Date.now();
|
|
201
|
+
}
|
|
202
|
+
} else if (status?.type === "idle" && wasBusy && !resolved) {
|
|
203
|
+
const endTime = Date.now();
|
|
204
|
+
if (lastMetadata) {
|
|
205
|
+
await onEvent({
|
|
206
|
+
type: "metadata",
|
|
207
|
+
model: lastMetadata.model,
|
|
208
|
+
provider: lastMetadata.provider,
|
|
209
|
+
tokens: lastMetadata.tokens,
|
|
210
|
+
durationMs: endTime - startTime
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
const questions = await client.getPendingQuestions();
|
|
214
|
+
const sessionQuestion = questions.find((q) => q.sessionID === targetSessionId);
|
|
215
|
+
if (sessionQuestion) {
|
|
216
|
+
await onEvent({ type: "question", question: sessionQuestion });
|
|
217
|
+
lastActivityTime = Date.now();
|
|
218
|
+
} else {
|
|
219
|
+
setTimeout(async () => {
|
|
220
|
+
try {
|
|
221
|
+
if (resolved) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const finalQuestions = await client.getPendingQuestions();
|
|
225
|
+
const finalQuestion = finalQuestions.find((q) => q.sessionID === targetSessionId);
|
|
226
|
+
if (finalQuestion) {
|
|
227
|
+
await onEvent({ type: "question", question: finalQuestion });
|
|
228
|
+
lastActivityTime = Date.now();
|
|
229
|
+
} else {
|
|
230
|
+
resolved = true;
|
|
231
|
+
await onEvent({ type: "done", sessionId: targetSessionId });
|
|
232
|
+
cleanup();
|
|
233
|
+
clearTimeout(timeout);
|
|
234
|
+
unsubscribe?.();
|
|
235
|
+
resolve();
|
|
236
|
+
}
|
|
237
|
+
} catch (err) {
|
|
238
|
+
console.error("[OpenCode SSE] Error in timeout callback:", err);
|
|
239
|
+
if (!resolved) {
|
|
240
|
+
resolved = true;
|
|
241
|
+
try {
|
|
242
|
+
await onEvent({ type: "done", sessionId: targetSessionId });
|
|
243
|
+
} catch (e2) {
|
|
244
|
+
console.error("[OpenCode SSE] Failed to emit done event:", e2);
|
|
245
|
+
}
|
|
246
|
+
cleanup();
|
|
247
|
+
clearTimeout(timeout);
|
|
248
|
+
unsubscribe?.();
|
|
249
|
+
resolve();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}, 2e3);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
case "message.updated": {
|
|
258
|
+
const info = properties.info;
|
|
259
|
+
if (info.role === "assistant") {
|
|
260
|
+
if (info.error) {
|
|
261
|
+
cleanup();
|
|
262
|
+
clearTimeout(timeout);
|
|
263
|
+
unsubscribe?.();
|
|
264
|
+
await onEvent({
|
|
265
|
+
type: "error",
|
|
266
|
+
error: `${info.error.name}: ${info.error.message || "Unknown error"}`
|
|
267
|
+
});
|
|
268
|
+
resolve();
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (info.time?.completed) {
|
|
272
|
+
lastMetadata = {
|
|
273
|
+
model: info.modelID,
|
|
274
|
+
provider: info.providerID,
|
|
275
|
+
tokens: info.tokens
|
|
276
|
+
};
|
|
277
|
+
await onEvent({
|
|
278
|
+
type: "debug",
|
|
279
|
+
partType: "message-completed",
|
|
280
|
+
data: { messageId: info.id, tokens: info.tokens }
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
case "message.part.updated": {
|
|
287
|
+
const part = properties.part;
|
|
288
|
+
const delta = properties.delta;
|
|
289
|
+
switch (part.type) {
|
|
290
|
+
case "text":
|
|
291
|
+
if (delta) {
|
|
292
|
+
await onEvent({ type: "text", content: delta });
|
|
293
|
+
}
|
|
294
|
+
break;
|
|
295
|
+
case "tool_use":
|
|
296
|
+
if (part.name) {
|
|
297
|
+
await onEvent({
|
|
298
|
+
type: "tool-call",
|
|
299
|
+
id: part.id,
|
|
300
|
+
toolName: part.name,
|
|
301
|
+
args: part.input
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
break;
|
|
305
|
+
case "tool_result":
|
|
306
|
+
await onEvent({
|
|
307
|
+
type: "tool-result",
|
|
308
|
+
id: part.tool_use_id || part.id,
|
|
309
|
+
result: part.content
|
|
310
|
+
});
|
|
311
|
+
break;
|
|
312
|
+
case "step-start":
|
|
313
|
+
case "step-finish":
|
|
314
|
+
await onEvent({ type: "debug", partType: part.type, data: part });
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
} catch (err) {
|
|
321
|
+
console.error("[OpenCode SSE] Error processing event:", err);
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
(error) => {
|
|
325
|
+
clearTimeout(timeout);
|
|
326
|
+
reject(error);
|
|
327
|
+
}
|
|
328
|
+
);
|
|
329
|
+
});
|
|
330
|
+
client.sendMessage(session.id, message, { model }).catch((err) => {
|
|
331
|
+
console.error("[OpenCode] Send error (SSE should handle):", err);
|
|
332
|
+
});
|
|
333
|
+
await eventPromise;
|
|
334
|
+
} catch (error) {
|
|
335
|
+
await onEvent({
|
|
336
|
+
type: "error",
|
|
337
|
+
error: error instanceof Error ? error.message : "OpenCode request failed"
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
async function handleOpenCodeAnswer(questionId, answer, sessionId, onEvent) {
|
|
342
|
+
const client = getClient();
|
|
343
|
+
try {
|
|
344
|
+
await client.answerQuestion(questionId, answer);
|
|
345
|
+
await onEvent({ type: "thinking" });
|
|
346
|
+
const maxAttempts = 30;
|
|
347
|
+
const pollInterval = 2e3;
|
|
348
|
+
let sameQuestionWaitCount = 0;
|
|
349
|
+
const maxSameQuestionWait = 5;
|
|
350
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
351
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
352
|
+
const status = await client.getSessionStatus(sessionId);
|
|
353
|
+
if (status.status === "idle" || status.status === "unknown") {
|
|
354
|
+
await onEvent({ type: "done", sessionId });
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (status.status === "waiting" && status.questionId && status.questionId !== questionId) {
|
|
358
|
+
const allQuestions = await client.getPendingQuestions();
|
|
359
|
+
const newQuestion = allQuestions.find((q) => q.id === status.questionId);
|
|
360
|
+
if (newQuestion) {
|
|
361
|
+
await onEvent({ type: "question", question: newQuestion });
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (status.status === "waiting" && status.questionId === questionId) {
|
|
366
|
+
sameQuestionWaitCount++;
|
|
367
|
+
if (sameQuestionWaitCount >= maxSameQuestionWait) {
|
|
368
|
+
await onEvent({ type: "done", sessionId });
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
sameQuestionWaitCount = 0;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
await onEvent({ type: "done", sessionId });
|
|
376
|
+
} catch (error) {
|
|
377
|
+
console.error("[OpenCode Answer] Error:", error);
|
|
378
|
+
await onEvent({
|
|
379
|
+
type: "error",
|
|
380
|
+
error: error instanceof Error ? error.message : "Failed to answer question"
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
async function getPendingQuestions() {
|
|
385
|
+
const client = getClient();
|
|
386
|
+
return client.getPendingQuestions();
|
|
387
|
+
}
|
|
388
|
+
export {
|
|
389
|
+
extractAllPartsFromResponse,
|
|
390
|
+
extractMetadataFromResponse,
|
|
391
|
+
extractTextFromResponse,
|
|
392
|
+
getPendingQuestions,
|
|
393
|
+
handleOpenCodeAnswer,
|
|
394
|
+
handleOpenCodeHealth,
|
|
395
|
+
handleOpenCodeMessage,
|
|
396
|
+
handleOpenCodeMessageStreaming
|
|
397
|
+
};
|
|
398
|
+
//# sourceMappingURL=opencode-handlers.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/ai_assistant/lib/opencode-handlers.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * OpenCode API Route Handlers\n *\n * These handlers can be used by Next.js API routes to interact with OpenCode.\n */\n\nimport {\n createOpenCodeClient,\n type OpenCodeClient,\n type OpenCodeQuestion,\n} from './opencode-client'\n\nlet clientInstance: OpenCodeClient | null = null\n\nfunction getClient(): OpenCodeClient {\n if (!clientInstance) {\n clientInstance = createOpenCodeClient()\n }\n return clientInstance\n}\n\nexport type OpenCodeTestRequest = {\n message: string\n sessionId?: string\n model?: {\n providerID: string\n modelID: string\n }\n}\n\nexport type OpenCodeTestResponse = {\n sessionId: string\n result: unknown\n}\n\nexport type OpenCodeHealthResponse = {\n status: 'ok' | 'error'\n opencode?: {\n healthy: boolean\n version: string\n }\n mcp?: Record<string, { status: string; error?: string }>\n search?: {\n available: boolean\n driver: string | null // 'meilisearch' or null\n }\n url: string\n message?: string\n}\n\n/**\n * Handle POST request to send a message to OpenCode.\n */\nexport async function handleOpenCodeMessage(\n request: OpenCodeTestRequest\n): Promise<OpenCodeTestResponse> {\n const client = getClient()\n\n const { message, sessionId, model } = request\n\n if (!message) {\n throw new Error('Message is required')\n }\n\n // Create or get session\n let session\n if (sessionId) {\n session = await client.getSession(sessionId)\n } else {\n session = await client.createSession()\n }\n\n // Send message\n const result = await client.sendMessage(session.id, message, { model })\n\n return {\n sessionId: session.id,\n result,\n }\n}\n\n/**\n * Handle GET request to check OpenCode health.\n */\nexport async function handleOpenCodeHealth(): Promise<OpenCodeHealthResponse> {\n const client = getClient()\n const url = process.env.OPENCODE_URL ?? 'http://localhost:4096'\n\n // Check search service availability\n let searchStatus: { available: boolean; driver: string | null } = {\n available: false,\n driver: null,\n }\n try {\n const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')\n const container = await createRequestContainer()\n const searchService = container.resolve<{\n isStrategyAvailable: (strategy: string) => boolean\n }>('searchService')\n const available = searchService.isStrategyAvailable('fulltext')\n searchStatus = {\n available,\n driver: available ? 'meilisearch' : null,\n }\n } catch {\n // Search service not available\n }\n\n try {\n const [health, mcp] = await Promise.all([client.health(), client.mcpStatus()])\n\n return {\n status: 'ok',\n opencode: health,\n mcp,\n search: searchStatus,\n url,\n }\n } catch (error) {\n return {\n status: 'error',\n search: searchStatus,\n message: error instanceof Error ? error.message : 'OpenCode not reachable',\n url,\n }\n }\n}\n\n/**\n * Extract text content from OpenCode message response.\n */\nexport function extractTextFromResponse(result: unknown): string | null {\n if (!result || typeof result !== 'object') return null\n\n const message = result as { parts?: Array<{ type: string; text?: string }> }\n if (!message.parts) return null\n\n const textParts = message.parts.filter((p) => p.type === 'text' && p.text)\n return textParts.map((p) => p.text).join('\\n') || null\n}\n\n/**\n * Response part from OpenCode - can be text, tool-call, tool-result, etc.\n */\nexport interface OpenCodeResponsePart {\n id: string\n type: string\n text?: string\n // Tool call fields (OpenCode uses 'tool_use' type)\n name?: string\n input?: unknown\n // Tool result fields (OpenCode uses 'tool_result' type)\n tool_use_id?: string\n content?: unknown\n // Step fields (step-start, step-finish)\n sessionID?: string\n messageID?: string\n reason?: string\n cost?: number\n tokens?: {\n input: number\n output: number\n reasoning?: number\n cache?: { read: number; write: number }\n }\n // Generic catch-all\n [key: string]: unknown\n}\n\n/**\n * Metadata about the OpenCode response.\n */\nexport interface OpenCodeResponseMetadata {\n modelID?: string\n providerID?: string\n tokens?: { input: number; output: number }\n timing?: { created: number; completed?: number }\n}\n\n/**\n * Extract all parts from OpenCode response for verbose debugging.\n */\nexport function extractAllPartsFromResponse(result: unknown): OpenCodeResponsePart[] {\n if (!result || typeof result !== 'object') return []\n\n const message = result as { parts?: OpenCodeResponsePart[] }\n return message.parts || []\n}\n\n/**\n * Extract metadata (model, tokens, timing) from OpenCode response.\n */\nexport function extractMetadataFromResponse(result: unknown): OpenCodeResponseMetadata | null {\n if (!result || typeof result !== 'object') return null\n\n const message = result as {\n info?: {\n modelID?: string\n providerID?: string\n tokens?: { input: number; output: number }\n time?: { created: number; completed?: number }\n }\n }\n\n if (!message.info) return null\n\n return {\n modelID: message.info.modelID,\n providerID: message.info.providerID,\n tokens: message.info.tokens,\n timing: message.info.time,\n }\n}\n\n/**\n * Event types emitted during streaming message handling.\n */\nexport type OpenCodeStreamEvent =\n | { type: 'thinking' }\n | { type: 'text'; content: string }\n | { type: 'tool-call'; id: string; toolName: string; args: unknown }\n | { type: 'tool-result'; id: string; result: unknown }\n | { type: 'question'; question: OpenCodeQuestion }\n | { type: 'metadata'; model?: string; provider?: string; tokens?: { input: number; output: number }; durationMs?: number }\n | { type: 'debug'; partType: string; data: unknown }\n | { type: 'done'; sessionId: string }\n | { type: 'error'; error: string }\n\n/**\n * Handle OpenCode message with real-time SSE streaming.\n * Uses OpenCode's /event SSE endpoint for live updates.\n *\n * OpenCode does agentic loops - it may generate multiple assistant messages\n * with tool calls in between. We complete only when the session becomes \"idle\"\n * after being \"busy\", indicating the full agentic loop is done.\n */\nexport async function handleOpenCodeMessageStreaming(\n request: OpenCodeTestRequest,\n onEvent: (event: OpenCodeStreamEvent) => Promise<void>\n): Promise<void> {\n const client = getClient()\n const { message, sessionId, model } = request\n const startTime = Date.now()\n\n if (!message) {\n await onEvent({ type: 'error', error: 'Message is required' })\n return\n }\n\n try {\n // Create or get session\n let session\n if (sessionId) {\n session = await client.getSession(sessionId)\n } else {\n session = await client.createSession()\n }\n\n const targetSessionId = session.id\n let unsubscribe: (() => void) | null = null\n let emittedThinking = false\n let wasBusy = false // Track if session was ever busy\n let resolved = false // Track if we've already completed\n let lastActivityTime = Date.now() // Track last event for heartbeat\n let heartbeatInterval: NodeJS.Timeout | null = null\n let lastMetadata: {\n model?: string\n provider?: string\n tokens?: { input: number; output: number }\n } | null = null\n\n // Helper to clean up resources\n const cleanup = () => {\n if (heartbeatInterval) {\n clearInterval(heartbeatInterval)\n heartbeatInterval = null\n }\n }\n\n // Set up SSE subscription for real-time events\n const eventPromise = new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n cleanup()\n unsubscribe?.()\n reject(new Error('OpenCode request timed out'))\n }, 300000) // 5 minute timeout for complex agentic tasks\n\n // Heartbeat: Check every second for completion conditions\n heartbeatInterval = setInterval(async () => {\n if (resolved) return\n\n const idleTime = Date.now() - lastActivityTime\n\n // If no activity for 5 seconds and we were busy, check session status\n if (idleTime >= 5000 && wasBusy && !resolved) {\n try {\n // Check actual session status before completing\n const status = await client.getSessionStatus(targetSessionId)\n\n if (status.status === 'busy') {\n // Session is still busy - wait\n return\n }\n\n if (status.status === 'waiting' && status.questionId) {\n // Session is waiting for a question answer\n const questions = await client.getPendingQuestions()\n const sessionQuestion = questions.find((q) => q.id === status.questionId)\n if (sessionQuestion) {\n await onEvent({ type: 'question', question: sessionQuestion })\n lastActivityTime = Date.now() // Reset timer after emitting question\n return\n }\n }\n\n // Check for any pending questions for this session\n const questions = await client.getPendingQuestions()\n const sessionQuestion = questions.find((q) => q.sessionID === targetSessionId)\n\n if (sessionQuestion) {\n await onEvent({ type: 'question', question: sessionQuestion })\n lastActivityTime = Date.now() // Reset timer after emitting question\n } else if (status.status === 'idle') {\n // Session is explicitly idle and no questions - complete\n resolved = true\n try {\n await onEvent({ type: 'done', sessionId: targetSessionId })\n } catch (err) {\n console.error('[OpenCode SSE] Heartbeat: Failed to emit done event:', err)\n }\n cleanup()\n clearTimeout(timeout)\n unsubscribe?.()\n resolve()\n }\n // Status is 'unknown' or something else - wait for SSE events\n } catch (err) {\n console.error('[OpenCode SSE] Heartbeat error:', err)\n }\n }\n }, 1000)\n\n unsubscribe = client.subscribeToEvents(\n async (sseEvent) => {\n try {\n const { type, properties } = sseEvent\n\n // Update activity timestamp for heartbeat\n lastActivityTime = Date.now()\n\n // Filter events for our session\n const eventSessionId =\n (properties.sessionID as string) ||\n (properties.info as { sessionID?: string })?.sessionID ||\n (properties.part as { sessionID?: string })?.sessionID ||\n (properties.question as { sessionID?: string })?.sessionID ||\n (properties.session as { id?: string })?.id ||\n (properties.status as { sessionID?: string })?.sessionID\n\n if (eventSessionId && eventSessionId !== targetSessionId) {\n return // Ignore events from other sessions\n }\n\n switch (type) {\n case 'question.asked': {\n // OpenCode is asking a question - use the data directly from the SSE event\n await onEvent({ type: 'debug', partType: 'question-asked', data: properties })\n\n // The question data is in properties.question (from SSE event)\n // This is more reliable than fetching from API which may return incomplete data\n const questionFromEvent = properties.question as OpenCodeQuestion | undefined\n\n if (questionFromEvent && questionFromEvent.sessionID === targetSessionId) {\n await onEvent({ type: 'question', question: questionFromEvent })\n } else {\n // Fallback to fetching from API if event doesn't have full question\n const questions = await client.getPendingQuestions()\n const sessionQuestion = questions.find((q) => q.sessionID === targetSessionId)\n\n if (sessionQuestion) {\n await onEvent({ type: 'question', question: sessionQuestion })\n }\n }\n break\n }\n\n case 'session.status': {\n const status = properties.status as { type: string; questionId?: string }\n\n if (status?.type === 'busy') {\n wasBusy = true\n if (!emittedThinking) {\n emittedThinking = true\n await onEvent({ type: 'thinking' })\n }\n } else if (status?.type === 'waiting' && !resolved) {\n // Session is waiting for user to answer a question\n const questions = await client.getPendingQuestions()\n const sessionQuestion = status.questionId\n ? questions.find((q) => q.id === status.questionId)\n : questions.find((q) => q.sessionID === targetSessionId)\n\n if (sessionQuestion) {\n await onEvent({ type: 'question', question: sessionQuestion })\n lastActivityTime = Date.now()\n }\n } else if (status?.type === 'idle' && wasBusy && !resolved) {\n // Session went from busy to idle - check if there are pending questions\n const endTime = Date.now()\n\n // Emit final metadata if we have it\n if (lastMetadata) {\n await onEvent({\n type: 'metadata',\n model: lastMetadata.model,\n provider: lastMetadata.provider,\n tokens: lastMetadata.tokens,\n durationMs: endTime - startTime,\n })\n }\n\n // Check for pending questions before declaring done\n const questions = await client.getPendingQuestions()\n const sessionQuestion = questions.find((q) => q.sessionID === targetSessionId)\n\n if (sessionQuestion) {\n // Question found - emit it but keep stream open for answer\n await onEvent({ type: 'question', question: sessionQuestion })\n // Reset activity time so heartbeat doesn't close prematurely\n lastActivityTime = Date.now()\n // Don't set resolved - let heartbeat handle completion after user answers\n } else {\n // No questions found - but give OpenCode a moment to register one\n // (race condition prevention)\n setTimeout(async () => {\n try {\n if (resolved) {\n return\n }\n\n // Check one more time for questions\n const finalQuestions = await client.getPendingQuestions()\n const finalQuestion = finalQuestions.find((q) => q.sessionID === targetSessionId)\n\n if (finalQuestion) {\n await onEvent({ type: 'question', question: finalQuestion })\n lastActivityTime = Date.now()\n } else {\n // Truly idle - complete the stream\n resolved = true\n await onEvent({ type: 'done', sessionId: targetSessionId })\n cleanup()\n clearTimeout(timeout)\n unsubscribe?.()\n resolve()\n }\n } catch (err) {\n console.error('[OpenCode SSE] Error in timeout callback:', err)\n // Still try to complete even if there was an error\n if (!resolved) {\n resolved = true\n try {\n await onEvent({ type: 'done', sessionId: targetSessionId })\n } catch (e2) {\n console.error('[OpenCode SSE] Failed to emit done event:', e2)\n }\n cleanup()\n clearTimeout(timeout)\n unsubscribe?.()\n resolve()\n }\n }\n }, 2000)\n }\n }\n break\n }\n\n case 'message.updated': {\n const info = properties.info as {\n id: string\n role: string\n time?: { completed?: number }\n modelID?: string\n providerID?: string\n tokens?: { input: number; output: number }\n error?: { name: string; message?: string }\n }\n\n if (info.role === 'assistant') {\n // Check for error\n if (info.error) {\n cleanup()\n clearTimeout(timeout)\n unsubscribe?.()\n await onEvent({\n type: 'error',\n error: `${info.error.name}: ${info.error.message || 'Unknown error'}`,\n })\n resolve()\n return\n }\n\n // Track metadata from completed messages\n if (info.time?.completed) {\n lastMetadata = {\n model: info.modelID,\n provider: info.providerID,\n tokens: info.tokens,\n }\n // Emit intermediate metadata for visibility\n await onEvent({\n type: 'debug',\n partType: 'message-completed',\n data: { messageId: info.id, tokens: info.tokens },\n })\n // Note: Completion is now handled by heartbeat interval\n }\n }\n break\n }\n\n case 'message.part.updated': {\n const part = properties.part as {\n type: string\n text?: string\n name?: string\n input?: unknown\n tool_use_id?: string\n content?: unknown\n id: string\n }\n const delta = properties.delta as string | undefined\n\n switch (part.type) {\n case 'text':\n // Use delta for streaming text if available\n if (delta) {\n await onEvent({ type: 'text', content: delta })\n }\n break\n case 'tool_use':\n if (part.name) {\n await onEvent({\n type: 'tool-call',\n id: part.id,\n toolName: part.name,\n args: part.input,\n })\n }\n break\n case 'tool_result':\n await onEvent({\n type: 'tool-result',\n id: part.tool_use_id || part.id,\n result: part.content,\n })\n break\n case 'step-start':\n case 'step-finish':\n await onEvent({ type: 'debug', partType: part.type, data: part })\n break\n }\n break\n }\n }\n } catch (err) {\n console.error('[OpenCode SSE] Error processing event:', err)\n }\n },\n (error) => {\n clearTimeout(timeout)\n reject(error)\n }\n )\n })\n\n // Send message (don't await - let SSE handle the response via events)\n // We only catch errors here - successful completion is signaled via SSE session.status: idle\n client.sendMessage(session.id, message, { model }).catch((err) => {\n // Log send errors - SSE should also receive an error event\n console.error('[OpenCode] Send error (SSE should handle):', err)\n })\n\n // Wait for SSE to indicate completion (session.status: idle or error)\n await eventPromise\n } catch (error) {\n await onEvent({\n type: 'error',\n error: error instanceof Error ? error.message : 'OpenCode request failed',\n })\n }\n}\n\n/**\n * Answer a pending question and continue processing.\n * Uses polling to check for completion/next question.\n */\nexport async function handleOpenCodeAnswer(\n questionId: string,\n answer: number,\n sessionId: string,\n onEvent: (event: OpenCodeStreamEvent) => Promise<void>\n): Promise<void> {\n const client = getClient()\n\n try {\n // Answer the question\n await client.answerQuestion(questionId, answer)\n await onEvent({ type: 'thinking' })\n\n // Poll for completion using session status (max 20 seconds for same-question wait, 60 seconds total)\n const maxAttempts = 30\n const pollInterval = 2000\n let sameQuestionWaitCount = 0\n const maxSameQuestionWait = 5 // Give up after 10 seconds of waiting on same question\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n await new Promise((resolve) => setTimeout(resolve, pollInterval))\n\n // Check session status - most reliable way to know if processing is done\n const status = await client.getSessionStatus(sessionId)\n\n if (status.status === 'idle' || status.status === 'unknown') {\n // Session is idle or unknown - processing complete\n await onEvent({ type: 'done', sessionId })\n return\n }\n\n if (status.status === 'waiting' && status.questionId && status.questionId !== questionId) {\n // A new question appeared - fetch and emit it\n const allQuestions = await client.getPendingQuestions()\n const newQuestion = allQuestions.find((q) => q.id === status.questionId)\n if (newQuestion) {\n await onEvent({ type: 'question', question: newQuestion })\n return\n }\n }\n\n // If waiting on the same question we answered, track how long\n if (status.status === 'waiting' && status.questionId === questionId) {\n sameQuestionWaitCount++\n if (sameQuestionWaitCount >= maxSameQuestionWait) {\n // OpenCode didn't properly clear the question - assume answered and complete\n await onEvent({ type: 'done', sessionId })\n return\n }\n } else {\n // Reset counter if status changed\n sameQuestionWaitCount = 0\n }\n\n // Session is busy - keep polling\n }\n\n // Timeout - assume complete\n await onEvent({ type: 'done', sessionId })\n } catch (error) {\n console.error('[OpenCode Answer] Error:', error)\n await onEvent({\n type: 'error',\n error: error instanceof Error ? error.message : 'Failed to answer question',\n })\n }\n}\n\n/**\n * Get pending questions for a session.\n */\nexport async function getPendingQuestions(): Promise<OpenCodeQuestion[]> {\n const client = getClient()\n return client.getPendingQuestions()\n}\n\n// Re-export the question type\nexport type { OpenCodeQuestion }\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["questions", "sessionQuestion"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const safeSchemaCache = /* @__PURE__ */ new WeakMap();
|
|
3
|
+
function jsonSchemaToZod(jsonSchema) {
|
|
4
|
+
const type = jsonSchema.type;
|
|
5
|
+
if (type === "string") {
|
|
6
|
+
return z.string();
|
|
7
|
+
}
|
|
8
|
+
if (type === "number" || type === "integer") {
|
|
9
|
+
return z.number();
|
|
10
|
+
}
|
|
11
|
+
if (type === "boolean") {
|
|
12
|
+
return z.boolean();
|
|
13
|
+
}
|
|
14
|
+
if (type === "null") {
|
|
15
|
+
return z.null();
|
|
16
|
+
}
|
|
17
|
+
if (type === "array") {
|
|
18
|
+
const items = jsonSchema.items;
|
|
19
|
+
if (items) {
|
|
20
|
+
return z.array(jsonSchemaToZod(items));
|
|
21
|
+
}
|
|
22
|
+
return z.array(z.unknown());
|
|
23
|
+
}
|
|
24
|
+
if (type === "object") {
|
|
25
|
+
const properties = jsonSchema.properties;
|
|
26
|
+
const required = jsonSchema.required || [];
|
|
27
|
+
const additionalProperties = jsonSchema.additionalProperties;
|
|
28
|
+
if (additionalProperties && (!properties || Object.keys(properties).length === 0)) {
|
|
29
|
+
if (typeof additionalProperties === "object") {
|
|
30
|
+
return z.record(z.string(), jsonSchemaToZod(additionalProperties));
|
|
31
|
+
}
|
|
32
|
+
return z.record(z.string(), z.unknown());
|
|
33
|
+
}
|
|
34
|
+
if (properties) {
|
|
35
|
+
const shape = {};
|
|
36
|
+
for (const [key, propSchema] of Object.entries(properties)) {
|
|
37
|
+
let fieldSchema = jsonSchemaToZod(propSchema);
|
|
38
|
+
if (!required.includes(key)) {
|
|
39
|
+
fieldSchema = fieldSchema.optional();
|
|
40
|
+
}
|
|
41
|
+
shape[key] = fieldSchema;
|
|
42
|
+
}
|
|
43
|
+
if (additionalProperties) {
|
|
44
|
+
return z.object(shape).passthrough();
|
|
45
|
+
}
|
|
46
|
+
return z.object(shape);
|
|
47
|
+
}
|
|
48
|
+
if (additionalProperties) {
|
|
49
|
+
return z.record(z.string(), z.unknown());
|
|
50
|
+
}
|
|
51
|
+
return z.object({});
|
|
52
|
+
}
|
|
53
|
+
const anyOf = jsonSchema.anyOf;
|
|
54
|
+
const oneOf = jsonSchema.oneOf;
|
|
55
|
+
const unionTypes = anyOf || oneOf;
|
|
56
|
+
if (unionTypes && unionTypes.length >= 2) {
|
|
57
|
+
const schemas = unionTypes.map((s) => jsonSchemaToZod(s));
|
|
58
|
+
return z.union(schemas);
|
|
59
|
+
}
|
|
60
|
+
if (anyOf && anyOf.length === 2) {
|
|
61
|
+
const types = anyOf.map((s) => s.type);
|
|
62
|
+
if (types.includes("null")) {
|
|
63
|
+
const nonNullSchema = anyOf.find((s) => s.type !== "null");
|
|
64
|
+
if (nonNullSchema) {
|
|
65
|
+
return jsonSchemaToZod(nonNullSchema).nullable();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const enumValues = jsonSchema.enum;
|
|
70
|
+
if (enumValues && enumValues.length > 0) {
|
|
71
|
+
return z.enum(enumValues);
|
|
72
|
+
}
|
|
73
|
+
return z.unknown();
|
|
74
|
+
}
|
|
75
|
+
function toSafeZodSchema(schema) {
|
|
76
|
+
const cached = safeSchemaCache.get(schema);
|
|
77
|
+
if (cached) {
|
|
78
|
+
return cached;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const jsonSchema = z.toJSONSchema(schema, { unrepresentable: "any" });
|
|
82
|
+
const safeSchema = jsonSchemaToZod(jsonSchema);
|
|
83
|
+
safeSchemaCache.set(schema, safeSchema);
|
|
84
|
+
return safeSchema;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error("[Schema Utils] Error converting schema:", error);
|
|
87
|
+
return schema;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export {
|
|
91
|
+
jsonSchemaToZod,
|
|
92
|
+
toSafeZodSchema
|
|
93
|
+
};
|
|
94
|
+
//# sourceMappingURL=schema-utils.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/ai_assistant/lib/schema-utils.ts"],
|
|
4
|
+
"sourcesContent": ["import { z, type ZodType } from 'zod'\n\n/**\n * Cache for converted safe schemas to avoid repeated conversions per request.\n */\nconst safeSchemaCache = new WeakMap<ZodType, ZodType>()\n\n/**\n * Convert a JSON Schema to a simple Zod schema.\n * This creates a schema that can be converted back to JSON Schema without errors.\n *\n * Supports:\n * - Basic types: string, number, integer, boolean, null\n * - Arrays with item types\n * - Objects with properties and required fields\n * - Records/dictionaries via additionalProperties\n * - Union types via anyOf/oneOf\n * - Enum values\n */\nexport function jsonSchemaToZod(jsonSchema: Record<string, unknown>): ZodType {\n const type = jsonSchema.type as string | undefined\n\n if (type === 'string') {\n return z.string()\n }\n if (type === 'number' || type === 'integer') {\n return z.number()\n }\n if (type === 'boolean') {\n return z.boolean()\n }\n if (type === 'null') {\n return z.null()\n }\n if (type === 'array') {\n const items = jsonSchema.items as Record<string, unknown> | undefined\n if (items) {\n return z.array(jsonSchemaToZod(items))\n }\n return z.array(z.unknown())\n }\n if (type === 'object') {\n const properties = jsonSchema.properties as Record<string, Record<string, unknown>> | undefined\n const required = (jsonSchema.required as string[]) || []\n const additionalProperties = jsonSchema.additionalProperties\n\n // Handle z.record() - objects with additionalProperties but no fixed properties\n if (additionalProperties && (!properties || Object.keys(properties).length === 0)) {\n // This is a record/dictionary type - allow any properties\n if (typeof additionalProperties === 'object') {\n return z.record(z.string(), jsonSchemaToZod(additionalProperties as Record<string, unknown>))\n }\n // additionalProperties: true means any value\n return z.record(z.string(), z.unknown())\n }\n\n if (properties) {\n const shape: Record<string, ZodType> = {}\n for (const [key, propSchema] of Object.entries(properties)) {\n let fieldSchema = jsonSchemaToZod(propSchema)\n // Make field optional if not in required array\n if (!required.includes(key)) {\n fieldSchema = fieldSchema.optional()\n }\n shape[key] = fieldSchema\n }\n // If additionalProperties is allowed, use passthrough\n if (additionalProperties) {\n return z.object(shape).passthrough()\n }\n return z.object(shape)\n }\n\n // Empty object with additionalProperties - treat as record\n if (additionalProperties) {\n return z.record(z.string(), z.unknown())\n }\n return z.object({})\n }\n\n // Handle union types (anyOf, oneOf)\n const anyOf = jsonSchema.anyOf as Record<string, unknown>[] | undefined\n const oneOf = jsonSchema.oneOf as Record<string, unknown>[] | undefined\n const unionTypes = anyOf || oneOf\n if (unionTypes && unionTypes.length >= 2) {\n const schemas = unionTypes.map(s => jsonSchemaToZod(s))\n return z.union(schemas as [ZodType, ZodType, ...ZodType[]])\n }\n\n // Handle nullable via anyOf with null\n if (anyOf && anyOf.length === 2) {\n const types = anyOf.map((s) => s.type)\n if (types.includes('null')) {\n const nonNullSchema = anyOf.find((s) => s.type !== 'null')\n if (nonNullSchema) {\n return jsonSchemaToZod(nonNullSchema).nullable()\n }\n }\n }\n\n // Handle enum\n const enumValues = jsonSchema.enum as string[] | undefined\n if (enumValues && enumValues.length > 0) {\n return z.enum(enumValues as [string, ...string[]])\n }\n\n // Fallback for empty schemas (like Date converted with unrepresentable: 'any')\n return z.unknown()\n}\n\n/**\n * Convert a Zod schema to a safe Zod schema that has no Date types.\n * Uses JSON Schema as an intermediate format to handle all Zod v4 internal complexities.\n * Results are cached to avoid repeated conversions.\n *\n * @param schema - The original Zod schema\n * @returns A safe Zod schema without Date types\n */\nexport function toSafeZodSchema(schema: ZodType): ZodType {\n // Check cache first\n const cached = safeSchemaCache.get(schema)\n if (cached) {\n return cached\n }\n\n try {\n // Use Zod 4's toJSONSchema with unrepresentable: 'any' to handle Date types\n const jsonSchema = z.toJSONSchema(schema, { unrepresentable: 'any' }) as Record<string, unknown>\n\n // Convert back to a simple Zod schema without Date types\n const safeSchema = jsonSchemaToZod(jsonSchema)\n\n // Cache the result\n safeSchemaCache.set(schema, safeSchema)\n\n return safeSchema\n } catch (error) {\n console.error('[Schema Utils] Error converting schema:', error)\n // Fallback to the original schema if conversion fails\n return schema\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { getToolRegistry } from "./tool-registry.js";
|
|
2
|
+
import { hasRequiredFeatures } from "./auth.js";
|
|
3
|
+
async function executeTool(toolName, input, context) {
|
|
4
|
+
const registry = getToolRegistry();
|
|
5
|
+
const tool = registry.getTool(toolName);
|
|
6
|
+
if (!tool) {
|
|
7
|
+
return {
|
|
8
|
+
success: false,
|
|
9
|
+
error: `Tool "${toolName}" not found`,
|
|
10
|
+
errorCode: "NOT_FOUND"
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
if (tool.requiredFeatures?.length) {
|
|
14
|
+
const hasAccess = hasRequiredFeatures(
|
|
15
|
+
tool.requiredFeatures,
|
|
16
|
+
context.userFeatures,
|
|
17
|
+
context.isSuperAdmin
|
|
18
|
+
);
|
|
19
|
+
if (!hasAccess) {
|
|
20
|
+
return {
|
|
21
|
+
success: false,
|
|
22
|
+
error: `Insufficient permissions for tool "${toolName}". Required: ${tool.requiredFeatures.join(", ")}`,
|
|
23
|
+
errorCode: "UNAUTHORIZED"
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const parseResult = tool.inputSchema.safeParse(input);
|
|
28
|
+
if (!parseResult.success) {
|
|
29
|
+
const issues = parseResult.error.issues ?? [];
|
|
30
|
+
const errorMessages = issues.map(
|
|
31
|
+
(issue) => `${issue.path.join(".")}: ${issue.message}`
|
|
32
|
+
).join("; ");
|
|
33
|
+
return {
|
|
34
|
+
success: false,
|
|
35
|
+
error: `Invalid input: ${errorMessages || "Validation failed"}`,
|
|
36
|
+
errorCode: "VALIDATION_ERROR"
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const result = await tool.handler(parseResult.data, context);
|
|
41
|
+
return { success: true, result };
|
|
42
|
+
} catch (error) {
|
|
43
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
44
|
+
console.error(`[MCP Tool] Error executing "${toolName}":`, error);
|
|
45
|
+
return {
|
|
46
|
+
success: false,
|
|
47
|
+
error: message,
|
|
48
|
+
errorCode: "EXECUTION_ERROR"
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export {
|
|
53
|
+
executeTool
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=tool-executor.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/ai_assistant/lib/tool-executor.ts"],
|
|
4
|
+
"sourcesContent": ["import type { McpToolContext, ToolExecutionResult } from './types'\nimport { getToolRegistry } from './tool-registry'\nimport { hasRequiredFeatures } from './auth'\n\n/**\n * Execute a tool with full context and ACL checks.\n */\nexport async function executeTool(\n toolName: string,\n input: unknown,\n context: McpToolContext\n): Promise<ToolExecutionResult> {\n const registry = getToolRegistry()\n const tool = registry.getTool(toolName)\n\n if (!tool) {\n return {\n success: false,\n error: `Tool \"${toolName}\" not found`,\n errorCode: 'NOT_FOUND',\n }\n }\n\n // ACL check\n if (tool.requiredFeatures?.length) {\n const hasAccess = hasRequiredFeatures(\n tool.requiredFeatures,\n context.userFeatures,\n context.isSuperAdmin\n )\n\n if (!hasAccess) {\n return {\n success: false,\n error: `Insufficient permissions for tool \"${toolName}\". Required: ${tool.requiredFeatures.join(', ')}`,\n errorCode: 'UNAUTHORIZED',\n }\n }\n }\n\n // Input validation\n const parseResult = tool.inputSchema.safeParse(input)\n if (!parseResult.success) {\n // Use any cast for Zod v4 compatibility\n const issues = (parseResult.error as any).issues ?? []\n const errorMessages = issues\n .map((issue: { path: PropertyKey[]; message: string }) =>\n `${issue.path.join('.')}: ${issue.message}`\n )\n .join('; ')\n return {\n success: false,\n error: `Invalid input: ${errorMessages || 'Validation failed'}`,\n errorCode: 'VALIDATION_ERROR',\n }\n }\n\n // Execute tool\n try {\n const result = await tool.handler(parseResult.data, context)\n return { success: true, result }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n console.error(`[MCP Tool] Error executing \"${toolName}\":`, error)\n return {\n success: false,\n error: message,\n errorCode: 'EXECUTION_ERROR',\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,uBAAuB;AAChC,SAAS,2BAA2B;AAKpC,eAAsB,YACpB,UACA,OACA,SAC8B;AAC9B,QAAM,WAAW,gBAAgB;AACjC,QAAM,OAAO,SAAS,QAAQ,QAAQ;AAEtC,MAAI,CAAC,MAAM;AACT,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO,SAAS,QAAQ;AAAA,MACxB,WAAW;AAAA,IACb;AAAA,EACF;AAGA,MAAI,KAAK,kBAAkB,QAAQ;AACjC,UAAM,YAAY;AAAA,MAChB,KAAK;AAAA,MACL,QAAQ;AAAA,MACR,QAAQ;AAAA,IACV;AAEA,QAAI,CAAC,WAAW;AACd,aAAO;AAAA,QACL,SAAS;AAAA,QACT,OAAO,sCAAsC,QAAQ,gBAAgB,KAAK,iBAAiB,KAAK,IAAI,CAAC;AAAA,QACrG,WAAW;AAAA,MACb;AAAA,IACF;AAAA,EACF;AAGA,QAAM,cAAc,KAAK,YAAY,UAAU,KAAK;AACpD,MAAI,CAAC,YAAY,SAAS;AAExB,UAAM,SAAU,YAAY,MAAc,UAAU,CAAC;AACrD,UAAM,gBAAgB,OACnB;AAAA,MAAI,CAAC,UACJ,GAAG,MAAM,KAAK,KAAK,GAAG,CAAC,KAAK,MAAM,OAAO;AAAA,IAC3C,EACC,KAAK,IAAI;AACZ,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO,kBAAkB,iBAAiB,mBAAmB;AAAA,MAC7D,WAAW;AAAA,IACb;AAAA,EACF;AAGA,MAAI;AACF,UAAM,SAAS,MAAM,KAAK,QAAQ,YAAY,MAAM,OAAO;AAC3D,WAAO,EAAE,SAAS,MAAM,OAAO;AAAA,EACjC,SAAS,OAAO;AACd,UAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,YAAQ,MAAM,+BAA+B,QAAQ,MAAM,KAAK;AAChE,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO;AAAA,MACP,WAAW;AAAA,IACb;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|