frogo 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/launch.js +74 -215
- package/dist/agent/ui.js +232 -0
- package/dist/cli/commands/configure.js +53 -24
- package/dist/core/investigator.js +0 -2
- package/dist/core/pattern-engine.js +0 -3
- package/dist/mcp/sentry-auth.js +115 -0
- package/dist/mcp/trigger-auth.js +115 -0
- package/package.json +7 -2
- package/src/agent/launch.tsx +300 -0
- package/src/agent/ui.tsx +324 -0
- package/src/cli/commands/configure.ts +54 -25
- package/src/core/investigator.ts +0 -2
- package/src/core/pattern-engine.ts +0 -4
- package/src/core/types.ts +14 -3
- package/src/mcp/sentry-auth.ts +134 -0
- package/tsconfig.json +1 -0
- package/src/agent/launch.ts +0 -449
- package/src/connectors/vercel.ts +0 -16
package/dist/agent/launch.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
2
|
import "dotenv/config";
|
|
2
|
-
import readline from "node:readline/promises";
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
|
-
import
|
|
5
|
-
import { stdin as input, stdout as output } from "node:process";
|
|
4
|
+
import { render } from "ink";
|
|
6
5
|
import { ToolLoopAgent, stepCountIs } from "ai";
|
|
7
6
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
8
7
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
9
8
|
import { createMCPClient } from "@ai-sdk/mcp";
|
|
10
9
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
11
10
|
import { loadConfig } from "../config/load.js";
|
|
11
|
+
import { getSentryOAuthProvider, hasSentryTokens } from "../mcp/sentry-auth.js";
|
|
12
|
+
import { FrogoChatApp } from "./ui.js";
|
|
12
13
|
const DEFAULT_SYSTEM_PROMPT = "You are Frogo, a deterministic incident investigator. Respond concisely, cite evidence, and avoid hallucinations. " +
|
|
13
14
|
"If the user says 'do a pass' or 'anything you can get', fetch a minimal overview: projects, recent runs, datasets, and prompts (if available) " +
|
|
14
15
|
"using MCP tools, then summarize what you found.";
|
|
@@ -87,6 +88,61 @@ async function buildLangSmithToolContext(config) {
|
|
|
87
88
|
return null;
|
|
88
89
|
}
|
|
89
90
|
}
|
|
91
|
+
async function createSentryClient(sentry) {
|
|
92
|
+
const { provider } = await getSentryOAuthProvider();
|
|
93
|
+
return await createMCPClient({
|
|
94
|
+
transport: {
|
|
95
|
+
type: "http",
|
|
96
|
+
url: sentry.mcpUrl,
|
|
97
|
+
authProvider: provider
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async function buildSentryToolContext(config) {
|
|
102
|
+
const sentry = config.sentry;
|
|
103
|
+
if (!sentry?.mcpUrl) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const hasTokens = await hasSentryTokens();
|
|
108
|
+
if (!hasTokens) {
|
|
109
|
+
console.log("↳ Sentry not authenticated. Run `frogo configure` to complete OAuth.");
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
const client = await createSentryClient(sentry);
|
|
113
|
+
const tools = await client.tools();
|
|
114
|
+
return { name: "Sentry", tools, client };
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
console.error("Sentry MCP initialization failed:", error);
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function buildTriggerMcpToolContext(config) {
|
|
122
|
+
const triggerMcp = config.triggerMcp;
|
|
123
|
+
if (!triggerMcp?.command || !triggerMcp?.args || triggerMcp.args.length === 0) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
const transport = new StdioClientTransport({
|
|
128
|
+
command: triggerMcp.command,
|
|
129
|
+
args: triggerMcp.args,
|
|
130
|
+
env: { ...process.env },
|
|
131
|
+
stderr: "inherit"
|
|
132
|
+
});
|
|
133
|
+
const client = await createMCPClient({
|
|
134
|
+
transport,
|
|
135
|
+
name: "frogo-trigger",
|
|
136
|
+
version: "0.1.0"
|
|
137
|
+
});
|
|
138
|
+
const tools = await client.tools();
|
|
139
|
+
return { name: "Trigger", tools, client };
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
console.error("Trigger MCP initialization failed:", error);
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
90
146
|
async function buildDatadogToolContext(config) {
|
|
91
147
|
const datadog = config.datadog;
|
|
92
148
|
if (!datadog?.apiKey || !datadog?.appKey) {
|
|
@@ -132,6 +188,14 @@ async function buildMcpToolContexts(config) {
|
|
|
132
188
|
if (langsmithContext) {
|
|
133
189
|
contexts.push(langsmithContext);
|
|
134
190
|
}
|
|
191
|
+
const sentryContext = await buildSentryToolContext(config);
|
|
192
|
+
if (sentryContext) {
|
|
193
|
+
contexts.push(sentryContext);
|
|
194
|
+
}
|
|
195
|
+
const triggerMcpContext = await buildTriggerMcpToolContext(config);
|
|
196
|
+
if (triggerMcpContext) {
|
|
197
|
+
contexts.push(triggerMcpContext);
|
|
198
|
+
}
|
|
135
199
|
const datadogContext = await buildDatadogToolContext(config);
|
|
136
200
|
if (datadogContext) {
|
|
137
201
|
contexts.push(datadogContext);
|
|
@@ -151,86 +215,9 @@ async function cleanupMcpContexts(contexts) {
|
|
|
151
215
|
}
|
|
152
216
|
}));
|
|
153
217
|
}
|
|
154
|
-
function createSpinner(label = "thinking") {
|
|
155
|
-
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧"];
|
|
156
|
-
let index = 0;
|
|
157
|
-
let handle;
|
|
158
|
-
const isTty = Boolean(process.stdout.isTTY);
|
|
159
|
-
const render = () => {
|
|
160
|
-
const frame = frames[index];
|
|
161
|
-
index = (index + 1) % frames.length;
|
|
162
|
-
process.stdout.write(`\r${chalk.gray(frame)} ${chalk.dim(label)}`);
|
|
163
|
-
};
|
|
164
|
-
return {
|
|
165
|
-
start() {
|
|
166
|
-
if (!isTty) {
|
|
167
|
-
process.stdout.write(`${chalk.dim(label)}...\n`);
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
render();
|
|
171
|
-
handle = setInterval(render, 80);
|
|
172
|
-
},
|
|
173
|
-
stop() {
|
|
174
|
-
if (!isTty) {
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
if (handle) {
|
|
178
|
-
clearInterval(handle);
|
|
179
|
-
handle = undefined;
|
|
180
|
-
}
|
|
181
|
-
process.stdout.write("\r");
|
|
182
|
-
process.stdout.write(" ".repeat(32));
|
|
183
|
-
process.stdout.write("\r");
|
|
184
|
-
}
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
218
|
function cleanAssistantOutput(content) {
|
|
188
219
|
return content.replace(/^Agent:\s*/i, "").trim();
|
|
189
220
|
}
|
|
190
|
-
function renderBanner(context, mcpContexts) {
|
|
191
|
-
const sessionId = crypto.randomBytes(8).toString("hex");
|
|
192
|
-
const workdir = process.cwd();
|
|
193
|
-
const providerName = context.languageModel.modelId ?? context.languageModel?.model ?? "unknown";
|
|
194
|
-
const providerLabel = context.languageModel?.provider ?? "openai";
|
|
195
|
-
const mcpLabel = mcpContexts.length ? mcpContexts.map((item) => item.name).join(", ") : "none";
|
|
196
|
-
const header = "🐸 Frogo CLI";
|
|
197
|
-
const headerLine = `│ ● ${header} │`;
|
|
198
|
-
const sessionLines = [
|
|
199
|
-
`│ session: ${sessionId} │`,
|
|
200
|
-
`│ ↳ workdir: ${workdir} │`,
|
|
201
|
-
`│ ↳ model: ${providerName} │`,
|
|
202
|
-
`│ ↳ provider: ${providerLabel} │`,
|
|
203
|
-
`│ ↳ mcp: ${mcpLabel} │`
|
|
204
|
-
];
|
|
205
|
-
const headerWidth = Math.max(headerLine.length, ...sessionLines.map((line) => line.length));
|
|
206
|
-
const drawLine = (text) => {
|
|
207
|
-
const padded = text.padEnd(headerWidth - 1, " ");
|
|
208
|
-
return `${padded}│`;
|
|
209
|
-
};
|
|
210
|
-
const top = `╭${"─".repeat(headerWidth - 1)}╮`;
|
|
211
|
-
const bottom = `╰${"─".repeat(headerWidth - 1)}╯`;
|
|
212
|
-
console.log(chalk.gray(top));
|
|
213
|
-
console.log(chalk.gray(drawLine(headerLine)));
|
|
214
|
-
console.log(chalk.gray(bottom));
|
|
215
|
-
console.log(chalk.gray(top));
|
|
216
|
-
sessionLines.forEach((line) => console.log(chalk.gray(drawLine(line))));
|
|
217
|
-
console.log(chalk.gray(bottom));
|
|
218
|
-
}
|
|
219
|
-
function renderAssistantPrefix() {
|
|
220
|
-
process.stdout.write(`${chalk.green("🐸")} `);
|
|
221
|
-
}
|
|
222
|
-
function formatToolPayload(payload, max = 400) {
|
|
223
|
-
try {
|
|
224
|
-
const raw = JSON.stringify(payload);
|
|
225
|
-
if (raw.length <= max) {
|
|
226
|
-
return raw;
|
|
227
|
-
}
|
|
228
|
-
return `${raw.slice(0, max)}…`;
|
|
229
|
-
}
|
|
230
|
-
catch {
|
|
231
|
-
return "[unserializable]";
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
221
|
export async function runAgentChat() {
|
|
235
222
|
const config = await loadConfig();
|
|
236
223
|
const context = buildProviderContext(config);
|
|
@@ -238,7 +225,6 @@ export async function runAgentChat() {
|
|
|
238
225
|
process.exit(1);
|
|
239
226
|
}
|
|
240
227
|
const mcpContexts = await buildMcpToolContexts(config);
|
|
241
|
-
renderBanner(context, mcpContexts);
|
|
242
228
|
const toolSet = combineToolSets(mcpContexts);
|
|
243
229
|
const agent = new ToolLoopAgent({
|
|
244
230
|
id: "frogo-agent",
|
|
@@ -247,138 +233,11 @@ export async function runAgentChat() {
|
|
|
247
233
|
tools: Object.keys(toolSet).length ? toolSet : undefined,
|
|
248
234
|
stopWhen: stepCountIs(1000)
|
|
249
235
|
});
|
|
250
|
-
const
|
|
251
|
-
const
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
rl.on("SIGINT", () => {
|
|
258
|
-
if (isGenerating && activeAbort) {
|
|
259
|
-
activeAbort.abort();
|
|
260
|
-
process.stdout.write(`\n${chalk.dim("↳ you canceled the current response. Ask another question when ready.")}\n`);
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
rl.close();
|
|
264
|
-
});
|
|
265
|
-
while (true) {
|
|
266
|
-
let question;
|
|
267
|
-
try {
|
|
268
|
-
question = await rl.question(`${chalk.cyan("›")} `);
|
|
269
|
-
}
|
|
270
|
-
catch (error) {
|
|
271
|
-
if (error instanceof Error && error.message === "SIGINT") {
|
|
272
|
-
break;
|
|
273
|
-
}
|
|
274
|
-
throw error;
|
|
275
|
-
}
|
|
276
|
-
const trimmed = question.trim();
|
|
277
|
-
if (!trimmed) {
|
|
278
|
-
continue;
|
|
279
|
-
}
|
|
280
|
-
conversation.push({ role: "user", content: trimmed });
|
|
281
|
-
spinner.start();
|
|
282
|
-
isGenerating = true;
|
|
283
|
-
activeAbort = new AbortController();
|
|
284
|
-
let spinnerActive = true;
|
|
285
|
-
let assistantReply = "";
|
|
286
|
-
let prefixPrinted = false;
|
|
287
|
-
let lineOpen = false;
|
|
288
|
-
const ensurePrefix = () => {
|
|
289
|
-
if (!prefixPrinted) {
|
|
290
|
-
renderAssistantPrefix();
|
|
291
|
-
prefixPrinted = true;
|
|
292
|
-
lineOpen = true;
|
|
293
|
-
}
|
|
294
|
-
};
|
|
295
|
-
const ensureNewline = () => {
|
|
296
|
-
if (lineOpen) {
|
|
297
|
-
process.stdout.write("\n");
|
|
298
|
-
lineOpen = false;
|
|
299
|
-
prefixPrinted = false;
|
|
300
|
-
}
|
|
301
|
-
};
|
|
302
|
-
try {
|
|
303
|
-
const streamResult = await agent.stream({
|
|
304
|
-
messages: conversation,
|
|
305
|
-
abortSignal: activeAbort.signal
|
|
306
|
-
});
|
|
307
|
-
for await (const part of streamResult.fullStream) {
|
|
308
|
-
switch (part.type) {
|
|
309
|
-
case "text-delta": {
|
|
310
|
-
if (spinnerActive) {
|
|
311
|
-
spinner.stop();
|
|
312
|
-
spinnerActive = false;
|
|
313
|
-
ensurePrefix();
|
|
314
|
-
}
|
|
315
|
-
ensurePrefix();
|
|
316
|
-
process.stdout.write(part.text);
|
|
317
|
-
assistantReply += part.text;
|
|
318
|
-
break;
|
|
319
|
-
}
|
|
320
|
-
case "tool-call": {
|
|
321
|
-
if (spinnerActive) {
|
|
322
|
-
spinner.stop();
|
|
323
|
-
spinnerActive = false;
|
|
324
|
-
}
|
|
325
|
-
ensureNewline();
|
|
326
|
-
const payload = formatToolPayload(part.input);
|
|
327
|
-
process.stdout.write(`${chalk.dim("↳ tool call")} ${chalk.cyan(part.toolName)} ${chalk.dim(payload)}\n`);
|
|
328
|
-
break;
|
|
329
|
-
}
|
|
330
|
-
case "tool-result": {
|
|
331
|
-
ensureNewline();
|
|
332
|
-
const payload = formatToolPayload(part.output);
|
|
333
|
-
process.stdout.write(`${chalk.dim("↳ tool result")} ${chalk.cyan(part.toolName)} ${chalk.dim(payload)}\n`);
|
|
334
|
-
break;
|
|
335
|
-
}
|
|
336
|
-
case "tool-error": {
|
|
337
|
-
ensureNewline();
|
|
338
|
-
process.stdout.write(`${chalk.red("↳ tool error")} ${chalk.cyan(part.toolName)} ${chalk.dim(String(part.error))}\n`);
|
|
339
|
-
break;
|
|
340
|
-
}
|
|
341
|
-
case "tool-approval-request": {
|
|
342
|
-
ensureNewline();
|
|
343
|
-
process.stdout.write(`${chalk.yellow("↳ tool approval")} ${chalk.cyan(part.toolCall.toolName)}\n`);
|
|
344
|
-
break;
|
|
345
|
-
}
|
|
346
|
-
case "tool-output-denied": {
|
|
347
|
-
ensureNewline();
|
|
348
|
-
process.stdout.write(`${chalk.yellow("↳ tool output denied")} ${chalk.cyan(part.toolName)}\n`);
|
|
349
|
-
break;
|
|
350
|
-
}
|
|
351
|
-
default:
|
|
352
|
-
break;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
if (spinnerActive) {
|
|
356
|
-
spinner.stop();
|
|
357
|
-
}
|
|
358
|
-
process.stdout.write("\n");
|
|
359
|
-
const cleaned = cleanAssistantOutput(assistantReply);
|
|
360
|
-
if (cleaned) {
|
|
361
|
-
conversation.push({ role: "assistant", content: cleaned });
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
catch (error) {
|
|
365
|
-
spinner.stop();
|
|
366
|
-
if (activeAbort?.signal.aborted) {
|
|
367
|
-
console.log(`\n${chalk.dim("↳ you canceled the current response. Ask another question when ready.")}`);
|
|
368
|
-
}
|
|
369
|
-
else {
|
|
370
|
-
console.error("Agent call failed:", error);
|
|
371
|
-
break;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
finally {
|
|
375
|
-
isGenerating = false;
|
|
376
|
-
activeAbort = null;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
finally {
|
|
381
|
-
rl.close();
|
|
382
|
-
await cleanupMcpContexts(mcpContexts);
|
|
383
|
-
}
|
|
236
|
+
const sessionId = crypto.randomBytes(8).toString("hex");
|
|
237
|
+
const workdir = process.cwd();
|
|
238
|
+
const modelLabel = context.languageModel.modelId ?? context.languageModel?.model ?? "unknown";
|
|
239
|
+
const providerLabel = context.languageModel?.provider ?? "openai";
|
|
240
|
+
const app = render(_jsx(FrogoChatApp, { agent: agent, systemPrompt: context.systemPrompt, mcpContexts: mcpContexts, modelLabel: modelLabel, providerLabel: providerLabel, workdir: workdir, sessionId: sessionId }));
|
|
241
|
+
await app.waitUntilExit();
|
|
242
|
+
await cleanupMcpContexts(mcpContexts);
|
|
384
243
|
}
|
package/dist/agent/ui.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
4
|
+
import TextInput from "ink-text-input";
|
|
5
|
+
import Spinner from "ink-spinner";
|
|
6
|
+
const makeId = (() => {
|
|
7
|
+
let i = 0;
|
|
8
|
+
return () => `${Date.now()}-${i++}`;
|
|
9
|
+
})();
|
|
10
|
+
function formatToolPayload(payload, max = 400) {
|
|
11
|
+
try {
|
|
12
|
+
const raw = JSON.stringify(payload);
|
|
13
|
+
if (raw.length <= max) {
|
|
14
|
+
return raw;
|
|
15
|
+
}
|
|
16
|
+
return `${raw.slice(0, max)}…`;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return "[unserializable]";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function renderHelpLines() {
|
|
23
|
+
return [
|
|
24
|
+
"Slash commands:",
|
|
25
|
+
"/mcp List MCP connections and tools",
|
|
26
|
+
"/mcp list Same as /mcp",
|
|
27
|
+
"/help Show help",
|
|
28
|
+
"/exit Exit Frogo"
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
export function FrogoChatApp(props) {
|
|
32
|
+
const { agent, mcpContexts, modelLabel, providerLabel, workdir, sessionId } = props;
|
|
33
|
+
const { exit } = useApp();
|
|
34
|
+
const [input, setInput] = useState("");
|
|
35
|
+
const [messages, setMessages] = useState([]);
|
|
36
|
+
const [isGenerating, setIsGenerating] = useState(false);
|
|
37
|
+
const [slashHint, setSlashHint] = useState(false);
|
|
38
|
+
const [slashIndex, setSlashIndex] = useState(0);
|
|
39
|
+
const abortRef = useRef(null);
|
|
40
|
+
const conversationRef = useRef([]);
|
|
41
|
+
const slashOptions = useMemo(() => ["/mcp", "/help", "/exit"], []);
|
|
42
|
+
const toolSummary = useMemo(() => {
|
|
43
|
+
if (!mcpContexts.length) {
|
|
44
|
+
return "none";
|
|
45
|
+
}
|
|
46
|
+
return mcpContexts.map((ctx) => ctx.name).join(", ");
|
|
47
|
+
}, [mcpContexts]);
|
|
48
|
+
const addMessage = useCallback((msg) => {
|
|
49
|
+
setMessages((prev) => [...prev, msg]);
|
|
50
|
+
}, []);
|
|
51
|
+
const updateMessage = useCallback((id, content) => {
|
|
52
|
+
setMessages((prev) => prev.map((msg) => (msg.id === id ? { ...msg, content } : msg)));
|
|
53
|
+
}, []);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
setSlashHint(input === "/");
|
|
56
|
+
if (input !== "/") {
|
|
57
|
+
setSlashIndex(0);
|
|
58
|
+
}
|
|
59
|
+
}, [input]);
|
|
60
|
+
useInput((_, key) => {
|
|
61
|
+
if (!key)
|
|
62
|
+
return;
|
|
63
|
+
const keyName = key.name;
|
|
64
|
+
if (key.ctrl && keyName === "c") {
|
|
65
|
+
if (isGenerating && abortRef.current) {
|
|
66
|
+
abortRef.current.abort();
|
|
67
|
+
addMessage({
|
|
68
|
+
id: makeId(),
|
|
69
|
+
role: "system",
|
|
70
|
+
content: "↳ you canceled the current response. Ask another question when ready."
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
exit();
|
|
75
|
+
}
|
|
76
|
+
if (!isGenerating && slashHint) {
|
|
77
|
+
if (key.upArrow || keyName === "up") {
|
|
78
|
+
setSlashIndex((prev) => (prev - 1 + slashOptions.length) % slashOptions.length);
|
|
79
|
+
setInput("/");
|
|
80
|
+
}
|
|
81
|
+
else if (key.downArrow || keyName === "down") {
|
|
82
|
+
setSlashIndex((prev) => (prev + 1) % slashOptions.length);
|
|
83
|
+
setInput("/");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
const handleSlashCommand = useCallback((command) => {
|
|
88
|
+
if (command === "/" || command === "/help") {
|
|
89
|
+
renderHelpLines().forEach((line) => {
|
|
90
|
+
addMessage({ id: makeId(), role: "system", content: line });
|
|
91
|
+
});
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
if (command === "/mcp" || command === "/mcp list") {
|
|
95
|
+
if (!mcpContexts.length) {
|
|
96
|
+
addMessage({ id: makeId(), role: "system", content: "No MCP integrations connected." });
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
mcpContexts.forEach((ctx) => {
|
|
100
|
+
const tools = Object.keys(ctx.tools);
|
|
101
|
+
addMessage({
|
|
102
|
+
id: makeId(),
|
|
103
|
+
role: "system",
|
|
104
|
+
content: `${ctx.name} (${tools.length} tools)`
|
|
105
|
+
});
|
|
106
|
+
if (tools.length) {
|
|
107
|
+
addMessage({
|
|
108
|
+
id: makeId(),
|
|
109
|
+
role: "system",
|
|
110
|
+
content: `tools: ${tools.join(", ")}`
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
if (command === "/exit") {
|
|
117
|
+
exit();
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
addMessage({ id: makeId(), role: "system", content: `Unknown command: ${command}` });
|
|
121
|
+
return true;
|
|
122
|
+
}, [addMessage, exit, mcpContexts]);
|
|
123
|
+
const handleSubmit = useCallback(async (value) => {
|
|
124
|
+
const trimmed = value.trim();
|
|
125
|
+
setInput("");
|
|
126
|
+
if (!trimmed) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (trimmed === "/" && slashHint) {
|
|
130
|
+
handleSlashCommand(slashOptions[slashIndex]);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (slashHint) {
|
|
134
|
+
handleSlashCommand(trimmed);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (trimmed.startsWith("/")) {
|
|
138
|
+
handleSlashCommand(trimmed);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
addMessage({ id: makeId(), role: "user", content: trimmed });
|
|
142
|
+
conversationRef.current.push({ role: "user", content: trimmed });
|
|
143
|
+
const assistantId = makeId();
|
|
144
|
+
addMessage({ id: assistantId, role: "assistant", content: "" });
|
|
145
|
+
abortRef.current = new AbortController();
|
|
146
|
+
setIsGenerating(true);
|
|
147
|
+
try {
|
|
148
|
+
const streamResult = await agent.stream({
|
|
149
|
+
messages: conversationRef.current,
|
|
150
|
+
abortSignal: abortRef.current.signal
|
|
151
|
+
});
|
|
152
|
+
let assistantContent = "";
|
|
153
|
+
for await (const part of streamResult.fullStream) {
|
|
154
|
+
if (part.type === "text-delta") {
|
|
155
|
+
assistantContent += part.text;
|
|
156
|
+
updateMessage(assistantId, assistantContent);
|
|
157
|
+
}
|
|
158
|
+
else if (part.type === "tool-call") {
|
|
159
|
+
addMessage({
|
|
160
|
+
id: makeId(),
|
|
161
|
+
role: "tool",
|
|
162
|
+
content: `↳ tool call ${part.toolName} ${formatToolPayload(part.input)}`
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
else if (part.type === "tool-result") {
|
|
166
|
+
addMessage({
|
|
167
|
+
id: makeId(),
|
|
168
|
+
role: "tool",
|
|
169
|
+
content: `↳ tool result ${part.toolName} ${formatToolPayload(part.output)}`
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
else if (part.type === "tool-error") {
|
|
173
|
+
addMessage({
|
|
174
|
+
id: makeId(),
|
|
175
|
+
role: "tool",
|
|
176
|
+
content: `↳ tool error ${part.toolName} ${String(part.error)}`
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
else if (part.type === "tool-output-denied") {
|
|
180
|
+
const toolName = part.toolName ?? "tool";
|
|
181
|
+
addMessage({
|
|
182
|
+
id: makeId(),
|
|
183
|
+
role: "tool",
|
|
184
|
+
content: `↳ tool output denied ${toolName}`
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
else if (part.type === "tool-approval-request") {
|
|
188
|
+
addMessage({
|
|
189
|
+
id: makeId(),
|
|
190
|
+
role: "tool",
|
|
191
|
+
content: `↳ tool approval ${part.toolCall.toolName}`
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const cleaned = assistantContent.replace(/^Agent:\\s*/i, "").trim();
|
|
196
|
+
updateMessage(assistantId, cleaned);
|
|
197
|
+
conversationRef.current.push({ role: "assistant", content: cleaned });
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
if (abortRef.current?.signal.aborted) {
|
|
201
|
+
addMessage({
|
|
202
|
+
id: makeId(),
|
|
203
|
+
role: "system",
|
|
204
|
+
content: "↳ you canceled the current response. Ask another question when ready."
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
addMessage({
|
|
209
|
+
id: makeId(),
|
|
210
|
+
role: "system",
|
|
211
|
+
content: `Agent call failed: ${error instanceof Error ? error.message : String(error)}`
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
abortRef.current = null;
|
|
217
|
+
setIsGenerating(false);
|
|
218
|
+
}
|
|
219
|
+
}, [addMessage, agent, handleSlashCommand, slashHint, slashIndex, slashOptions, updateMessage]);
|
|
220
|
+
return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "gray", children: "\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E" }), _jsx(Text, { color: "gray", children: "\u2502 \u25CF \uD83D\uDC38 Frogo CLI \u2502 \u2502" }), _jsx(Text, { color: "gray", children: "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F" }), _jsx(Text, { color: "gray", children: "\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E" }), _jsxs(Text, { color: "gray", children: ["\u2502 session: ", sessionId, " \u2502"] }), _jsxs(Text, { color: "gray", children: ["\u2502 \u21B3 workdir: ", workdir, " \u2502"] }), _jsxs(Text, { color: "gray", children: ["\u2502 \u21B3 model: ", modelLabel, " \u2502"] }), _jsxs(Text, { color: "gray", children: ["\u2502 \u21B3 provider: ", providerLabel, " \u2502"] }), _jsxs(Text, { color: "gray", children: ["\u2502 \u21B3 mcp: ", toolSummary, " \u2502"] }), _jsx(Text, { color: "gray", children: "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F" })] }), _jsx(Box, { flexDirection: "column", flexGrow: 1, marginTop: 1, children: messages.map((msg) => {
|
|
221
|
+
if (msg.role === "user") {
|
|
222
|
+
return (_jsxs(Text, { color: "cyan", children: ["\u203A ", msg.content] }, msg.id));
|
|
223
|
+
}
|
|
224
|
+
if (msg.role === "assistant") {
|
|
225
|
+
return (_jsxs(Text, { color: "green", children: ["\uD83D\uDC38 ", msg.content] }, msg.id));
|
|
226
|
+
}
|
|
227
|
+
if (msg.role === "tool") {
|
|
228
|
+
return (_jsx(Text, { color: "gray", children: msg.content }, msg.id));
|
|
229
|
+
}
|
|
230
|
+
return (_jsx(Text, { color: "gray", children: msg.content }, msg.id));
|
|
231
|
+
}) }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "cyan", children: "\u203A " }), _jsx(TextInput, { value: input, onChange: setInput, onSubmit: handleSubmit }), isGenerating ? (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: "gray", children: [_jsx(Spinner, { type: "dots" }), " thinking"] }) })) : null] }), slashHint ? (_jsx(Box, { marginTop: 1, flexDirection: "column", children: slashOptions.map((option, idx) => (_jsxs(Text, { color: idx === slashIndex ? "cyan" : "gray", children: [idx === slashIndex ? "› " : " ", option] }, option))) })) : null] }));
|
|
232
|
+
}
|