system.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. import { Ripgrep } from "../file/ripgrep"
  2. import { Global } from "../global"
  3. import { Filesystem } from "../util/filesystem"
  4. import { Config } from "../config/config"
  5. import { Log } from "../util/log"
  6. import { Instance } from "../project/instance"
  7. import path from "path"
  8. import os from "os"
  9. import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
  10. import PROMPT_ANTHROPIC_WITHOUT_TODO from "./prompt/qwen.txt"
  11. import PROMPT_BEAST from "./prompt/beast.txt"
  12. import PROMPT_GEMINI from "./prompt/gemini.txt"
  13. import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt"
  14. import PROMPT_CODEX from "./prompt/codex_header.txt"
  15. import type { Provider } from "@/provider/provider"
  16. import { Flag } from "@/flag/flag"
  17. const log = Log.create({ service: "system-prompt" })
  18. async function resolveRelativeInstruction(instruction: string): Promise<string[]> {
  19. if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
  20. return Filesystem.globUp(instruction, Instance.directory, Instance.worktree).catch(() => [])
  21. }
  22. if (!Flag.OPENCODE_CONFIG_DIR) {
  23. log.warn(
  24. `Skipping relative instruction "${instruction}" - no OPENCODE_CONFIG_DIR set while project config is disabled`,
  25. )
  26. return []
  27. }
  28. return Filesystem.globUp(instruction, Flag.OPENCODE_CONFIG_DIR, Flag.OPENCODE_CONFIG_DIR).catch(() => [])
  29. }
  30. export namespace SystemPrompt {
  31. export function header(providerID: string) {
  32. if (providerID.includes("anthropic")) return [PROMPT_ANTHROPIC_SPOOF.trim()]
  33. return []
  34. }
  35. export function instructions() {
  36. return PROMPT_CODEX.trim()
  37. }
  38. export function provider(model: Provider.Model) {
  39. if (model.api.id.includes("gpt-5")) return [PROMPT_CODEX]
  40. if (model.api.id.includes("gpt-") || model.api.id.includes("o1") || model.api.id.includes("o3"))
  41. return [PROMPT_BEAST]
  42. if (model.api.id.includes("gemini-")) return [PROMPT_GEMINI]
  43. if (model.api.id.includes("claude")) return [PROMPT_ANTHROPIC]
  44. return [PROMPT_ANTHROPIC_WITHOUT_TODO]
  45. }
  46. export async function environment(model: Provider.Model) {
  47. const project = Instance.project
  48. return [
  49. [
  50. `You are powered by the model named ${model.api.id}. The exact model ID is ${model.providerID}/${model.api.id}`,
  51. `Here is some useful information about the environment you are running in:`,
  52. `<env>`,
  53. ` Working directory: ${Instance.directory}`,
  54. ` Is directory a git repo: ${project.vcs === "git" ? "yes" : "no"}`,
  55. ` Platform: ${process.platform}`,
  56. ` Today's date: ${new Date().toDateString()}`,
  57. `</env>`,
  58. `<files>`,
  59. ` ${
  60. project.vcs === "git" && false
  61. ? await Ripgrep.tree({
  62. cwd: Instance.directory,
  63. limit: 200,
  64. })
  65. : ""
  66. }`,
  67. `</files>`,
  68. ].join("\n"),
  69. ]
  70. }
  71. const LOCAL_RULE_FILES = [
  72. "AGENTS.md",
  73. "CLAUDE.md",
  74. "CONTEXT.md", // deprecated
  75. ]
  76. const GLOBAL_RULE_FILES = [path.join(Global.Path.config, "AGENTS.md")]
  77. if (!Flag.OPENCODE_DISABLE_CLAUDE_CODE_PROMPT) {
  78. GLOBAL_RULE_FILES.push(path.join(os.homedir(), ".claude", "CLAUDE.md"))
  79. }
  80. if (Flag.OPENCODE_CONFIG_DIR) {
  81. GLOBAL_RULE_FILES.push(path.join(Flag.OPENCODE_CONFIG_DIR, "AGENTS.md"))
  82. }
  83. export async function custom() {
  84. const config = await Config.get()
  85. const paths = new Set<string>()
  86. // Only scan local rule files when project discovery is enabled
  87. if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
  88. for (const localRuleFile of LOCAL_RULE_FILES) {
  89. const matches = await Filesystem.findUp(localRuleFile, Instance.directory, Instance.worktree)
  90. if (matches.length > 0) {
  91. matches.forEach((path) => paths.add(path))
  92. break
  93. }
  94. }
  95. }
  96. for (const globalRuleFile of GLOBAL_RULE_FILES) {
  97. if (await Bun.file(globalRuleFile).exists()) {
  98. paths.add(globalRuleFile)
  99. break
  100. }
  101. }
  102. const urls: string[] = []
  103. if (config.instructions) {
  104. for (let instruction of config.instructions) {
  105. if (instruction.startsWith("https://") || instruction.startsWith("http://")) {
  106. urls.push(instruction)
  107. continue
  108. }
  109. if (instruction.startsWith("~/")) {
  110. instruction = path.join(os.homedir(), instruction.slice(2))
  111. }
  112. let matches: string[] = []
  113. if (path.isAbsolute(instruction)) {
  114. matches = await Array.fromAsync(
  115. new Bun.Glob(path.basename(instruction)).scan({
  116. cwd: path.dirname(instruction),
  117. absolute: true,
  118. onlyFiles: true,
  119. }),
  120. ).catch(() => [])
  121. } else {
  122. matches = await resolveRelativeInstruction(instruction)
  123. }
  124. matches.forEach((path) => paths.add(path))
  125. }
  126. }
  127. const foundFiles = Array.from(paths).map((p) =>
  128. Bun.file(p)
  129. .text()
  130. .catch(() => "")
  131. .then((x) => "Instructions from: " + p + "\n" + x),
  132. )
  133. const foundUrls = urls.map((url) =>
  134. fetch(url, { signal: AbortSignal.timeout(5000) })
  135. .then((res) => (res.ok ? res.text() : ""))
  136. .catch(() => "")
  137. .then((x) => (x ? "Instructions from: " + url + "\n" + x : "")),
  138. )
  139. return Promise.all([...foundFiles, ...foundUrls]).then((result) => result.filter(Boolean))
  140. }
  141. }