@aaronshaf/ger 0.1.0 → 0.1.3

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.
@@ -1,167 +0,0 @@
1
- import { Effect, Layer } from 'effect'
2
- import { AiService, AiServiceError, NoAiToolFoundError, AiResponseParseError } from './ai'
3
- import { ConfigService, ConfigServiceLive } from './config'
4
- import { exec } from 'node:child_process'
5
- import { promisify } from 'node:util'
6
-
7
- const execAsync = promisify(exec)
8
-
9
- // Enhanced AI Service that uses configuration
10
- export const AiServiceEnhanced = Layer.effect(
11
- AiService,
12
- Effect.gen(function* () {
13
- const configService = yield* ConfigService
14
-
15
- const detectAiTool = () =>
16
- Effect.gen(function* () {
17
- // First check configured preference
18
- const aiConfig = yield* configService.getAiConfig.pipe(
19
- Effect.orElseSucceed(() => ({ autoDetect: true })),
20
- )
21
-
22
- if ('tool' in aiConfig && aiConfig.tool && !aiConfig.autoDetect) {
23
- // Check if configured tool is available
24
- const result = yield* Effect.tryPromise({
25
- try: () => execAsync(`which ${aiConfig.tool}`),
26
- catch: () => null,
27
- }).pipe(Effect.orElseSucceed(() => null))
28
-
29
- if (result && result.stdout.trim()) {
30
- return aiConfig.tool
31
- }
32
-
33
- // Configured tool not available, fall back to auto-detect
34
- yield* Effect.logWarning(
35
- `Configured AI tool '${aiConfig.tool}' not found, auto-detecting...`,
36
- )
37
- }
38
-
39
- // Auto-detect available tools
40
- const tools =
41
- 'tool' in aiConfig && aiConfig.tool
42
- ? [aiConfig.tool, 'claude', 'llm', 'opencode', 'gemini'].filter(
43
- (v, i, a) => a.indexOf(v) === i,
44
- )
45
- : ['claude', 'llm', 'opencode', 'gemini']
46
-
47
- for (const tool of tools) {
48
- const result = yield* Effect.tryPromise({
49
- try: () => execAsync(`which ${tool}`),
50
- catch: () => null,
51
- }).pipe(Effect.orElseSucceed(() => null))
52
-
53
- if (result && result.stdout.trim()) {
54
- return tool
55
- }
56
- }
57
-
58
- return yield* Effect.fail(
59
- new NoAiToolFoundError({
60
- message: 'No AI tool found. Please install claude, llm, opencode, or gemini CLI.',
61
- }),
62
- )
63
- })
64
-
65
- const extractResponseTag = (output: string) =>
66
- Effect.gen(function* () {
67
- // Extract content between <response> tags
68
- const responseMatch = output.match(/<response>([\s\S]*?)<\/response>/i)
69
-
70
- if (!responseMatch || !responseMatch[1]) {
71
- return yield* Effect.fail(
72
- new AiResponseParseError({
73
- message: 'No <response> tag found in AI output',
74
- rawOutput: output,
75
- }),
76
- )
77
- }
78
-
79
- return responseMatch[1].trim()
80
- })
81
-
82
- const runPrompt = (prompt: string, input: string) =>
83
- Effect.gen(function* () {
84
- const tool = yield* detectAiTool()
85
-
86
- // Prepare the command based on the tool
87
- const fullInput = `${prompt}\n\n${input}`
88
- let command: string
89
-
90
- switch (tool) {
91
- case 'claude':
92
- // Claude CLI uses -p flag for piped input
93
- command = 'claude -p'
94
- break
95
- case 'llm':
96
- // LLM CLI syntax
97
- command = 'llm'
98
- break
99
- case 'opencode':
100
- // Opencode CLI syntax
101
- command = 'opencode'
102
- break
103
- case 'gemini':
104
- // Gemini CLI syntax (adjust as needed)
105
- command = 'gemini'
106
- break
107
- default:
108
- command = tool
109
- }
110
-
111
- // Run the AI tool with the prompt and input
112
- const result = yield* Effect.tryPromise({
113
- try: async () => {
114
- const child = require('node:child_process').spawn(command, {
115
- shell: true,
116
- stdio: ['pipe', 'pipe', 'pipe'],
117
- })
118
-
119
- // Write input to stdin
120
- child.stdin.write(fullInput)
121
- child.stdin.end()
122
-
123
- // Collect output
124
- let stdout = ''
125
- let stderr = ''
126
-
127
- child.stdout.on('data', (data: Buffer) => {
128
- stdout += data.toString()
129
- })
130
-
131
- child.stderr.on('data', (data: Buffer) => {
132
- stderr += data.toString()
133
- })
134
-
135
- return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
136
- child.on('close', (code: number) => {
137
- if (code !== 0) {
138
- reject(new Error(`AI tool exited with code ${code}: ${stderr}`))
139
- } else {
140
- resolve({ stdout, stderr })
141
- }
142
- })
143
-
144
- child.on('error', reject)
145
- })
146
- },
147
- catch: (error) =>
148
- new AiServiceError({
149
- message: `Failed to run AI tool: ${error instanceof Error ? error.message : String(error)}`,
150
- cause: error,
151
- }),
152
- })
153
-
154
- // Extract response tag
155
- return yield* extractResponseTag(result.stdout)
156
- })
157
-
158
- return AiService.of({
159
- detectAiTool,
160
- extractResponseTag,
161
- runPrompt,
162
- })
163
- }),
164
- ).pipe(Layer.provide(ConfigServiceLive))
165
-
166
- // Export a simpler Live layer for backward compatibility
167
- export const AiServiceLive = AiServiceEnhanced
@@ -1,182 +0,0 @@
1
- import { Context, Data, Effect, Layer } from 'effect'
2
- import { exec } from 'node:child_process'
3
- import { promisify } from 'node:util'
4
-
5
- const execAsync = promisify(exec)
6
-
7
- // Error types
8
- export class AiServiceError extends Data.TaggedError('AiServiceError')<{
9
- message: string
10
- cause?: unknown
11
- }> {}
12
-
13
- export class NoAiToolFoundError extends Data.TaggedError('NoAiToolFoundError')<{
14
- message: string
15
- }> {}
16
-
17
- export class AiResponseParseError extends Data.TaggedError('AiResponseParseError')<{
18
- message: string
19
- rawOutput: string
20
- }> {}
21
-
22
- // Service interface
23
- export class AiService extends Context.Tag('AiService')<
24
- AiService,
25
- {
26
- readonly runPrompt: (
27
- prompt: string,
28
- input: string,
29
- ) => Effect.Effect<string, AiServiceError | NoAiToolFoundError | AiResponseParseError>
30
- readonly detectAiTool: () => Effect.Effect<string, NoAiToolFoundError>
31
- readonly extractResponseTag: (output: string) => Effect.Effect<string, AiResponseParseError>
32
- }
33
- >() {}
34
-
35
- // Service implementation
36
- export const AiServiceLive = Layer.succeed(
37
- AiService,
38
- AiService.of({
39
- detectAiTool: () =>
40
- Effect.gen(function* () {
41
- // Try to detect available AI tools in order of preference
42
- const tools = ['claude', 'llm', 'opencode', 'gemini']
43
-
44
- for (const tool of tools) {
45
- const result = yield* Effect.tryPromise({
46
- try: () => execAsync(`which ${tool}`),
47
- catch: () => null,
48
- }).pipe(Effect.orElseSucceed(() => null))
49
-
50
- if (result && result.stdout.trim()) {
51
- return tool
52
- }
53
- }
54
-
55
- return yield* Effect.fail(
56
- new NoAiToolFoundError({
57
- message: 'No AI tool found. Please install claude, llm, opencode, or gemini CLI.',
58
- }),
59
- )
60
- }),
61
-
62
- extractResponseTag: (output: string) =>
63
- Effect.gen(function* () {
64
- // Extract content between <response> tags
65
- const responseMatch = output.match(/<response>([\s\S]*?)<\/response>/i)
66
-
67
- if (!responseMatch || !responseMatch[1]) {
68
- return yield* Effect.fail(
69
- new AiResponseParseError({
70
- message: 'No <response> tag found in AI output',
71
- rawOutput: output,
72
- }),
73
- )
74
- }
75
-
76
- return responseMatch[1].trim()
77
- }),
78
-
79
- runPrompt: (prompt: string, input: string) =>
80
- Effect.gen(function* () {
81
- const tool = yield* Effect.gen(function* () {
82
- // Try to detect available AI tools in order of preference
83
- const tools = ['claude', 'llm', 'opencode', 'gemini']
84
-
85
- for (const tool of tools) {
86
- const result = yield* Effect.tryPromise({
87
- try: () => execAsync(`which ${tool}`),
88
- catch: () => null,
89
- }).pipe(Effect.orElseSucceed(() => null))
90
-
91
- if (result && result.stdout.trim()) {
92
- return tool
93
- }
94
- }
95
-
96
- return yield* Effect.fail(
97
- new NoAiToolFoundError({
98
- message: 'No AI tool found. Please install claude, llm, opencode, or gemini CLI.',
99
- }),
100
- )
101
- })
102
-
103
- // Prepare the command based on the tool
104
- const fullInput = `${prompt}\n\n${input}`
105
- let command: string
106
-
107
- switch (tool) {
108
- case 'claude':
109
- // Claude CLI uses -p flag for piped input
110
- command = 'claude -p'
111
- break
112
- case 'llm':
113
- // LLM CLI syntax
114
- command = 'llm'
115
- break
116
- case 'opencode':
117
- // Opencode CLI syntax
118
- command = 'opencode'
119
- break
120
- default:
121
- command = tool
122
- }
123
-
124
- // Run the AI tool with the prompt and input
125
- const result = yield* Effect.tryPromise({
126
- try: async () => {
127
- const child = require('node:child_process').spawn(command, {
128
- shell: true,
129
- stdio: ['pipe', 'pipe', 'pipe'],
130
- })
131
-
132
- // Write input to stdin
133
- child.stdin.write(fullInput)
134
- child.stdin.end()
135
-
136
- // Collect output
137
- let stdout = ''
138
- let stderr = ''
139
-
140
- child.stdout.on('data', (data: Buffer) => {
141
- stdout += data.toString()
142
- })
143
-
144
- child.stderr.on('data', (data: Buffer) => {
145
- stderr += data.toString()
146
- })
147
-
148
- return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
149
- child.on('close', (code: number) => {
150
- if (code !== 0) {
151
- reject(new Error(`AI tool exited with code ${code}: ${stderr}`))
152
- } else {
153
- resolve({ stdout, stderr })
154
- }
155
- })
156
-
157
- child.on('error', reject)
158
- })
159
- },
160
- catch: (error) =>
161
- new AiServiceError({
162
- message: `Failed to run AI tool: ${error instanceof Error ? error.message : String(error)}`,
163
- cause: error,
164
- }),
165
- })
166
-
167
- // Extract response tag
168
- const responseMatch = result.stdout.match(/<response>([\s\S]*?)<\/response>/i)
169
-
170
- if (!responseMatch || !responseMatch[1]) {
171
- return yield* Effect.fail(
172
- new AiResponseParseError({
173
- message: 'No <response> tag found in AI output',
174
- rawOutput: result.stdout,
175
- }),
176
- )
177
- }
178
-
179
- return responseMatch[1].trim()
180
- }),
181
- }),
182
- )