saeeol 1.2.1 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/saeeol.cjs +187 -0
- package/npm/bin/saeeol +0 -0
- package/package.json +12 -12
- package/src/cli/cmd/tui/component/dialog/dialog-agent.tsx +32 -0
- package/src/cli/cmd/tui/component/dialog/dialog-command.tsx +190 -0
- package/src/cli/cmd/tui/component/dialog/dialog-console-org.tsx +103 -0
- package/src/cli/cmd/tui/component/dialog/dialog-go-upsell.tsx +159 -0
- package/src/cli/cmd/tui/component/dialog/dialog-mcp.tsx +86 -0
- package/src/cli/cmd/tui/component/dialog/dialog-model.tsx +238 -0
- package/src/cli/cmd/tui/component/dialog/dialog-provider.tsx +343 -0
- package/src/cli/cmd/tui/component/dialog/dialog-session-delete-failed.tsx +103 -0
- package/src/cli/cmd/tui/component/dialog/dialog-session-list.tsx +301 -0
- package/src/cli/cmd/tui/component/dialog/dialog-session-rename.tsx +35 -0
- package/src/cli/cmd/tui/component/dialog/dialog-skill.tsx +37 -0
- package/src/cli/cmd/tui/component/dialog/dialog-stash.tsx +87 -0
- package/src/cli/cmd/tui/component/dialog/dialog-status.tsx +190 -0
- package/src/cli/cmd/tui/component/dialog/dialog-tag.tsx +44 -0
- package/src/cli/cmd/tui/component/dialog/dialog-theme-list.tsx +50 -0
- package/src/cli/cmd/tui/component/dialog/dialog-variant.tsx +39 -0
- package/src/cli/cmd/tui/component/dialog/dialog-workspace-create.tsx +200 -0
- package/src/cli/cmd/tui/component/dialog/dialog-workspace-unavailable.tsx +81 -0
- package/src/cli/cmd/tui/component/dialog-agent.tsx +1 -32
- package/src/cli/cmd/tui/component/dialog-command.tsx +1 -190
- package/src/cli/cmd/tui/component/dialog-console-org.tsx +1 -103
- package/src/cli/cmd/tui/component/dialog-go-upsell.tsx +1 -159
- package/src/cli/cmd/tui/component/dialog-mcp.tsx +1 -86
- package/src/cli/cmd/tui/component/dialog-model.tsx +1 -238
- package/src/cli/cmd/tui/component/dialog-provider.tsx +1 -343
- package/src/cli/cmd/tui/component/dialog-session-delete-failed.tsx +1 -103
- package/src/cli/cmd/tui/component/dialog-session-list.tsx +1 -301
- package/src/cli/cmd/tui/component/dialog-session-rename.tsx +1 -35
- package/src/cli/cmd/tui/component/dialog-skill.tsx +1 -37
- package/src/cli/cmd/tui/component/dialog-stash.tsx +1 -87
- package/src/cli/cmd/tui/component/dialog-status.tsx +1 -190
- package/src/cli/cmd/tui/component/dialog-tag.tsx +1 -44
- package/src/cli/cmd/tui/component/dialog-theme-list.tsx +1 -50
- package/src/cli/cmd/tui/component/dialog-variant.tsx +1 -39
- package/src/cli/cmd/tui/component/dialog-workspace-create.tsx +1 -200
- package/src/cli/cmd/tui/component/dialog-workspace-unavailable.tsx +1 -81
- package/src/cli/cmd/tui/context/app/args.tsx +15 -0
- package/src/cli/cmd/tui/context/app/directory.ts +15 -0
- package/src/cli/cmd/tui/context/app/editor-zed.ts +281 -0
- package/src/cli/cmd/tui/context/app/editor.ts +425 -0
- package/src/cli/cmd/tui/context/app/helper.tsx +25 -0
- package/src/cli/cmd/tui/context/app/project.tsx +109 -0
- package/src/cli/cmd/tui/context/app/route.tsx +67 -0
- package/src/cli/cmd/tui/context/app/sdk.tsx +142 -0
- package/src/cli/cmd/tui/context/app/sync.tsx +713 -0
- package/src/cli/cmd/tui/context/app/theme.tsx +307 -0
- package/src/cli/cmd/tui/context/app/tui-config.tsx +9 -0
- package/src/cli/cmd/tui/context/args.tsx +1 -15
- package/src/cli/cmd/tui/context/directory.ts +1 -15
- package/src/cli/cmd/tui/context/editor-zed.ts +1 -281
- package/src/cli/cmd/tui/context/editor.ts +1 -425
- package/src/cli/cmd/tui/context/event.ts +1 -45
- package/src/cli/cmd/tui/context/exit.tsx +1 -67
- package/src/cli/cmd/tui/context/helper.tsx +1 -25
- package/src/cli/cmd/tui/context/keybind.tsx +1 -105
- package/src/cli/cmd/tui/context/kv.tsx +1 -76
- package/src/cli/cmd/tui/context/local.tsx +1 -478
- package/src/cli/cmd/tui/context/plugin-keybinds.ts +1 -41
- package/src/cli/cmd/tui/context/project.tsx +1 -109
- package/src/cli/cmd/tui/context/prompt.tsx +1 -18
- package/src/cli/cmd/tui/context/route.tsx +1 -67
- package/src/cli/cmd/tui/context/runtime/event.ts +45 -0
- package/src/cli/cmd/tui/context/runtime/exit.tsx +67 -0
- package/src/cli/cmd/tui/context/runtime/keybind.tsx +105 -0
- package/src/cli/cmd/tui/context/runtime/kv.tsx +76 -0
- package/src/cli/cmd/tui/context/runtime/local.tsx +478 -0
- package/src/cli/cmd/tui/context/runtime/plugin-keybinds.ts +41 -0
- package/src/cli/cmd/tui/context/sdk.tsx +1 -142
- package/src/cli/cmd/tui/context/session/prompt.tsx +18 -0
- package/src/cli/cmd/tui/context/sync.tsx +1 -713
- package/src/cli/cmd/tui/context/theme.tsx +1 -307
- package/src/cli/cmd/tui/context/tui-config.tsx +1 -9
- package/src/tool/apply_patch.ts +1 -334
- package/src/tool/bash.ts +1 -656
- package/src/tool/core/external-directory.ts +55 -0
- package/src/tool/core/invalid.ts +21 -0
- package/src/tool/core/recall.ts +164 -0
- package/src/tool/core/recall.txt +12 -0
- package/src/tool/core/schema.ts +16 -0
- package/src/tool/core/tool.ts +162 -0
- package/src/tool/core/truncate.ts +160 -0
- package/src/tool/core/truncation-dir.ts +4 -0
- package/src/tool/diagnostics.ts +1 -20
- package/src/tool/edit-replacers.ts +1 -288
- package/src/tool/edit-utils.ts +1 -86
- package/src/tool/edit.ts +1 -262
- package/src/tool/external-directory.ts +1 -55
- package/src/tool/file/apply_patch.ts +334 -0
- package/src/tool/file/apply_patch.txt +33 -0
- package/src/tool/file/bash.ts +656 -0
- package/src/tool/file/bash.txt +119 -0
- package/src/tool/file/edit-replacers.ts +288 -0
- package/src/tool/file/edit-utils.ts +86 -0
- package/src/tool/file/edit.ts +262 -0
- package/src/tool/file/edit.txt +10 -0
- package/src/tool/file/read.ts +389 -0
- package/src/tool/file/read.txt +14 -0
- package/src/tool/file/write.ts +114 -0
- package/src/tool/file/write.txt +8 -0
- package/src/tool/glob.ts +1 -115
- package/src/tool/grep.ts +1 -151
- package/src/tool/integration/diagnostics.ts +20 -0
- package/src/tool/integration/lsp.ts +113 -0
- package/src/tool/integration/lsp.txt +24 -0
- package/src/tool/integration/mcp-exa.ts +73 -0
- package/src/tool/integration/package.ts +168 -0
- package/src/tool/integration/registry.ts +375 -0
- package/src/tool/invalid.ts +1 -21
- package/src/tool/lsp.ts +1 -113
- package/src/tool/mcp-exa.ts +1 -73
- package/src/tool/package.ts +1 -168
- package/src/tool/plan.ts +1 -30
- package/src/tool/question.ts +1 -52
- package/src/tool/read.ts +1 -389
- package/src/tool/recall.ts +1 -164
- package/src/tool/registry.ts +1 -375
- package/src/tool/schema.ts +1 -16
- package/src/tool/search/glob.ts +115 -0
- package/src/tool/search/glob.txt +6 -0
- package/src/tool/search/grep.ts +151 -0
- package/src/tool/search/grep.txt +8 -0
- package/src/tool/search/warpgrep.ts +107 -0
- package/src/tool/search/warpgrep.txt +10 -0
- package/src/tool/search/webfetch.ts +202 -0
- package/src/tool/search/webfetch.txt +13 -0
- package/src/tool/search/websearch.ts +71 -0
- package/src/tool/search/websearch.txt +14 -0
- package/src/tool/skill.ts +1 -91
- package/src/tool/task.ts +1 -197
- package/src/tool/todo.ts +1 -62
- package/src/tool/tool.ts +1 -162
- package/src/tool/truncate.ts +1 -160
- package/src/tool/truncation-dir.ts +1 -4
- package/src/tool/warpgrep.ts +1 -107
- package/src/tool/webfetch.ts +1 -202
- package/src/tool/websearch.ts +1 -71
- package/src/tool/workflow/plan-enter.txt +14 -0
- package/src/tool/workflow/plan-exit.txt +13 -0
- package/src/tool/workflow/plan.ts +30 -0
- package/src/tool/workflow/question.ts +52 -0
- package/src/tool/workflow/question.txt +11 -0
- package/src/tool/workflow/skill.ts +91 -0
- package/src/tool/workflow/skill.txt +5 -0
- package/src/tool/workflow/task.ts +197 -0
- package/src/tool/workflow/task.txt +57 -0
- package/src/tool/workflow/todo.ts +62 -0
- package/src/tool/workflow/todowrite.txt +167 -0
- package/src/tool/write.ts +1 -114
package/src/tool/registry.ts
CHANGED
|
@@ -1,375 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { Session } from "@/session/session"
|
|
3
|
-
import { QuestionTool } from "./question"
|
|
4
|
-
import { SuggestTool } from "../overlay/suggestion/tool"
|
|
5
|
-
import { BashTool } from "./bash"
|
|
6
|
-
import { EditTool } from "./edit"
|
|
7
|
-
import { GlobTool } from "./glob"
|
|
8
|
-
import { GrepTool } from "./grep"
|
|
9
|
-
import { ReadTool } from "./read"
|
|
10
|
-
import { TaskTool } from "./task"
|
|
11
|
-
import { TodoWriteTool } from "./todo"
|
|
12
|
-
import { WebFetchTool } from "./webfetch"
|
|
13
|
-
import { WriteTool } from "./write"
|
|
14
|
-
import { InvalidTool } from "./invalid"
|
|
15
|
-
import { SkillTool } from "./skill"
|
|
16
|
-
import { PackageTool } from "./package"
|
|
17
|
-
import * as Tool from "./tool"
|
|
18
|
-
import { Config } from "@/config/config"
|
|
19
|
-
import { type ToolContext as PluginToolContext, type ToolDefinition } from "@saeeol/plugin"
|
|
20
|
-
import { Schema } from "effect"
|
|
21
|
-
import z from "zod"
|
|
22
|
-
import { ZodOverride } from "@/util/effect-zod"
|
|
23
|
-
import { Plugin } from "../plugin"
|
|
24
|
-
import { Provider } from "@/provider/provider"
|
|
25
|
-
import { ProviderID, type ModelID } from "../provider/schema"
|
|
26
|
-
import { WebSearchTool } from "./websearch"
|
|
27
|
-
import { SaeeolToolRegistry } from "../overlay/tool/registry"
|
|
28
|
-
import { makeRuntime } from "@/effect/run-service"
|
|
29
|
-
import { Flag } from "@saeeol/core/flag/flag"
|
|
30
|
-
import * as Log from "@saeeol/core/util/log"
|
|
31
|
-
import { LspTool } from "./lsp"
|
|
32
|
-
import * as Truncate from "./truncate"
|
|
33
|
-
import { ApplyPatchTool } from "./apply_patch"
|
|
34
|
-
import { Glob } from "@saeeol/core/util/glob"
|
|
35
|
-
import path from "path"
|
|
36
|
-
import { pathToFileURL } from "url"
|
|
37
|
-
import { Effect, Layer, Context } from "effect"
|
|
38
|
-
import { FetchHttpClient, HttpClient } from "effect/unstable/http"
|
|
39
|
-
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
|
|
40
|
-
import { CrossSpawnSpawner } from "@saeeol/core/cross-spawn-spawner"
|
|
41
|
-
import { Ripgrep } from "../file/ripgrep"
|
|
42
|
-
import { Format } from "../format"
|
|
43
|
-
import { InstanceState } from "@/effect/instance-state"
|
|
44
|
-
import { Question } from "../question"
|
|
45
|
-
import { Todo } from "../session/todo"
|
|
46
|
-
import { LSP } from "@/lsp/lsp"
|
|
47
|
-
import { Instruction } from "../session/instruction"
|
|
48
|
-
import { AppFileSystem } from "@saeeol/core/filesystem"
|
|
49
|
-
import { Bus } from "../bus"
|
|
50
|
-
import { Agent } from "../agent/agent"
|
|
51
|
-
import { Skill } from "../skill"
|
|
52
|
-
import { Permission } from "@/permission"
|
|
53
|
-
|
|
54
|
-
const log = Log.create({ service: "tool.registry" })
|
|
55
|
-
|
|
56
|
-
type TaskDef = Tool.InferDef<typeof TaskTool>
|
|
57
|
-
type ReadDef = Tool.InferDef<typeof ReadTool>
|
|
58
|
-
|
|
59
|
-
type State = {
|
|
60
|
-
custom: Tool.Def[]
|
|
61
|
-
builtin: Tool.Def[]
|
|
62
|
-
task: TaskDef
|
|
63
|
-
read: ReadDef
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export interface Interface {
|
|
67
|
-
readonly ids: () => Effect.Effect<string[]>
|
|
68
|
-
readonly all: () => Effect.Effect<Tool.Def[]>
|
|
69
|
-
readonly named: () => Effect.Effect<{ task: TaskDef; read: ReadDef }>
|
|
70
|
-
readonly tools: (model: { providerID: ProviderID; modelID: ModelID; agent: Agent.Info }) => Effect.Effect<Tool.Def[]>
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export class Service extends Context.Service<Service, Interface>()("@saeeol/ToolRegistry") {}
|
|
74
|
-
|
|
75
|
-
export const layer: Layer.Layer<
|
|
76
|
-
Service,
|
|
77
|
-
never,
|
|
78
|
-
| Config.Service
|
|
79
|
-
| Plugin.Service
|
|
80
|
-
| Question.Service
|
|
81
|
-
| Todo.Service
|
|
82
|
-
| Agent.Service
|
|
83
|
-
| Skill.Service
|
|
84
|
-
| Session.Service
|
|
85
|
-
| Provider.Service
|
|
86
|
-
| LSP.Service
|
|
87
|
-
| Instruction.Service
|
|
88
|
-
| AppFileSystem.Service
|
|
89
|
-
| Bus.Service
|
|
90
|
-
| HttpClient.HttpClient
|
|
91
|
-
| ChildProcessSpawner
|
|
92
|
-
| Ripgrep.Service
|
|
93
|
-
| Format.Service
|
|
94
|
-
| Truncate.Service
|
|
95
|
-
> = Layer.effect(
|
|
96
|
-
Service,
|
|
97
|
-
Effect.gen(function* () {
|
|
98
|
-
const config = yield* Config.Service
|
|
99
|
-
const plugin = yield* Plugin.Service
|
|
100
|
-
const agents = yield* Agent.Service
|
|
101
|
-
const skill = yield* Skill.Service
|
|
102
|
-
const truncate = yield* Truncate.Service
|
|
103
|
-
|
|
104
|
-
const invalid = yield* InvalidTool
|
|
105
|
-
const task = yield* TaskTool
|
|
106
|
-
const read = yield* ReadTool
|
|
107
|
-
const question = yield* QuestionTool
|
|
108
|
-
const todo = yield* TodoWriteTool
|
|
109
|
-
const lsptool = yield* LspTool
|
|
110
|
-
const plan = yield* PlanExitTool
|
|
111
|
-
const webfetch = yield* WebFetchTool
|
|
112
|
-
const websearch = yield* WebSearchTool
|
|
113
|
-
const bash = yield* BashTool
|
|
114
|
-
const globtool = yield* GlobTool
|
|
115
|
-
const writetool = yield* WriteTool
|
|
116
|
-
const edit = yield* EditTool
|
|
117
|
-
const greptool = yield* GrepTool
|
|
118
|
-
const patchtool = yield* ApplyPatchTool
|
|
119
|
-
const skilltool = yield* SkillTool
|
|
120
|
-
const packagetool = yield* PackageTool
|
|
121
|
-
const agent = yield* Agent.Service
|
|
122
|
-
const suggesttool = yield* SuggestTool
|
|
123
|
-
const saeeolToolInfos = yield* SaeeolToolRegistry.infos()
|
|
124
|
-
|
|
125
|
-
const state = yield* InstanceState.make<State>(
|
|
126
|
-
Effect.fn("ToolRegistry.state")(function* (ctx) {
|
|
127
|
-
const custom: Tool.Def[] = []
|
|
128
|
-
|
|
129
|
-
function fromPlugin(id: string, def: ToolDefinition): Tool.Def {
|
|
130
|
-
// Plugin tools define their args as a raw Zod shape. Wrap the
|
|
131
|
-
// derived Zod object in a `Schema.declare` so it slots into the
|
|
132
|
-
// Schema-typed framework, and annotate with `ZodOverride` so the
|
|
133
|
-
// walker emits the original Zod object for LLM JSON Schema.
|
|
134
|
-
const zodParams = z.object(def.args)
|
|
135
|
-
const parameters = Schema.declare<unknown>((u): u is unknown => zodParams.safeParse(u).success).annotate({
|
|
136
|
-
[ZodOverride]: zodParams,
|
|
137
|
-
})
|
|
138
|
-
return {
|
|
139
|
-
id,
|
|
140
|
-
parameters,
|
|
141
|
-
description: def.description,
|
|
142
|
-
execute: (args, toolCtx) =>
|
|
143
|
-
Effect.gen(function* () {
|
|
144
|
-
const pluginCtx: PluginToolContext = {
|
|
145
|
-
...toolCtx,
|
|
146
|
-
ask: (req) => toolCtx.ask(req),
|
|
147
|
-
directory: ctx.directory,
|
|
148
|
-
worktree: ctx.worktree,
|
|
149
|
-
}
|
|
150
|
-
const result = yield* Effect.promise(() => def.execute(args as any, pluginCtx))
|
|
151
|
-
const output = typeof result === "string" ? result : result.output
|
|
152
|
-
const metadata = typeof result === "string" ? {} : (result.metadata ?? {})
|
|
153
|
-
const info = yield* agent.get(toolCtx.agent)
|
|
154
|
-
const out = yield* truncate.output(output, {}, info)
|
|
155
|
-
return {
|
|
156
|
-
title: "",
|
|
157
|
-
output: out.truncated ? out.content : output,
|
|
158
|
-
metadata: {
|
|
159
|
-
...metadata,
|
|
160
|
-
truncated: out.truncated,
|
|
161
|
-
...(out.truncated && { outputPath: out.outputPath }),
|
|
162
|
-
},
|
|
163
|
-
}
|
|
164
|
-
}).pipe(
|
|
165
|
-
Effect.withSpan("Tool.execute", {
|
|
166
|
-
attributes: {
|
|
167
|
-
"tool.name": id,
|
|
168
|
-
"session.id": toolCtx.sessionID,
|
|
169
|
-
"message.id": toolCtx.messageID,
|
|
170
|
-
...(toolCtx.callID ? { "tool.call_id": toolCtx.callID } : {}),
|
|
171
|
-
},
|
|
172
|
-
}),
|
|
173
|
-
),
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const dirs = yield* config.directories()
|
|
178
|
-
const matches = dirs.flatMap((dir) =>
|
|
179
|
-
Glob.scanSync("{tool,tools}/*.{js,ts}", { cwd: dir, absolute: true, dot: true, symlink: true }),
|
|
180
|
-
)
|
|
181
|
-
if (matches.length) yield* config.waitForDependencies()
|
|
182
|
-
for (const match of matches) {
|
|
183
|
-
const namespace = path.basename(match, path.extname(match))
|
|
184
|
-
// `match` is an absolute filesystem path from `Glob.scanSync(..., { absolute: true })`.
|
|
185
|
-
// Import it as `file://` so Node on Windows accepts the dynamic import.
|
|
186
|
-
const mod = yield* Effect.promise(() => import(pathToFileURL(match).href))
|
|
187
|
-
for (const [id, def] of Object.entries<ToolDefinition>(mod)) {
|
|
188
|
-
custom.push(fromPlugin(id === "default" ? namespace : `${namespace}_${id}`, def))
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const plugins = yield* plugin.list()
|
|
193
|
-
for (const p of plugins) {
|
|
194
|
-
for (const [id, def] of Object.entries(p.tool ?? {})) {
|
|
195
|
-
custom.push(fromPlugin(id, def))
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const cfg = yield* config.get()
|
|
200
|
-
const questionEnabled =
|
|
201
|
-
["app", "cli", "desktop", "vscode"].includes(Flag.SAEEOL_CLIENT) || Flag.SAEEOL_ENABLE_QUESTION_TOOL
|
|
202
|
-
|
|
203
|
-
const tool = yield* Effect.all({
|
|
204
|
-
invalid: Tool.init(invalid),
|
|
205
|
-
bash: Tool.init(bash),
|
|
206
|
-
read: Tool.init(read),
|
|
207
|
-
glob: Tool.init(globtool),
|
|
208
|
-
grep: Tool.init(greptool),
|
|
209
|
-
edit: Tool.init(edit),
|
|
210
|
-
write: Tool.init(writetool),
|
|
211
|
-
task: Tool.init(task),
|
|
212
|
-
fetch: Tool.init(webfetch),
|
|
213
|
-
todo: Tool.init(todo),
|
|
214
|
-
search: Tool.init(websearch),
|
|
215
|
-
skill: Tool.init(skilltool),
|
|
216
|
-
package: Tool.init(packagetool),
|
|
217
|
-
patch: Tool.init(patchtool),
|
|
218
|
-
question: Tool.init(question),
|
|
219
|
-
lsp: Tool.init(lsptool),
|
|
220
|
-
plan: Tool.init(plan),
|
|
221
|
-
suggest: Tool.init(suggesttool),
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
const saeeol = yield* SaeeolToolRegistry.build(saeeolToolInfos, { agent: agents, truncate })
|
|
225
|
-
|
|
226
|
-
return {
|
|
227
|
-
custom,
|
|
228
|
-
builtin: SaeeolToolRegistry.describe(
|
|
229
|
-
[
|
|
230
|
-
tool.invalid,
|
|
231
|
-
...(questionEnabled ? [tool.question] : []),
|
|
232
|
-
tool.bash,
|
|
233
|
-
tool.read,
|
|
234
|
-
tool.glob,
|
|
235
|
-
tool.grep,
|
|
236
|
-
tool.edit,
|
|
237
|
-
tool.write,
|
|
238
|
-
tool.task,
|
|
239
|
-
tool.fetch,
|
|
240
|
-
tool.todo,
|
|
241
|
-
tool.search,
|
|
242
|
-
tool.skill,
|
|
243
|
-
tool.package,
|
|
244
|
-
tool.patch,
|
|
245
|
-
tool.plan,
|
|
246
|
-
...(["cli", "vscode"].includes(Flag.SAEEOL_CLIENT) ? [tool.suggest] : []),
|
|
247
|
-
...SaeeolToolRegistry.extra(saeeol, cfg),
|
|
248
|
-
...(Flag.SAEEOL_EXPERIMENTAL_LSP_TOOL ? [tool.lsp] : []),
|
|
249
|
-
],
|
|
250
|
-
saeeol,
|
|
251
|
-
),
|
|
252
|
-
task: tool.task,
|
|
253
|
-
read: tool.read,
|
|
254
|
-
}
|
|
255
|
-
}),
|
|
256
|
-
)
|
|
257
|
-
|
|
258
|
-
const all: Interface["all"] = Effect.fn("ToolRegistry.all")(function* () {
|
|
259
|
-
const s = yield* InstanceState.get(state)
|
|
260
|
-
return [...s.builtin, ...s.custom] as Tool.Def[]
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
const ids: Interface["ids"] = Effect.fn("ToolRegistry.ids")(function* () {
|
|
264
|
-
return (yield* all()).map((tool) => tool.id)
|
|
265
|
-
})
|
|
266
|
-
|
|
267
|
-
const describeSkill = Effect.fn("ToolRegistry.describeSkill")(function* (agent: Agent.Info) {
|
|
268
|
-
const list = yield* skill.available(agent)
|
|
269
|
-
if (list.length === 0) return "No skills are currently available."
|
|
270
|
-
return [
|
|
271
|
-
"Load a specialized skill that provides domain-specific instructions and workflows.",
|
|
272
|
-
"",
|
|
273
|
-
"When you recognize that a task matches one of the available skills listed below, use this tool to load the full skill instructions.",
|
|
274
|
-
"",
|
|
275
|
-
"The skill will inject detailed instructions, workflows, and access to bundled resources (scripts, references, templates) into the conversation context.",
|
|
276
|
-
"",
|
|
277
|
-
'Tool output includes a `<skill_content name="...">` block with the loaded content.',
|
|
278
|
-
"",
|
|
279
|
-
"The following skills provide specialized sets of instructions for particular tasks",
|
|
280
|
-
"Invoke this tool to load a skill when a task matches one of the available skills listed below:",
|
|
281
|
-
"",
|
|
282
|
-
Skill.fmt(list, { verbose: false }),
|
|
283
|
-
].join("\n")
|
|
284
|
-
})
|
|
285
|
-
|
|
286
|
-
const describeTask = Effect.fn("ToolRegistry.describeTask")(function* (agent: Agent.Info) {
|
|
287
|
-
const items = (yield* agents.list()).filter((item) => item.mode !== "primary")
|
|
288
|
-
const filtered = items.filter(
|
|
289
|
-
(item) => Permission.evaluate("task", item.name, agent.permission).action !== "deny",
|
|
290
|
-
)
|
|
291
|
-
const list = filtered.toSorted((a, b) => a.name.localeCompare(b.name))
|
|
292
|
-
const description = list
|
|
293
|
-
.map(
|
|
294
|
-
(item) =>
|
|
295
|
-
`- ${item.name}: ${item.description ?? "This subagent should only be called manually by the user."}`,
|
|
296
|
-
)
|
|
297
|
-
.join("\n")
|
|
298
|
-
return ["Available agent types and the tools they have access to:", description].join("\n")
|
|
299
|
-
})
|
|
300
|
-
|
|
301
|
-
const tools: Interface["tools"] = Effect.fn("ToolRegistry.tools")(function* (input) {
|
|
302
|
-
const filtered = (yield* all()).filter((tool) => {
|
|
303
|
-
if (tool.id === WebSearchTool.id) {
|
|
304
|
-
return input.providerID === ProviderID.saeeol || Flag.SAEEOL_ENABLE_EXA
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const usePatch =
|
|
308
|
-
!!process.env["SAEEOL_E2E_LLM_URL"] ||
|
|
309
|
-
(input.modelID.includes("gpt-") && !input.modelID.includes("oss") && !input.modelID.includes("gpt-4"))
|
|
310
|
-
if (tool.id === ApplyPatchTool.id) return usePatch
|
|
311
|
-
if (tool.id === EditTool.id) return !usePatch
|
|
312
|
-
|
|
313
|
-
return true
|
|
314
|
-
})
|
|
315
|
-
|
|
316
|
-
return yield* Effect.forEach(
|
|
317
|
-
filtered,
|
|
318
|
-
Effect.fnUntraced(function* (tool: Tool.Def) {
|
|
319
|
-
using _ = log.time(tool.id)
|
|
320
|
-
const output = {
|
|
321
|
-
description: tool.description,
|
|
322
|
-
parameters: tool.parameters,
|
|
323
|
-
}
|
|
324
|
-
yield* plugin.trigger("tool.definition", { toolID: tool.id }, output)
|
|
325
|
-
return {
|
|
326
|
-
id: tool.id,
|
|
327
|
-
description: [
|
|
328
|
-
output.description,
|
|
329
|
-
tool.id === TaskTool.id ? yield* describeTask(input.agent) : undefined,
|
|
330
|
-
tool.id === SkillTool.id ? yield* describeSkill(input.agent) : undefined,
|
|
331
|
-
]
|
|
332
|
-
.filter(Boolean)
|
|
333
|
-
.join("\n"),
|
|
334
|
-
parameters: output.parameters,
|
|
335
|
-
execute: tool.execute,
|
|
336
|
-
formatValidationError: tool.formatValidationError,
|
|
337
|
-
}
|
|
338
|
-
}),
|
|
339
|
-
{ concurrency: "unbounded" },
|
|
340
|
-
)
|
|
341
|
-
})
|
|
342
|
-
|
|
343
|
-
const named: Interface["named"] = Effect.fn("ToolRegistry.named")(function* () {
|
|
344
|
-
const s = yield* InstanceState.get(state)
|
|
345
|
-
return { task: s.task, read: s.read }
|
|
346
|
-
})
|
|
347
|
-
|
|
348
|
-
return Service.of({ ids, all, named, tools })
|
|
349
|
-
}),
|
|
350
|
-
)
|
|
351
|
-
|
|
352
|
-
export const defaultLayer = Layer.suspend(() =>
|
|
353
|
-
layer.pipe(
|
|
354
|
-
Layer.provide(Config.defaultLayer),
|
|
355
|
-
Layer.provide(Plugin.defaultLayer),
|
|
356
|
-
Layer.provide(Question.defaultLayer),
|
|
357
|
-
Layer.provide(Todo.defaultLayer),
|
|
358
|
-
Layer.provide(Skill.defaultLayer),
|
|
359
|
-
Layer.provide(Agent.defaultLayer),
|
|
360
|
-
Layer.provide(Session.defaultLayer),
|
|
361
|
-
Layer.provide(Provider.defaultLayer),
|
|
362
|
-
Layer.provide(LSP.defaultLayer),
|
|
363
|
-
Layer.provide(Instruction.defaultLayer),
|
|
364
|
-
Layer.provide(AppFileSystem.defaultLayer),
|
|
365
|
-
Layer.provide(Bus.layer),
|
|
366
|
-
Layer.provide(FetchHttpClient.layer),
|
|
367
|
-
Layer.provide(Format.defaultLayer),
|
|
368
|
-
Layer.provide(CrossSpawnSpawner.defaultLayer),
|
|
369
|
-
Layer.provide(Ripgrep.defaultLayer),
|
|
370
|
-
Layer.provide(Truncate.defaultLayer),
|
|
371
|
-
),
|
|
372
|
-
)
|
|
373
|
-
const { runPromise } = makeRuntime(Service, defaultLayer)
|
|
374
|
-
export const ids = () => runPromise((svc) => svc.ids())
|
|
375
|
-
export * as ToolRegistry from "./registry"
|
|
1
|
+
export * from "./integration/registry"
|
package/src/tool/schema.ts
CHANGED
|
@@ -1,16 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { Identifier } from "@/id/id"
|
|
4
|
-
import { zod, ZodOverride } from "@/util/effect-zod"
|
|
5
|
-
import { withStatics } from "@/util/schema"
|
|
6
|
-
|
|
7
|
-
const toolIdSchema = Schema.String.annotate({ [ZodOverride]: Identifier.schema("tool") }).pipe(Schema.brand("ToolID"))
|
|
8
|
-
|
|
9
|
-
export type ToolID = typeof toolIdSchema.Type
|
|
10
|
-
|
|
11
|
-
export const ToolID = toolIdSchema.pipe(
|
|
12
|
-
withStatics((schema: typeof toolIdSchema) => ({
|
|
13
|
-
ascending: (id?: string) => schema.make(Identifier.ascending("tool", id)),
|
|
14
|
-
zod: zod(schema),
|
|
15
|
-
})),
|
|
16
|
-
)
|
|
1
|
+
export * from "./core/schema"
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
import { Effect, Option, Schema } from "effect"
|
|
3
|
+
import * as Stream from "effect/Stream"
|
|
4
|
+
import { InstanceState } from "@/effect/instance-state"
|
|
5
|
+
import { AppFileSystem } from "@saeeol/core/filesystem"
|
|
6
|
+
import { Ripgrep } from "../../file/ripgrep"
|
|
7
|
+
import { assertExternalDirectoryEffect } from "../core/external-directory"
|
|
8
|
+
import DESCRIPTION from "./glob.txt"
|
|
9
|
+
import * as Tool from "../core/tool"
|
|
10
|
+
function normalize(p: string) {
|
|
11
|
+
return p.replaceAll("\\", "/")
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function split(pattern: string) {
|
|
15
|
+
const normalized = normalize(pattern)
|
|
16
|
+
if (!path.isAbsolute(normalized)) return
|
|
17
|
+
const index = normalized.search(/[*?{[]/)
|
|
18
|
+
if (index === -1) return { dir: normalized, pattern: "*" }
|
|
19
|
+
const slice = normalized.slice(0, index)
|
|
20
|
+
const cut = slice.lastIndexOf("/")
|
|
21
|
+
const dir = cut > 0 ? slice.slice(0, cut) : "/"
|
|
22
|
+
const next = normalized.slice(cut + 1)
|
|
23
|
+
return { dir, pattern: next || "*" }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const Parameters = Schema.Struct({
|
|
27
|
+
pattern: Schema.String.annotate({ description: "The glob pattern to match files against" }),
|
|
28
|
+
path: Schema.optional(Schema.String).annotate({
|
|
29
|
+
description: `The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter "undefined" or "null" - simply omit it for the default behavior. Must be a valid directory path if provided.`,
|
|
30
|
+
}),
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
export const GlobTool = Tool.define(
|
|
34
|
+
"glob",
|
|
35
|
+
Effect.gen(function* () {
|
|
36
|
+
const rg = yield* Ripgrep.Service
|
|
37
|
+
const fs = yield* AppFileSystem.Service
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
description: DESCRIPTION,
|
|
41
|
+
parameters: Parameters,
|
|
42
|
+
execute: (params: { pattern: string; path?: string }, ctx: Tool.Context) =>
|
|
43
|
+
Effect.gen(function* () {
|
|
44
|
+
const ins = yield* InstanceState.context
|
|
45
|
+
const absolute = split(params.pattern)
|
|
46
|
+
yield* ctx.ask({
|
|
47
|
+
permission: "glob",
|
|
48
|
+
patterns: [params.pattern],
|
|
49
|
+
always: ["*"],
|
|
50
|
+
metadata: {
|
|
51
|
+
pattern: params.pattern,
|
|
52
|
+
path: params.path,
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const base = absolute?.dir ?? params.path ?? ins.directory
|
|
57
|
+
const search = path.isAbsolute(base) ? base : path.resolve(ins.directory, base)
|
|
58
|
+
const info = yield* fs.stat(search).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
|
59
|
+
if (info?.type === "File") {
|
|
60
|
+
throw new Error(`glob path must be a directory: ${search}`)
|
|
61
|
+
}
|
|
62
|
+
yield* assertExternalDirectoryEffect(ctx, search, { kind: "directory" })
|
|
63
|
+
|
|
64
|
+
const limit = 100
|
|
65
|
+
let truncated = false
|
|
66
|
+
const files = yield* rg
|
|
67
|
+
.files({ cwd: search, glob: [absolute?.pattern ?? params.pattern], signal: ctx.abort })
|
|
68
|
+
.pipe(
|
|
69
|
+
Stream.mapEffect((file) =>
|
|
70
|
+
Effect.gen(function* () {
|
|
71
|
+
const full = path.resolve(search, file)
|
|
72
|
+
const info = yield* fs.stat(full).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
|
73
|
+
const mtime =
|
|
74
|
+
info?.mtime.pipe(
|
|
75
|
+
Option.map((date) => date.getTime()),
|
|
76
|
+
Option.getOrElse(() => 0),
|
|
77
|
+
) ?? 0
|
|
78
|
+
return { path: full, mtime }
|
|
79
|
+
}),
|
|
80
|
+
),
|
|
81
|
+
Stream.take(limit + 1),
|
|
82
|
+
Stream.runCollect,
|
|
83
|
+
Effect.map((chunk) => [...chunk]),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if (files.length > limit) {
|
|
87
|
+
truncated = true
|
|
88
|
+
files.length = limit
|
|
89
|
+
}
|
|
90
|
+
files.sort((a, b) => b.mtime - a.mtime)
|
|
91
|
+
|
|
92
|
+
const output = []
|
|
93
|
+
if (files.length === 0) output.push("No files found")
|
|
94
|
+
if (files.length > 0) {
|
|
95
|
+
output.push(...files.map((file) => file.path))
|
|
96
|
+
if (truncated) {
|
|
97
|
+
output.push("")
|
|
98
|
+
output.push(
|
|
99
|
+
`(Results are truncated: showing first ${limit} results. Consider using a more specific path or pattern.)`,
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
title: path.relative(ins.worktree, search),
|
|
106
|
+
metadata: {
|
|
107
|
+
count: files.length,
|
|
108
|
+
truncated,
|
|
109
|
+
},
|
|
110
|
+
output: output.join("\n"),
|
|
111
|
+
}
|
|
112
|
+
}).pipe(Effect.orDie),
|
|
113
|
+
}
|
|
114
|
+
}),
|
|
115
|
+
)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
- Fast file pattern matching tool that works with any codebase size
|
|
2
|
+
- Supports glob patterns like "**/*.js" or "src/**/*.ts"
|
|
3
|
+
- Returns matching file paths sorted by modification time
|
|
4
|
+
- Use this tool when you need to find files by name patterns
|
|
5
|
+
- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead
|
|
6
|
+
- You have the capability to call multiple tools in a single response. It is always better to speculatively perform multiple searches as a batch that are potentially useful.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
import { Schema } from "effect"
|
|
3
|
+
import { Effect, Option } from "effect"
|
|
4
|
+
import { InstanceState } from "@/effect/instance-state"
|
|
5
|
+
import { AppFileSystem } from "@saeeol/core/filesystem"
|
|
6
|
+
import { Ripgrep } from "../../file/ripgrep"
|
|
7
|
+
import { assertExternalDirectoryEffect } from "../core/external-directory"
|
|
8
|
+
import DESCRIPTION from "./grep.txt"
|
|
9
|
+
import * as Tool from "../core/tool"
|
|
10
|
+
|
|
11
|
+
const MAX_LINE_LENGTH = 2000
|
|
12
|
+
|
|
13
|
+
export const Parameters = Schema.Struct({
|
|
14
|
+
pattern: Schema.String.annotate({ description: "The regex pattern to search for in file contents" }),
|
|
15
|
+
path: Schema.optional(Schema.String).annotate({
|
|
16
|
+
description: "The directory to search in. Defaults to the current working directory.",
|
|
17
|
+
}),
|
|
18
|
+
include: Schema.optional(Schema.String).annotate({
|
|
19
|
+
description: 'File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")',
|
|
20
|
+
}),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export const GrepTool = Tool.define(
|
|
24
|
+
"grep",
|
|
25
|
+
Effect.gen(function* () {
|
|
26
|
+
const fs = yield* AppFileSystem.Service
|
|
27
|
+
const rg = yield* Ripgrep.Service
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
description: DESCRIPTION,
|
|
31
|
+
parameters: Parameters,
|
|
32
|
+
execute: (params: { pattern: string; path?: string; include?: string }, ctx: Tool.Context) =>
|
|
33
|
+
Effect.gen(function* () {
|
|
34
|
+
const empty = {
|
|
35
|
+
title: params.pattern,
|
|
36
|
+
metadata: { matches: 0, truncated: false },
|
|
37
|
+
output: "No files found",
|
|
38
|
+
}
|
|
39
|
+
if (!params.pattern) {
|
|
40
|
+
throw new Error("pattern is required")
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
yield* ctx.ask({
|
|
44
|
+
permission: "grep",
|
|
45
|
+
patterns: [params.pattern],
|
|
46
|
+
always: ["*"],
|
|
47
|
+
metadata: {
|
|
48
|
+
pattern: params.pattern,
|
|
49
|
+
path: params.path,
|
|
50
|
+
include: params.include,
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const ins = yield* InstanceState.context
|
|
55
|
+
const search = AppFileSystem.resolve(
|
|
56
|
+
path.isAbsolute(params.path ?? ins.directory)
|
|
57
|
+
? (params.path ?? ins.directory)
|
|
58
|
+
: path.join(ins.directory, params.path ?? "."),
|
|
59
|
+
)
|
|
60
|
+
const info = yield* fs.stat(search).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
|
61
|
+
const cwd = info?.type === "Directory" ? search : path.dirname(search)
|
|
62
|
+
const file = info?.type === "Directory" ? undefined : [path.relative(cwd, search)]
|
|
63
|
+
yield* assertExternalDirectoryEffect(ctx, search, {
|
|
64
|
+
kind: info?.type === "Directory" ? "directory" : "file",
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const result = yield* rg.search({
|
|
68
|
+
cwd,
|
|
69
|
+
pattern: params.pattern,
|
|
70
|
+
glob: params.include ? [params.include] : undefined,
|
|
71
|
+
file,
|
|
72
|
+
signal: ctx.abort,
|
|
73
|
+
})
|
|
74
|
+
if (result.items.length === 0) return empty
|
|
75
|
+
|
|
76
|
+
const rows = result.items.map((item) => ({
|
|
77
|
+
path: AppFileSystem.resolve(
|
|
78
|
+
path.isAbsolute(item.path.text) ? item.path.text : path.join(cwd, item.path.text),
|
|
79
|
+
),
|
|
80
|
+
line: item.line_number,
|
|
81
|
+
text: item.lines.text,
|
|
82
|
+
}))
|
|
83
|
+
const times = new Map(
|
|
84
|
+
(yield* Effect.forEach(
|
|
85
|
+
[...new Set(rows.map((row) => row.path))],
|
|
86
|
+
Effect.fnUntraced(function* (file) {
|
|
87
|
+
const info = yield* fs.stat(file).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
|
88
|
+
if (!info || info.type === "Directory") return undefined
|
|
89
|
+
return [
|
|
90
|
+
file,
|
|
91
|
+
info.mtime.pipe(
|
|
92
|
+
Option.map((time) => time.getTime()),
|
|
93
|
+
Option.getOrElse(() => 0),
|
|
94
|
+
) ?? 0,
|
|
95
|
+
] as const
|
|
96
|
+
}),
|
|
97
|
+
{ concurrency: 16 },
|
|
98
|
+
)).filter((entry): entry is readonly [string, number] => Boolean(entry)),
|
|
99
|
+
)
|
|
100
|
+
const matches = rows.flatMap((row) => {
|
|
101
|
+
const mtime = times.get(row.path)
|
|
102
|
+
if (mtime === undefined) return []
|
|
103
|
+
return [{ ...row, mtime }]
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
matches.sort((a, b) => b.mtime - a.mtime)
|
|
107
|
+
|
|
108
|
+
const limit = 100
|
|
109
|
+
const truncated = matches.length > limit
|
|
110
|
+
const final = truncated ? matches.slice(0, limit) : matches
|
|
111
|
+
if (final.length === 0) return empty
|
|
112
|
+
|
|
113
|
+
const total = matches.length
|
|
114
|
+
const output = [`Found ${total} matches${truncated ? ` (showing first ${limit})` : ""}`]
|
|
115
|
+
|
|
116
|
+
let current = ""
|
|
117
|
+
for (const match of final) {
|
|
118
|
+
if (current !== match.path) {
|
|
119
|
+
if (current !== "") output.push("")
|
|
120
|
+
current = match.path
|
|
121
|
+
output.push(`${match.path}:`)
|
|
122
|
+
}
|
|
123
|
+
const text =
|
|
124
|
+
match.text.length > MAX_LINE_LENGTH ? match.text.substring(0, MAX_LINE_LENGTH) + "..." : match.text
|
|
125
|
+
output.push(` Line ${match.line}: ${text}`)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (truncated) {
|
|
129
|
+
output.push("")
|
|
130
|
+
output.push(
|
|
131
|
+
`(Results truncated: showing ${limit} of ${total} matches (${total - limit} hidden). Consider using a more specific path or pattern.)`,
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (result.partial) {
|
|
136
|
+
output.push("")
|
|
137
|
+
output.push("(Some paths were inaccessible and skipped)")
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
title: params.pattern,
|
|
142
|
+
metadata: {
|
|
143
|
+
matches: total,
|
|
144
|
+
truncated,
|
|
145
|
+
},
|
|
146
|
+
output: output.join("\n"),
|
|
147
|
+
}
|
|
148
|
+
}).pipe(Effect.orDie),
|
|
149
|
+
}
|
|
150
|
+
}),
|
|
151
|
+
)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
- Fast content search tool that works with any codebase size
|
|
2
|
+
- Searches file contents using regular expressions
|
|
3
|
+
- Supports full regex syntax (eg. "log.*Error", "function\s+\w+", etc.)
|
|
4
|
+
- Filter files by pattern with the include parameter (eg. "*.js", "*.{ts,tsx}")
|
|
5
|
+
- Returns file paths and line numbers with at least one match sorted by modification time
|
|
6
|
+
- Use this tool when you need to find files containing specific patterns
|
|
7
|
+
- If you need to identify/count the number of matches within files, use the Bash tool with `rg` (ripgrep) directly. Do NOT use `grep`.
|
|
8
|
+
- When you are doing a deep search that may require multiple tool invocations, use the Task tool instead
|