config.ts 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. /**
  2. * Reasoning-offload configuration: the persistent, machine-level settings the
  3. * `codegraph offload` CLI writes, merged with `CODEGRAPH_OFFLOAD_*` env overrides.
  4. *
  5. * Stored in `~/.codegraph/config.json` under the `offload` key — the same global
  6. * home CodeGraph already uses for the daemon registry — because the reasoning
  7. * endpoint is a per-machine choice (the model you bring), not per-project state.
  8. * Every codegraph MCP server on the machine picks it up, so a user configures it
  9. * once. Env vars override the file (CI / ephemeral / advanced use).
  10. *
  11. * For a BYO endpoint, the API key is NEVER written to disk: the CLI stores the
  12. * NAME of an env var (`keyEnv`) and reads the key from it at call time. The
  13. * MANAGED tier ("CodeGraph AI") instead authenticates with a revocable, org-scoped
  14. * token from `codegraph offload login`, stored separately in `credentials.json`
  15. * (see ./credentials) — so `config.json` itself never carries a secret either way.
  16. */
  17. import * as fs from 'fs';
  18. import * as path from 'path';
  19. import * as os from 'os';
  20. import { readOffloadToken } from './credentials';
  21. /** Managed tier ("CodeGraph AI") — the metered gateway used when logged in. */
  22. export const MANAGED_DEFAULT_URL = 'https://ai.getcodegraph.com/v1';
  23. /** The gateway's public model id (it translates this to the upstream provider id). */
  24. export const MANAGED_DEFAULT_MODEL = 'openai/gpt-oss-120b';
  25. export interface OffloadConfig {
  26. /** Managed tier: route through CodeGraph AI (metered) with the logged-in org token. */
  27. managed?: boolean;
  28. /** OpenAI-compatible base URL ending in `/v1` (e.g. https://api.cerebras.ai/v1). */
  29. url?: string;
  30. /** Model id to request (default `gpt-oss-120b` BYO, `openai/gpt-oss-120b` managed). */
  31. model?: string;
  32. /** Name of the env var holding the provider API key (never persisted). BYO only. */
  33. keyEnv?: string;
  34. /** reasoning_effort: low | medium | high (default `low`). */
  35. effort?: string;
  36. /** Output style: plain | report (default `plain`). */
  37. style?: string;
  38. }
  39. export interface ResolvedOffload {
  40. /** True when the offload is usable (endpoint present; for managed, a token too). */
  41. enabled: boolean;
  42. /** Managed tier (CodeGraph AI, metered) vs BYO endpoint. */
  43. managed: boolean;
  44. url?: string;
  45. model: string;
  46. /** Resolved API key / org token (from env, the configured `keyEnv`, or login), if any. */
  47. apiKey?: string;
  48. /** Where the key/token came from (for `status` display) — never the secret itself. */
  49. keySource?: string;
  50. effort: string;
  51. style: string;
  52. timeoutMs: number;
  53. maxTokens: number;
  54. strip: boolean;
  55. debug: boolean;
  56. /** Where the endpoint came from — drives `codegraph offload status`. */
  57. origin: 'env' | 'config' | 'none';
  58. }
  59. function configDir(): string {
  60. return path.join(os.homedir(), '.codegraph');
  61. }
  62. function configPath(): string {
  63. return path.join(configDir(), 'config.json');
  64. }
  65. function readUserConfig(): Record<string, unknown> {
  66. try {
  67. return JSON.parse(fs.readFileSync(configPath(), 'utf8')) as Record<string, unknown>;
  68. } catch {
  69. return {};
  70. }
  71. }
  72. function writeUserConfig(cfg: Record<string, unknown>): void {
  73. fs.mkdirSync(configDir(), { recursive: true });
  74. fs.writeFileSync(configPath(), JSON.stringify(cfg, null, 2) + '\n');
  75. }
  76. /** The persisted offload block (empty object if none). */
  77. export function readOffloadConfig(): OffloadConfig {
  78. const cfg = readUserConfig();
  79. const o = cfg.offload;
  80. return o && typeof o === 'object' ? (o as OffloadConfig) : {};
  81. }
  82. /** Persist (or, with `null`, clear) the offload block, leaving other config keys intact. */
  83. export function writeOffloadConfig(offload: OffloadConfig | null): void {
  84. const cfg = readUserConfig();
  85. if (offload === null) delete cfg.offload;
  86. else cfg.offload = offload;
  87. writeUserConfig(cfg);
  88. }
  89. const trimmed = (v: string | undefined): string | undefined => {
  90. const t = v?.trim();
  91. return t ? t : undefined;
  92. };
  93. /** Merge the persisted config with `CODEGRAPH_OFFLOAD_*` env overrides (env wins). */
  94. export function resolveOffload(env: NodeJS.ProcessEnv = process.env): ResolvedOffload {
  95. const c = readOffloadConfig();
  96. const managed = !!c.managed;
  97. const envUrl = trimmed(env.CODEGRAPH_OFFLOAD_URL);
  98. const envKey = trimmed(env.CODEGRAPH_OFFLOAD_KEY);
  99. let url: string | undefined;
  100. let apiKey: string | undefined;
  101. let keySource: string | undefined;
  102. let model: string;
  103. if (managed) {
  104. // Managed tier: default to the CodeGraph AI gateway + its public model id; the
  105. // bearer is the org token from `codegraph offload login` (or an env override).
  106. url = envUrl ?? trimmed(c.url) ?? MANAGED_DEFAULT_URL;
  107. model = trimmed(env.CODEGRAPH_OFFLOAD_MODEL) ?? trimmed(c.model) ?? MANAGED_DEFAULT_MODEL;
  108. if (envKey) { apiKey = envKey; keySource = 'CODEGRAPH_OFFLOAD_KEY'; }
  109. else { const t = readOffloadToken(); if (t) { apiKey = t; keySource = 'codegraph login'; } }
  110. } else {
  111. // BYO: endpoint + (optional) provider key resolved from env or the named env var.
  112. url = envUrl ?? trimmed(c.url);
  113. model = trimmed(env.CODEGRAPH_OFFLOAD_MODEL) ?? trimmed(c.model) ?? 'gpt-oss-120b';
  114. if (envKey) { apiKey = envKey; keySource = 'CODEGRAPH_OFFLOAD_KEY'; }
  115. else if (c.keyEnv && trimmed(env[c.keyEnv])) { apiKey = trimmed(env[c.keyEnv]); keySource = c.keyEnv; }
  116. }
  117. const origin: ResolvedOffload['origin'] = envUrl ? 'env' : (managed || trimmed(c.url)) ? 'config' : 'none';
  118. return {
  119. // Managed needs both an endpoint AND a token (no token → effectively logged out);
  120. // BYO needs only an endpoint (some endpoints require no auth).
  121. enabled: managed ? (!!url && !!apiKey) : !!url,
  122. managed,
  123. url,
  124. model,
  125. apiKey,
  126. keySource,
  127. effort: trimmed(env.CODEGRAPH_OFFLOAD_EFFORT) ?? trimmed(c.effort) ?? 'low',
  128. style: trimmed(env.CODEGRAPH_OFFLOAD_STYLE) ?? trimmed(c.style) ?? 'plain',
  129. timeoutMs: Number(env.CODEGRAPH_OFFLOAD_TIMEOUT_MS) || 20000,
  130. maxTokens: Number(env.CODEGRAPH_OFFLOAD_MAXTOKENS) || 12000,
  131. strip: env.CODEGRAPH_OFFLOAD_STRIP === '1',
  132. debug: env.CODEGRAPH_OFFLOAD_DEBUG === '1',
  133. origin,
  134. };
  135. }