This repository has been archived on 2026-03-28. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files

165 lines
5.4 KiB
TypeScript

#!/usr/bin/env node
import React from "react";
import path from "path";
import os from "os";
import { render } from "ink";
import { Command } from "@commander-js/extra-typings";
import { fileExists } from "./fs-utils.ts";
import App from "./app.tsx";
import { readConfig, readMetadata } from "./config.ts";
import { tokenCounts } from "./token-tracker.ts";
import { getMcpClient, connectMcpServer } from "./tools/tool-defs/mcp.ts";
import OpenAI from "openai";
import { LlmMessage } from "./llm.ts";
import { FirstTimeSetup } from "./first-time-setup.tsx";
const CONFIG_STANDARD_DIR = path.join(os.homedir(), ".config/octofriend/");
const CONFIG_JSON5_FILE = path.join(CONFIG_STANDARD_DIR, "octofriend.json5")
const cli = new Command()
.description("If run with no subcommands, runs Octo interactively.")
.option("--config <path>")
.option("--unchained", "Skips confirmation for all tools, running them immediately. Dangerous.")
.action(async (opts) => {
const metadata = await readMetadata();
const { config, configPath } = await loadConfig(opts.config);
for(const model of config.models) {
if(!process.env[model.apiEnvVar]) {
console.error(`
Octo is set to use the ${model.apiEnvVar} env var to authenticate with the ${model.nickname} API,
but that env var isn't set in your current shell.
Hint: do you need to re-source your .bash_profile or .zshrc?
`.trim());
process.exit(1);
}
}
// Connect to all MCP servers on boot
if(config.mcpServers && Object.keys(config.mcpServers).length > 0) {
for(const server of Object.keys(config.mcpServers)) {
console.log("Connecting to", server, "MCP server...");
// Run the basic connection setup with logging enabled, so that first-time setup gets logged
const client = await connectMcpServer(server, config, true);
await client.close();
// Then run the cache setup codepath, so future results use a cached client with logging off
await getMcpClient(server, config);
}
console.log("All MCP servers connected.");
}
const { waitUntilExit } = render(
<App config={config} configPath={configPath} metadata={metadata} unchained={!!opts.unchained} />
);
await waitUntilExit();
console.log("\nApprox. tokens used:");
if(Object.keys(tokenCounts()).length === 0) {
console.log("0");
}
else {
for(const [ model, count ] of Object.entries(tokenCounts())) {
const input = count.input.toLocaleString();
const output = count.output.toLocaleString();
console.log(`${model}: ${input} input, ${output} output`);
}
}
});
cli.command("version")
.description("Prints the current version")
.action(async () => {
const metadata = await readMetadata();
console.log(metadata.version);
});
cli.command("init")
.description("Create a fresh config file for Octo")
.action(() => {
render(
<FirstTimeSetup configPath={CONFIG_JSON5_FILE} />
);
});
cli.command("list")
.description("List all models you've configured with Octo")
.action(async () => {
const { config } = await loadConfig();
console.log(config.models.map(m => m.nickname).join("\n"));
});
cli.command("prompt")
.description("Sends a prompt to a model")
.option("--system <prompt>", "An optional system prompt")
.option("--model <model-nickname>", "The nickname you gave for the model you want to use. If unspecified, uses your default model")
.argument("<prompt>", "The prompt you want to send to this model")
.action(async (prompt, opts) => {
const { config } = await loadConfig();
const model = opts.model ? config.models.find(m => m.nickname === opts.model) : config.models[0];
if(model == null) {
console.error(`No model with the nickname ${opts.model} found. Did you add it to Octo?`);
console.error("The available models are:");
console.error("- " + config.models.map(m => m.nickname).join("\n- "));
process.exit(1);
}
if(!process.env[model.apiEnvVar]) {
console.error(`${model.nickname} is set to use the ${model.apiEnvVar} env var, but that env var doesn't exist in your current shell. Do you need to re-source your .bash_profile or .zshrc?`);
process.exit(1);
}
const client = new OpenAI({
apiKey: process.env[model.apiEnvVar],
baseURL: model.baseUrl,
});
const messages: LlmMessage[] = [];
if(opts.system) {
messages.push({
role: "system",
content: opts.system,
});
}
messages.push({
role: "user",
content: prompt,
});
const response = await client.chat.completions.create({
model: model.model,
messages,
stream: true,
});
for await(const chunk of response) {
const content = chunk.choices[0].delta?.content;
if(content) process.stdout.write(content);
}
process.stdout.write("\n");
});
async function loadConfig(configPath?: string) {
if(configPath) return { configPath, config: await readConfig(configPath) };
if(await fileExists(CONFIG_JSON5_FILE)) {
return { configPath: CONFIG_JSON5_FILE, config: await readConfig(CONFIG_JSON5_FILE) };
}
const jsonFile = path.join(CONFIG_STANDARD_DIR, "octofriend.json")
if(await fileExists(jsonFile)) {
return { configPath: jsonFile, config: await readConfig(jsonFile) };
}
const { waitUntilExit } = render(
<FirstTimeSetup configPath={CONFIG_JSON5_FILE} />
);
await waitUntilExit();
if(await fileExists(CONFIG_JSON5_FILE)) {
return { configPath: CONFIG_JSON5_FILE, config: await readConfig(CONFIG_JSON5_FILE) };
}
process.exit(1);
}
cli.parse();