botholomew 0.15.3 → 0.15.5
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/package.json +1 -1
- package/src/chat/agent.ts +18 -5
- package/src/commands/chat.ts +2 -1
- package/src/context/store.ts +4 -22
- package/src/fs/patches.ts +39 -0
- package/src/schedules/store.ts +11 -5
- package/src/tasks/store.ts +6 -6
- package/src/tools/file/edit.ts +2 -11
- package/src/tools/prompt/edit.ts +148 -0
- package/src/tools/prompt/read.ts +70 -0
- package/src/tools/registry.ts +9 -4
- package/src/tools/schedule/create.ts +3 -1
- package/src/tools/schedule/edit.ts +126 -0
- package/src/tools/skill/edit.ts +3 -33
- package/src/tools/task/create.ts +3 -1
- package/src/tools/task/delete.ts +3 -1
- package/src/tools/task/edit.ts +214 -0
- package/src/tools/task/update.ts +3 -1
- package/src/tools/tool.ts +7 -0
- package/src/tui/App.tsx +67 -7
- package/src/tui/components/ContextPanel.tsx +3 -2
- package/src/tui/components/HelpPanel.tsx +9 -7
- package/src/tui/components/SchedulePanel.tsx +2 -2
- package/src/tui/components/TaskPanel.tsx +2 -2
- package/src/tui/components/ThreadPanel.tsx +2 -2
- package/src/tui/components/ToolCall.tsx +14 -0
- package/src/tui/components/WorkerPanel.tsx +15 -4
- package/src/tools/context/update-beliefs.ts +0 -64
- package/src/tools/context/update-goals.ts +0 -64
package/package.json
CHANGED
package/src/chat/agent.ts
CHANGED
|
@@ -35,11 +35,13 @@ import {
|
|
|
35
35
|
|
|
36
36
|
registerAllTools();
|
|
37
37
|
|
|
38
|
-
/** Tools available in chat mode — no worker terminal tools (complete/fail/wait), no bulk-destructive file tools (
|
|
38
|
+
/** Tools available in chat mode — no worker terminal tools (complete/fail/wait), no bulk-destructive file tools (copy/move, dir ops) */
|
|
39
39
|
const CHAT_TOOL_NAMES = new Set([
|
|
40
40
|
"create_task",
|
|
41
41
|
"list_tasks",
|
|
42
42
|
"view_task",
|
|
43
|
+
"update_task",
|
|
44
|
+
"delete_task",
|
|
43
45
|
"context_info",
|
|
44
46
|
"context_tree",
|
|
45
47
|
"context_read",
|
|
@@ -50,9 +52,11 @@ const CHAT_TOOL_NAMES = new Set([
|
|
|
50
52
|
"view_thread",
|
|
51
53
|
"search_threads",
|
|
52
54
|
"create_schedule",
|
|
55
|
+
"schedule_edit",
|
|
53
56
|
"list_schedules",
|
|
54
|
-
"
|
|
55
|
-
"
|
|
57
|
+
"prompt_read",
|
|
58
|
+
"prompt_edit",
|
|
59
|
+
"task_edit",
|
|
56
60
|
"capabilities_refresh",
|
|
57
61
|
"mcp_list_tools",
|
|
58
62
|
"mcp_search",
|
|
@@ -98,7 +102,7 @@ You do NOT execute long-running work directly — enqueue tasks for a background
|
|
|
98
102
|
Use the available tools to look up tasks, threads, schedules, and context when the user asks about them. Files the agent can read and write live under \`context/\` as project-relative paths (e.g. \`notes/foo.md\`). Use \`context_tree\` to see what's there, \`search\` (hybrid regexp + semantic) to find content, then \`context_read\` / \`context_info\` to drill in.
|
|
99
103
|
Past conversations live in CSV files under \`threads/\`; use \`list_threads\`, \`search_threads\`, and \`view_thread\` to find and page through them.
|
|
100
104
|
When multiple tool calls are independent of each other (i.e., one does not depend on the result of another), call them all in a single response. They will be executed in parallel, which is faster than calling them one at a time.
|
|
101
|
-
You can
|
|
105
|
+
You can read and edit the agent's prompt files (beliefs, goals, capabilities, soul) under \`prompts/\` via \`prompt_read\` and \`prompt_edit\` (line-range patches). Files marked \`agent-modification: false\` (e.g. soul.md) are read-only.
|
|
102
106
|
You can author and refine slash-command skills (reusable prompt templates stored in \`skills/\`) via \`skill_list\`, \`skill_search\`, \`skill_read\`, \`skill_write\`, \`skill_edit\`, and \`skill_delete\`. New or edited skills are usable as \`/<name>\` on the user's next message.
|
|
103
107
|
Format your responses using Markdown. Use headings, bold, italic, lists, and code blocks to make your responses clear and well-structured.
|
|
104
108
|
`;
|
|
@@ -156,6 +160,11 @@ export interface ChatTurnCallbacks {
|
|
|
156
160
|
isError: boolean,
|
|
157
161
|
meta?: ToolEndMeta,
|
|
158
162
|
) => void;
|
|
163
|
+
/** Side-effect notification from inside a tool ("Created subtask: …"). The
|
|
164
|
+
* TUI renders these inside the tool-call card so they stay anchored to the
|
|
165
|
+
* tool that produced them. Workers don't supply this; tools fall back to
|
|
166
|
+
* `logger.info`. */
|
|
167
|
+
onToolNotify?: (toolUseId: string, message: string) => void;
|
|
159
168
|
/** Called between LLM turns. The TUI returns any queued user messages so
|
|
160
169
|
* the agent can inject them into the running turn instead of waiting for
|
|
161
170
|
* the entire tool loop to finish. Each returned message is logged + pushed
|
|
@@ -245,7 +254,7 @@ export async function runChatTurn(input: {
|
|
|
245
254
|
// Rebuild the system prompt every iteration so that:
|
|
246
255
|
// (1) `loading: contextual` files get matched against the latest user
|
|
247
256
|
// message, and
|
|
248
|
-
// (2) any
|
|
257
|
+
// (2) any prompt_edit tool call in the previous
|
|
249
258
|
// iteration is reflected in the next LLM call.
|
|
250
259
|
const keywordSource = findLastUserText(messages);
|
|
251
260
|
const systemPrompt = await buildChatSystemPrompt(projectDir, {
|
|
@@ -406,6 +415,9 @@ export async function runChatTurn(input: {
|
|
|
406
415
|
config,
|
|
407
416
|
mcpxClient,
|
|
408
417
|
shouldAbort: session ? () => session.aborted : undefined,
|
|
418
|
+
notify: callbacks.onToolNotify
|
|
419
|
+
? (msg) => callbacks.onToolNotify?.(toolUse.id, msg)
|
|
420
|
+
: undefined,
|
|
409
421
|
});
|
|
410
422
|
const durationMs = Date.now() - start;
|
|
411
423
|
const stored = maybeStoreResult(toolUse.name, result.output);
|
|
@@ -454,6 +466,7 @@ interface ChatToolCallCtx {
|
|
|
454
466
|
config: Required<BotholomewConfig>;
|
|
455
467
|
mcpxClient: McpxClient | null;
|
|
456
468
|
shouldAbort?: () => boolean;
|
|
469
|
+
notify?: (message: string) => void;
|
|
457
470
|
}
|
|
458
471
|
|
|
459
472
|
async function executeChatToolCall(
|
package/src/commands/chat.ts
CHANGED
|
@@ -8,8 +8,9 @@ export function registerChatCommand(program: Command) {
|
|
|
8
8
|
"Open the interactive chat TUI\n\n" +
|
|
9
9
|
" Tab navigation (Ctrl+<letter> from any tab):\n" +
|
|
10
10
|
" Ctrl+a Chat Ctrl+t Tasks Ctrl+w Workers\n" +
|
|
11
|
-
" Ctrl+o Tools Ctrl+
|
|
11
|
+
" Ctrl+o Tools Ctrl+e Threads Ctrl+g Help\n" +
|
|
12
12
|
" Ctrl+n Context Ctrl+s Schedules Esc Return to Chat\n\n" +
|
|
13
|
+
" Refresh: Ctrl+R refreshes Context · Tasks · Threads · Schedules · Workers\n\n" +
|
|
13
14
|
" Chat input:\n" +
|
|
14
15
|
" Enter Send message\n" +
|
|
15
16
|
" ⌥+Enter Insert newline\n" +
|
package/src/context/store.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { dirname, join, posix, relative, sep } from "node:path";
|
|
14
14
|
import { CONTEXT_DIR, PROTECTED_AREAS } from "../constants.ts";
|
|
15
15
|
import { atomicWrite } from "../fs/atomic.ts";
|
|
16
|
+
import { applyLinePatches, type LinePatch } from "../fs/patches.ts";
|
|
16
17
|
import {
|
|
17
18
|
getCanonicalRoot,
|
|
18
19
|
PathEscapeError,
|
|
@@ -59,11 +60,7 @@ export class PathConflictError extends Error {
|
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
export
|
|
63
|
-
start_line: number;
|
|
64
|
-
end_line: number;
|
|
65
|
-
content: string;
|
|
66
|
-
}
|
|
63
|
+
export type Patch = LinePatch;
|
|
67
64
|
|
|
68
65
|
export interface ContextEntry {
|
|
69
66
|
/** Project-relative path under context/, e.g. "notes/foo.md". Forward-slashes. */
|
|
@@ -775,26 +772,11 @@ export async function applyPatches(
|
|
|
775
772
|
patches: Patch[],
|
|
776
773
|
): Promise<{ applied: number; lines: number }> {
|
|
777
774
|
const content = await readContextFile(projectDir, path);
|
|
778
|
-
const
|
|
779
|
-
|
|
780
|
-
const sorted = [...patches].sort((a, b) => b.start_line - a.start_line);
|
|
781
|
-
|
|
782
|
-
for (const patch of sorted) {
|
|
783
|
-
if (patch.end_line === 0) {
|
|
784
|
-
const insertLines = patch.content === "" ? [] : patch.content.split("\n");
|
|
785
|
-
lines.splice(patch.start_line - 1, 0, ...insertLines);
|
|
786
|
-
} else {
|
|
787
|
-
const deleteCount = patch.end_line - patch.start_line + 1;
|
|
788
|
-
const insertLines = patch.content === "" ? [] : patch.content.split("\n");
|
|
789
|
-
lines.splice(patch.start_line - 1, deleteCount, ...insertLines);
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
const newContent = lines.join("\n");
|
|
775
|
+
const newContent = applyLinePatches(content, patches);
|
|
794
776
|
await writeContextFile(projectDir, path, newContent, {
|
|
795
777
|
onConflict: "overwrite",
|
|
796
778
|
});
|
|
797
|
-
return { applied: patches.length, lines:
|
|
779
|
+
return { applied: patches.length, lines: newContent.split("\n").length };
|
|
798
780
|
}
|
|
799
781
|
|
|
800
782
|
/**
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export interface LinePatch {
|
|
4
|
+
start_line: number;
|
|
5
|
+
end_line: number;
|
|
6
|
+
content: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const LinePatchSchema = z.object({
|
|
10
|
+
start_line: z.number().describe("1-based inclusive start line"),
|
|
11
|
+
end_line: z
|
|
12
|
+
.number()
|
|
13
|
+
.describe("1-based inclusive end line (0 to insert without replacing)"),
|
|
14
|
+
content: z
|
|
15
|
+
.string()
|
|
16
|
+
.describe("Replacement text (empty string to delete lines)"),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Apply git-style line-range patches to a string. Patches are applied
|
|
21
|
+
* bottom-up so earlier line numbers stay stable. `end_line === 0` is an
|
|
22
|
+
* insert that doesn't replace; an empty `content` deletes.
|
|
23
|
+
*/
|
|
24
|
+
export function applyLinePatches(raw: string, patches: LinePatch[]): string {
|
|
25
|
+
const lines = raw.split("\n");
|
|
26
|
+
const sorted = [...patches].sort((a, b) => b.start_line - a.start_line);
|
|
27
|
+
|
|
28
|
+
for (const patch of sorted) {
|
|
29
|
+
const insertLines = patch.content === "" ? [] : patch.content.split("\n");
|
|
30
|
+
if (patch.end_line === 0) {
|
|
31
|
+
lines.splice(patch.start_line - 1, 0, ...insertLines);
|
|
32
|
+
} else {
|
|
33
|
+
const deleteCount = patch.end_line - patch.start_line + 1;
|
|
34
|
+
lines.splice(patch.start_line - 1, deleteCount, ...insertLines);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return lines.join("\n");
|
|
39
|
+
}
|
package/src/schedules/store.ts
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
ScheduleFrontmatterSchema,
|
|
20
20
|
} from "./schema.ts";
|
|
21
21
|
|
|
22
|
-
function scheduleFilePath(projectDir: string, id: string): string {
|
|
22
|
+
export function scheduleFilePath(projectDir: string, id: string): string {
|
|
23
23
|
return join(getSchedulesDir(projectDir), `${id}.md`);
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -27,20 +27,26 @@ function scheduleLockPath(projectDir: string, id: string): string {
|
|
|
27
27
|
return join(getSchedulesLockDir(projectDir), `${id}.lock`);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
function serializeSchedule(
|
|
30
|
+
export function serializeSchedule(
|
|
31
|
+
fm: ScheduleFrontmatter,
|
|
32
|
+
body: string,
|
|
33
|
+
): string {
|
|
31
34
|
return matter.stringify(`\n${body.trim()}\n`, fm as Record<string, unknown>);
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
interface
|
|
37
|
+
export interface ScheduleParseOk {
|
|
35
38
|
ok: true;
|
|
36
39
|
schedule: Schedule;
|
|
37
40
|
}
|
|
38
|
-
interface
|
|
41
|
+
export interface ScheduleParseFail {
|
|
39
42
|
ok: false;
|
|
40
43
|
reason: string;
|
|
41
44
|
}
|
|
42
45
|
|
|
43
|
-
function parseScheduleFile(
|
|
46
|
+
export function parseScheduleFile(
|
|
47
|
+
raw: string,
|
|
48
|
+
mtimeMs: number,
|
|
49
|
+
): ScheduleParseOk | ScheduleParseFail {
|
|
44
50
|
let parsed: matter.GrayMatterFile<string>;
|
|
45
51
|
try {
|
|
46
52
|
parsed = matter(raw);
|
package/src/tasks/store.ts
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
type TaskStatus,
|
|
22
22
|
} from "./schema.ts";
|
|
23
23
|
|
|
24
|
-
function taskFilePath(projectDir: string, id: string): string {
|
|
24
|
+
export function taskFilePath(projectDir: string, id: string): string {
|
|
25
25
|
return join(getTasksDir(projectDir), `${id}.md`);
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -33,23 +33,23 @@ function taskLockPath(projectDir: string, id: string): string {
|
|
|
33
33
|
* Render a Task to its on-disk markdown form. Frontmatter contains every
|
|
34
34
|
* field; the body is preserved as-is. Trailing newline keeps line count sane.
|
|
35
35
|
*/
|
|
36
|
-
function serializeTask(fm: TaskFrontmatter, body: string): string {
|
|
36
|
+
export function serializeTask(fm: TaskFrontmatter, body: string): string {
|
|
37
37
|
return matter.stringify(`\n${body.trim()}\n`, fm as Record<string, unknown>);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
interface
|
|
40
|
+
export interface TaskParseOk {
|
|
41
41
|
ok: true;
|
|
42
42
|
task: Task;
|
|
43
43
|
}
|
|
44
|
-
interface
|
|
44
|
+
export interface TaskParseFail {
|
|
45
45
|
ok: false;
|
|
46
46
|
reason: string;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
function parseTaskFile(
|
|
49
|
+
export function parseTaskFile(
|
|
50
50
|
raw: string,
|
|
51
51
|
mtimeMs: number,
|
|
52
|
-
):
|
|
52
|
+
): TaskParseOk | TaskParseFail {
|
|
53
53
|
let parsed: matter.GrayMatterFile<string>;
|
|
54
54
|
try {
|
|
55
55
|
parsed = matter(raw);
|
package/src/tools/file/edit.ts
CHANGED
|
@@ -5,21 +5,12 @@ import {
|
|
|
5
5
|
NotFoundError,
|
|
6
6
|
readContextFile,
|
|
7
7
|
} from "../../context/store.ts";
|
|
8
|
+
import { LinePatchSchema } from "../../fs/patches.ts";
|
|
8
9
|
import type { ToolDefinition } from "../tool.ts";
|
|
9
10
|
|
|
10
|
-
const PatchSchema = z.object({
|
|
11
|
-
start_line: z.number().describe("1-based inclusive start line"),
|
|
12
|
-
end_line: z
|
|
13
|
-
.number()
|
|
14
|
-
.describe("1-based inclusive end line (0 to insert without replacing)"),
|
|
15
|
-
content: z
|
|
16
|
-
.string()
|
|
17
|
-
.describe("Replacement text (empty string to delete lines)"),
|
|
18
|
-
});
|
|
19
|
-
|
|
20
11
|
const inputSchema = z.object({
|
|
21
12
|
path: z.string().describe("Project-relative path under context/"),
|
|
22
|
-
patches: z.array(
|
|
13
|
+
patches: z.array(LinePatchSchema).describe("Patches to apply"),
|
|
23
14
|
});
|
|
24
15
|
|
|
25
16
|
const outputSchema = z.object({
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { getPromptsDir } from "../../constants.ts";
|
|
4
|
+
import {
|
|
5
|
+
atomicWriteIfUnchanged,
|
|
6
|
+
MtimeConflictError,
|
|
7
|
+
readWithMtime,
|
|
8
|
+
} from "../../fs/atomic.ts";
|
|
9
|
+
import { applyLinePatches, LinePatchSchema } from "../../fs/patches.ts";
|
|
10
|
+
import {
|
|
11
|
+
parseContextFile,
|
|
12
|
+
serializeContextFile,
|
|
13
|
+
} from "../../utils/frontmatter.ts";
|
|
14
|
+
import type { ToolDefinition } from "../tool.ts";
|
|
15
|
+
|
|
16
|
+
const inputSchema = z.object({
|
|
17
|
+
name: z
|
|
18
|
+
.string()
|
|
19
|
+
.describe(
|
|
20
|
+
"Prompt name without extension (e.g. 'beliefs', 'goals', 'capabilities'). Resolves to prompts/<name>.md.",
|
|
21
|
+
),
|
|
22
|
+
patches: z.array(LinePatchSchema).describe("Patches to apply"),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const outputSchema = z.object({
|
|
26
|
+
name: z.string(),
|
|
27
|
+
path: z.string().nullable(),
|
|
28
|
+
applied: z.number(),
|
|
29
|
+
content: z.string(),
|
|
30
|
+
is_error: z.boolean(),
|
|
31
|
+
error_type: z.string().optional(),
|
|
32
|
+
message: z.string().optional(),
|
|
33
|
+
next_action_hint: z.string().optional(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const promptEditTool = {
|
|
37
|
+
name: "prompt_edit",
|
|
38
|
+
description:
|
|
39
|
+
"[[ bash equivalent command: patch ]] Apply git-style line-range patches to a prompt file under prompts/. Operates on the whole file (frontmatter + body). Files marked `agent-modification: false` (e.g. soul.md) are protected. Use prompt_read first to inspect current line numbers.",
|
|
40
|
+
group: "context",
|
|
41
|
+
inputSchema,
|
|
42
|
+
outputSchema,
|
|
43
|
+
execute: async (input, ctx) => {
|
|
44
|
+
if (input.name.includes("/") || input.name.includes("..")) {
|
|
45
|
+
return {
|
|
46
|
+
name: input.name,
|
|
47
|
+
path: null,
|
|
48
|
+
applied: 0,
|
|
49
|
+
content: "",
|
|
50
|
+
is_error: true,
|
|
51
|
+
error_type: "invalid_name",
|
|
52
|
+
message: `Invalid prompt name: ${input.name}`,
|
|
53
|
+
next_action_hint:
|
|
54
|
+
"Use a basename without slashes or dots (e.g. 'beliefs').",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const filePath = join(getPromptsDir(ctx.projectDir), `${input.name}.md`);
|
|
58
|
+
const file = await readWithMtime(filePath);
|
|
59
|
+
if (!file) {
|
|
60
|
+
return {
|
|
61
|
+
name: input.name,
|
|
62
|
+
path: null,
|
|
63
|
+
applied: 0,
|
|
64
|
+
content: "",
|
|
65
|
+
is_error: true,
|
|
66
|
+
error_type: "not_found",
|
|
67
|
+
message: `Prompt not found: prompts/${input.name}.md`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const original = file.content;
|
|
72
|
+
const preParsed = parseContextFile(original);
|
|
73
|
+
if (!preParsed.meta["agent-modification"]) {
|
|
74
|
+
return {
|
|
75
|
+
name: input.name,
|
|
76
|
+
path: filePath,
|
|
77
|
+
applied: 0,
|
|
78
|
+
content: original,
|
|
79
|
+
is_error: true,
|
|
80
|
+
error_type: "agent_modification_disabled",
|
|
81
|
+
message: `Agent modification not allowed for prompts/${input.name}.md`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const updated = applyLinePatches(original, input.patches);
|
|
86
|
+
let postParsed: { meta: Record<string, unknown>; content: string };
|
|
87
|
+
try {
|
|
88
|
+
postParsed = parseContextFile(updated);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
return {
|
|
91
|
+
name: input.name,
|
|
92
|
+
path: filePath,
|
|
93
|
+
applied: 0,
|
|
94
|
+
content: original,
|
|
95
|
+
is_error: true,
|
|
96
|
+
error_type: "invalid_frontmatter",
|
|
97
|
+
message: `Patched content failed to parse: ${err instanceof Error ? err.message : String(err)}`,
|
|
98
|
+
next_action_hint:
|
|
99
|
+
"Check that the frontmatter delimiters and YAML stay valid.",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (!postParsed.meta["agent-modification"]) {
|
|
103
|
+
return {
|
|
104
|
+
name: input.name,
|
|
105
|
+
path: filePath,
|
|
106
|
+
applied: 0,
|
|
107
|
+
content: original,
|
|
108
|
+
is_error: true,
|
|
109
|
+
error_type: "agent_modification_disabled",
|
|
110
|
+
message: `Patch would clear agent-modification on prompts/${input.name}.md`,
|
|
111
|
+
next_action_hint:
|
|
112
|
+
"Don't change the agent-modification frontmatter flag.",
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const serialized = serializeContextFile(
|
|
117
|
+
postParsed.meta,
|
|
118
|
+
postParsed.content,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
await atomicWriteIfUnchanged(filePath, serialized, file.mtimeMs);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
if (err instanceof MtimeConflictError) {
|
|
125
|
+
return {
|
|
126
|
+
name: input.name,
|
|
127
|
+
path: filePath,
|
|
128
|
+
applied: 0,
|
|
129
|
+
content: original,
|
|
130
|
+
is_error: true,
|
|
131
|
+
error_type: "mtime_conflict",
|
|
132
|
+
message: `Prompt was modified concurrently: ${err.message}`,
|
|
133
|
+
next_action_hint:
|
|
134
|
+
"Re-read with prompt_read and recompute your patch line numbers before retrying.",
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
name: input.name,
|
|
142
|
+
path: filePath,
|
|
143
|
+
applied: input.patches.length,
|
|
144
|
+
content: serialized,
|
|
145
|
+
is_error: false,
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { getPromptsDir } from "../../constants.ts";
|
|
4
|
+
import type { ToolDefinition } from "../tool.ts";
|
|
5
|
+
|
|
6
|
+
const inputSchema = z.object({
|
|
7
|
+
name: z
|
|
8
|
+
.string()
|
|
9
|
+
.describe(
|
|
10
|
+
"Prompt name without extension (e.g. 'beliefs', 'goals', 'capabilities'). Resolves to prompts/<name>.md.",
|
|
11
|
+
),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const outputSchema = z.object({
|
|
15
|
+
name: z.string(),
|
|
16
|
+
path: z.string().nullable(),
|
|
17
|
+
content: z.string(),
|
|
18
|
+
agent_modification: z.boolean(),
|
|
19
|
+
is_error: z.boolean(),
|
|
20
|
+
error_type: z.string().optional(),
|
|
21
|
+
message: z.string().optional(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const promptReadTool = {
|
|
25
|
+
name: "prompt_read",
|
|
26
|
+
description:
|
|
27
|
+
"[[ bash equivalent command: cat ]] Read a prompt file under prompts/ (e.g. beliefs, goals, capabilities, soul). Returns the whole file (frontmatter + body) for use with prompt_edit.",
|
|
28
|
+
group: "context",
|
|
29
|
+
inputSchema,
|
|
30
|
+
outputSchema,
|
|
31
|
+
execute: async (input, ctx) => {
|
|
32
|
+
if (input.name.includes("/") || input.name.includes("..")) {
|
|
33
|
+
return {
|
|
34
|
+
name: input.name,
|
|
35
|
+
path: null,
|
|
36
|
+
content: "",
|
|
37
|
+
agent_modification: false,
|
|
38
|
+
is_error: true,
|
|
39
|
+
error_type: "invalid_name",
|
|
40
|
+
message: `Invalid prompt name: ${input.name}`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const filePath = join(getPromptsDir(ctx.projectDir), `${input.name}.md`);
|
|
44
|
+
const file = Bun.file(filePath);
|
|
45
|
+
if (!(await file.exists())) {
|
|
46
|
+
return {
|
|
47
|
+
name: input.name,
|
|
48
|
+
path: null,
|
|
49
|
+
content: "",
|
|
50
|
+
agent_modification: false,
|
|
51
|
+
is_error: true,
|
|
52
|
+
error_type: "not_found",
|
|
53
|
+
message: `Prompt not found: prompts/${input.name}.md`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const content = await file.text();
|
|
57
|
+
// Cheap header sniff so the agent knows whether prompt_edit will be
|
|
58
|
+
// accepted before it constructs patches.
|
|
59
|
+
const agent_modification = /agent-modification:\s*true/.test(
|
|
60
|
+
content.split("---", 3).slice(0, 3).join("---"),
|
|
61
|
+
);
|
|
62
|
+
return {
|
|
63
|
+
name: input.name,
|
|
64
|
+
path: filePath,
|
|
65
|
+
content,
|
|
66
|
+
agent_modification,
|
|
67
|
+
is_error: false,
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
package/src/tools/registry.ts
CHANGED
|
@@ -3,8 +3,6 @@ import { capabilitiesRefreshTool } from "./capabilities/refresh.ts";
|
|
|
3
3
|
// Context tools
|
|
4
4
|
import { pipeToContextTool } from "./context/pipe.ts";
|
|
5
5
|
import { readLargeResultTool } from "./context/read-large-result.ts";
|
|
6
|
-
import { updateBeliefsTool } from "./context/update-beliefs.ts";
|
|
7
|
-
import { updateGoalsTool } from "./context/update-goals.ts";
|
|
8
6
|
// Context — directory operations
|
|
9
7
|
import { contextCreateDirTool } from "./dir/create.ts";
|
|
10
8
|
import { contextDirSizeTool } from "./dir/size.ts";
|
|
@@ -24,8 +22,12 @@ import { mcpExecTool } from "./mcp/exec.ts";
|
|
|
24
22
|
import { mcpInfoTool } from "./mcp/info.ts";
|
|
25
23
|
import { mcpListToolsTool } from "./mcp/list-tools.ts";
|
|
26
24
|
import { mcpSearchTool } from "./mcp/search.ts";
|
|
25
|
+
// Prompt tools
|
|
26
|
+
import { promptEditTool } from "./prompt/edit.ts";
|
|
27
|
+
import { promptReadTool } from "./prompt/read.ts";
|
|
27
28
|
// Schedule tools
|
|
28
29
|
import { createScheduleTool } from "./schedule/create.ts";
|
|
30
|
+
import { scheduleEditTool } from "./schedule/edit.ts";
|
|
29
31
|
import { listSchedulesTool } from "./schedule/list.ts";
|
|
30
32
|
// Search tools
|
|
31
33
|
import { searchTool } from "./search/index.ts";
|
|
@@ -40,6 +42,7 @@ import { skillWriteTool } from "./skill/write.ts";
|
|
|
40
42
|
import { completeTaskTool } from "./task/complete.ts";
|
|
41
43
|
import { createTaskTool } from "./task/create.ts";
|
|
42
44
|
import { deleteTaskTool } from "./task/delete.ts";
|
|
45
|
+
import { taskEditTool } from "./task/edit.ts";
|
|
43
46
|
import { failTaskTool } from "./task/fail.ts";
|
|
44
47
|
import { listTasksTool } from "./task/list.ts";
|
|
45
48
|
import { updateTaskTool } from "./task/update.ts";
|
|
@@ -62,6 +65,7 @@ export function registerAllTools(): void {
|
|
|
62
65
|
registerTool(waitTaskTool);
|
|
63
66
|
registerTool(createTaskTool);
|
|
64
67
|
registerTool(updateTaskTool);
|
|
68
|
+
registerTool(taskEditTool);
|
|
65
69
|
registerTool(deleteTaskTool);
|
|
66
70
|
registerTool(listTasksTool);
|
|
67
71
|
registerTool(viewTaskTool);
|
|
@@ -79,8 +83,8 @@ export function registerAllTools(): void {
|
|
|
79
83
|
registerTool(contextInfoTool);
|
|
80
84
|
registerTool(contextExistsTool);
|
|
81
85
|
registerTool(contextCountLinesTool);
|
|
82
|
-
registerTool(
|
|
83
|
-
registerTool(
|
|
86
|
+
registerTool(promptReadTool);
|
|
87
|
+
registerTool(promptEditTool);
|
|
84
88
|
registerTool(readLargeResultTool);
|
|
85
89
|
registerTool(pipeToContextTool);
|
|
86
90
|
|
|
@@ -89,6 +93,7 @@ export function registerAllTools(): void {
|
|
|
89
93
|
|
|
90
94
|
// Schedule
|
|
91
95
|
registerTool(createScheduleTool);
|
|
96
|
+
registerTool(scheduleEditTool);
|
|
92
97
|
registerTool(listSchedulesTool);
|
|
93
98
|
|
|
94
99
|
// Search
|
|
@@ -33,7 +33,9 @@ export const createScheduleTool = {
|
|
|
33
33
|
description: input.description,
|
|
34
34
|
frequency: input.frequency,
|
|
35
35
|
});
|
|
36
|
-
|
|
36
|
+
const msg = `Created schedule: ${schedule.name} (${schedule.id})`;
|
|
37
|
+
if (ctx.notify) ctx.notify(msg);
|
|
38
|
+
else logger.info(msg);
|
|
37
39
|
return {
|
|
38
40
|
id: schedule.id,
|
|
39
41
|
name: schedule.name,
|