run.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. import type { Argv } from "yargs"
  2. import { App } from "../../app/app"
  3. import { Bus } from "../../bus"
  4. import { Provider } from "../../provider/provider"
  5. import { Session } from "../../session"
  6. import { Share } from "../../share/share"
  7. import { Message } from "../../session/message"
  8. import { UI } from "../ui"
  9. import { cmd } from "./cmd"
  10. import { Flag } from "../../flag/flag"
  11. import { Config } from "../../config/config"
  12. const TOOL: Record<string, [string, string]> = {
  13. todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
  14. todoread: ["Todo", UI.Style.TEXT_WARNING_BOLD],
  15. bash: ["Bash", UI.Style.TEXT_DANGER_BOLD],
  16. edit: ["Edit", UI.Style.TEXT_SUCCESS_BOLD],
  17. glob: ["Glob", UI.Style.TEXT_INFO_BOLD],
  18. grep: ["Grep", UI.Style.TEXT_INFO_BOLD],
  19. list: ["List", UI.Style.TEXT_INFO_BOLD],
  20. read: ["Read", UI.Style.TEXT_HIGHLIGHT_BOLD],
  21. write: ["Write", UI.Style.TEXT_SUCCESS_BOLD],
  22. websearch: ["Search", UI.Style.TEXT_DIM_BOLD],
  23. }
  24. export const RunCommand = cmd({
  25. command: "run [message..]",
  26. describe: "run opencode with a message",
  27. builder: (yargs: Argv) => {
  28. return yargs
  29. .positional("message", {
  30. describe: "message to send",
  31. type: "string",
  32. array: true,
  33. default: [],
  34. })
  35. .option("continue", {
  36. alias: ["c"],
  37. describe: "continue the last session",
  38. type: "boolean",
  39. })
  40. .option("session", {
  41. alias: ["s"],
  42. describe: "session id to continue",
  43. type: "string",
  44. })
  45. .option("share", {
  46. type: "boolean",
  47. describe: "share the session",
  48. })
  49. .option("model", {
  50. type: "string",
  51. alias: ["m"],
  52. describe: "model to use in the format of provider/model",
  53. })
  54. },
  55. handler: async (args) => {
  56. const message = args.message.join(" ")
  57. await App.provide(
  58. {
  59. cwd: process.cwd(),
  60. },
  61. async () => {
  62. await Share.init()
  63. const session = await (async () => {
  64. if (args.continue) {
  65. const first = await Session.list().next()
  66. if (first.done) return
  67. return first.value
  68. }
  69. if (args.session) return Session.get(args.session)
  70. return Session.create()
  71. })()
  72. if (!session) {
  73. UI.error("Session not found")
  74. return
  75. }
  76. const isPiped = !process.stdout.isTTY
  77. UI.empty()
  78. UI.println(UI.logo())
  79. UI.empty()
  80. UI.println(UI.Style.TEXT_NORMAL_BOLD + "> ", message)
  81. UI.empty()
  82. const cfg = await Config.get()
  83. if (cfg.autoshare || Flag.OPENCODE_AUTO_SHARE || args.share) {
  84. await Session.share(session.id)
  85. UI.println(
  86. UI.Style.TEXT_INFO_BOLD +
  87. "~ https://opencode.ai/s/" +
  88. session.id.slice(-8),
  89. )
  90. }
  91. UI.empty()
  92. const { providerID, modelID } = args.model
  93. ? Provider.parseModel(args.model)
  94. : await Provider.defaultModel()
  95. UI.println(
  96. UI.Style.TEXT_NORMAL_BOLD + "@ ",
  97. UI.Style.TEXT_NORMAL + `${providerID}/${modelID}`,
  98. )
  99. UI.empty()
  100. function printEvent(color: string, type: string, title: string) {
  101. UI.println(
  102. color + `|`,
  103. UI.Style.TEXT_NORMAL +
  104. UI.Style.TEXT_DIM +
  105. ` ${type.padEnd(7, " ")}`,
  106. "",
  107. UI.Style.TEXT_NORMAL + title,
  108. )
  109. }
  110. Bus.subscribe(Message.Event.PartUpdated, async (evt) => {
  111. if (evt.properties.sessionID !== session.id) return
  112. const part = evt.properties.part
  113. const message = await Session.getMessage(
  114. evt.properties.sessionID,
  115. evt.properties.messageID,
  116. )
  117. if (
  118. part.type === "tool-invocation" &&
  119. part.toolInvocation.state === "result"
  120. ) {
  121. const metadata =
  122. message.metadata.tool[part.toolInvocation.toolCallId]
  123. const [tool, color] = TOOL[part.toolInvocation.toolName] ?? [
  124. part.toolInvocation.toolName,
  125. UI.Style.TEXT_INFO_BOLD,
  126. ]
  127. printEvent(color, tool, metadata?.title || "Unknown")
  128. }
  129. if (part.type === "text") {
  130. if (part.text.includes("\n")) {
  131. UI.empty()
  132. UI.println(part.text)
  133. UI.empty()
  134. return
  135. }
  136. printEvent(UI.Style.TEXT_NORMAL_BOLD, "Text", part.text)
  137. }
  138. })
  139. const result = await Session.chat({
  140. sessionID: session.id,
  141. providerID,
  142. modelID,
  143. parts: [
  144. {
  145. type: "text",
  146. text: message,
  147. },
  148. ],
  149. })
  150. if (isPiped) {
  151. const match = result.parts.findLast((x) => x.type === "text")
  152. if (match) process.stdout.write(match.text)
  153. }
  154. UI.empty()
  155. },
  156. )
  157. },
  158. })