clanka 0.0.26 → 0.0.27

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 (44) hide show
  1. package/dist/Agent.d.ts +5 -4
  2. package/dist/Agent.d.ts.map +1 -1
  3. package/dist/Agent.js.map +1 -1
  4. package/dist/Agent.test.js +7 -8
  5. package/dist/Agent.test.js.map +1 -1
  6. package/dist/AgentTools.d.ts +274 -2
  7. package/dist/AgentTools.d.ts.map +1 -1
  8. package/dist/AgentTools.js +37 -2
  9. package/dist/AgentTools.js.map +1 -1
  10. package/dist/ApplyPatch.d.ts.map +1 -1
  11. package/dist/ApplyPatch.js +6 -2
  12. package/dist/ApplyPatch.js.map +1 -1
  13. package/dist/ApplyPatch.test.js +39 -0
  14. package/dist/ApplyPatch.test.js.map +1 -1
  15. package/dist/Codex.d.ts +1 -1
  16. package/dist/CodexAuth.d.ts +4 -4
  17. package/dist/Copilot.d.ts +1 -1
  18. package/dist/CopilotAuth.d.ts +3 -3
  19. package/dist/ExaSearch.d.ts +37 -0
  20. package/dist/ExaSearch.d.ts.map +1 -0
  21. package/dist/ExaSearch.js +56 -0
  22. package/dist/ExaSearch.js.map +1 -0
  23. package/dist/McpClient.d.ts +35 -0
  24. package/dist/McpClient.d.ts.map +1 -0
  25. package/dist/McpClient.js +51 -0
  26. package/dist/McpClient.js.map +1 -0
  27. package/dist/WebToMarkdown.d.ts +22 -0
  28. package/dist/WebToMarkdown.d.ts.map +1 -0
  29. package/dist/WebToMarkdown.js +66 -0
  30. package/dist/WebToMarkdown.js.map +1 -0
  31. package/package.json +13 -10
  32. package/src/Agent.test.ts +15 -9
  33. package/src/Agent.ts +11 -5
  34. package/src/AgentTools.ts +49 -1
  35. package/src/ApplyPatch.test.ts +44 -0
  36. package/src/ApplyPatch.ts +6 -2
  37. package/src/ExaSearch.ts +78 -0
  38. package/src/McpClient.ts +81 -0
  39. package/src/WebToMarkdown.ts +87 -0
  40. package/dist/AgentTools.test.d.ts +0 -2
  41. package/dist/AgentTools.test.d.ts.map +0 -1
  42. package/dist/AgentTools.test.js +0 -714
  43. package/dist/AgentTools.test.js.map +0 -1
  44. package/src/AgentTools.test.ts +0 -954
@@ -0,0 +1,66 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+ import { Effect, Layer, ServiceMap } from "effect";
5
+ import { HttpClient, HttpClientError } from "effect/unstable/http";
6
+ import TurndownService from "turndown";
7
+ /**
8
+ * @since 1.0.0
9
+ * @category Services
10
+ */
11
+ export class WebToMarkdown extends ServiceMap.Service()("clanka/WebToMarkdown") {
12
+ }
13
+ /**
14
+ * @since 1.0.0
15
+ * @category Layers
16
+ */
17
+ export const layer = Layer.effect(WebToMarkdown, Effect.gen(function* () {
18
+ const client = (yield* HttpClient.HttpClient).pipe(HttpClient.filterStatusOk, HttpClient.retryTransient({
19
+ times: 3,
20
+ }));
21
+ const toRemove = new Set([
22
+ "head",
23
+ "footer",
24
+ "header",
25
+ "script",
26
+ "style",
27
+ "meta",
28
+ "link",
29
+ "noscript",
30
+ "iframe",
31
+ "object",
32
+ "embed",
33
+ "svg",
34
+ "canvas",
35
+ "audio",
36
+ "video",
37
+ "source",
38
+ "track",
39
+ "map",
40
+ "area",
41
+ "base",
42
+ "form",
43
+ "input",
44
+ "textarea",
45
+ "button",
46
+ "select",
47
+ "option",
48
+ "optgroup",
49
+ "datalist",
50
+ "keygen",
51
+ "output",
52
+ "progress",
53
+ "meter",
54
+ ]);
55
+ const turndown = new TurndownService().remove((node) => toRemove.has(node.nodeName.toLowerCase()));
56
+ const convertHtml = Effect.fn("WebToMarkdown.convertHtml")((html) => Effect.sync(() => turndown.turndown(html)));
57
+ return WebToMarkdown.of({
58
+ convertHtml,
59
+ convertUrl: Effect.fn("WebToMarkdown.convertUrl")(function* (url) {
60
+ const response = yield* client.get(url);
61
+ const html = yield* response.text;
62
+ return turndown.turndown(html);
63
+ }),
64
+ });
65
+ }));
66
+ //# sourceMappingURL=WebToMarkdown.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WebToMarkdown.js","sourceRoot":"","sources":["../src/WebToMarkdown.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AAClD,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAClE,OAAO,eAAe,MAAM,UAAU,CAAA;AAEtC;;;GAGG;AACH,MAAM,OAAO,aAAc,SAAQ,UAAU,CAAC,OAAO,EAQlD,CAAC,sBAAsB,CAAC;CAAG;AAE9B;;;GAGG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAC/B,aAAa,EACb,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAClB,MAAM,MAAM,GAAG,CAAC,KAAK,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,IAAI,CAChD,UAAU,CAAC,cAAc,EACzB,UAAU,CAAC,cAAc,CAAC;QACxB,KAAK,EAAE,CAAC;KACT,CAAC,CACH,CAAA;IAED,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC;QACvB,MAAM;QACN,QAAQ;QACR,QAAQ;QACR,QAAQ;QACR,OAAO;QACP,MAAM;QACN,MAAM;QACN,UAAU;QACV,QAAQ;QACR,QAAQ;QACR,OAAO;QACP,KAAK;QACL,QAAQ;QACR,OAAO;QACP,OAAO;QACP,QAAQ;QACR,OAAO;QACP,KAAK;QACL,MAAM;QACN,MAAM;QACN,MAAM;QACN,OAAO;QACP,UAAU;QACV,QAAQ;QACR,QAAQ;QACR,QAAQ;QACR,UAAU;QACV,UAAU;QACV,QAAQ;QACR,QAAQ;QACR,UAAU;QACV,OAAO;KACR,CAAC,CAAA;IACF,MAAM,QAAQ,GAAG,IAAI,eAAe,EAAE,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CACrD,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,CAC1C,CAAA;IAED,MAAM,WAAW,GAAG,MAAM,CAAC,EAAE,CAAC,2BAA2B,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,CAClE,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAC3C,CAAA;IAED,OAAO,aAAa,CAAC,EAAE,CAAC;QACtB,WAAW;QACX,UAAU,EAAE,MAAM,CAAC,EAAE,CAAC,0BAA0B,CAAC,CAAC,QAAQ,CAAC,EAAE,GAAG;YAC9D,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;YACvC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAA;YACjC,OAAO,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAA;QAChC,CAAC,CAAC;KACH,CAAC,CAAA;AACJ,CAAC,CAAC,CACH,CAAA"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clanka",
3
3
  "type": "module",
4
- "version": "0.0.26",
4
+ "version": "0.0.27",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -22,27 +22,30 @@
22
22
  "url": "https://github.com/tim-smart/clanka.git"
23
23
  },
24
24
  "dependencies": {
25
+ "@modelcontextprotocol/sdk": "^1.27.1",
25
26
  "@vscode/ripgrep": "^1.17.0",
26
27
  "chalk": "^5.6.2",
27
- "glob": "^13.0.6"
28
+ "glob": "^13.0.6",
29
+ "turndown": "^7.2.2"
28
30
  },
29
31
  "peerDependencies": {
30
- "@effect/ai-openai": "4.0.0-beta.30",
31
- "@effect/ai-openai-compat": "4.0.0-beta.30",
32
- "effect": "4.0.0-beta.30"
32
+ "@effect/ai-openai": "4.0.0-beta.31",
33
+ "@effect/ai-openai-compat": "4.0.0-beta.31",
34
+ "effect": "4.0.0-beta.31"
33
35
  },
34
36
  "devDependencies": {
35
37
  "@changesets/changelog-github": "^0.5.2",
36
38
  "@changesets/cli": "^2.29.8",
37
- "@effect/ai-openai": "4.0.0-beta.30",
38
- "@effect/ai-openai-compat": "4.0.0-beta.30",
39
+ "@effect/ai-openai": "4.0.0-beta.31",
40
+ "@effect/ai-openai-compat": "4.0.0-beta.31",
39
41
  "@effect/language-service": "^0.75.1",
40
- "@effect/platform-node": "4.0.0-beta.30",
41
- "@effect/vitest": "4.0.0-beta.30",
42
+ "@effect/platform-node": "4.0.0-beta.31",
43
+ "@effect/vitest": "4.0.0-beta.31",
42
44
  "@linear/sdk": "^75.0.0",
43
45
  "@types/node": "^25.3.5",
46
+ "@types/turndown": "^5.0.6",
44
47
  "@typescript/native-preview": "7.0.0-dev.20260219.1",
45
- "effect": "4.0.0-beta.30",
48
+ "effect": "4.0.0-beta.31",
46
49
  "husky": "^9.1.7",
47
50
  "lint-staged": "^16.2.7",
48
51
  "oxlint": "^1.49.0",
package/src/Agent.test.ts CHANGED
@@ -2,10 +2,13 @@ import { NodeServices } from "@effect/platform-node"
2
2
  import { Effect, Layer, Stream } from "effect"
3
3
  import { describe, it } from "@effect/vitest"
4
4
  import { expect } from "vitest"
5
- import { AgentModelConfig, layerServices, make } from "./Agent.ts"
5
+ import { AgentModelConfig, make } from "./Agent.ts"
6
6
  import { pretty } from "./OutputFormatter.ts"
7
7
  import { LanguageModel, Prompt } from "effect/unstable/ai"
8
8
  import * as Model from "effect/unstable/ai/Model"
9
+ import { Executor } from "./Executor.ts"
10
+ import { ToolkitRenderer } from "./ToolkitRenderer.ts"
11
+ import { AgentToolHandlersTest } from "./AgentTools.ts"
9
12
 
10
13
  const usage = {
11
14
  inputTokens: {
@@ -140,14 +143,17 @@ describe("Agent", () => {
140
143
  "root summary: child summary: grandchild summary",
141
144
  )
142
145
  }).pipe(
143
- Effect.provide([
144
- layerServices,
145
- TestModel,
146
- AgentModelConfig.layer({
147
- supportsNoTools: true,
148
- }),
149
- ]),
150
- Effect.provide(NodeServices.layer),
146
+ Effect.provide(
147
+ Layer.mergeAll(
148
+ AgentToolHandlersTest,
149
+ TestModel,
150
+ AgentModelConfig.layer({
151
+ supportsNoTools: true,
152
+ }),
153
+ Executor.layer,
154
+ ToolkitRenderer.layer,
155
+ ).pipe(Layer.provideMerge(NodeServices.layer)),
156
+ ),
151
157
  ),
152
158
  )
153
159
  })
package/src/Agent.ts CHANGED
@@ -36,6 +36,7 @@ import { ToolkitRenderer } from "./ToolkitRenderer.ts"
36
36
  import { ModelName, ProviderName } from "effect/unstable/ai/Model"
37
37
  import { type StreamPart } from "effect/unstable/ai/Response"
38
38
  import type { ChildProcessSpawner } from "effect/unstable/process"
39
+ import type { HttpClient } from "effect/unstable/http"
39
40
 
40
41
  /**
41
42
  * @since 1.0.0
@@ -62,7 +63,8 @@ export interface Agent {
62
63
  * @category Constructors
63
64
  */
64
65
  export const make: <
65
- Tools extends Record<string, Tool.Any> = {},
66
+ // oxlint-disable-next-line typescript/no-explicit-any
67
+ Toolkit extends Toolkit.Toolkit<any> = never,
66
68
  SE = never,
67
69
  SR = never,
68
70
  >(options: {
@@ -82,7 +84,7 @@ export const make: <
82
84
  }) => string)
83
85
  | undefined
84
86
  /** Additional tools to provide to the agent */
85
- readonly tools?: Toolkit.Toolkit<Tools> | undefined
87
+ readonly tools?: Toolkit | undefined
86
88
  /** Layer to use for subagents */
87
89
  readonly subagentModel?:
88
90
  | Layer.Layer<
@@ -102,9 +104,10 @@ export const make: <
102
104
  | ProviderName
103
105
  | ModelName
104
106
  | ToolkitRenderer
105
- | Tool.HandlersFor<Tools>
107
+ | (Toolkit extends Toolkit.Toolkit<infer T>
108
+ ? Tool.HandlersFor<T> | Tool.HandlerServices<T[keyof T]>
109
+ : never)
106
110
  | Tool.HandlersFor<typeof AgentTools.tools>
107
- | Tool.HandlerServices<Tools[keyof Tools]>
108
111
  | SR
109
112
  > = Effect.fnUntraced(function* (options: {
110
113
  readonly directory: string
@@ -628,7 +631,10 @@ export class AgentModelConfig extends ServiceMap.Reference<{
628
631
  export const layerServices: Layer.Layer<
629
632
  Tool.HandlersFor<typeof AgentTools.tools> | Executor | ToolkitRenderer,
630
633
  never,
631
- FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner
634
+ | FileSystem.FileSystem
635
+ | Path.Path
636
+ | ChildProcessSpawner.ChildProcessSpawner
637
+ | HttpClient.HttpClient
632
638
  > = Layer.mergeAll(AgentToolHandlers, Executor.layer, ToolkitRenderer.layer)
633
639
 
634
640
  /**
package/src/AgentTools.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  Deferred,
8
8
  Effect,
9
9
  FileSystem,
10
+ Layer,
10
11
  Path,
11
12
  pipe,
12
13
  Schema,
@@ -17,6 +18,8 @@ import { Tool, Toolkit } from "effect/unstable/ai"
17
18
  import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
18
19
  import * as Glob from "glob"
19
20
  import { parsePatch, patchChunks } from "./ApplyPatch.ts"
21
+ import * as ExaSearch from "./ExaSearch.ts"
22
+ import * as WebToMarkdown from "./WebToMarkdown.ts"
20
23
 
21
24
  /**
22
25
  * @since 1.0.0
@@ -175,6 +178,18 @@ export const AgentTools = Toolkit.make(
175
178
  success: Schema.String,
176
179
  dependencies: [SubagentContext],
177
180
  }),
181
+ Tool.make("webSearch", {
182
+ description: "Search the web for recent information.",
183
+ parameters: ExaSearch.ExaSearchOptions,
184
+ success: Schema.String,
185
+ }),
186
+ Tool.make("fetchMarkdown", {
187
+ description: "Fetch a web page and convert it to markdown.",
188
+ parameters: Schema.String.annotate({
189
+ identifier: "url",
190
+ }),
191
+ success: Schema.String,
192
+ }),
178
193
  Tool.make("sleep", {
179
194
  description: "Sleep for a specified number of milliseconds",
180
195
  parameters: Schema.Finite.annotate({
@@ -195,11 +210,13 @@ export const AgentTools = Toolkit.make(
195
210
  * @since 1.0.0
196
211
  * @category Toolkit
197
212
  */
198
- export const AgentToolHandlers = AgentTools.toLayer(
213
+ export const AgentToolHandlersNoDeps = AgentTools.toLayer(
199
214
  Effect.gen(function* () {
200
215
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
201
216
  const fs = yield* FileSystem.FileSystem
202
217
  const pathService = yield* Path.Path
218
+ const webSearch = yield* ExaSearch.ExaSearch
219
+ const fetchMarkdown = yield* WebToMarkdown.WebToMarkdown
203
220
 
204
221
  const execute = Effect.fn(function* (command: ChildProcess.Command) {
205
222
  const handle = yield* spawner.spawn(command)
@@ -356,6 +373,18 @@ export const AgentToolHandlers = AgentTools.toLayer(
356
373
  })
357
374
  return yield* execute(cmd)
358
375
  }, Effect.orDie),
376
+ webSearch: Effect.fn("AgentTools.webSearch")(function* (options) {
377
+ yield* Effect.logInfo(`Calling "webSearch"`).pipe(
378
+ Effect.annotateLogs(options),
379
+ )
380
+ return yield* webSearch.search(options)
381
+ }, Effect.orDie),
382
+ fetchMarkdown: Effect.fn("AgentTools.fetchMarkdown")(function* (url) {
383
+ yield* Effect.logInfo(`Calling "fetchMarkdown"`).pipe(
384
+ Effect.annotateLogs({ url }),
385
+ )
386
+ return yield* fetchMarkdown.convertUrl(url)
387
+ }, Effect.orDie),
359
388
  sleep: Effect.fn("AgentTools.sleep")(function* (ms) {
360
389
  yield* Effect.logInfo(`Calling "sleep" for ${ms}ms`)
361
390
  return yield* Effect.sleep(ms)
@@ -507,6 +536,25 @@ export const AgentToolHandlers = AgentTools.toLayer(
507
536
  }),
508
537
  )
509
538
 
539
+ /**
540
+ * @since 1.0.0
541
+ * @category Layers
542
+ */
543
+ export const AgentToolHandlers = AgentToolHandlersNoDeps.pipe(
544
+ Layer.provide([ExaSearch.layer, WebToMarkdown.layer]),
545
+ )
546
+
547
+ /**
548
+ * @since 1.0.0
549
+ * @category Layers
550
+ */
551
+ export const AgentToolHandlersTest = AgentToolHandlersNoDeps.pipe(
552
+ Layer.provide([
553
+ Layer.mock(ExaSearch.ExaSearch)({}),
554
+ Layer.mock(WebToMarkdown.WebToMarkdown)({}),
555
+ ]),
556
+ )
557
+
510
558
  class ApplyPatchError extends Data.TaggedClass("ApplyPatchError")<{
511
559
  readonly message: string
512
560
  }> {}
@@ -28,6 +28,50 @@ describe("patchContent", () => {
28
28
  ).toBe("alpha\nbeta\nomega\n")
29
29
  })
30
30
 
31
+ it("parses wrapped patches without an end marker at EOF", () => {
32
+ expect(
33
+ parsePatch(
34
+ [
35
+ "*** Begin Patch",
36
+ "*** Update File: src/ExaSearch.ts",
37
+ "@@",
38
+ " export class ExaSearch extends ServiceMap.Service<",
39
+ " ExaSearch,",
40
+ " {",
41
+ "- search(query: string): Effect.Effect<Array<SearchResponse<{}>>, ExaError>",
42
+ "+ search(query: string): Effect.Effect<SearchResponse<{}>, ExaError>",
43
+ " }",
44
+ ' >()("clanka/ExaSearch") {}',
45
+ ].join("\n"),
46
+ ),
47
+ ).toEqual([
48
+ {
49
+ type: "update",
50
+ path: "src/ExaSearch.ts",
51
+ chunks: [
52
+ {
53
+ old: [
54
+ "export class ExaSearch extends ServiceMap.Service<",
55
+ " ExaSearch,",
56
+ " {",
57
+ " search(query: string): Effect.Effect<Array<SearchResponse<{}>>, ExaError>",
58
+ " }",
59
+ '>()("clanka/ExaSearch") {}',
60
+ ],
61
+ next: [
62
+ "export class ExaSearch extends ServiceMap.Service<",
63
+ " ExaSearch,",
64
+ " {",
65
+ " search(query: string): Effect.Effect<SearchResponse<{}>, ExaError>",
66
+ " }",
67
+ '>()("clanka/ExaSearch") {}',
68
+ ],
69
+ },
70
+ ],
71
+ },
72
+ ])
73
+ })
74
+
31
75
  it("parses multi-file wrapped patches", () => {
32
76
  expect(
33
77
  parsePatch(
package/src/ApplyPatch.ts CHANGED
@@ -55,8 +55,12 @@ const fail = (message: string): never => {
55
55
  const locate = (text: string) => {
56
56
  const lines = text.split("\n")
57
57
  const begin = lines.findIndex((line) => line === BEGIN)
58
- const end = lines.findIndex((line) => line === END)
59
- if (begin === -1 || end === -1 || begin >= end) {
58
+ const explicitEnd = lines.findIndex((line) => line === END)
59
+ if (begin === -1) {
60
+ fail("Invalid patch format: missing Begin/End markers")
61
+ }
62
+ const end = explicitEnd === -1 ? lines.length : explicitEnd
63
+ if (begin >= end) {
60
64
  fail("Invalid patch format: missing Begin/End markers")
61
65
  }
62
66
  return {
@@ -0,0 +1,78 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+ import { Effect, Layer, pipe, Schema, ServiceMap } from "effect"
5
+ import * as McpClient from "./McpClient.ts"
6
+
7
+ /**
8
+ * @since 1.0.0
9
+ * @category Services
10
+ */
11
+ export class ExaSearch extends ServiceMap.Service<
12
+ ExaSearch,
13
+ {
14
+ search(
15
+ options: typeof ExaSearchOptions.Type,
16
+ ): Effect.Effect<string, ExaError>
17
+ }
18
+ >()("clanka/ExaSearch") {}
19
+
20
+ /**
21
+ * @since 1.0.0
22
+ * @category Schemas
23
+ */
24
+ export const ExaSearchOptions = Schema.Struct({
25
+ query: Schema.String,
26
+ numResults: Schema.optional(Schema.Number).annotate({
27
+ documentation: "The number of search results to return. Defaults to 3.",
28
+ }),
29
+ })
30
+
31
+ class ExaSearchResult extends Schema.Class<ExaSearchResult>("ExaSearchResult")({
32
+ type: Schema.Literal("text"),
33
+ text: Schema.String,
34
+ }) {}
35
+
36
+ /**
37
+ * @since 1.0.0
38
+ * @category Errors
39
+ */
40
+ export class ExaError extends Schema.TaggedErrorClass<ExaError>()("ExaError", {
41
+ cause: Schema.Defect,
42
+ }) {}
43
+
44
+ /**
45
+ * @since 1.0.0
46
+ * @category Layers
47
+ */
48
+ export const layer = Layer.effect(
49
+ ExaSearch,
50
+ Effect.gen(function* () {
51
+ const client = yield* McpClient.McpClient
52
+
53
+ yield* client.connect({ url: "https://mcp.exa.ai/mcp" }).pipe(Effect.orDie)
54
+
55
+ const decode = Schema.decodeUnknownEffect(
56
+ Schema.NonEmptyArray(ExaSearchResult),
57
+ )
58
+
59
+ return ExaSearch.of({
60
+ search: Effect.fn("ExaSearch.search")(
61
+ function* (options) {
62
+ const results = yield* pipe(
63
+ client.toolCall({
64
+ name: "web_search_exa",
65
+ arguments: {
66
+ query: options.query,
67
+ num_results: options.numResults ?? 3,
68
+ },
69
+ }),
70
+ Effect.flatMap(decode),
71
+ )
72
+ return results[0].text
73
+ },
74
+ Effect.mapError((cause) => new ExaError({ cause })),
75
+ ),
76
+ })
77
+ }),
78
+ ).pipe(Layer.provide(McpClient.layer))
@@ -0,0 +1,81 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+ import { Effect, Layer, Schema, ServiceMap } from "effect"
5
+ import { Client } from "@modelcontextprotocol/sdk/client"
6
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
7
+ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"
8
+
9
+ /**
10
+ * @since 1.0.0
11
+ * @category Services
12
+ */
13
+ export class McpClient extends ServiceMap.Service<
14
+ McpClient,
15
+ {
16
+ connect(options: {
17
+ readonly url: string
18
+ }): Effect.Effect<void, McpClientError>
19
+ toolCall(options: {
20
+ readonly name: string
21
+ readonly arguments: Record<string, unknown>
22
+ }): Effect.Effect<unknown, McpClientError>
23
+ }
24
+ >()("clanka/McpClient") {}
25
+
26
+ /**
27
+ * @since 1.0.0
28
+ * @category Errors
29
+ */
30
+ export class McpClientError extends Schema.TaggedErrorClass<McpClientError>()(
31
+ "McpClientError",
32
+ {
33
+ cause: Schema.Defect,
34
+ },
35
+ ) {}
36
+
37
+ /**
38
+ * @since 1.0.0
39
+ * @category Layers
40
+ */
41
+ export const layer = Layer.effect(
42
+ McpClient,
43
+ Effect.gen(function* () {
44
+ const client = yield* Effect.acquireRelease(
45
+ Effect.sync(
46
+ () =>
47
+ new Client({
48
+ name: "clanka",
49
+ version: "0.1.0",
50
+ }),
51
+ ),
52
+ (client) => Effect.promise(() => client.close()),
53
+ )
54
+
55
+ const connect = Effect.fn("McpClient.connect")(function* (options: {
56
+ readonly url: string
57
+ }) {
58
+ const transport = new StreamableHTTPClientTransport(new URL(options.url))
59
+ return yield* Effect.tryPromise({
60
+ try: (signal) => client.connect(transport as Transport, { signal }),
61
+ catch: (cause) => new McpClientError({ cause }),
62
+ })
63
+ })
64
+
65
+ return McpClient.of({
66
+ connect,
67
+ toolCall: Effect.fn("McpClient.toolCall")((options) =>
68
+ Effect.tryPromise({
69
+ try: async () => {
70
+ const response = await client.callTool({
71
+ name: options.name,
72
+ arguments: options.arguments,
73
+ })
74
+ return response.structuredContent ?? response.content
75
+ },
76
+ catch: (cause) => new McpClientError({ cause }),
77
+ }),
78
+ ),
79
+ })
80
+ }),
81
+ )
@@ -0,0 +1,87 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+ import { Effect, Layer, ServiceMap } from "effect"
5
+ import { HttpClient, HttpClientError } from "effect/unstable/http"
6
+ import TurndownService from "turndown"
7
+
8
+ /**
9
+ * @since 1.0.0
10
+ * @category Services
11
+ */
12
+ export class WebToMarkdown extends ServiceMap.Service<
13
+ WebToMarkdown,
14
+ {
15
+ convertHtml(html: string): Effect.Effect<string>
16
+ convertUrl(
17
+ url: string,
18
+ ): Effect.Effect<string, HttpClientError.HttpClientError>
19
+ }
20
+ >()("clanka/WebToMarkdown") {}
21
+
22
+ /**
23
+ * @since 1.0.0
24
+ * @category Layers
25
+ */
26
+ export const layer = Layer.effect(
27
+ WebToMarkdown,
28
+ Effect.gen(function* () {
29
+ const client = (yield* HttpClient.HttpClient).pipe(
30
+ HttpClient.filterStatusOk,
31
+ HttpClient.retryTransient({
32
+ times: 3,
33
+ }),
34
+ )
35
+
36
+ const toRemove = new Set([
37
+ "head",
38
+ "footer",
39
+ "header",
40
+ "script",
41
+ "style",
42
+ "meta",
43
+ "link",
44
+ "noscript",
45
+ "iframe",
46
+ "object",
47
+ "embed",
48
+ "svg",
49
+ "canvas",
50
+ "audio",
51
+ "video",
52
+ "source",
53
+ "track",
54
+ "map",
55
+ "area",
56
+ "base",
57
+ "form",
58
+ "input",
59
+ "textarea",
60
+ "button",
61
+ "select",
62
+ "option",
63
+ "optgroup",
64
+ "datalist",
65
+ "keygen",
66
+ "output",
67
+ "progress",
68
+ "meter",
69
+ ])
70
+ const turndown = new TurndownService().remove((node) =>
71
+ toRemove.has(node.nodeName.toLowerCase()),
72
+ )
73
+
74
+ const convertHtml = Effect.fn("WebToMarkdown.convertHtml")((html) =>
75
+ Effect.sync(() => turndown.turndown(html)),
76
+ )
77
+
78
+ return WebToMarkdown.of({
79
+ convertHtml,
80
+ convertUrl: Effect.fn("WebToMarkdown.convertUrl")(function* (url) {
81
+ const response = yield* client.get(url)
82
+ const html = yield* response.text
83
+ return turndown.turndown(html)
84
+ }),
85
+ })
86
+ }),
87
+ )
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=AgentTools.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"AgentTools.test.d.ts","sourceRoot":"","sources":["../src/AgentTools.test.ts"],"names":[],"mappings":""}