botholomew 0.3.0 → 0.3.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 +9 -0
- package/package.json +3 -1
- package/src/chat/agent.ts +87 -23
- package/src/chat/session.ts +19 -6
- package/src/cli.ts +2 -0
- package/src/commands/chat.ts +5 -2
- package/src/commands/context.ts +91 -35
- package/src/commands/thread.ts +180 -0
- package/src/config/schemas.ts +3 -1
- package/src/context/embedder.ts +0 -3
- package/src/daemon/context.ts +146 -0
- package/src/daemon/large-results.ts +100 -0
- package/src/daemon/llm.ts +45 -19
- package/src/daemon/prompt.ts +1 -6
- package/src/daemon/tick.ts +9 -0
- package/src/db/sql/4-unique_context_path.sql +1 -0
- package/src/db/threads.ts +17 -0
- package/src/init/templates.ts +2 -1
- package/src/tools/context/read-large-result.ts +33 -0
- package/src/tools/context/search.ts +2 -0
- package/src/tools/context/update-beliefs.ts +2 -0
- package/src/tools/context/update-goals.ts +2 -0
- package/src/tools/dir/create.ts +3 -2
- package/src/tools/dir/list.ts +2 -1
- package/src/tools/dir/size.ts +2 -1
- package/src/tools/dir/tree.ts +3 -2
- package/src/tools/file/copy.ts +12 -3
- package/src/tools/file/count-lines.ts +2 -1
- package/src/tools/file/delete.ts +3 -2
- package/src/tools/file/edit.ts +3 -2
- package/src/tools/file/exists.ts +2 -1
- package/src/tools/file/info.ts +2 -0
- package/src/tools/file/move.ts +12 -3
- package/src/tools/file/read.ts +2 -1
- package/src/tools/file/write.ts +5 -4
- package/src/tools/mcp/exec.ts +70 -3
- package/src/tools/mcp/info.ts +8 -0
- package/src/tools/mcp/list-tools.ts +18 -6
- package/src/tools/mcp/search.ts +38 -10
- package/src/tools/registry.ts +4 -0
- package/src/tools/schedule/create.ts +2 -0
- package/src/tools/schedule/list.ts +2 -0
- package/src/tools/search/grep.ts +3 -2
- package/src/tools/search/semantic.ts +2 -0
- package/src/tools/task/complete.ts +2 -0
- package/src/tools/task/create.ts +17 -4
- package/src/tools/task/fail.ts +2 -0
- package/src/tools/task/list.ts +2 -0
- package/src/tools/task/update.ts +87 -0
- package/src/tools/task/view.ts +3 -1
- package/src/tools/task/wait.ts +2 -0
- package/src/tools/thread/list.ts +2 -0
- package/src/tools/thread/view.ts +3 -1
- package/src/tools/tool.ts +7 -3
- package/src/tui/App.tsx +323 -78
- package/src/tui/components/ContextPanel.tsx +415 -0
- package/src/tui/components/Divider.tsx +14 -0
- package/src/tui/components/HelpPanel.tsx +166 -0
- package/src/tui/components/InputBar.tsx +157 -47
- package/src/tui/components/Logo.tsx +79 -0
- package/src/tui/components/MessageList.tsx +50 -23
- package/src/tui/components/QueuePanel.tsx +57 -0
- package/src/tui/components/StatusBar.tsx +21 -9
- package/src/tui/components/TabBar.tsx +40 -0
- package/src/tui/components/TaskPanel.tsx +409 -0
- package/src/tui/components/ThreadPanel.tsx +541 -0
- package/src/tui/components/ToolCall.tsx +68 -5
- package/src/tui/components/ToolPanel.tsx +295 -281
- package/src/tui/theme.ts +75 -0
- package/src/utils/title.ts +47 -0
package/README.md
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "botholomew",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "An AI agent for knowledge work",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
19
|
"dev": "bun run src/cli.ts",
|
|
20
|
+
"dev:demo": "bun run src/cli.ts chat -p 'learn everything you can about me from the connected MCP services and then save what you'\\''ve learned about me to context'",
|
|
20
21
|
"test": "bun test",
|
|
21
22
|
"build": "bun build --compile --minify --sourcemap --external react-devtools-core ./src/cli.ts --outfile dist/botholomew",
|
|
22
23
|
"lint": "tsc --noEmit && biome check ."
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
"ink-spinner": "^5.0.0",
|
|
34
35
|
"ink-text-input": "^6.0.0",
|
|
35
36
|
"istextorbinary": "^9.5.0",
|
|
37
|
+
"nanospinner": "^1.2.2",
|
|
36
38
|
"react": "^19.1.0",
|
|
37
39
|
"uuid": "^13.0.0",
|
|
38
40
|
"zod": "^4.3.6"
|
package/src/chat/agent.ts
CHANGED
|
@@ -5,6 +5,8 @@ import type {
|
|
|
5
5
|
ToolUseBlock,
|
|
6
6
|
} from "@anthropic-ai/sdk/resources/messages";
|
|
7
7
|
import type { BotholomewConfig } from "../config/schemas.ts";
|
|
8
|
+
import { fitToContextWindow, getMaxInputTokens } from "../daemon/context.ts";
|
|
9
|
+
import { maybeStoreResult } from "../daemon/large-results.ts";
|
|
8
10
|
import { buildMetaHeader, loadPersistentContext } from "../daemon/prompt.ts";
|
|
9
11
|
import type { DbConnection } from "../db/connection.ts";
|
|
10
12
|
import { logInteraction } from "../db/threads.ts";
|
|
@@ -36,6 +38,7 @@ const CHAT_TOOL_NAMES = new Set([
|
|
|
36
38
|
"mcp_search",
|
|
37
39
|
"mcp_info",
|
|
38
40
|
"mcp_exec",
|
|
41
|
+
"read_large_result",
|
|
39
42
|
]);
|
|
40
43
|
|
|
41
44
|
export function getChatTools() {
|
|
@@ -54,7 +57,7 @@ export async function buildChatSystemPrompt(
|
|
|
54
57
|
|
|
55
58
|
parts.push("## Instructions");
|
|
56
59
|
parts.push(
|
|
57
|
-
"You are Botholomew
|
|
60
|
+
"You are Botholomew, an AI agent personified by a wise owl. This is your interactive chat interface. Help the user manage tasks, review results from daemon activity, search context, and answer questions.",
|
|
58
61
|
);
|
|
59
62
|
parts.push(
|
|
60
63
|
"You do NOT execute long-running work directly — enqueue tasks for the daemon instead using create_task.",
|
|
@@ -62,6 +65,9 @@ export async function buildChatSystemPrompt(
|
|
|
62
65
|
parts.push(
|
|
63
66
|
"Use the available tools to look up tasks, threads, schedules, and context when the user asks about them.",
|
|
64
67
|
);
|
|
68
|
+
parts.push(
|
|
69
|
+
"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.",
|
|
70
|
+
);
|
|
65
71
|
parts.push(
|
|
66
72
|
"You can update the agent's beliefs and goals files when the user asks you to.",
|
|
67
73
|
);
|
|
@@ -73,10 +79,20 @@ export async function buildChatSystemPrompt(
|
|
|
73
79
|
return parts.join("\n");
|
|
74
80
|
}
|
|
75
81
|
|
|
82
|
+
export interface ToolEndMeta {
|
|
83
|
+
largeResult?: { id: string; chars: number; pages: number };
|
|
84
|
+
}
|
|
85
|
+
|
|
76
86
|
export interface ChatTurnCallbacks {
|
|
77
87
|
onToken: (text: string) => void;
|
|
78
|
-
onToolStart: (name: string, input: string) => void;
|
|
79
|
-
onToolEnd: (
|
|
88
|
+
onToolStart: (id: string, name: string, input: string) => void;
|
|
89
|
+
onToolEnd: (
|
|
90
|
+
id: string,
|
|
91
|
+
name: string,
|
|
92
|
+
output: string,
|
|
93
|
+
isError: boolean,
|
|
94
|
+
meta?: ToolEndMeta,
|
|
95
|
+
) => void;
|
|
80
96
|
}
|
|
81
97
|
|
|
82
98
|
/**
|
|
@@ -101,11 +117,16 @@ export async function runChatTurn(input: {
|
|
|
101
117
|
});
|
|
102
118
|
|
|
103
119
|
const chatTools = getChatTools();
|
|
104
|
-
const
|
|
120
|
+
const maxInputTokens = await getMaxInputTokens(
|
|
121
|
+
config.anthropic_api_key,
|
|
122
|
+
config.model,
|
|
123
|
+
);
|
|
124
|
+
const maxTurns = config.max_turns;
|
|
105
125
|
|
|
106
|
-
for (let turn = 0; turn < maxTurns; turn++) {
|
|
126
|
+
for (let turn = 0; !maxTurns || turn < maxTurns; turn++) {
|
|
107
127
|
const startTime = Date.now();
|
|
108
128
|
|
|
129
|
+
fitToContextWindow(messages, systemPrompt, maxInputTokens);
|
|
109
130
|
const stream = client.messages.stream({
|
|
110
131
|
model: config.model,
|
|
111
132
|
max_tokens: 4096,
|
|
@@ -116,12 +137,24 @@ export async function runChatTurn(input: {
|
|
|
116
137
|
|
|
117
138
|
// Collect the full response
|
|
118
139
|
let assistantText = "";
|
|
140
|
+
const earlyReportedToolIds = new Set<string>();
|
|
119
141
|
|
|
120
142
|
stream.on("text", (text) => {
|
|
121
143
|
assistantText += text;
|
|
122
144
|
callbacks.onToken(text);
|
|
123
145
|
});
|
|
124
146
|
|
|
147
|
+
stream.on("contentBlock", (block) => {
|
|
148
|
+
if (block.type === "tool_use") {
|
|
149
|
+
earlyReportedToolIds.add(block.id);
|
|
150
|
+
callbacks.onToolStart(
|
|
151
|
+
block.id,
|
|
152
|
+
block.name,
|
|
153
|
+
JSON.stringify(block.input),
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
125
158
|
const response = await stream.finalMessage();
|
|
126
159
|
const durationMs = Date.now() - startTime;
|
|
127
160
|
const tokenCount =
|
|
@@ -152,12 +185,12 @@ export async function runChatTurn(input: {
|
|
|
152
185
|
// Add assistant response to conversation
|
|
153
186
|
messages.push({ role: "assistant", content: response.content });
|
|
154
187
|
|
|
155
|
-
//
|
|
156
|
-
const toolResults: ToolResultBlockParam[] = [];
|
|
157
|
-
|
|
188
|
+
// Log all tool_use entries and notify UI
|
|
158
189
|
for (const toolUse of toolUseBlocks) {
|
|
159
190
|
const toolInput = JSON.stringify(toolUse.input);
|
|
160
|
-
|
|
191
|
+
if (!earlyReportedToolIds.has(toolUse.id)) {
|
|
192
|
+
callbacks.onToolStart(toolUse.id, toolUse.name, toolInput);
|
|
193
|
+
}
|
|
161
194
|
|
|
162
195
|
await logInteraction(conn, threadId, {
|
|
163
196
|
role: "assistant",
|
|
@@ -166,25 +199,45 @@ export async function runChatTurn(input: {
|
|
|
166
199
|
toolName: toolUse.name,
|
|
167
200
|
toolInput,
|
|
168
201
|
});
|
|
202
|
+
}
|
|
169
203
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
204
|
+
// Execute all tools in parallel
|
|
205
|
+
const execResults = await Promise.all(
|
|
206
|
+
toolUseBlocks.map(async (toolUse) => {
|
|
207
|
+
const start = Date.now();
|
|
208
|
+
const result = await executeChatToolCall(toolUse, toolCtx);
|
|
209
|
+
const durationMs = Date.now() - start;
|
|
210
|
+
const stored = maybeStoreResult(toolUse.name, result.output);
|
|
211
|
+
const meta: ToolEndMeta | undefined = stored.stored
|
|
212
|
+
? { largeResult: stored.stored }
|
|
213
|
+
: undefined;
|
|
214
|
+
callbacks.onToolEnd(
|
|
215
|
+
toolUse.id,
|
|
216
|
+
toolUse.name,
|
|
217
|
+
result.output,
|
|
218
|
+
result.isError,
|
|
219
|
+
meta,
|
|
220
|
+
);
|
|
221
|
+
return { toolUse, result, durationMs, stored };
|
|
222
|
+
}),
|
|
223
|
+
);
|
|
173
224
|
|
|
225
|
+
// Log results and collect tool_result messages
|
|
226
|
+
const toolResults: ToolResultBlockParam[] = [];
|
|
227
|
+
for (const { toolUse, result, durationMs, stored } of execResults) {
|
|
174
228
|
await logInteraction(conn, threadId, {
|
|
175
229
|
role: "tool",
|
|
176
230
|
kind: "tool_result",
|
|
177
|
-
content: result,
|
|
231
|
+
content: result.output,
|
|
178
232
|
toolName: toolUse.name,
|
|
179
|
-
durationMs
|
|
233
|
+
durationMs,
|
|
180
234
|
});
|
|
181
235
|
|
|
182
|
-
callbacks.onToolEnd(toolUse.name, result);
|
|
183
|
-
|
|
184
236
|
toolResults.push({
|
|
185
237
|
type: "tool_result",
|
|
186
238
|
tool_use_id: toolUse.id,
|
|
187
|
-
content:
|
|
239
|
+
content: stored.text,
|
|
240
|
+
is_error: result.isError || undefined,
|
|
188
241
|
});
|
|
189
242
|
}
|
|
190
243
|
|
|
@@ -196,21 +249,32 @@ export async function runChatTurn(input: {
|
|
|
196
249
|
async function executeChatToolCall(
|
|
197
250
|
toolUse: ToolUseBlock,
|
|
198
251
|
ctx: ToolContext,
|
|
199
|
-
): Promise<string> {
|
|
252
|
+
): Promise<{ output: string; isError: boolean }> {
|
|
200
253
|
const tool = getTool(toolUse.name);
|
|
201
|
-
if (!tool) return `Unknown tool: ${toolUse.name}
|
|
254
|
+
if (!tool) return { output: `Unknown tool: ${toolUse.name}`, isError: true };
|
|
202
255
|
if (!CHAT_TOOL_NAMES.has(tool.name))
|
|
203
|
-
return
|
|
256
|
+
return {
|
|
257
|
+
output: `Tool not available in chat mode: ${tool.name}`,
|
|
258
|
+
isError: true,
|
|
259
|
+
};
|
|
204
260
|
|
|
205
261
|
const parsed = tool.inputSchema.safeParse(toolUse.input);
|
|
206
262
|
if (!parsed.success) {
|
|
207
|
-
return
|
|
263
|
+
return {
|
|
264
|
+
output: `Invalid input: ${JSON.stringify(parsed.error)}`,
|
|
265
|
+
isError: true,
|
|
266
|
+
};
|
|
208
267
|
}
|
|
209
268
|
|
|
210
269
|
try {
|
|
211
270
|
const result = await tool.execute(parsed.data, ctx);
|
|
212
|
-
|
|
271
|
+
const isError =
|
|
272
|
+
typeof result === "object" && result !== null && "is_error" in result
|
|
273
|
+
? (result as { is_error: boolean }).is_error
|
|
274
|
+
: false;
|
|
275
|
+
const output = typeof result === "string" ? result : JSON.stringify(result);
|
|
276
|
+
return { output, isError };
|
|
213
277
|
} catch (err) {
|
|
214
|
-
return `Tool error: ${err}
|
|
278
|
+
return { output: `Tool error: ${err}`, isError: true };
|
|
215
279
|
}
|
|
216
280
|
}
|
package/src/chat/session.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "../db/threads.ts";
|
|
15
15
|
import { createMcpxClient } from "../mcpx/client.ts";
|
|
16
16
|
import type { ToolContext } from "../tools/tool.ts";
|
|
17
|
+
import { generateThreadTitle } from "../utils/title.ts";
|
|
17
18
|
import {
|
|
18
19
|
buildChatSystemPrompt,
|
|
19
20
|
type ChatTurnCallbacks,
|
|
@@ -60,21 +61,23 @@ export async function startChatSession(
|
|
|
60
61
|
await reopenThread(conn, threadId);
|
|
61
62
|
|
|
62
63
|
// Rebuild message history from interactions
|
|
64
|
+
let firstUserMessage: string | undefined;
|
|
63
65
|
for (const interaction of result.interactions) {
|
|
64
66
|
if (interaction.kind !== "message") continue;
|
|
65
67
|
if (interaction.role === "user") {
|
|
68
|
+
if (!firstUserMessage) firstUserMessage = interaction.content;
|
|
66
69
|
messages.push({ role: "user", content: interaction.content });
|
|
67
70
|
} else if (interaction.role === "assistant") {
|
|
68
71
|
messages.push({ role: "assistant", content: interaction.content });
|
|
69
72
|
}
|
|
70
73
|
}
|
|
74
|
+
|
|
75
|
+
// Backfill title for threads that still have the default
|
|
76
|
+
if (result.thread.title === "New chat" && firstUserMessage) {
|
|
77
|
+
void generateThreadTitle(config, conn, threadId, firstUserMessage);
|
|
78
|
+
}
|
|
71
79
|
} else {
|
|
72
|
-
threadId = await createThread(
|
|
73
|
-
conn,
|
|
74
|
-
"chat_session",
|
|
75
|
-
undefined,
|
|
76
|
-
"Interactive chat",
|
|
77
|
-
);
|
|
80
|
+
threadId = await createThread(conn, "chat_session", undefined, "New chat");
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
const systemPrompt = await buildChatSystemPrompt(projectDir);
|
|
@@ -118,6 +121,16 @@ export async function sendMessage(
|
|
|
118
121
|
|
|
119
122
|
session.messages.push({ role: "user", content: userMessage });
|
|
120
123
|
|
|
124
|
+
// Auto-generate title after first user message in a new thread
|
|
125
|
+
if (session.messages.length === 1) {
|
|
126
|
+
void generateThreadTitle(
|
|
127
|
+
session.config,
|
|
128
|
+
session.conn,
|
|
129
|
+
session.threadId,
|
|
130
|
+
userMessage,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
121
134
|
await runChatTurn({
|
|
122
135
|
messages: session.messages,
|
|
123
136
|
systemPrompt: session.systemPrompt,
|
package/src/cli.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { registerMcpxCommand } from "./commands/mcpx.ts";
|
|
|
11
11
|
import { registerPrepareCommand } from "./commands/prepare.ts";
|
|
12
12
|
import { registerScheduleCommand } from "./commands/schedule.ts";
|
|
13
13
|
import { registerTaskCommand } from "./commands/task.ts";
|
|
14
|
+
import { registerThreadCommand } from "./commands/thread.ts";
|
|
14
15
|
import { registerToolCommands } from "./commands/tools.ts";
|
|
15
16
|
import { registerUpgradeCommand } from "./commands/upgrade.ts";
|
|
16
17
|
import { maybeCheckForUpdate } from "./update/background.ts";
|
|
@@ -33,6 +34,7 @@ program
|
|
|
33
34
|
registerInitCommand(program);
|
|
34
35
|
registerDaemonCommand(program);
|
|
35
36
|
registerTaskCommand(program);
|
|
37
|
+
registerThreadCommand(program);
|
|
36
38
|
registerScheduleCommand(program);
|
|
37
39
|
registerChatCommand(program);
|
|
38
40
|
registerContextCommand(program);
|
package/src/commands/chat.ts
CHANGED
|
@@ -15,7 +15,8 @@ export function registerChatCommand(program: Command) {
|
|
|
15
15
|
" /quit, /exit End the chat session",
|
|
16
16
|
)
|
|
17
17
|
.option("--thread-id <id>", "Resume an existing chat thread")
|
|
18
|
-
.
|
|
18
|
+
.option("-p, --prompt <text>", "Start chat with an initial prompt")
|
|
19
|
+
.action(async (opts: { threadId?: string; prompt?: string }) => {
|
|
19
20
|
const { render } = await import("ink");
|
|
20
21
|
const React = await import("react");
|
|
21
22
|
const { App } = await import("../tui/App.tsx");
|
|
@@ -24,11 +25,13 @@ export function registerChatCommand(program: Command) {
|
|
|
24
25
|
React.createElement(App, {
|
|
25
26
|
projectDir: dir,
|
|
26
27
|
threadId: opts.threadId,
|
|
28
|
+
initialPrompt: opts.prompt,
|
|
27
29
|
}),
|
|
28
30
|
{
|
|
31
|
+
exitOnCtrlC: false,
|
|
29
32
|
kittyKeyboard: {
|
|
30
33
|
mode: "enabled",
|
|
31
|
-
flags: ["disambiguateEscapeCodes"
|
|
34
|
+
flags: ["disambiguateEscapeCodes"],
|
|
32
35
|
},
|
|
33
36
|
},
|
|
34
37
|
);
|
package/src/commands/context.ts
CHANGED
|
@@ -3,14 +3,19 @@ import { basename, join, resolve } from "node:path";
|
|
|
3
3
|
import ansis from "ansis";
|
|
4
4
|
import type { Command } from "commander";
|
|
5
5
|
import { isText } from "istextorbinary";
|
|
6
|
+
import { createSpinner } from "nanospinner";
|
|
6
7
|
import { loadConfig } from "../config/loader.ts";
|
|
7
8
|
import { embedSingle, warmupEmbedder } from "../context/embedder.ts";
|
|
8
9
|
import { ingestContextItem } from "../context/ingest.ts";
|
|
9
10
|
import type { DbConnection } from "../db/connection.ts";
|
|
10
11
|
import {
|
|
12
|
+
type ContextItem,
|
|
11
13
|
createContextItem,
|
|
14
|
+
deleteContextItemByPath,
|
|
15
|
+
getContextItemByPath,
|
|
12
16
|
listContextItems,
|
|
13
17
|
listContextItemsByPrefix,
|
|
18
|
+
updateContextItem,
|
|
14
19
|
} from "../db/context.ts";
|
|
15
20
|
import { hybridSearch, initVectorSearch } from "../db/embeddings.ts";
|
|
16
21
|
import { logger } from "../utils/logger.ts";
|
|
@@ -67,43 +72,71 @@ export function registerContextCommand(program: Command) {
|
|
|
67
72
|
.option("--prefix <prefix>", "virtual path prefix", "/")
|
|
68
73
|
.action((paths: string[], opts) =>
|
|
69
74
|
withDb(program, async (conn, dir) => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
let added = 0;
|
|
74
|
-
let chunks = 0;
|
|
75
|
+
// Phase 1: Scan all paths and validate they exist
|
|
76
|
+
const filesToAdd: { filePath: string; contextPath: string }[] = [];
|
|
77
|
+
const spinner = createSpinner("Scanning files...").start();
|
|
75
78
|
|
|
76
79
|
for (const path of paths) {
|
|
77
80
|
const resolvedPath = resolve(path);
|
|
78
|
-
|
|
81
|
+
let info: Awaited<ReturnType<typeof stat>>;
|
|
82
|
+
try {
|
|
83
|
+
info = await stat(resolvedPath);
|
|
84
|
+
} catch {
|
|
85
|
+
spinner.error({ text: `Path not found: ${resolvedPath}` });
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
79
88
|
|
|
80
89
|
if (info.isDirectory()) {
|
|
81
90
|
const entries = await walkDirectory(resolvedPath);
|
|
82
91
|
for (const filePath of entries) {
|
|
83
92
|
const relativePath = filePath.slice(resolvedPath.length);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
chunks += count;
|
|
89
|
-
}
|
|
93
|
+
filesToAdd.push({
|
|
94
|
+
filePath,
|
|
95
|
+
contextPath: join(opts.prefix, relativePath),
|
|
96
|
+
});
|
|
90
97
|
}
|
|
91
98
|
} else {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
filesToAdd.push({
|
|
100
|
+
filePath: resolvedPath,
|
|
101
|
+
contextPath: join(opts.prefix, basename(resolvedPath)),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
spinner.success({
|
|
107
|
+
text: `Found ${filesToAdd.length} file(s) to add.`,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Phase 2: Warmup embedder
|
|
111
|
+
const embedSpinner = createSpinner(
|
|
112
|
+
"Loading embedding model...",
|
|
113
|
+
).start();
|
|
114
|
+
const config = await loadConfig(dir);
|
|
115
|
+
await warmupEmbedder();
|
|
116
|
+
embedSpinner.success({ text: "Embedding model loaded." });
|
|
117
|
+
|
|
118
|
+
// Phase 3: Process files one-by-one
|
|
119
|
+
let added = 0;
|
|
120
|
+
let chunks = 0;
|
|
121
|
+
|
|
122
|
+
for (const [i, { filePath, contextPath }] of filesToAdd.entries()) {
|
|
123
|
+
const fileSpinner = createSpinner(
|
|
124
|
+
`Processing ${basename(filePath)} (${i + 1}/${filesToAdd.length})...`,
|
|
125
|
+
).start();
|
|
126
|
+
const count = await addFile(conn, config, filePath, contextPath);
|
|
127
|
+
if (count >= 0) {
|
|
128
|
+
added++;
|
|
129
|
+
chunks += count;
|
|
130
|
+
fileSpinner.success({
|
|
131
|
+
text: `${contextPath} (${count} chunks)`,
|
|
132
|
+
});
|
|
133
|
+
} else {
|
|
134
|
+
fileSpinner.warn({ text: `${contextPath}: skipped` });
|
|
103
135
|
}
|
|
104
136
|
}
|
|
105
137
|
|
|
106
138
|
logger.success(`Added ${added} file(s), ${chunks} chunk(s) indexed.`);
|
|
139
|
+
process.exit(0);
|
|
107
140
|
}),
|
|
108
141
|
);
|
|
109
142
|
|
|
@@ -139,6 +172,19 @@ export function registerContextCommand(program: Command) {
|
|
|
139
172
|
}
|
|
140
173
|
}),
|
|
141
174
|
);
|
|
175
|
+
ctx
|
|
176
|
+
.command("delete <path>")
|
|
177
|
+
.description("Delete a context item by path")
|
|
178
|
+
.action((path: string) =>
|
|
179
|
+
withDb(program, async (conn) => {
|
|
180
|
+
const deleted = await deleteContextItemByPath(conn, path);
|
|
181
|
+
if (!deleted) {
|
|
182
|
+
logger.error(`Context item not found: ${path}`);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
logger.success(`Deleted context item: ${path}`);
|
|
186
|
+
}),
|
|
187
|
+
);
|
|
142
188
|
}
|
|
143
189
|
|
|
144
190
|
async function addFile(
|
|
@@ -155,22 +201,32 @@ async function addFile(
|
|
|
155
201
|
|
|
156
202
|
const content = textual ? await bunFile.text() : null;
|
|
157
203
|
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
204
|
+
const existing = await getContextItemByPath(conn, contextPath);
|
|
205
|
+
let item: ContextItem;
|
|
206
|
+
|
|
207
|
+
if (existing) {
|
|
208
|
+
const updated = await updateContextItem(conn, existing.id, {
|
|
209
|
+
title: filename,
|
|
210
|
+
content: content ?? undefined,
|
|
211
|
+
mime_type: mimeType,
|
|
212
|
+
});
|
|
213
|
+
if (!updated) throw new Error(`Failed to update: ${contextPath}`);
|
|
214
|
+
item = updated;
|
|
215
|
+
} else {
|
|
216
|
+
item = await createContextItem(conn, {
|
|
217
|
+
title: filename,
|
|
218
|
+
content: content ?? undefined,
|
|
219
|
+
mimeType,
|
|
220
|
+
sourcePath: filePath,
|
|
221
|
+
contextPath,
|
|
222
|
+
isTextual: textual,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
166
225
|
|
|
167
226
|
if (textual && content) {
|
|
168
|
-
|
|
169
|
-
console.log(` + ${contextPath} (${count} chunks)`);
|
|
170
|
-
return count;
|
|
227
|
+
return await ingestContextItem(conn, item.id, config);
|
|
171
228
|
}
|
|
172
229
|
|
|
173
|
-
console.log(` + ${contextPath} (binary, not indexed)`);
|
|
174
230
|
return 0;
|
|
175
231
|
} catch (err) {
|
|
176
232
|
logger.warn(` ! ${contextPath}: ${err}`);
|