@vladimirven/openswe 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/AGENTS.md +203 -0
  2. package/CLAUDE.md +203 -0
  3. package/README.md +166 -0
  4. package/bun.lock +447 -0
  5. package/bunfig.toml +4 -0
  6. package/package.json +42 -0
  7. package/src/app.tsx +84 -0
  8. package/src/components/App.tsx +526 -0
  9. package/src/components/ConfirmDialog.tsx +88 -0
  10. package/src/components/Footer.tsx +50 -0
  11. package/src/components/HelpModal.tsx +136 -0
  12. package/src/components/IssueSelectorModal.tsx +701 -0
  13. package/src/components/ManualSessionModal.tsx +191 -0
  14. package/src/components/PhaseProgress.tsx +45 -0
  15. package/src/components/Preview.tsx +249 -0
  16. package/src/components/ProviderSwitcherModal.tsx +156 -0
  17. package/src/components/ScrollableText.tsx +120 -0
  18. package/src/components/SessionCard.tsx +60 -0
  19. package/src/components/SessionList.tsx +79 -0
  20. package/src/components/SessionTerminal.tsx +89 -0
  21. package/src/components/StatusBar.tsx +84 -0
  22. package/src/components/ThemeSwitcherModal.tsx +237 -0
  23. package/src/components/index.ts +58 -0
  24. package/src/components/session-utils.ts +337 -0
  25. package/src/components/theme.ts +206 -0
  26. package/src/components/types.ts +215 -0
  27. package/src/config/defaults.ts +44 -0
  28. package/src/config/env.ts +67 -0
  29. package/src/config/global.ts +252 -0
  30. package/src/config/index.ts +171 -0
  31. package/src/config/types.ts +131 -0
  32. package/src/core/.gitkeep +0 -0
  33. package/src/core/index.ts +5 -0
  34. package/src/core/parser.ts +62 -0
  35. package/src/core/process-manager.ts +52 -0
  36. package/src/core/session.ts +423 -0
  37. package/src/core/tmux.ts +206 -0
  38. package/src/git/.gitkeep +0 -0
  39. package/src/git/index.ts +8 -0
  40. package/src/git/repo.ts +443 -0
  41. package/src/git/worktree.ts +317 -0
  42. package/src/github/.gitkeep +0 -0
  43. package/src/github/client.ts +208 -0
  44. package/src/github/index.ts +8 -0
  45. package/src/github/issues.ts +351 -0
  46. package/src/index.ts +369 -0
  47. package/src/prompts/.gitkeep +0 -0
  48. package/src/prompts/index.ts +1 -0
  49. package/src/prompts/swe-system.ts +22 -0
  50. package/src/providers/claude.ts +103 -0
  51. package/src/providers/index.ts +21 -0
  52. package/src/providers/opencode.ts +98 -0
  53. package/src/providers/registry.ts +53 -0
  54. package/src/providers/types.ts +117 -0
  55. package/src/store/buffers.ts +234 -0
  56. package/src/store/db.test.ts +579 -0
  57. package/src/store/db.ts +249 -0
  58. package/src/store/index.ts +101 -0
  59. package/src/store/project.ts +119 -0
  60. package/src/store/schema.sql +71 -0
  61. package/src/store/sessions.ts +454 -0
  62. package/src/store/types.ts +194 -0
  63. package/src/theme/context.tsx +170 -0
  64. package/src/theme/custom.ts +134 -0
  65. package/src/theme/index.ts +58 -0
  66. package/src/theme/loader.ts +264 -0
  67. package/src/theme/themes/aura.json +69 -0
  68. package/src/theme/themes/ayu.json +80 -0
  69. package/src/theme/themes/carbonfox.json +248 -0
  70. package/src/theme/themes/catppuccin-frappe.json +233 -0
  71. package/src/theme/themes/catppuccin-macchiato.json +233 -0
  72. package/src/theme/themes/catppuccin.json +112 -0
  73. package/src/theme/themes/cobalt2.json +228 -0
  74. package/src/theme/themes/cursor.json +249 -0
  75. package/src/theme/themes/dracula.json +219 -0
  76. package/src/theme/themes/everforest.json +241 -0
  77. package/src/theme/themes/flexoki.json +237 -0
  78. package/src/theme/themes/github.json +233 -0
  79. package/src/theme/themes/gruvbox.json +242 -0
  80. package/src/theme/themes/kanagawa.json +77 -0
  81. package/src/theme/themes/lucent-orng.json +237 -0
  82. package/src/theme/themes/material.json +235 -0
  83. package/src/theme/themes/matrix.json +77 -0
  84. package/src/theme/themes/mercury.json +252 -0
  85. package/src/theme/themes/monokai.json +221 -0
  86. package/src/theme/themes/nightowl.json +221 -0
  87. package/src/theme/themes/nord.json +223 -0
  88. package/src/theme/themes/one-dark.json +84 -0
  89. package/src/theme/themes/opencode.json +245 -0
  90. package/src/theme/themes/orng.json +249 -0
  91. package/src/theme/themes/osaka-jade.json +93 -0
  92. package/src/theme/themes/palenight.json +222 -0
  93. package/src/theme/themes/rosepine.json +234 -0
  94. package/src/theme/themes/solarized.json +223 -0
  95. package/src/theme/themes/synthwave84.json +226 -0
  96. package/src/theme/themes/tokyonight.json +243 -0
  97. package/src/theme/themes/vercel.json +245 -0
  98. package/src/theme/themes/vesper.json +218 -0
  99. package/src/theme/themes/zenburn.json +223 -0
  100. package/src/theme/types.ts +225 -0
  101. package/src/types/sql.d.ts +4 -0
  102. package/src/utils/ansi-parser.ts +225 -0
  103. package/src/utils/format.ts +46 -0
  104. package/src/utils/id.ts +15 -0
  105. package/src/utils/logger.ts +112 -0
  106. package/src/utils/prerequisites.ts +118 -0
  107. package/src/utils/shell.ts +9 -0
  108. package/src/wizard/flows.ts +419 -0
  109. package/src/wizard/index.ts +37 -0
  110. package/src/wizard/prompts.ts +190 -0
  111. package/src/workspace/detect.test.ts +51 -0
  112. package/src/workspace/detect.ts +223 -0
  113. package/src/workspace/index.ts +71 -0
  114. package/src/workspace/init.ts +131 -0
  115. package/src/workspace/paths.ts +143 -0
  116. package/src/workspace/project.ts +164 -0
  117. package/tsconfig.json +22 -0
@@ -0,0 +1 @@
1
+ export { generateSWEPrompt } from "./swe-system"
@@ -0,0 +1,22 @@
1
+ /**
2
+ * SWE system prompt generator
3
+ *
4
+ * Creates the system prompt used to guide AI coding sessions.
5
+ */
6
+
7
+ import type { Session } from "../store"
8
+
9
+ /**
10
+ * Generate the SWE system prompt for a session
11
+ */
12
+ export function generateSWEPrompt(session: Session): string {
13
+ const taskSection = session.issueNumber
14
+ ? `Title: Issue #${session.issueNumber}: "${session.issueTitle ?? ""}"
15
+ ${session.issueBody ?? ""}`
16
+ : `Task: ${session.name}`
17
+
18
+ return `Investigate and create a plan to implement this feature:
19
+
20
+ ${taskSection}
21
+ `
22
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Claude Code provider implementation
3
+ *
4
+ * Supports the Claude CLI (https://docs.anthropic.com/claude-code)
5
+ */
6
+
7
+ import type { Provider, ProviderBranding, ParserPatterns, SpawnCommand } from "./types"
8
+ import type { Session } from "../store"
9
+ import type { ClaudeConfig } from "../config/types"
10
+
11
+ // ============================================================================
12
+ // Branding
13
+ // ============================================================================
14
+
15
+ const branding: ProviderBranding = {
16
+ displayName: "Claude Code",
17
+ shortName: "claude",
18
+ accentColor: "#cc785c",
19
+ secondaryColor: "#a65f48",
20
+ logoText: "CC",
21
+ }
22
+
23
+ // ============================================================================
24
+ // Parser Patterns
25
+ // ============================================================================
26
+
27
+ const parserPatterns: ParserPatterns = {
28
+ // Claude Code outputs status messages differently
29
+ workingRegex: /(?:starting|begin|entering).+(?:implementation|coding|execution)|(?:mode|status):\s*(?:implement|coding)|editing|writing.*file/i,
30
+ doneRegex: /\[?OPENSWE:DONE\]?|completed successfully|task completed/i,
31
+ }
32
+
33
+ // ============================================================================
34
+ // Provider Implementation
35
+ // ============================================================================
36
+
37
+ export const claudeProvider: Provider = {
38
+ id: "claude",
39
+ name: "Claude Code",
40
+ branding,
41
+ parserPatterns,
42
+
43
+ buildSpawnCommand(
44
+ _session: Session,
45
+ prompt?: string,
46
+ resumeSessionId?: string,
47
+ config?: Record<string, unknown>
48
+ ): SpawnCommand {
49
+ const claudeConfig = config as ClaudeConfig | undefined
50
+ const model = claudeConfig?.model ?? "claude-sonnet-4-20250514"
51
+
52
+ // Run Claude Code interactively (no --print flag)
53
+ // User can attach to the tmux session to review and approve changes
54
+ const args = [
55
+ "--model", model,
56
+ ]
57
+
58
+ if (resumeSessionId) {
59
+ args.push("--resume", resumeSessionId)
60
+ }
61
+
62
+ // Add the prompt as the final argument (omit for interactive mode)
63
+ if (prompt) {
64
+ args.push(prompt)
65
+ }
66
+
67
+ return {
68
+ command: "claude",
69
+ args,
70
+ }
71
+ },
72
+
73
+ async validateInstallation(): Promise<boolean> {
74
+ try {
75
+ const proc = Bun.spawn(["which", "claude"], {
76
+ stdout: "pipe",
77
+ stderr: "pipe",
78
+ })
79
+ const exitCode = await proc.exited
80
+ return exitCode === 0
81
+ } catch {
82
+ return false
83
+ }
84
+ },
85
+
86
+ async getVersion(): Promise<string | null> {
87
+ try {
88
+ const proc = Bun.spawn(["claude", "--version"], {
89
+ stdout: "pipe",
90
+ stderr: "pipe",
91
+ })
92
+ const exitCode = await proc.exited
93
+ if (exitCode !== 0) return null
94
+
95
+ const output = await new Response(proc.stdout).text()
96
+ // Extract version from output
97
+ const match = output.match(/v?(\d+\.\d+\.\d+(?:-[\w.]+)?)/)
98
+ return match ? match[1] ?? null : output.trim() || null
99
+ } catch {
100
+ return null
101
+ }
102
+ },
103
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Provider system exports
3
+ *
4
+ * Abstract AI backend provider interface with implementations
5
+ * for OpenCode and Claude Code.
6
+ */
7
+
8
+ // Registry functions
9
+ export { getProvider, getAllProviders, isProviderSupported } from "./registry"
10
+
11
+ // Provider implementations
12
+ export { openCodeProvider } from "./opencode"
13
+ export { claudeProvider } from "./claude"
14
+
15
+ // Types
16
+ export type {
17
+ Provider,
18
+ ProviderBranding,
19
+ ParserPatterns,
20
+ SpawnCommand,
21
+ } from "./types"
@@ -0,0 +1,98 @@
1
+ /**
2
+ * OpenCode provider implementation
3
+ *
4
+ * Supports the OpenCode CLI (https://github.com/opencode-ai/opencode)
5
+ */
6
+
7
+ import type { Provider, ProviderBranding, ParserPatterns, SpawnCommand } from "./types"
8
+ import type { Session } from "../store"
9
+ import type { OpenCodeConfig } from "../config/types"
10
+
11
+ // ============================================================================
12
+ // Branding
13
+ // ============================================================================
14
+
15
+ const branding: ProviderBranding = {
16
+ displayName: "OpenCode",
17
+ shortName: "opencode",
18
+ accentColor: "#fab283",
19
+ secondaryColor: "#d4956b",
20
+ logoText: "OC",
21
+ terminalBackground: "#000000",
22
+ }
23
+
24
+ // ============================================================================
25
+ // Parser Patterns
26
+ // ============================================================================
27
+
28
+ const parserPatterns: ParserPatterns = {
29
+ workingRegex: /(?:starting|begin|entering).+(?:implementation|coding|execution)|(?:mode|status):\s*(?:implement|coding)/i,
30
+ doneRegex: /\[?OPENSWE:DONE\]?/i,
31
+ }
32
+
33
+ // ============================================================================
34
+ // Provider Implementation
35
+ // ============================================================================
36
+
37
+ export const openCodeProvider: Provider = {
38
+ id: "opencode",
39
+ name: "OpenCode",
40
+ branding,
41
+ parserPatterns,
42
+
43
+ buildSpawnCommand(
44
+ _session: Session,
45
+ prompt?: string,
46
+ resumeSessionId?: string,
47
+ config?: Record<string, unknown>
48
+ ): SpawnCommand {
49
+ const openCodeConfig = config as OpenCodeConfig | undefined
50
+
51
+ const args = []
52
+
53
+ // Without them, opencode launches in interactive TUI mode
54
+ if (prompt) {
55
+ args.push("--agent", "plan", "--prompt", prompt)
56
+ }
57
+
58
+ if (resumeSessionId) {
59
+ args.push("--session", resumeSessionId)
60
+ }
61
+
62
+ return {
63
+ command: "opencode",
64
+ args,
65
+ }
66
+ },
67
+
68
+ async validateInstallation(): Promise<boolean> {
69
+ try {
70
+ const proc = Bun.spawn(["which", "opencode"], {
71
+ stdout: "pipe",
72
+ stderr: "pipe",
73
+ })
74
+ const exitCode = await proc.exited
75
+ return exitCode === 0
76
+ } catch {
77
+ return false
78
+ }
79
+ },
80
+
81
+ async getVersion(): Promise<string | null> {
82
+ try {
83
+ const proc = Bun.spawn(["opencode", "--version"], {
84
+ stdout: "pipe",
85
+ stderr: "pipe",
86
+ })
87
+ const exitCode = await proc.exited
88
+ if (exitCode !== 0) return null
89
+
90
+ const output = await new Response(proc.stdout).text()
91
+ // Extract version from output (e.g., "opencode v1.2.3" or just "1.2.3")
92
+ const match = output.match(/v?(\d+\.\d+\.\d+(?:-[\w.]+)?)/)
93
+ return match ? match[1] ?? null : output.trim() || null
94
+ } catch {
95
+ return null
96
+ }
97
+ },
98
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Provider registry for AI backend lookup
3
+ *
4
+ * Centralized access to all registered providers.
5
+ */
6
+
7
+ import type { AIBackend } from "../config/types"
8
+ import type { Provider } from "./types"
9
+ import { openCodeProvider } from "./opencode"
10
+ import { claudeProvider } from "./claude"
11
+
12
+ // ============================================================================
13
+ // Registry
14
+ // ============================================================================
15
+
16
+ const providers: Map<AIBackend, Provider> = new Map([
17
+ ["opencode", openCodeProvider],
18
+ ["claude", claudeProvider],
19
+ ])
20
+
21
+ // ============================================================================
22
+ // Public API
23
+ // ============================================================================
24
+
25
+ /**
26
+ * Get a provider by its backend ID
27
+ * @param backend - The backend identifier (e.g., "opencode", "claude")
28
+ * @returns The provider instance
29
+ * @throws Error if provider not found
30
+ */
31
+ export function getProvider(backend: AIBackend): Provider {
32
+ const provider = providers.get(backend)
33
+ if (!provider) {
34
+ throw new Error(`Unknown AI backend: ${backend}. Available: ${Array.from(providers.keys()).join(", ")}`)
35
+ }
36
+ return provider
37
+ }
38
+
39
+ /**
40
+ * Get all registered providers
41
+ * @returns Array of all provider instances
42
+ */
43
+ export function getAllProviders(): Provider[] {
44
+ return Array.from(providers.values())
45
+ }
46
+
47
+ /**
48
+ * Check if a backend is supported
49
+ * @param backend - The backend identifier to check
50
+ */
51
+ export function isProviderSupported(backend: string): backend is AIBackend {
52
+ return providers.has(backend as AIBackend)
53
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Provider type definitions for AI backend abstraction
3
+ *
4
+ * Supports multiple AI backends (Claude Code, OpenCode, etc.)
5
+ * with provider-specific command building and branding.
6
+ */
7
+
8
+ import type { AIBackend } from "../config/types"
9
+ import type { Session } from "../store"
10
+
11
+ // ============================================================================
12
+ // Core Provider Interface
13
+ // ============================================================================
14
+
15
+ /**
16
+ * Abstract provider interface for AI backends
17
+ *
18
+ * Implementations handle:
19
+ * - Command building for spawning AI sessions
20
+ * - Branding for UI display
21
+ * - Parser patterns for output detection
22
+ * - Installation/version validation
23
+ */
24
+ export interface Provider {
25
+ /** Unique identifier matching AIBackend type */
26
+ readonly id: AIBackend
27
+ /** Human-readable display name */
28
+ readonly name: string
29
+ /** UI branding configuration */
30
+ readonly branding: ProviderBranding
31
+ /** Output parser patterns */
32
+ readonly parserPatterns: ParserPatterns
33
+
34
+ /**
35
+ * Build the spawn command for starting an AI session
36
+ * @param session - Session to start
37
+ * @param prompt - Optional prompt to send to the AI (omit for interactive mode)
38
+ * @param resumeSessionId - Optional session ID to resume
39
+ * @param config - Provider-specific configuration
40
+ */
41
+ buildSpawnCommand(
42
+ session: Session,
43
+ prompt?: string,
44
+ resumeSessionId?: string,
45
+ config?: Record<string, unknown>
46
+ ): SpawnCommand
47
+
48
+ /**
49
+ * Validate that the provider CLI is installed
50
+ * @returns true if installed and accessible
51
+ */
52
+ validateInstallation(): Promise<boolean>
53
+
54
+ /**
55
+ * Get the installed version of the provider CLI
56
+ * @returns Version string or null if not installed
57
+ */
58
+ getVersion(): Promise<string | null>
59
+ }
60
+
61
+ // ============================================================================
62
+ // Branding Configuration
63
+ // ============================================================================
64
+
65
+ /**
66
+ * UI branding configuration for providers
67
+ *
68
+ * Used for consistent styling in Preview, StatusBar, and other components.
69
+ */
70
+ export interface ProviderBranding {
71
+ /** Full display name (e.g., "Claude Code") */
72
+ displayName: string
73
+ /** Short name for compact displays (e.g., "claude") */
74
+ shortName: string
75
+ /** Primary accent color (hex) for borders and highlights */
76
+ accentColor: string
77
+ /** Secondary color (hex) for backgrounds and subtle elements */
78
+ secondaryColor: string
79
+ /** Optional custom header background color */
80
+ headerBackground?: string
81
+ /** Optional logo text for header display */
82
+ logoText?: string
83
+ /** Optional terminal output background color (undefined = transparent) */
84
+ terminalBackground?: string
85
+ }
86
+
87
+ // ============================================================================
88
+ // Parser Patterns
89
+ // ============================================================================
90
+
91
+ /**
92
+ * Regex patterns for parsing AI output
93
+ *
94
+ * Each provider may have different markers for phase transitions.
95
+ */
96
+ export interface ParserPatterns {
97
+ /** Pattern to detect working/implementation phase */
98
+ workingRegex: RegExp
99
+ /** Pattern to detect completion */
100
+ doneRegex: RegExp
101
+ }
102
+
103
+ // ============================================================================
104
+ // Spawn Command
105
+ // ============================================================================
106
+
107
+ /**
108
+ * Command specification for spawning an AI session
109
+ */
110
+ export interface SpawnCommand {
111
+ /** CLI command to execute (e.g., "opencode", "claude") */
112
+ command: string
113
+ /** Arguments to pass to the command */
114
+ args: string[]
115
+ /** Optional environment variables to set */
116
+ env?: Record<string, string>
117
+ }
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Output buffer database operations
3
+ *
4
+ * Manages circular buffers for session output storage.
5
+ * Each session has a buffer that stores the most recent output lines.
6
+ */
7
+
8
+ import { getDatabase, nowISO } from "./db"
9
+ import type { OutputBuffer } from "./types"
10
+
11
+ // ============================================================================
12
+ // Constants
13
+ // ============================================================================
14
+
15
+ /** Maximum number of lines to store per buffer */
16
+ export const MAX_BUFFER_LINES = 1000
17
+
18
+ // ============================================================================
19
+ // Database Row Type
20
+ // ============================================================================
21
+
22
+ interface BufferRow {
23
+ session_id: string
24
+ lines: string
25
+ last_updated: string
26
+ }
27
+
28
+ // ============================================================================
29
+ // Row Mapping
30
+ // ============================================================================
31
+
32
+ /**
33
+ * Convert database row to OutputBuffer
34
+ */
35
+ function rowToBuffer(row: BufferRow): OutputBuffer {
36
+ let lines: string[]
37
+ try {
38
+ lines = JSON.parse(row.lines)
39
+ if (!Array.isArray(lines)) {
40
+ lines = []
41
+ }
42
+ } catch {
43
+ lines = []
44
+ }
45
+
46
+ return {
47
+ sessionId: row.session_id,
48
+ lines,
49
+ lastUpdated: row.last_updated,
50
+ }
51
+ }
52
+
53
+ // ============================================================================
54
+ // Query Operations
55
+ // ============================================================================
56
+
57
+ /**
58
+ * Get the output buffer for a session
59
+ *
60
+ * @param sessionId - Session UUID
61
+ * @returns OutputBuffer or null if not found
62
+ */
63
+ export function getBuffer(sessionId: string): OutputBuffer | null {
64
+ const db = getDatabase()
65
+ const row = db
66
+ .query<BufferRow, [string]>(
67
+ "SELECT * FROM output_buffers WHERE session_id = ?"
68
+ )
69
+ .get(sessionId)
70
+
71
+ return row ? rowToBuffer(row) : null
72
+ }
73
+
74
+ /**
75
+ * Get recent lines from a session's buffer
76
+ *
77
+ * @param sessionId - Session UUID
78
+ * @param count - Number of recent lines to retrieve
79
+ * @returns Array of lines (may be empty if buffer doesn't exist)
80
+ */
81
+ export function getRecentLines(sessionId: string, count: number): string[] {
82
+ const buffer = getBuffer(sessionId)
83
+ if (!buffer) {
84
+ return []
85
+ }
86
+
87
+ // Return last N lines
88
+ return buffer.lines.slice(-count)
89
+ }
90
+
91
+ /**
92
+ * Get all lines from a session's buffer
93
+ *
94
+ * @param sessionId - Session UUID
95
+ * @returns Array of all lines
96
+ */
97
+ export function getAllLines(sessionId: string): string[] {
98
+ const buffer = getBuffer(sessionId)
99
+ return buffer?.lines ?? []
100
+ }
101
+
102
+ // ============================================================================
103
+ // Create/Update Operations
104
+ // ============================================================================
105
+
106
+ /**
107
+ * Create a new output buffer for a session
108
+ *
109
+ * @param sessionId - Session UUID
110
+ * @returns The created OutputBuffer
111
+ */
112
+ export function createBuffer(sessionId: string): OutputBuffer {
113
+ const db = getDatabase()
114
+ const now = nowISO()
115
+
116
+ db.query(
117
+ "INSERT INTO output_buffers (session_id, lines, last_updated) VALUES (?, '[]', ?)"
118
+ ).run(sessionId, now)
119
+
120
+ return {
121
+ sessionId,
122
+ lines: [],
123
+ lastUpdated: now,
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Ensure a buffer exists for a session (create if needed)
129
+ *
130
+ * @param sessionId - Session UUID
131
+ * @returns The OutputBuffer
132
+ */
133
+ export function ensureBuffer(sessionId: string): OutputBuffer {
134
+ const existing = getBuffer(sessionId)
135
+ if (existing) {
136
+ return existing
137
+ }
138
+ return createBuffer(sessionId)
139
+ }
140
+
141
+ /**
142
+ * Append lines to a session's output buffer
143
+ *
144
+ * Implements circular buffer logic - drops oldest lines when exceeding MAX_BUFFER_LINES.
145
+ *
146
+ * @param sessionId - Session UUID
147
+ * @param newLines - Lines to append
148
+ */
149
+ export function appendLines(sessionId: string, newLines: string[]): void {
150
+ if (newLines.length === 0) {
151
+ return
152
+ }
153
+
154
+ const db = getDatabase()
155
+ const now = nowISO()
156
+
157
+ // Ensure buffer exists
158
+ const buffer = ensureBuffer(sessionId)
159
+
160
+ // Combine existing and new lines
161
+ let allLines = [...buffer.lines, ...newLines]
162
+
163
+ // Apply circular buffer limit
164
+ if (allLines.length > MAX_BUFFER_LINES) {
165
+ allLines = allLines.slice(-MAX_BUFFER_LINES)
166
+ }
167
+
168
+ // Update in database
169
+ db.query(
170
+ "UPDATE output_buffers SET lines = ?, last_updated = ? WHERE session_id = ?"
171
+ ).run(JSON.stringify(allLines), now, sessionId)
172
+ }
173
+
174
+ /**
175
+ * Append a single line to a session's output buffer
176
+ *
177
+ * @param sessionId - Session UUID
178
+ * @param line - Line to append
179
+ */
180
+ export function appendLine(sessionId: string, line: string): void {
181
+ appendLines(sessionId, [line])
182
+ }
183
+
184
+ /**
185
+ * Replace all lines in a buffer
186
+ *
187
+ * @param sessionId - Session UUID
188
+ * @param lines - New lines (will be truncated to MAX_BUFFER_LINES)
189
+ */
190
+ export function setLines(sessionId: string, lines: string[]): void {
191
+ const db = getDatabase()
192
+ const now = nowISO()
193
+
194
+ // Ensure buffer exists
195
+ ensureBuffer(sessionId)
196
+
197
+ // Apply limit
198
+ const truncatedLines = lines.slice(-MAX_BUFFER_LINES)
199
+
200
+ // Update in database
201
+ db.query(
202
+ "UPDATE output_buffers SET lines = ?, last_updated = ? WHERE session_id = ?"
203
+ ).run(JSON.stringify(truncatedLines), now, sessionId)
204
+ }
205
+
206
+ // ============================================================================
207
+ // Delete Operations
208
+ // ============================================================================
209
+
210
+ /**
211
+ * Clear all lines from a session's buffer
212
+ *
213
+ * @param sessionId - Session UUID
214
+ */
215
+ export function clearBuffer(sessionId: string): void {
216
+ const db = getDatabase()
217
+ const now = nowISO()
218
+
219
+ db.query(
220
+ "UPDATE output_buffers SET lines = '[]', last_updated = ? WHERE session_id = ?"
221
+ ).run(now, sessionId)
222
+ }
223
+
224
+ /**
225
+ * Delete a session's buffer
226
+ *
227
+ * Note: This is also done automatically via CASCADE when session is deleted.
228
+ *
229
+ * @param sessionId - Session UUID
230
+ */
231
+ export function deleteBuffer(sessionId: string): void {
232
+ const db = getDatabase()
233
+ db.query("DELETE FROM output_buffers WHERE session_id = ?").run(sessionId)
234
+ }