botholomew 0.15.6 → 0.16.2
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/README.md +6 -6
- package/package.json +2 -1
- package/src/chat/agent.ts +1 -1
- package/src/chat/usage.ts +1 -1
- package/src/cli.ts +2 -0
- package/src/commands/prompts.ts +333 -0
- package/src/constants.ts +1 -1
- package/src/context/capabilities.ts +4 -0
- package/src/init/index.ts +4 -4
- package/src/init/templates.ts +10 -16
- package/src/tools/prompt/create.ts +136 -0
- package/src/tools/prompt/delete.ts +103 -0
- package/src/tools/prompt/edit.ts +34 -13
- package/src/tools/prompt/list.ts +109 -0
- package/src/tools/prompt/read.ts +46 -14
- package/src/tools/registry.ts +6 -0
- package/src/tui/App.tsx +91 -109
- package/src/tui/components/ContextPanel.tsx +7 -5
- package/src/tui/components/MessageList.tsx +44 -21
- package/src/tui/components/SchedulePanel.tsx +7 -5
- package/src/tui/components/TaskPanel.tsx +7 -5
- package/src/tui/components/ThreadPanel.tsx +7 -5
- package/src/tui/components/ToolCall.tsx +3 -2
- package/src/tui/components/ToolPanel.tsx +9 -5
- package/src/tui/restoreMessages.ts +106 -0
- package/src/tui/useTerminalSize.ts +26 -0
- package/src/tui/wrapDetail.ts +15 -0
- package/src/utils/frontmatter.ts +93 -4
- package/src/worker/prompt.ts +29 -23
- package/src/worker/tick.ts +16 -7
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { unlink } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { getPromptsDir } from "../../constants.ts";
|
|
5
|
+
import {
|
|
6
|
+
PromptValidationError,
|
|
7
|
+
parsePromptFile,
|
|
8
|
+
} from "../../utils/frontmatter.ts";
|
|
9
|
+
import type { ToolDefinition } from "../tool.ts";
|
|
10
|
+
|
|
11
|
+
const inputSchema = z.object({
|
|
12
|
+
name: z
|
|
13
|
+
.string()
|
|
14
|
+
.describe("Prompt name without extension. Resolves to prompts/<name>.md."),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const outputSchema = z.object({
|
|
18
|
+
name: z.string(),
|
|
19
|
+
path: z.string().nullable(),
|
|
20
|
+
deleted: z.boolean(),
|
|
21
|
+
is_error: z.boolean(),
|
|
22
|
+
error_type: z.string().optional(),
|
|
23
|
+
message: z.string().optional(),
|
|
24
|
+
next_action_hint: z.string().optional(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export const promptDeleteTool = {
|
|
28
|
+
name: "prompt_delete",
|
|
29
|
+
description:
|
|
30
|
+
"[[ bash equivalent command: rm ]] Delete a prompt file under prompts/. Files marked `agent-modification: false` are protected and will not be removed.",
|
|
31
|
+
group: "context",
|
|
32
|
+
inputSchema,
|
|
33
|
+
outputSchema,
|
|
34
|
+
execute: async (input, ctx) => {
|
|
35
|
+
if (input.name.includes("/") || input.name.includes("..")) {
|
|
36
|
+
return {
|
|
37
|
+
name: input.name,
|
|
38
|
+
path: null,
|
|
39
|
+
deleted: false,
|
|
40
|
+
is_error: true,
|
|
41
|
+
error_type: "invalid_name",
|
|
42
|
+
message: `Invalid prompt name: ${input.name}`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const filePath = join(getPromptsDir(ctx.projectDir), `${input.name}.md`);
|
|
47
|
+
const file = Bun.file(filePath);
|
|
48
|
+
if (!(await file.exists())) {
|
|
49
|
+
return {
|
|
50
|
+
name: input.name,
|
|
51
|
+
path: null,
|
|
52
|
+
deleted: false,
|
|
53
|
+
is_error: true,
|
|
54
|
+
error_type: "not_found",
|
|
55
|
+
message: `Prompt not found: prompts/${input.name}.md`,
|
|
56
|
+
next_action_hint: "Use prompt_list to see available prompts.",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const raw = await file.text();
|
|
61
|
+
try {
|
|
62
|
+
const { meta } = parsePromptFile(filePath, raw);
|
|
63
|
+
if (!meta["agent-modification"]) {
|
|
64
|
+
return {
|
|
65
|
+
name: input.name,
|
|
66
|
+
path: filePath,
|
|
67
|
+
deleted: false,
|
|
68
|
+
is_error: true,
|
|
69
|
+
error_type: "agent_modification_disabled",
|
|
70
|
+
message: `Agent deletion not allowed for prompts/${input.name}.md`,
|
|
71
|
+
next_action_hint:
|
|
72
|
+
"Edit the file manually with `botholomew prompts delete` or your editor.",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
} catch (err) {
|
|
76
|
+
// A malformed prompt is still a valid target for deletion — the agent
|
|
77
|
+
// shouldn't be locked out of cleaning up an unparseable file. Surface
|
|
78
|
+
// the parse error in the message but allow the unlink.
|
|
79
|
+
const reason =
|
|
80
|
+
err instanceof PromptValidationError
|
|
81
|
+
? err.reason
|
|
82
|
+
: err instanceof Error
|
|
83
|
+
? err.message
|
|
84
|
+
: String(err);
|
|
85
|
+
await unlink(filePath);
|
|
86
|
+
return {
|
|
87
|
+
name: input.name,
|
|
88
|
+
path: filePath,
|
|
89
|
+
deleted: true,
|
|
90
|
+
is_error: false,
|
|
91
|
+
message: `Deleted unparseable prompt (${reason})`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
await unlink(filePath);
|
|
96
|
+
return {
|
|
97
|
+
name: input.name,
|
|
98
|
+
path: filePath,
|
|
99
|
+
deleted: true,
|
|
100
|
+
is_error: false,
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
package/src/tools/prompt/edit.ts
CHANGED
|
@@ -8,8 +8,9 @@ import {
|
|
|
8
8
|
} from "../../fs/atomic.ts";
|
|
9
9
|
import { applyLinePatches, LinePatchSchema } from "../../fs/patches.ts";
|
|
10
10
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
PromptValidationError,
|
|
12
|
+
parsePromptFile,
|
|
13
|
+
serializePromptFile,
|
|
13
14
|
} from "../../utils/frontmatter.ts";
|
|
14
15
|
import type { ToolDefinition } from "../tool.ts";
|
|
15
16
|
|
|
@@ -17,7 +18,7 @@ const inputSchema = z.object({
|
|
|
17
18
|
name: z
|
|
18
19
|
.string()
|
|
19
20
|
.describe(
|
|
20
|
-
"Prompt name without extension (e.g. 'beliefs', 'goals'
|
|
21
|
+
"Prompt name without extension (e.g. 'beliefs', 'goals'). Resolves to prompts/<name>.md.",
|
|
21
22
|
),
|
|
22
23
|
patches: z.array(LinePatchSchema).describe("Patches to apply"),
|
|
23
24
|
});
|
|
@@ -36,7 +37,7 @@ const outputSchema = z.object({
|
|
|
36
37
|
export const promptEditTool = {
|
|
37
38
|
name: "prompt_edit",
|
|
38
39
|
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`
|
|
40
|
+
"[[ 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` are protected. Use prompt_read first to inspect current line numbers.",
|
|
40
41
|
group: "context",
|
|
41
42
|
inputSchema,
|
|
42
43
|
outputSchema,
|
|
@@ -65,11 +66,31 @@ export const promptEditTool = {
|
|
|
65
66
|
is_error: true,
|
|
66
67
|
error_type: "not_found",
|
|
67
68
|
message: `Prompt not found: prompts/${input.name}.md`,
|
|
69
|
+
next_action_hint:
|
|
70
|
+
"Use prompt_list to see available prompts, or prompt_create to add a new one.",
|
|
68
71
|
};
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
const original = file.content;
|
|
72
|
-
|
|
75
|
+
let preParsed: ReturnType<typeof parsePromptFile>;
|
|
76
|
+
try {
|
|
77
|
+
preParsed = parsePromptFile(filePath, original);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
return {
|
|
80
|
+
name: input.name,
|
|
81
|
+
path: filePath,
|
|
82
|
+
applied: 0,
|
|
83
|
+
content: original,
|
|
84
|
+
is_error: true,
|
|
85
|
+
error_type: "invalid_frontmatter",
|
|
86
|
+
message:
|
|
87
|
+
err instanceof PromptValidationError
|
|
88
|
+
? err.message
|
|
89
|
+
: `Existing prompt failed to parse: ${err instanceof Error ? err.message : String(err)}`,
|
|
90
|
+
next_action_hint:
|
|
91
|
+
"Fix the file's frontmatter directly before patching. Required keys: title, loading, agent-modification.",
|
|
92
|
+
};
|
|
93
|
+
}
|
|
73
94
|
if (!preParsed.meta["agent-modification"]) {
|
|
74
95
|
return {
|
|
75
96
|
name: input.name,
|
|
@@ -83,9 +104,9 @@ export const promptEditTool = {
|
|
|
83
104
|
}
|
|
84
105
|
|
|
85
106
|
const updated = applyLinePatches(original, input.patches);
|
|
86
|
-
let postParsed:
|
|
107
|
+
let postParsed: ReturnType<typeof parsePromptFile>;
|
|
87
108
|
try {
|
|
88
|
-
postParsed =
|
|
109
|
+
postParsed = parsePromptFile(filePath, updated);
|
|
89
110
|
} catch (err) {
|
|
90
111
|
return {
|
|
91
112
|
name: input.name,
|
|
@@ -94,9 +115,12 @@ export const promptEditTool = {
|
|
|
94
115
|
content: original,
|
|
95
116
|
is_error: true,
|
|
96
117
|
error_type: "invalid_frontmatter",
|
|
97
|
-
message:
|
|
118
|
+
message:
|
|
119
|
+
err instanceof PromptValidationError
|
|
120
|
+
? `Patched content failed to parse — ${err.reason}`
|
|
121
|
+
: `Patched content failed to parse: ${err instanceof Error ? err.message : String(err)}`,
|
|
98
122
|
next_action_hint:
|
|
99
|
-
"Check that the frontmatter delimiters and YAML stay valid.",
|
|
123
|
+
"Check that the frontmatter delimiters and YAML stay valid (title, loading, agent-modification all required).",
|
|
100
124
|
};
|
|
101
125
|
}
|
|
102
126
|
if (!postParsed.meta["agent-modification"]) {
|
|
@@ -113,10 +137,7 @@ export const promptEditTool = {
|
|
|
113
137
|
};
|
|
114
138
|
}
|
|
115
139
|
|
|
116
|
-
const serialized =
|
|
117
|
-
postParsed.meta,
|
|
118
|
-
postParsed.content,
|
|
119
|
-
);
|
|
140
|
+
const serialized = serializePromptFile(postParsed.meta, postParsed.content);
|
|
120
141
|
|
|
121
142
|
try {
|
|
122
143
|
await atomicWriteIfUnchanged(filePath, serialized, file.mtimeMs);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { getPromptsDir } from "../../constants.ts";
|
|
5
|
+
import {
|
|
6
|
+
PromptValidationError,
|
|
7
|
+
parsePromptFile,
|
|
8
|
+
} from "../../utils/frontmatter.ts";
|
|
9
|
+
import type { ToolDefinition } from "../tool.ts";
|
|
10
|
+
|
|
11
|
+
const inputSchema = z.object({
|
|
12
|
+
limit: z
|
|
13
|
+
.number()
|
|
14
|
+
.optional()
|
|
15
|
+
.default(100)
|
|
16
|
+
.describe("Max number of prompts to return (default 100)"),
|
|
17
|
+
offset: z
|
|
18
|
+
.number()
|
|
19
|
+
.optional()
|
|
20
|
+
.default(0)
|
|
21
|
+
.describe("Skip the first N prompts (default 0)"),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const outputSchema = z.object({
|
|
25
|
+
prompts: z.array(
|
|
26
|
+
z.object({
|
|
27
|
+
name: z.string(),
|
|
28
|
+
title: z.string().nullable(),
|
|
29
|
+
loading: z.string().nullable(),
|
|
30
|
+
agent_modification: z.boolean(),
|
|
31
|
+
size_bytes: z.number(),
|
|
32
|
+
path: z.string(),
|
|
33
|
+
valid: z.boolean(),
|
|
34
|
+
error: z.string().nullable(),
|
|
35
|
+
}),
|
|
36
|
+
),
|
|
37
|
+
total: z.number(),
|
|
38
|
+
is_error: z.boolean(),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export const promptListTool = {
|
|
42
|
+
name: "prompt_list",
|
|
43
|
+
description:
|
|
44
|
+
"[[ bash equivalent command: ls ]] List prompt files under prompts/. Returns name, title, loading mode, agent_modification flag, file size, and a valid/error pair per file (so you can see at a glance which prompts have broken frontmatter).",
|
|
45
|
+
group: "context",
|
|
46
|
+
inputSchema,
|
|
47
|
+
outputSchema,
|
|
48
|
+
execute: async (input, ctx) => {
|
|
49
|
+
const dir = getPromptsDir(ctx.projectDir);
|
|
50
|
+
let entries: string[];
|
|
51
|
+
try {
|
|
52
|
+
entries = await readdir(dir);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
55
|
+
return { prompts: [], total: 0, is_error: false };
|
|
56
|
+
}
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const mdFiles = entries.filter((f) => f.endsWith(".md")).sort();
|
|
61
|
+
const total = mdFiles.length;
|
|
62
|
+
const offset = input.offset ?? 0;
|
|
63
|
+
const limit = input.limit ?? 100;
|
|
64
|
+
const page = mdFiles.slice(offset, offset + limit);
|
|
65
|
+
|
|
66
|
+
const rows = await Promise.all(
|
|
67
|
+
page.map(async (filename) => {
|
|
68
|
+
const filePath = join(dir, filename);
|
|
69
|
+
const name = filename.replace(/\.md$/, "");
|
|
70
|
+
const [raw, st] = await Promise.all([
|
|
71
|
+
Bun.file(filePath).text(),
|
|
72
|
+
stat(filePath),
|
|
73
|
+
]);
|
|
74
|
+
try {
|
|
75
|
+
const { meta } = parsePromptFile(filePath, raw);
|
|
76
|
+
return {
|
|
77
|
+
name,
|
|
78
|
+
title: meta.title,
|
|
79
|
+
loading: meta.loading,
|
|
80
|
+
agent_modification: meta["agent-modification"],
|
|
81
|
+
size_bytes: st.size,
|
|
82
|
+
path: filePath,
|
|
83
|
+
valid: true,
|
|
84
|
+
error: null,
|
|
85
|
+
};
|
|
86
|
+
} catch (err) {
|
|
87
|
+
const reason =
|
|
88
|
+
err instanceof PromptValidationError
|
|
89
|
+
? err.reason
|
|
90
|
+
: err instanceof Error
|
|
91
|
+
? err.message
|
|
92
|
+
: String(err);
|
|
93
|
+
return {
|
|
94
|
+
name,
|
|
95
|
+
title: null,
|
|
96
|
+
loading: null,
|
|
97
|
+
agent_modification: false,
|
|
98
|
+
size_bytes: st.size,
|
|
99
|
+
path: filePath,
|
|
100
|
+
valid: false,
|
|
101
|
+
error: reason,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return { prompts: rows, total, is_error: false };
|
|
108
|
+
},
|
|
109
|
+
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
package/src/tools/prompt/read.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { getPromptsDir } from "../../constants.ts";
|
|
4
|
+
import {
|
|
5
|
+
PromptValidationError,
|
|
6
|
+
parsePromptFile,
|
|
7
|
+
} from "../../utils/frontmatter.ts";
|
|
4
8
|
import type { ToolDefinition } from "../tool.ts";
|
|
5
9
|
|
|
6
10
|
const inputSchema = z.object({
|
|
7
11
|
name: z
|
|
8
12
|
.string()
|
|
9
13
|
.describe(
|
|
10
|
-
"Prompt name without extension (e.g. 'beliefs', 'goals'
|
|
14
|
+
"Prompt name without extension (e.g. 'beliefs', 'goals'). Resolves to prompts/<name>.md.",
|
|
11
15
|
),
|
|
12
16
|
});
|
|
13
17
|
|
|
@@ -15,16 +19,19 @@ const outputSchema = z.object({
|
|
|
15
19
|
name: z.string(),
|
|
16
20
|
path: z.string().nullable(),
|
|
17
21
|
content: z.string(),
|
|
22
|
+
title: z.string().nullable(),
|
|
23
|
+
loading: z.string().nullable(),
|
|
18
24
|
agent_modification: z.boolean(),
|
|
19
25
|
is_error: z.boolean(),
|
|
20
26
|
error_type: z.string().optional(),
|
|
21
27
|
message: z.string().optional(),
|
|
28
|
+
next_action_hint: z.string().optional(),
|
|
22
29
|
});
|
|
23
30
|
|
|
24
31
|
export const promptReadTool = {
|
|
25
32
|
name: "prompt_read",
|
|
26
33
|
description:
|
|
27
|
-
"[[ bash equivalent command: cat ]] Read a prompt file under prompts
|
|
34
|
+
"[[ bash equivalent command: cat ]] Read a prompt file under prompts/. Returns the whole file (frontmatter + body) plus the parsed title / loading / agent_modification flags so you can decide whether prompt_edit will be accepted.",
|
|
28
35
|
group: "context",
|
|
29
36
|
inputSchema,
|
|
30
37
|
outputSchema,
|
|
@@ -34,6 +41,8 @@ export const promptReadTool = {
|
|
|
34
41
|
name: input.name,
|
|
35
42
|
path: null,
|
|
36
43
|
content: "",
|
|
44
|
+
title: null,
|
|
45
|
+
loading: null,
|
|
37
46
|
agent_modification: false,
|
|
38
47
|
is_error: true,
|
|
39
48
|
error_type: "invalid_name",
|
|
@@ -47,24 +56,47 @@ export const promptReadTool = {
|
|
|
47
56
|
name: input.name,
|
|
48
57
|
path: null,
|
|
49
58
|
content: "",
|
|
59
|
+
title: null,
|
|
60
|
+
loading: null,
|
|
50
61
|
agent_modification: false,
|
|
51
62
|
is_error: true,
|
|
52
63
|
error_type: "not_found",
|
|
53
64
|
message: `Prompt not found: prompts/${input.name}.md`,
|
|
65
|
+
next_action_hint: "Use prompt_list to see available prompts.",
|
|
54
66
|
};
|
|
55
67
|
}
|
|
56
68
|
const content = await file.text();
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
+
try {
|
|
70
|
+
const { meta } = parsePromptFile(filePath, content);
|
|
71
|
+
return {
|
|
72
|
+
name: input.name,
|
|
73
|
+
path: filePath,
|
|
74
|
+
content,
|
|
75
|
+
title: meta.title,
|
|
76
|
+
loading: meta.loading,
|
|
77
|
+
agent_modification: meta["agent-modification"],
|
|
78
|
+
is_error: false,
|
|
79
|
+
};
|
|
80
|
+
} catch (err) {
|
|
81
|
+
const message =
|
|
82
|
+
err instanceof PromptValidationError
|
|
83
|
+
? err.message
|
|
84
|
+
: err instanceof Error
|
|
85
|
+
? err.message
|
|
86
|
+
: String(err);
|
|
87
|
+
return {
|
|
88
|
+
name: input.name,
|
|
89
|
+
path: filePath,
|
|
90
|
+
content,
|
|
91
|
+
title: null,
|
|
92
|
+
loading: null,
|
|
93
|
+
agent_modification: false,
|
|
94
|
+
is_error: true,
|
|
95
|
+
error_type: "invalid_frontmatter",
|
|
96
|
+
message,
|
|
97
|
+
next_action_hint:
|
|
98
|
+
"Fix the file's frontmatter (required: title, loading, agent-modification). The raw content is returned for inspection.",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
69
101
|
},
|
|
70
102
|
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
package/src/tools/registry.ts
CHANGED
|
@@ -23,7 +23,10 @@ import { mcpInfoTool } from "./mcp/info.ts";
|
|
|
23
23
|
import { mcpListToolsTool } from "./mcp/list-tools.ts";
|
|
24
24
|
import { mcpSearchTool } from "./mcp/search.ts";
|
|
25
25
|
// Prompt tools
|
|
26
|
+
import { promptCreateTool } from "./prompt/create.ts";
|
|
27
|
+
import { promptDeleteTool } from "./prompt/delete.ts";
|
|
26
28
|
import { promptEditTool } from "./prompt/edit.ts";
|
|
29
|
+
import { promptListTool } from "./prompt/list.ts";
|
|
27
30
|
import { promptReadTool } from "./prompt/read.ts";
|
|
28
31
|
// Schedule tools
|
|
29
32
|
import { createScheduleTool } from "./schedule/create.ts";
|
|
@@ -83,8 +86,11 @@ export function registerAllTools(): void {
|
|
|
83
86
|
registerTool(contextInfoTool);
|
|
84
87
|
registerTool(contextExistsTool);
|
|
85
88
|
registerTool(contextCountLinesTool);
|
|
89
|
+
registerTool(promptListTool);
|
|
86
90
|
registerTool(promptReadTool);
|
|
91
|
+
registerTool(promptCreateTool);
|
|
87
92
|
registerTool(promptEditTool);
|
|
93
|
+
registerTool(promptDeleteTool);
|
|
88
94
|
registerTool(readLargeResultTool);
|
|
89
95
|
registerTool(pipeToContextTool);
|
|
90
96
|
|