| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147 |
- import { readFile, access } from "node:fs/promises";
- import { homedir } from "node:os";
- import { join, resolve } from "node:path";
- import { config as loadEnv } from "dotenv";
- import { createLLMClient, StateManager, type ProjectConfig, ProjectConfigSchema, type PipelineConfig } from "@actalk/inkos-core";
- export const GLOBAL_CONFIG_DIR = join(homedir(), ".inkos");
- export const GLOBAL_ENV_PATH = join(GLOBAL_CONFIG_DIR, ".env");
- export async function resolveContext(opts: {
- readonly context?: string;
- readonly contextFile?: string;
- }): Promise<string | undefined> {
- if (opts.context) return opts.context;
- if (opts.contextFile) {
- return readFile(resolve(opts.contextFile), "utf-8");
- }
- // Read from stdin if piped (non-TTY)
- if (!process.stdin.isTTY) {
- const chunks: Buffer[] = [];
- for await (const chunk of process.stdin) {
- chunks.push(chunk as Buffer);
- }
- const text = Buffer.concat(chunks).toString("utf-8").trim();
- if (text.length > 0) return text;
- }
- return undefined;
- }
- export function findProjectRoot(): string {
- return process.cwd();
- }
- export async function loadConfig(): Promise<ProjectConfig> {
- const root = findProjectRoot();
- // Load global ~/.inkos/.env first, then project .env overrides
- loadEnv({ path: GLOBAL_ENV_PATH });
- loadEnv({ path: join(root, ".env"), override: true });
- const configPath = join(root, "inkos.json");
- // Step 1: Check file exists — give a clear message if not
- try {
- await access(configPath);
- } catch {
- throw new Error(
- `inkos.json not found in ${root}.\nMake sure you are inside an InkOS project directory (cd into the project created by 'inkos init').`,
- );
- }
- // Step 2: Read and parse — surface real errors instead of swallowing them
- const raw = await readFile(configPath, "utf-8");
- let config: Record<string, unknown>;
- try {
- config = JSON.parse(raw);
- } catch {
- throw new Error(`inkos.json in ${root} is not valid JSON. Check the file for syntax errors.`);
- }
- // .env overrides inkos.json for LLM settings
- const env = process.env;
- const llm = (config.llm ?? {}) as Record<string, unknown>;
- if (env.INKOS_LLM_PROVIDER) llm.provider = env.INKOS_LLM_PROVIDER;
- if (env.INKOS_LLM_BASE_URL) llm.baseUrl = env.INKOS_LLM_BASE_URL;
- if (env.INKOS_LLM_MODEL) llm.model = env.INKOS_LLM_MODEL;
- if (env.INKOS_LLM_TEMPERATURE) llm.temperature = parseFloat(env.INKOS_LLM_TEMPERATURE);
- if (env.INKOS_LLM_MAX_TOKENS) llm.maxTokens = parseInt(env.INKOS_LLM_MAX_TOKENS, 10);
- if (env.INKOS_LLM_THINKING_BUDGET) llm.thinkingBudget = parseInt(env.INKOS_LLM_THINKING_BUDGET, 10);
- if (env.INKOS_LLM_API_FORMAT) llm.apiFormat = env.INKOS_LLM_API_FORMAT;
- config.llm = llm;
- // API key ONLY from env — never stored in inkos.json
- const apiKey = env.INKOS_LLM_API_KEY;
- if (!apiKey) {
- throw new Error(
- "INKOS_LLM_API_KEY not set. Run 'inkos config set-global' or add it to project .env file.",
- );
- }
- llm.apiKey = apiKey;
- return ProjectConfigSchema.parse(config);
- }
- export function createClient(config: ProjectConfig) {
- return createLLMClient(config.llm);
- }
- export function buildPipelineConfig(
- config: ProjectConfig,
- root: string,
- extra?: Partial<Pick<PipelineConfig, "notifyChannels" | "radarSources" | "externalContext">>,
- ): PipelineConfig {
- return {
- client: createLLMClient(config.llm),
- model: config.llm.model,
- projectRoot: root,
- defaultLLMConfig: config.llm,
- modelOverrides: config.modelOverrides,
- notifyChannels: extra?.notifyChannels ?? config.notify,
- radarSources: extra?.radarSources,
- externalContext: extra?.externalContext,
- };
- }
- export function log(message: string): void {
- process.stdout.write(`${message}\n`);
- }
- export function logError(message: string): void {
- process.stderr.write(`[ERROR] ${message}\n`);
- }
- /**
- * Resolve book-id: if provided use it, otherwise auto-detect when exactly one book exists.
- * Validates that the book actually exists.
- */
- export async function resolveBookId(
- bookIdArg: string | undefined,
- root: string,
- ): Promise<string> {
- const state = new StateManager(root);
- const books = await state.listBooks();
- if (bookIdArg) {
- if (!books.includes(bookIdArg)) {
- const available = books.length > 0 ? books.join(", ") : "(none)";
- throw new Error(
- `Book "${bookIdArg}" not found. Available books: ${available}`,
- );
- }
- return bookIdArg;
- }
- if (books.length === 0) {
- throw new Error(
- "No books found. Create one first:\n inkos book create --title '...' --genre xuanhuan",
- );
- }
- if (books.length === 1) {
- return books[0]!;
- }
- throw new Error(
- `Multiple books found: ${books.join(", ")}\nPlease specify a book-id.`,
- );
- }
|