botholomew 0.6.1 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/chat/agent.ts +1 -1
- package/src/chat/session.ts +5 -0
- package/src/cli.ts +2 -2
- package/src/commands/context.ts +31 -47
- package/src/commands/skill.ts +100 -0
- package/src/commands/tools.ts +78 -42
- package/src/constants.ts +5 -0
- package/src/db/context.ts +49 -2
- package/src/db/uuid.ts +7 -0
- package/src/init/index.ts +14 -1
- package/src/init/templates.ts +23 -0
- package/src/skills/commands.ts +61 -0
- package/src/skills/loader.ts +36 -0
- package/src/skills/parser.ts +95 -0
- package/src/tools/context/search.ts +3 -3
- package/src/tools/dir/create.ts +4 -4
- package/src/tools/dir/list.ts +4 -4
- package/src/tools/dir/size.ts +4 -4
- package/src/tools/dir/tree.ts +234 -51
- package/src/tools/file/copy.ts +4 -4
- package/src/tools/file/count-lines.ts +7 -8
- package/src/tools/file/delete.ts +4 -4
- package/src/tools/file/edit.ts +4 -4
- package/src/tools/file/exists.ts +8 -8
- package/src/tools/file/info.ts +8 -8
- package/src/tools/file/move.ts +4 -4
- package/src/tools/file/read.ts +7 -8
- package/src/tools/file/write.ts +4 -4
- package/src/tools/registry.ts +35 -38
- package/src/tui/App.tsx +63 -4
- package/src/tui/components/InputBar.tsx +39 -1
package/src/tools/file/edit.ts
CHANGED
|
@@ -24,11 +24,11 @@ const outputSchema = z.object({
|
|
|
24
24
|
is_error: z.boolean(),
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
export const
|
|
28
|
-
name: "
|
|
27
|
+
export const contextEditTool = {
|
|
28
|
+
name: "context_edit",
|
|
29
29
|
description:
|
|
30
|
-
"Apply git-style patches to a
|
|
31
|
-
group: "
|
|
30
|
+
"Apply git-style patches to a context item. Each patch specifies a line range to replace.",
|
|
31
|
+
group: "context",
|
|
32
32
|
inputSchema,
|
|
33
33
|
outputSchema,
|
|
34
34
|
execute: async (input, ctx) => {
|
package/src/tools/file/exists.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveContextItem } from "../../db/context.ts";
|
|
3
3
|
import type { ToolDefinition } from "../tool.ts";
|
|
4
4
|
|
|
5
5
|
const inputSchema = z.object({
|
|
6
|
-
path: z.string().describe("File path
|
|
6
|
+
path: z.string().describe("File path or context item ID"),
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
const outputSchema = z.object({
|
|
@@ -11,14 +11,14 @@ const outputSchema = z.object({
|
|
|
11
11
|
is_error: z.boolean(),
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
export const
|
|
15
|
-
name: "
|
|
16
|
-
description: "Check if a
|
|
17
|
-
group: "
|
|
14
|
+
export const contextExistsTool = {
|
|
15
|
+
name: "context_exists",
|
|
16
|
+
description: "Check if a context item exists.",
|
|
17
|
+
group: "context",
|
|
18
18
|
inputSchema,
|
|
19
19
|
outputSchema,
|
|
20
20
|
execute: async (input, ctx) => {
|
|
21
|
-
const
|
|
22
|
-
return { exists, is_error: false };
|
|
21
|
+
const item = await resolveContextItem(ctx.conn, input.path);
|
|
22
|
+
return { exists: item !== null, is_error: false };
|
|
23
23
|
},
|
|
24
24
|
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
package/src/tools/file/info.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveContextItemOrThrow } from "../../db/context.ts";
|
|
3
3
|
import type { ToolDefinition } from "../tool.ts";
|
|
4
4
|
|
|
5
5
|
const inputSchema = z.object({
|
|
6
|
-
path: z.string().describe("File path"),
|
|
6
|
+
path: z.string().describe("File path or context item ID"),
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
const outputSchema = z.object({
|
|
@@ -22,15 +22,15 @@ const outputSchema = z.object({
|
|
|
22
22
|
is_error: z.boolean(),
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
export const
|
|
26
|
-
name: "
|
|
27
|
-
description:
|
|
28
|
-
|
|
25
|
+
export const contextInfoTool = {
|
|
26
|
+
name: "context_info",
|
|
27
|
+
description:
|
|
28
|
+
"Show context item metadata (size, MIME type, line count, etc.).",
|
|
29
|
+
group: "context",
|
|
29
30
|
inputSchema,
|
|
30
31
|
outputSchema,
|
|
31
32
|
execute: async (input, ctx) => {
|
|
32
|
-
const item = await
|
|
33
|
-
if (!item) throw new Error(`Not found: ${input.path}`);
|
|
33
|
+
const item = await resolveContextItemOrThrow(ctx.conn, input.path);
|
|
34
34
|
|
|
35
35
|
const content = item.content ?? "";
|
|
36
36
|
return {
|
package/src/tools/file/move.ts
CHANGED
|
@@ -17,10 +17,10 @@ const outputSchema = z.object({
|
|
|
17
17
|
is_error: z.boolean(),
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
export const
|
|
21
|
-
name: "
|
|
22
|
-
description: "Move or rename a
|
|
23
|
-
group: "
|
|
20
|
+
export const contextMoveTool = {
|
|
21
|
+
name: "context_move",
|
|
22
|
+
description: "Move or rename a context item.",
|
|
23
|
+
group: "context",
|
|
24
24
|
inputSchema,
|
|
25
25
|
outputSchema,
|
|
26
26
|
execute: async (input, ctx) => {
|
package/src/tools/file/read.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveContextItemOrThrow } from "../../db/context.ts";
|
|
3
3
|
import type { ToolDefinition } from "../tool.ts";
|
|
4
4
|
|
|
5
5
|
const inputSchema = z.object({
|
|
6
|
-
path: z.string().describe("File path
|
|
6
|
+
path: z.string().describe("File path or context item ID"),
|
|
7
7
|
offset: z
|
|
8
8
|
.number()
|
|
9
9
|
.optional()
|
|
@@ -16,15 +16,14 @@ const outputSchema = z.object({
|
|
|
16
16
|
is_error: z.boolean(),
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
export const
|
|
20
|
-
name: "
|
|
21
|
-
description: "Read a
|
|
22
|
-
group: "
|
|
19
|
+
export const contextReadTool = {
|
|
20
|
+
name: "context_read",
|
|
21
|
+
description: "Read a context item's contents.",
|
|
22
|
+
group: "context",
|
|
23
23
|
inputSchema,
|
|
24
24
|
outputSchema,
|
|
25
25
|
execute: async (input, ctx) => {
|
|
26
|
-
const item = await
|
|
27
|
-
if (!item) throw new Error(`Not found: ${input.path}`);
|
|
26
|
+
const item = await resolveContextItemOrThrow(ctx.conn, input.path);
|
|
28
27
|
if (item.content == null) throw new Error(`No text content: ${input.path}`);
|
|
29
28
|
|
|
30
29
|
let content = item.content;
|
package/src/tools/file/write.ts
CHANGED
|
@@ -38,11 +38,11 @@ const outputSchema = z.object({
|
|
|
38
38
|
is_error: z.boolean(),
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
export const
|
|
42
|
-
name: "
|
|
41
|
+
export const contextWriteTool = {
|
|
42
|
+
name: "context_write",
|
|
43
43
|
description:
|
|
44
|
-
"Write content to a
|
|
45
|
-
group: "
|
|
44
|
+
"Write content to a context item. Creates the item if it doesn't exist, or overwrites if it does.",
|
|
45
|
+
group: "context",
|
|
46
46
|
inputSchema,
|
|
47
47
|
outputSchema,
|
|
48
48
|
execute: async (input, ctx) => {
|
package/src/tools/registry.ts
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
// Context tools
|
|
2
|
+
|
|
2
3
|
import { readLargeResultTool } from "./context/read-large-result.ts";
|
|
3
|
-
import {
|
|
4
|
+
import { contextSearchTool } from "./context/search.ts";
|
|
4
5
|
import { updateBeliefsTool } from "./context/update-beliefs.ts";
|
|
5
6
|
import { updateGoalsTool } from "./context/update-goals.ts";
|
|
6
|
-
//
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
7
|
+
// Context — directory operations
|
|
8
|
+
import { contextCreateDirTool } from "./dir/create.ts";
|
|
9
|
+
import { contextListDirTool } from "./dir/list.ts";
|
|
10
|
+
import { contextDirSizeTool } from "./dir/size.ts";
|
|
11
|
+
import { contextTreeTool } from "./dir/tree.ts";
|
|
12
|
+
// Context — file operations
|
|
13
|
+
import { contextCopyTool } from "./file/copy.ts";
|
|
14
|
+
import { contextCountLinesTool } from "./file/count-lines.ts";
|
|
15
|
+
import { contextDeleteTool } from "./file/delete.ts";
|
|
16
|
+
import { contextEditTool } from "./file/edit.ts";
|
|
17
|
+
import { contextExistsTool } from "./file/exists.ts";
|
|
18
|
+
import { contextInfoTool } from "./file/info.ts";
|
|
19
|
+
import { contextMoveTool } from "./file/move.ts";
|
|
20
|
+
import { contextReadTool } from "./file/read.ts";
|
|
21
|
+
import { contextWriteTool } from "./file/write.ts";
|
|
21
22
|
// MCP tools
|
|
22
23
|
import { mcpExecTool } from "./mcp/exec.ts";
|
|
23
24
|
import { mcpInfoTool } from "./mcp/info.ts";
|
|
@@ -52,22 +53,24 @@ export function registerAllTools(): void {
|
|
|
52
53
|
registerTool(listTasksTool);
|
|
53
54
|
registerTool(viewTaskTool);
|
|
54
55
|
|
|
55
|
-
//
|
|
56
|
-
registerTool(
|
|
57
|
-
registerTool(
|
|
58
|
-
registerTool(
|
|
59
|
-
registerTool(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
registerTool(
|
|
63
|
-
registerTool(
|
|
64
|
-
registerTool(
|
|
65
|
-
registerTool(
|
|
66
|
-
registerTool(
|
|
67
|
-
registerTool(
|
|
68
|
-
registerTool(
|
|
69
|
-
registerTool(
|
|
70
|
-
registerTool(
|
|
56
|
+
// Context
|
|
57
|
+
registerTool(contextCreateDirTool);
|
|
58
|
+
registerTool(contextListDirTool);
|
|
59
|
+
registerTool(contextTreeTool);
|
|
60
|
+
registerTool(contextDirSizeTool);
|
|
61
|
+
registerTool(contextReadTool);
|
|
62
|
+
registerTool(contextWriteTool);
|
|
63
|
+
registerTool(contextEditTool);
|
|
64
|
+
registerTool(contextDeleteTool);
|
|
65
|
+
registerTool(contextCopyTool);
|
|
66
|
+
registerTool(contextMoveTool);
|
|
67
|
+
registerTool(contextInfoTool);
|
|
68
|
+
registerTool(contextExistsTool);
|
|
69
|
+
registerTool(contextCountLinesTool);
|
|
70
|
+
registerTool(contextSearchTool);
|
|
71
|
+
registerTool(updateBeliefsTool);
|
|
72
|
+
registerTool(updateGoalsTool);
|
|
73
|
+
registerTool(readLargeResultTool);
|
|
71
74
|
|
|
72
75
|
// Schedule
|
|
73
76
|
registerTool(createScheduleTool);
|
|
@@ -81,12 +84,6 @@ export function registerAllTools(): void {
|
|
|
81
84
|
registerTool(listThreadsTool);
|
|
82
85
|
registerTool(viewThreadTool);
|
|
83
86
|
|
|
84
|
-
// Context
|
|
85
|
-
registerTool(searchContextTool);
|
|
86
|
-
registerTool(updateBeliefsTool);
|
|
87
|
-
registerTool(updateGoalsTool);
|
|
88
|
-
registerTool(readLargeResultTool);
|
|
89
|
-
|
|
90
87
|
// MCP
|
|
91
88
|
registerTool(mcpListToolsTool);
|
|
92
89
|
registerTool(mcpSearchTool);
|
package/src/tui/App.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import { MAX_INLINE_CHARS, PAGE_SIZE_CHARS } from "../daemon/large-results.ts";
|
|
10
10
|
import type { Interaction } from "../db/threads.ts";
|
|
11
11
|
import { getThread } from "../db/threads.ts";
|
|
12
|
+
import { handleSlashCommand } from "../skills/commands.ts";
|
|
12
13
|
import { ContextPanel } from "./components/ContextPanel.tsx";
|
|
13
14
|
import { HelpPanel } from "./components/HelpPanel.tsx";
|
|
14
15
|
import { InputBar } from "./components/InputBar.tsx";
|
|
@@ -209,6 +210,11 @@ export function App({
|
|
|
209
210
|
queuedMessagesRef.current = queuedMessages;
|
|
210
211
|
selectedQueueIndexRef.current = selectedQueueIndex;
|
|
211
212
|
|
|
213
|
+
const tabConsumedRef = useRef(false);
|
|
214
|
+
const handleTabConsumed = useCallback(() => {
|
|
215
|
+
tabConsumedRef.current = true;
|
|
216
|
+
}, []);
|
|
217
|
+
|
|
212
218
|
const stableAppHandler = useCallback(
|
|
213
219
|
// biome-ignore lint/suspicious/noExplicitAny: Ink's Key type is not exported
|
|
214
220
|
(input: string, key: any) => {
|
|
@@ -218,8 +224,12 @@ export function App({
|
|
|
218
224
|
return;
|
|
219
225
|
}
|
|
220
226
|
|
|
221
|
-
// Tab key cycles tabs —
|
|
227
|
+
// Tab key cycles tabs — unless InputBar consumed it for completion
|
|
222
228
|
if (key.tab && !key.shift) {
|
|
229
|
+
if (tabConsumedRef.current) {
|
|
230
|
+
tabConsumedRef.current = false;
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
223
233
|
setActiveTab((t) => ((t % 7) + 1) as TabId);
|
|
224
234
|
return;
|
|
225
235
|
}
|
|
@@ -414,6 +424,23 @@ export function App({
|
|
|
414
424
|
setInputValue("");
|
|
415
425
|
|
|
416
426
|
if (trimmed === "/help") {
|
|
427
|
+
const skills = sessionRef.current.skills;
|
|
428
|
+
const skillLines: string[] = [];
|
|
429
|
+
if (skills.size > 0) {
|
|
430
|
+
skillLines.push("", "Skills:");
|
|
431
|
+
for (const [skillName, skill] of skills) {
|
|
432
|
+
skillLines.push(
|
|
433
|
+
` /${skillName.padEnd(14)} ${skill.description || "(no description)"}`,
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
} else {
|
|
437
|
+
skillLines.push(
|
|
438
|
+
"",
|
|
439
|
+
"Skills:",
|
|
440
|
+
" (none — add .md files to .botholomew/skills/)",
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
417
444
|
const helpMsg: ChatMessage = {
|
|
418
445
|
id: msgId(),
|
|
419
446
|
role: "system",
|
|
@@ -467,7 +494,9 @@ export function App({
|
|
|
467
494
|
"",
|
|
468
495
|
"Commands:",
|
|
469
496
|
" /help Show this help",
|
|
497
|
+
" /skills List available skills",
|
|
470
498
|
" /quit, /exit End the chat session",
|
|
499
|
+
...skillLines,
|
|
471
500
|
].join("\n"),
|
|
472
501
|
timestamp: new Date(),
|
|
473
502
|
};
|
|
@@ -475,9 +504,28 @@ export function App({
|
|
|
475
504
|
return;
|
|
476
505
|
}
|
|
477
506
|
|
|
478
|
-
if (trimmed
|
|
479
|
-
|
|
480
|
-
|
|
507
|
+
if (trimmed.startsWith("/")) {
|
|
508
|
+
const skills = sessionRef.current.skills;
|
|
509
|
+
const handled = handleSlashCommand(trimmed, {
|
|
510
|
+
skills,
|
|
511
|
+
addSystemMessage: (content) => {
|
|
512
|
+
const msg: ChatMessage = {
|
|
513
|
+
id: msgId(),
|
|
514
|
+
role: "system",
|
|
515
|
+
content,
|
|
516
|
+
timestamp: new Date(),
|
|
517
|
+
};
|
|
518
|
+
setMessages((prev) => [...prev, msg]);
|
|
519
|
+
},
|
|
520
|
+
queueUserMessage: (content) => {
|
|
521
|
+
setInputHistory((prev) => [...prev, trimmed]);
|
|
522
|
+
queueRef.current.push(content);
|
|
523
|
+
syncQueue();
|
|
524
|
+
processQueue();
|
|
525
|
+
},
|
|
526
|
+
exit,
|
|
527
|
+
});
|
|
528
|
+
if (handled) return;
|
|
481
529
|
}
|
|
482
530
|
|
|
483
531
|
setInputHistory((prev) => [...prev, trimmed]);
|
|
@@ -502,6 +550,15 @@ export function App({
|
|
|
502
550
|
[projectDir, sessionConn, chatTitle],
|
|
503
551
|
);
|
|
504
552
|
|
|
553
|
+
const sessionSkills = ready ? sessionRef.current?.skills : undefined;
|
|
554
|
+
const skillCompletions = useMemo(() => {
|
|
555
|
+
const builtins = ["/help", "/quit", "/exit", "/skills"];
|
|
556
|
+
const skillNames = Array.from(sessionSkills?.keys() ?? []).map(
|
|
557
|
+
(name) => `/${name}`,
|
|
558
|
+
);
|
|
559
|
+
return [...builtins, ...skillNames];
|
|
560
|
+
}, [sessionSkills]);
|
|
561
|
+
|
|
505
562
|
const allToolCalls = useMemo(
|
|
506
563
|
() => messages.flatMap((m) => m.toolCalls ?? []),
|
|
507
564
|
[messages],
|
|
@@ -624,6 +681,8 @@ export function App({
|
|
|
624
681
|
disabled={activeTab !== 1}
|
|
625
682
|
history={inputHistory}
|
|
626
683
|
header={inputBarHeader}
|
|
684
|
+
completions={skillCompletions}
|
|
685
|
+
onTabConsumed={handleTabConsumed}
|
|
627
686
|
/>
|
|
628
687
|
<TabBar activeTab={activeTab} />
|
|
629
688
|
</Box>
|
|
@@ -15,6 +15,8 @@ interface InputBarProps {
|
|
|
15
15
|
disabled: boolean;
|
|
16
16
|
history: string[];
|
|
17
17
|
header?: ReactNode;
|
|
18
|
+
completions?: string[];
|
|
19
|
+
onTabConsumed?: () => void;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export const InputBar = memo(function InputBar({
|
|
@@ -24,6 +26,8 @@ export const InputBar = memo(function InputBar({
|
|
|
24
26
|
disabled,
|
|
25
27
|
history,
|
|
26
28
|
header,
|
|
29
|
+
completions,
|
|
30
|
+
onTabConsumed,
|
|
27
31
|
}: InputBarProps) {
|
|
28
32
|
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
29
33
|
const [cursorPos, setCursorPos] = useState(0);
|
|
@@ -39,6 +43,9 @@ export const InputBar = memo(function InputBar({
|
|
|
39
43
|
const onChangeRef = useRef(onChange);
|
|
40
44
|
const onSubmitRef = useRef(onSubmit);
|
|
41
45
|
const historyRef = useRef(history);
|
|
46
|
+
const completionsRef = useRef(completions);
|
|
47
|
+
const onTabConsumedRef = useRef(onTabConsumed);
|
|
48
|
+
const tabCycleRef = useRef(-1);
|
|
42
49
|
|
|
43
50
|
valueRef.current = value;
|
|
44
51
|
cursorPosRef.current = cursorPos;
|
|
@@ -46,6 +53,8 @@ export const InputBar = memo(function InputBar({
|
|
|
46
53
|
onChangeRef.current = onChange;
|
|
47
54
|
onSubmitRef.current = onSubmit;
|
|
48
55
|
historyRef.current = history;
|
|
56
|
+
completionsRef.current = completions;
|
|
57
|
+
onTabConsumedRef.current = onTabConsumed;
|
|
49
58
|
|
|
50
59
|
// Blink cursor when input is active — skip ticks while typing so the
|
|
51
60
|
// cursor stays solid and we avoid unnecessary renders during rapid input.
|
|
@@ -173,8 +182,37 @@ export const InputBar = memo(function InputBar({
|
|
|
173
182
|
return;
|
|
174
183
|
}
|
|
175
184
|
|
|
185
|
+
// Tab-completion for slash commands
|
|
186
|
+
if (key.tab) {
|
|
187
|
+
const comps = completionsRef.current;
|
|
188
|
+
if (val.startsWith("/") && comps && comps.length > 0) {
|
|
189
|
+
const matches = comps.filter((c) => c.startsWith(val));
|
|
190
|
+
if (matches.length === 1) {
|
|
191
|
+
const completed = `${matches[0] ?? ""} `;
|
|
192
|
+
valueRef.current = completed;
|
|
193
|
+
cursorPosRef.current = completed.length;
|
|
194
|
+
onChangeRef.current(completed);
|
|
195
|
+
setCursorPos(completed.length);
|
|
196
|
+
tabCycleRef.current = -1;
|
|
197
|
+
} else if (matches.length > 1) {
|
|
198
|
+
const idx = (tabCycleRef.current + 1) % matches.length;
|
|
199
|
+
tabCycleRef.current = idx;
|
|
200
|
+
const completed = matches[idx] ?? "";
|
|
201
|
+
valueRef.current = completed;
|
|
202
|
+
cursorPosRef.current = completed.length;
|
|
203
|
+
onChangeRef.current(completed);
|
|
204
|
+
setCursorPos(completed.length);
|
|
205
|
+
}
|
|
206
|
+
onTabConsumedRef.current?.();
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Reset tab cycle on any non-tab key
|
|
212
|
+
tabCycleRef.current = -1;
|
|
213
|
+
|
|
176
214
|
// Ignore other control keys
|
|
177
|
-
if (key.ctrl || key.escape
|
|
215
|
+
if (key.ctrl || key.escape) {
|
|
178
216
|
return;
|
|
179
217
|
}
|
|
180
218
|
|