config.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. /**
  2. * Framework configuration
  3. *
  4. * Default configuration for the evaluation framework.
  5. * Can be overridden by passing custom config to components.
  6. */
  7. import { FrameworkConfig } from './types';
  8. import * as path from 'path';
  9. import * as os from 'os';
  10. import * as crypto from 'crypto';
  11. import * as fs from 'fs';
  12. /**
  13. * Find the git root directory by walking up from a given path
  14. *
  15. * OpenCode agents typically run from the git root directory.
  16. * Sessions are stored based on the git root, not subdirectories.
  17. *
  18. * @param startPath - Path to start searching from (defaults to cwd)
  19. * @returns Git root path or the start path if no git root found
  20. */
  21. export const findGitRoot = (startPath: string = process.cwd()): string => {
  22. let currentPath = path.resolve(startPath);
  23. // Walk up the directory tree looking for .git
  24. while (currentPath !== path.dirname(currentPath)) {
  25. const gitPath = path.join(currentPath, '.git');
  26. if (fs.existsSync(gitPath)) {
  27. return currentPath;
  28. }
  29. currentPath = path.dirname(currentPath);
  30. }
  31. // No git root found, return the start path
  32. return startPath;
  33. };
  34. /**
  35. * Get default session storage path
  36. * OpenCode stores sessions in ~/.local/share/opencode/
  37. */
  38. const getDefaultSessionStoragePath = (): string => {
  39. const homeDir = os.homedir();
  40. return path.join(homeDir, '.local', 'share', 'opencode');
  41. };
  42. /**
  43. * Default framework configuration
  44. *
  45. * IMPORTANT: Uses git root as projectPath, not process.cwd()
  46. *
  47. * Why? When testing agents like OpenAgent, the agent runs from the git root,
  48. * but tests run from /evals/framework. Sessions are created in the git root's
  49. * context, so we need to look there for session storage.
  50. *
  51. * Example:
  52. * - Git root: /Users/user/opencode-agents
  53. * - Test CWD: /Users/user/opencode-agents/evals/framework
  54. * - Sessions stored under git root hash, not test framework hash
  55. */
  56. export const defaultConfig: FrameworkConfig = {
  57. projectPath: findGitRoot(process.cwd()), // Use git root, not cwd
  58. sessionStoragePath: getDefaultSessionStoragePath(),
  59. resultsPath: path.join(process.cwd(), 'evals', 'results'),
  60. passThreshold: 75,
  61. };
  62. /**
  63. * Create custom configuration by merging with defaults
  64. */
  65. export const createConfig = (overrides: Partial<FrameworkConfig> = {}): FrameworkConfig => {
  66. return {
  67. ...defaultConfig,
  68. ...overrides,
  69. };
  70. };
  71. /**
  72. * Encode project path for OpenCode storage (legacy format)
  73. * OpenCode replaces slashes with dashes in project paths
  74. * Example: /Users/user/project -> Users-user-project
  75. *
  76. * NOTE: This is the LEGACY format used by older OpenCode versions.
  77. * The SDK now uses a hash-based format instead.
  78. */
  79. export const encodeProjectPath = (projectPath: string): string => {
  80. // Remove leading slash and replace remaining slashes with dashes
  81. return projectPath.replace(/^\//, '').replace(/\//g, '-');
  82. };
  83. /**
  84. * Calculate project hash (SHA-1) used by OpenCode SDK
  85. * The SDK stores sessions using a hash of the project path instead of the encoded path.
  86. * This matches the projectID field in session JSON files.
  87. *
  88. * NOTE: The exact hashing algorithm used by OpenCode is not documented.
  89. * This function attempts to calculate it, but may not match in all cases.
  90. * The SessionReader falls back to scanning all session directories if needed.
  91. *
  92. * Example: /Users/user/project -> 9b95828208165943d702402641ce831a3cda362e
  93. */
  94. export const getProjectHash = (projectPath: string): string => {
  95. // OpenCode uses SHA-1 hash of the absolute project path
  96. // However, the exact implementation may vary (e.g., trailing slashes, normalization)
  97. return crypto.createHash('sha1').update(projectPath).digest('hex');
  98. };
  99. /**
  100. * Get session storage path for a specific project (SDK format)
  101. *
  102. * The OpenCode SDK uses a FLAT structure with project hash:
  103. * ~/.local/share/opencode/storage/session/{projectHash}/
  104. *
  105. * This is different from the legacy nested structure:
  106. * ~/.local/share/opencode/project/{encoded-path}/storage/session/
  107. *
  108. * @param projectPath - Absolute path to the project
  109. * @param sessionStoragePath - Base storage path (defaults to ~/.local/share/opencode)
  110. * @returns Path to session storage directory
  111. */
  112. export const getProjectSessionPath = (
  113. projectPath: string,
  114. sessionStoragePath: string = getDefaultSessionStoragePath()
  115. ): string => {
  116. // Use SDK's hash-based flat structure
  117. const projectHash = getProjectHash(projectPath);
  118. return path.join(sessionStoragePath, 'storage', 'session', projectHash);
  119. };
  120. /**
  121. * Get legacy session storage path for a specific project
  122. *
  123. * This is the OLD format used before the SDK migration.
  124. * We keep this for backward compatibility when reading old sessions.
  125. *
  126. * @param projectPath - Absolute path to the project
  127. * @param sessionStoragePath - Base storage path
  128. * @returns Path to legacy session storage directory
  129. */
  130. export const getLegacyProjectSessionPath = (
  131. projectPath: string,
  132. sessionStoragePath: string = getDefaultSessionStoragePath()
  133. ): string => {
  134. const encodedPath = encodeProjectPath(projectPath);
  135. return path.join(sessionStoragePath, 'project', encodedPath, 'storage', 'session');
  136. };
  137. /**
  138. * Get session info path (SDK format)
  139. *
  140. * SDK stores session info files directly in the project hash directory:
  141. * ~/.local/share/opencode/storage/session/{projectHash}/{sessionId}.json
  142. *
  143. * NOT in a nested info/ subdirectory like the legacy format.
  144. */
  145. export const getSessionInfoPath = (
  146. projectPath: string,
  147. sessionStoragePath?: string
  148. ): string => {
  149. // SDK uses flat structure - session files are directly in the project hash directory
  150. return getProjectSessionPath(projectPath, sessionStoragePath);
  151. };
  152. /**
  153. * Get legacy session info path (for backward compatibility)
  154. *
  155. * Legacy format uses nested structure:
  156. * ~/.local/share/opencode/project/{encoded-path}/storage/session/info/
  157. */
  158. export const getLegacySessionInfoPath = (
  159. projectPath: string,
  160. sessionStoragePath?: string
  161. ): string => {
  162. return path.join(getLegacyProjectSessionPath(projectPath, sessionStoragePath), 'info');
  163. };
  164. /**
  165. * Get session message path (SDK format)
  166. *
  167. * NOTE: The SDK currently stores sessions as single JSON files.
  168. * Message/part subdirectories may not exist for SDK-created sessions.
  169. * This path is kept for compatibility with legacy sessions.
  170. */
  171. export const getSessionMessagePath = (
  172. projectPath: string,
  173. sessionStoragePath?: string
  174. ): string => {
  175. return path.join(getProjectSessionPath(projectPath, sessionStoragePath), 'message');
  176. };
  177. /**
  178. * Get legacy session message path
  179. */
  180. export const getLegacySessionMessagePath = (
  181. projectPath: string,
  182. sessionStoragePath?: string
  183. ): string => {
  184. return path.join(getLegacyProjectSessionPath(projectPath, sessionStoragePath), 'message');
  185. };
  186. /**
  187. * Get session part path (SDK format)
  188. *
  189. * NOTE: The SDK currently stores sessions as single JSON files.
  190. * Message/part subdirectories may not exist for SDK-created sessions.
  191. * This path is kept for compatibility with legacy sessions.
  192. */
  193. export const getSessionPartPath = (
  194. projectPath: string,
  195. sessionStoragePath?: string
  196. ): string => {
  197. return path.join(getProjectSessionPath(projectPath, sessionStoragePath), 'part');
  198. };
  199. /**
  200. * Get legacy session part path
  201. */
  202. export const getLegacySessionPartPath = (
  203. projectPath: string,
  204. sessionStoragePath?: string
  205. ): string => {
  206. return path.join(getLegacyProjectSessionPath(projectPath, sessionStoragePath), 'part');
  207. };
  208. /**
  209. * Resolve agent path to support both old and new formats
  210. *
  211. * Supports:
  212. * - Old format: "openagent" → ".opencode/agent/openagent.md"
  213. * - New format: "core/openagent" → ".opencode/agent/core/openagent.md"
  214. * - Subagents: "subagents/code/tester" → ".opencode/agent/subagents/code/tester.md"
  215. *
  216. * @param agent - Agent identifier (e.g., "openagent" or "core/openagent")
  217. * @param projectPath - Project root path (defaults to git root)
  218. * @returns Absolute path to agent file
  219. */
  220. export const resolveAgentPath = (agent: string, projectPath?: string): string => {
  221. const root = projectPath || findGitRoot(process.cwd());
  222. const agentDir = path.join(root, '.opencode', 'agent');
  223. // If agent contains a slash, it's a category-based path
  224. if (agent.includes('/')) {
  225. return path.join(agentDir, `${agent}.md`);
  226. }
  227. // Old format - flat structure
  228. return path.join(agentDir, `${agent}.md`);
  229. };
  230. /**
  231. * Normalize agent identifier to category-based format
  232. *
  233. * Maps old agent names to new category-based paths:
  234. * - "openagent" → "core/openagent"
  235. * - "opencoder" → "core/opencoder"
  236. * - "system-builder" → "meta/system-builder"
  237. *
  238. * Already category-based paths are returned as-is:
  239. * - "core/openagent" → "core/openagent"
  240. * - "development/frontend-specialist" → "development/frontend-specialist"
  241. *
  242. * @param agent - Agent identifier
  243. * @returns Normalized agent identifier
  244. */
  245. export const normalizeAgentId = (agent: string): string => {
  246. // Already category-based
  247. if (agent.includes('/')) {
  248. return agent;
  249. }
  250. // Map old core agents to new paths
  251. const coreAgents: Record<string, string> = {
  252. 'openagent': 'core/openagent',
  253. 'OpenAgent': 'core/openagent',
  254. 'opencoder': 'core/opencoder',
  255. 'OpenCoder': 'core/opencoder',
  256. 'system-builder': 'meta/system-builder',
  257. 'OpenSystemBuilder': 'meta/system-builder',
  258. 'codebase-agent': 'development/codebase-agent',
  259. 'OpenCodebaseAgent': 'development/codebase-agent',
  260. 'devops-specialist': 'development/devops-specialist',
  261. 'OpenDevopsSpecialist': 'development/devops-specialist',
  262. 'frontend-specialist': 'development/frontend-specialist',
  263. 'OpenFrontendSpecialist': 'development/frontend-specialist',
  264. 'backend-specialist': 'development/backend-specialist',
  265. 'OpenBackendSpecialist': 'development/backend-specialist',
  266. 'technical-writer': 'content/technical-writer',
  267. 'OpenTechnicalWriter': 'content/technical-writer',
  268. 'copywriter': 'content/copywriter',
  269. 'OpenCopywriter': 'content/copywriter',
  270. 'data-analyst': 'data/data-analyst',
  271. 'OpenDataAnalyst': 'data/data-analyst',
  272. 'repo-manager': 'meta/repo-manager',
  273. 'OpenRepoManager': 'meta/repo-manager',
  274. };
  275. return coreAgents[agent] || agent;
  276. };
  277. /**
  278. * Extract category from agent identifier
  279. *
  280. * Examples:
  281. * - "core/openagent" → "core"
  282. * - "development/frontend-specialist" → "development"
  283. * - "subagents/code/tester" → "subagents/code"
  284. * - "openagent" → undefined (flat structure)
  285. *
  286. * @param agent - Agent identifier
  287. * @returns Category path or undefined
  288. */
  289. export const extractAgentCategory = (agent: string): string | undefined => {
  290. if (!agent.includes('/')) {
  291. return undefined;
  292. }
  293. const parts = agent.split('/');
  294. // For subagents, include both levels (e.g., "subagents/code")
  295. if (parts[0] === 'subagents' && parts.length >= 2) {
  296. return `${parts[0]}/${parts[1]}`;
  297. }
  298. // For regular categories, just the first part
  299. return parts[0];
  300. };