clanka 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +3 -0
  2. package/dist/Agent.d.ts +119 -0
  3. package/dist/Agent.d.ts.map +1 -0
  4. package/dist/Agent.js +240 -0
  5. package/dist/Agent.js.map +1 -0
  6. package/dist/AgentTools.d.ts +246 -0
  7. package/dist/AgentTools.d.ts.map +1 -0
  8. package/dist/AgentTools.js +374 -0
  9. package/dist/AgentTools.js.map +1 -0
  10. package/dist/AgentTools.test.d.ts +2 -0
  11. package/dist/AgentTools.test.d.ts.map +1 -0
  12. package/dist/AgentTools.test.js +147 -0
  13. package/dist/AgentTools.test.js.map +1 -0
  14. package/dist/ApplyPatch.d.ts +27 -0
  15. package/dist/ApplyPatch.d.ts.map +1 -0
  16. package/dist/ApplyPatch.js +343 -0
  17. package/dist/ApplyPatch.js.map +1 -0
  18. package/dist/ApplyPatch.test.d.ts +2 -0
  19. package/dist/ApplyPatch.test.d.ts.map +1 -0
  20. package/dist/ApplyPatch.test.js +99 -0
  21. package/dist/ApplyPatch.test.js.map +1 -0
  22. package/dist/Codex.d.ts +11 -0
  23. package/dist/Codex.d.ts.map +1 -0
  24. package/dist/Codex.js +14 -0
  25. package/dist/Codex.js.map +1 -0
  26. package/dist/CodexAuth.d.ts +68 -0
  27. package/dist/CodexAuth.d.ts.map +1 -0
  28. package/dist/CodexAuth.js +270 -0
  29. package/dist/CodexAuth.js.map +1 -0
  30. package/dist/CodexAuth.test.d.ts +2 -0
  31. package/dist/CodexAuth.test.d.ts.map +1 -0
  32. package/dist/CodexAuth.test.js +425 -0
  33. package/dist/CodexAuth.test.js.map +1 -0
  34. package/dist/Executor.d.ts +20 -0
  35. package/dist/Executor.d.ts.map +1 -0
  36. package/dist/Executor.js +76 -0
  37. package/dist/Executor.js.map +1 -0
  38. package/dist/OutputFormatter.d.ts +11 -0
  39. package/dist/OutputFormatter.d.ts.map +1 -0
  40. package/dist/OutputFormatter.js +5 -0
  41. package/dist/OutputFormatter.js.map +1 -0
  42. package/dist/ToolkitRenderer.d.ts +17 -0
  43. package/dist/ToolkitRenderer.d.ts.map +1 -0
  44. package/dist/ToolkitRenderer.js +25 -0
  45. package/dist/ToolkitRenderer.js.map +1 -0
  46. package/dist/TypeBuilder.d.ts +11 -0
  47. package/dist/TypeBuilder.d.ts.map +1 -0
  48. package/dist/TypeBuilder.js +383 -0
  49. package/dist/TypeBuilder.js.map +1 -0
  50. package/dist/TypeBuilder.test.d.ts +2 -0
  51. package/dist/TypeBuilder.test.d.ts.map +1 -0
  52. package/dist/TypeBuilder.test.js +243 -0
  53. package/dist/TypeBuilder.test.js.map +1 -0
  54. package/dist/index.d.ts +25 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +25 -0
  57. package/dist/index.js.map +1 -0
  58. package/package.json +72 -0
  59. package/src/Agent.ts +398 -0
  60. package/src/AgentTools.test.ts +215 -0
  61. package/src/AgentTools.ts +507 -0
  62. package/src/ApplyPatch.test.ts +154 -0
  63. package/src/ApplyPatch.ts +473 -0
  64. package/src/Codex.ts +14 -0
  65. package/src/CodexAuth.test.ts +729 -0
  66. package/src/CodexAuth.ts +571 -0
  67. package/src/Executor.ts +129 -0
  68. package/src/OutputFormatter.ts +17 -0
  69. package/src/ToolkitRenderer.ts +39 -0
  70. package/src/TypeBuilder.test.ts +508 -0
  71. package/src/TypeBuilder.ts +670 -0
  72. package/src/index.ts +29 -0
@@ -0,0 +1,571 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+ import {
5
+ Console,
6
+ Effect,
7
+ Encoding,
8
+ flow,
9
+ Layer,
10
+ Option,
11
+ Result,
12
+ Schedule,
13
+ Schema,
14
+ Semaphore,
15
+ ServiceMap,
16
+ } from "effect"
17
+ import {
18
+ HttpClient,
19
+ HttpClientRequest,
20
+ HttpClientResponse,
21
+ } from "effect/unstable/http"
22
+ import { KeyValueStore } from "effect/unstable/persistence"
23
+
24
+ export const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
25
+ export const ISSUER = "https://auth.openai.com"
26
+ export const POLLING_SAFETY_MARGIN_MS = 3000
27
+ export const TOKEN_EXPIRY_BUFFER_MS = 30_000
28
+ export const STORE_PREFIX = "codex.auth/"
29
+ export const STORE_TOKEN_KEY = "token"
30
+
31
+ const DEVICE_CODE_URL = `/api/accounts/deviceauth/usercode`
32
+ const DEVICE_TOKEN_URL = `/api/accounts/deviceauth/token`
33
+ const TOKEN_URL = `/oauth/token`
34
+ const DEVICE_REDIRECT_URI = `${ISSUER}/deviceauth/callback`
35
+ const DEVICE_VERIFICATION_URL = `/codex/device`
36
+ const DEFAULT_DEVICE_POLL_INTERVAL_SECONDS = 5
37
+ const DEFAULT_TOKEN_EXPIRY_SECONDS = 3600
38
+ const ACCOUNT_ID_HEADER = "ChatGPT-Account-Id"
39
+
40
+ export class TokenData extends Schema.Class<TokenData>(
41
+ "clanka/CodexAuth/TokenData",
42
+ )({
43
+ access: Schema.String,
44
+ refresh: Schema.String,
45
+ expires: Schema.Number,
46
+ accountId: Schema.OptionFromOptional(Schema.String),
47
+ }) {
48
+ isExpired(): boolean {
49
+ return this.expires < Date.now() + TOKEN_EXPIRY_BUFFER_MS
50
+ }
51
+ }
52
+
53
+ export class CodexAuthError extends Schema.TaggedErrorClass<CodexAuthError>()(
54
+ "CodexAuthError",
55
+ {
56
+ reason: Schema.Literals([
57
+ "DeviceFlowFailed",
58
+ "TokenExchangeFailed",
59
+ "RefreshFailed",
60
+ ]),
61
+ message: Schema.String,
62
+ cause: Schema.optional(Schema.Defect),
63
+ },
64
+ ) {}
65
+
66
+ const DeviceCodeResponseSchema = Schema.Struct({
67
+ device_auth_id: Schema.String,
68
+ user_code: Schema.String,
69
+ interval: Schema.String,
70
+ })
71
+
72
+ const AuthorizationCodeResponseSchema = Schema.Struct({
73
+ authorization_code: Schema.String,
74
+ code_verifier: Schema.String,
75
+ })
76
+
77
+ const TokenResponseSchema = Schema.Struct({
78
+ id_token: Schema.optional(Schema.String),
79
+ access_token: Schema.String,
80
+ refresh_token: Schema.String,
81
+ expires_in: Schema.optional(Schema.Number),
82
+ })
83
+
84
+ type TokenResponse = typeof TokenResponseSchema.Type
85
+
86
+ export interface DeviceCodeData {
87
+ readonly deviceAuthId: string
88
+ readonly userCode: string
89
+ readonly intervalMs: number
90
+ }
91
+
92
+ export interface AuthorizationCodeData {
93
+ readonly authorizationCode: string
94
+ readonly codeVerifier: string
95
+ }
96
+
97
+ export interface JwtClaims {
98
+ readonly chatgpt_account_id?: string
99
+ readonly "https://api.openai.com/auth"?: {
100
+ readonly chatgpt_account_id?: string
101
+ }
102
+ readonly organizations?: ReadonlyArray<{
103
+ readonly id: string
104
+ }>
105
+ }
106
+
107
+ const decodeJwtJson = Schema.decodeUnknownOption(
108
+ Schema.fromJsonString(Schema.Unknown),
109
+ )
110
+
111
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
112
+ typeof value === "object" && value !== null && !Array.isArray(value)
113
+
114
+ const getString = (value: unknown): string | undefined =>
115
+ typeof value === "string" ? value : undefined
116
+
117
+ const toJwtClaims = (value: unknown): Option.Option<JwtClaims> => {
118
+ if (!isRecord(value)) {
119
+ return Option.none()
120
+ }
121
+
122
+ const accountId = getString(value["chatgpt_account_id"])
123
+ const authValue = value["https://api.openai.com/auth"]
124
+ const nestedAccountId = isRecord(authValue)
125
+ ? getString(authValue["chatgpt_account_id"])
126
+ : undefined
127
+ const organizationsValue = value["organizations"]
128
+ const organizationId =
129
+ Array.isArray(organizationsValue) &&
130
+ organizationsValue[0] !== undefined &&
131
+ isRecord(organizationsValue[0])
132
+ ? getString(organizationsValue[0]["id"])
133
+ : undefined
134
+
135
+ return Option.some({
136
+ ...(accountId === undefined ? {} : { chatgpt_account_id: accountId }),
137
+ ...(nestedAccountId === undefined
138
+ ? {}
139
+ : {
140
+ "https://api.openai.com/auth": {
141
+ chatgpt_account_id: nestedAccountId,
142
+ },
143
+ }),
144
+ ...(organizationId === undefined
145
+ ? {}
146
+ : {
147
+ organizations: [{ id: organizationId }],
148
+ }),
149
+ })
150
+ }
151
+
152
+ const decodeJwtPayload = (token: string): Option.Option<string> => {
153
+ const parts = token.split(".")
154
+ if (parts.length !== 3) {
155
+ return Option.none()
156
+ }
157
+
158
+ const payload = parts[1]
159
+ if (payload === undefined) {
160
+ return Option.none()
161
+ }
162
+
163
+ return Option.fromNullishOr(
164
+ Result.getOrUndefined(Encoding.decodeBase64UrlString(payload)),
165
+ )
166
+ }
167
+
168
+ export const parseJwtClaims = (token: string): Option.Option<JwtClaims> =>
169
+ decodeJwtPayload(token).pipe(
170
+ Option.flatMap(decodeJwtJson),
171
+ Option.flatMap(toJwtClaims),
172
+ )
173
+
174
+ export const extractAccountIdFromClaims = (
175
+ claims: JwtClaims,
176
+ ): Option.Option<string> => {
177
+ if (
178
+ claims.chatgpt_account_id !== undefined &&
179
+ claims.chatgpt_account_id !== ""
180
+ ) {
181
+ return Option.some(claims.chatgpt_account_id)
182
+ }
183
+
184
+ const nestedAccountId =
185
+ claims["https://api.openai.com/auth"]?.chatgpt_account_id
186
+ if (nestedAccountId !== undefined && nestedAccountId !== "") {
187
+ return Option.some(nestedAccountId)
188
+ }
189
+
190
+ const organizationId = claims.organizations?.[0]?.id
191
+ if (organizationId !== undefined && organizationId !== "") {
192
+ return Option.some(organizationId)
193
+ }
194
+
195
+ return Option.none()
196
+ }
197
+
198
+ export const extractAccountIdFromToken = (
199
+ token: string,
200
+ ): Option.Option<string> =>
201
+ parseJwtClaims(token).pipe(Option.flatMap(extractAccountIdFromClaims))
202
+
203
+ const normalizePollInterval = (interval: string): number =>
204
+ Math.max(
205
+ Number.parseInt(interval, 10) || DEFAULT_DEVICE_POLL_INTERVAL_SECONDS,
206
+ 1,
207
+ ) * 1_000
208
+
209
+ const extractAccountIdFromTokens = (
210
+ token: TokenResponse,
211
+ ): Option.Option<string> => {
212
+ if (token.id_token !== undefined && token.id_token !== "") {
213
+ const accountId = extractAccountIdFromToken(token.id_token)
214
+ if (Option.isSome(accountId)) {
215
+ return accountId
216
+ }
217
+ }
218
+
219
+ return extractAccountIdFromToken(token.access_token)
220
+ }
221
+
222
+ const applyTokenHeaders = (
223
+ request: HttpClientRequest.HttpClientRequest,
224
+ token: TokenData,
225
+ ): HttpClientRequest.HttpClientRequest => {
226
+ const authenticatedRequest = request.pipe(
227
+ HttpClientRequest.bearerToken(token.access),
228
+ )
229
+
230
+ return Option.match(token.accountId, {
231
+ onNone: () => authenticatedRequest,
232
+ onSome: (accountId) =>
233
+ authenticatedRequest.pipe(
234
+ HttpClientRequest.setHeader(ACCOUNT_ID_HEADER, accountId),
235
+ ),
236
+ })
237
+ }
238
+
239
+ const toTokenDataFromResponse = (token: TokenResponse): TokenData =>
240
+ new TokenData({
241
+ access: token.access_token,
242
+ refresh: token.refresh_token,
243
+ expires:
244
+ Date.now() + (token.expires_in ?? DEFAULT_TOKEN_EXPIRY_SECONDS) * 1_000,
245
+ accountId: extractAccountIdFromTokens(token),
246
+ })
247
+
248
+ const preserveAccountId = (
249
+ token: TokenData,
250
+ fallback: Option.Option<string>,
251
+ ): TokenData => {
252
+ if (Option.isSome(token.accountId) || Option.isNone(fallback)) {
253
+ return token
254
+ }
255
+
256
+ return new TokenData({
257
+ access: token.access,
258
+ refresh: token.refresh,
259
+ expires: token.expires,
260
+ accountId: fallback,
261
+ })
262
+ }
263
+
264
+ const requestDeviceCodeError = (message: string, cause?: unknown) =>
265
+ new CodexAuthError({
266
+ reason: "DeviceFlowFailed",
267
+ message,
268
+ ...(cause === undefined ? {} : { cause }),
269
+ })
270
+
271
+ const tokenExchangeError = (message: string, cause?: unknown) =>
272
+ new CodexAuthError({
273
+ reason: "TokenExchangeFailed",
274
+ message,
275
+ ...(cause === undefined ? {} : { cause }),
276
+ })
277
+
278
+ const refreshTokenError = (message: string, cause?: unknown) =>
279
+ new CodexAuthError({
280
+ reason: "RefreshFailed",
281
+ message,
282
+ ...(cause === undefined ? {} : { cause }),
283
+ })
284
+
285
+ export const toCodexAuthKeyValueStore = (store: KeyValueStore.KeyValueStore) =>
286
+ KeyValueStore.prefix(store, STORE_PREFIX)
287
+
288
+ export const toTokenStore = (store: KeyValueStore.KeyValueStore) =>
289
+ KeyValueStore.toSchemaStore(toCodexAuthKeyValueStore(store), TokenData)
290
+
291
+ export class CodexAuth extends ServiceMap.Service<
292
+ CodexAuth,
293
+ {
294
+ readonly get: Effect.Effect<TokenData, CodexAuthError>
295
+ readonly authenticate: Effect.Effect<TokenData, CodexAuthError>
296
+ readonly logout: Effect.Effect<void>
297
+ }
298
+ >()("clanka/CodexAuth") {
299
+ static readonly make = Effect.gen(function* () {
300
+ const tokenStore = toTokenStore(yield* KeyValueStore.KeyValueStore)
301
+ const httpClient = (yield* HttpClient.HttpClient).pipe(
302
+ HttpClient.mapRequest(flow(HttpClientRequest.prependUrl(ISSUER))),
303
+ HttpClient.filterStatusOk,
304
+ HttpClient.retryTransient({
305
+ times: 5,
306
+ schedule: Schedule.exponential(150).pipe(
307
+ Schedule.either(Schedule.spaced(5000)),
308
+ ),
309
+ }),
310
+ )
311
+ const semaphore = Semaphore.makeUnsafe(1)
312
+
313
+ let currentToken = yield* tokenStore.get(STORE_TOKEN_KEY).pipe(
314
+ Effect.catchTag("SchemaError", (error) =>
315
+ Console.warn(
316
+ `Failed to decode persisted Codex token, clearing it: ${error.message}`,
317
+ ).pipe(
318
+ Effect.andThen(tokenStore.remove(STORE_TOKEN_KEY)),
319
+ Effect.as(Option.none()),
320
+ ),
321
+ ),
322
+ Effect.orDie,
323
+ )
324
+
325
+ const saveToken = (token: TokenData) =>
326
+ Effect.orDie(tokenStore.set(STORE_TOKEN_KEY, token)).pipe(
327
+ Effect.tap(() =>
328
+ Effect.sync(() => {
329
+ currentToken = Option.some(token)
330
+ }),
331
+ ),
332
+ Effect.as(token),
333
+ )
334
+
335
+ const clearToken = Effect.orDie(tokenStore.remove(STORE_TOKEN_KEY)).pipe(
336
+ Effect.tap(() =>
337
+ Effect.sync(() => {
338
+ currentToken = Option.none()
339
+ }),
340
+ ),
341
+ )
342
+
343
+ const authenticateWithDeviceFlow = Effect.gen(function* () {
344
+ const deviceCode = yield* requestDeviceCode
345
+ yield* Console.log(
346
+ `Open ${ISSUER}${DEVICE_VERIFICATION_URL} and enter code: ${deviceCode.userCode}`,
347
+ )
348
+ const authorization = yield* pollAuthorization(deviceCode)
349
+ return yield* exchangeAuthorizationCode(authorization)
350
+ })
351
+
352
+ const authenticateNoLock = Effect.uninterruptibleMask((restore) =>
353
+ Effect.gen(function* () {
354
+ const token = yield* restore(authenticateWithDeviceFlow)
355
+ return yield* saveToken(token)
356
+ }),
357
+ )
358
+
359
+ const getNoLock = Effect.uninterruptibleMask((restore) =>
360
+ Effect.gen(function* () {
361
+ if (Option.isSome(currentToken) && !currentToken.value.isExpired()) {
362
+ return currentToken.value
363
+ }
364
+
365
+ if (Option.isNone(currentToken)) {
366
+ const token = yield* restore(authenticateWithDeviceFlow)
367
+ return yield* saveToken(token)
368
+ }
369
+
370
+ const refreshedToken = yield* restore(
371
+ refreshToken(currentToken.value.refresh).pipe(
372
+ Effect.tapError((error) =>
373
+ Console.warn(
374
+ `Codex token refresh failed, falling back to device auth: ${error.message}`,
375
+ ),
376
+ ),
377
+ Effect.option,
378
+ ),
379
+ )
380
+
381
+ if (Option.isSome(refreshedToken)) {
382
+ return yield* saveToken(
383
+ preserveAccountId(
384
+ refreshedToken.value,
385
+ currentToken.value.accountId,
386
+ ),
387
+ )
388
+ }
389
+
390
+ yield* clearToken
391
+ const token = yield* restore(authenticateWithDeviceFlow)
392
+ return yield* saveToken(token)
393
+ }),
394
+ )
395
+
396
+ const requestDeviceCode = Effect.gen(function* (): Effect.fn.Return<
397
+ DeviceCodeData,
398
+ CodexAuthError
399
+ > {
400
+ const response = yield* HttpClientRequest.post(DEVICE_CODE_URL).pipe(
401
+ HttpClientRequest.bodyJsonUnsafe({
402
+ client_id: CLIENT_ID,
403
+ }),
404
+ httpClient.execute,
405
+ Effect.mapError((cause) =>
406
+ requestDeviceCodeError(
407
+ "Failed to request a Codex device authorization code",
408
+ cause,
409
+ ),
410
+ ),
411
+ )
412
+
413
+ const payload = yield* HttpClientResponse.schemaBodyJson(
414
+ DeviceCodeResponseSchema,
415
+ )(response).pipe(
416
+ Effect.mapError((cause) =>
417
+ requestDeviceCodeError(
418
+ "Failed to decode the Codex device authorization response",
419
+ cause,
420
+ ),
421
+ ),
422
+ )
423
+
424
+ return {
425
+ deviceAuthId: payload.device_auth_id,
426
+ userCode: payload.user_code,
427
+ intervalMs: normalizePollInterval(payload.interval),
428
+ }
429
+ }).pipe(Effect.withSpan("CodexAuth.requestDeviceCode"))
430
+
431
+ const pollAuthorization = Effect.fn("CodexAuth.pollAuthorization")(
432
+ function* (
433
+ deviceCode: DeviceCodeData,
434
+ ): Effect.fn.Return<AuthorizationCodeData, CodexAuthError> {
435
+ const request = HttpClientRequest.post(DEVICE_TOKEN_URL).pipe(
436
+ HttpClientRequest.bodyJsonUnsafe({
437
+ device_auth_id: deviceCode.deviceAuthId,
438
+ user_code: deviceCode.userCode,
439
+ }),
440
+ )
441
+
442
+ const delayMs = deviceCode.intervalMs + POLLING_SAFETY_MARGIN_MS
443
+
444
+ return yield* httpClient.execute(request).pipe(
445
+ Effect.retry({
446
+ while: (e) =>
447
+ e.response?.status === 403 || e.response?.status === 404,
448
+ schedule: Schedule.spaced(delayMs),
449
+ }),
450
+ Effect.mapError((cause) =>
451
+ requestDeviceCodeError(
452
+ "Failed to poll Codex device authorization",
453
+ cause,
454
+ ),
455
+ ),
456
+ Effect.flatMap((response) =>
457
+ HttpClientResponse.schemaBodyJson(AuthorizationCodeResponseSchema)(
458
+ response,
459
+ ).pipe(
460
+ Effect.mapError((cause) =>
461
+ requestDeviceCodeError(
462
+ "Failed to decode the Codex authorization approval response",
463
+ cause,
464
+ ),
465
+ ),
466
+ Effect.map((payload) => ({
467
+ authorizationCode: payload.authorization_code,
468
+ codeVerifier: payload.code_verifier,
469
+ })),
470
+ ),
471
+ ),
472
+ )
473
+ },
474
+ )
475
+
476
+ const exchangeAuthorizationCode = Effect.fn(
477
+ "CodexAuth.exchangeAuthorizationCode",
478
+ )(function* (
479
+ authorization: AuthorizationCodeData,
480
+ ): Effect.fn.Return<TokenData, CodexAuthError> {
481
+ const response = yield* HttpClientRequest.post(TOKEN_URL).pipe(
482
+ HttpClientRequest.bodyUrlParams({
483
+ grant_type: "authorization_code",
484
+ code: authorization.authorizationCode,
485
+ redirect_uri: DEVICE_REDIRECT_URI,
486
+ client_id: CLIENT_ID,
487
+ code_verifier: authorization.codeVerifier,
488
+ }),
489
+ httpClient.execute,
490
+ Effect.mapError((cause) =>
491
+ tokenExchangeError(
492
+ "Failed to exchange the Codex authorization code",
493
+ cause,
494
+ ),
495
+ ),
496
+ )
497
+
498
+ const payload = yield* HttpClientResponse.schemaBodyJson(
499
+ TokenResponseSchema,
500
+ )(response).pipe(
501
+ Effect.mapError((cause) =>
502
+ tokenExchangeError(
503
+ "Failed to decode the Codex token exchange response",
504
+ cause,
505
+ ),
506
+ ),
507
+ )
508
+
509
+ return toTokenDataFromResponse(payload)
510
+ })
511
+
512
+ const refreshToken = Effect.fn("CodexAuth.refreshToken")(function* (
513
+ refresh: string,
514
+ ): Effect.fn.Return<TokenData, CodexAuthError> {
515
+ const response = yield* HttpClientRequest.post(TOKEN_URL).pipe(
516
+ HttpClientRequest.bodyUrlParams({
517
+ grant_type: "refresh_token",
518
+ refresh_token: refresh,
519
+ client_id: CLIENT_ID,
520
+ }),
521
+ httpClient.execute,
522
+ Effect.mapError((cause) =>
523
+ refreshTokenError("Failed to refresh the Codex access token", cause),
524
+ ),
525
+ )
526
+
527
+ const payload = yield* HttpClientResponse.schemaBodyJson(
528
+ TokenResponseSchema,
529
+ )(response).pipe(
530
+ Effect.mapError((cause) =>
531
+ refreshTokenError(
532
+ "Failed to decode the Codex refresh token response",
533
+ cause,
534
+ ),
535
+ ),
536
+ )
537
+
538
+ return toTokenDataFromResponse(payload)
539
+ })
540
+
541
+ return CodexAuth.of({
542
+ get: semaphore.withPermit(getNoLock),
543
+ authenticate: semaphore.withPermit(authenticateNoLock),
544
+ logout: semaphore.withPermit(Effect.uninterruptible(clearToken)),
545
+ })
546
+ })
547
+
548
+ static readonly layer = Layer.effect(CodexAuth, CodexAuth.make)
549
+
550
+ static readonly layerClientNoDeps = Layer.effect(
551
+ HttpClient.HttpClient,
552
+ Effect.gen(function* () {
553
+ const auth = yield* CodexAuth
554
+ const httpClient = yield* HttpClient.HttpClient
555
+
556
+ const injectAuthHeaders = (
557
+ request: HttpClientRequest.HttpClientRequest,
558
+ ): Effect.Effect<HttpClientRequest.HttpClientRequest> =>
559
+ auth.get.pipe(
560
+ Effect.map((token) => applyTokenHeaders(request, token)),
561
+ Effect.orDie,
562
+ )
563
+
564
+ return httpClient.pipe(HttpClient.mapRequestEffect(injectAuthHeaders))
565
+ }),
566
+ )
567
+
568
+ static readonly layerClient = this.layerClientNoDeps.pipe(
569
+ Layer.provide(CodexAuth.layer),
570
+ )
571
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+ import {
5
+ Cause,
6
+ Console,
7
+ Effect,
8
+ FiberSet,
9
+ Layer,
10
+ Queue,
11
+ ServiceMap,
12
+ Stream,
13
+ } from "effect"
14
+ import { Tool, Toolkit } from "effect/unstable/ai"
15
+ import * as NodeConsole from "node:console"
16
+ import * as NodeVm from "node:vm"
17
+ import { Writable } from "node:stream"
18
+
19
+ /**
20
+ * @since 1.0.0
21
+ * @category Services
22
+ */
23
+ export class Executor extends ServiceMap.Service<
24
+ Executor,
25
+ {
26
+ execute<Tools extends Record<string, Tool.Any>>(options: {
27
+ readonly tools: Toolkit.WithHandler<Tools>
28
+ readonly script: string
29
+ }): Stream.Stream<string, never, Tool.HandlerServices<Tools[keyof Tools]>>
30
+ }
31
+ >()("clanka/Executor") {
32
+ static readonly layer = Layer.effect(
33
+ Executor,
34
+ // oxlint-disable-next-line require-yield
35
+ Effect.gen(function* () {
36
+ const execute = Effect.fnUntraced(function* <
37
+ Tools extends Record<string, Tool.Any>,
38
+ >(options: {
39
+ readonly tools: Toolkit.WithHandler<Tools>
40
+ readonly script: string
41
+ }) {
42
+ const output = yield* Queue.unbounded<string, Cause.Done>()
43
+
44
+ yield* Effect.gen(function* () {
45
+ const console = yield* Console.Console
46
+ const services = yield* Effect.services()
47
+ const runPromise = yield* FiberSet.makeRuntimePromise()
48
+
49
+ const script = new NodeVm.Script(`async function main() {
50
+ ${options.script}
51
+ }`)
52
+ const sandbox: ScriptSandbox = {
53
+ main: defaultMain,
54
+ console,
55
+ fetch,
56
+ process: undefined,
57
+ }
58
+
59
+ for (const [name, tool] of Object.entries(options.tools.tools)) {
60
+ const handler = services.mapUnsafe.get(
61
+ tool.id,
62
+ ) as Tool.Handler<string>
63
+ // oxlint-disable-next-line typescript/no-explicit-any
64
+ sandbox[name] = function (params: any) {
65
+ return handler
66
+ .handler(params, {})
67
+ .pipe(Effect.provideServices(handler.services), runPromise)
68
+ }
69
+ }
70
+
71
+ script.runInNewContext(sandbox, {
72
+ timeout: 1000,
73
+ })
74
+ yield* Effect.promise(sandbox.main)
75
+ }).pipe(
76
+ Effect.timeout("3 minutes"),
77
+ Effect.catchCause(Effect.logFatal),
78
+ Effect.provideServiceEffect(Console.Console, makeConsole(output)),
79
+ Effect.scoped,
80
+ Effect.ensuring(Queue.end(output)),
81
+ Effect.forkScoped,
82
+ )
83
+
84
+ return Stream.fromQueue(output)
85
+ }, Stream.unwrap)
86
+
87
+ return Executor.of({
88
+ execute,
89
+ })
90
+ }),
91
+ )
92
+ }
93
+
94
+ interface ScriptSandbox {
95
+ main: () => Promise<void>
96
+ console: Console.Console
97
+ [toolName: string]: unknown
98
+ }
99
+
100
+ const defaultMain = () => Promise.resolve()
101
+
102
+ const makeConsole = Effect.fn(function* (
103
+ queue: Queue.Queue<string, Cause.Done>,
104
+ ) {
105
+ const writable = new QueueWriteStream(queue)
106
+ const newConsole = new NodeConsole.Console(writable)
107
+ yield* Effect.addFinalizer(() => {
108
+ writable.end()
109
+ return Effect.void
110
+ })
111
+ return newConsole
112
+ })
113
+
114
+ class QueueWriteStream extends Writable {
115
+ readonly queue: Queue.Enqueue<string, Cause.Done>
116
+ constructor(queue: Queue.Enqueue<string, Cause.Done>) {
117
+ super()
118
+ this.queue = queue
119
+ }
120
+ _write(
121
+ // oxlint-disable-next-line typescript/no-explicit-any
122
+ chunk: any,
123
+ _encoding: BufferEncoding,
124
+ callback: (error?: Error | null) => void,
125
+ ): void {
126
+ Queue.offerUnsafe(this.queue, chunk.toString())
127
+ callback()
128
+ }
129
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+ import { Sink } from "effect"
5
+ import type { Output } from "./Agent.ts"
6
+
7
+ /**
8
+ * @since 1.0.0
9
+ * @category Models
10
+ */
11
+ export type OutputFormatter<E = never, R = never> = Sink.Sink<
12
+ void,
13
+ Output,
14
+ never,
15
+ E,
16
+ R
17
+ >