ashlrcode 1.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/LICENSE +21 -0
- package/README.md +295 -0
- package/package.json +46 -0
- package/src/__tests__/branded-types.test.ts +47 -0
- package/src/__tests__/context.test.ts +163 -0
- package/src/__tests__/cost-tracker.test.ts +274 -0
- package/src/__tests__/cron.test.ts +197 -0
- package/src/__tests__/dream.test.ts +204 -0
- package/src/__tests__/error-handler.test.ts +192 -0
- package/src/__tests__/features.test.ts +69 -0
- package/src/__tests__/file-history.test.ts +177 -0
- package/src/__tests__/hooks.test.ts +145 -0
- package/src/__tests__/keybindings.test.ts +159 -0
- package/src/__tests__/model-patches.test.ts +82 -0
- package/src/__tests__/permissions-rules.test.ts +121 -0
- package/src/__tests__/permissions.test.ts +108 -0
- package/src/__tests__/project-config.test.ts +63 -0
- package/src/__tests__/retry.test.ts +321 -0
- package/src/__tests__/router.test.ts +158 -0
- package/src/__tests__/session-compact.test.ts +191 -0
- package/src/__tests__/session.test.ts +145 -0
- package/src/__tests__/skill-registry.test.ts +130 -0
- package/src/__tests__/speculation.test.ts +196 -0
- package/src/__tests__/tasks-v2.test.ts +267 -0
- package/src/__tests__/telemetry.test.ts +149 -0
- package/src/__tests__/tool-executor.test.ts +141 -0
- package/src/__tests__/tool-registry.test.ts +166 -0
- package/src/__tests__/undercover.test.ts +93 -0
- package/src/__tests__/workflow.test.ts +195 -0
- package/src/agent/async-context.ts +64 -0
- package/src/agent/context.ts +245 -0
- package/src/agent/cron.ts +189 -0
- package/src/agent/dream.ts +165 -0
- package/src/agent/error-handler.ts +108 -0
- package/src/agent/ipc.ts +256 -0
- package/src/agent/kairos.ts +207 -0
- package/src/agent/loop.ts +314 -0
- package/src/agent/model-patches.ts +68 -0
- package/src/agent/speculation.ts +219 -0
- package/src/agent/sub-agent.ts +125 -0
- package/src/agent/system-prompt.ts +231 -0
- package/src/agent/team.ts +220 -0
- package/src/agent/tool-executor.ts +162 -0
- package/src/agent/workflow.ts +189 -0
- package/src/agent/worktree-manager.ts +86 -0
- package/src/autopilot/queue.ts +186 -0
- package/src/autopilot/scanner.ts +245 -0
- package/src/autopilot/types.ts +58 -0
- package/src/bridge/bridge-client.ts +57 -0
- package/src/bridge/bridge-server.ts +81 -0
- package/src/cli.ts +1120 -0
- package/src/config/features.ts +51 -0
- package/src/config/git.ts +137 -0
- package/src/config/hooks.ts +201 -0
- package/src/config/permissions.ts +251 -0
- package/src/config/project-config.ts +63 -0
- package/src/config/remote-settings.ts +163 -0
- package/src/config/settings-sync.ts +170 -0
- package/src/config/settings.ts +113 -0
- package/src/config/undercover.ts +76 -0
- package/src/config/upgrade-notice.ts +65 -0
- package/src/mcp/client.ts +197 -0
- package/src/mcp/manager.ts +125 -0
- package/src/mcp/oauth.ts +252 -0
- package/src/mcp/types.ts +61 -0
- package/src/persistence/memory.ts +129 -0
- package/src/persistence/session.ts +289 -0
- package/src/planning/plan-mode.ts +128 -0
- package/src/planning/plan-tools.ts +138 -0
- package/src/providers/anthropic.ts +177 -0
- package/src/providers/cost-tracker.ts +184 -0
- package/src/providers/retry.ts +264 -0
- package/src/providers/router.ts +159 -0
- package/src/providers/types.ts +79 -0
- package/src/providers/xai.ts +217 -0
- package/src/repl.tsx +1384 -0
- package/src/setup.ts +119 -0
- package/src/skills/loader.ts +78 -0
- package/src/skills/registry.ts +78 -0
- package/src/skills/types.ts +11 -0
- package/src/state/file-history.ts +264 -0
- package/src/telemetry/event-log.ts +116 -0
- package/src/tools/agent.ts +133 -0
- package/src/tools/ask-user.ts +229 -0
- package/src/tools/bash.ts +146 -0
- package/src/tools/config.ts +147 -0
- package/src/tools/diff.ts +137 -0
- package/src/tools/file-edit.ts +123 -0
- package/src/tools/file-read.ts +82 -0
- package/src/tools/file-write.ts +82 -0
- package/src/tools/glob.ts +76 -0
- package/src/tools/grep.ts +187 -0
- package/src/tools/ls.ts +77 -0
- package/src/tools/lsp.ts +375 -0
- package/src/tools/mcp-resources.ts +83 -0
- package/src/tools/mcp-tool.ts +47 -0
- package/src/tools/memory.ts +148 -0
- package/src/tools/notebook-edit.ts +133 -0
- package/src/tools/peers.ts +113 -0
- package/src/tools/powershell.ts +83 -0
- package/src/tools/registry.ts +114 -0
- package/src/tools/send-message.ts +75 -0
- package/src/tools/sleep.ts +50 -0
- package/src/tools/snip.ts +143 -0
- package/src/tools/tasks.ts +349 -0
- package/src/tools/team.ts +309 -0
- package/src/tools/todo-write.ts +93 -0
- package/src/tools/tool-search.ts +83 -0
- package/src/tools/types.ts +52 -0
- package/src/tools/web-browser.ts +263 -0
- package/src/tools/web-fetch.ts +118 -0
- package/src/tools/web-search.ts +107 -0
- package/src/tools/workflow.ts +188 -0
- package/src/tools/worktree.ts +143 -0
- package/src/types/branded.ts +22 -0
- package/src/ui/App.tsx +184 -0
- package/src/ui/BuddyPanel.tsx +52 -0
- package/src/ui/PermissionPrompt.tsx +29 -0
- package/src/ui/banner.ts +217 -0
- package/src/ui/buddy-ai.ts +108 -0
- package/src/ui/buddy.ts +466 -0
- package/src/ui/context-bar.ts +60 -0
- package/src/ui/effort.ts +65 -0
- package/src/ui/keybindings.ts +143 -0
- package/src/ui/markdown.ts +271 -0
- package/src/ui/message-renderer.ts +73 -0
- package/src/ui/mode.ts +80 -0
- package/src/ui/notifications.ts +57 -0
- package/src/ui/speech-bubble.ts +95 -0
- package/src/ui/spinner.ts +116 -0
- package/src/ui/theme.ts +98 -0
- package/src/version.ts +5 -0
- package/src/voice/voice-mode.ts +169 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Team management tools — create, manage, and delegate to persistent teammates.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Tool, ToolContext } from "./types.ts";
|
|
6
|
+
import type { Teammate } from "../agent/team.ts";
|
|
7
|
+
import {
|
|
8
|
+
createTeam,
|
|
9
|
+
addTeammate,
|
|
10
|
+
removeTeammate,
|
|
11
|
+
deleteTeam,
|
|
12
|
+
listTeams,
|
|
13
|
+
loadTeam,
|
|
14
|
+
pickTeammateForTask,
|
|
15
|
+
recordTeammateActivity,
|
|
16
|
+
} from "../agent/team.ts";
|
|
17
|
+
import { runSubAgent } from "../agent/sub-agent.ts";
|
|
18
|
+
import type { ProviderRouter } from "../providers/router.ts";
|
|
19
|
+
import type { ToolRegistry } from "./registry.ts";
|
|
20
|
+
|
|
21
|
+
// Module-level refs injected at registration time (same pattern as agent.ts)
|
|
22
|
+
let _router: ProviderRouter | null = null;
|
|
23
|
+
let _registry: ToolRegistry | null = null;
|
|
24
|
+
let _baseSystemPrompt: string = "";
|
|
25
|
+
|
|
26
|
+
export function initTeamTools(
|
|
27
|
+
router: ProviderRouter,
|
|
28
|
+
registry: ToolRegistry,
|
|
29
|
+
systemPrompt: string,
|
|
30
|
+
) {
|
|
31
|
+
_router = router;
|
|
32
|
+
_registry = registry;
|
|
33
|
+
_baseSystemPrompt = systemPrompt;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const teamCreateTool: Tool = {
|
|
37
|
+
name: "TeamCreate",
|
|
38
|
+
prompt() {
|
|
39
|
+
return "Create a new team or add a teammate. Teams persist across sessions and teammates can be assigned specialized roles.";
|
|
40
|
+
},
|
|
41
|
+
inputSchema() {
|
|
42
|
+
return {
|
|
43
|
+
type: "object",
|
|
44
|
+
properties: {
|
|
45
|
+
action: {
|
|
46
|
+
type: "string",
|
|
47
|
+
enum: ["create_team", "add_teammate"],
|
|
48
|
+
description: "What to do",
|
|
49
|
+
},
|
|
50
|
+
teamName: {
|
|
51
|
+
type: "string",
|
|
52
|
+
description: "Team name (for create_team)",
|
|
53
|
+
},
|
|
54
|
+
teamId: {
|
|
55
|
+
type: "string",
|
|
56
|
+
description: "Team ID (for add_teammate)",
|
|
57
|
+
},
|
|
58
|
+
name: { type: "string", description: "Teammate name" },
|
|
59
|
+
role: {
|
|
60
|
+
type: "string",
|
|
61
|
+
enum: ["code-reviewer", "test-writer", "explorer", "implementer"],
|
|
62
|
+
description: "Teammate role",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
required: ["action"],
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
isReadOnly() {
|
|
69
|
+
return false;
|
|
70
|
+
},
|
|
71
|
+
isDestructive() {
|
|
72
|
+
return false;
|
|
73
|
+
},
|
|
74
|
+
isConcurrencySafe() {
|
|
75
|
+
return true;
|
|
76
|
+
},
|
|
77
|
+
validateInput(input) {
|
|
78
|
+
const action = input.action as string;
|
|
79
|
+
if (action === "create_team" && !input.teamName)
|
|
80
|
+
return "teamName required";
|
|
81
|
+
if (
|
|
82
|
+
action === "add_teammate" &&
|
|
83
|
+
(!input.teamId || !input.name || !input.role)
|
|
84
|
+
)
|
|
85
|
+
return "teamId, name, and role required";
|
|
86
|
+
return null;
|
|
87
|
+
},
|
|
88
|
+
async call(input) {
|
|
89
|
+
const action = input.action as string;
|
|
90
|
+
if (action === "create_team") {
|
|
91
|
+
const team = await createTeam(input.teamName as string);
|
|
92
|
+
return `Team "${team.name}" created (ID: ${team.id})`;
|
|
93
|
+
}
|
|
94
|
+
if (action === "add_teammate") {
|
|
95
|
+
const mate = await addTeammate(
|
|
96
|
+
input.teamId as string,
|
|
97
|
+
input.name as string,
|
|
98
|
+
input.role as string,
|
|
99
|
+
);
|
|
100
|
+
return `Teammate "${mate.name}" (${mate.role}) added (ID: ${mate.id})`;
|
|
101
|
+
}
|
|
102
|
+
return "Unknown action";
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const teamDeleteTool: Tool = {
|
|
107
|
+
name: "TeamDelete",
|
|
108
|
+
prompt() {
|
|
109
|
+
return "Delete a team or remove a teammate.";
|
|
110
|
+
},
|
|
111
|
+
inputSchema() {
|
|
112
|
+
return {
|
|
113
|
+
type: "object",
|
|
114
|
+
properties: {
|
|
115
|
+
action: {
|
|
116
|
+
type: "string",
|
|
117
|
+
enum: ["delete_team", "remove_teammate"],
|
|
118
|
+
},
|
|
119
|
+
teamId: { type: "string" },
|
|
120
|
+
teammateId: {
|
|
121
|
+
type: "string",
|
|
122
|
+
description: "For remove_teammate",
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
required: ["action", "teamId"],
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
isReadOnly() {
|
|
129
|
+
return false;
|
|
130
|
+
},
|
|
131
|
+
isDestructive() {
|
|
132
|
+
return true;
|
|
133
|
+
},
|
|
134
|
+
isConcurrencySafe() {
|
|
135
|
+
return true;
|
|
136
|
+
},
|
|
137
|
+
validateInput(input) {
|
|
138
|
+
if (!input.teamId) return "teamId required";
|
|
139
|
+
if (input.action === "remove_teammate" && !input.teammateId)
|
|
140
|
+
return "teammateId required";
|
|
141
|
+
return null;
|
|
142
|
+
},
|
|
143
|
+
async call(input) {
|
|
144
|
+
if (input.action === "delete_team") {
|
|
145
|
+
const ok = await deleteTeam(input.teamId as string);
|
|
146
|
+
return ok ? `Team deleted` : `Team not found`;
|
|
147
|
+
}
|
|
148
|
+
if (input.action === "remove_teammate") {
|
|
149
|
+
const ok = await removeTeammate(
|
|
150
|
+
input.teamId as string,
|
|
151
|
+
input.teammateId as string,
|
|
152
|
+
);
|
|
153
|
+
return ok ? `Teammate removed` : `Not found`;
|
|
154
|
+
}
|
|
155
|
+
return "Unknown action";
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const teamListTool: Tool = {
|
|
160
|
+
name: "TeamList",
|
|
161
|
+
prompt() {
|
|
162
|
+
return "List all teams and their teammates.";
|
|
163
|
+
},
|
|
164
|
+
inputSchema() {
|
|
165
|
+
return {
|
|
166
|
+
type: "object",
|
|
167
|
+
properties: {
|
|
168
|
+
teamId: {
|
|
169
|
+
type: "string",
|
|
170
|
+
description: "Optional: show detail for specific team",
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
},
|
|
175
|
+
isReadOnly() {
|
|
176
|
+
return true;
|
|
177
|
+
},
|
|
178
|
+
isDestructive() {
|
|
179
|
+
return false;
|
|
180
|
+
},
|
|
181
|
+
isConcurrencySafe() {
|
|
182
|
+
return true;
|
|
183
|
+
},
|
|
184
|
+
validateInput() {
|
|
185
|
+
return null;
|
|
186
|
+
},
|
|
187
|
+
async call(input) {
|
|
188
|
+
if (input.teamId) {
|
|
189
|
+
const team = await loadTeam(input.teamId as string);
|
|
190
|
+
if (!team) return "Team not found";
|
|
191
|
+
const mates = team.teammates
|
|
192
|
+
.map(
|
|
193
|
+
(t) =>
|
|
194
|
+
` ${t.name} (${t.role}) — ${t.stats.tasksCompleted} tasks done${t.lastActiveAt ? `, last active ${new Date(t.lastActiveAt).toLocaleDateString()}` : ""}`,
|
|
195
|
+
)
|
|
196
|
+
.join("\n");
|
|
197
|
+
return `Team: ${team.name}\nTeammates:\n${mates || " (none)"}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const teams = await listTeams();
|
|
201
|
+
if (teams.length === 0) return "No teams. Use TeamCreate to create one.";
|
|
202
|
+
return teams
|
|
203
|
+
.map((t) => `${t.name} (${t.id}) — ${t.teammates.length} teammates`)
|
|
204
|
+
.join("\n");
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export const teamDispatchTool: Tool = {
|
|
209
|
+
name: "TeamDispatch",
|
|
210
|
+
prompt() {
|
|
211
|
+
return "Dispatch a task to a teammate. The teammate runs as a sub-agent with their specialized role and system prompt. Returns the teammate's findings/results.";
|
|
212
|
+
},
|
|
213
|
+
inputSchema() {
|
|
214
|
+
return {
|
|
215
|
+
type: "object",
|
|
216
|
+
properties: {
|
|
217
|
+
teamId: { type: "string", description: "Team ID" },
|
|
218
|
+
teammateId: {
|
|
219
|
+
type: "string",
|
|
220
|
+
description:
|
|
221
|
+
"Teammate ID (optional — auto-picks best match if omitted)",
|
|
222
|
+
},
|
|
223
|
+
task: {
|
|
224
|
+
type: "string",
|
|
225
|
+
description: "Task description for the teammate",
|
|
226
|
+
},
|
|
227
|
+
taskType: {
|
|
228
|
+
type: "string",
|
|
229
|
+
description:
|
|
230
|
+
"Task type for auto-matching (e.g., 'code-reviewer', 'test-writer')",
|
|
231
|
+
},
|
|
232
|
+
mode: {
|
|
233
|
+
type: "string",
|
|
234
|
+
enum: ["in_process", "worktree"],
|
|
235
|
+
description: "Execution mode",
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
required: ["teamId", "task"],
|
|
239
|
+
};
|
|
240
|
+
},
|
|
241
|
+
isReadOnly() {
|
|
242
|
+
return false;
|
|
243
|
+
},
|
|
244
|
+
isDestructive() {
|
|
245
|
+
return false;
|
|
246
|
+
},
|
|
247
|
+
isConcurrencySafe() {
|
|
248
|
+
return false;
|
|
249
|
+
},
|
|
250
|
+
validateInput(input) {
|
|
251
|
+
if (!input.teamId) return "teamId required";
|
|
252
|
+
if (!input.task) return "task required";
|
|
253
|
+
if (!_router || !_registry)
|
|
254
|
+
return "Team tools not initialized. Call initTeamTools() first.";
|
|
255
|
+
return null;
|
|
256
|
+
},
|
|
257
|
+
async call(input: Record<string, unknown>, context: ToolContext) {
|
|
258
|
+
const teamId = input.teamId as string;
|
|
259
|
+
const team = await loadTeam(teamId);
|
|
260
|
+
if (!team) return "Team not found";
|
|
261
|
+
|
|
262
|
+
let teammate: Teammate | null = null;
|
|
263
|
+
if (input.teammateId) {
|
|
264
|
+
teammate =
|
|
265
|
+
team.teammates.find((t) => t.id === (input.teammateId as string)) ??
|
|
266
|
+
null;
|
|
267
|
+
} else {
|
|
268
|
+
teammate = pickTeammateForTask(
|
|
269
|
+
team,
|
|
270
|
+
(input.taskType as string) ?? "",
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!teammate) return "No suitable teammate found";
|
|
275
|
+
|
|
276
|
+
const task = input.task as string;
|
|
277
|
+
const mode = (input.mode as "in_process" | "worktree") ?? "in_process";
|
|
278
|
+
const readOnly =
|
|
279
|
+
teammate.role === "code-reviewer" || teammate.role === "explorer";
|
|
280
|
+
|
|
281
|
+
console.log(` ◈ Dispatching to ${teammate.name} (${teammate.role})`);
|
|
282
|
+
|
|
283
|
+
const result = await runSubAgent({
|
|
284
|
+
name: `${teammate.name}-${teammate.role}`,
|
|
285
|
+
prompt: task,
|
|
286
|
+
systemPrompt:
|
|
287
|
+
_baseSystemPrompt + "\n\n" + teammate.systemPrompt,
|
|
288
|
+
router: _router!,
|
|
289
|
+
toolRegistry: _registry!,
|
|
290
|
+
toolContext: context,
|
|
291
|
+
readOnly,
|
|
292
|
+
maxIterations: 15,
|
|
293
|
+
mode,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
await recordTeammateActivity(
|
|
297
|
+
teamId,
|
|
298
|
+
teammate.id,
|
|
299
|
+
1,
|
|
300
|
+
result.toolCalls.length,
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
const worktreeInfo = result.worktree
|
|
304
|
+
? `\nWorktree: ${result.worktree.path} (${result.worktree.branch})`
|
|
305
|
+
: "";
|
|
306
|
+
|
|
307
|
+
return `## ${teammate.name} (${teammate.role})\n\n${result.text}${worktreeInfo}`;
|
|
308
|
+
},
|
|
309
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TodoWrite — write a structured todo/plan list to a file.
|
|
3
|
+
* Equivalent to Claude Code's TodoWriteTool.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { writeFile, mkdir } from "fs/promises";
|
|
7
|
+
import { dirname, resolve } from "path";
|
|
8
|
+
import type { Tool, ToolContext } from "./types.ts";
|
|
9
|
+
|
|
10
|
+
export const todoWriteTool: Tool = {
|
|
11
|
+
name: "TodoWrite",
|
|
12
|
+
|
|
13
|
+
prompt() {
|
|
14
|
+
return "Write a structured todo list or plan to a file. Use this to create implementation checklists, track work items, or document a plan. The file is written in markdown format with checkboxes.";
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
inputSchema() {
|
|
18
|
+
return {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
file_path: {
|
|
22
|
+
type: "string",
|
|
23
|
+
description: "Path to write the todo file (e.g., PLAN.md, TODO.md)",
|
|
24
|
+
},
|
|
25
|
+
todos: {
|
|
26
|
+
type: "array",
|
|
27
|
+
items: {
|
|
28
|
+
type: "object",
|
|
29
|
+
properties: {
|
|
30
|
+
task: { type: "string", description: "Task description" },
|
|
31
|
+
completed: { type: "boolean", description: "Whether the task is done" },
|
|
32
|
+
subtasks: {
|
|
33
|
+
type: "array",
|
|
34
|
+
items: {
|
|
35
|
+
type: "object",
|
|
36
|
+
properties: {
|
|
37
|
+
task: { type: "string" },
|
|
38
|
+
completed: { type: "boolean" },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
required: ["task"],
|
|
44
|
+
},
|
|
45
|
+
description: "List of todo items",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
required: ["file_path", "todos"],
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
isReadOnly() { return false; },
|
|
53
|
+
isDestructive() { return false; },
|
|
54
|
+
isConcurrencySafe() { return false; },
|
|
55
|
+
|
|
56
|
+
validateInput(input) {
|
|
57
|
+
if (!input.file_path) return "file_path is required";
|
|
58
|
+
if (!Array.isArray(input.todos) || input.todos.length === 0) {
|
|
59
|
+
return "todos must be a non-empty array";
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
async call(input, context) {
|
|
65
|
+
const filePath = resolve(context.cwd, input.file_path as string);
|
|
66
|
+
const todos = input.todos as Array<{
|
|
67
|
+
task: string;
|
|
68
|
+
completed?: boolean;
|
|
69
|
+
subtasks?: Array<{ task: string; completed?: boolean }>;
|
|
70
|
+
}>;
|
|
71
|
+
|
|
72
|
+
const lines: string[] = ["# Plan\n"];
|
|
73
|
+
|
|
74
|
+
for (const todo of todos) {
|
|
75
|
+
const check = todo.completed ? "x" : " ";
|
|
76
|
+
lines.push(`- [${check}] ${todo.task}`);
|
|
77
|
+
|
|
78
|
+
if (todo.subtasks) {
|
|
79
|
+
for (const sub of todo.subtasks) {
|
|
80
|
+
const subCheck = sub.completed ? "x" : " ";
|
|
81
|
+
lines.push(` - [${subCheck}] ${sub.task}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
87
|
+
await writeFile(filePath, lines.join("\n") + "\n", "utf-8");
|
|
88
|
+
|
|
89
|
+
const total = todos.length;
|
|
90
|
+
const completed = todos.filter((t) => t.completed).length;
|
|
91
|
+
return `Wrote ${total} todos (${completed} completed) to ${filePath}`;
|
|
92
|
+
},
|
|
93
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ToolSearch — find tools by keyword.
|
|
3
|
+
* Useful when 30+ tools are registered (including MCP tools).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Tool, ToolContext } from "./types.ts";
|
|
7
|
+
import type { ToolRegistry } from "./registry.ts";
|
|
8
|
+
|
|
9
|
+
// Injected at registration time
|
|
10
|
+
let _registry: ToolRegistry | null = null;
|
|
11
|
+
|
|
12
|
+
export function initToolSearch(registry: ToolRegistry): void {
|
|
13
|
+
_registry = registry;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const toolSearchTool: Tool = {
|
|
17
|
+
name: "ToolSearch",
|
|
18
|
+
|
|
19
|
+
prompt() {
|
|
20
|
+
return "Search for available tools by keyword. Returns matching tool names and descriptions. Useful when many tools are registered (including MCP tools).";
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
inputSchema() {
|
|
24
|
+
return {
|
|
25
|
+
type: "object",
|
|
26
|
+
properties: {
|
|
27
|
+
query: {
|
|
28
|
+
type: "string",
|
|
29
|
+
description: "Keyword to search for in tool names and descriptions",
|
|
30
|
+
},
|
|
31
|
+
maxResults: {
|
|
32
|
+
type: "number",
|
|
33
|
+
description: "Maximum results to return (default: 10)",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
required: ["query"],
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
isReadOnly() {
|
|
41
|
+
return true;
|
|
42
|
+
},
|
|
43
|
+
isDestructive() {
|
|
44
|
+
return false;
|
|
45
|
+
},
|
|
46
|
+
isConcurrencySafe() {
|
|
47
|
+
return true;
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
validateInput(input) {
|
|
51
|
+
if (!input.query || typeof input.query !== "string") {
|
|
52
|
+
return "query is required";
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
async call(input, _context) {
|
|
58
|
+
if (!_registry) return "ToolSearch not initialized";
|
|
59
|
+
|
|
60
|
+
const query = (input.query as string).toLowerCase();
|
|
61
|
+
const max = (input.maxResults as number) ?? 10;
|
|
62
|
+
|
|
63
|
+
const allTools = _registry.getAll();
|
|
64
|
+
const matches = allTools
|
|
65
|
+
.filter((t) => {
|
|
66
|
+
const name = t.name.toLowerCase();
|
|
67
|
+
const desc = t.prompt().toLowerCase();
|
|
68
|
+
return name.includes(query) || desc.includes(query);
|
|
69
|
+
})
|
|
70
|
+
.slice(0, max);
|
|
71
|
+
|
|
72
|
+
if (matches.length === 0) {
|
|
73
|
+
return `No tools matching "${query}". Total tools: ${allTools.length}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const lines = matches.map((t) => {
|
|
77
|
+
const readOnly = t.isReadOnly() ? " [read-only]" : "";
|
|
78
|
+
return `- **${t.name}**${readOnly}: ${t.prompt().slice(0, 100)}`;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return `${matches.length} tool(s) matching "${query}":\n\n${lines.join("\n")}`;
|
|
82
|
+
},
|
|
83
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool interface — every tool implements this contract.
|
|
3
|
+
* Follows Claude Code's pattern: validate → permissions → execute.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ToolDefinition } from "../providers/types.ts";
|
|
7
|
+
|
|
8
|
+
export interface ToolContext {
|
|
9
|
+
cwd: string;
|
|
10
|
+
/** Ask user for permission to run a tool */
|
|
11
|
+
requestPermission: (tool: string, description: string) => Promise<boolean>;
|
|
12
|
+
/** Current turn number (for file history tracking) */
|
|
13
|
+
turnNumber?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface Tool {
|
|
17
|
+
/** Tool name used in API calls */
|
|
18
|
+
name: string;
|
|
19
|
+
|
|
20
|
+
/** Human-readable description for the LLM */
|
|
21
|
+
prompt(): string;
|
|
22
|
+
|
|
23
|
+
/** JSON Schema for the tool's input parameters */
|
|
24
|
+
inputSchema(): Record<string, unknown>;
|
|
25
|
+
|
|
26
|
+
/** Whether this tool only reads data (safe in plan mode) */
|
|
27
|
+
isReadOnly(): boolean;
|
|
28
|
+
|
|
29
|
+
/** Whether this tool could cause damage if misused */
|
|
30
|
+
isDestructive(): boolean;
|
|
31
|
+
|
|
32
|
+
/** Whether multiple instances can run concurrently */
|
|
33
|
+
isConcurrencySafe(): boolean;
|
|
34
|
+
|
|
35
|
+
/** Validate input before execution */
|
|
36
|
+
validateInput(input: Record<string, unknown>): string | null;
|
|
37
|
+
|
|
38
|
+
/** Tool-specific permission check. Return null if allowed, or error string if denied. */
|
|
39
|
+
checkPermissions?(input: Record<string, unknown>, context: ToolContext): string | null;
|
|
40
|
+
|
|
41
|
+
/** Execute the tool and return result text */
|
|
42
|
+
call(input: Record<string, unknown>, context: ToolContext): Promise<string>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Convert a Tool to the provider-facing ToolDefinition */
|
|
46
|
+
export function toolToDefinition(tool: Tool): ToolDefinition {
|
|
47
|
+
return {
|
|
48
|
+
name: tool.name,
|
|
49
|
+
description: tool.prompt(),
|
|
50
|
+
input_schema: tool.inputSchema(),
|
|
51
|
+
};
|
|
52
|
+
}
|