@meowlynxsea/koi 0.1.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 +34 -0
- package/NOTICE +35 -0
- package/README.md +15 -0
- package/bin/koi +12 -0
- package/dist/highlights-eq9cgrbb.scm +604 -0
- package/dist/highlights-ghv9g403.scm +205 -0
- package/dist/highlights-hk7bwhj4.scm +284 -0
- package/dist/highlights-r812a2qc.scm +150 -0
- package/dist/highlights-x6tmsnaa.scm +115 -0
- package/dist/injections-73j83es3.scm +27 -0
- package/dist/main.js +489918 -0
- package/dist/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
- package/dist/tree-sitter-markdown-411r6y9b.wasm +0 -0
- package/dist/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
- package/dist/tree-sitter-typescript-zxjzwt75.wasm +0 -0
- package/dist/tree-sitter-zig-e78zbjpm.wasm +0 -0
- package/package.json +51 -0
- package/src/agent/check-permissions.ts +239 -0
- package/src/agent/hooks/message-utils.ts +305 -0
- package/src/agent/hooks/types.ts +32 -0
- package/src/agent/hooks.ts +1560 -0
- package/src/agent/mode.ts +163 -0
- package/src/agent/monitor-registry.ts +308 -0
- package/src/agent/permission-ui.ts +71 -0
- package/src/agent/plan-ui.ts +74 -0
- package/src/agent/question-ui.ts +58 -0
- package/src/agent/session-fork.ts +299 -0
- package/src/agent/session-snapshots.ts +216 -0
- package/src/agent/session-store.ts +649 -0
- package/src/agent/session-tasks.ts +305 -0
- package/src/agent/session.ts +27 -0
- package/src/agent/subagent-registry.ts +176 -0
- package/src/agent/subagent.ts +194 -0
- package/src/agent/tool-orchestration.ts +55 -0
- package/src/agent/tools.ts +8 -0
- package/src/cli/args.ts +6 -0
- package/src/cli/commands.ts +5 -0
- package/src/commands/skills/index.ts +23 -0
- package/src/config/models.ts +6 -0
- package/src/config/settings.ts +392 -0
- package/src/main.tsx +64 -0
- package/src/services/mcp/client.ts +194 -0
- package/src/services/mcp/config.ts +232 -0
- package/src/services/mcp/connection-manager.ts +258 -0
- package/src/services/mcp/index.ts +80 -0
- package/src/services/mcp/mcp-commands.ts +114 -0
- package/src/services/mcp/stdio-transport.ts +246 -0
- package/src/services/mcp/types.ts +155 -0
- package/src/skills/SkillsMenu.tsx +370 -0
- package/src/skills/bundled/batch.ts +106 -0
- package/src/skills/bundled/debug.ts +86 -0
- package/src/skills/bundled/loremIpsum.ts +101 -0
- package/src/skills/bundled/remember.ts +97 -0
- package/src/skills/bundled/simplify.ts +100 -0
- package/src/skills/bundled/skillify.ts +123 -0
- package/src/skills/bundled/stuck.ts +101 -0
- package/src/skills/bundled/updateConfig.ts +228 -0
- package/src/skills/bundled.ts +46 -0
- package/src/skills/frontmatter.ts +179 -0
- package/src/skills/index.ts +87 -0
- package/src/skills/invoke.ts +231 -0
- package/src/skills/loader.ts +710 -0
- package/src/skills/substitution.ts +169 -0
- package/src/skills/types.ts +201 -0
- package/src/tools/agent.ts +143 -0
- package/src/tools/ask-user-question.ts +46 -0
- package/src/tools/bash.ts +148 -0
- package/src/tools/edit.ts +164 -0
- package/src/tools/glob.ts +102 -0
- package/src/tools/grep.ts +248 -0
- package/src/tools/index.ts +73 -0
- package/src/tools/list-mcp-resources.ts +74 -0
- package/src/tools/ls.ts +85 -0
- package/src/tools/mcp.ts +76 -0
- package/src/tools/monitor.ts +159 -0
- package/src/tools/plan-mode.ts +134 -0
- package/src/tools/read-mcp-resource.ts +79 -0
- package/src/tools/read.ts +137 -0
- package/src/tools/skill.ts +176 -0
- package/src/tools/task.ts +349 -0
- package/src/tools/types.ts +52 -0
- package/src/tools/webfetch-domains.ts +239 -0
- package/src/tools/webfetch.ts +533 -0
- package/src/tools/write.ts +101 -0
- package/src/tui/app.tsx +1178 -0
- package/src/tui/components/chat-panel.tsx +1071 -0
- package/src/tui/components/command-panel.tsx +261 -0
- package/src/tui/components/confirm-modal.tsx +135 -0
- package/src/tui/components/connect-modal.tsx +435 -0
- package/src/tui/components/connecting-modal.tsx +167 -0
- package/src/tui/components/edit-pending-modal.tsx +103 -0
- package/src/tui/components/exit-modal.tsx +131 -0
- package/src/tui/components/fork-modal.tsx +377 -0
- package/src/tui/components/image-preview-modal.tsx +141 -0
- package/src/tui/components/image-utils.ts +128 -0
- package/src/tui/components/info-bar.tsx +103 -0
- package/src/tui/components/input-box.tsx +352 -0
- package/src/tui/components/mcp/MCPSettings.tsx +386 -0
- package/src/tui/components/mcp/index.ts +7 -0
- package/src/tui/components/model-modal.tsx +310 -0
- package/src/tui/components/pending-area.tsx +88 -0
- package/src/tui/components/rename-modal.tsx +119 -0
- package/src/tui/components/session-modal.tsx +233 -0
- package/src/tui/components/side-bar.tsx +349 -0
- package/src/tui/components/tool-output.ts +6 -0
- package/src/tui/hooks/user-prompt-history.ts +114 -0
- package/src/tui/theme.ts +63 -0
- package/src/types/commands.ts +80 -0
- package/src/types/cross-spawn.d.ts +24 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileReadTool — 多格式文件阅读器
|
|
3
|
+
*
|
|
4
|
+
* Supports plain text files with line-range reading.
|
|
5
|
+
* Image / PDF / Notebook support can be added in future iterations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Type } from "typebox";
|
|
9
|
+
import { readFileSync, statSync, existsSync } from "fs";
|
|
10
|
+
import { resolve } from "path";
|
|
11
|
+
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
12
|
+
import type { TextContent } from "@mariozechner/pi-ai";
|
|
13
|
+
import { checkPermission } from "../agent/check-permissions.js";
|
|
14
|
+
import { requestPermission } from "../agent/permission-ui.js";
|
|
15
|
+
import type { ToolResultWithError } from "./types.js";
|
|
16
|
+
import { getErrorMessage } from "./types.js";
|
|
17
|
+
|
|
18
|
+
export const readSchema = Type.Object({
|
|
19
|
+
path: Type.String({ description: "Path to the file to read (relative or absolute)" }),
|
|
20
|
+
offset: Type.Optional(Type.Number({ description: "Line number to start reading from (1-indexed)" })),
|
|
21
|
+
limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export type ReadToolInput = {
|
|
25
|
+
path: string;
|
|
26
|
+
offset?: number;
|
|
27
|
+
limit?: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const BLOCKED_PATHS = [
|
|
31
|
+
"/dev/zero",
|
|
32
|
+
"/dev/random",
|
|
33
|
+
"/dev/urandom",
|
|
34
|
+
"/dev/full",
|
|
35
|
+
"/dev/stdin",
|
|
36
|
+
"/dev/tty",
|
|
37
|
+
"/dev/console",
|
|
38
|
+
"/dev/stdout",
|
|
39
|
+
"/dev/stderr",
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/** 可配置的文件大小上限(默认 256 MiB) */
|
|
43
|
+
const MAX_FILE_SIZE_BYTES = 256 * 1024 * 1024;
|
|
44
|
+
|
|
45
|
+
function isBlockedPath(p: string): boolean {
|
|
46
|
+
const normalized = p.toLowerCase().replace(/\\/g, "/");
|
|
47
|
+
return BLOCKED_PATHS.some((bp) => normalized.startsWith(bp.toLowerCase()));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readFileInRange(filePath: string, offset?: number, limit?: number): { content: string; totalLines: number } {
|
|
51
|
+
const buf = readFileSync(filePath, "utf-8");
|
|
52
|
+
const allLines = buf.split("\n");
|
|
53
|
+
const totalLines = allLines.length;
|
|
54
|
+
|
|
55
|
+
const start = offset ? Math.max(0, offset - 1) : 0;
|
|
56
|
+
const end = limit !== undefined ? start + limit : allLines.length;
|
|
57
|
+
const sliced = allLines.slice(start, end);
|
|
58
|
+
|
|
59
|
+
return { content: sliced.join("\n"), totalLines };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function executeRead(
|
|
63
|
+
_toolCallId: string,
|
|
64
|
+
params: ReadToolInput
|
|
65
|
+
): Promise<{ content: TextContent[]; details: { path: string; totalLines: number; readLines: number } }> {
|
|
66
|
+
const filePath = resolve(params.path);
|
|
67
|
+
|
|
68
|
+
if (!existsSync(filePath)) {
|
|
69
|
+
throw new Error(`File not found: ${params.path}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (isBlockedPath(filePath)) {
|
|
73
|
+
throw new Error(`Reading from device/special paths is not allowed: ${params.path}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let stats: ReturnType<typeof statSync>;
|
|
77
|
+
try {
|
|
78
|
+
stats = statSync(filePath);
|
|
79
|
+
} catch (err: unknown) {
|
|
80
|
+
throw new Error(`Unable to read file metadata: ${params.path} (${getErrorMessage(err)})`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (stats.isDirectory()) {
|
|
84
|
+
throw new Error(`Path is a directory, not a file: ${params.path}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (stats.size > MAX_FILE_SIZE_BYTES) {
|
|
88
|
+
throw new Error(`File too large (${(stats.size / 1024 / 1024).toFixed(1)} MiB). Use a more targeted approach.`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const { content, totalLines } = readFileInRange(filePath, params.offset, params.limit);
|
|
92
|
+
const readLines = content.split("\n").length;
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
content: [{ type: "text", text: content }],
|
|
96
|
+
details: { path: filePath, totalLines, readLines },
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function createReadToolDefinition(_cwd: string): ToolDefinition<typeof readSchema, { path: string; totalLines: number; readLines: number }> {
|
|
101
|
+
return {
|
|
102
|
+
name: "read",
|
|
103
|
+
label: "Read",
|
|
104
|
+
description: "Reads a file from the local filesystem.",
|
|
105
|
+
promptSnippet: "Read: reads files (text, images, PDFs, notebooks). Use absolute paths.",
|
|
106
|
+
promptGuidelines: [
|
|
107
|
+
"The file_path parameter must be an absolute path",
|
|
108
|
+
"By default, reads up to 2000 lines from the beginning",
|
|
109
|
+
"If you read a file that exists but has empty contents you will receive a system reminder warning",
|
|
110
|
+
],
|
|
111
|
+
parameters: readSchema,
|
|
112
|
+
executionMode: "parallel",
|
|
113
|
+
async execute(toolCallId, params, _signal, _onUpdate) {
|
|
114
|
+
const perm = checkPermission("read", params);
|
|
115
|
+
if (perm.decision === "deny") {
|
|
116
|
+
const result: ToolResultWithError<{ path: string; totalLines: number; readLines: number }> = {
|
|
117
|
+
content: [{ type: "text", text: `Permission denied: ${perm.reason ?? "read operation blocked"}` }],
|
|
118
|
+
details: { path: params.path, totalLines: 0, readLines: 0 },
|
|
119
|
+
isError: true,
|
|
120
|
+
};
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
if (perm.decision === "ask") {
|
|
124
|
+
const allowed = await requestPermission({ toolName: "read", args: params, reason: perm.reason ?? "Confirm file read" });
|
|
125
|
+
if (!allowed) {
|
|
126
|
+
const result: ToolResultWithError<{ path: string; totalLines: number; readLines: number }> = {
|
|
127
|
+
content: [{ type: "text", text: "User denied permission to read the file." }],
|
|
128
|
+
details: { path: params.path, totalLines: 0, readLines: 0 },
|
|
129
|
+
isError: true,
|
|
130
|
+
};
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return await executeRead(toolCallId, params);
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Tool — Load and invoke Koi skills
|
|
3
|
+
*
|
|
4
|
+
* Provides the agent with a dedicated tool to:
|
|
5
|
+
* 1. List all available skills
|
|
6
|
+
* 2. Load and execute a skill's content with argument substitution
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Type } from "typebox";
|
|
10
|
+
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
11
|
+
import type { ContentBlockParam } from "../skills/types.js";
|
|
12
|
+
import {
|
|
13
|
+
getAllSkills,
|
|
14
|
+
getSkillByName,
|
|
15
|
+
invokeSkill,
|
|
16
|
+
type SkillCommand,
|
|
17
|
+
} from "../skills/index.js";
|
|
18
|
+
|
|
19
|
+
// ─── Schemas ─────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
export const skillSchema = Type.Object({
|
|
22
|
+
name: Type.Optional(
|
|
23
|
+
Type.String({
|
|
24
|
+
description: "Name of the skill to invoke (e.g., 'review', 'test')",
|
|
25
|
+
})
|
|
26
|
+
),
|
|
27
|
+
args: Type.Optional(
|
|
28
|
+
Type.String({
|
|
29
|
+
description: "Arguments to pass to the skill (will substitute {{skill.args}} and <arg> placeholders)",
|
|
30
|
+
})
|
|
31
|
+
),
|
|
32
|
+
list: Type.Optional(
|
|
33
|
+
Type.Boolean({
|
|
34
|
+
description: "If true, list all available skills without invoking any",
|
|
35
|
+
})
|
|
36
|
+
),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export type SkillInput = {
|
|
40
|
+
name?: string;
|
|
41
|
+
args?: string;
|
|
42
|
+
list?: boolean;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// ─── Formatting ──────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
function formatSkillList(skills: SkillCommand[]): string {
|
|
48
|
+
if (skills.length === 0) {
|
|
49
|
+
return "No skills available.";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const lines: string[] = ["Available skills:\n"];
|
|
53
|
+
|
|
54
|
+
for (const skill of skills) {
|
|
55
|
+
const usage = skill.argumentHint
|
|
56
|
+
? `/${skill.name} ${skill.argumentHint}`
|
|
57
|
+
: `/${skill.name}`;
|
|
58
|
+
|
|
59
|
+
lines.push(`## ${skill.name}`);
|
|
60
|
+
lines.push(`Usage: ${usage}`);
|
|
61
|
+
lines.push(`Description: ${skill.description}`);
|
|
62
|
+
|
|
63
|
+
if (skill.source !== "bundled") {
|
|
64
|
+
lines.push(`Source: ${skill.source}`);
|
|
65
|
+
} else {
|
|
66
|
+
lines.push(`Source: bundled`);
|
|
67
|
+
}
|
|
68
|
+
lines.push("");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return lines.join("\n");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function formatSkillResult(content: ContentBlockParam[]): string {
|
|
75
|
+
return content.map(c => {
|
|
76
|
+
if (c.type === "text") {
|
|
77
|
+
return c.text;
|
|
78
|
+
}
|
|
79
|
+
// For other content types, stringify them
|
|
80
|
+
return JSON.stringify(c);
|
|
81
|
+
}).join("\n");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─── Execute Function ─────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
export async function executeSkill(
|
|
87
|
+
_toolCallId: string,
|
|
88
|
+
params: SkillInput
|
|
89
|
+
) {
|
|
90
|
+
// List all skills
|
|
91
|
+
if (params.list === true || (!params.name && !params.args)) {
|
|
92
|
+
const allSkills = getAllSkills();
|
|
93
|
+
return {
|
|
94
|
+
content: [{ type: "text" as const, text: formatSkillList(allSkills) }],
|
|
95
|
+
details: { skills: allSkills },
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Require a skill name if not listing
|
|
100
|
+
if (!params.name) {
|
|
101
|
+
return {
|
|
102
|
+
content: [{
|
|
103
|
+
type: "text" as const,
|
|
104
|
+
text: "Error: Skill name is required when not listing skills. Use Skill(list: true) to see available skills.",
|
|
105
|
+
}],
|
|
106
|
+
details: {},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Find the skill
|
|
111
|
+
const skill = getSkillByName(params.name);
|
|
112
|
+
if (!skill) {
|
|
113
|
+
const allSkills = getAllSkills();
|
|
114
|
+
const suggestions = allSkills
|
|
115
|
+
.filter(s => s.name.toLowerCase().includes(params.name!.toLowerCase()))
|
|
116
|
+
.map(s => s.name);
|
|
117
|
+
|
|
118
|
+
let message = `Skill not found: "${params.name}"`;
|
|
119
|
+
if (suggestions.length > 0) {
|
|
120
|
+
message += `\n\nDid you mean: ${suggestions.join(", ")}?`;
|
|
121
|
+
} else {
|
|
122
|
+
message += `\n\nUse Skill(list: true) to see all available skills.`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
content: [{ type: "text" as const, text: message }],
|
|
127
|
+
details: {},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Invoke the skill
|
|
132
|
+
const args = params.args ?? "";
|
|
133
|
+
try {
|
|
134
|
+
const content = await invokeSkill(skill, args, null);
|
|
135
|
+
const text = formatSkillResult(content);
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
content: [{ type: "text" as const, text }],
|
|
139
|
+
details: { skill },
|
|
140
|
+
};
|
|
141
|
+
} catch (error) {
|
|
142
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
143
|
+
return {
|
|
144
|
+
content: [{ type: "text" as const, text: `Error invoking skill "${skill.name}": ${errorMessage}` }],
|
|
145
|
+
details: { skill },
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ─── Tool Definition Factory ──────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
export function createSkillToolDefinition(): ToolDefinition {
|
|
153
|
+
return {
|
|
154
|
+
name: "skill",
|
|
155
|
+
label: "Skill",
|
|
156
|
+
description:
|
|
157
|
+
"Load and invoke a Koi skill.\n\n" +
|
|
158
|
+
"Skills are reusable prompt templates that provide specialized workflows " +
|
|
159
|
+
"for specific tasks (e.g., code review, testing, documentation).\n\n" +
|
|
160
|
+
"Use this tool to:\n" +
|
|
161
|
+
"- List all available skills (Skill(list: true))\n" +
|
|
162
|
+
"- Invoke a skill with arguments (Skill(name: 'review', args: 'src/'))\n\n" +
|
|
163
|
+
"When a skill matches the current task, use it to get specialized instructions.",
|
|
164
|
+
promptSnippet: "Skill: load and invoke a skill",
|
|
165
|
+
promptGuidelines: [
|
|
166
|
+
"Use Skill(list: true) to see all available skills.",
|
|
167
|
+
"When a task matches a skill's description, use Skill(name: '<skill>') to load specialized instructions.",
|
|
168
|
+
"Skills may accept arguments - check the skill's argument_hint for usage.",
|
|
169
|
+
],
|
|
170
|
+
parameters: skillSchema,
|
|
171
|
+
executionMode: "parallel",
|
|
172
|
+
async execute(toolCallId, params, _signal, _onUpdate) {
|
|
173
|
+
return executeSkill(toolCallId, params as SkillInput);
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Management Tools — Session-scoped task tracking with persistence
|
|
3
|
+
*
|
|
4
|
+
* Implements Claude Code's 4-tool task system:
|
|
5
|
+
* TaskCreate → create a new task
|
|
6
|
+
* TaskGet → retrieve a single task by ID
|
|
7
|
+
* TaskList → list all tasks (optionally filtered by status)
|
|
8
|
+
* TaskUpdate → update task fields, status, and dependency relationships
|
|
9
|
+
*
|
|
10
|
+
* Tasks are now isolated per session via SessionTaskManager and persisted
|
|
11
|
+
* to ~/.config/koi/sessions/<sessionId>/tasks.json.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { Type } from "typebox";
|
|
15
|
+
import type { ToolDefinition, AgentToolResult } from "@mariozechner/pi-coding-agent";
|
|
16
|
+
import type { TextContent, ImageContent } from "@mariozechner/pi-ai";
|
|
17
|
+
import type { SessionTaskManager, Task } from "../agent/session-tasks.js";
|
|
18
|
+
import type { ToolResultWithError } from "./types.js";
|
|
19
|
+
|
|
20
|
+
// ─── Schemas ─────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
export const taskCreateSchema = Type.Object({
|
|
23
|
+
content: Type.String({
|
|
24
|
+
description: "Task description / content (max 40 characters)",
|
|
25
|
+
maxLength: 40,
|
|
26
|
+
}),
|
|
27
|
+
priority: Type.Optional(
|
|
28
|
+
Type.Union(
|
|
29
|
+
[Type.Literal("high"), Type.Literal("medium"), Type.Literal("low")],
|
|
30
|
+
{ description: "Task priority (default: medium)" }
|
|
31
|
+
)
|
|
32
|
+
),
|
|
33
|
+
blockedBy: Type.Optional(
|
|
34
|
+
Type.Array(Type.String({ description: "Task ID that blocks this task" }), {
|
|
35
|
+
description: "IDs of tasks that must be completed before this one can start",
|
|
36
|
+
})
|
|
37
|
+
),
|
|
38
|
+
blocks: Type.Optional(
|
|
39
|
+
Type.Array(Type.String({ description: "Task ID that this task blocks" }), {
|
|
40
|
+
description: "IDs of tasks that are blocked until this one is completed",
|
|
41
|
+
})
|
|
42
|
+
),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export type TaskCreateInput = {
|
|
46
|
+
content: string;
|
|
47
|
+
priority?: "high" | "medium" | "low";
|
|
48
|
+
blockedBy?: string[];
|
|
49
|
+
blocks?: string[];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const taskGetSchema = Type.Object({
|
|
53
|
+
taskId: Type.String({ description: "Unique task identifier" }),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export type TaskGetInput = {
|
|
57
|
+
taskId: string;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const taskListSchema = Type.Object({
|
|
61
|
+
status: Type.Optional(
|
|
62
|
+
Type.Union(
|
|
63
|
+
[Type.Literal("pending"), Type.Literal("in_progress"), Type.Literal("completed")],
|
|
64
|
+
{ description: "Filter by status" }
|
|
65
|
+
)
|
|
66
|
+
),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
export type TaskListInput = {
|
|
70
|
+
status?: "pending" | "in_progress" | "completed";
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const taskUpdateSchema = Type.Object({
|
|
74
|
+
taskId: Type.String({ description: "Unique task identifier" }),
|
|
75
|
+
content: Type.Optional(Type.String({
|
|
76
|
+
description: "New task description (max 40 characters)",
|
|
77
|
+
maxLength: 40,
|
|
78
|
+
})),
|
|
79
|
+
status: Type.Optional(
|
|
80
|
+
Type.Union(
|
|
81
|
+
[Type.Literal("pending"), Type.Literal("in_progress"), Type.Literal("completed")],
|
|
82
|
+
{ description: "New task status" }
|
|
83
|
+
)
|
|
84
|
+
),
|
|
85
|
+
priority: Type.Optional(
|
|
86
|
+
Type.Union(
|
|
87
|
+
[Type.Literal("high"), Type.Literal("medium"), Type.Literal("low")],
|
|
88
|
+
{ description: "New task priority" }
|
|
89
|
+
)
|
|
90
|
+
),
|
|
91
|
+
addBlockedBy: Type.Optional(
|
|
92
|
+
Type.Array(Type.String(), { description: "Task IDs to add to blockedBy" })
|
|
93
|
+
),
|
|
94
|
+
removeBlockedBy: Type.Optional(
|
|
95
|
+
Type.Array(Type.String(), { description: "Task IDs to remove from blockedBy" })
|
|
96
|
+
),
|
|
97
|
+
addBlocks: Type.Optional(
|
|
98
|
+
Type.Array(Type.String(), { description: "Task IDs to add to blocks" })
|
|
99
|
+
),
|
|
100
|
+
removeBlocks: Type.Optional(
|
|
101
|
+
Type.Array(Type.String(), { description: "Task IDs to remove from blocks" })
|
|
102
|
+
),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
export type TaskUpdateInput = {
|
|
106
|
+
taskId: string;
|
|
107
|
+
content?: string;
|
|
108
|
+
status?: "pending" | "in_progress" | "completed";
|
|
109
|
+
priority?: "high" | "medium" | "low";
|
|
110
|
+
addBlockedBy?: string[];
|
|
111
|
+
removeBlockedBy?: string[];
|
|
112
|
+
addBlocks?: string[];
|
|
113
|
+
removeBlocks?: string[];
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// ─── Formatting ──────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
function formatTaskList(taskArray: Task[]): string {
|
|
119
|
+
if (taskArray.length === 0) return "No tasks found.";
|
|
120
|
+
|
|
121
|
+
const lines: string[] = [];
|
|
122
|
+
for (const t of taskArray) {
|
|
123
|
+
const depInfo: string[] = [];
|
|
124
|
+
if (t.blockedBy.length > 0) depInfo.push(`blockedBy:[${t.blockedBy.join(", ")}]`);
|
|
125
|
+
if (t.blocks.length > 0) depInfo.push(`blocks:[${t.blocks.join(", ")}]`);
|
|
126
|
+
const depStr = depInfo.length > 0 ? ` {${depInfo.join(", ")}}` : "";
|
|
127
|
+
lines.push(`- [${t.status}] ${t.id} (${t.priority}): ${t.content}${depStr}`);
|
|
128
|
+
}
|
|
129
|
+
return lines.join("\n");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ─── Execute Functions (injected with SessionTaskManager) ────────────────────
|
|
133
|
+
|
|
134
|
+
export async function executeTaskCreate(
|
|
135
|
+
taskManager: SessionTaskManager,
|
|
136
|
+
_toolCallId: string,
|
|
137
|
+
params: TaskCreateInput
|
|
138
|
+
): Promise<AgentToolResult<{ task: Task }>> {
|
|
139
|
+
// Block overly long task content to prevent AI misuse of todo list
|
|
140
|
+
if (params.content.length > 40) {
|
|
141
|
+
return {
|
|
142
|
+
content: [{
|
|
143
|
+
type: "text",
|
|
144
|
+
text: `Error: Task content exceeds 40 characters (${params.content.length} chars). Please shorten the description to 40 characters or less. Example: "Fix login bug" instead of "Fix the login bug where users cannot authenticate with SSO"`,
|
|
145
|
+
}],
|
|
146
|
+
details: { task: null! },
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const task = taskManager.createTask(
|
|
151
|
+
params.content,
|
|
152
|
+
params.priority ?? "medium",
|
|
153
|
+
params.blockedBy,
|
|
154
|
+
params.blocks
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
content: [{ type: "text", text: `Created task ${task.id}: ${task.content}` }],
|
|
159
|
+
details: { task },
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function executeTaskGet(
|
|
164
|
+
taskManager: SessionTaskManager,
|
|
165
|
+
_toolCallId: string,
|
|
166
|
+
params: TaskGetInput
|
|
167
|
+
): Promise<{ content: (TextContent | ImageContent)[]; details: { task: Task | null } }> {
|
|
168
|
+
const task = taskManager.getTask(params.taskId);
|
|
169
|
+
|
|
170
|
+
if (!task) {
|
|
171
|
+
const result: ToolResultWithError<{ task: Task | null }> = {
|
|
172
|
+
content: [{ type: "text", text: `Task not found: ${params.taskId}` }],
|
|
173
|
+
details: { task: null },
|
|
174
|
+
isError: true,
|
|
175
|
+
};
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const lines = [
|
|
180
|
+
`id: ${task.id}`,
|
|
181
|
+
`content: ${task.content}`,
|
|
182
|
+
`status: ${task.status}`,
|
|
183
|
+
`priority: ${task.priority}`,
|
|
184
|
+
`blockedBy: [${task.blockedBy.join(", ") || "none"}]`,
|
|
185
|
+
`blocks: [${task.blocks.join(", ") || "none"}]`,
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
190
|
+
details: { task },
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function executeTaskList(
|
|
195
|
+
taskManager: SessionTaskManager,
|
|
196
|
+
_toolCallId: string,
|
|
197
|
+
params: TaskListInput
|
|
198
|
+
): Promise<{ content: TextContent[]; details: { tasks: Task[]; count: number } }> {
|
|
199
|
+
const all = taskManager.listTasks(params.status);
|
|
200
|
+
const text = formatTaskList(all);
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
content: [{ type: "text", text }],
|
|
204
|
+
details: { tasks: all, count: all.length },
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export async function executeTaskUpdate(
|
|
209
|
+
taskManager: SessionTaskManager,
|
|
210
|
+
_toolCallId: string,
|
|
211
|
+
params: TaskUpdateInput
|
|
212
|
+
): Promise<AgentToolResult<{ task: Task | null }>> {
|
|
213
|
+
// Block overly long task content to prevent AI misuse of todo list
|
|
214
|
+
if (params.content !== undefined && params.content.length > 40) {
|
|
215
|
+
return {
|
|
216
|
+
content: [{
|
|
217
|
+
type: "text",
|
|
218
|
+
text: `Error: Task content exceeds 40 characters (${params.content.length} chars). Please shorten the description to 40 characters or less.`,
|
|
219
|
+
}],
|
|
220
|
+
details: { task: null },
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const task = taskManager.updateTask(params.taskId, {
|
|
225
|
+
content: params.content,
|
|
226
|
+
status: params.status,
|
|
227
|
+
priority: params.priority,
|
|
228
|
+
addBlockedBy: params.addBlockedBy,
|
|
229
|
+
removeBlockedBy: params.removeBlockedBy,
|
|
230
|
+
addBlocks: params.addBlocks,
|
|
231
|
+
removeBlocks: params.removeBlocks,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (!task) {
|
|
235
|
+
return {
|
|
236
|
+
content: [{ type: "text", text: `Task not found: ${params.taskId}` }],
|
|
237
|
+
details: { task: null },
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
content: [{ type: "text", text: `Updated task ${task.id}: ${task.content} [${task.status}]` }],
|
|
243
|
+
details: { task },
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ─── Tool Definition Factories ───────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
export function createTaskCreateToolDefinition(
|
|
250
|
+
_cwd: string,
|
|
251
|
+
taskManager: SessionTaskManager
|
|
252
|
+
): ToolDefinition<typeof taskCreateSchema, { task: Task }> {
|
|
253
|
+
return {
|
|
254
|
+
name: "taskCreate",
|
|
255
|
+
label: "TaskCreate",
|
|
256
|
+
description:
|
|
257
|
+
"Create a new task in the session todo list.\n\n" +
|
|
258
|
+
"Use VERY frequently to break down complex tasks into atomic steps. " +
|
|
259
|
+
"ALWAYS create a todo list before starting multi-step work. " +
|
|
260
|
+
"If you do not use this tool when planning, you may forget important tasks.",
|
|
261
|
+
promptSnippet: "TaskCreate: create a new task in the session todo list",
|
|
262
|
+
promptGuidelines: [
|
|
263
|
+
"Use TaskCreate VERY frequently to track and plan tasks.",
|
|
264
|
+
"ALWAYS create a todo list before starting multi-step or non-trivial work.",
|
|
265
|
+
"Break large complex tasks into smaller atomic steps.",
|
|
266
|
+
"⚠️ Task content MUST be 40 characters or less. Use concise, brief descriptions.",
|
|
267
|
+
"Set priority to 'high' for critical path items, 'low' for nice-to-haves.",
|
|
268
|
+
"Use blockedBy/blocks to express dependency relationships between tasks.",
|
|
269
|
+
],
|
|
270
|
+
parameters: taskCreateSchema,
|
|
271
|
+
executionMode: "parallel",
|
|
272
|
+
async execute(toolCallId, params, _signal, _onUpdate) {
|
|
273
|
+
return executeTaskCreate(taskManager, toolCallId, params);
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function createTaskGetToolDefinition(
|
|
279
|
+
_cwd: string,
|
|
280
|
+
taskManager: SessionTaskManager
|
|
281
|
+
): ToolDefinition<typeof taskGetSchema, { task: Task | null }> {
|
|
282
|
+
return {
|
|
283
|
+
name: "taskGet",
|
|
284
|
+
label: "TaskGet",
|
|
285
|
+
description: "Retrieve the full details of a single task by its ID.",
|
|
286
|
+
promptSnippet: "TaskGet: retrieve details of a specific task by ID",
|
|
287
|
+
promptGuidelines: [
|
|
288
|
+
"Use TaskGet when you need to inspect a task's dependencies or full state.",
|
|
289
|
+
],
|
|
290
|
+
parameters: taskGetSchema,
|
|
291
|
+
executionMode: "parallel",
|
|
292
|
+
async execute(toolCallId, params, _signal, _onUpdate) {
|
|
293
|
+
return executeTaskGet(taskManager, toolCallId, params);
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function createTaskListToolDefinition(
|
|
299
|
+
_cwd: string,
|
|
300
|
+
taskManager: SessionTaskManager
|
|
301
|
+
): ToolDefinition<typeof taskListSchema, { tasks: Task[]; count: number }> {
|
|
302
|
+
return {
|
|
303
|
+
name: "taskList",
|
|
304
|
+
label: "TaskList",
|
|
305
|
+
description:
|
|
306
|
+
"List all tasks in the session todo list, optionally filtered by status.\n\n" +
|
|
307
|
+
"Check your progress regularly — at the start of each turn, after completing a step, " +
|
|
308
|
+
"and whenever you're unsure what to do next.",
|
|
309
|
+
promptSnippet: "TaskList: list all tasks (optionally filter by status)",
|
|
310
|
+
promptGuidelines: [
|
|
311
|
+
"Call TaskList at the start of each conversation turn to review progress.",
|
|
312
|
+
"Call TaskList after completing any task to verify next steps.",
|
|
313
|
+
"Call TaskList whenever you are unsure what to do next.",
|
|
314
|
+
],
|
|
315
|
+
parameters: taskListSchema,
|
|
316
|
+
executionMode: "parallel",
|
|
317
|
+
async execute(toolCallId, params, _signal, _onUpdate) {
|
|
318
|
+
return executeTaskList(taskManager, toolCallId, params);
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function createTaskUpdateToolDefinition(
|
|
324
|
+
_cwd: string,
|
|
325
|
+
taskManager: SessionTaskManager
|
|
326
|
+
): ToolDefinition<typeof taskUpdateSchema, { task: Task | null }> {
|
|
327
|
+
return {
|
|
328
|
+
name: "taskUpdate",
|
|
329
|
+
label: "TaskUpdate",
|
|
330
|
+
description:
|
|
331
|
+
"Update an existing task's content, status, priority, or dependency relationships.\n\n" +
|
|
332
|
+
"Mark tasks as 'in_progress' BEFORE starting work on them. " +
|
|
333
|
+
"ONLY mark as 'completed' when FULLY done — tests passing, no errors, no partial implementations. " +
|
|
334
|
+
"If you encountered unresolved errors, do NOT mark the task as completed.",
|
|
335
|
+
promptSnippet: "TaskUpdate: update task status, content, priority, or dependencies",
|
|
336
|
+
promptGuidelines: [
|
|
337
|
+
"Mark a task as 'in_progress' before you begin working on it.",
|
|
338
|
+
"ONLY mark a task as 'completed' when you have FULLY accomplished it.",
|
|
339
|
+
"Never mark completed if: tests are failing, implementation is partial, unresolved errors exist.",
|
|
340
|
+
"Ideally you should only have one task as 'in_progress' at a time (single-threaded focus).",
|
|
341
|
+
"Update blockedBy / blocks to reflect changing dependency relationships.",
|
|
342
|
+
],
|
|
343
|
+
parameters: taskUpdateSchema,
|
|
344
|
+
executionMode: "parallel",
|
|
345
|
+
async execute(toolCallId, params, _signal, _onUpdate) {
|
|
346
|
+
return executeTaskUpdate(taskManager, toolCallId, params);
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
}
|