botholomew 0.12.3 → 0.13.0
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 +91 -68
- package/package.json +3 -3
- package/src/chat/agent.ts +42 -82
- package/src/chat/session.ts +29 -25
- package/src/commands/capabilities.ts +1 -1
- package/src/commands/context.ts +177 -926
- package/src/commands/db.ts +9 -13
- package/src/commands/init.ts +4 -1
- package/src/commands/nuke.ts +57 -90
- package/src/commands/schedule.ts +103 -124
- package/src/commands/skill.ts +2 -2
- package/src/commands/task.ts +86 -95
- package/src/commands/thread.ts +107 -112
- package/src/commands/worker.ts +88 -88
- package/src/constants.ts +93 -16
- package/src/context/capabilities.ts +10 -10
- package/src/context/fetcher.ts +9 -10
- package/src/context/reindex.ts +189 -0
- package/src/context/store.ts +630 -0
- package/src/db/doctor.ts +1 -8
- package/src/db/embeddings.ts +227 -175
- package/src/db/sql/19-disk_backed_index.sql +36 -0
- package/src/db/sql/20-drop_db_tables_for_files.sql +19 -0
- package/src/fs/atomic.ts +217 -0
- package/src/fs/compat.ts +86 -0
- package/src/fs/sandbox.ts +279 -0
- package/src/init/index.ts +69 -52
- package/src/init/templates.ts +1 -1
- package/src/mcpx/client.ts +1 -1
- package/src/schedules/schema.ts +19 -0
- package/src/schedules/store.ts +296 -0
- package/src/skills/commands.ts +1 -3
- package/src/tasks/schema.ts +47 -0
- package/src/tasks/store.ts +486 -0
- package/src/threads/store.ts +559 -0
- package/src/tools/capabilities/refresh.ts +42 -21
- package/src/tools/context/pipe.ts +15 -71
- package/src/tools/context/update-beliefs.ts +3 -3
- package/src/tools/context/update-goals.ts +3 -3
- package/src/tools/dir/create.ts +26 -23
- package/src/tools/dir/size.ts +46 -17
- package/src/tools/dir/tree.ts +73 -279
- package/src/tools/file/copy.ts +50 -24
- package/src/tools/file/count-lines.ts +34 -10
- package/src/tools/file/delete.ts +44 -23
- package/src/tools/file/edit.ts +39 -14
- package/src/tools/file/exists.ts +12 -26
- package/src/tools/file/info.ts +25 -85
- package/src/tools/file/move.ts +39 -24
- package/src/tools/file/read.ts +32 -80
- package/src/tools/file/write.ts +14 -91
- package/src/tools/registry.ts +3 -7
- package/src/tools/schedule/create.ts +2 -2
- package/src/tools/schedule/list.ts +7 -3
- package/src/tools/search/fuse.ts +12 -33
- package/src/tools/search/index.ts +36 -43
- package/src/tools/search/regexp.ts +29 -17
- package/src/tools/search/semantic.ts +137 -51
- package/src/tools/skill/delete.ts +1 -1
- package/src/tools/skill/list.ts +1 -1
- package/src/tools/skill/write.ts +1 -1
- package/src/tools/task/create.ts +41 -16
- package/src/tools/task/delete.ts +3 -3
- package/src/tools/task/list.ts +6 -3
- package/src/tools/task/update.ts +31 -9
- package/src/tools/task/view.ts +6 -6
- package/src/tools/thread/list.ts +2 -2
- package/src/tools/thread/search.ts +208 -0
- package/src/tools/thread/view.ts +50 -5
- package/src/tools/worker/spawn.ts +28 -14
- package/src/tui/App.tsx +12 -19
- package/src/tui/components/ContextPanel.tsx +83 -316
- package/src/tui/components/SchedulePanel.tsx +34 -48
- package/src/tui/components/StatusBar.tsx +15 -15
- package/src/tui/components/TaskPanel.tsx +34 -38
- package/src/tui/components/ThreadPanel.tsx +29 -38
- package/src/tui/components/WorkerPanel.tsx +21 -19
- package/src/tui/markdown.ts +2 -8
- package/src/types/file-imports.d.ts +9 -0
- package/src/utils/title.ts +5 -7
- package/src/utils/v7-date.ts +47 -0
- package/src/worker/heartbeat.ts +46 -24
- package/src/worker/index.ts +13 -15
- package/src/worker/llm.ts +30 -37
- package/src/worker/prompt.ts +19 -41
- package/src/worker/schedules.ts +48 -69
- package/src/worker/spawn.ts +11 -11
- package/src/worker/tick.ts +39 -43
- package/src/workers/store.ts +247 -0
- package/src/commands/tools.ts +0 -367
- package/src/context/describer.ts +0 -140
- package/src/context/drives.ts +0 -110
- package/src/context/ingest.ts +0 -162
- package/src/context/refresh.ts +0 -183
- package/src/db/context.ts +0 -637
- package/src/db/daemon-state.ts +0 -6
- package/src/db/reembed.ts +0 -113
- package/src/db/schedules.ts +0 -213
- package/src/db/tasks.ts +0 -347
- package/src/db/threads.ts +0 -276
- package/src/db/workers.ts +0 -212
- package/src/tools/context/list-drives.ts +0 -36
- package/src/tools/context/refresh.ts +0 -165
- package/src/tools/context/search.ts +0 -54
package/README.md
CHANGED
|
@@ -13,12 +13,14 @@ that works its way through a task queue — reading email, summarizing
|
|
|
13
13
|
documents, researching topics, organizing notes, and maintaining context
|
|
14
14
|
over time — while you sleep, work, or chat with it.
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
Botholomew has **no shell and no access to your real filesystem**. The
|
|
17
|
+
agent's world is a sandboxed `context/` tree inside the project: it can
|
|
18
|
+
read, write, edit, and grep files there, but cannot escape upward,
|
|
19
|
+
follow symlinks, or touch anything outside. Local files and URLs are
|
|
20
|
+
brought in through `botholomew context add`. External capabilities
|
|
21
|
+
(email, Slack, the web, and hundreds of other services) are granted
|
|
22
|
+
deliberately, per project, through MCP servers wired up via
|
|
23
|
+
[MCPX](https://github.com/evantahler/mcpx).
|
|
22
24
|
|
|
23
25
|
---
|
|
24
26
|
|
|
@@ -27,13 +29,15 @@ through MCP servers wired up via [MCPX](https://github.com/evantahler/mcpx).
|
|
|
27
29
|
- **Autonomous.** Background **workers** claim tasks, work them with Claude,
|
|
28
30
|
and log every interaction. You can spawn one-shot workers on demand, a
|
|
29
31
|
long-running `--persist` worker, or point cron at `botholomew worker run`.
|
|
30
|
-
- **Portable.**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
- **Portable.** A project is just a directory of files — markdown for
|
|
33
|
+
prompts, tasks, schedules, and context; CSVs for conversation history.
|
|
34
|
+
Copy it, share it, `git diff` it, check it in (or `.gitignore` it).
|
|
35
|
+
- **Your data, your disk.** Tasks, schedules, threads, and the agent's
|
|
36
|
+
context tree are all real files you can `vim`, `grep`, and `git`.
|
|
37
|
+
DuckDB is demoted to a single search-index sidecar (`index.duckdb`)
|
|
38
|
+
that's fully derivable from disk and safe to delete. Model calls go
|
|
39
|
+
direct to Anthropic; any further reach is scoped to the MCP servers
|
|
40
|
+
you add.
|
|
37
41
|
- **Extensible.** External tools come from MCP servers via
|
|
38
42
|
[MCPX](https://github.com/evantahler/mcpx) — run them locally (Gmail,
|
|
39
43
|
Slack, GitHub) or connect through an MCP gateway like
|
|
@@ -42,11 +46,12 @@ through MCP servers wired up via [MCPX](https://github.com/evantahler/mcpx).
|
|
|
42
46
|
Reusable workflows are defined as markdown "skills" (slash commands)
|
|
43
47
|
that the chat agent can also create, edit, and search at runtime.
|
|
44
48
|
- **Safe by default.** The agent has no shell and no direct filesystem
|
|
45
|
-
access.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
access. Every path-taking tool is sandboxed to the project's `context/`
|
|
50
|
+
tree (NFC normalization + lstat-walk to reject symlinks at any level);
|
|
51
|
+
every external capability is an MCP server you explicitly add.
|
|
52
|
+
- **Concurrent.** Many workers can run at once. Each writes a pidfile
|
|
53
|
+
and heartbeats; tasks and schedules are claimed via `O_EXCL` lockfiles
|
|
54
|
+
and crashed workers get reaped automatically.
|
|
50
55
|
- **Self-modifying.** The agent maintains its own `beliefs.md` and
|
|
51
56
|
`goals.md` — it learns, updates its priors, and revises its goals as it
|
|
52
57
|
works. It can also author its own slash-command skills mid-conversation,
|
|
@@ -88,7 +93,7 @@ bun run dev -- --help
|
|
|
88
93
|
# 1. Initialize a project in the current directory
|
|
89
94
|
botholomew init
|
|
90
95
|
|
|
91
|
-
# 2. Add your Anthropic key to
|
|
96
|
+
# 2. Add your Anthropic key to config/config.json, or export it
|
|
92
97
|
export ANTHROPIC_API_KEY=sk-ant-...
|
|
93
98
|
# Embeddings run locally — no API key required.
|
|
94
99
|
|
|
@@ -110,25 +115,40 @@ want Botholomew to advance on its own.
|
|
|
110
115
|
|
|
111
116
|
## What a project looks like
|
|
112
117
|
|
|
118
|
+
A project is the directory you ran `botholomew init` in. Every entity
|
|
119
|
+
the agent or worker touches is a real file you can `vim`, `grep`, and
|
|
120
|
+
`git diff`:
|
|
121
|
+
|
|
113
122
|
```
|
|
114
123
|
my-project/
|
|
115
|
-
.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
124
|
+
config/config.json # models, tick interval, API keys
|
|
125
|
+
prompts/ # always-loaded markdown
|
|
126
|
+
soul.md # identity (not agent-editable)
|
|
127
|
+
beliefs.md # agent-editable priors
|
|
128
|
+
goals.md # agent-editable goals
|
|
129
|
+
capabilities.md # agent-editable tool inventory
|
|
130
|
+
skills/ # slash commands (built-ins + user-defined)
|
|
131
|
+
summarize.md
|
|
132
|
+
standup.md
|
|
133
|
+
capabilities.md
|
|
134
|
+
mcpx/servers.json # external MCP servers (Gmail, Slack, …)
|
|
135
|
+
models/ # local embedding model cache
|
|
136
|
+
context/ # agent-writable knowledge tree
|
|
137
|
+
tasks/ # one markdown file per task
|
|
138
|
+
<id>.md # status & metadata in frontmatter
|
|
139
|
+
.locks/<id>.lock # O_EXCL claim file (held by a worker)
|
|
140
|
+
schedules/ # one markdown file per schedule
|
|
141
|
+
<id>.md
|
|
142
|
+
.locks/<id>.lock
|
|
143
|
+
threads/<YYYY-MM-DD>/<id>.csv # full conversation history
|
|
144
|
+
workers/<id>.json # worker pidfile + heartbeat
|
|
145
|
+
logs/<YYYY-MM-DD>/<id>.log # per-worker logs
|
|
146
|
+
index.duckdb # search index sidecar (rebuildable; safe to delete)
|
|
129
147
|
```
|
|
130
148
|
|
|
131
|
-
|
|
149
|
+
`index.duckdb` is the only opaque file; everything else is plain text.
|
|
150
|
+
Delete the index any time and `botholomew context reindex` rebuilds it
|
|
151
|
+
from `context/`.
|
|
132
152
|
|
|
133
153
|
---
|
|
134
154
|
|
|
@@ -138,19 +158,19 @@ Everything the agent can touch is here. No surprises.
|
|
|
138
158
|
|
|
139
159
|
| Command | Purpose |
|
|
140
160
|
|---|---|
|
|
141
|
-
| `botholomew init` |
|
|
161
|
+
| `botholomew init` | Initialize the current directory as a project (refuses on iCloud/Dropbox/NFS without `--force`) |
|
|
142
162
|
| `botholomew worker run\|start` | Run a worker (foreground or background); `--persist` for long-running, `--task-id <id>` to target one task |
|
|
143
163
|
| `botholomew worker list\|status\|stop\|kill\|reap` | Inspect and manage running workers |
|
|
144
164
|
| `botholomew chat` | Interactive Ink/React TUI |
|
|
145
|
-
| `botholomew task list\|add\|view\|update\|reset\|delete` | Manage the task queue |
|
|
146
|
-
| `botholomew schedule list\|add\|view\|enable\|disable\|trigger\|delete` | Recurring work |
|
|
147
|
-
| `botholomew context add\|
|
|
148
|
-
| `botholomew capabilities` | Rescan built-in + MCPX tools and rewrite
|
|
165
|
+
| `botholomew task list\|add\|view\|update\|reset\|delete` | Manage the task queue (markdown files in `tasks/`) |
|
|
166
|
+
| `botholomew schedule list\|add\|view\|enable\|disable\|trigger\|delete` | Recurring work (markdown files in `schedules/`) |
|
|
167
|
+
| `botholomew context add\|import\|tree\|stats\|reindex\|search\|read\|write\|edit\|move\|delete\|…` | Bring files/URLs into `context/`; rebuild the search index; expose the agent's file/dir tools as CLI subcommands |
|
|
168
|
+
| `botholomew capabilities` | Rescan built-in + MCPX tools and rewrite `prompts/capabilities.md` |
|
|
149
169
|
| `botholomew mcpx servers\|list\|add\|remove\|info\|search\|exec\|ping\|auth\|deauth\|import-global\|…` | Configure external MCP servers (passthrough to `mcpx`) |
|
|
150
170
|
| `botholomew skill list\|show\|create\|validate` | Manage slash-command skills |
|
|
151
|
-
| `botholomew thread list\|view` | Browse the agent's
|
|
152
|
-
| `botholomew nuke context\|tasks\|schedules\|threads\|all` | Bulk-erase
|
|
153
|
-
| `botholomew db doctor [--repair]` | Probe
|
|
171
|
+
| `botholomew thread list\|view` | Browse the agent's conversation history (CSVs in `threads/`) |
|
|
172
|
+
| `botholomew nuke context\|tasks\|schedules\|threads\|all` | Bulk-erase project state |
|
|
173
|
+
| `botholomew db doctor [--repair]` | Probe the search-index DB; rebuild via EXPORT/IMPORT |
|
|
154
174
|
| `botholomew upgrade` | Self-update |
|
|
155
175
|
|
|
156
176
|
All `list` subcommands support `-l, --limit <n>` and `-o, --offset <n>` for pagination.
|
|
@@ -166,25 +186,25 @@ All `list` subcommands support `-l, --limit <n>` and `-o, --offset <n>` for pagi
|
|
|
166
186
|
│ │ │ │ │ (optional)│
|
|
167
187
|
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
|
|
168
188
|
│ │ │
|
|
169
|
-
│ enqueue tasks │
|
|
170
|
-
│ browse history │ claim
|
|
189
|
+
│ enqueue tasks │ pidfile + heartbeat │ fire
|
|
190
|
+
│ browse history │ claim via O_EXCL lock │ `worker run`
|
|
171
191
|
│ spawn_worker tool │ run LLM tool loops │ on a
|
|
172
|
-
│ invoke skills │ reap
|
|
173
|
-
│ │ log
|
|
192
|
+
│ invoke skills │ reap orphan locks │ schedule
|
|
193
|
+
│ │ log threads → CSV │
|
|
174
194
|
└────────────┬───────────┴────────────┬───────────┘
|
|
175
195
|
│ │
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
│
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
196
|
+
┌──────▼────────────────────────▼──────┐
|
|
197
|
+
│ <project-root>/ │
|
|
198
|
+
│ tasks/<id>.md │
|
|
199
|
+
│ schedules/<id>.md │
|
|
200
|
+
│ threads/<date>/<id>.csv │
|
|
201
|
+
│ workers/<id>.json │
|
|
202
|
+
│ context/ ─► index.duckdb │
|
|
203
|
+
│ (search sidecar) │
|
|
204
|
+
└──────────────────┬────────────────────┘
|
|
205
|
+
│
|
|
206
|
+
▼
|
|
207
|
+
MCPX ─► Gmail, Slack, GitHub, Firecrawl, …
|
|
188
208
|
```
|
|
189
209
|
|
|
190
210
|
See [docs/architecture.md](docs/architecture.md) for a deeper tour.
|
|
@@ -205,17 +225,19 @@ Topics worth understanding in detail:
|
|
|
205
225
|
- **[The TUI](docs/tui.md)** — the `botholomew chat` Ink/React terminal UI:
|
|
206
226
|
eight tabs, slash-command autocomplete, message queue, tool-call
|
|
207
227
|
visualization, and a live workers panel.
|
|
208
|
-
- **[
|
|
209
|
-
|
|
228
|
+
- **[Files & the sandbox](docs/files.md)** — the agent's `context/`
|
|
229
|
+
tree, the path sandbox (NFC + lstat-walk), and how
|
|
230
|
+
`context_read`/`context_write`/`context_edit` work.
|
|
210
231
|
- **[Context & hybrid search](docs/context-and-search.md)** — LLM-driven
|
|
211
|
-
chunking,
|
|
232
|
+
chunking, local embeddings, and DuckDB BM25 + linear-scan vector
|
|
212
233
|
search merged with reciprocal rank fusion.
|
|
213
|
-
- **[Tasks & schedules](docs/tasks-and-schedules.md)** —
|
|
214
|
-
|
|
234
|
+
- **[Tasks & schedules](docs/tasks-and-schedules.md)** — markdown
|
|
235
|
+
frontmatter as the source of truth, lockfile-based claim, DAG
|
|
236
|
+
validation, and natural-language recurring schedules.
|
|
215
237
|
- **[The Tool class](docs/tools.md)** — one Zod definition, three consumers
|
|
216
238
|
(Anthropic tool-use, Commander CLI, tests).
|
|
217
|
-
- **[
|
|
218
|
-
|
|
239
|
+
- **[Prompts](docs/prompts.md)** — `soul.md`, `beliefs.md`, `goals.md`,
|
|
240
|
+
frontmatter flags, and agent self-modification.
|
|
219
241
|
- **[Skills (slash commands)](docs/skills.md)** — reusable prompt templates
|
|
220
242
|
with positional arguments and tab completion; the chat agent can also
|
|
221
243
|
create, edit, and search them at runtime.
|
|
@@ -231,9 +253,10 @@ Topics worth understanding in detail:
|
|
|
231
253
|
## Tech stack
|
|
232
254
|
|
|
233
255
|
- **[Bun](https://bun.sh)** + TypeScript
|
|
234
|
-
- **[DuckDB](https://duckdb.org)** via `@duckdb/node-api` —
|
|
235
|
-
`array_cosine_distance()` (core DuckDB) for
|
|
236
|
-
built-in FTS extension for BM25 keyword
|
|
256
|
+
- **[DuckDB](https://duckdb.org)** via `@duckdb/node-api` — drives the
|
|
257
|
+
search-index sidecar only. `array_cosine_distance()` (core DuckDB) for
|
|
258
|
+
vector search, plus the built-in FTS extension for BM25 keyword
|
|
259
|
+
search; the index is rebuildable from `context/` at any time
|
|
237
260
|
- **[Anthropic SDK](https://docs.anthropic.com/en/api/client-sdks)** for
|
|
238
261
|
Claude — the reasoning model
|
|
239
262
|
- **[`@huggingface/transformers`](https://huggingface.co/docs/transformers.js)**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "botholomew",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "An autonomous AI agent for knowledge work — works your task queue while you sleep.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"url": "https://github.com/evantahler/botholomew.git"
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
|
-
"dev": "bun run src/cli.ts",
|
|
19
|
+
"dev": "bun run src/cli.ts -d .botholomew",
|
|
20
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'",
|
|
21
21
|
"test": "bun test",
|
|
22
22
|
"lint": "tsc --noEmit && biome check .",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@anthropic-ai/sdk": "^0.92.0",
|
|
30
30
|
"@duckdb/node-api": "^1.5.2-r.1",
|
|
31
|
-
"@evantahler/mcpx": "0.21.
|
|
31
|
+
"@evantahler/mcpx": "0.21.3",
|
|
32
32
|
"@huggingface/transformers": "^4.2.0",
|
|
33
33
|
"ansis": "^4.2.0",
|
|
34
34
|
"commander": "^14.0.0",
|
package/src/chat/agent.ts
CHANGED
|
@@ -8,10 +8,8 @@ import type {
|
|
|
8
8
|
} from "@anthropic-ai/sdk/resources/messages";
|
|
9
9
|
import type { McpxClient } from "@evantahler/mcpx";
|
|
10
10
|
import type { BotholomewConfig } from "../config/schemas.ts";
|
|
11
|
-
import { embedSingle } from "../context/embedder.ts";
|
|
12
11
|
import { withDb } from "../db/connection.ts";
|
|
13
|
-
import {
|
|
14
|
-
import { logInteraction } from "../db/threads.ts";
|
|
12
|
+
import { logInteraction } from "../threads/store.ts";
|
|
15
13
|
import { registerAllTools } from "../tools/registry.ts";
|
|
16
14
|
import {
|
|
17
15
|
getAllTools,
|
|
@@ -19,7 +17,6 @@ import {
|
|
|
19
17
|
type ToolContext,
|
|
20
18
|
toAnthropicTool,
|
|
21
19
|
} from "../tools/tool.ts";
|
|
22
|
-
import { logger } from "../utils/logger.ts";
|
|
23
20
|
import { fitToContextWindow, getMaxInputTokens } from "../worker/context.ts";
|
|
24
21
|
import { maybeStoreResult } from "../worker/large-results.ts";
|
|
25
22
|
import { createLlmClient } from "../worker/llm-client.ts";
|
|
@@ -38,17 +35,15 @@ const CHAT_TOOL_NAMES = new Set([
|
|
|
38
35
|
"create_task",
|
|
39
36
|
"list_tasks",
|
|
40
37
|
"view_task",
|
|
41
|
-
"context_search",
|
|
42
38
|
"context_info",
|
|
43
|
-
"context_refresh",
|
|
44
39
|
"context_tree",
|
|
45
|
-
"context_list_drives",
|
|
46
40
|
"context_read",
|
|
47
41
|
"context_write",
|
|
48
42
|
"context_edit",
|
|
49
43
|
"search",
|
|
50
44
|
"list_threads",
|
|
51
45
|
"view_thread",
|
|
46
|
+
"search_threads",
|
|
52
47
|
"create_schedule",
|
|
53
48
|
"list_schedules",
|
|
54
49
|
"update_beliefs",
|
|
@@ -91,39 +86,14 @@ export async function buildChatSystemPrompt(
|
|
|
91
86
|
|
|
92
87
|
prompt += await loadPersistentContext(projectDir, taskKeywords);
|
|
93
88
|
|
|
94
|
-
const dbPath = options?.dbPath;
|
|
95
|
-
const config = options?.config;
|
|
96
|
-
if (dbPath && config && keywordSource) {
|
|
97
|
-
try {
|
|
98
|
-
const queryVec = await embedSingle(keywordSource, config);
|
|
99
|
-
const results = await withDb(dbPath, (conn) =>
|
|
100
|
-
hybridSearch(conn, keywordSource, queryVec, 5),
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
if (results.length > 0) {
|
|
104
|
-
prompt += "## Relevant Context\n";
|
|
105
|
-
for (const r of results) {
|
|
106
|
-
const ref =
|
|
107
|
-
r.drive && r.path ? `${r.drive}:${r.path}` : r.context_item_id;
|
|
108
|
-
prompt += `### ${r.title} (${ref})\n`;
|
|
109
|
-
if (r.chunk_content) {
|
|
110
|
-
prompt += `${r.chunk_content.slice(0, 1000)}\n`;
|
|
111
|
-
}
|
|
112
|
-
prompt += "\n";
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
} catch (err) {
|
|
116
|
-
logger.debug(`Failed to load contextual embeddings: ${err}`);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
89
|
prompt += `## Instructions
|
|
121
90
|
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 background worker activity, search context, and answer questions.
|
|
122
91
|
You do NOT execute long-running work directly — enqueue tasks for a background worker instead using create_task, and spawn a worker via spawn_worker when the user wants the task run now.
|
|
123
|
-
Use the available tools to look up tasks, threads, schedules, and context when the user asks about them.
|
|
92
|
+
Use the available tools to look up tasks, threads, schedules, and context when the user asks about them. Files the agent can read and write live under \`context/\` as project-relative paths (e.g. \`notes/foo.md\`). Use \`context_tree\` to see what's there, \`search\` (hybrid regexp + semantic) to find content, then \`context_read\` / \`context_info\` to drill in.
|
|
93
|
+
Past conversations live in CSV files under \`threads/\`; use \`list_threads\`, \`search_threads\`, and \`view_thread\` to find and page through them.
|
|
124
94
|
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.
|
|
125
95
|
You can update the agent's beliefs and goals files when the user asks you to.
|
|
126
|
-
You can author and refine slash-command skills (reusable prompt templates stored in
|
|
96
|
+
You can author and refine slash-command skills (reusable prompt templates stored in \`skills/\`) via \`skill_list\`, \`skill_search\`, \`skill_read\`, \`skill_write\`, \`skill_edit\`, and \`skill_delete\`. New or edited skills are usable as \`/<name>\` on the user's next message.
|
|
127
97
|
Format your responses using Markdown. Use headings, bold, italic, lists, and code blocks to make your responses clear and well-structured.
|
|
128
98
|
`;
|
|
129
99
|
|
|
@@ -133,19 +103,19 @@ Format your responses using Markdown. Use headings, bold, italic, lists, and cod
|
|
|
133
103
|
|
|
134
104
|
### Local context first
|
|
135
105
|
|
|
136
|
-
**Before any MCP read, search local context.**
|
|
106
|
+
**Before any MCP read, search local context.** Files in \`context/\` (Gmail dumps, GitHub fetches, URL ingests, prior agent outputs) are usually already there — refetching is slower, costs tokens, and risks rate limits.
|
|
137
107
|
|
|
138
108
|
Workflow for any "look up / find / read" intent:
|
|
139
109
|
|
|
140
|
-
1. \`search\` (hybrid regexp + semantic)
|
|
141
|
-
2. If freshness matters, call \`context_info\` and check
|
|
110
|
+
1. \`search\` (hybrid regexp + semantic) over \`context/\`, then \`context_read\` / \`context_tree\` to drill in.
|
|
111
|
+
2. If freshness matters, call \`context_info\` and check the file's mtime. To re-pull stale content, write fresh into \`context/\` (\`pipe_to_context\` from an \`mcp_exec\` call is the typical path) rather than going to MCP for the whole document on every question.
|
|
142
112
|
3. Only call \`mcp_exec\` for reads when the data is genuinely missing locally **or** must be real-time (e.g., "what's on my calendar right now").
|
|
143
113
|
|
|
144
114
|
Writes always go through MCP — sending an email, creating an issue, posting to Slack. Don't search context first for those.
|
|
145
115
|
|
|
146
116
|
Examples:
|
|
147
117
|
- "What does doc X say?" → \`search\` first.
|
|
148
|
-
- "Any new emails from Y?" →
|
|
118
|
+
- "Any new emails from Y?" → \`search\` for the sender under \`context/gmail/\` (or wherever you've been ingesting mail) before hitting Gmail MCP.
|
|
149
119
|
- "Send an email to Y" → MCP write directly; no context lookup.
|
|
150
120
|
|
|
151
121
|
### Calling MCP tools
|
|
@@ -250,13 +220,11 @@ export async function runChatTurn(input: {
|
|
|
250
220
|
// the whole tool loop to finish.
|
|
251
221
|
const injections = callbacks.takeInjections?.() ?? [];
|
|
252
222
|
for (const text of injections) {
|
|
253
|
-
await
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}),
|
|
259
|
-
);
|
|
223
|
+
await logInteraction(projectDir, threadId, {
|
|
224
|
+
role: "user",
|
|
225
|
+
kind: "message",
|
|
226
|
+
content: text,
|
|
227
|
+
});
|
|
260
228
|
messages.push({ role: "user", content: text });
|
|
261
229
|
}
|
|
262
230
|
|
|
@@ -327,15 +295,13 @@ export async function runChatTurn(input: {
|
|
|
327
295
|
// `assistantText` is the right partial value). Deliberately drop any
|
|
328
296
|
// partial tool_use blocks — they would be unmatched on the next turn.
|
|
329
297
|
if (assistantText) {
|
|
330
|
-
await
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}),
|
|
338
|
-
);
|
|
298
|
+
await logInteraction(projectDir, threadId, {
|
|
299
|
+
role: "assistant",
|
|
300
|
+
kind: "message",
|
|
301
|
+
content: assistantText,
|
|
302
|
+
durationMs: Date.now() - startTime,
|
|
303
|
+
tokenCount: 0,
|
|
304
|
+
});
|
|
339
305
|
messages.push({ role: "assistant", content: assistantText });
|
|
340
306
|
}
|
|
341
307
|
return;
|
|
@@ -348,15 +314,13 @@ export async function runChatTurn(input: {
|
|
|
348
314
|
|
|
349
315
|
// Log assistant text
|
|
350
316
|
if (assistantText) {
|
|
351
|
-
await
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}),
|
|
359
|
-
);
|
|
317
|
+
await logInteraction(projectDir, threadId, {
|
|
318
|
+
role: "assistant",
|
|
319
|
+
kind: "message",
|
|
320
|
+
content: assistantText,
|
|
321
|
+
durationMs,
|
|
322
|
+
tokenCount,
|
|
323
|
+
});
|
|
360
324
|
}
|
|
361
325
|
|
|
362
326
|
// Check for tool calls
|
|
@@ -380,15 +344,13 @@ export async function runChatTurn(input: {
|
|
|
380
344
|
callbacks.onToolStart(toolUse.id, toolUse.name, toolInput);
|
|
381
345
|
}
|
|
382
346
|
|
|
383
|
-
await
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}),
|
|
391
|
-
);
|
|
347
|
+
await logInteraction(projectDir, threadId, {
|
|
348
|
+
role: "assistant",
|
|
349
|
+
kind: "tool_use",
|
|
350
|
+
content: `Calling ${toolUse.name}`,
|
|
351
|
+
toolName: toolUse.name,
|
|
352
|
+
toolInput,
|
|
353
|
+
});
|
|
392
354
|
}
|
|
393
355
|
|
|
394
356
|
// Execute all tools in parallel. Each tool call opens its own short-lived
|
|
@@ -422,15 +384,13 @@ export async function runChatTurn(input: {
|
|
|
422
384
|
// Log results and collect tool_result messages
|
|
423
385
|
const toolResults: ToolResultBlockParam[] = [];
|
|
424
386
|
for (const { toolUse, result, durationMs, stored } of execResults) {
|
|
425
|
-
await
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
}),
|
|
433
|
-
);
|
|
387
|
+
await logInteraction(projectDir, threadId, {
|
|
388
|
+
role: "tool",
|
|
389
|
+
kind: "tool_result",
|
|
390
|
+
content: result.output,
|
|
391
|
+
toolName: toolUse.name,
|
|
392
|
+
durationMs,
|
|
393
|
+
});
|
|
434
394
|
|
|
435
395
|
toolResults.push({
|
|
436
396
|
type: "tool_result",
|
package/src/chat/session.ts
CHANGED
|
@@ -5,16 +5,17 @@ import type { BotholomewConfig } from "../config/schemas.ts";
|
|
|
5
5
|
import { getDbPath } from "../constants.ts";
|
|
6
6
|
import { withDb } from "../db/connection.ts";
|
|
7
7
|
import { migrate } from "../db/schema.ts";
|
|
8
|
+
import { createMcpxClient } from "../mcpx/client.ts";
|
|
9
|
+
import { loadSkills } from "../skills/loader.ts";
|
|
10
|
+
import type { SkillDefinition } from "../skills/parser.ts";
|
|
8
11
|
import {
|
|
9
12
|
createThread,
|
|
10
13
|
endThread,
|
|
14
|
+
ensureThreadsDir,
|
|
11
15
|
getThread,
|
|
12
16
|
logInteraction,
|
|
13
17
|
reopenThread,
|
|
14
|
-
} from "../
|
|
15
|
-
import { createMcpxClient } from "../mcpx/client.ts";
|
|
16
|
-
import { loadSkills } from "../skills/loader.ts";
|
|
17
|
-
import type { SkillDefinition } from "../skills/parser.ts";
|
|
18
|
+
} from "../threads/store.ts";
|
|
18
19
|
import { generateThreadTitle } from "../utils/title.ts";
|
|
19
20
|
import { type ChatTurnCallbacks, runChatTurn } from "./agent.ts";
|
|
20
21
|
|
|
@@ -56,26 +57,25 @@ export async function startChatSession(
|
|
|
56
57
|
|
|
57
58
|
if (!config.anthropic_api_key) {
|
|
58
59
|
throw new Error(
|
|
59
|
-
"no API key found. add anthropic_api_key to
|
|
60
|
+
"no API key found. add anthropic_api_key to config/config.json",
|
|
60
61
|
);
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
const dbPath = getDbPath(projectDir);
|
|
64
65
|
await withDb(dbPath, (conn) => migrate(conn));
|
|
66
|
+
await ensureThreadsDir(projectDir);
|
|
65
67
|
|
|
66
68
|
let threadId: string;
|
|
67
69
|
const messages: MessageParam[] = [];
|
|
68
70
|
|
|
69
71
|
if (existingThreadId) {
|
|
70
72
|
// Resume existing thread
|
|
71
|
-
const result = await
|
|
72
|
-
getThread(conn, existingThreadId),
|
|
73
|
-
);
|
|
73
|
+
const result = await getThread(projectDir, existingThreadId);
|
|
74
74
|
if (!result) {
|
|
75
75
|
throw new Error(`Thread not found: ${existingThreadId}`);
|
|
76
76
|
}
|
|
77
77
|
threadId = existingThreadId;
|
|
78
|
-
await
|
|
78
|
+
await reopenThread(projectDir, threadId);
|
|
79
79
|
|
|
80
80
|
// Rebuild message history from interactions
|
|
81
81
|
let firstUserMessage: string | undefined;
|
|
@@ -91,11 +91,14 @@ export async function startChatSession(
|
|
|
91
91
|
|
|
92
92
|
// Backfill title for threads that still have the default
|
|
93
93
|
if (result.thread.title === "New chat" && firstUserMessage) {
|
|
94
|
-
void generateThreadTitle(config,
|
|
94
|
+
void generateThreadTitle(config, projectDir, threadId, firstUserMessage);
|
|
95
95
|
}
|
|
96
96
|
} else {
|
|
97
|
-
threadId = await
|
|
98
|
-
|
|
97
|
+
threadId = await createThread(
|
|
98
|
+
projectDir,
|
|
99
|
+
"chat_session",
|
|
100
|
+
undefined,
|
|
101
|
+
"New chat",
|
|
99
102
|
);
|
|
100
103
|
}
|
|
101
104
|
|
|
@@ -133,13 +136,11 @@ export async function sendMessage(
|
|
|
133
136
|
session.skills = await loadSkills(session.projectDir);
|
|
134
137
|
|
|
135
138
|
// Log and append user message
|
|
136
|
-
await
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}),
|
|
142
|
-
);
|
|
139
|
+
await logInteraction(session.projectDir, session.threadId, {
|
|
140
|
+
role: "user",
|
|
141
|
+
kind: "message",
|
|
142
|
+
content: userMessage,
|
|
143
|
+
});
|
|
143
144
|
|
|
144
145
|
session.messages.push({ role: "user", content: userMessage });
|
|
145
146
|
|
|
@@ -147,7 +148,7 @@ export async function sendMessage(
|
|
|
147
148
|
if (session.messages.length === 1) {
|
|
148
149
|
void generateThreadTitle(
|
|
149
150
|
session.config,
|
|
150
|
-
session.
|
|
151
|
+
session.projectDir,
|
|
151
152
|
session.threadId,
|
|
152
153
|
userMessage,
|
|
153
154
|
);
|
|
@@ -166,7 +167,7 @@ export async function sendMessage(
|
|
|
166
167
|
}
|
|
167
168
|
|
|
168
169
|
export async function endChatSession(session: ChatSession): Promise<void> {
|
|
169
|
-
await
|
|
170
|
+
await endThread(session.projectDir, session.threadId);
|
|
170
171
|
await session.cleanup();
|
|
171
172
|
}
|
|
172
173
|
|
|
@@ -180,10 +181,13 @@ export async function clearChatSession(
|
|
|
180
181
|
session: ChatSession,
|
|
181
182
|
): Promise<{ previousThreadId: string; newThreadId: string }> {
|
|
182
183
|
const previousThreadId = session.threadId;
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
184
|
+
await endThread(session.projectDir, previousThreadId);
|
|
185
|
+
const newThreadId = await createThread(
|
|
186
|
+
session.projectDir,
|
|
187
|
+
"chat_session",
|
|
188
|
+
undefined,
|
|
189
|
+
"New chat",
|
|
190
|
+
);
|
|
187
191
|
session.threadId = newThreadId;
|
|
188
192
|
session.messages.length = 0;
|
|
189
193
|
session.activeStream = null;
|
|
@@ -9,7 +9,7 @@ export function registerCapabilitiesCommand(program: Command) {
|
|
|
9
9
|
program
|
|
10
10
|
.command("capabilities")
|
|
11
11
|
.description(
|
|
12
|
-
"Regenerate
|
|
12
|
+
"Regenerate prompts/capabilities.md by scanning built-in tools and MCPX tools",
|
|
13
13
|
)
|
|
14
14
|
.option("--no-mcp", "Skip MCPX tool enumeration (built-in tools only)")
|
|
15
15
|
.action((opts: { mcp?: boolean }) =>
|