non_interactive_mode.go 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. package cmd
  2. import (
  3. "context"
  4. "fmt"
  5. "io"
  6. "os"
  7. "sync"
  8. "time"
  9. "log/slog"
  10. charmlog "github.com/charmbracelet/log"
  11. "github.com/sst/opencode/internal/app"
  12. "github.com/sst/opencode/internal/config"
  13. "github.com/sst/opencode/internal/db"
  14. "github.com/sst/opencode/internal/format"
  15. "github.com/sst/opencode/internal/llm/agent"
  16. "github.com/sst/opencode/internal/llm/tools"
  17. "github.com/sst/opencode/internal/message"
  18. "github.com/sst/opencode/internal/permission"
  19. "github.com/sst/opencode/internal/tui/components/spinner"
  20. "github.com/sst/opencode/internal/tui/theme"
  21. )
  22. // syncWriter is a thread-safe writer that prevents interleaved output
  23. type syncWriter struct {
  24. w io.Writer
  25. mu sync.Mutex
  26. }
  27. // Write implements io.Writer
  28. func (sw *syncWriter) Write(p []byte) (n int, err error) {
  29. sw.mu.Lock()
  30. defer sw.mu.Unlock()
  31. return sw.w.Write(p)
  32. }
  33. // newSyncWriter creates a new synchronized writer
  34. func newSyncWriter(w io.Writer) io.Writer {
  35. return &syncWriter{w: w}
  36. }
  37. // filterTools filters the provided tools based on allowed or excluded tool names
  38. func filterTools(allTools []tools.BaseTool, allowedTools, excludedTools []string) []tools.BaseTool {
  39. // If neither allowed nor excluded tools are specified, return all tools
  40. if len(allowedTools) == 0 && len(excludedTools) == 0 {
  41. return allTools
  42. }
  43. // Create a map for faster lookups
  44. allowedMap := make(map[string]bool)
  45. for _, name := range allowedTools {
  46. allowedMap[name] = true
  47. }
  48. excludedMap := make(map[string]bool)
  49. for _, name := range excludedTools {
  50. excludedMap[name] = true
  51. }
  52. var filteredTools []tools.BaseTool
  53. for _, tool := range allTools {
  54. toolName := tool.Info().Name
  55. // If we have an allowed list, only include tools in that list
  56. if len(allowedTools) > 0 {
  57. if allowedMap[toolName] {
  58. filteredTools = append(filteredTools, tool)
  59. }
  60. } else if len(excludedTools) > 0 {
  61. // If we have an excluded list, include all tools except those in the list
  62. if !excludedMap[toolName] {
  63. filteredTools = append(filteredTools, tool)
  64. }
  65. }
  66. }
  67. return filteredTools
  68. }
  69. // handleNonInteractiveMode processes a single prompt in non-interactive mode
  70. func handleNonInteractiveMode(ctx context.Context, prompt string, outputFormat format.OutputFormat, quiet bool, verbose bool, allowedTools, excludedTools []string) error {
  71. // Initial log message using standard slog
  72. slog.Info("Running in non-interactive mode", "prompt", prompt, "format", outputFormat, "quiet", quiet, "verbose", verbose,
  73. "allowedTools", allowedTools, "excludedTools", excludedTools)
  74. // Sanity check for mutually exclusive flags
  75. if quiet && verbose {
  76. return fmt.Errorf("--quiet and --verbose flags cannot be used together")
  77. }
  78. // Set up logging to stderr if verbose mode is enabled
  79. if verbose {
  80. // Create a synchronized writer to prevent interleaved output
  81. syncWriter := newSyncWriter(os.Stderr)
  82. // Create a charmbracelet/log logger that writes to the synchronized writer
  83. charmLogger := charmlog.NewWithOptions(syncWriter, charmlog.Options{
  84. Level: charmlog.DebugLevel,
  85. ReportCaller: true,
  86. ReportTimestamp: true,
  87. TimeFormat: time.RFC3339,
  88. Prefix: "OpenCode",
  89. })
  90. // Set the global logger for charmbracelet/log
  91. charmlog.SetDefault(charmLogger)
  92. // Create a slog handler that uses charmbracelet/log
  93. // This will forward all slog logs to charmbracelet/log
  94. slog.SetDefault(slog.New(charmLogger))
  95. // Log a message to confirm verbose logging is enabled
  96. charmLogger.Info("Verbose logging enabled")
  97. }
  98. // Start spinner if not in quiet mode
  99. var s *spinner.Spinner
  100. if !quiet {
  101. // Get the current theme to style the spinner
  102. currentTheme := theme.CurrentTheme()
  103. // Create a themed spinner
  104. if currentTheme != nil {
  105. // Use the primary color from the theme
  106. s = spinner.NewThemedSpinner("Thinking...", currentTheme.Primary())
  107. } else {
  108. // Fallback to default spinner if no theme is available
  109. s = spinner.NewSpinner("Thinking...")
  110. }
  111. s.Start()
  112. defer s.Stop()
  113. }
  114. // Connect DB, this will also run migrations
  115. conn, err := db.Connect()
  116. if err != nil {
  117. return err
  118. }
  119. // Create a context with cancellation
  120. ctx, cancel := context.WithCancel(ctx)
  121. defer cancel()
  122. // Create the app
  123. app, err := app.New(ctx, conn)
  124. if err != nil {
  125. slog.Error("Failed to create app", "error", err)
  126. return err
  127. }
  128. // Create a new session for this prompt
  129. session, err := app.Sessions.Create(ctx, "Non-interactive prompt")
  130. if err != nil {
  131. return fmt.Errorf("failed to create session: %w", err)
  132. }
  133. // Set the session as current
  134. app.CurrentSession = &session
  135. // Auto-approve all permissions for this session
  136. permission.AutoApproveSession(ctx, session.ID)
  137. // Create the user message
  138. _, err = app.Messages.Create(ctx, session.ID, message.CreateMessageParams{
  139. Role: message.User,
  140. Parts: []message.ContentPart{message.TextContent{Text: prompt}},
  141. })
  142. if err != nil {
  143. return fmt.Errorf("failed to create message: %w", err)
  144. }
  145. // If tool restrictions are specified, create a new agent with filtered tools
  146. if len(allowedTools) > 0 || len(excludedTools) > 0 {
  147. // Initialize MCP tools synchronously to ensure they're included in filtering
  148. mcpCtx, mcpCancel := context.WithTimeout(ctx, 10*time.Second)
  149. agent.GetMcpTools(mcpCtx, app.Permissions)
  150. mcpCancel()
  151. // Get all available tools including MCP tools
  152. allTools := agent.PrimaryAgentTools(
  153. app.Permissions,
  154. app.Sessions,
  155. app.Messages,
  156. app.History,
  157. app.LSPClients,
  158. )
  159. // Filter tools based on allowed/excluded lists
  160. filteredTools := filterTools(allTools, allowedTools, excludedTools)
  161. // Log the filtered tools for debugging
  162. var toolNames []string
  163. for _, tool := range filteredTools {
  164. toolNames = append(toolNames, tool.Info().Name)
  165. }
  166. slog.Debug("Using filtered tools", "count", len(filteredTools), "tools", toolNames)
  167. // Create a new agent with the filtered tools
  168. restrictedAgent, err := agent.NewAgent(
  169. config.AgentPrimary,
  170. app.Sessions,
  171. app.Messages,
  172. filteredTools,
  173. )
  174. if err != nil {
  175. return fmt.Errorf("failed to create restricted agent: %w", err)
  176. }
  177. // Use the restricted agent for this request
  178. eventCh, err := restrictedAgent.Run(ctx, session.ID, prompt)
  179. if err != nil {
  180. return fmt.Errorf("failed to run restricted agent: %w", err)
  181. }
  182. // Wait for the response
  183. var response message.Message
  184. for event := range eventCh {
  185. if event.Err() != nil {
  186. return fmt.Errorf("agent error: %w", event.Err())
  187. }
  188. response = event.Response()
  189. }
  190. // Format and print the output
  191. content := ""
  192. if textContent := response.Content(); textContent != nil {
  193. content = textContent.Text
  194. }
  195. formattedOutput, err := format.FormatOutput(content, outputFormat)
  196. if err != nil {
  197. return fmt.Errorf("failed to format output: %w", err)
  198. }
  199. // Stop spinner before printing output
  200. if !quiet && s != nil {
  201. s.Stop()
  202. }
  203. // Print the formatted output to stdout
  204. fmt.Println(formattedOutput)
  205. // Shutdown the app
  206. app.Shutdown()
  207. return nil
  208. }
  209. // Run the default agent if no tool restrictions
  210. eventCh, err := app.PrimaryAgent.Run(ctx, session.ID, prompt)
  211. if err != nil {
  212. return fmt.Errorf("failed to run agent: %w", err)
  213. }
  214. // Wait for the response
  215. var response message.Message
  216. for event := range eventCh {
  217. if event.Err() != nil {
  218. return fmt.Errorf("agent error: %w", event.Err())
  219. }
  220. response = event.Response()
  221. }
  222. // Get the text content from the response
  223. content := ""
  224. if textContent := response.Content(); textContent != nil {
  225. content = textContent.Text
  226. }
  227. // Format the output according to the specified format
  228. formattedOutput, err := format.FormatOutput(content, outputFormat)
  229. if err != nil {
  230. return fmt.Errorf("failed to format output: %w", err)
  231. }
  232. // Stop spinner before printing output
  233. if !quiet && s != nil {
  234. s.Stop()
  235. }
  236. // Print the formatted output to stdout
  237. fmt.Println(formattedOutput)
  238. // Shutdown the app
  239. app.Shutdown()
  240. return nil
  241. }