@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.
- package/AGENTS.md +203 -0
- package/CLAUDE.md +203 -0
- package/README.md +166 -0
- package/bun.lock +447 -0
- package/bunfig.toml +4 -0
- package/package.json +42 -0
- package/src/app.tsx +84 -0
- package/src/components/App.tsx +526 -0
- package/src/components/ConfirmDialog.tsx +88 -0
- package/src/components/Footer.tsx +50 -0
- package/src/components/HelpModal.tsx +136 -0
- package/src/components/IssueSelectorModal.tsx +701 -0
- package/src/components/ManualSessionModal.tsx +191 -0
- package/src/components/PhaseProgress.tsx +45 -0
- package/src/components/Preview.tsx +249 -0
- package/src/components/ProviderSwitcherModal.tsx +156 -0
- package/src/components/ScrollableText.tsx +120 -0
- package/src/components/SessionCard.tsx +60 -0
- package/src/components/SessionList.tsx +79 -0
- package/src/components/SessionTerminal.tsx +89 -0
- package/src/components/StatusBar.tsx +84 -0
- package/src/components/ThemeSwitcherModal.tsx +237 -0
- package/src/components/index.ts +58 -0
- package/src/components/session-utils.ts +337 -0
- package/src/components/theme.ts +206 -0
- package/src/components/types.ts +215 -0
- package/src/config/defaults.ts +44 -0
- package/src/config/env.ts +67 -0
- package/src/config/global.ts +252 -0
- package/src/config/index.ts +171 -0
- package/src/config/types.ts +131 -0
- package/src/core/.gitkeep +0 -0
- package/src/core/index.ts +5 -0
- package/src/core/parser.ts +62 -0
- package/src/core/process-manager.ts +52 -0
- package/src/core/session.ts +423 -0
- package/src/core/tmux.ts +206 -0
- package/src/git/.gitkeep +0 -0
- package/src/git/index.ts +8 -0
- package/src/git/repo.ts +443 -0
- package/src/git/worktree.ts +317 -0
- package/src/github/.gitkeep +0 -0
- package/src/github/client.ts +208 -0
- package/src/github/index.ts +8 -0
- package/src/github/issues.ts +351 -0
- package/src/index.ts +369 -0
- package/src/prompts/.gitkeep +0 -0
- package/src/prompts/index.ts +1 -0
- package/src/prompts/swe-system.ts +22 -0
- package/src/providers/claude.ts +103 -0
- package/src/providers/index.ts +21 -0
- package/src/providers/opencode.ts +98 -0
- package/src/providers/registry.ts +53 -0
- package/src/providers/types.ts +117 -0
- package/src/store/buffers.ts +234 -0
- package/src/store/db.test.ts +579 -0
- package/src/store/db.ts +249 -0
- package/src/store/index.ts +101 -0
- package/src/store/project.ts +119 -0
- package/src/store/schema.sql +71 -0
- package/src/store/sessions.ts +454 -0
- package/src/store/types.ts +194 -0
- package/src/theme/context.tsx +170 -0
- package/src/theme/custom.ts +134 -0
- package/src/theme/index.ts +58 -0
- package/src/theme/loader.ts +264 -0
- package/src/theme/themes/aura.json +69 -0
- package/src/theme/themes/ayu.json +80 -0
- package/src/theme/themes/carbonfox.json +248 -0
- package/src/theme/themes/catppuccin-frappe.json +233 -0
- package/src/theme/themes/catppuccin-macchiato.json +233 -0
- package/src/theme/themes/catppuccin.json +112 -0
- package/src/theme/themes/cobalt2.json +228 -0
- package/src/theme/themes/cursor.json +249 -0
- package/src/theme/themes/dracula.json +219 -0
- package/src/theme/themes/everforest.json +241 -0
- package/src/theme/themes/flexoki.json +237 -0
- package/src/theme/themes/github.json +233 -0
- package/src/theme/themes/gruvbox.json +242 -0
- package/src/theme/themes/kanagawa.json +77 -0
- package/src/theme/themes/lucent-orng.json +237 -0
- package/src/theme/themes/material.json +235 -0
- package/src/theme/themes/matrix.json +77 -0
- package/src/theme/themes/mercury.json +252 -0
- package/src/theme/themes/monokai.json +221 -0
- package/src/theme/themes/nightowl.json +221 -0
- package/src/theme/themes/nord.json +223 -0
- package/src/theme/themes/one-dark.json +84 -0
- package/src/theme/themes/opencode.json +245 -0
- package/src/theme/themes/orng.json +249 -0
- package/src/theme/themes/osaka-jade.json +93 -0
- package/src/theme/themes/palenight.json +222 -0
- package/src/theme/themes/rosepine.json +234 -0
- package/src/theme/themes/solarized.json +223 -0
- package/src/theme/themes/synthwave84.json +226 -0
- package/src/theme/themes/tokyonight.json +243 -0
- package/src/theme/themes/vercel.json +245 -0
- package/src/theme/themes/vesper.json +218 -0
- package/src/theme/themes/zenburn.json +223 -0
- package/src/theme/types.ts +225 -0
- package/src/types/sql.d.ts +4 -0
- package/src/utils/ansi-parser.ts +225 -0
- package/src/utils/format.ts +46 -0
- package/src/utils/id.ts +15 -0
- package/src/utils/logger.ts +112 -0
- package/src/utils/prerequisites.ts +118 -0
- package/src/utils/shell.ts +9 -0
- package/src/wizard/flows.ts +419 -0
- package/src/wizard/index.ts +37 -0
- package/src/wizard/prompts.ts +190 -0
- package/src/workspace/detect.test.ts +51 -0
- package/src/workspace/detect.ts +223 -0
- package/src/workspace/index.ts +71 -0
- package/src/workspace/init.ts +131 -0
- package/src/workspace/paths.ts +143 -0
- package/src/workspace/project.ts +164 -0
- 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
|
+
}
|