botholomew 0.16.4 → 0.18.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 +46 -41
- package/package.json +4 -9
- package/src/chat/agent.ts +37 -40
- package/src/chat/session.ts +10 -10
- package/src/cli.ts +0 -2
- package/src/commands/capabilities.ts +35 -33
- package/src/commands/context.ts +133 -221
- package/src/commands/init.ts +22 -1
- package/src/commands/mcpx.ts +21 -8
- package/src/commands/nuke.ts +52 -15
- package/src/commands/prepare.ts +16 -13
- package/src/config/loader.ts +1 -8
- package/src/config/schemas.ts +6 -0
- package/src/constants.ts +16 -32
- package/src/init/index.ts +52 -27
- package/src/mcpx/client.ts +21 -5
- package/src/mem/client.ts +33 -0
- package/src/{context → prompts}/capabilities.ts +11 -7
- package/src/schedules/store.ts +1 -1
- package/src/tasks/store.ts +1 -1
- package/src/threads/store.ts +1 -1
- package/src/tools/capabilities/refresh.ts +1 -1
- package/src/tools/membot/adapter.ts +111 -0
- package/src/tools/membot/copy.ts +59 -0
- package/src/tools/membot/count_lines.ts +53 -0
- package/src/tools/membot/edit.ts +72 -0
- package/src/tools/membot/exists.ts +54 -0
- package/src/tools/membot/index.ts +26 -0
- package/src/tools/{context → membot}/pipe.ts +34 -32
- package/src/tools/registry.ts +6 -37
- package/src/tools/tool.ts +6 -8
- package/src/tui/App.tsx +3 -4
- package/src/tui/components/ContextPanel.tsx +109 -226
- package/src/tui/components/HelpPanel.tsx +2 -2
- package/src/tui/components/StatusBar.tsx +0 -6
- package/src/tui/components/ThreadPanel.tsx +8 -7
- package/src/tui/wrapDetail.ts +11 -0
- package/src/worker/heartbeat.ts +0 -20
- package/src/worker/index.ts +13 -13
- package/src/worker/llm.ts +7 -9
- package/src/worker/prompt.ts +25 -13
- package/src/worker/spawn.ts +1 -1
- package/src/worker/tick.ts +10 -9
- package/src/commands/db.ts +0 -119
- package/src/commands/with-db.ts +0 -22
- package/src/context/chunker.ts +0 -275
- package/src/context/embedder-impl.ts +0 -100
- package/src/context/embedder.ts +0 -9
- package/src/context/fetcher-errors.ts +0 -8
- package/src/context/fetcher.ts +0 -515
- package/src/context/locks.ts +0 -146
- package/src/context/markdown-converter.ts +0 -186
- package/src/context/reindex.ts +0 -198
- package/src/context/store.ts +0 -841
- package/src/context/url-utils.ts +0 -25
- package/src/db/connection.ts +0 -255
- package/src/db/doctor.ts +0 -235
- package/src/db/embeddings.ts +0 -317
- package/src/db/query.ts +0 -56
- package/src/db/schema.ts +0 -93
- package/src/db/sql/1-core_tables.sql +0 -53
- package/src/db/sql/10-dedupe_context_items.sql +0 -26
- package/src/db/sql/11-rebuild_hnsw.sql +0 -8
- package/src/db/sql/12-workers.sql +0 -66
- package/src/db/sql/13-drive-paths.sql +0 -47
- package/src/db/sql/14-drop_hnsw_index.sql +0 -8
- package/src/db/sql/15-fts_index.sql +0 -8
- package/src/db/sql/16-source_url.sql +0 -7
- package/src/db/sql/17-worker_log_path.sql +0 -3
- package/src/db/sql/18-reset_embeddings_for_local.sql +0 -39
- package/src/db/sql/19-disk_backed_index.sql +0 -36
- package/src/db/sql/2-logging_tables.sql +0 -24
- package/src/db/sql/20-drop_db_tables_for_files.sql +0 -19
- package/src/db/sql/3-daemon_state.sql +0 -5
- package/src/db/sql/4-unique_context_path.sql +0 -1
- package/src/db/sql/5-reset_embeddings_for_openai.sql +0 -1
- package/src/db/sql/6-vss_index.sql +0 -7
- package/src/db/sql/7-drop_embeddings_fk.sql +0 -23
- package/src/db/sql/8-task_output.sql +0 -1
- package/src/db/sql/9-source-type.sql +0 -1
- package/src/tools/context/read-large-result.ts +0 -33
- package/src/tools/dir/create.ts +0 -47
- package/src/tools/dir/size.ts +0 -77
- package/src/tools/dir/tree.ts +0 -124
- package/src/tools/file/copy.ts +0 -73
- package/src/tools/file/count-lines.ts +0 -54
- package/src/tools/file/delete.ts +0 -83
- package/src/tools/file/edit.ts +0 -76
- package/src/tools/file/exists.ts +0 -33
- package/src/tools/file/info.ts +0 -66
- package/src/tools/file/move.ts +0 -66
- package/src/tools/file/read.ts +0 -67
- package/src/tools/file/write.ts +0 -58
- package/src/tools/search/fuse.ts +0 -96
- package/src/tools/search/index.ts +0 -127
- package/src/tools/search/regexp.ts +0 -82
- package/src/tools/search/semantic.ts +0 -167
- /package/src/{db → utils}/uuid.ts +0 -0
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
-- Switch from OpenAI 1536-dim embeddings to local 384-dim embeddings.
|
|
2
|
-
--
|
|
3
|
-
-- DuckDB encodes array dimension in the column type, so we rebuild the
|
|
4
|
-
-- embeddings table preserving every row's metadata (chunk_content, title,
|
|
5
|
-
-- description, context_item_id, chunk_index, created_at). The vectors
|
|
6
|
-
-- themselves are NULLed and repopulated by `botholomew context reembed`
|
|
7
|
-
-- using the locally-loaded embedding model.
|
|
8
|
-
--
|
|
9
|
-
-- Idempotency: every destructive step uses IF EXISTS so a partial prior
|
|
10
|
-
-- run can be re-attempted cleanly. The FTS index is dropped here but NOT
|
|
11
|
-
-- recreated — `migrate()` calls rebuildSearchIndex once after all SQL
|
|
12
|
-
-- migrations apply, which avoids a same-migration drop-then-create that
|
|
13
|
-
-- DuckDB rejects with "Could not commit creation of dependency, subject
|
|
14
|
-
-- 'stopwords' has been deleted".
|
|
15
|
-
|
|
16
|
-
DROP SCHEMA IF EXISTS fts_main_embeddings CASCADE;
|
|
17
|
-
|
|
18
|
-
DROP TABLE IF EXISTS embeddings_new;
|
|
19
|
-
|
|
20
|
-
CREATE TABLE embeddings_new (
|
|
21
|
-
id TEXT PRIMARY KEY,
|
|
22
|
-
context_item_id TEXT NOT NULL,
|
|
23
|
-
chunk_index INTEGER NOT NULL,
|
|
24
|
-
chunk_content TEXT,
|
|
25
|
-
title TEXT NOT NULL,
|
|
26
|
-
description TEXT NOT NULL DEFAULT '',
|
|
27
|
-
embedding FLOAT[384],
|
|
28
|
-
created_at TEXT NOT NULL DEFAULT (current_timestamp::VARCHAR),
|
|
29
|
-
UNIQUE(context_item_id, chunk_index)
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
INSERT INTO embeddings_new (id, context_item_id, chunk_index, chunk_content, title, description, embedding, created_at)
|
|
33
|
-
SELECT id, context_item_id, chunk_index, chunk_content, title, description, NULL, created_at
|
|
34
|
-
FROM embeddings;
|
|
35
|
-
|
|
36
|
-
DROP TABLE embeddings;
|
|
37
|
-
ALTER TABLE embeddings_new RENAME TO embeddings;
|
|
38
|
-
|
|
39
|
-
CHECKPOINT;
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
-- Switch the search index from "tracks DuckDB-backed virtual files" to
|
|
2
|
-
-- "tracks real files on disk under context/", and drop every table whose
|
|
3
|
-
-- contents now live on the filesystem (tasks, schedules) or that nothing
|
|
4
|
-
-- writes to anymore (daemon_state). The remaining DuckDB tables are:
|
|
5
|
-
-- workers, threads, interactions, context_index, _migrations
|
|
6
|
-
--
|
|
7
|
-
-- A new `context_index` table holds one row per (path, chunk_index), with a
|
|
8
|
-
-- file-level content hash + mtime so `botholomew context reindex` can detect
|
|
9
|
-
-- adds, updates, and removals in one pass.
|
|
10
|
-
--
|
|
11
|
-
-- Idempotent: every step uses IF EXISTS so a partial prior run is safe to
|
|
12
|
-
-- re-attempt. The FTS index over the new chunk_content column is created by
|
|
13
|
-
-- migrate() via rebuildSearchIndex() after all migrations apply.
|
|
14
|
-
|
|
15
|
-
DROP SCHEMA IF EXISTS fts_main_embeddings CASCADE;
|
|
16
|
-
DROP TABLE IF EXISTS embeddings;
|
|
17
|
-
DROP TABLE IF EXISTS context_items;
|
|
18
|
-
DROP TABLE IF EXISTS tasks;
|
|
19
|
-
DROP TABLE IF EXISTS schedules;
|
|
20
|
-
DROP TABLE IF EXISTS daemon_state;
|
|
21
|
-
|
|
22
|
-
CREATE TABLE IF NOT EXISTS context_index (
|
|
23
|
-
path TEXT NOT NULL,
|
|
24
|
-
chunk_index INTEGER NOT NULL,
|
|
25
|
-
content_hash TEXT NOT NULL,
|
|
26
|
-
chunk_content TEXT NOT NULL,
|
|
27
|
-
embedding FLOAT[384],
|
|
28
|
-
mtime_ms BIGINT NOT NULL,
|
|
29
|
-
size_bytes BIGINT NOT NULL,
|
|
30
|
-
indexed_at TEXT NOT NULL DEFAULT (current_timestamp::VARCHAR),
|
|
31
|
-
PRIMARY KEY (path, chunk_index)
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
CREATE INDEX IF NOT EXISTS idx_context_index_path ON context_index(path);
|
|
35
|
-
|
|
36
|
-
CHECKPOINT;
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
CREATE TABLE threads (
|
|
2
|
-
id TEXT PRIMARY KEY,
|
|
3
|
-
type TEXT NOT NULL CHECK(type IN ('daemon_tick', 'chat_session')),
|
|
4
|
-
task_id TEXT,
|
|
5
|
-
title TEXT NOT NULL DEFAULT '',
|
|
6
|
-
started_at TEXT NOT NULL DEFAULT (current_timestamp::VARCHAR),
|
|
7
|
-
ended_at TEXT,
|
|
8
|
-
metadata TEXT
|
|
9
|
-
);
|
|
10
|
-
|
|
11
|
-
CREATE TABLE interactions (
|
|
12
|
-
id TEXT PRIMARY KEY,
|
|
13
|
-
thread_id TEXT NOT NULL REFERENCES threads(id),
|
|
14
|
-
sequence INTEGER NOT NULL,
|
|
15
|
-
role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system', 'tool')),
|
|
16
|
-
kind TEXT NOT NULL CHECK(kind IN ('message', 'thinking', 'tool_use', 'tool_result', 'context_update', 'status_change')),
|
|
17
|
-
content TEXT NOT NULL,
|
|
18
|
-
tool_name TEXT,
|
|
19
|
-
tool_input TEXT,
|
|
20
|
-
duration_ms INTEGER,
|
|
21
|
-
token_count INTEGER,
|
|
22
|
-
created_at TEXT NOT NULL DEFAULT (current_timestamp::VARCHAR),
|
|
23
|
-
UNIQUE(thread_id, sequence)
|
|
24
|
-
);
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
-- Tasks, schedules, threads, interactions, and workers all moved out of
|
|
2
|
-
-- DuckDB onto disk:
|
|
3
|
-
-- tasks/ markdown files with frontmatter (one per task)
|
|
4
|
-
-- schedules/ markdown files with frontmatter (one per schedule)
|
|
5
|
-
-- threads/ CSV per conversation (searchable via the index)
|
|
6
|
-
-- workers/ JSON pidfile per worker, mtime-checked heartbeats
|
|
7
|
-
--
|
|
8
|
-
-- The only remaining DuckDB objects after this migration are _migrations,
|
|
9
|
-
-- context_index, and the FTS index built over context_index by
|
|
10
|
-
-- rebuildSearchIndex(). Idempotent via IF EXISTS.
|
|
11
|
-
|
|
12
|
-
DROP TABLE IF EXISTS interactions;
|
|
13
|
-
DROP TABLE IF EXISTS threads;
|
|
14
|
-
DROP TABLE IF EXISTS workers;
|
|
15
|
-
|
|
16
|
-
DROP TABLE IF EXISTS tasks;
|
|
17
|
-
DROP TABLE IF EXISTS schedules;
|
|
18
|
-
|
|
19
|
-
CHECKPOINT;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_context_items_context_path ON context_items(context_path);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
DELETE FROM embeddings
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
-- Historical: this migration used to CREATE an HNSW index on embeddings
|
|
2
|
-
-- via the VSS extension. HNSW has since been removed (see migration 12)
|
|
3
|
-
-- (see migration 14) and the VSS extension is no longer loaded at
|
|
4
|
-
-- connection time, so running `CREATE INDEX ... USING HNSW` here would
|
|
5
|
-
-- fail on fresh DBs. Kept as a no-op to preserve migration numbering
|
|
6
|
-
-- for existing databases that have already recorded id 6 in _migrations.
|
|
7
|
-
SELECT 1;
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
-- DuckDB implements UPDATE as delete+insert on tables with unique indexes.
|
|
2
|
-
-- The foreign key from embeddings → context_items causes every UPDATE to
|
|
3
|
-
-- context_items to fail when embeddings exist. Cascading deletes are already
|
|
4
|
-
-- handled in application code (deleteContextItem), so the FK is redundant.
|
|
5
|
-
--
|
|
6
|
-
-- Clear embeddings before DROP to avoid DuckDB WAL replay crash with FK refs.
|
|
7
|
-
-- Embeddings are recreated on next `context add` or `context refresh`.
|
|
8
|
-
DELETE FROM embeddings;
|
|
9
|
-
UPDATE context_items SET indexed_at = NULL;
|
|
10
|
-
DROP TABLE embeddings;
|
|
11
|
-
CREATE TABLE embeddings (
|
|
12
|
-
id TEXT PRIMARY KEY,
|
|
13
|
-
context_item_id TEXT NOT NULL,
|
|
14
|
-
chunk_index INTEGER NOT NULL,
|
|
15
|
-
chunk_content TEXT,
|
|
16
|
-
title TEXT NOT NULL,
|
|
17
|
-
description TEXT NOT NULL DEFAULT '',
|
|
18
|
-
source_path TEXT,
|
|
19
|
-
embedding FLOAT[1536],
|
|
20
|
-
created_at TEXT NOT NULL DEFAULT (current_timestamp::VARCHAR),
|
|
21
|
-
UNIQUE(context_item_id, chunk_index)
|
|
22
|
-
);
|
|
23
|
-
CHECKPOINT;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
ALTER TABLE tasks ADD COLUMN output TEXT;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
ALTER TABLE context_items ADD COLUMN source_type TEXT DEFAULT 'file';
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { readLargeResultPage } from "../../worker/large-results.ts";
|
|
3
|
-
import type { ToolDefinition } from "../tool.ts";
|
|
4
|
-
|
|
5
|
-
const inputSchema = z.object({
|
|
6
|
-
id: z.string().describe("The large result ID (e.g. lr_1)"),
|
|
7
|
-
page: z.number().int().min(1).describe("Page number to read (1-based)"),
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
const outputSchema = z.object({
|
|
11
|
-
content: z.string(),
|
|
12
|
-
page: z.number(),
|
|
13
|
-
totalPages: z.number(),
|
|
14
|
-
is_error: z.boolean(),
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
export const readLargeResultTool = {
|
|
18
|
-
name: "read_large_result",
|
|
19
|
-
description:
|
|
20
|
-
"Read a page from a large tool result that was too big to display inline. Use this to paginate through stored results.",
|
|
21
|
-
group: "context",
|
|
22
|
-
inputSchema,
|
|
23
|
-
outputSchema,
|
|
24
|
-
execute: async (input) => {
|
|
25
|
-
const result = readLargeResultPage(input.id, input.page);
|
|
26
|
-
if (!result) {
|
|
27
|
-
throw new Error(
|
|
28
|
-
`No result found for id="${input.id}" page=${input.page}. The id may be invalid or the page may be out of range.`,
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
return { ...result, is_error: false };
|
|
32
|
-
},
|
|
33
|
-
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
package/src/tools/dir/create.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { createContextDir, fileExists } from "../../context/store.ts";
|
|
3
|
-
import type { ToolDefinition } from "../tool.ts";
|
|
4
|
-
|
|
5
|
-
const inputSchema = z.object({
|
|
6
|
-
path: z.string().describe("Directory path to create under context/"),
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
const outputSchema = z.object({
|
|
10
|
-
path: z.string(),
|
|
11
|
-
created: z.boolean(),
|
|
12
|
-
is_error: z.boolean(),
|
|
13
|
-
error_type: z.string().optional(),
|
|
14
|
-
message: z.string().optional(),
|
|
15
|
-
next_action_hint: z.string().optional(),
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
export const contextCreateDirTool = {
|
|
19
|
-
name: "context_create_dir",
|
|
20
|
-
description:
|
|
21
|
-
"[[ bash equivalent command: mkdir -p ]] Create a directory (recursively) under context/. Paths that traverse a user symlink fail with PathEscapeError.",
|
|
22
|
-
group: "context",
|
|
23
|
-
inputSchema,
|
|
24
|
-
outputSchema,
|
|
25
|
-
execute: async (input, ctx) => {
|
|
26
|
-
try {
|
|
27
|
-
const existed = await fileExists(ctx.projectDir, input.path);
|
|
28
|
-
await createContextDir(ctx.projectDir, input.path);
|
|
29
|
-
return { path: input.path, created: !existed, is_error: false };
|
|
30
|
-
} catch (err) {
|
|
31
|
-
// mkdir surfaces ENOTDIR when a path component is a file, EACCES on
|
|
32
|
-
// permission issues, etc. Convert to a structured error so the agent
|
|
33
|
-
// can pick a different parent or read what's actually there first.
|
|
34
|
-
const code = (err as NodeJS.ErrnoException).code;
|
|
35
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
36
|
-
return {
|
|
37
|
-
path: input.path,
|
|
38
|
-
created: false,
|
|
39
|
-
is_error: true,
|
|
40
|
-
error_type: code === "ENOTDIR" ? "not_a_directory" : "create_failed",
|
|
41
|
-
message,
|
|
42
|
-
next_action_hint:
|
|
43
|
-
"Run context_tree on the parent path to see what's there before retrying.",
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
},
|
|
47
|
-
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
package/src/tools/dir/size.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
dirSizeBytes,
|
|
4
|
-
NotDirectoryError,
|
|
5
|
-
NotFoundError,
|
|
6
|
-
} from "../../context/store.ts";
|
|
7
|
-
import type { ToolDefinition } from "../tool.ts";
|
|
8
|
-
|
|
9
|
-
function formatBytes(bytes: number): string {
|
|
10
|
-
if (bytes === 0) return "0 B";
|
|
11
|
-
const units = ["B", "KB", "MB", "GB"];
|
|
12
|
-
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
13
|
-
const value = bytes / 1024 ** i;
|
|
14
|
-
return `${value.toFixed(i > 0 ? 1 : 0)} ${units[i]}`;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const inputSchema = z.object({
|
|
18
|
-
path: z
|
|
19
|
-
.string()
|
|
20
|
-
.optional()
|
|
21
|
-
.default("")
|
|
22
|
-
.describe("Directory path under context/ (defaults to context root)"),
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
const outputSchema = z.object({
|
|
26
|
-
files: z.number(),
|
|
27
|
-
bytes: z.number(),
|
|
28
|
-
formatted: z.string(),
|
|
29
|
-
is_error: z.boolean(),
|
|
30
|
-
error_type: z.string().optional(),
|
|
31
|
-
message: z.string().optional(),
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
export const contextDirSizeTool = {
|
|
35
|
-
name: "context_dir_size",
|
|
36
|
-
description:
|
|
37
|
-
"[[ bash equivalent command: du -s ]] Get the total size of files under a directory in context/.",
|
|
38
|
-
group: "context",
|
|
39
|
-
inputSchema,
|
|
40
|
-
outputSchema,
|
|
41
|
-
execute: async (input, ctx) => {
|
|
42
|
-
try {
|
|
43
|
-
const { files, bytes } = await dirSizeBytes(
|
|
44
|
-
ctx.projectDir,
|
|
45
|
-
input.path ?? "",
|
|
46
|
-
);
|
|
47
|
-
return {
|
|
48
|
-
files,
|
|
49
|
-
bytes,
|
|
50
|
-
formatted: formatBytes(bytes),
|
|
51
|
-
is_error: false,
|
|
52
|
-
};
|
|
53
|
-
} catch (err) {
|
|
54
|
-
if (err instanceof NotFoundError) {
|
|
55
|
-
return {
|
|
56
|
-
files: 0,
|
|
57
|
-
bytes: 0,
|
|
58
|
-
formatted: formatBytes(0),
|
|
59
|
-
is_error: true,
|
|
60
|
-
error_type: "not_found",
|
|
61
|
-
message: `No directory at context/${err.path}`,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
if (err instanceof NotDirectoryError) {
|
|
65
|
-
return {
|
|
66
|
-
files: 0,
|
|
67
|
-
bytes: 0,
|
|
68
|
-
formatted: formatBytes(0),
|
|
69
|
-
is_error: true,
|
|
70
|
-
error_type: "not_a_directory",
|
|
71
|
-
message: `context/${err.path} is not a directory`,
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
throw err;
|
|
75
|
-
}
|
|
76
|
-
},
|
|
77
|
-
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
package/src/tools/dir/tree.ts
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
buildTree,
|
|
4
|
-
NotFoundError,
|
|
5
|
-
type TreeNode,
|
|
6
|
-
} from "../../context/store.ts";
|
|
7
|
-
import type { ToolDefinition } from "../tool.ts";
|
|
8
|
-
|
|
9
|
-
const DEFAULT_MAX_DEPTH = 10;
|
|
10
|
-
|
|
11
|
-
export interface BuildContextTreeResult {
|
|
12
|
-
tree: string;
|
|
13
|
-
total_items: number;
|
|
14
|
-
hint: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Render a TreeNode as an indented string. Files are listed; directories show
|
|
19
|
-
* children. Depth is enforced inside `buildTree`.
|
|
20
|
-
*/
|
|
21
|
-
function renderTree(node: TreeNode, prefix = "", isLast = true): string[] {
|
|
22
|
-
const lines: string[] = [];
|
|
23
|
-
const connector = prefix === "" ? "" : isLast ? "└── " : "├── ";
|
|
24
|
-
const base = node.is_directory ? `${node.name}/` : node.name;
|
|
25
|
-
const label = node.is_symlink ? `${base} -> (symlink)` : base;
|
|
26
|
-
lines.push(`${prefix}${connector}${label}`);
|
|
27
|
-
if (node.is_directory && node.children) {
|
|
28
|
-
const childPrefix =
|
|
29
|
-
prefix + (prefix === "" ? "" : isLast ? " " : "│ ");
|
|
30
|
-
const children = node.children;
|
|
31
|
-
children.forEach((c, i) => {
|
|
32
|
-
const last = i === children.length - 1;
|
|
33
|
-
lines.push(...renderTree(c, childPrefix, last));
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
return lines;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function countItems(node: TreeNode): number {
|
|
40
|
-
if (!node.is_directory) return 1;
|
|
41
|
-
let total = 0;
|
|
42
|
-
for (const c of node.children ?? []) total += countItems(c);
|
|
43
|
-
return total;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const inputSchema = z.object({
|
|
47
|
-
path: z
|
|
48
|
-
.string()
|
|
49
|
-
.optional()
|
|
50
|
-
.default("")
|
|
51
|
-
.describe(
|
|
52
|
-
"Directory path under context/ to render (defaults to the context root).",
|
|
53
|
-
),
|
|
54
|
-
max_depth: z
|
|
55
|
-
.number()
|
|
56
|
-
.int()
|
|
57
|
-
.positive()
|
|
58
|
-
.optional()
|
|
59
|
-
.default(DEFAULT_MAX_DEPTH)
|
|
60
|
-
.describe(
|
|
61
|
-
`Maximum depth of directories to render (defaults to ${DEFAULT_MAX_DEPTH}).`,
|
|
62
|
-
),
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
const outputSchema = z.object({
|
|
66
|
-
tree: z.string(),
|
|
67
|
-
total_items: z.number(),
|
|
68
|
-
is_error: z.boolean(),
|
|
69
|
-
error_type: z.string().optional(),
|
|
70
|
-
message: z.string().optional(),
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
export const contextTreeTool = {
|
|
74
|
-
name: "context_tree",
|
|
75
|
-
description:
|
|
76
|
-
"[[ bash equivalent command: tree ]] Render the file tree under context/ (or a sub-directory). Symlinks are followed for listing and indexing; entries that are symlinks are tagged ' -> (symlink)' in the output.",
|
|
77
|
-
group: "context",
|
|
78
|
-
inputSchema,
|
|
79
|
-
outputSchema,
|
|
80
|
-
execute: async (input, ctx) => {
|
|
81
|
-
try {
|
|
82
|
-
const node = await buildTree(
|
|
83
|
-
ctx.projectDir,
|
|
84
|
-
input.path ?? "",
|
|
85
|
-
input.max_depth,
|
|
86
|
-
);
|
|
87
|
-
return {
|
|
88
|
-
tree: renderTree(node).join("\n"),
|
|
89
|
-
total_items: countItems(node),
|
|
90
|
-
is_error: false,
|
|
91
|
-
};
|
|
92
|
-
} catch (err) {
|
|
93
|
-
if (err instanceof NotFoundError) {
|
|
94
|
-
return {
|
|
95
|
-
tree: "",
|
|
96
|
-
total_items: 0,
|
|
97
|
-
is_error: true,
|
|
98
|
-
error_type: "not_found",
|
|
99
|
-
message: `No path at context/${err.path}`,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
throw err;
|
|
103
|
-
}
|
|
104
|
-
},
|
|
105
|
-
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Convenience for callers that want a string tree from outside the tool layer.
|
|
109
|
-
*/
|
|
110
|
-
export async function buildContextTree(
|
|
111
|
-
projectDir: string,
|
|
112
|
-
opts: { path?: string; maxDepth?: number } = {},
|
|
113
|
-
): Promise<BuildContextTreeResult> {
|
|
114
|
-
const node = await buildTree(
|
|
115
|
-
projectDir,
|
|
116
|
-
opts.path ?? "",
|
|
117
|
-
opts.maxDepth ?? DEFAULT_MAX_DEPTH,
|
|
118
|
-
);
|
|
119
|
-
return {
|
|
120
|
-
tree: renderTree(node).join("\n"),
|
|
121
|
-
total_items: countItems(node),
|
|
122
|
-
hint: "",
|
|
123
|
-
};
|
|
124
|
-
}
|
package/src/tools/file/copy.ts
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
copyContextPath,
|
|
4
|
-
deleteContextPath,
|
|
5
|
-
fileExists,
|
|
6
|
-
IsDirectoryError,
|
|
7
|
-
NotFoundError,
|
|
8
|
-
PathConflictError,
|
|
9
|
-
} from "../../context/store.ts";
|
|
10
|
-
import type { ToolDefinition } from "../tool.ts";
|
|
11
|
-
|
|
12
|
-
const inputSchema = z.object({
|
|
13
|
-
src: z.string().describe("Source path under context/"),
|
|
14
|
-
dst: z.string().describe("Destination path under context/"),
|
|
15
|
-
overwrite: z.boolean().optional().describe("Overwrite if destination exists"),
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
const outputSchema = z.object({
|
|
19
|
-
src: z.string(),
|
|
20
|
-
dst: z.string(),
|
|
21
|
-
is_error: z.boolean(),
|
|
22
|
-
error_type: z.string().optional(),
|
|
23
|
-
message: z.string().optional(),
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
export const contextCopyTool = {
|
|
27
|
-
name: "context_copy",
|
|
28
|
-
description:
|
|
29
|
-
"[[ bash equivalent command: cp ]] Copy a file under context/ to a new path. Source/destination paths that traverse a user symlink fail with PathEscapeError.",
|
|
30
|
-
group: "context",
|
|
31
|
-
inputSchema,
|
|
32
|
-
outputSchema,
|
|
33
|
-
execute: async (input, ctx) => {
|
|
34
|
-
try {
|
|
35
|
-
if (input.overwrite && (await fileExists(ctx.projectDir, input.dst))) {
|
|
36
|
-
await deleteContextPath(ctx.projectDir, input.dst, {
|
|
37
|
-
holderId: ctx.workerId,
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
await copyContextPath(ctx.projectDir, input.src, input.dst);
|
|
41
|
-
return { src: input.src, dst: input.dst, is_error: false };
|
|
42
|
-
} catch (err) {
|
|
43
|
-
if (err instanceof NotFoundError) {
|
|
44
|
-
return {
|
|
45
|
-
src: input.src,
|
|
46
|
-
dst: input.dst,
|
|
47
|
-
is_error: true,
|
|
48
|
-
error_type: "not_found",
|
|
49
|
-
message: `No file at context/${err.path}`,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
if (err instanceof PathConflictError) {
|
|
53
|
-
return {
|
|
54
|
-
src: input.src,
|
|
55
|
-
dst: input.dst,
|
|
56
|
-
is_error: true,
|
|
57
|
-
error_type: "path_conflict",
|
|
58
|
-
message: `Destination already exists at context/${err.path}; pass overwrite=true to replace.`,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
if (err instanceof IsDirectoryError) {
|
|
62
|
-
return {
|
|
63
|
-
src: input.src,
|
|
64
|
-
dst: input.dst,
|
|
65
|
-
is_error: true,
|
|
66
|
-
error_type: "is_directory",
|
|
67
|
-
message: `Source is a directory: context/${err.path}. Copy is file-only.`,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
throw err;
|
|
71
|
-
}
|
|
72
|
-
},
|
|
73
|
-
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
IsDirectoryError,
|
|
4
|
-
NotFoundError,
|
|
5
|
-
readContextFile,
|
|
6
|
-
} from "../../context/store.ts";
|
|
7
|
-
import type { ToolDefinition } from "../tool.ts";
|
|
8
|
-
|
|
9
|
-
const inputSchema = z.object({
|
|
10
|
-
path: z.string().describe("Path under context/"),
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
const outputSchema = z.object({
|
|
14
|
-
lines: z.number(),
|
|
15
|
-
is_error: z.boolean(),
|
|
16
|
-
error_type: z.string().optional(),
|
|
17
|
-
message: z.string().optional(),
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
export const contextCountLinesTool = {
|
|
21
|
-
name: "context_count_lines",
|
|
22
|
-
description:
|
|
23
|
-
"[[ bash equivalent command: wc -l ]] Count the number of lines in a text file under context/.",
|
|
24
|
-
group: "context",
|
|
25
|
-
inputSchema,
|
|
26
|
-
outputSchema,
|
|
27
|
-
execute: async (input, ctx) => {
|
|
28
|
-
try {
|
|
29
|
-
const content = await readContextFile(ctx.projectDir, input.path);
|
|
30
|
-
return {
|
|
31
|
-
lines: content === "" ? 0 : content.split("\n").length,
|
|
32
|
-
is_error: false,
|
|
33
|
-
};
|
|
34
|
-
} catch (err) {
|
|
35
|
-
if (err instanceof NotFoundError) {
|
|
36
|
-
return {
|
|
37
|
-
lines: 0,
|
|
38
|
-
is_error: true,
|
|
39
|
-
error_type: "not_found",
|
|
40
|
-
message: `No file at context/${err.path}`,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
if (err instanceof IsDirectoryError) {
|
|
44
|
-
return {
|
|
45
|
-
lines: 0,
|
|
46
|
-
is_error: true,
|
|
47
|
-
error_type: "is_directory",
|
|
48
|
-
message: `context/${err.path} is a directory`,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
throw err;
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
|
-
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
package/src/tools/file/delete.ts
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
deleteContextPath,
|
|
4
|
-
IsDirectoryError,
|
|
5
|
-
NotFoundError,
|
|
6
|
-
} from "../../context/store.ts";
|
|
7
|
-
import type { ToolDefinition } from "../tool.ts";
|
|
8
|
-
|
|
9
|
-
const inputSchema = z.object({
|
|
10
|
-
path: z.string().describe("Path under context/ to delete"),
|
|
11
|
-
recursive: z
|
|
12
|
-
.boolean()
|
|
13
|
-
.optional()
|
|
14
|
-
.describe("Delete a directory and its contents recursively"),
|
|
15
|
-
force: z
|
|
16
|
-
.boolean()
|
|
17
|
-
.optional()
|
|
18
|
-
.describe("Do not error if the path does not exist"),
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
const outputSchema = z.object({
|
|
22
|
-
deleted: z.number(),
|
|
23
|
-
was_directory: z.boolean(),
|
|
24
|
-
was_symlink: z.boolean(),
|
|
25
|
-
is_error: z.boolean(),
|
|
26
|
-
error_type: z.string().optional(),
|
|
27
|
-
message: z.string().optional(),
|
|
28
|
-
next_action_hint: z.string().optional(),
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
export const contextDeleteTool = {
|
|
32
|
-
name: "context_delete",
|
|
33
|
-
description:
|
|
34
|
-
"[[ bash equivalent command: rm -r ]] Delete a file or (with recursive=true) a directory under context/. Symlinks are unlinked without touching their target — `recursive` is not required for a symlinked directory.",
|
|
35
|
-
group: "context",
|
|
36
|
-
inputSchema,
|
|
37
|
-
outputSchema,
|
|
38
|
-
execute: async (input, ctx) => {
|
|
39
|
-
try {
|
|
40
|
-
const result = await deleteContextPath(ctx.projectDir, input.path, {
|
|
41
|
-
recursive: input.recursive,
|
|
42
|
-
holderId: ctx.workerId,
|
|
43
|
-
});
|
|
44
|
-
return {
|
|
45
|
-
deleted: result.removed,
|
|
46
|
-
was_directory: result.was_directory,
|
|
47
|
-
was_symlink: result.was_symlink,
|
|
48
|
-
is_error: false,
|
|
49
|
-
};
|
|
50
|
-
} catch (err) {
|
|
51
|
-
if (err instanceof NotFoundError) {
|
|
52
|
-
if (input.force) {
|
|
53
|
-
return {
|
|
54
|
-
deleted: 0,
|
|
55
|
-
was_directory: false,
|
|
56
|
-
was_symlink: false,
|
|
57
|
-
is_error: false,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
return {
|
|
61
|
-
deleted: 0,
|
|
62
|
-
was_directory: false,
|
|
63
|
-
was_symlink: false,
|
|
64
|
-
is_error: true,
|
|
65
|
-
error_type: "not_found",
|
|
66
|
-
message: `No file at context/${err.path}`,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
if (err instanceof IsDirectoryError) {
|
|
70
|
-
return {
|
|
71
|
-
deleted: 0,
|
|
72
|
-
was_directory: true,
|
|
73
|
-
was_symlink: false,
|
|
74
|
-
is_error: true,
|
|
75
|
-
error_type: "is_directory",
|
|
76
|
-
message: `context/${err.path} is a directory`,
|
|
77
|
-
next_action_hint: "Pass recursive=true to delete a directory tree.",
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
throw err;
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|