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.
Files changed (70) hide show
  1. package/README.md +9 -0
  2. package/package.json +3 -1
  3. package/src/chat/agent.ts +87 -23
  4. package/src/chat/session.ts +19 -6
  5. package/src/cli.ts +2 -0
  6. package/src/commands/chat.ts +5 -2
  7. package/src/commands/context.ts +91 -35
  8. package/src/commands/thread.ts +180 -0
  9. package/src/config/schemas.ts +3 -1
  10. package/src/context/embedder.ts +0 -3
  11. package/src/daemon/context.ts +146 -0
  12. package/src/daemon/large-results.ts +100 -0
  13. package/src/daemon/llm.ts +45 -19
  14. package/src/daemon/prompt.ts +1 -6
  15. package/src/daemon/tick.ts +9 -0
  16. package/src/db/sql/4-unique_context_path.sql +1 -0
  17. package/src/db/threads.ts +17 -0
  18. package/src/init/templates.ts +2 -1
  19. package/src/tools/context/read-large-result.ts +33 -0
  20. package/src/tools/context/search.ts +2 -0
  21. package/src/tools/context/update-beliefs.ts +2 -0
  22. package/src/tools/context/update-goals.ts +2 -0
  23. package/src/tools/dir/create.ts +3 -2
  24. package/src/tools/dir/list.ts +2 -1
  25. package/src/tools/dir/size.ts +2 -1
  26. package/src/tools/dir/tree.ts +3 -2
  27. package/src/tools/file/copy.ts +12 -3
  28. package/src/tools/file/count-lines.ts +2 -1
  29. package/src/tools/file/delete.ts +3 -2
  30. package/src/tools/file/edit.ts +3 -2
  31. package/src/tools/file/exists.ts +2 -1
  32. package/src/tools/file/info.ts +2 -0
  33. package/src/tools/file/move.ts +12 -3
  34. package/src/tools/file/read.ts +2 -1
  35. package/src/tools/file/write.ts +5 -4
  36. package/src/tools/mcp/exec.ts +70 -3
  37. package/src/tools/mcp/info.ts +8 -0
  38. package/src/tools/mcp/list-tools.ts +18 -6
  39. package/src/tools/mcp/search.ts +38 -10
  40. package/src/tools/registry.ts +4 -0
  41. package/src/tools/schedule/create.ts +2 -0
  42. package/src/tools/schedule/list.ts +2 -0
  43. package/src/tools/search/grep.ts +3 -2
  44. package/src/tools/search/semantic.ts +2 -0
  45. package/src/tools/task/complete.ts +2 -0
  46. package/src/tools/task/create.ts +17 -4
  47. package/src/tools/task/fail.ts +2 -0
  48. package/src/tools/task/list.ts +2 -0
  49. package/src/tools/task/update.ts +87 -0
  50. package/src/tools/task/view.ts +3 -1
  51. package/src/tools/task/wait.ts +2 -0
  52. package/src/tools/thread/list.ts +2 -0
  53. package/src/tools/thread/view.ts +3 -1
  54. package/src/tools/tool.ts +7 -3
  55. package/src/tui/App.tsx +323 -78
  56. package/src/tui/components/ContextPanel.tsx +415 -0
  57. package/src/tui/components/Divider.tsx +14 -0
  58. package/src/tui/components/HelpPanel.tsx +166 -0
  59. package/src/tui/components/InputBar.tsx +157 -47
  60. package/src/tui/components/Logo.tsx +79 -0
  61. package/src/tui/components/MessageList.tsx +50 -23
  62. package/src/tui/components/QueuePanel.tsx +57 -0
  63. package/src/tui/components/StatusBar.tsx +21 -9
  64. package/src/tui/components/TabBar.tsx +40 -0
  65. package/src/tui/components/TaskPanel.tsx +409 -0
  66. package/src/tui/components/ThreadPanel.tsx +541 -0
  67. package/src/tui/components/ToolCall.tsx +68 -5
  68. package/src/tui/components/ToolPanel.tsx +295 -281
  69. package/src/tui/theme.ts +75 -0
  70. package/src/utils/title.ts +47 -0
package/README.md ADDED
@@ -0,0 +1,9 @@
1
+ # Botholomew
2
+
3
+ ```
4
+ {o,o}
5
+ /)_)
6
+ " "
7
+ ```
8
+
9
+ An AI agent for knowledge work.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botholomew",
3
- "version": "0.3.0",
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's interactive chat interface. Help the user manage tasks, review results from daemon activity, search context, and answer questions.",
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: (name: string, output: string) => void;
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 maxTurns = 10;
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
- // Execute tool calls
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
- callbacks.onToolStart(toolUse.name, toolInput);
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
- const toolStart = Date.now();
171
- const result = await executeChatToolCall(toolUse, toolCtx);
172
- const toolDuration = Date.now() - toolStart;
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: toolDuration,
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: result,
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 `Tool not available in chat mode: ${tool.name}`;
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 `Invalid input: ${JSON.stringify(parsed.error)}`;
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
- return typeof result === "string" ? result : JSON.stringify(result);
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
  }
@@ -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);
@@ -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
- .action(async (opts: { threadId?: string }) => {
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", "reportEventTypes"],
34
+ flags: ["disambiguateEscapeCodes"],
32
35
  },
33
36
  },
34
37
  );
@@ -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
- const config = await loadConfig(dir);
71
- await warmupEmbedder();
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
- const info = await stat(resolvedPath);
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
- const contextPath = join(opts.prefix, relativePath);
85
- const count = await addFile(conn, config, filePath, contextPath);
86
- if (count >= 0) {
87
- added++;
88
- chunks += count;
89
- }
93
+ filesToAdd.push({
94
+ filePath,
95
+ contextPath: join(opts.prefix, relativePath),
96
+ });
90
97
  }
91
98
  } else {
92
- const contextPath = join(opts.prefix, basename(resolvedPath));
93
- const count = await addFile(
94
- conn,
95
- config,
96
- resolvedPath,
97
- contextPath,
98
- );
99
- if (count >= 0) {
100
- added++;
101
- chunks += count;
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 item = await createContextItem(conn, {
159
- title: filename,
160
- content: content ?? undefined,
161
- mimeType,
162
- sourcePath: filePath,
163
- contextPath,
164
- isTextual: textual,
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
- const count = await ingestContextItem(conn, item.id, config);
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}`);