mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-06 19:35:44 +02:00
commit
428faea4f0
4 changed files with 71 additions and 48 deletions
|
|
@ -47,7 +47,7 @@ Inspired by Claude Code, RowboatX brings the same shell-native power to backgrou
|
|||
|
||||
## Quick start
|
||||
```bash
|
||||
npx @rowboatlabs/rowboatx
|
||||
npx @rowboatlabs/rowboatx@latest
|
||||
```
|
||||
|
||||
## Demo
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@rowboatlabs/rowboatx",
|
||||
"version": "0.11.0",
|
||||
"version": "0.13.0",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ import { WorkDir, getModelConfig, updateModelConfig } from "./application/config
|
|||
import { RunEvent } from "./application/entities/run-events.js";
|
||||
import { createInterface, Interface } from "node:readline/promises";
|
||||
import { ToolCallPart } from "./application/entities/message.js";
|
||||
import { z } from "zod";
|
||||
import { keyof, z } from "zod";
|
||||
import { Flavor, ModelConfig } from "./application/entities/models.js";
|
||||
|
||||
export async function updateState(agent: string, runId: string) {
|
||||
const state = new AgentState(agent, runId);
|
||||
|
|
@ -216,15 +217,15 @@ export async function modelConfig() {
|
|||
|
||||
const rl = createInterface({ input, output });
|
||||
try {
|
||||
const flavors = [
|
||||
"openai",
|
||||
"anthropic",
|
||||
"google",
|
||||
"ollama",
|
||||
"openai-compatible",
|
||||
"openrouter",
|
||||
] as const;
|
||||
const defaultBaseUrls: Record<(typeof flavors)[number], string> = {
|
||||
const defaultApiKeyEnvVars: Record<z.infer<typeof Flavor>, string> = {
|
||||
openai: "OPENAI_API_KEY",
|
||||
anthropic: "ANTHROPIC_API_KEY",
|
||||
google: "GOOGLE_GENERATIVE_AI_API_KEY",
|
||||
ollama: "",
|
||||
"openai-compatible": "",
|
||||
openrouter: "",
|
||||
};
|
||||
const defaultBaseUrls: Record<z.infer<typeof Flavor>, string> = {
|
||||
openai: "https://api.openai.com/v1",
|
||||
anthropic: "https://api.anthropic.com/v1",
|
||||
google: "https://generativelanguage.googleapis.com/v1beta",
|
||||
|
|
@ -232,12 +233,12 @@ export async function modelConfig() {
|
|||
"openai-compatible": "http://localhost:8080/v1",
|
||||
openrouter: "https://openrouter.ai/api/v1",
|
||||
};
|
||||
const defaultModels: Record<(typeof flavors)[number], string> = {
|
||||
const defaultModels: Record<z.infer<typeof Flavor>, string> = {
|
||||
openai: "gpt-5.1",
|
||||
anthropic: "claude-3.5-sonnet",
|
||||
google: "gemini-1.5-pro",
|
||||
anthropic: "claude-sonnet-4-5",
|
||||
google: "gemini-2.5-pro",
|
||||
ollama: "llama3.1",
|
||||
"openai-compatible": "gpt-4o",
|
||||
"openai-compatible": "openai/gpt-5.1",
|
||||
openrouter: "openrouter/auto",
|
||||
};
|
||||
|
||||
|
|
@ -245,31 +246,25 @@ export async function modelConfig() {
|
|||
const currentModel = config?.defaults?.model;
|
||||
const currentProviderConfig = currentProvider ? config?.providers?.[currentProvider] : undefined;
|
||||
if (config) {
|
||||
console.log("Currently using:");
|
||||
console.log(`- provider: ${currentProvider || "none"}${currentProviderConfig?.flavor ? ` (${currentProviderConfig.flavor})` : ""}`);
|
||||
console.log(`- model: ${currentModel || "none"}`);
|
||||
console.log("");
|
||||
renderCurrentModel(currentProvider || "none", currentProviderConfig?.flavor || "", currentModel || "none");
|
||||
}
|
||||
|
||||
const flavorPromptLines = flavors
|
||||
const FlavorList = [...Flavor.options];
|
||||
const flavorPromptLines = FlavorList
|
||||
.map((f, idx) => ` ${idx + 1}. ${f}`)
|
||||
.join("\n");
|
||||
const flavorAnswer = await rl.question(
|
||||
`Select a provider type:\n${flavorPromptLines}\nEnter number or name` +
|
||||
(currentProvider ? ` [${currentProvider}]` : "") +
|
||||
": ",
|
||||
`Select a provider type:\n${flavorPromptLines}\nEnter number or name: `
|
||||
);
|
||||
let selectedFlavorRaw = flavorAnswer.trim();
|
||||
let selectedFlavor: (typeof flavors)[number] | null = null;
|
||||
if (selectedFlavorRaw === "" && currentProvider && (flavors as readonly string[]).includes(currentProvider)) {
|
||||
selectedFlavor = currentProvider as (typeof flavors)[number];
|
||||
} else if (/^\d+$/.test(selectedFlavorRaw)) {
|
||||
let selectedFlavor: z.infer<typeof Flavor> | null = null;
|
||||
if (/^\d+$/.test(selectedFlavorRaw)) {
|
||||
const idx = parseInt(selectedFlavorRaw, 10) - 1;
|
||||
if (idx >= 0 && idx < flavors.length) {
|
||||
selectedFlavor = flavors[idx];
|
||||
if (idx >= 0 && idx < FlavorList.length) {
|
||||
selectedFlavor = FlavorList[idx];
|
||||
}
|
||||
} else if ((flavors as readonly string[]).includes(selectedFlavorRaw)) {
|
||||
selectedFlavor = selectedFlavorRaw as (typeof flavors)[number];
|
||||
} else if (FlavorList.includes(selectedFlavorRaw as z.infer<typeof Flavor>)) {
|
||||
selectedFlavor = selectedFlavorRaw as z.infer<typeof Flavor>;
|
||||
}
|
||||
if (!selectedFlavor) {
|
||||
console.error("Invalid selection. Exiting.");
|
||||
|
|
@ -338,21 +333,38 @@ export async function modelConfig() {
|
|||
return;
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
|
||||
const providerNameAns = await rl.question(
|
||||
`Enter a name/alias for this provider [${selectedFlavor}]: `,
|
||||
);
|
||||
providerName = providerNameAns.trim() || selectedFlavor;
|
||||
|
||||
const baseUrlDefault = defaultBaseUrls[selectedFlavor] || "";
|
||||
const baseUrlAns = await rl.question(
|
||||
`Enter baseURL for ${selectedFlavor} [${baseUrlDefault}]: `,
|
||||
`Enter baseURL for ${selectedFlavor} [${defaultBaseUrls[selectedFlavor]}]: `,
|
||||
);
|
||||
const baseURL = (baseUrlAns.trim() || baseUrlDefault) || undefined;
|
||||
const baseURL = baseUrlAns.trim() || undefined;
|
||||
|
||||
const apiKeyAns = await rl.question(
|
||||
`Enter API key for ${selectedFlavor} (leave blank to skip): `,
|
||||
);
|
||||
const apiKey = apiKeyAns.trim() || undefined;
|
||||
let apiKey: string | undefined = undefined;
|
||||
if (selectedFlavor !== "ollama") {
|
||||
let autopickText = "";
|
||||
if (defaultApiKeyEnvVars[selectedFlavor]) {
|
||||
autopickText = ` (leave blank to pick from environment variable ${defaultApiKeyEnvVars[selectedFlavor]})`;
|
||||
}
|
||||
const apiKeyAns = await rl.question(
|
||||
`Enter API key for ${selectedFlavor}${autopickText}: `,
|
||||
);
|
||||
apiKey = apiKeyAns.trim() || undefined;
|
||||
}
|
||||
if (selectedFlavor === "ollama") {
|
||||
const keyAns = await rl.question(
|
||||
`Enter API key for ${selectedFlavor} (optional): `
|
||||
);
|
||||
const key = keyAns.trim();
|
||||
if (key) {
|
||||
headers["Authorization"] = `Bearer ${key}`;
|
||||
}
|
||||
}
|
||||
|
||||
const modelDefault = defaultModels[selectedFlavor];
|
||||
const modelAns = await rl.question(
|
||||
|
|
@ -366,6 +378,7 @@ export async function modelConfig() {
|
|||
flavor: selectedFlavor,
|
||||
...(apiKey ? { apiKey } : {}),
|
||||
...(baseURL ? { baseURL } : {}),
|
||||
...(headers ? { headers } : {}),
|
||||
},
|
||||
};
|
||||
const newConfig = {
|
||||
|
|
@ -377,8 +390,16 @@ export async function modelConfig() {
|
|||
};
|
||||
|
||||
await updateModelConfig(newConfig as any);
|
||||
console.log(`Model configuration updated. Provider '${providerName}' ${config?.providers?.[providerName] ? "overwritten" : "added"}.`);
|
||||
renderCurrentModel(providerName, selectedFlavor, model);
|
||||
console.log(`Configuration written to ${WorkDir}/config/models.json. You can also edit this file manually`);
|
||||
} finally {
|
||||
rl.close();
|
||||
}
|
||||
}
|
||||
|
||||
function renderCurrentModel(provider: string, flavor: string, model: string) {
|
||||
console.log("Currently using:");
|
||||
console.log(`- provider: ${provider}${flavor ? ` (${flavor})` : ""}`);
|
||||
console.log(`- model: ${model}`);
|
||||
console.log("");
|
||||
}
|
||||
|
|
@ -1,14 +1,16 @@
|
|||
import z from "zod";
|
||||
|
||||
export const Flavor = z.enum([
|
||||
"anthropic",
|
||||
"google",
|
||||
"ollama",
|
||||
"openai",
|
||||
"openai-compatible",
|
||||
"openrouter",
|
||||
]);
|
||||
|
||||
export const Provider = z.object({
|
||||
flavor: z.enum([
|
||||
"anthropic",
|
||||
"google",
|
||||
"ollama",
|
||||
"openai",
|
||||
"openai-compatible",
|
||||
"openrouter",
|
||||
]),
|
||||
flavor: Flavor,
|
||||
apiKey: z.string().optional(),
|
||||
baseURL: z.string().optional(),
|
||||
headers: z.record(z.string(), z.string()).optional(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue