diff --git a/.gitignore b/.gitignore index 345bc6a..4fdc3a3 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ temp/ # Standalone prototype scripts index.ts +.worktrees/ diff --git a/src/cli.ts b/src/cli.ts index f1c7472..0205c75 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -821,6 +821,7 @@ clientsCmd .option("--pi-agent", "Set up Pi Agent integration") .option("--claude-code", "Set up Claude Code integration") .option("--hermes", "Set up Hermes integration") + .option("--goose", "Set up Goose integration") .action( async (options: { name?: string; @@ -829,6 +830,7 @@ clientsCmd piAgent?: boolean; claudeCode?: boolean; hermes?: boolean; + goose?: boolean; }) => { await addClientAction(options); }, diff --git a/src/integrations/goose.ts b/src/integrations/goose.ts new file mode 100644 index 0000000..e0935b5 --- /dev/null +++ b/src/integrations/goose.ts @@ -0,0 +1,91 @@ +import { existsSync, mkdirSync } from "fs"; +import { readFile, writeFile } from "fs/promises"; +import { dirname } from "path"; +import type { RoutstrdConfig } from "../utils/config"; +import { logger } from "../utils/logger"; +import type { IntegrationConfig, RoutstrModel } from "./registry"; +import { callDaemon, getDaemonBaseUrl } from "../utils/daemon-client"; + +const GOOSE_DEFAULT_MODEL = "glm-5.1"; + +// Keys managed by this integration +const MANAGED_KEYS = [ + "GOOSE_TELEMETRY_ENABLED", + "OPENAI_BASE_URL", + "OPENAI_BASE_PATH", + "OPENAI_TIMEOUT", + "GOOSE_PROVIDER", + "GOOSE_MODEL", +]; + +export async function installGooseIntegration( + config: RoutstrdConfig, + apiKey: string, + integrationConfig: IntegrationConfig, +): Promise { + const { name, configPath } = integrationConfig; + + logger.log(`\nInstalling routstr configuration in ${configPath}...`); + logger.log(`Using API key for ${name}`); + + const baseUrl = getDaemonBaseUrl(config); + + let gooseModel = GOOSE_DEFAULT_MODEL; + + try { + const data = await callDaemon("/models"); + const models = (data.output as { models: RoutstrModel[] } | undefined)?.models || []; + + if (models.length >= 2) { + gooseModel = models[1]!.id; + logger.log(`Set Goose model to 2nd available model: ${gooseModel}`); + } else if (models.length === 1) { + gooseModel = models[0]!.id; + logger.log(`Only 1 model available, using ${gooseModel} as Goose model.`); + } else { + logger.log("No models available from routstr daemon, using fallback default model."); + } + } catch (error) { + logger.error("Failed to fetch models for Goose integration:", error); + logger.log("Using fallback default model."); + } + + let content = ""; + try { + if (existsSync(configPath)) { + content = await readFile(configPath, "utf-8"); + } + } catch (error) { + logger.error(`Error reading ${configPath}, creating new one.`); + } + + // Remove existing managed key lines so we can rewrite them + for (const key of MANAGED_KEYS) { + content = content.replace(new RegExp(`^${key}:.*\\n?`, "gm"), ""); + } + + // Remove OPENAI_HOST if it was left from a previous manual config + content = content.replace(/^OPENAI_HOST:.*\n?/gm, ""); + + // Clean up trailing blank lines + content = content.replace(/\n{3,}/g, "\n\n").trimEnd(); + + const envBlock = [ + "GOOSE_TELEMETRY_ENABLED: false", + `OPENAI_BASE_URL: ${baseUrl}`, + "OPENAI_BASE_PATH: v1/chat/completions", + "OPENAI_TIMEOUT: '600'", + "GOOSE_PROVIDER: openai", + `GOOSE_MODEL: ${gooseModel}`, + ].join("\n"); + + const newContent = (content ? content + "\n\n" : "") + envBlock + "\n"; + + try { + mkdirSync(dirname(configPath), { recursive: true }); + await writeFile(configPath, newContent); + logger.log(`Successfully updated ${configPath} with routstr settings.`); + } catch (error) { + logger.error(`Failed to write to ${configPath}:`, error); + } +} diff --git a/src/integrations/index.ts b/src/integrations/index.ts index 54ccf9c..47ae70e 100644 --- a/src/integrations/index.ts +++ b/src/integrations/index.ts @@ -10,6 +10,7 @@ import { installOpenClawIntegration } from "./openclaw"; import { installPiIntegration } from "./pi"; import { installClaudeCodeIntegration } from "./claudecode"; import { installHermesIntegration } from "./hermes"; +import { installGooseIntegration } from "./goose"; import type { IntegrationConfig } from "./registry"; import { CLIENT_CONFIGS, runIntegrationsForClients } from "./registry"; export { CLIENT_INTEGRATIONS, CLIENT_CONFIGS, runIntegrationsForClients } from "./registry"; @@ -57,7 +58,7 @@ function parseChoice(input: string): number { } const parsed = Number.parseInt(input, 10); - if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 6) { + if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 7) { return parsed; } @@ -73,7 +74,8 @@ export async function setupIntegration( logger.log("3. Pi"); logger.log("4. Claude Code"); logger.log("5. Hermes"); - logger.log("6. Skip for now"); + logger.log("6. Goose"); + logger.log("7. Skip for now"); const answer = await ask("Select integration [1]: "); const choice = parseChoice(answer); @@ -84,6 +86,7 @@ export async function setupIntegration( 3: "pi-agent", 4: "claude-code", 5: "hermes", + 6: "goose", }; const key = integrationByChoice[choice]; @@ -127,4 +130,9 @@ export async function setupIntegration( await installHermesIntegration(config, client.apiKey, integrationConfig); return; } + + if (key === "goose") { + await installGooseIntegration(config, client.apiKey, integrationConfig); + return; + } } diff --git a/src/integrations/registry.ts b/src/integrations/registry.ts index c69375f..25b107f 100644 --- a/src/integrations/registry.ts +++ b/src/integrations/registry.ts @@ -5,6 +5,7 @@ import { installPiIntegration } from "./pi"; import { installOpenClawIntegration } from "./openclaw"; import { installClaudeCodeIntegration } from "./claudecode"; import { installHermesIntegration } from "./hermes"; +import { installGooseIntegration } from "./goose"; export interface IntegrationConfig { clientId: string; @@ -49,6 +50,11 @@ export const CLIENT_CONFIGS: Record = { name: "Hermes", configPath: join(process.env.HOME || "", ".hermes/config.yaml"), }, + goose: { + clientId: "goose", + name: "Goose", + configPath: join(process.env.HOME || "", ".config/goose/config.yaml"), + }, }; export const CLIENT_INTEGRATIONS: Record = { @@ -57,6 +63,7 @@ export const CLIENT_INTEGRATIONS: Record = { openclaw: installOpenClawIntegration, "claude-code": installClaudeCodeIntegration, hermes: installHermesIntegration, + goose: installGooseIntegration, }; export async function runIntegrationsForClients( diff --git a/src/utils/clients.ts b/src/utils/clients.ts index 4a8d78a..4bc8f6a 100644 --- a/src/utils/clients.ts +++ b/src/utils/clients.ts @@ -188,6 +188,7 @@ export interface AddClientOptions { piAgent?: boolean; claudeCode?: boolean; hermes?: boolean; + goose?: boolean; } export async function addClientAction(options: AddClientOptions): Promise { @@ -200,6 +201,7 @@ export async function addClientAction(options: AddClientOptions): Promise if (options.piAgent) integrationKeys.push("pi-agent"); if (options.claudeCode) integrationKeys.push("claude-code"); if (options.hermes) integrationKeys.push("hermes"); + if (options.goose) integrationKeys.push("goose"); if (integrationKeys.length > 0) { for (const key of integrationKeys) { @@ -243,6 +245,7 @@ export async function addClientAction(options: AddClientOptions): Promise console.error(" --pi-agent Set up Pi Agent integration"); console.error(" --claude-code Set up Claude Code integration"); console.error(" --hermes Set up Hermes integration"); + console.error(" --goose Set up Goose integration"); process.exit(1); }