mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-26 00:46:23 +02:00
first commit
This commit is contained in:
parent
476654af80
commit
6014437479
20 changed files with 2231 additions and 0 deletions
31
apps/cli/src/application/lib/mcp.ts
Normal file
31
apps/cli/src/application/lib/mcp.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
||||
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
||||
|
||||
export async function getMcpClient(serverUrl: string, serverName: string): Promise<Client> {
|
||||
let client: Client | undefined = undefined;
|
||||
const baseUrl = new URL(serverUrl);
|
||||
|
||||
// Try to connect using Streamable HTTP transport
|
||||
try {
|
||||
client = new Client({
|
||||
name: 'streamable-http-client',
|
||||
version: '1.0.0'
|
||||
});
|
||||
const transport = new StreamableHTTPClientTransport(baseUrl);
|
||||
await client.connect(transport);
|
||||
console.log(`[MCP] Connected using Streamable HTTP transport to ${serverName}`);
|
||||
return client;
|
||||
} catch (error) {
|
||||
// If that fails with a 4xx error, try the older SSE transport
|
||||
console.log(`[MCP] Streamable HTTP connection failed, falling back to SSE transport for ${serverName}`);
|
||||
client = new Client({
|
||||
name: 'sse-client',
|
||||
version: '1.0.0'
|
||||
});
|
||||
const sseTransport = new SSEClientTransport(baseUrl);
|
||||
await client.connect(sseTransport);
|
||||
console.log(`[MCP] Connected using SSE transport to ${serverName}`);
|
||||
return client;
|
||||
}
|
||||
}
|
||||
7
apps/cli/src/application/lib/random-id.ts
Normal file
7
apps/cli/src/application/lib/random-id.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { customAlphabet } from 'nanoid';
|
||||
const alphabet = '0123456789abcdefghijklmnopqrstuvwxyz-';
|
||||
const nanoid = customAlphabet(alphabet, 7);
|
||||
|
||||
export async function randomId(): Promise<string> {
|
||||
return nanoid();
|
||||
}
|
||||
151
apps/cli/src/application/lib/stream-renderer.ts
Normal file
151
apps/cli/src/application/lib/stream-renderer.ts
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
import { z } from "zod";
|
||||
import { StreamEvent } from "../entities/stream-event.js";
|
||||
|
||||
export interface StreamRendererOptions {
|
||||
showHeaders?: boolean;
|
||||
dimReasoning?: boolean;
|
||||
jsonIndent?: number;
|
||||
truncateJsonAt?: number;
|
||||
}
|
||||
|
||||
export class StreamRenderer {
|
||||
private options: Required<StreamRendererOptions>;
|
||||
private reasoningActive = false;
|
||||
private textActive = false;
|
||||
|
||||
constructor(options?: StreamRendererOptions) {
|
||||
this.options = {
|
||||
showHeaders: true,
|
||||
dimReasoning: true,
|
||||
jsonIndent: 2,
|
||||
truncateJsonAt: 500,
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
render(event: z.infer<typeof StreamEvent>) {
|
||||
switch (event.type) {
|
||||
case "reasoning-start":
|
||||
this.onReasoningStart();
|
||||
break;
|
||||
case "reasoning-delta":
|
||||
this.onReasoningDelta(event.delta);
|
||||
break;
|
||||
case "reasoning-end":
|
||||
this.onReasoningEnd();
|
||||
break;
|
||||
case "text-start":
|
||||
this.onTextStart();
|
||||
break;
|
||||
case "text-delta":
|
||||
this.onTextDelta(event.delta);
|
||||
break;
|
||||
case "text-end":
|
||||
this.onTextEnd();
|
||||
break;
|
||||
case "tool-call":
|
||||
this.onToolCall(event.toolCallId, event.toolName, event.input);
|
||||
break;
|
||||
case "usage":
|
||||
this.onUsage(event.usage);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private onReasoningStart() {
|
||||
if (this.reasoningActive) return;
|
||||
this.reasoningActive = true;
|
||||
if (this.options.showHeaders) {
|
||||
this.write("\n");
|
||||
this.write(this.dim("Reasoning: "));
|
||||
}
|
||||
}
|
||||
|
||||
private onReasoningDelta(delta: string) {
|
||||
if (!this.reasoningActive) this.onReasoningStart();
|
||||
this.write(this.options.dimReasoning ? this.dim(delta) : delta);
|
||||
}
|
||||
|
||||
private onReasoningEnd() {
|
||||
if (!this.reasoningActive) return;
|
||||
this.reasoningActive = false;
|
||||
this.write(this.dim("\n"));
|
||||
}
|
||||
|
||||
private onTextStart() {
|
||||
if (this.textActive) return;
|
||||
this.textActive = true;
|
||||
if (this.options.showHeaders) {
|
||||
this.write("\n");
|
||||
this.write(this.bold("Assistant: "));
|
||||
}
|
||||
}
|
||||
|
||||
private onTextDelta(delta: string) {
|
||||
if (!this.textActive) this.onTextStart();
|
||||
this.write(delta);
|
||||
}
|
||||
|
||||
private onTextEnd() {
|
||||
if (!this.textActive) return;
|
||||
this.textActive = false;
|
||||
this.write("\n");
|
||||
}
|
||||
|
||||
private onToolCall(toolCallId: string, toolName: string, input: unknown) {
|
||||
const inputStr = this.truncate(JSON.stringify(input, null, this.options.jsonIndent));
|
||||
this.write("\n");
|
||||
this.write(this.cyan(`→ Tool call ${toolName} (${toolCallId})`));
|
||||
this.write("\n");
|
||||
this.write(this.dim(this.indent(inputStr)));
|
||||
this.write("\n");
|
||||
}
|
||||
|
||||
private onUsage(usage: {
|
||||
inputTokens?: number;
|
||||
outputTokens?: number;
|
||||
totalTokens?: number;
|
||||
reasoningTokens?: number;
|
||||
cachedInputTokens?: number;
|
||||
}) {
|
||||
const parts: string[] = [];
|
||||
if (usage.inputTokens !== undefined) parts.push(`input=${usage.inputTokens}`);
|
||||
if (usage.outputTokens !== undefined) parts.push(`output=${usage.outputTokens}`);
|
||||
if (usage.reasoningTokens !== undefined) parts.push(`reasoning=${usage.reasoningTokens}`);
|
||||
if (usage.cachedInputTokens !== undefined) parts.push(`cached=${usage.cachedInputTokens}`);
|
||||
if (usage.totalTokens !== undefined) parts.push(`total=${usage.totalTokens}`);
|
||||
const line = parts.join(", ");
|
||||
this.write(this.dim(`\nUsage: ${line}\n`));
|
||||
}
|
||||
|
||||
// Formatting helpers
|
||||
private write(text: string) {
|
||||
process.stdout.write(text);
|
||||
}
|
||||
|
||||
private indent(text: string): string {
|
||||
return text
|
||||
.split("\n")
|
||||
.map((line) => (line.length ? ` ${line}` : line))
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
private truncate(text: string): string {
|
||||
if (text.length <= this.options.truncateJsonAt) return text;
|
||||
return text.slice(0, this.options.truncateJsonAt) + "…";
|
||||
}
|
||||
|
||||
private bold(text: string): string {
|
||||
return "\x1b[1m" + text + "\x1b[0m";
|
||||
}
|
||||
|
||||
private dim(text: string): string {
|
||||
return "\x1b[2m" + text + "\x1b[0m";
|
||||
}
|
||||
|
||||
private cyan(text: string): string {
|
||||
return "\x1b[36m" + text + "\x1b[0m";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue