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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "botholomew",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "An AI agent for knowledge work",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@anthropic-ai/sdk": "^0.88.0",
|
|
26
26
|
"@duckdb/node-api": "^1.5.2-r.1",
|
|
27
|
-
"@evantahler/mcpx": "0.18.
|
|
27
|
+
"@evantahler/mcpx": "0.18.6",
|
|
28
28
|
"ansis": "^4.2.0",
|
|
29
29
|
"commander": "^14.0.0",
|
|
30
30
|
"gray-matter": "^4.0.3",
|
package/src/chat/agent.ts
CHANGED
package/src/chat/session.ts
CHANGED
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
reopenThread,
|
|
14
14
|
} from "../db/threads.ts";
|
|
15
15
|
import { createMcpxClient } from "../mcpx/client.ts";
|
|
16
|
+
import { loadSkills } from "../skills/loader.ts";
|
|
17
|
+
import type { SkillDefinition } from "../skills/parser.ts";
|
|
16
18
|
import type { ToolContext } from "../tools/tool.ts";
|
|
17
19
|
import { generateThreadTitle } from "../utils/title.ts";
|
|
18
20
|
import {
|
|
@@ -29,6 +31,7 @@ export interface ChatSession {
|
|
|
29
31
|
messages: MessageParam[];
|
|
30
32
|
systemPrompt: string;
|
|
31
33
|
toolCtx: ToolContext;
|
|
34
|
+
skills: Map<string, SkillDefinition>;
|
|
32
35
|
cleanup: () => Promise<void>;
|
|
33
36
|
}
|
|
34
37
|
|
|
@@ -83,6 +86,7 @@ export async function startChatSession(
|
|
|
83
86
|
const systemPrompt = await buildChatSystemPrompt(projectDir);
|
|
84
87
|
|
|
85
88
|
const mcpxClient = await createMcpxClient(projectDir);
|
|
89
|
+
const skills = await loadSkills(projectDir);
|
|
86
90
|
|
|
87
91
|
const toolCtx: ToolContext = {
|
|
88
92
|
conn,
|
|
@@ -103,6 +107,7 @@ export async function startChatSession(
|
|
|
103
107
|
messages,
|
|
104
108
|
systemPrompt,
|
|
105
109
|
toolCtx,
|
|
110
|
+
skills,
|
|
106
111
|
cleanup,
|
|
107
112
|
};
|
|
108
113
|
}
|
package/src/cli.ts
CHANGED
|
@@ -10,9 +10,9 @@ import { registerInitCommand } from "./commands/init.ts";
|
|
|
10
10
|
import { registerMcpxCommand } from "./commands/mcpx.ts";
|
|
11
11
|
import { registerPrepareCommand } from "./commands/prepare.ts";
|
|
12
12
|
import { registerScheduleCommand } from "./commands/schedule.ts";
|
|
13
|
+
import { registerSkillCommand } from "./commands/skill.ts";
|
|
13
14
|
import { registerTaskCommand } from "./commands/task.ts";
|
|
14
15
|
import { registerThreadCommand } from "./commands/thread.ts";
|
|
15
|
-
import { registerToolCommands } from "./commands/tools.ts";
|
|
16
16
|
import { registerUpgradeCommand } from "./commands/upgrade.ts";
|
|
17
17
|
import { maybeCheckForUpdate } from "./update/background.ts";
|
|
18
18
|
|
|
@@ -39,7 +39,7 @@ registerScheduleCommand(program);
|
|
|
39
39
|
registerChatCommand(program);
|
|
40
40
|
registerContextCommand(program);
|
|
41
41
|
registerMcpxCommand(program);
|
|
42
|
-
|
|
42
|
+
registerSkillCommand(program);
|
|
43
43
|
registerPrepareCommand(program);
|
|
44
44
|
registerCheckUpdateCommand(program);
|
|
45
45
|
registerUpgradeCommand(program);
|
package/src/commands/context.ts
CHANGED
|
@@ -17,14 +17,18 @@ import type { DbConnection } from "../db/connection.ts";
|
|
|
17
17
|
import {
|
|
18
18
|
type ContextItem,
|
|
19
19
|
deleteContextItemByPath,
|
|
20
|
-
getContextItemByPath,
|
|
21
20
|
listContextItems,
|
|
22
21
|
listContextItemsByPrefix,
|
|
22
|
+
resolveContextItem,
|
|
23
23
|
updateContextItem,
|
|
24
24
|
upsertContextItem,
|
|
25
25
|
} from "../db/context.ts";
|
|
26
26
|
import { getEmbeddingsForItem, hybridSearch } from "../db/embeddings.ts";
|
|
27
27
|
import { logger } from "../utils/logger.ts";
|
|
28
|
+
import {
|
|
29
|
+
registerContextToolSubcommands,
|
|
30
|
+
registerSearchToolSubcommands,
|
|
31
|
+
} from "./tools.ts";
|
|
28
32
|
import { withDb } from "./with-db.ts";
|
|
29
33
|
|
|
30
34
|
function fmtDate(d: Date): string {
|
|
@@ -33,11 +37,11 @@ function fmtDate(d: Date): string {
|
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
export function registerContextCommand(program: Command) {
|
|
36
|
-
const ctx = program.command("context").description("Manage context
|
|
40
|
+
const ctx = program.command("context").description("Manage context");
|
|
37
41
|
|
|
38
42
|
ctx
|
|
39
43
|
.command("list")
|
|
40
|
-
.description("List context
|
|
44
|
+
.description("List context entries")
|
|
41
45
|
.option("--path <prefix>", "filter by path prefix")
|
|
42
46
|
.option("-l, --limit <n>", "max number of items", Number.parseInt)
|
|
43
47
|
.option("-o, --offset <n>", "skip first N items", Number.parseInt)
|
|
@@ -55,7 +59,7 @@ export function registerContextCommand(program: Command) {
|
|
|
55
59
|
});
|
|
56
60
|
|
|
57
61
|
if (items.length === 0) {
|
|
58
|
-
logger.dim("No context
|
|
62
|
+
logger.dim("No context entries found.");
|
|
59
63
|
return;
|
|
60
64
|
}
|
|
61
65
|
|
|
@@ -80,37 +84,6 @@ export function registerContextCommand(program: Command) {
|
|
|
80
84
|
}),
|
|
81
85
|
);
|
|
82
86
|
|
|
83
|
-
ctx
|
|
84
|
-
.command("show <path>")
|
|
85
|
-
.description("Show details and content of a context item")
|
|
86
|
-
.action((path: string) =>
|
|
87
|
-
withDb(program, async (conn) => {
|
|
88
|
-
const item = await getContextItemByPath(conn, path);
|
|
89
|
-
if (!item) {
|
|
90
|
-
logger.error(`Context item not found: ${path}`);
|
|
91
|
-
process.exit(1);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
console.log(ansis.bold(item.title));
|
|
95
|
-
if (item.description) console.log(` Description: ${item.description}`);
|
|
96
|
-
console.log(` Path: ${item.context_path}`);
|
|
97
|
-
console.log(` MIME type: ${item.mime_type}`);
|
|
98
|
-
if (item.source_path) console.log(` Source: ${item.source_path}`);
|
|
99
|
-
const indexed = item.indexed_at
|
|
100
|
-
? `${ansis.green("yes")} (${fmtDate(item.indexed_at)})`
|
|
101
|
-
: ansis.dim("no");
|
|
102
|
-
console.log(` Indexed: ${indexed}`);
|
|
103
|
-
console.log(` Created: ${fmtDate(item.created_at)}`);
|
|
104
|
-
console.log(` Updated: ${fmtDate(item.updated_at)}`);
|
|
105
|
-
|
|
106
|
-
if (item.is_textual && item.content) {
|
|
107
|
-
console.log(`\n${"─".repeat(60)}\n${item.content}`);
|
|
108
|
-
} else if (!item.is_textual) {
|
|
109
|
-
console.log(ansis.dim("\n (binary content not shown)"));
|
|
110
|
-
}
|
|
111
|
-
}),
|
|
112
|
-
);
|
|
113
|
-
|
|
114
87
|
ctx
|
|
115
88
|
.command("add <paths...>")
|
|
116
89
|
.description("Add files or directories to context")
|
|
@@ -236,12 +209,17 @@ export function registerContextCommand(program: Command) {
|
|
|
236
209
|
}),
|
|
237
210
|
);
|
|
238
211
|
|
|
239
|
-
ctx
|
|
240
|
-
.command("search
|
|
241
|
-
.description("Search context
|
|
212
|
+
const search = ctx
|
|
213
|
+
.command("search")
|
|
214
|
+
.description("Search context entries")
|
|
215
|
+
.argument("[query]", "search query (hybrid keyword + semantic)")
|
|
242
216
|
.option("-k, --top-k <n>", "max results", Number.parseInt, 10)
|
|
243
217
|
.action((query, opts) =>
|
|
244
218
|
withDb(program, async (conn, dir) => {
|
|
219
|
+
if (!query) {
|
|
220
|
+
search.help();
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
245
223
|
const config = await loadConfig(dir);
|
|
246
224
|
const queryVec = await embedSingle(query, config);
|
|
247
225
|
const results = await hybridSearch(conn, query, queryVec, opts.topK);
|
|
@@ -267,27 +245,29 @@ export function registerContextCommand(program: Command) {
|
|
|
267
245
|
}
|
|
268
246
|
}),
|
|
269
247
|
);
|
|
248
|
+
|
|
249
|
+
registerSearchToolSubcommands(search);
|
|
270
250
|
ctx
|
|
271
251
|
.command("delete <path>")
|
|
272
|
-
.description("Delete a context
|
|
252
|
+
.description("Delete a context entry by path")
|
|
273
253
|
.action((path: string) =>
|
|
274
254
|
withDb(program, async (conn) => {
|
|
275
255
|
const deleted = await deleteContextItemByPath(conn, path);
|
|
276
256
|
if (!deleted) {
|
|
277
|
-
logger.error(`Context
|
|
257
|
+
logger.error(`Context entry not found: ${path}`);
|
|
278
258
|
process.exit(1);
|
|
279
259
|
}
|
|
280
|
-
logger.success(`Deleted context
|
|
260
|
+
logger.success(`Deleted context entry: ${path}`);
|
|
281
261
|
}),
|
|
282
262
|
);
|
|
283
263
|
ctx
|
|
284
264
|
.command("chunks <path>")
|
|
285
|
-
.description("Show chunks and embeddings for a context
|
|
265
|
+
.description("Show chunks and embeddings for a context entry")
|
|
286
266
|
.action((path: string) =>
|
|
287
267
|
withDb(program, async (conn) => {
|
|
288
|
-
const item = await
|
|
268
|
+
const item = await resolveContextItem(conn, path);
|
|
289
269
|
if (!item) {
|
|
290
|
-
logger.error(`Context
|
|
270
|
+
logger.error(`Context entry not found: ${path}`);
|
|
291
271
|
process.exit(1);
|
|
292
272
|
}
|
|
293
273
|
|
|
@@ -336,7 +316,7 @@ export function registerContextCommand(program: Command) {
|
|
|
336
316
|
withDb(program, async (conn, dir) => {
|
|
337
317
|
const items = await resolveItems(conn, path, !!opts.all);
|
|
338
318
|
if (items.length === 0) {
|
|
339
|
-
logger.error("No matching context
|
|
319
|
+
logger.error("No matching context entries found.");
|
|
340
320
|
process.exit(1);
|
|
341
321
|
}
|
|
342
322
|
|
|
@@ -436,6 +416,10 @@ export function registerContextCommand(program: Command) {
|
|
|
436
416
|
);
|
|
437
417
|
}),
|
|
438
418
|
);
|
|
419
|
+
|
|
420
|
+
// Register context tool subcommands (read, write, edit, list-dir, etc.)
|
|
421
|
+
// Must come after management subcommands so collision detection works.
|
|
422
|
+
registerContextToolSubcommands(ctx);
|
|
439
423
|
}
|
|
440
424
|
|
|
441
425
|
async function resolveItems(
|
|
@@ -449,12 +433,12 @@ async function resolveItems(
|
|
|
449
433
|
}
|
|
450
434
|
if (all) return listContextItems(conn);
|
|
451
435
|
const p = path as string;
|
|
452
|
-
const exact = await
|
|
436
|
+
const exact = await resolveContextItem(conn, p);
|
|
453
437
|
if (exact) return [exact];
|
|
454
438
|
return listContextItemsByPrefix(conn, p, { recursive: true });
|
|
455
439
|
}
|
|
456
440
|
|
|
457
|
-
/** Upsert a file into
|
|
441
|
+
/** Upsert a file into context. Returns the item ID if textual, null otherwise. */
|
|
458
442
|
async function addFile(
|
|
459
443
|
conn: DbConnection,
|
|
460
444
|
filePath: string,
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import ansis from "ansis";
|
|
3
|
+
import type { Command } from "commander";
|
|
4
|
+
import { getSkillsDir } from "../constants.ts";
|
|
5
|
+
import { loadSkills } from "../skills/loader.ts";
|
|
6
|
+
import { parseSkillFile } from "../skills/parser.ts";
|
|
7
|
+
import { logger } from "../utils/logger.ts";
|
|
8
|
+
|
|
9
|
+
export function registerSkillCommand(program: Command) {
|
|
10
|
+
const skill = program
|
|
11
|
+
.command("skill")
|
|
12
|
+
.description("Manage skill slash-commands");
|
|
13
|
+
|
|
14
|
+
skill
|
|
15
|
+
.command("validate [file]")
|
|
16
|
+
.description("Validate skill files in .botholomew/skills/")
|
|
17
|
+
.action(async (file?: string) => {
|
|
18
|
+
const dir = program.opts().dir;
|
|
19
|
+
|
|
20
|
+
if (file) {
|
|
21
|
+
await validateSingleFile(file);
|
|
22
|
+
} else {
|
|
23
|
+
await validateAllSkills(dir);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
skill
|
|
28
|
+
.command("create <name>")
|
|
29
|
+
.description("Create a new skill file from a template")
|
|
30
|
+
.option("--force", "overwrite existing file")
|
|
31
|
+
.action(async (name: string, opts: { force?: boolean }) => {
|
|
32
|
+
const dir = program.opts().dir;
|
|
33
|
+
const skillsDir = getSkillsDir(dir);
|
|
34
|
+
const normalized = name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
35
|
+
const filePath = join(skillsDir, `${normalized}.md`);
|
|
36
|
+
|
|
37
|
+
if (!opts.force && (await Bun.file(filePath).exists())) {
|
|
38
|
+
logger.error(`Skill file already exists: ${filePath}`);
|
|
39
|
+
logger.dim("Use --force to overwrite.");
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const template = `---
|
|
44
|
+
name: ${normalized}
|
|
45
|
+
description: ""
|
|
46
|
+
arguments: []
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
Your prompt template here. Use $ARGUMENTS for the full argument string
|
|
50
|
+
or $1, $2, etc. for positional arguments.
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
await Bun.write(filePath, template);
|
|
54
|
+
logger.success(`Created skill: ${filePath}`);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function validateSingleFile(filePath: string): Promise<void> {
|
|
59
|
+
try {
|
|
60
|
+
const raw = await Bun.file(filePath).text();
|
|
61
|
+
const skill = parseSkillFile(raw, filePath);
|
|
62
|
+
logger.success(`${ansis.bold(skill.name)} — valid`);
|
|
63
|
+
if (skill.description) logger.dim(` ${skill.description}`);
|
|
64
|
+
if (skill.arguments.length > 0) {
|
|
65
|
+
logger.dim(` ${skill.arguments.length} argument(s)`);
|
|
66
|
+
}
|
|
67
|
+
} catch (e) {
|
|
68
|
+
logger.error(`${filePath}: ${e instanceof Error ? e.message : e}`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function validateAllSkills(dir: string): Promise<void> {
|
|
74
|
+
let hasErrors = false;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const skills = await loadSkills(dir);
|
|
78
|
+
|
|
79
|
+
if (skills.size === 0) {
|
|
80
|
+
logger.dim("No skill files found.");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const [name, skill] of skills) {
|
|
85
|
+
const argCount = skill.arguments.length;
|
|
86
|
+
const args = argCount > 0 ? `${argCount} arg(s)` : "no args";
|
|
87
|
+
logger.success(
|
|
88
|
+
`${ansis.bold(name).padEnd(20)} ${skill.description || ansis.dim("(no description)")} ${ansis.dim(`[${args}]`)}`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
logger.info("");
|
|
93
|
+
logger.dim(`${skills.size} skill(s) validated.`);
|
|
94
|
+
} catch (e) {
|
|
95
|
+
logger.error(`Validation failed: ${e instanceof Error ? e.message : e}`);
|
|
96
|
+
hasErrors = true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (hasErrors) process.exit(1);
|
|
100
|
+
}
|
package/src/commands/tools.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import ansis from "ansis";
|
|
1
2
|
import type { Command } from "commander";
|
|
2
3
|
import { z } from "zod";
|
|
3
|
-
import
|
|
4
|
-
import { DEFAULT_CONFIG } from "../config/schemas.ts";
|
|
4
|
+
import { loadConfig } from "../config/loader.ts";
|
|
5
5
|
import { registerAllTools } from "../tools/registry.ts";
|
|
6
6
|
import {
|
|
7
7
|
type AnyToolDefinition,
|
|
@@ -13,31 +13,42 @@ import { withDb } from "./with-db.ts";
|
|
|
13
13
|
|
|
14
14
|
registerAllTools();
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Register context tool subcommands (read, write, edit, etc.) onto an
|
|
18
|
+
* existing Commander command. Skips tools whose derived subcommand name
|
|
19
|
+
* collides with an already-registered subcommand on the parent.
|
|
20
|
+
*/
|
|
21
|
+
/** Context tools that are agent-only (not exposed as CLI subcommands) */
|
|
22
|
+
const AGENT_ONLY_TOOLS = new Set(["update_beliefs", "update_goals"]);
|
|
23
|
+
|
|
24
|
+
export function registerContextToolSubcommands(parent: Command) {
|
|
25
|
+
const existing = new Set(parent.commands.map((c: Command) => c.name()));
|
|
26
|
+
|
|
27
|
+
for (const tool of getToolsByGroup("context")) {
|
|
28
|
+
if (AGENT_ONLY_TOOLS.has(tool.name)) continue;
|
|
29
|
+
const subName = deriveSubName(tool.name);
|
|
30
|
+
if (existing.has(subName)) continue; // skip conflicts with management subcommands
|
|
31
|
+
registerToolAsCLI(parent, tool);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
27
34
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Register search tool subcommands (grep, semantic) onto an
|
|
37
|
+
* existing Commander command (e.g. the "context search" group).
|
|
38
|
+
*/
|
|
39
|
+
export function registerSearchToolSubcommands(parent: Command) {
|
|
40
|
+
for (const tool of getToolsByGroup("search")) {
|
|
41
|
+
registerToolAsCLI(parent, tool);
|
|
31
42
|
}
|
|
32
43
|
}
|
|
33
44
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const subName = tool.name
|
|
45
|
+
/** Derive CLI subcommand name from tool name: "context_read" → "read", "context_list_dir" → "list-dir" */
|
|
46
|
+
function deriveSubName(toolName: string): string {
|
|
47
|
+
return toolName.replace(/^[^_]+_/, "").replace(/_/g, "-");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function registerToolAsCLI(parent: Command, tool: AnyToolDefinition) {
|
|
51
|
+
const subName = deriveSubName(tool.name);
|
|
41
52
|
|
|
42
53
|
// Inspect zod schema to determine positional args and options
|
|
43
54
|
const shape = tool.inputSchema.shape as Record<string, z.ZodType>;
|
|
@@ -92,15 +103,17 @@ function registerToolAsCLI(
|
|
|
92
103
|
}
|
|
93
104
|
}
|
|
94
105
|
|
|
95
|
-
cmd.action((...args: unknown[]) =>
|
|
96
|
-
|
|
106
|
+
cmd.action((...args: unknown[]) => {
|
|
107
|
+
let root: Command = parent;
|
|
108
|
+
while (root.parent) root = root.parent;
|
|
109
|
+
return withDb(root, async (conn, dir) => {
|
|
97
110
|
try {
|
|
98
111
|
const input = buildInput(tool, positionals, options, shape, args);
|
|
99
112
|
|
|
100
113
|
const ctx: ToolContext = {
|
|
101
114
|
conn,
|
|
102
115
|
projectDir: dir,
|
|
103
|
-
config:
|
|
116
|
+
config: await loadConfig(dir),
|
|
104
117
|
mcpxClient: null,
|
|
105
118
|
};
|
|
106
119
|
|
|
@@ -110,8 +123,8 @@ function registerToolAsCLI(
|
|
|
110
123
|
logger.error(String(err));
|
|
111
124
|
process.exit(1);
|
|
112
125
|
}
|
|
113
|
-
})
|
|
114
|
-
);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
115
128
|
}
|
|
116
129
|
|
|
117
130
|
function buildInput(
|
|
@@ -214,6 +227,29 @@ function formatOutput(result: unknown, _toolName: string) {
|
|
|
214
227
|
return;
|
|
215
228
|
}
|
|
216
229
|
|
|
230
|
+
if ("results" in obj && Array.isArray(obj.results)) {
|
|
231
|
+
for (const [i, r] of (
|
|
232
|
+
obj.results as {
|
|
233
|
+
path: string;
|
|
234
|
+
title: string;
|
|
235
|
+
score: number;
|
|
236
|
+
snippet: string;
|
|
237
|
+
}[]
|
|
238
|
+
).entries()) {
|
|
239
|
+
const score = (r.score * 100).toFixed(1);
|
|
240
|
+
console.log(
|
|
241
|
+
`${ansis.bold(`${i + 1}.`)} ${ansis.cyan(r.title)} ${ansis.dim(`(${score}%)`)}`,
|
|
242
|
+
);
|
|
243
|
+
console.log(` ${ansis.dim(r.path)}`);
|
|
244
|
+
if (r.snippet) {
|
|
245
|
+
const snippet = r.snippet.slice(0, 120).replace(/\n/g, " ");
|
|
246
|
+
console.log(` ${snippet}...`);
|
|
247
|
+
}
|
|
248
|
+
console.log("");
|
|
249
|
+
}
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
217
253
|
// Default: print as JSON
|
|
218
254
|
console.log(JSON.stringify(obj, null, 2));
|
|
219
255
|
} else {
|
|
@@ -224,20 +260,20 @@ function formatOutput(result: unknown, _toolName: string) {
|
|
|
224
260
|
function isPositionalArg(key: string, toolName: string): boolean {
|
|
225
261
|
// These keys are treated as positional arguments
|
|
226
262
|
const positionalKeys: Record<string, string[]> = {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
263
|
+
context_create_dir: ["path"],
|
|
264
|
+
context_list_dir: ["path"],
|
|
265
|
+
context_tree: ["path"],
|
|
266
|
+
context_dir_size: ["path"],
|
|
267
|
+
context_read: ["path"],
|
|
268
|
+
context_write: ["path"],
|
|
269
|
+
context_edit: ["path"],
|
|
270
|
+
context_delete: ["path"],
|
|
271
|
+
context_copy: ["src", "dst"],
|
|
272
|
+
context_move: ["src", "dst"],
|
|
273
|
+
context_info: ["path"],
|
|
274
|
+
context_exists: ["path"],
|
|
275
|
+
context_count_lines: ["path"],
|
|
276
|
+
context_search: ["query"],
|
|
241
277
|
search_grep: ["pattern"],
|
|
242
278
|
search_semantic: ["query"],
|
|
243
279
|
};
|
package/src/constants.ts
CHANGED
|
@@ -17,6 +17,7 @@ export const PID_FILENAME = "daemon.pid";
|
|
|
17
17
|
export const LOG_FILENAME = "daemon.log";
|
|
18
18
|
export const CONFIG_FILENAME = "config.json";
|
|
19
19
|
export const MCPX_DIR = "mcpx";
|
|
20
|
+
export const SKILLS_DIR = "skills";
|
|
20
21
|
export const MCPX_SERVERS_FILENAME = "servers.json";
|
|
21
22
|
export const EMBEDDING_DIMENSION = 1536;
|
|
22
23
|
export const EMBEDDING_MODEL = "text-embedding-3-small";
|
|
@@ -50,6 +51,10 @@ export function getMcpxDir(projectDir: string): string {
|
|
|
50
51
|
return join(projectDir, BOTHOLOMEW_DIR, MCPX_DIR);
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
export function getSkillsDir(projectDir: string): string {
|
|
55
|
+
return join(projectDir, BOTHOLOMEW_DIR, SKILLS_DIR);
|
|
56
|
+
}
|
|
57
|
+
|
|
53
58
|
export function getWatchdogLogPath(projectDir: string): string {
|
|
54
59
|
return join(projectDir, BOTHOLOMEW_DIR, WATCHDOG_LOG_FILENAME);
|
|
55
60
|
}
|
package/src/db/context.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { DbConnection } from "./connection.ts";
|
|
2
2
|
import { buildSetClauses, buildWhereClause, sanitizeInt } from "./query.ts";
|
|
3
|
-
import { uuidv7 } from "./uuid.ts";
|
|
3
|
+
import { isUuid, uuidv7 } from "./uuid.ts";
|
|
4
4
|
|
|
5
5
|
export interface ContextItem {
|
|
6
6
|
id: string;
|
|
@@ -90,7 +90,7 @@ export async function createContextItem(
|
|
|
90
90
|
*
|
|
91
91
|
* DuckDB implements UPDATE as delete+insert on tables with unique indexes,
|
|
92
92
|
* which violates foreign keys from the embeddings table. We must delete
|
|
93
|
-
* embeddings before updating; callers (context add,
|
|
93
|
+
* embeddings before updating; callers (context add, context_write) re-create
|
|
94
94
|
* them in their ingestion phase.
|
|
95
95
|
*/
|
|
96
96
|
export async function upsertContextItem(
|
|
@@ -140,6 +140,30 @@ export async function getContextItemByPath(
|
|
|
140
140
|
return row ? rowToContextItem(row) : null;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Look up a context item by UUID (if the value looks like one) or by context_path.
|
|
145
|
+
*/
|
|
146
|
+
export async function resolveContextItem(
|
|
147
|
+
db: DbConnection,
|
|
148
|
+
pathOrId: string,
|
|
149
|
+
): Promise<ContextItem | null> {
|
|
150
|
+
return isUuid(pathOrId)
|
|
151
|
+
? getContextItem(db, pathOrId)
|
|
152
|
+
: getContextItemByPath(db, pathOrId);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Like resolveContextItem but throws if not found.
|
|
157
|
+
*/
|
|
158
|
+
export async function resolveContextItemOrThrow(
|
|
159
|
+
db: DbConnection,
|
|
160
|
+
pathOrId: string,
|
|
161
|
+
): Promise<ContextItem> {
|
|
162
|
+
const item = await resolveContextItem(db, pathOrId);
|
|
163
|
+
if (!item) throw new Error(`Not found: ${pathOrId}`);
|
|
164
|
+
return item;
|
|
165
|
+
}
|
|
166
|
+
|
|
143
167
|
export async function listContextItems(
|
|
144
168
|
db: DbConnection,
|
|
145
169
|
filters?: {
|
|
@@ -207,6 +231,29 @@ export async function contextPathExists(
|
|
|
207
231
|
return row != null;
|
|
208
232
|
}
|
|
209
233
|
|
|
234
|
+
export async function countContextItemsByPrefix(
|
|
235
|
+
db: DbConnection,
|
|
236
|
+
prefix: string,
|
|
237
|
+
opts?: { recursive?: boolean },
|
|
238
|
+
): Promise<number> {
|
|
239
|
+
const normalizedPrefix = prefix.endsWith("/") ? prefix : `${prefix}/`;
|
|
240
|
+
let row: { cnt: number } | null;
|
|
241
|
+
if (opts?.recursive !== false) {
|
|
242
|
+
row = await db.queryGet<{ cnt: number }>(
|
|
243
|
+
`SELECT COUNT(*) AS cnt FROM context_items WHERE context_path LIKE ?1`,
|
|
244
|
+
`${normalizedPrefix}%`,
|
|
245
|
+
);
|
|
246
|
+
} else {
|
|
247
|
+
row = await db.queryGet<{ cnt: number }>(
|
|
248
|
+
`SELECT COUNT(*) AS cnt FROM context_items
|
|
249
|
+
WHERE context_path LIKE ?1 AND context_path NOT LIKE ?2`,
|
|
250
|
+
`${normalizedPrefix}%`,
|
|
251
|
+
`${normalizedPrefix}%/%`,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
return row ? Number(row.cnt) : 0;
|
|
255
|
+
}
|
|
256
|
+
|
|
210
257
|
export async function getDistinctDirectories(
|
|
211
258
|
db: DbConnection,
|
|
212
259
|
prefix?: string,
|
package/src/db/uuid.ts
CHANGED
package/src/init/index.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { mkdir } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
getBotholomewDir,
|
|
5
|
+
getDbPath,
|
|
6
|
+
getMcpxDir,
|
|
7
|
+
getSkillsDir,
|
|
8
|
+
} from "../constants.ts";
|
|
4
9
|
import { getConnection } from "../db/connection.ts";
|
|
5
10
|
import { migrate } from "../db/schema.ts";
|
|
6
11
|
import { logger } from "../utils/logger.ts";
|
|
@@ -10,6 +15,8 @@ import {
|
|
|
10
15
|
DEFAULT_MCPX_SERVERS,
|
|
11
16
|
GOALS_MD,
|
|
12
17
|
SOUL_MD,
|
|
18
|
+
STANDUP_SKILL,
|
|
19
|
+
SUMMARIZE_SKILL,
|
|
13
20
|
} from "./templates.ts";
|
|
14
21
|
|
|
15
22
|
export async function initProject(
|
|
@@ -18,6 +25,7 @@ export async function initProject(
|
|
|
18
25
|
): Promise<void> {
|
|
19
26
|
const dotDir = getBotholomewDir(projectDir);
|
|
20
27
|
const mcpxDir = getMcpxDir(projectDir);
|
|
28
|
+
const skillsDir = getSkillsDir(projectDir);
|
|
21
29
|
|
|
22
30
|
// Check if already initialized
|
|
23
31
|
const dirExists = await Bun.file(join(dotDir, "soul.md")).exists();
|
|
@@ -30,12 +38,17 @@ export async function initProject(
|
|
|
30
38
|
// Create directories
|
|
31
39
|
await mkdir(dotDir, { recursive: true });
|
|
32
40
|
await mkdir(mcpxDir, { recursive: true });
|
|
41
|
+
await mkdir(skillsDir, { recursive: true });
|
|
33
42
|
|
|
34
43
|
// Write template files
|
|
35
44
|
await Bun.write(join(dotDir, "soul.md"), SOUL_MD);
|
|
36
45
|
await Bun.write(join(dotDir, "beliefs.md"), BELIEFS_MD);
|
|
37
46
|
await Bun.write(join(dotDir, "goals.md"), GOALS_MD);
|
|
38
47
|
|
|
48
|
+
// Write default skills
|
|
49
|
+
await Bun.write(join(skillsDir, "summarize.md"), SUMMARIZE_SKILL);
|
|
50
|
+
await Bun.write(join(skillsDir, "standup.md"), STANDUP_SKILL);
|
|
51
|
+
|
|
39
52
|
// Write config (with placeholder API key)
|
|
40
53
|
await Bun.write(
|
|
41
54
|
join(dotDir, "config.json"),
|