@llm4ts/runner 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/LICENSE +21 -0
- package/README.md +61 -0
- package/dist/Cli.d.ts +5 -0
- package/dist/Cli.d.ts.map +1 -0
- package/dist/Cli.js +36 -0
- package/dist/Cli.js.map +1 -0
- package/dist/CoderPolicy.d.ts +12 -0
- package/dist/CoderPolicy.d.ts.map +1 -0
- package/dist/CoderPolicy.js +59 -0
- package/dist/CoderPolicy.js.map +1 -0
- package/dist/Connectors.d.ts +28 -0
- package/dist/Connectors.d.ts.map +1 -0
- package/dist/Connectors.js +165 -0
- package/dist/Connectors.js.map +1 -0
- package/dist/Doctor.d.ts +4 -0
- package/dist/Doctor.d.ts.map +1 -0
- package/dist/Doctor.js +33 -0
- package/dist/Doctor.js.map +1 -0
- package/dist/FlowArgs.d.ts +34 -0
- package/dist/FlowArgs.d.ts.map +1 -0
- package/dist/FlowArgs.js +136 -0
- package/dist/FlowArgs.js.map +1 -0
- package/dist/FlowRunner.d.ts +48 -0
- package/dist/FlowRunner.d.ts.map +1 -0
- package/dist/FlowRunner.js +97 -0
- package/dist/FlowRunner.js.map +1 -0
- package/dist/McpStdio.d.ts +7 -0
- package/dist/McpStdio.d.ts.map +1 -0
- package/dist/McpStdio.js +19 -0
- package/dist/McpStdio.js.map +1 -0
- package/dist/NodeGeminiCliExecutor.d.ts +7 -0
- package/dist/NodeGeminiCliExecutor.d.ts.map +1 -0
- package/dist/NodeGeminiCliExecutor.js +26 -0
- package/dist/NodeGeminiCliExecutor.js.map +1 -0
- package/dist/NodeHttpClient.d.ts +5 -0
- package/dist/NodeHttpClient.d.ts.map +1 -0
- package/dist/NodeHttpClient.js +109 -0
- package/dist/NodeHttpClient.js.map +1 -0
- package/dist/NodePlainFileStore.d.ts +5 -0
- package/dist/NodePlainFileStore.d.ts.map +1 -0
- package/dist/NodePlainFileStore.js +64 -0
- package/dist/NodePlainFileStore.js.map +1 -0
- package/dist/NodeProcessExecutor.d.ts +5 -0
- package/dist/NodeProcessExecutor.d.ts.map +1 -0
- package/dist/NodeProcessExecutor.js +212 -0
- package/dist/NodeProcessExecutor.js.map +1 -0
- package/dist/NodeRuntime.d.ts +4 -0
- package/dist/NodeRuntime.d.ts.map +1 -0
- package/dist/NodeRuntime.js +11 -0
- package/dist/NodeRuntime.js.map +1 -0
- package/dist/NodeTemporaryFiles.d.ts +5 -0
- package/dist/NodeTemporaryFiles.d.ts.map +1 -0
- package/dist/NodeTemporaryFiles.js +30 -0
- package/dist/NodeTemporaryFiles.js.map +1 -0
- package/dist/NodeWorkspace.d.ts +6 -0
- package/dist/NodeWorkspace.d.ts.map +1 -0
- package/dist/NodeWorkspace.js +231 -0
- package/dist/NodeWorkspace.js.map +1 -0
- package/dist/Package.d.ts +2 -0
- package/dist/Package.d.ts.map +1 -0
- package/dist/Package.js +2 -0
- package/dist/Package.js.map +1 -0
- package/dist/ReviewFingerprint.d.ts +2 -0
- package/dist/ReviewFingerprint.d.ts.map +1 -0
- package/dist/ReviewFingerprint.js +13 -0
- package/dist/ReviewFingerprint.js.map +1 -0
- package/dist/Terminal.d.ts +46 -0
- package/dist/Terminal.d.ts.map +1 -0
- package/dist/Terminal.js +226 -0
- package/dist/Terminal.js.map +1 -0
- package/dist/TerminalInteraction.d.ts +4 -0
- package/dist/TerminalInteraction.d.ts.map +1 -0
- package/dist/TerminalInteraction.js +25 -0
- package/dist/TerminalInteraction.js.map +1 -0
- package/dist/cli-main.d.ts +3 -0
- package/dist/cli-main.d.ts.map +1 -0
- package/dist/cli-main.js +39 -0
- package/dist/cli-main.js.map +1 -0
- package/package.json +74 -0
- package/src/Cli.ts +51 -0
- package/src/CoderPolicy.ts +82 -0
- package/src/Connectors.ts +225 -0
- package/src/Doctor.ts +44 -0
- package/src/FlowArgs.ts +167 -0
- package/src/FlowRunner.ts +220 -0
- package/src/McpStdio.ts +41 -0
- package/src/NodeGeminiCliExecutor.ts +56 -0
- package/src/NodeHttpClient.ts +224 -0
- package/src/NodePlainFileStore.ts +75 -0
- package/src/NodeProcessExecutor.ts +331 -0
- package/src/NodeRuntime.ts +19 -0
- package/src/NodeTemporaryFiles.ts +46 -0
- package/src/NodeWorkspace.ts +282 -0
- package/src/Package.ts +1 -0
- package/src/ReviewFingerprint.ts +13 -0
- package/src/Terminal.ts +329 -0
- package/src/TerminalInteraction.ts +43 -0
- package/src/cli-main.ts +53 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import * as Duration from "effect/Duration"
|
|
2
|
+
import * as Effect from "effect/Effect"
|
|
3
|
+
import * as Redacted from "effect/Redacted"
|
|
4
|
+
import {
|
|
5
|
+
ApiConnectorConfig,
|
|
6
|
+
CliConnectorConfig,
|
|
7
|
+
type ConnectorConfig
|
|
8
|
+
} from "@llm4ts/core/ConnectorConfig"
|
|
9
|
+
import { ConnectorIds, connectorDefaultBaseUrl } from "@llm4ts/core/Models"
|
|
10
|
+
import { ScriptUsage } from "./FlowArgs.ts"
|
|
11
|
+
|
|
12
|
+
export const claude = CliConnectorConfig.make({
|
|
13
|
+
connectorId: ConnectorIds.ClaudeCli,
|
|
14
|
+
flags: {
|
|
15
|
+
"permission-mode": "acceptEdits"
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
export const codex = CliConnectorConfig.make({
|
|
20
|
+
connectorId: ConnectorIds.Codex,
|
|
21
|
+
flags: {
|
|
22
|
+
sandbox: "workspace-write"
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export const gemini = CliConnectorConfig.make({
|
|
27
|
+
connectorId: ConnectorIds.GeminiCli
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
export const pi = CliConnectorConfig.make({
|
|
31
|
+
connectorId: ConnectorIds.Pi
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
export const antigravity = CliConnectorConfig.make({
|
|
35
|
+
connectorId: ConnectorIds.AntigravityCli,
|
|
36
|
+
flags: {
|
|
37
|
+
mode: "accept-edits"
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
export const grok = CliConnectorConfig.make({
|
|
42
|
+
connectorId: ConnectorIds.Grok
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
export const cursor = CliConnectorConfig.make({
|
|
46
|
+
connectorId: ConnectorIds.Cursor
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
export const opencode = CliConnectorConfig.make({
|
|
50
|
+
connectorId: ConnectorIds.OpenCode
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
export const lmStudio = ApiConnectorConfig.make({
|
|
54
|
+
connectorId: ConnectorIds.LmStudio,
|
|
55
|
+
baseUrl: "http://localhost:1234/v1"
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
export const openAI = ApiConnectorConfig.make({
|
|
59
|
+
connectorId: ConnectorIds.OpenAI
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
export const anthropic = ApiConnectorConfig.make({
|
|
63
|
+
connectorId: ConnectorIds.Anthropic
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
export const geminiApi = ApiConnectorConfig.make({
|
|
67
|
+
connectorId: ConnectorIds.GeminiApi
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
export const ollama = ApiConnectorConfig.make({
|
|
71
|
+
connectorId: ConnectorIds.Ollama
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
export const mock = ApiConnectorConfig.make({
|
|
75
|
+
connectorId: ConnectorIds.Mock,
|
|
76
|
+
model: "mock"
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
export function withModel(config: CliConnectorConfig, model: string): CliConnectorConfig
|
|
80
|
+
export function withModel(config: ApiConnectorConfig, model: string): ApiConnectorConfig
|
|
81
|
+
export function withModel(
|
|
82
|
+
config: CliConnectorConfig | ApiConnectorConfig,
|
|
83
|
+
model: string
|
|
84
|
+
): CliConnectorConfig | ApiConnectorConfig {
|
|
85
|
+
return config instanceof CliConnectorConfig
|
|
86
|
+
? CliConnectorConfig.make({ ...config, model })
|
|
87
|
+
: ApiConnectorConfig.make({ ...config, model })
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const withTurnLimit = (config: CliConnectorConfig, turnLimit: number): CliConnectorConfig =>
|
|
91
|
+
CliConnectorConfig.make({
|
|
92
|
+
...config,
|
|
93
|
+
turnLimit
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
export const withEnvironment = (
|
|
97
|
+
config: CliConnectorConfig,
|
|
98
|
+
environment: Readonly<Record<string, string>>
|
|
99
|
+
): CliConnectorConfig =>
|
|
100
|
+
CliConnectorConfig.make({
|
|
101
|
+
...config,
|
|
102
|
+
envVars: {
|
|
103
|
+
...config.envVars,
|
|
104
|
+
...environment
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
export const asReadOnly = (config: CliConnectorConfig): CliConnectorConfig =>
|
|
109
|
+
CliConnectorConfig.make({ ...config, readOnly: true })
|
|
110
|
+
|
|
111
|
+
export const withTimeoutSeconds = (
|
|
112
|
+
config: ApiConnectorConfig,
|
|
113
|
+
seconds: number
|
|
114
|
+
): ApiConnectorConfig =>
|
|
115
|
+
ApiConnectorConfig.make({
|
|
116
|
+
...config,
|
|
117
|
+
timeout: Duration.seconds(seconds)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
const environmentApiKey = (
|
|
121
|
+
config: ApiConnectorConfig,
|
|
122
|
+
environment: Readonly<Record<string, string | undefined>>
|
|
123
|
+
): string | undefined => {
|
|
124
|
+
switch (config.connectorId.value) {
|
|
125
|
+
case "openai":
|
|
126
|
+
return environment.OPENAI_API_KEY
|
|
127
|
+
case "anthropic":
|
|
128
|
+
return environment.ANTHROPIC_API_KEY
|
|
129
|
+
case "gemini-api":
|
|
130
|
+
return environment.GEMINI_API_KEY ?? environment.GOOGLE_API_KEY
|
|
131
|
+
default:
|
|
132
|
+
return undefined
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export const enrichApiConnector = (
|
|
137
|
+
config: ApiConnectorConfig,
|
|
138
|
+
environment: Readonly<Record<string, string | undefined>> = process.env
|
|
139
|
+
): ApiConnectorConfig => {
|
|
140
|
+
const baseUrl = config.baseUrl ?? connectorDefaultBaseUrl(config.connectorId)
|
|
141
|
+
const environmentKey = environmentApiKey(config, environment)
|
|
142
|
+
return ApiConnectorConfig.make({
|
|
143
|
+
...config,
|
|
144
|
+
...(baseUrl === undefined ? {} : { baseUrl }),
|
|
145
|
+
...(config.apiKey !== undefined
|
|
146
|
+
? { apiKey: config.apiKey }
|
|
147
|
+
: environmentKey === undefined
|
|
148
|
+
? {}
|
|
149
|
+
: { apiKey: Redacted.make(environmentKey) })
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export const prepareConnector = (
|
|
154
|
+
config: ConnectorConfig,
|
|
155
|
+
workDir: string,
|
|
156
|
+
environment: Readonly<Record<string, string | undefined>> = process.env
|
|
157
|
+
): ConnectorConfig =>
|
|
158
|
+
config instanceof ApiConnectorConfig
|
|
159
|
+
? enrichApiConnector(config, environment)
|
|
160
|
+
: CliConnectorConfig.make({ ...config, workingDir: workDir })
|
|
161
|
+
|
|
162
|
+
export const apiConnectorFromEnvironment = Effect.fn(
|
|
163
|
+
"@llm4ts/runner/Connectors.apiConnectorFromEnvironment"
|
|
164
|
+
)(function* (
|
|
165
|
+
environment: Readonly<Record<string, string | undefined>> = process.env
|
|
166
|
+
): Effect.fn.Return<ApiConnectorConfig, ScriptUsage> {
|
|
167
|
+
const provider = environment.LLM4TS_PROVIDER?.trim().toLowerCase() ?? "mock"
|
|
168
|
+
const preset = (() => {
|
|
169
|
+
switch (provider) {
|
|
170
|
+
case "mock":
|
|
171
|
+
return mock
|
|
172
|
+
case "openai":
|
|
173
|
+
return openAI
|
|
174
|
+
case "anthropic":
|
|
175
|
+
return anthropic
|
|
176
|
+
case "gemini":
|
|
177
|
+
case "gemini-api":
|
|
178
|
+
return geminiApi
|
|
179
|
+
case "lm-studio":
|
|
180
|
+
case "lmstudio":
|
|
181
|
+
return lmStudio
|
|
182
|
+
case "ollama":
|
|
183
|
+
return ollama
|
|
184
|
+
default:
|
|
185
|
+
return undefined
|
|
186
|
+
}
|
|
187
|
+
})()
|
|
188
|
+
if (preset === undefined) {
|
|
189
|
+
return yield* ScriptUsage.make({
|
|
190
|
+
message:
|
|
191
|
+
`unknown LLM4TS_PROVIDER '${provider}'; expected ` +
|
|
192
|
+
"mock|openai|anthropic|gemini|lm-studio|ollama"
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
const model = environment.LLM4TS_MODEL?.trim()
|
|
196
|
+
if (provider !== "mock" && (model === undefined || model.length === 0)) {
|
|
197
|
+
return yield* ScriptUsage.make({
|
|
198
|
+
message: `LLM4TS_MODEL is required for provider '${provider}' (e.g. LLM4TS_MODEL=gpt-4o-mini)`
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
return model === undefined || model.length === 0 ? preset : withModel(preset, model)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
export const coderFromEnv = (
|
|
205
|
+
env: Readonly<Record<string, string | undefined>> = process.env
|
|
206
|
+
): CliConnectorConfig => {
|
|
207
|
+
switch (env.LLM4TS_CODER ?? env.LLM4ZIO_CODER ?? "claude") {
|
|
208
|
+
case "codex":
|
|
209
|
+
return codex
|
|
210
|
+
case "gemini":
|
|
211
|
+
return gemini
|
|
212
|
+
case "pi":
|
|
213
|
+
return pi
|
|
214
|
+
case "agy":
|
|
215
|
+
return antigravity
|
|
216
|
+
case "grok":
|
|
217
|
+
return grok
|
|
218
|
+
case "cursor":
|
|
219
|
+
return cursor
|
|
220
|
+
case "opencode":
|
|
221
|
+
return opencode
|
|
222
|
+
default:
|
|
223
|
+
return claude
|
|
224
|
+
}
|
|
225
|
+
}
|
package/src/Doctor.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect"
|
|
2
|
+
import type { HealthStatus } from "@llm4ts/core/Models"
|
|
3
|
+
import type { ConnectorRegistryShape } from "@llm4ts/core/ConnectorRegistry"
|
|
4
|
+
import { nodeFlowRunnerDependencies } from "./FlowRunner.ts"
|
|
5
|
+
|
|
6
|
+
const credentialKeys = [
|
|
7
|
+
"OPENAI_API_KEY",
|
|
8
|
+
"ANTHROPIC_API_KEY",
|
|
9
|
+
"GEMINI_API_KEY",
|
|
10
|
+
"GOOGLE_API_KEY"
|
|
11
|
+
] as const
|
|
12
|
+
|
|
13
|
+
const statusLine = (id: string, status: HealthStatus): string => {
|
|
14
|
+
const mark =
|
|
15
|
+
status.availability === "Healthy" ? "✔" : status.availability === "Unknown" ? "?" : "✖"
|
|
16
|
+
const auth = status.authStatus === "Unknown" ? "" : ` auth: ${status.authStatus.toLowerCase()}`
|
|
17
|
+
return ` ${mark} ${id.padEnd(14)} ${status.availability.toLowerCase()}${auth}`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const makeDoctorProgram = (
|
|
21
|
+
registry: ConnectorRegistryShape = nodeFlowRunnerDependencies().registry,
|
|
22
|
+
environment: Readonly<Record<string, string | undefined>> = process.env
|
|
23
|
+
): Effect.Effect<string> =>
|
|
24
|
+
Effect.map(registry.healthCheckAll, (statuses) => {
|
|
25
|
+
const lines: Array<string> = []
|
|
26
|
+
lines.push("llm4ts doctor")
|
|
27
|
+
lines.push("")
|
|
28
|
+
lines.push("connectors:")
|
|
29
|
+
const sorted = Array.from(statuses.entries()).sort(([a], [b]) => a.value.localeCompare(b.value))
|
|
30
|
+
for (const [id, status] of sorted) {
|
|
31
|
+
lines.push(statusLine(id.value, status))
|
|
32
|
+
}
|
|
33
|
+
lines.push("")
|
|
34
|
+
lines.push("credentials:")
|
|
35
|
+
for (const key of credentialKeys) {
|
|
36
|
+
const value = environment[key]
|
|
37
|
+
lines.push(` ${value === undefined || value.length === 0 ? "✖" : "✔"} ${key}`)
|
|
38
|
+
}
|
|
39
|
+
lines.push("")
|
|
40
|
+
lines.push(
|
|
41
|
+
`coder: ${environment.LLM4TS_CODER ?? environment.LLM4ZIO_CODER ?? "claude (default)"}`
|
|
42
|
+
)
|
|
43
|
+
return `${lines.join("\n")}\n`
|
|
44
|
+
})
|
package/src/FlowArgs.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises"
|
|
2
|
+
import { resolve } from "node:path"
|
|
3
|
+
import * as Effect from "effect/Effect"
|
|
4
|
+
import * as Schema from "effect/Schema"
|
|
5
|
+
|
|
6
|
+
export interface FlowArgs {
|
|
7
|
+
readonly promptText?: string
|
|
8
|
+
readonly promptFile?: string
|
|
9
|
+
readonly repo?: string
|
|
10
|
+
readonly help?: boolean
|
|
11
|
+
readonly version?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const FlowUsage =
|
|
15
|
+
'usage: llm4ts ["<prompt>" | --prompt-file <path> | @<path>] [--repo <path>] | llm4ts doctor'
|
|
16
|
+
|
|
17
|
+
export const FlowHelp = `${FlowUsage}
|
|
18
|
+
|
|
19
|
+
Stream one prompt to your configured coding agent and print the response.
|
|
20
|
+
|
|
21
|
+
arguments:
|
|
22
|
+
"<prompt>" prompt text
|
|
23
|
+
@<path> read the prompt from a file
|
|
24
|
+
--prompt-file <path> read the prompt from a file
|
|
25
|
+
--repo, -C <path> run against this repository (default: current directory)
|
|
26
|
+
-h, --help show this help
|
|
27
|
+
--version print the version
|
|
28
|
+
|
|
29
|
+
commands:
|
|
30
|
+
doctor check connector availability and credentials
|
|
31
|
+
|
|
32
|
+
environment:
|
|
33
|
+
LLM4TS_CODER coding agent: claude|codex|gemini|pi|agy|grok|cursor|opencode (default: claude)
|
|
34
|
+
LLM4TS_VERBOSITY terminal verbosity: quiet|normal|verbose
|
|
35
|
+
OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY
|
|
36
|
+
credentials for API providers
|
|
37
|
+
|
|
38
|
+
A trace of each run is written under .llm4ts/ in the target repository.
|
|
39
|
+
`
|
|
40
|
+
|
|
41
|
+
export class ScriptUsage extends Schema.TaggedErrorClass<ScriptUsage>()("ScriptUsage", {
|
|
42
|
+
message: Schema.String
|
|
43
|
+
}) {}
|
|
44
|
+
|
|
45
|
+
export type FlowArgsParseResult =
|
|
46
|
+
| { readonly ok: true; readonly value: FlowArgs }
|
|
47
|
+
| { readonly ok: false; readonly error: ScriptUsage }
|
|
48
|
+
|
|
49
|
+
export const parseFlowArgs = (args: ReadonlyArray<string>): FlowArgsParseResult => {
|
|
50
|
+
let parsed: FlowArgs = {}
|
|
51
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
52
|
+
const argument = args[index] ?? ""
|
|
53
|
+
if (argument === "--") {
|
|
54
|
+
continue
|
|
55
|
+
} else if (argument === "--help" || argument === "-h") {
|
|
56
|
+
parsed = { ...parsed, help: true }
|
|
57
|
+
} else if (argument === "--version") {
|
|
58
|
+
parsed = { ...parsed, version: true }
|
|
59
|
+
} else if (argument === "--prompt-file") {
|
|
60
|
+
const value = args[index + 1]
|
|
61
|
+
if (value === undefined) {
|
|
62
|
+
return {
|
|
63
|
+
ok: false,
|
|
64
|
+
error: ScriptUsage.make({
|
|
65
|
+
message: `--prompt-file requires a path\n${FlowUsage}`
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
parsed = { ...parsed, promptFile: value }
|
|
70
|
+
index += 1
|
|
71
|
+
} else if (argument.startsWith("--prompt-file=")) {
|
|
72
|
+
parsed = {
|
|
73
|
+
...parsed,
|
|
74
|
+
promptFile: argument.slice("--prompt-file=".length)
|
|
75
|
+
}
|
|
76
|
+
} else if (argument === "--repo" || argument === "-C") {
|
|
77
|
+
const value = args[index + 1]
|
|
78
|
+
if (value === undefined) {
|
|
79
|
+
return {
|
|
80
|
+
ok: false,
|
|
81
|
+
error: ScriptUsage.make({
|
|
82
|
+
message: `${argument} requires a path\n${FlowUsage}`
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
parsed = { ...parsed, repo: value }
|
|
87
|
+
index += 1
|
|
88
|
+
} else if (argument.startsWith("--repo=")) {
|
|
89
|
+
parsed = { ...parsed, repo: argument.slice("--repo=".length) }
|
|
90
|
+
} else if (argument.startsWith("--")) {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
error: ScriptUsage.make({
|
|
94
|
+
message: `unknown flag: ${argument}\n${FlowUsage}`
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
} else if (argument.startsWith("@") && argument.length > 1 && parsed.promptFile === undefined) {
|
|
98
|
+
parsed = { ...parsed, promptFile: argument.slice(1) }
|
|
99
|
+
} else if (parsed.promptText === undefined && argument.trim().length > 0) {
|
|
100
|
+
parsed = { ...parsed, promptText: argument.trim() }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { ok: true, value: parsed }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const readFlowPrompt = (
|
|
107
|
+
args: FlowArgs,
|
|
108
|
+
defaultPrompt?: string
|
|
109
|
+
): Effect.Effect<string, ScriptUsage> => {
|
|
110
|
+
if (args.promptFile !== undefined) {
|
|
111
|
+
return Effect.tryPromise({
|
|
112
|
+
try: () => readFile(args.promptFile ?? "", "utf8"),
|
|
113
|
+
catch: (error) =>
|
|
114
|
+
ScriptUsage.make({
|
|
115
|
+
message: `could not read prompt file ${args.promptFile}: ${
|
|
116
|
+
error instanceof Error ? error.message : String(error)
|
|
117
|
+
}`
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
const prompt = args.promptText ?? defaultPrompt
|
|
122
|
+
return prompt === undefined
|
|
123
|
+
? Effect.fail(ScriptUsage.make({ message: FlowUsage }))
|
|
124
|
+
: Effect.succeed(prompt)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const resolveFlowRepo = (
|
|
128
|
+
args: FlowArgs,
|
|
129
|
+
workspace: string
|
|
130
|
+
): Effect.Effect<string, ScriptUsage> => {
|
|
131
|
+
const path = resolve(workspace, args.repo ?? ".")
|
|
132
|
+
return Effect.tryPromise({
|
|
133
|
+
try: async () => {
|
|
134
|
+
const information = await stat(path)
|
|
135
|
+
if (!information.isDirectory()) {
|
|
136
|
+
throw new Error("not a directory")
|
|
137
|
+
}
|
|
138
|
+
return path
|
|
139
|
+
},
|
|
140
|
+
catch: (error) =>
|
|
141
|
+
ScriptUsage.make({
|
|
142
|
+
message: `--repo is not a directory: ${path}${
|
|
143
|
+
error instanceof Error ? ` (${error.message})` : ""
|
|
144
|
+
}`
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface FlowInput {
|
|
150
|
+
readonly prompt: string
|
|
151
|
+
readonly workDir: string
|
|
152
|
+
readonly workspace: string
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export const resolveFlowInput = Effect.fn("@llm4ts/runner/FlowArgs.resolveFlowInput")(function* (
|
|
156
|
+
defaultPrompt?: string,
|
|
157
|
+
argv: ReadonlyArray<string> = process.argv.slice(2),
|
|
158
|
+
workspace = process.cwd()
|
|
159
|
+
): Effect.fn.Return<FlowInput, ScriptUsage> {
|
|
160
|
+
const parsed = parseFlowArgs(argv)
|
|
161
|
+
if (!parsed.ok) {
|
|
162
|
+
return yield* parsed.error
|
|
163
|
+
}
|
|
164
|
+
const prompt = yield* readFlowPrompt(parsed.value, defaultPrompt)
|
|
165
|
+
const workDir = yield* resolveFlowRepo(parsed.value, workspace)
|
|
166
|
+
return { prompt, workDir, workspace }
|
|
167
|
+
})
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import * as Clock from "effect/Clock"
|
|
2
|
+
import * as Effect from "effect/Effect"
|
|
3
|
+
import type * as Scope from "effect/Scope"
|
|
4
|
+
import type { ConnectorConfig } from "@llm4ts/core/ConnectorConfig"
|
|
5
|
+
import { defaultReasoningConfig } from "@llm4ts/core/ConnectorConfig"
|
|
6
|
+
import type { ConnectorRegistryShape } from "@llm4ts/core/ConnectorRegistry"
|
|
7
|
+
import type { HttpClientShape } from "@llm4ts/core/HttpClient"
|
|
8
|
+
import type { ProcessExecutorShape } from "@llm4ts/core/ProcessExecutor"
|
|
9
|
+
import type { TemporaryFilesShape } from "@llm4ts/core/TemporaryFiles"
|
|
10
|
+
import type { GeminiCliExecutorShape } from "@llm4ts/core/providers/GeminiCliProvider"
|
|
11
|
+
import { createConnectorRegistry } from "@llm4ts/core/providers/ConnectorFactories"
|
|
12
|
+
import { makeCostTracker } from "@llm4ts/flow/CostTracker"
|
|
13
|
+
import { FlowLlmError, type FlowError } from "@llm4ts/flow/FlowError"
|
|
14
|
+
import { makeFlowEventHub, type FlowEventHub } from "@llm4ts/flow/FlowEvents"
|
|
15
|
+
import { FlowContext, type FlowContextShape } from "@llm4ts/flow/FlowContext"
|
|
16
|
+
import { makeFlowRecorder } from "@llm4ts/flow/FlowRecorder"
|
|
17
|
+
import { makeGitHubTool } from "@llm4ts/flow/GitHubTool"
|
|
18
|
+
import { makeGitTool } from "@llm4ts/flow/GitTool"
|
|
19
|
+
import type { PlainFileStoreShape } from "@llm4ts/flow/Persistence"
|
|
20
|
+
import { nodeHttpClient } from "./NodeHttpClient.ts"
|
|
21
|
+
import { nodeGeminiCliExecutor } from "./NodeGeminiCliExecutor.ts"
|
|
22
|
+
import { nodePlainFileStore } from "./NodePlainFileStore.ts"
|
|
23
|
+
import { nodeProcessExecutor } from "./NodeProcessExecutor.ts"
|
|
24
|
+
import { nodeTemporaryFiles } from "./NodeTemporaryFiles.ts"
|
|
25
|
+
import { prepareConnector } from "./Connectors.ts"
|
|
26
|
+
import {
|
|
27
|
+
consumeTerminalEvents,
|
|
28
|
+
makeTerminalSurface,
|
|
29
|
+
type TerminalSurface,
|
|
30
|
+
type Verbosity
|
|
31
|
+
} from "./Terminal.ts"
|
|
32
|
+
|
|
33
|
+
export interface FlowRunnerDependencies {
|
|
34
|
+
readonly registry: ConnectorRegistryShape
|
|
35
|
+
readonly process: ProcessExecutorShape
|
|
36
|
+
readonly files: PlainFileStoreShape
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface NodeConnectorDependencies {
|
|
40
|
+
readonly http: HttpClientShape
|
|
41
|
+
readonly process: ProcessExecutorShape
|
|
42
|
+
readonly temporaryFiles: TemporaryFilesShape
|
|
43
|
+
readonly geminiCli: GeminiCliExecutorShape
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const nodeFlowRunnerDependencies = (): FlowRunnerDependencies => ({
|
|
47
|
+
process: nodeProcessExecutor,
|
|
48
|
+
files: nodePlainFileStore,
|
|
49
|
+
registry: createConnectorRegistry({
|
|
50
|
+
http: nodeHttpClient,
|
|
51
|
+
process: nodeProcessExecutor,
|
|
52
|
+
temporaryFiles: nodeTemporaryFiles,
|
|
53
|
+
geminiCli: nodeGeminiCliExecutor
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
export interface FlowRunnerOptions {
|
|
58
|
+
readonly workDir: string
|
|
59
|
+
readonly workspace: string
|
|
60
|
+
readonly userPrompt: string
|
|
61
|
+
readonly coder: ConnectorConfig
|
|
62
|
+
readonly reasoning?: ConnectorConfig
|
|
63
|
+
readonly reviewers?: ReadonlyArray<ConnectorConfig>
|
|
64
|
+
readonly tracePath?: string
|
|
65
|
+
readonly runId?: string
|
|
66
|
+
readonly verbosity?: Verbosity
|
|
67
|
+
readonly surface?: TerminalSurface
|
|
68
|
+
readonly environment?: Readonly<Record<string, string | undefined>>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface FlowRunnerBundle {
|
|
72
|
+
readonly context: FlowContextShape
|
|
73
|
+
readonly events: FlowEventHub
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeContext")(function* (
|
|
77
|
+
options: FlowRunnerOptions,
|
|
78
|
+
dependencies: FlowRunnerDependencies
|
|
79
|
+
): Effect.fn.Return<FlowRunnerBundle, FlowError> {
|
|
80
|
+
const events = yield* makeFlowEventHub()
|
|
81
|
+
const coderConfig = prepareConnector(
|
|
82
|
+
options.coder,
|
|
83
|
+
options.workDir,
|
|
84
|
+
options.environment ?? process.env
|
|
85
|
+
)
|
|
86
|
+
const reasoning = defaultReasoningConfig(options.coder, options.reasoning)
|
|
87
|
+
const reasoningPrepared = prepareConnector(
|
|
88
|
+
reasoning,
|
|
89
|
+
options.workDir,
|
|
90
|
+
options.environment ?? process.env
|
|
91
|
+
)
|
|
92
|
+
const coder = yield* dependencies.registry
|
|
93
|
+
.resolve(coderConfig)
|
|
94
|
+
.pipe(Effect.mapError(FlowLlmError.from))
|
|
95
|
+
const reasoningService = yield* dependencies.registry
|
|
96
|
+
.resolve(reasoningPrepared)
|
|
97
|
+
.pipe(Effect.mapError(FlowLlmError.from))
|
|
98
|
+
const reviewers = yield* Effect.forEach(options.reviewers ?? [], (configuration) =>
|
|
99
|
+
dependencies.registry
|
|
100
|
+
.resolve(prepareConnector(configuration, options.workDir, options.environment ?? process.env))
|
|
101
|
+
.pipe(Effect.mapError(FlowLlmError.from))
|
|
102
|
+
)
|
|
103
|
+
return {
|
|
104
|
+
events,
|
|
105
|
+
context: FlowContext.of({
|
|
106
|
+
reasoning: reasoningService,
|
|
107
|
+
coder,
|
|
108
|
+
git: makeGitTool(dependencies.process, options.workDir, events),
|
|
109
|
+
hosting: makeGitHubTool(dependencies.process, options.workDir, events),
|
|
110
|
+
events,
|
|
111
|
+
reviewers,
|
|
112
|
+
coderCapabilities: coder.capabilities,
|
|
113
|
+
userPrompt: options.userPrompt,
|
|
114
|
+
workDir: options.workDir,
|
|
115
|
+
workspace: options.workspace
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
export const runWithBundle = Effect.fn("@llm4ts/runner/FlowRunner.runWithBundle")(function* <
|
|
121
|
+
A,
|
|
122
|
+
E,
|
|
123
|
+
R
|
|
124
|
+
>(
|
|
125
|
+
bundle: FlowRunnerBundle,
|
|
126
|
+
options: FlowRunnerOptions,
|
|
127
|
+
body: (context: FlowContextShape) => Effect.Effect<A, E, R>,
|
|
128
|
+
dependencies: FlowRunnerDependencies = nodeFlowRunnerDependencies()
|
|
129
|
+
): Effect.fn.Return<A, E | FlowError, R | Scope.Scope> {
|
|
130
|
+
const tracker = yield* makeCostTracker()
|
|
131
|
+
const surface =
|
|
132
|
+
options.surface ?? (yield* makeTerminalSurface(options.environment ?? process.env))
|
|
133
|
+
const palette = surface.palette
|
|
134
|
+
const terminal = yield* consumeTerminalEvents(
|
|
135
|
+
bundle.events,
|
|
136
|
+
surface,
|
|
137
|
+
options.verbosity ?? "Normal"
|
|
138
|
+
)
|
|
139
|
+
yield* tracker.consume(bundle.events)
|
|
140
|
+
const recorder =
|
|
141
|
+
options.tracePath === undefined
|
|
142
|
+
? undefined
|
|
143
|
+
: yield* makeFlowRecorder(
|
|
144
|
+
dependencies.files,
|
|
145
|
+
options.tracePath,
|
|
146
|
+
options.runId ?? `run-${(yield* Clock.currentTimeMillis).toString()}`
|
|
147
|
+
)
|
|
148
|
+
if (recorder !== undefined) {
|
|
149
|
+
yield* recorder.consume(bundle.events)
|
|
150
|
+
}
|
|
151
|
+
yield* surface.setStatus("preparing flow")
|
|
152
|
+
return yield* body(bundle.context).pipe(
|
|
153
|
+
Effect.provideService(FlowContext, bundle.context),
|
|
154
|
+
Effect.ensuring(
|
|
155
|
+
Effect.all(
|
|
156
|
+
[
|
|
157
|
+
terminal.awaitDrained(),
|
|
158
|
+
tracker.awaitDrained(bundle.events),
|
|
159
|
+
recorder === undefined ? Effect.void : recorder.awaitDrained(bundle.events)
|
|
160
|
+
],
|
|
161
|
+
{ concurrency: "unbounded" }
|
|
162
|
+
).pipe(
|
|
163
|
+
Effect.andThen(surface.setStatus(undefined)),
|
|
164
|
+
Effect.andThen(tracker.summary),
|
|
165
|
+
Effect.flatMap((summary) => surface.log(`\n${summary}`))
|
|
166
|
+
)
|
|
167
|
+
),
|
|
168
|
+
Effect.tapError((error) =>
|
|
169
|
+
surface
|
|
170
|
+
.setStatus(undefined)
|
|
171
|
+
.pipe(
|
|
172
|
+
Effect.andThen(
|
|
173
|
+
surface.log(
|
|
174
|
+
`\n${palette.fail(
|
|
175
|
+
`flow failed: ${error instanceof Error ? error.message : String(error)}`
|
|
176
|
+
)}`
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
),
|
|
181
|
+
Effect.tap(() =>
|
|
182
|
+
surface
|
|
183
|
+
.setStatus(undefined)
|
|
184
|
+
.pipe(Effect.andThen(surface.log(`\n${palette.stageDone("flow completed")}`)))
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
export const runEmbedded = Effect.fn("@llm4ts/runner/FlowRunner.runEmbedded")(function* <A, E, R>(
|
|
190
|
+
options: FlowRunnerOptions,
|
|
191
|
+
body: (context: FlowContextShape) => Effect.Effect<A, E, R>,
|
|
192
|
+
dependencies: FlowRunnerDependencies = nodeFlowRunnerDependencies()
|
|
193
|
+
): Effect.fn.Return<A, E | FlowError, R | Scope.Scope> {
|
|
194
|
+
const bundle = yield* makeFlowRunnerContext(options, dependencies)
|
|
195
|
+
return yield* runWithBundle(bundle, options, body, dependencies)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
export const runNode = <A, E, R>(
|
|
199
|
+
options: FlowRunnerOptions,
|
|
200
|
+
body: (context: FlowContextShape) => Effect.Effect<A, E, R>
|
|
201
|
+
): Effect.Effect<A, E | FlowError, R> => Effect.scoped(runEmbedded(options, body))
|
|
202
|
+
|
|
203
|
+
const mainErrorMessage = (error: unknown): string =>
|
|
204
|
+
error instanceof Error && error.message.length > 0 ? error.message : String(error)
|
|
205
|
+
|
|
206
|
+
export const runFlowMain = <E>(program: Effect.Effect<void, E>): void => {
|
|
207
|
+
Effect.runFork(
|
|
208
|
+
program.pipe(
|
|
209
|
+
Effect.match({
|
|
210
|
+
onFailure: (error) =>
|
|
211
|
+
Effect.sync(() => {
|
|
212
|
+
process.stderr.write(`${mainErrorMessage(error)}\n`)
|
|
213
|
+
process.exitCode = 1
|
|
214
|
+
}),
|
|
215
|
+
onSuccess: () => Effect.void
|
|
216
|
+
}),
|
|
217
|
+
Effect.flatten
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
}
|
package/src/McpStdio.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createInterface } from "node:readline"
|
|
2
|
+
import { stdin, stdout } from "node:process"
|
|
3
|
+
import * as Effect from "effect/Effect"
|
|
4
|
+
import * as Stream from "effect/Stream"
|
|
5
|
+
import { ProcessError, type FlowError } from "@llm4ts/flow/FlowError"
|
|
6
|
+
import { handleMcpLine, type McpTool } from "@llm4ts/flow/McpServer"
|
|
7
|
+
|
|
8
|
+
export const mcpStdioResponses = <R>(
|
|
9
|
+
lines: Stream.Stream<string, FlowError, R>,
|
|
10
|
+
tools: ReadonlyArray<McpTool>
|
|
11
|
+
): Stream.Stream<string, FlowError, R> =>
|
|
12
|
+
lines.pipe(
|
|
13
|
+
Stream.mapEffect((line) => handleMcpLine(line, tools)),
|
|
14
|
+
Stream.filter((response): response is string => response !== undefined)
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
export const runMcpStdio = (
|
|
18
|
+
tools: ReadonlyArray<McpTool>,
|
|
19
|
+
write: (line: string) => void = (line) => stdout.write(`${line}\n`)
|
|
20
|
+
): Effect.Effect<void, FlowError> =>
|
|
21
|
+
Effect.acquireUseRelease(
|
|
22
|
+
Effect.sync(() =>
|
|
23
|
+
createInterface({
|
|
24
|
+
input: stdin,
|
|
25
|
+
crlfDelay: Infinity,
|
|
26
|
+
terminal: false
|
|
27
|
+
})
|
|
28
|
+
),
|
|
29
|
+
(input) => {
|
|
30
|
+
const lines = Stream.fromAsyncIterable(input, (error) =>
|
|
31
|
+
ProcessError.make({
|
|
32
|
+
message: "MCP stdio input",
|
|
33
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
34
|
+
})
|
|
35
|
+
)
|
|
36
|
+
return mcpStdioResponses(lines, tools).pipe(
|
|
37
|
+
Stream.runForEach((line) => Effect.sync(() => write(line)))
|
|
38
|
+
)
|
|
39
|
+
},
|
|
40
|
+
(input) => Effect.sync(() => input.close())
|
|
41
|
+
)
|