utils.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. import { readFile } from "node:fs/promises";
  2. import { resolve } from "node:path";
  3. import { createLLMClient, StateManager, createLogger, createStderrSink, createJsonLineSink, loadProjectConfig, GLOBAL_CONFIG_DIR, GLOBAL_ENV_PATH, type ProjectConfig, type PipelineConfig, type LogSink } from "@actalk/inkos-core";
  4. import { formatSqliteMemorySupportWarning } from "./runtime-requirements.js";
  5. export { GLOBAL_CONFIG_DIR, GLOBAL_ENV_PATH };
  6. let sqliteMemorySupportWarned = false;
  7. export async function resolveContext(opts: {
  8. readonly context?: string;
  9. readonly contextFile?: string;
  10. }): Promise<string | undefined> {
  11. if (opts.context) return opts.context;
  12. if (opts.contextFile) {
  13. return readFile(resolve(opts.contextFile), "utf-8");
  14. }
  15. // Read from stdin if piped (non-TTY)
  16. if (!process.stdin.isTTY) {
  17. const chunks: Buffer[] = [];
  18. for await (const chunk of process.stdin) {
  19. chunks.push(chunk as Buffer);
  20. }
  21. const text = Buffer.concat(chunks).toString("utf-8").trim();
  22. if (text.length > 0) return text;
  23. }
  24. return undefined;
  25. }
  26. export function findProjectRoot(): string {
  27. return process.cwd();
  28. }
  29. export async function loadConfig(options?: { readonly requireApiKey?: boolean }): Promise<ProjectConfig> {
  30. return loadProjectConfig(findProjectRoot(), options);
  31. }
  32. export function createClient(config: ProjectConfig) {
  33. return createLLMClient(config.llm);
  34. }
  35. export function buildPipelineConfig(
  36. config: ProjectConfig,
  37. root: string,
  38. extra?: Partial<Pick<PipelineConfig, "notifyChannels" | "radarSources" | "externalContext" | "inputGovernanceMode">> & {
  39. readonly quiet?: boolean;
  40. readonly logFile?: NodeJS.WritableStream;
  41. },
  42. ): PipelineConfig {
  43. if (!extra?.quiet && !sqliteMemorySupportWarned) {
  44. const warning = formatSqliteMemorySupportWarning();
  45. if (warning) {
  46. sqliteMemorySupportWarned = true;
  47. process.stderr.write(`[WARN] ${warning}\n`);
  48. }
  49. }
  50. const sinks: LogSink[] = [];
  51. if (!extra?.quiet) {
  52. sinks.push(createStderrSink({ minLevel: "info" }));
  53. }
  54. if (extra?.logFile) {
  55. sinks.push(createJsonLineSink(extra.logFile));
  56. }
  57. const hasLogging = sinks.length > 0;
  58. const logger = hasLogging ? createLogger({ tag: "inkos", sinks }) : undefined;
  59. const onStreamProgress = hasLogging
  60. ? (progress: { readonly elapsedMs: number; readonly totalChars: number; readonly chineseChars: number; readonly status: string }) => {
  61. if (progress.status === "streaming") {
  62. logger?.info(
  63. `streaming ${Math.round(progress.elapsedMs / 1000)}s, ${progress.totalChars} chars (${progress.chineseChars} CJK)`,
  64. );
  65. }
  66. }
  67. : undefined;
  68. return {
  69. client: createLLMClient(config.llm),
  70. model: config.llm.model,
  71. projectRoot: root,
  72. defaultLLMConfig: config.llm,
  73. modelOverrides: config.modelOverrides,
  74. inputGovernanceMode: extra?.inputGovernanceMode ?? config.inputGovernanceMode,
  75. notifyChannels: extra?.notifyChannels ?? config.notify,
  76. radarSources: extra?.radarSources,
  77. externalContext: extra?.externalContext,
  78. logger,
  79. onStreamProgress,
  80. };
  81. }
  82. export function log(message: string): void {
  83. process.stdout.write(`${message}\n`);
  84. }
  85. export function logError(message: string): void {
  86. process.stderr.write(`[ERROR] ${message}\n`);
  87. }
  88. /**
  89. * Resolve book-id: if provided use it, otherwise auto-detect when exactly one book exists.
  90. * Validates that the book actually exists.
  91. */
  92. export async function resolveBookId(
  93. bookIdArg: string | undefined,
  94. root: string,
  95. ): Promise<string> {
  96. const state = new StateManager(root);
  97. const books = await state.listBooks();
  98. if (bookIdArg) {
  99. if (!books.includes(bookIdArg)) {
  100. const available = books.length > 0 ? books.join(", ") : "(none)";
  101. throw new Error(
  102. `Book "${bookIdArg}" not found. Available books: ${available}`,
  103. );
  104. }
  105. return bookIdArg;
  106. }
  107. if (books.length === 0) {
  108. throw new Error(
  109. "No books found. Create one first:\n inkos book create --title '...' --genre xuanhuan",
  110. );
  111. }
  112. if (books.length === 1) {
  113. return books[0]!;
  114. }
  115. throw new Error(
  116. `Multiple books found: ${books.join(", ")}\nPlease specify a book-id.`,
  117. );
  118. }