lalph 0.0.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/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "lalph",
3
+ "type": "module",
4
+ "version": "0.0.0",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "bin": {
9
+ "lalph": "./dist/cli.mjs"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "src"
14
+ ],
15
+ "license": "MIT",
16
+ "author": "Tim Smart <hello@timsmart.co>",
17
+ "devDependencies": {
18
+ "@changesets/changelog-github": "^0.5.2",
19
+ "@changesets/cli": "^2.29.8",
20
+ "@effect/language-service": "^0.64.1",
21
+ "@effect/platform-node": "https://pkg.pr.new/Effect-TS/effect-smol/@effect/platform-node@bc26c98",
22
+ "@linear/sdk": "^69.0.0",
23
+ "effect": "https://pkg.pr.new/Effect-TS/effect-smol/effect@bc26c98",
24
+ "prettier": "^3.7.4",
25
+ "tsdown": "^0.19.0",
26
+ "typescript": "^5.9.3"
27
+ },
28
+ "scripts": {
29
+ "check": "tsc --noEmit && prettier --check .",
30
+ "build": "tsdown"
31
+ }
32
+ }
package/src/Kvs.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { KeyValueStore } from "effect/unstable/persistence"
2
+
3
+ export const layerKvs = KeyValueStore.layerFileSystem(".lalph/config")
@@ -0,0 +1,183 @@
1
+ import {
2
+ DateTime,
3
+ Deferred,
4
+ Effect,
5
+ Layer,
6
+ Option,
7
+ Schedule,
8
+ Schema,
9
+ ServiceMap,
10
+ } from "effect"
11
+ import {
12
+ FetchHttpClient,
13
+ HttpClient,
14
+ HttpClientRequest,
15
+ HttpClientResponse,
16
+ HttpRouter,
17
+ HttpServerRequest,
18
+ HttpServerResponse,
19
+ } from "effect/unstable/http"
20
+ import { Base64Url } from "effect/encoding"
21
+ import { NodeHttpServer } from "@effect/platform-node"
22
+ import { createServer } from "node:http"
23
+ import { KeyValueStore } from "effect/unstable/persistence"
24
+ import { layerKvs } from "../Kvs.ts"
25
+
26
+ const clientId = "852ed0906088135c1f591d234a4eaa4b"
27
+
28
+ export class TokenManager extends ServiceMap.Service<TokenManager>()(
29
+ "lalph/Linear/TokenManager",
30
+ {
31
+ make: Effect.gen(function* () {
32
+ const kvs = yield* KeyValueStore.KeyValueStore
33
+ const tokenStore = KeyValueStore.toSchemaStore(kvs, AccessToken)
34
+
35
+ const httpClient = (yield* HttpClient.HttpClient).pipe(
36
+ HttpClient.filterStatusOk,
37
+ HttpClient.retryTransient({
38
+ schedule: Schedule.spaced(1000),
39
+ }),
40
+ )
41
+
42
+ let currentToken = yield* Effect.orDie(tokenStore.get("accessToken"))
43
+ const set = (token: AccessToken) =>
44
+ Effect.orDie(tokenStore.set("accessToken", token))
45
+ const clear = Effect.orDie(tokenStore.remove("accessToken"))
46
+
47
+ const getNoLock: Effect.Effect<AccessToken> = Effect.gen(function* () {
48
+ if (Option.isNone(currentToken)) {
49
+ const newToken = yield* pkce
50
+ yield* set(newToken)
51
+ return newToken
52
+ } else if (currentToken.value.isExpired()) {
53
+ const newToken = yield* refresh(currentToken.value)
54
+ if (Option.isNone(newToken)) {
55
+ yield* clear
56
+ return yield* getNoLock
57
+ }
58
+ yield* set(newToken.value)
59
+ currentToken = newToken
60
+ return newToken.value
61
+ }
62
+ return currentToken.value
63
+ })
64
+ const get = Effect.makeSemaphoreUnsafe(1).withPermit(getNoLock)
65
+
66
+ const pkce = Effect.gen(function* () {
67
+ const deferred = yield* Deferred.make<typeof CallbackParams.Type>()
68
+
69
+ const CallbackRoute = HttpRouter.add(
70
+ "GET",
71
+ "/callback",
72
+ Effect.gen(function* () {
73
+ const params = yield* callbackParams
74
+ yield* Deferred.succeed(deferred, params)
75
+ return yield* HttpServerResponse.html`
76
+ <html>
77
+ <body style="font-family: sans-serif; text-align: center; margin-top: 50px;">
78
+ <h1>Lalph login Successful</h1>
79
+ <p>You can close this window now.</p>
80
+ </body>
81
+ </html>
82
+ `
83
+ }),
84
+ )
85
+ yield* HttpRouter.serve(CallbackRoute, {
86
+ disableListenLog: true,
87
+ disableLogger: true,
88
+ }).pipe(
89
+ Layer.provide(NodeHttpServer.layer(createServer, { port: 34338 })),
90
+ Layer.build,
91
+ )
92
+ const redirectUri = `http://localhost:34338/callback`
93
+
94
+ // client
95
+ const verifier = crypto.randomUUID()
96
+ const verifierSha256 = yield* Effect.promise(() =>
97
+ crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier)),
98
+ )
99
+ const challenge = Base64Url.encode(new Uint8Array(verifierSha256))
100
+
101
+ const url = `https://linear.app/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=read,write&code_challenge=${challenge}&code_challenge_method=S256`
102
+
103
+ console.log("Open this URL to login to Linear:", url)
104
+
105
+ const params = yield* Deferred.await(deferred)
106
+
107
+ const res = yield* HttpClientRequest.post(
108
+ "https://api.linear.app/oauth/token",
109
+ ).pipe(
110
+ HttpClientRequest.bodyUrlParams({
111
+ code: params.code,
112
+ redirect_uri: redirectUri,
113
+ client_id: clientId,
114
+ code_verifier: verifier,
115
+ grant_type: "authorization_code",
116
+ }),
117
+ httpClient.execute,
118
+ Effect.flatMap(HttpClientResponse.schemaBodyJson(TokenResponse)),
119
+ )
120
+
121
+ return AccessToken.fromResponse(res)
122
+ }).pipe(Effect.scoped, Effect.orDie)
123
+
124
+ const refresh = Effect.fnUntraced(function* (token: AccessToken) {
125
+ const res = yield* HttpClientRequest.post(
126
+ "https://api.linear.app/oauth/token",
127
+ ).pipe(
128
+ HttpClientRequest.bodyUrlParams({
129
+ refresh_token: token.refreshToken,
130
+ client_id: clientId,
131
+ grant_type: "refresh_token",
132
+ }),
133
+ httpClient.execute,
134
+ Effect.flatMap(HttpClientResponse.schemaBodyJson(TokenResponse)),
135
+ )
136
+ return AccessToken.fromResponse(res)
137
+ }, Effect.option)
138
+
139
+ return { get } as const
140
+ }),
141
+ },
142
+ ) {
143
+ static layer = Layer.effect(this, this.make).pipe(
144
+ Layer.provide([layerKvs, FetchHttpClient.layer]),
145
+ )
146
+ }
147
+
148
+ export class AccessToken extends Schema.Class<AccessToken>(
149
+ "lalph/Linear/AccessToken",
150
+ )({
151
+ token: Schema.String,
152
+ expiresAt: Schema.DateTimeUtc,
153
+ refreshToken: Schema.String,
154
+ }) {
155
+ static fromResponse(res: typeof TokenResponse.Type): AccessToken {
156
+ return new AccessToken({
157
+ token: res.access_token,
158
+ refreshToken: res.refresh_token,
159
+ expiresAt: DateTime.nowUnsafe().pipe(
160
+ DateTime.add({ seconds: res.expires_in }),
161
+ ),
162
+ })
163
+ }
164
+
165
+ isExpired(): boolean {
166
+ return DateTime.isPastUnsafe(
167
+ this.expiresAt.pipe(DateTime.subtract({ minutes: 5 })),
168
+ )
169
+ }
170
+ }
171
+
172
+ const CallbackParams = Schema.Struct({
173
+ code: Schema.String,
174
+ })
175
+ const callbackParams = HttpServerRequest.schemaSearchParams(CallbackParams)
176
+
177
+ const TokenResponse = Schema.Struct({
178
+ access_token: Schema.String,
179
+ token_type: Schema.String,
180
+ expires_in: Schema.Number,
181
+ refresh_token: Schema.String,
182
+ scope: Schema.String,
183
+ })
package/src/Linear.ts ADDED
@@ -0,0 +1,149 @@
1
+ import { Effect, Stream, Layer, Schema, ServiceMap, Option } from "effect"
2
+ import {
3
+ Connection,
4
+ IssueLabel,
5
+ LinearClient,
6
+ Project,
7
+ WorkflowState,
8
+ } from "@linear/sdk"
9
+ import { TokenManager } from "./Linear/TokenManager.ts"
10
+ import { KeyValueStore } from "effect/unstable/persistence"
11
+ import { Prompt } from "effect/unstable/cli"
12
+ import { layerKvs } from "./Kvs.ts"
13
+ import { selectedLabelId, selectedTeamId, Settings } from "./Settings.ts"
14
+
15
+ export class Linear extends ServiceMap.Service<Linear>()("lalph/Linear", {
16
+ make: Effect.gen(function* () {
17
+ const tokens = yield* TokenManager
18
+
19
+ const client = new LinearClient({
20
+ accessToken: (yield* tokens.get).token,
21
+ })
22
+
23
+ const use = <A>(f: (client: LinearClient) => Promise<A>) =>
24
+ Effect.tryPromise({
25
+ try: () => f(client),
26
+ catch: (cause) => new LinearError({ cause }),
27
+ })
28
+
29
+ const stream = <A>(f: (client: LinearClient) => Promise<Connection<A>>) =>
30
+ Stream.paginate(
31
+ null as null | Connection<A>,
32
+ Effect.fnUntraced(function* (prev) {
33
+ const connection = yield* prev
34
+ ? Effect.tryPromise({
35
+ try: () => prev.fetchNext(),
36
+ catch: (cause) => new LinearError({ cause }),
37
+ })
38
+ : use(f)
39
+
40
+ return [
41
+ connection.nodes,
42
+ Option.some(connection).pipe(
43
+ Option.filter((c) => c.pageInfo.hasNextPage),
44
+ ),
45
+ ]
46
+ }),
47
+ )
48
+
49
+ const projects = stream((client) => client.projects())
50
+ const labels = stream((client) => client.issueLabels())
51
+ const states = yield* Stream.runFold(
52
+ stream((client) => client.workflowStates()),
53
+ () => new Map<string, WorkflowState>(),
54
+ (map, state) => map.set(state.id, state),
55
+ )
56
+
57
+ return { use, stream, projects, labels, states } as const
58
+ }),
59
+ }) {
60
+ static layer = Layer.effect(this, this.make).pipe(
61
+ Layer.provide(TokenManager.layer),
62
+ )
63
+ }
64
+
65
+ export class LinearError extends Schema.ErrorClass("lalph/LinearError")({
66
+ _tag: Schema.tag("LinearError"),
67
+ cause: Schema.Defect,
68
+ }) {}
69
+
70
+ export class CurrentProject extends ServiceMap.Service<
71
+ CurrentProject,
72
+ Project
73
+ >()("lalph/Linear/CurrentProject") {
74
+ static store = KeyValueStore.KeyValueStore.use((_) =>
75
+ Effect.succeed(KeyValueStore.prefix(_, "linear.currentProjectId")),
76
+ )
77
+
78
+ static select = Effect.gen(function* () {
79
+ const kvs = yield* CurrentProject.store
80
+ const linear = yield* Linear
81
+
82
+ const projects = yield* Stream.runCollect(linear.projects)
83
+
84
+ const project = yield* Prompt.select({
85
+ message: "Select a Linear project",
86
+ choices: projects.map((project) => ({
87
+ title: project.name,
88
+ value: project,
89
+ })),
90
+ })
91
+
92
+ yield* kvs.set("", project.id)
93
+
94
+ yield* teamSelect(project)
95
+ yield* labelSelect
96
+
97
+ return project
98
+ })
99
+
100
+ static get = Effect.gen(function* () {
101
+ const linear = yield* Linear
102
+ const kvs = yield* CurrentProject.store
103
+ const projectId = yield* kvs.get("")
104
+
105
+ return projectId
106
+ ? yield* linear
107
+ .use((c) => c.project(projectId))
108
+ .pipe(Effect.catch(() => CurrentProject.select))
109
+ : yield* CurrentProject.select
110
+ })
111
+
112
+ static layer = Layer.effect(this, this.get).pipe(
113
+ Layer.provide([Linear.layer, layerKvs, Settings.layer]),
114
+ )
115
+ }
116
+
117
+ export const labelSelect = Effect.gen(function* () {
118
+ const linear = yield* Linear
119
+ const labels = yield* Stream.runCollect(linear.labels)
120
+ const label = yield* Prompt.select({
121
+ message: "Select a label to filter issues by",
122
+ choices: [
123
+ {
124
+ title: "No Label",
125
+ value: Option.none<IssueLabel>(),
126
+ },
127
+ ].concat(
128
+ labels.map((label) => ({
129
+ title: label.name,
130
+ value: Option.some(label),
131
+ })),
132
+ ),
133
+ })
134
+ yield* selectedLabelId.set(Option.map(label, (l) => l.id))
135
+ return label
136
+ })
137
+
138
+ const teamSelect = Effect.fnUntraced(function* (project: Project) {
139
+ const linear = yield* Linear
140
+ const teams = yield* Stream.runCollect(linear.stream(() => project.teams()))
141
+ const teamId = yield* Prompt.select({
142
+ message: "Select a team for new issues",
143
+ choices: teams.map((team) => ({
144
+ title: team.name,
145
+ value: team.id,
146
+ })),
147
+ })
148
+ yield* selectedTeamId.set(Option.some(teamId))
149
+ })
package/src/Prd.ts ADDED
@@ -0,0 +1,188 @@
1
+ import {
2
+ Data,
3
+ Effect,
4
+ FileSystem,
5
+ Layer,
6
+ Option,
7
+ Schema,
8
+ ServiceMap,
9
+ Stream,
10
+ SubscriptionRef,
11
+ } from "effect"
12
+ import { CurrentProject, Linear } from "./Linear.ts"
13
+ import { selectedLabelId, selectedTeamId, Settings } from "./Settings.ts"
14
+ import type { Issue } from "@linear/sdk"
15
+
16
+ export class Prd extends ServiceMap.Service<Prd>()("lalph/Prd", {
17
+ make: Effect.gen(function* () {
18
+ const fs = yield* FileSystem.FileSystem
19
+ const linear = yield* Linear
20
+ const project = yield* CurrentProject
21
+ const teamId = Option.getOrThrow(yield* selectedTeamId.get)
22
+ const labelId = yield* selectedLabelId.get
23
+
24
+ const getIssues = linear
25
+ .stream(() =>
26
+ project.issues({
27
+ filter: {
28
+ labels: {
29
+ id: labelId.pipe(
30
+ Option.map((eq) => ({ eq })),
31
+ Option.getOrNull,
32
+ ),
33
+ },
34
+ state: {
35
+ type: { eq: "unstarted" },
36
+ },
37
+ },
38
+ }),
39
+ )
40
+ .pipe(Stream.runCollect)
41
+
42
+ const initial = yield* getIssues.pipe(Effect.map(PrdList.fromLinearIssues))
43
+ if (initial.issues.size === 0) {
44
+ return yield* new NoMoreWork({})
45
+ }
46
+
47
+ const current = yield* SubscriptionRef.make(initial)
48
+
49
+ const prdFile = `.lalph/prd.json`
50
+
51
+ yield* fs.writeFileString(prdFile, initial.toJson())
52
+
53
+ const sync = Effect.gen(function* () {
54
+ const json = yield* fs.readFileString(prdFile)
55
+ const currentValue = yield* SubscriptionRef.get(current)
56
+ const updated = PrdList.fromJson(json)
57
+
58
+ for (const issue of updated) {
59
+ if (issue.id === null) {
60
+ // create new issue
61
+ yield* linear.use((c) =>
62
+ c.createIssue({
63
+ teamId,
64
+ projectId: project.id,
65
+ labelIds: Option.toArray(labelId),
66
+ title: issue.title,
67
+ description: issue.description,
68
+ priority: issue.priority,
69
+ estimate: issue.estimate,
70
+ stateId: issue.stateId,
71
+ }),
72
+ )
73
+ continue
74
+ }
75
+ const existing = currentValue.issues.get(issue.id)
76
+ if (!existing || !existing.isChangedComparedTo(issue)) continue
77
+
78
+ // update existing issue
79
+ yield* linear.use((c) =>
80
+ c.updateIssue(issue.id!, {
81
+ description: issue.description,
82
+ stateId: issue.stateId,
83
+ }),
84
+ )
85
+ }
86
+ })
87
+
88
+ yield* Effect.addFinalizer(() => Effect.orDie(sync))
89
+
90
+ yield* fs.watch(prdFile).pipe(
91
+ Stream.buffer({
92
+ capacity: 1,
93
+ strategy: "dropping",
94
+ }),
95
+ Stream.runForEach(() => Effect.ignore(sync)),
96
+ Effect.forkScoped,
97
+ )
98
+
99
+ return { current } as const
100
+ }),
101
+ }) {
102
+ static layer = Layer.effect(this, this.make).pipe(
103
+ Layer.provide([Linear.layer, Settings.layer, CurrentProject.layer]),
104
+ )
105
+ }
106
+
107
+ export class NoMoreWork extends Schema.ErrorClass<NoMoreWork>(
108
+ "lalph/Prd/NoMoreWork",
109
+ )({
110
+ _tag: Schema.tag("NoMoreWork"),
111
+ }) {
112
+ readonly message = "No more work to be done!"
113
+ }
114
+
115
+ export class PrdIssue extends Schema.Class<PrdIssue>("PrdIssue")({
116
+ id: Schema.NullOr(Schema.String).annotate({
117
+ description:
118
+ "The unique identifier of the issue. If null, it is considered a new issue.",
119
+ }),
120
+ title: Schema.String.annotate({
121
+ description: "The title of the issue",
122
+ }),
123
+ description: Schema.String.annotate({
124
+ description: "The description of the issue",
125
+ }),
126
+ priority: Schema.Finite.annotate({
127
+ description:
128
+ "The priority of the issue. 0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low.",
129
+ }),
130
+ estimate: Schema.NullOr(Schema.Finite).annotate({
131
+ description:
132
+ "The estimate of the issue in points. Null if no estimate is set.",
133
+ }),
134
+ stateId: Schema.String.annotate({
135
+ description: "The state ID of the issue.",
136
+ }),
137
+ }) {
138
+ static Array = Schema.Array(this)
139
+ static ArrayFromJson = Schema.toCodecJson(this.Array)
140
+
141
+ static jsonSchemaDoc = Schema.toJsonSchemaDocument(this)
142
+ static jsonSchema = {
143
+ ...this.jsonSchemaDoc.schema,
144
+ $defs: this.jsonSchemaDoc.definitions,
145
+ }
146
+
147
+ static fromLinearIssue(issue: Issue): PrdIssue {
148
+ return new PrdIssue({
149
+ id: issue.id,
150
+ title: issue.title,
151
+ description: issue.description ?? "",
152
+ priority: issue.priority,
153
+ estimate: issue.estimate ?? null,
154
+ stateId: issue.stateId!,
155
+ })
156
+ }
157
+
158
+ isChangedComparedTo(issue: PrdIssue): boolean {
159
+ return (
160
+ this.description !== issue.description || this.stateId !== issue.stateId
161
+ )
162
+ }
163
+ }
164
+
165
+ export class PrdList extends Data.Class<{
166
+ readonly issues: ReadonlyMap<string, PrdIssue>
167
+ }> {
168
+ static fromLinearIssues(issues: Issue[]): PrdList {
169
+ const map = new Map<string, PrdIssue>()
170
+ for (const issue of issues) {
171
+ const prdIssue = PrdIssue.fromLinearIssue(issue)
172
+ if (!prdIssue.id) continue
173
+ map.set(prdIssue.id, prdIssue)
174
+ }
175
+ return new PrdList({ issues: map })
176
+ }
177
+
178
+ static fromJson(json: string): ReadonlyArray<PrdIssue> {
179
+ const issues = Schema.decodeSync(PrdIssue.ArrayFromJson)(JSON.parse(json))
180
+ return issues
181
+ }
182
+
183
+ toJson(): string {
184
+ const issuesArray = Array.from(this.issues.values())
185
+ const encoded = Schema.encodeSync(PrdIssue.ArrayFromJson)(issuesArray)
186
+ return JSON.stringify(encoded, null, 2)
187
+ }
188
+ }
@@ -0,0 +1,64 @@
1
+ import { Effect, FileSystem, Layer, ServiceMap } from "effect"
2
+ import { Linear } from "./Linear.ts"
3
+ import { PrdIssue } from "./Prd.ts"
4
+
5
+ export class PromptGen extends ServiceMap.Service<PromptGen>()(
6
+ "lalph/PromptGen",
7
+ {
8
+ make: Effect.gen(function* () {
9
+ const linear = yield* Linear
10
+ const fs = yield* FileSystem.FileSystem
11
+
12
+ yield* Effect.scoped(
13
+ fs.open("PROGRESS.md", {
14
+ flag: "a+",
15
+ }),
16
+ )
17
+
18
+ const prompt = `# Instructions
19
+
20
+ 1. Decide which single task to work on next from the prd.json file. This should
21
+ be the task YOU decide as the most important to work on next, not just the
22
+ first task in the list.
23
+ - **Important**: Before starting the chosen task, mark it as "in progress" by
24
+ updating its \`stateId\` in the prd.json file.
25
+ This prevents other people or agents from working on the same task simultaneously.
26
+ 3. Run any checks / feedback loops, such as type checks, unit tests, or linting.
27
+ 4. APPEND your progress to the PROGRESS.md file.
28
+ 5. Make a git commit when you have made significant progress or completed the task.
29
+ 6. Update the prd.json file to reflect any changes in task states.
30
+ - Add follow up tasks only if needed.
31
+ - Append to the \`description\` field with any notes.
32
+ - When a task is complete, set its \`stateId\` to the id that indicates
33
+ a review is required, or completion if a review state is unavailable.
34
+
35
+ Remember, only work on a single task at a time, that you decide is the most
36
+ important to work on next.
37
+
38
+ ## prd.json format
39
+
40
+ Each item in the prd.json file represents a task for the current project.
41
+
42
+ The \`stateId\` field indicates the current state of the task. The possible states
43
+ are:
44
+
45
+ ${Array.from(linear.states.values(), (state) => `- **${state.name}** (stateId: \`${state.id}\`)`).join("\n")}
46
+
47
+ ### Adding tasks
48
+
49
+ To add a new task, append a new item to the prd.json file with the id set to
50
+ \`null\`.
51
+
52
+ ### prd.json json schema
53
+
54
+ \`\`\`json
55
+ ${JSON.stringify(PrdIssue.jsonSchema, null, 2)}
56
+ \`\`\`
57
+ `
58
+
59
+ return { prompt } as const
60
+ }),
61
+ },
62
+ ) {
63
+ static layer = Layer.effect(this, this.make).pipe(Layer.provide(Linear.layer))
64
+ }
package/src/Runner.ts ADDED
@@ -0,0 +1,52 @@
1
+ import { Array, Effect, Option } from "effect"
2
+ import { PromptGen } from "./PromptGen.ts"
3
+ import { Prd } from "./Prd.ts"
4
+ import { ChildProcess } from "effect/unstable/process"
5
+ import { Prompt } from "effect/unstable/cli"
6
+ import { allCliAgents } from "./domain/CliAgent.ts"
7
+ import { selectedCliAgentId } from "./Settings.ts"
8
+
9
+ export const run = Effect.gen(function* () {
10
+ const promptGen = yield* PromptGen
11
+ const cliAgent = yield* getOrSelectCliAgent
12
+ const cliCommand = cliAgent.command({
13
+ prompt: promptGen.prompt,
14
+ prdFilePath: ".lalph/prd.json",
15
+ progressFilePath: "PROGRESS.md",
16
+ })
17
+ const command = ChildProcess.make(cliCommand[0]!, cliCommand.slice(1), {
18
+ extendEnv: true,
19
+ stdout: "inherit",
20
+ stderr: "inherit",
21
+ stdin: "inherit",
22
+ })
23
+
24
+ const agent = yield* command
25
+ const exitCode = yield* agent.exitCode
26
+
27
+ yield* Effect.log(`Agent exited with code: ${exitCode}`)
28
+ }).pipe(Effect.scoped, Effect.provide([PromptGen.layer, Prd.layer]))
29
+
30
+ export const selectCliAgent = Effect.gen(function* () {
31
+ const agent = yield* Prompt.select({
32
+ message: "Select the CLI agent to use",
33
+ choices: allCliAgents.map((agent) => ({
34
+ title: agent.name,
35
+ value: agent,
36
+ })),
37
+ })
38
+ yield* selectedCliAgentId.set(Option.some(agent.id))
39
+ return agent
40
+ })
41
+
42
+ const getOrSelectCliAgent = Effect.gen(function* () {
43
+ const selectedAgent = (yield* selectedCliAgentId.get).pipe(
44
+ Option.filterMap((id) =>
45
+ Array.findFirst(allCliAgents, (agent) => agent.id === id),
46
+ ),
47
+ )
48
+ if (Option.isSome(selectedAgent)) {
49
+ return selectedAgent.value
50
+ }
51
+ return yield* selectCliAgent
52
+ })