Merge pull request #308 from rowboatlabs/dev

Dev
This commit is contained in:
Ramnique Singh 2025-11-20 18:02:11 +05:30 committed by GitHub
commit 428faea4f0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 71 additions and 48 deletions

View file

@ -47,7 +47,7 @@ Inspired by Claude Code, RowboatX brings the same shell-native power to backgrou
## Quick start ## Quick start
```bash ```bash
npx @rowboatlabs/rowboatx npx @rowboatlabs/rowboatx@latest
``` ```
## Demo ## Demo

View file

@ -1,6 +1,6 @@
{ {
"name": "@rowboatlabs/rowboatx", "name": "@rowboatlabs/rowboatx",
"version": "0.11.0", "version": "0.13.0",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
"scripts": { "scripts": {

View file

@ -7,7 +7,8 @@ import { WorkDir, getModelConfig, updateModelConfig } from "./application/config
import { RunEvent } from "./application/entities/run-events.js"; import { RunEvent } from "./application/entities/run-events.js";
import { createInterface, Interface } from "node:readline/promises"; import { createInterface, Interface } from "node:readline/promises";
import { ToolCallPart } from "./application/entities/message.js"; 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) { export async function updateState(agent: string, runId: string) {
const state = new AgentState(agent, runId); const state = new AgentState(agent, runId);
@ -216,15 +217,15 @@ export async function modelConfig() {
const rl = createInterface({ input, output }); const rl = createInterface({ input, output });
try { try {
const flavors = [ const defaultApiKeyEnvVars: Record<z.infer<typeof Flavor>, string> = {
"openai", openai: "OPENAI_API_KEY",
"anthropic", anthropic: "ANTHROPIC_API_KEY",
"google", google: "GOOGLE_GENERATIVE_AI_API_KEY",
"ollama", ollama: "",
"openai-compatible", "openai-compatible": "",
"openrouter", openrouter: "",
] as const; };
const defaultBaseUrls: Record<(typeof flavors)[number], string> = { const defaultBaseUrls: Record<z.infer<typeof Flavor>, string> = {
openai: "https://api.openai.com/v1", openai: "https://api.openai.com/v1",
anthropic: "https://api.anthropic.com/v1", anthropic: "https://api.anthropic.com/v1",
google: "https://generativelanguage.googleapis.com/v1beta", google: "https://generativelanguage.googleapis.com/v1beta",
@ -232,12 +233,12 @@ export async function modelConfig() {
"openai-compatible": "http://localhost:8080/v1", "openai-compatible": "http://localhost:8080/v1",
openrouter: "https://openrouter.ai/api/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", openai: "gpt-5.1",
anthropic: "claude-3.5-sonnet", anthropic: "claude-sonnet-4-5",
google: "gemini-1.5-pro", google: "gemini-2.5-pro",
ollama: "llama3.1", ollama: "llama3.1",
"openai-compatible": "gpt-4o", "openai-compatible": "openai/gpt-5.1",
openrouter: "openrouter/auto", openrouter: "openrouter/auto",
}; };
@ -245,31 +246,25 @@ export async function modelConfig() {
const currentModel = config?.defaults?.model; const currentModel = config?.defaults?.model;
const currentProviderConfig = currentProvider ? config?.providers?.[currentProvider] : undefined; const currentProviderConfig = currentProvider ? config?.providers?.[currentProvider] : undefined;
if (config) { if (config) {
console.log("Currently using:"); renderCurrentModel(currentProvider || "none", currentProviderConfig?.flavor || "", currentModel || "none");
console.log(`- provider: ${currentProvider || "none"}${currentProviderConfig?.flavor ? ` (${currentProviderConfig.flavor})` : ""}`);
console.log(`- model: ${currentModel || "none"}`);
console.log("");
} }
const flavorPromptLines = flavors const FlavorList = [...Flavor.options];
const flavorPromptLines = FlavorList
.map((f, idx) => ` ${idx + 1}. ${f}`) .map((f, idx) => ` ${idx + 1}. ${f}`)
.join("\n"); .join("\n");
const flavorAnswer = await rl.question( const flavorAnswer = await rl.question(
`Select a provider type:\n${flavorPromptLines}\nEnter number or name` + `Select a provider type:\n${flavorPromptLines}\nEnter number or name: `
(currentProvider ? ` [${currentProvider}]` : "") +
": ",
); );
let selectedFlavorRaw = flavorAnswer.trim(); let selectedFlavorRaw = flavorAnswer.trim();
let selectedFlavor: (typeof flavors)[number] | null = null; let selectedFlavor: z.infer<typeof Flavor> | null = null;
if (selectedFlavorRaw === "" && currentProvider && (flavors as readonly string[]).includes(currentProvider)) { if (/^\d+$/.test(selectedFlavorRaw)) {
selectedFlavor = currentProvider as (typeof flavors)[number];
} else if (/^\d+$/.test(selectedFlavorRaw)) {
const idx = parseInt(selectedFlavorRaw, 10) - 1; const idx = parseInt(selectedFlavorRaw, 10) - 1;
if (idx >= 0 && idx < flavors.length) { if (idx >= 0 && idx < FlavorList.length) {
selectedFlavor = flavors[idx]; selectedFlavor = FlavorList[idx];
} }
} else if ((flavors as readonly string[]).includes(selectedFlavorRaw)) { } else if (FlavorList.includes(selectedFlavorRaw as z.infer<typeof Flavor>)) {
selectedFlavor = selectedFlavorRaw as (typeof flavors)[number]; selectedFlavor = selectedFlavorRaw as z.infer<typeof Flavor>;
} }
if (!selectedFlavor) { if (!selectedFlavor) {
console.error("Invalid selection. Exiting."); console.error("Invalid selection. Exiting.");
@ -338,21 +333,38 @@ export async function modelConfig() {
return; return;
} }
const headers: Record<string, string> = {};
const providerNameAns = await rl.question( const providerNameAns = await rl.question(
`Enter a name/alias for this provider [${selectedFlavor}]: `, `Enter a name/alias for this provider [${selectedFlavor}]: `,
); );
providerName = providerNameAns.trim() || selectedFlavor; providerName = providerNameAns.trim() || selectedFlavor;
const baseUrlDefault = defaultBaseUrls[selectedFlavor] || "";
const baseUrlAns = await rl.question( 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( let apiKey: string | undefined = undefined;
`Enter API key for ${selectedFlavor} (leave blank to skip): `, if (selectedFlavor !== "ollama") {
); let autopickText = "";
const apiKey = apiKeyAns.trim() || undefined; 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 modelDefault = defaultModels[selectedFlavor];
const modelAns = await rl.question( const modelAns = await rl.question(
@ -366,6 +378,7 @@ export async function modelConfig() {
flavor: selectedFlavor, flavor: selectedFlavor,
...(apiKey ? { apiKey } : {}), ...(apiKey ? { apiKey } : {}),
...(baseURL ? { baseURL } : {}), ...(baseURL ? { baseURL } : {}),
...(headers ? { headers } : {}),
}, },
}; };
const newConfig = { const newConfig = {
@ -377,8 +390,16 @@ export async function modelConfig() {
}; };
await updateModelConfig(newConfig as any); 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 { } finally {
rl.close(); 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("");
} }

View file

@ -1,14 +1,16 @@
import z from "zod"; import z from "zod";
export const Flavor = z.enum([
"anthropic",
"google",
"ollama",
"openai",
"openai-compatible",
"openrouter",
]);
export const Provider = z.object({ export const Provider = z.object({
flavor: z.enum([ flavor: Flavor,
"anthropic",
"google",
"ollama",
"openai",
"openai-compatible",
"openrouter",
]),
apiKey: z.string().optional(), apiKey: z.string().optional(),
baseURL: z.string().optional(), baseURL: z.string().optional(),
headers: z.record(z.string(), z.string()).optional(), headers: z.record(z.string(), z.string()).optional(),