botholomew 0.1.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/package.json +42 -0
- package/src/cli.ts +45 -0
- package/src/commands/chat.ts +11 -0
- package/src/commands/check-update.ts +62 -0
- package/src/commands/context.ts +27 -0
- package/src/commands/daemon.ts +61 -0
- package/src/commands/init.ts +19 -0
- package/src/commands/mcpx.ts +29 -0
- package/src/commands/task.ts +126 -0
- package/src/commands/tools.ts +257 -0
- package/src/commands/upgrade.ts +185 -0
- package/src/config/loader.ts +31 -0
- package/src/config/schemas.ts +15 -0
- package/src/constants.ts +44 -0
- package/src/daemon/index.ts +39 -0
- package/src/daemon/llm.ts +186 -0
- package/src/daemon/prompt.ts +55 -0
- package/src/daemon/run.ts +14 -0
- package/src/daemon/spawn.ts +38 -0
- package/src/daemon/tick.ts +70 -0
- package/src/db/connection.ts +32 -0
- package/src/db/context.ts +415 -0
- package/src/db/embeddings.ts +22 -0
- package/src/db/schedules.ts +17 -0
- package/src/db/schema.ts +66 -0
- package/src/db/sql/1-core_tables.sql +53 -0
- package/src/db/sql/2-logging_tables.sql +24 -0
- package/src/db/sql/3-daemon_state.sql +5 -0
- package/src/db/tasks.ts +194 -0
- package/src/db/threads.ts +202 -0
- package/src/db/uuid.ts +1 -0
- package/src/init/index.ts +84 -0
- package/src/init/templates.ts +48 -0
- package/src/tools/dir/create.ts +39 -0
- package/src/tools/dir/list.ts +87 -0
- package/src/tools/dir/size.ts +45 -0
- package/src/tools/dir/tree.ts +91 -0
- package/src/tools/file/copy.ts +30 -0
- package/src/tools/file/count-lines.ts +26 -0
- package/src/tools/file/delete.ts +43 -0
- package/src/tools/file/edit.ts +40 -0
- package/src/tools/file/exists.ts +23 -0
- package/src/tools/file/info.ts +50 -0
- package/src/tools/file/move.ts +29 -0
- package/src/tools/file/read.ts +40 -0
- package/src/tools/file/write.ts +90 -0
- package/src/tools/registry.ts +53 -0
- package/src/tools/search/grep.ts +94 -0
- package/src/tools/search/semantic.ts +40 -0
- package/src/tools/task/complete.ts +23 -0
- package/src/tools/task/create.ts +42 -0
- package/src/tools/task/fail.ts +22 -0
- package/src/tools/task/wait.ts +23 -0
- package/src/tools/tool.ts +73 -0
- package/src/tui/App.tsx +14 -0
- package/src/types/istextorbinary.d.ts +10 -0
- package/src/update/background.ts +89 -0
- package/src/update/cache.ts +40 -0
- package/src/update/checker.ts +133 -0
- package/src/utils/frontmatter.ts +24 -0
- package/src/utils/logger.ts +29 -0
- package/src/utils/pid.ts +55 -0
package/src/db/tasks.ts
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import type { DbConnection } from "./connection.ts";
|
|
2
|
+
import { uuidv7 } from "./uuid.ts";
|
|
3
|
+
|
|
4
|
+
export const TASK_PRIORITIES = ["low", "medium", "high"] as const;
|
|
5
|
+
export const TASK_STATUSES = [
|
|
6
|
+
"pending",
|
|
7
|
+
"in_progress",
|
|
8
|
+
"failed",
|
|
9
|
+
"complete",
|
|
10
|
+
"waiting",
|
|
11
|
+
] as const;
|
|
12
|
+
|
|
13
|
+
export interface Task {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
priority: (typeof TASK_PRIORITIES)[number];
|
|
18
|
+
status: (typeof TASK_STATUSES)[number];
|
|
19
|
+
waiting_reason: string | null;
|
|
20
|
+
claimed_by: string | null;
|
|
21
|
+
claimed_at: Date | null;
|
|
22
|
+
blocked_by: string[];
|
|
23
|
+
context_ids: string[];
|
|
24
|
+
created_at: Date;
|
|
25
|
+
updated_at: Date;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface TaskRow {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
description: string;
|
|
32
|
+
priority: string;
|
|
33
|
+
status: string;
|
|
34
|
+
waiting_reason: string | null;
|
|
35
|
+
claimed_by: string | null;
|
|
36
|
+
claimed_at: string | null;
|
|
37
|
+
blocked_by: string;
|
|
38
|
+
context_ids: string;
|
|
39
|
+
created_at: string;
|
|
40
|
+
updated_at: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function rowToTask(row: TaskRow): Task {
|
|
44
|
+
return {
|
|
45
|
+
id: row.id,
|
|
46
|
+
name: row.name,
|
|
47
|
+
description: row.description,
|
|
48
|
+
priority: row.priority as Task["priority"],
|
|
49
|
+
status: row.status as Task["status"],
|
|
50
|
+
waiting_reason: row.waiting_reason,
|
|
51
|
+
claimed_by: row.claimed_by,
|
|
52
|
+
claimed_at: row.claimed_at ? new Date(row.claimed_at) : null,
|
|
53
|
+
blocked_by: JSON.parse(row.blocked_by || "[]"),
|
|
54
|
+
context_ids: JSON.parse(row.context_ids || "[]"),
|
|
55
|
+
created_at: new Date(row.created_at),
|
|
56
|
+
updated_at: new Date(row.updated_at),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function createTask(
|
|
61
|
+
db: DbConnection,
|
|
62
|
+
params: {
|
|
63
|
+
name: string;
|
|
64
|
+
description?: string;
|
|
65
|
+
priority?: Task["priority"];
|
|
66
|
+
blocked_by?: string[];
|
|
67
|
+
context_ids?: string[];
|
|
68
|
+
},
|
|
69
|
+
): Promise<Task> {
|
|
70
|
+
const id = uuidv7();
|
|
71
|
+
const blockedBy = JSON.stringify(params.blocked_by ?? []);
|
|
72
|
+
const contextIds = JSON.stringify(params.context_ids ?? []);
|
|
73
|
+
|
|
74
|
+
const row = db
|
|
75
|
+
.query(
|
|
76
|
+
`INSERT INTO tasks (id, name, description, priority, blocked_by, context_ids)
|
|
77
|
+
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
|
78
|
+
RETURNING *`,
|
|
79
|
+
)
|
|
80
|
+
.get(
|
|
81
|
+
id,
|
|
82
|
+
params.name,
|
|
83
|
+
params.description ?? "",
|
|
84
|
+
params.priority ?? "medium",
|
|
85
|
+
blockedBy,
|
|
86
|
+
contextIds,
|
|
87
|
+
) as TaskRow | null;
|
|
88
|
+
if (!row) throw new Error("INSERT did not return a row");
|
|
89
|
+
return rowToTask(row);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function getTask(
|
|
93
|
+
db: DbConnection,
|
|
94
|
+
id: string,
|
|
95
|
+
): Promise<Task | null> {
|
|
96
|
+
const row = db
|
|
97
|
+
.query("SELECT * FROM tasks WHERE id = ?1")
|
|
98
|
+
.get(id) as TaskRow | null;
|
|
99
|
+
return row ? rowToTask(row) : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function listTasks(
|
|
103
|
+
db: DbConnection,
|
|
104
|
+
filters?: {
|
|
105
|
+
status?: Task["status"];
|
|
106
|
+
priority?: Task["priority"];
|
|
107
|
+
limit?: number;
|
|
108
|
+
},
|
|
109
|
+
): Promise<Task[]> {
|
|
110
|
+
const conditions: string[] = [];
|
|
111
|
+
const params: string[] = [];
|
|
112
|
+
|
|
113
|
+
if (filters?.status) {
|
|
114
|
+
params.push(filters.status);
|
|
115
|
+
conditions.push(`status = ?${params.length}`);
|
|
116
|
+
}
|
|
117
|
+
if (filters?.priority) {
|
|
118
|
+
params.push(filters.priority);
|
|
119
|
+
conditions.push(`priority = ?${params.length}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const where =
|
|
123
|
+
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
124
|
+
const limit = filters?.limit ? `LIMIT ${filters.limit}` : "";
|
|
125
|
+
|
|
126
|
+
const rows = db
|
|
127
|
+
.query(
|
|
128
|
+
`SELECT * FROM tasks ${where}
|
|
129
|
+
ORDER BY
|
|
130
|
+
CASE priority WHEN 'high' THEN 0 WHEN 'medium' THEN 1 WHEN 'low' THEN 2 END,
|
|
131
|
+
created_at ASC
|
|
132
|
+
${limit}`,
|
|
133
|
+
)
|
|
134
|
+
.all(...params) as TaskRow[];
|
|
135
|
+
return rows.map(rowToTask);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function updateTaskStatus(
|
|
139
|
+
db: DbConnection,
|
|
140
|
+
id: string,
|
|
141
|
+
status: Task["status"],
|
|
142
|
+
reason?: string,
|
|
143
|
+
): Promise<void> {
|
|
144
|
+
db.query(
|
|
145
|
+
`UPDATE tasks
|
|
146
|
+
SET status = ?1, waiting_reason = ?2, updated_at = datetime('now')
|
|
147
|
+
WHERE id = ?3`,
|
|
148
|
+
).run(status, reason ?? null, id);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function claimNextTask(
|
|
152
|
+
db: DbConnection,
|
|
153
|
+
claimedBy = "daemon",
|
|
154
|
+
): Promise<Task | null> {
|
|
155
|
+
// Find highest-priority unblocked pending task
|
|
156
|
+
const row = db
|
|
157
|
+
.query(
|
|
158
|
+
`SELECT * FROM tasks
|
|
159
|
+
WHERE status = 'pending'
|
|
160
|
+
AND (
|
|
161
|
+
blocked_by = '[]'
|
|
162
|
+
OR blocked_by IS NULL
|
|
163
|
+
OR NOT EXISTS (
|
|
164
|
+
SELECT 1 FROM json_each(blocked_by) AS b
|
|
165
|
+
WHERE b.value NOT IN (SELECT id FROM tasks WHERE status = 'complete')
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
ORDER BY
|
|
169
|
+
CASE priority WHEN 'high' THEN 0 WHEN 'medium' THEN 1 WHEN 'low' THEN 2 END,
|
|
170
|
+
created_at ASC
|
|
171
|
+
LIMIT 1`,
|
|
172
|
+
)
|
|
173
|
+
.get() as TaskRow | null;
|
|
174
|
+
|
|
175
|
+
if (!row) return null;
|
|
176
|
+
const task = rowToTask(row);
|
|
177
|
+
|
|
178
|
+
// Claim it
|
|
179
|
+
db.query(
|
|
180
|
+
`UPDATE tasks
|
|
181
|
+
SET status = 'in_progress',
|
|
182
|
+
claimed_by = ?1,
|
|
183
|
+
claimed_at = datetime('now'),
|
|
184
|
+
updated_at = datetime('now')
|
|
185
|
+
WHERE id = ?2 AND status = 'pending'`,
|
|
186
|
+
).run(claimedBy, task.id);
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
...task,
|
|
190
|
+
status: "in_progress",
|
|
191
|
+
claimed_by: claimedBy,
|
|
192
|
+
claimed_at: new Date(),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { DbConnection } from "./connection.ts";
|
|
2
|
+
import { uuidv7 } from "./uuid.ts";
|
|
3
|
+
|
|
4
|
+
export interface Thread {
|
|
5
|
+
id: string;
|
|
6
|
+
type: "daemon_tick" | "chat_session";
|
|
7
|
+
task_id: string | null;
|
|
8
|
+
title: string;
|
|
9
|
+
started_at: Date;
|
|
10
|
+
ended_at: Date | null;
|
|
11
|
+
metadata: string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface Interaction {
|
|
15
|
+
id: string;
|
|
16
|
+
thread_id: string;
|
|
17
|
+
sequence: number;
|
|
18
|
+
role: "user" | "assistant" | "system" | "tool";
|
|
19
|
+
kind:
|
|
20
|
+
| "message"
|
|
21
|
+
| "thinking"
|
|
22
|
+
| "tool_use"
|
|
23
|
+
| "tool_result"
|
|
24
|
+
| "context_update"
|
|
25
|
+
| "status_change";
|
|
26
|
+
content: string;
|
|
27
|
+
tool_name: string | null;
|
|
28
|
+
tool_input: string | null;
|
|
29
|
+
duration_ms: number | null;
|
|
30
|
+
token_count: number | null;
|
|
31
|
+
created_at: Date;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface ThreadRow {
|
|
35
|
+
id: string;
|
|
36
|
+
type: string;
|
|
37
|
+
task_id: string | null;
|
|
38
|
+
title: string;
|
|
39
|
+
started_at: string;
|
|
40
|
+
ended_at: string | null;
|
|
41
|
+
metadata: string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface InteractionRow {
|
|
45
|
+
id: string;
|
|
46
|
+
thread_id: string;
|
|
47
|
+
sequence: number;
|
|
48
|
+
role: string;
|
|
49
|
+
kind: string;
|
|
50
|
+
content: string;
|
|
51
|
+
tool_name: string | null;
|
|
52
|
+
tool_input: string | null;
|
|
53
|
+
duration_ms: number | null;
|
|
54
|
+
token_count: number | null;
|
|
55
|
+
created_at: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function rowToThread(row: ThreadRow): Thread {
|
|
59
|
+
return {
|
|
60
|
+
id: row.id,
|
|
61
|
+
type: row.type as Thread["type"],
|
|
62
|
+
task_id: row.task_id,
|
|
63
|
+
title: row.title,
|
|
64
|
+
started_at: new Date(row.started_at),
|
|
65
|
+
ended_at: row.ended_at ? new Date(row.ended_at) : null,
|
|
66
|
+
metadata: row.metadata,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function rowToInteraction(row: InteractionRow): Interaction {
|
|
71
|
+
return {
|
|
72
|
+
id: row.id,
|
|
73
|
+
thread_id: row.thread_id,
|
|
74
|
+
sequence: row.sequence,
|
|
75
|
+
role: row.role as Interaction["role"],
|
|
76
|
+
kind: row.kind as Interaction["kind"],
|
|
77
|
+
content: row.content,
|
|
78
|
+
tool_name: row.tool_name,
|
|
79
|
+
tool_input: row.tool_input,
|
|
80
|
+
duration_ms: row.duration_ms,
|
|
81
|
+
token_count: row.token_count,
|
|
82
|
+
created_at: new Date(row.created_at),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function createThread(
|
|
87
|
+
db: DbConnection,
|
|
88
|
+
type: Thread["type"],
|
|
89
|
+
taskId?: string,
|
|
90
|
+
title?: string,
|
|
91
|
+
): Promise<string> {
|
|
92
|
+
const id = uuidv7();
|
|
93
|
+
db.query(
|
|
94
|
+
`INSERT INTO threads (id, type, task_id, title)
|
|
95
|
+
VALUES (?1, ?2, ?3, ?4)`,
|
|
96
|
+
).run(id, type, taskId ?? null, title ?? "");
|
|
97
|
+
return id;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function logInteraction(
|
|
101
|
+
db: DbConnection,
|
|
102
|
+
threadId: string,
|
|
103
|
+
params: {
|
|
104
|
+
role: Interaction["role"];
|
|
105
|
+
kind: Interaction["kind"];
|
|
106
|
+
content: string;
|
|
107
|
+
toolName?: string;
|
|
108
|
+
toolInput?: string;
|
|
109
|
+
durationMs?: number;
|
|
110
|
+
tokenCount?: number;
|
|
111
|
+
},
|
|
112
|
+
): Promise<string> {
|
|
113
|
+
// Get next sequence number
|
|
114
|
+
const seqRow = db
|
|
115
|
+
.query(
|
|
116
|
+
"SELECT COALESCE(MAX(sequence), 0) + 1 AS next_seq FROM interactions WHERE thread_id = ?1",
|
|
117
|
+
)
|
|
118
|
+
.get(threadId) as { next_seq: number };
|
|
119
|
+
const sequence = seqRow.next_seq;
|
|
120
|
+
|
|
121
|
+
const id = uuidv7();
|
|
122
|
+
db.query(
|
|
123
|
+
`INSERT INTO interactions (id, thread_id, sequence, role, kind, content, tool_name, tool_input, duration_ms, token_count)
|
|
124
|
+
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)`,
|
|
125
|
+
).run(
|
|
126
|
+
id,
|
|
127
|
+
threadId,
|
|
128
|
+
sequence,
|
|
129
|
+
params.role,
|
|
130
|
+
params.kind,
|
|
131
|
+
params.content,
|
|
132
|
+
params.toolName ?? null,
|
|
133
|
+
params.toolInput ?? null,
|
|
134
|
+
params.durationMs ?? null,
|
|
135
|
+
params.tokenCount ?? null,
|
|
136
|
+
);
|
|
137
|
+
return id;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function endThread(
|
|
141
|
+
db: DbConnection,
|
|
142
|
+
threadId: string,
|
|
143
|
+
): Promise<void> {
|
|
144
|
+
db.query("UPDATE threads SET ended_at = datetime('now') WHERE id = ?1").run(
|
|
145
|
+
threadId,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function getThread(
|
|
150
|
+
db: DbConnection,
|
|
151
|
+
threadId: string,
|
|
152
|
+
): Promise<{ thread: Thread; interactions: Interaction[] } | null> {
|
|
153
|
+
const threadRow = db
|
|
154
|
+
.query("SELECT * FROM threads WHERE id = ?1")
|
|
155
|
+
.get(threadId) as ThreadRow | null;
|
|
156
|
+
if (!threadRow) return null;
|
|
157
|
+
|
|
158
|
+
const interactionRows = db
|
|
159
|
+
.query(
|
|
160
|
+
"SELECT * FROM interactions WHERE thread_id = ?1 ORDER BY sequence ASC",
|
|
161
|
+
)
|
|
162
|
+
.all(threadId) as InteractionRow[];
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
thread: rowToThread(threadRow),
|
|
166
|
+
interactions: interactionRows.map(rowToInteraction),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function listThreads(
|
|
171
|
+
db: DbConnection,
|
|
172
|
+
filters?: {
|
|
173
|
+
type?: Thread["type"];
|
|
174
|
+
taskId?: string;
|
|
175
|
+
limit?: number;
|
|
176
|
+
},
|
|
177
|
+
): Promise<Thread[]> {
|
|
178
|
+
const conditions: string[] = [];
|
|
179
|
+
const params: string[] = [];
|
|
180
|
+
|
|
181
|
+
if (filters?.type) {
|
|
182
|
+
params.push(filters.type);
|
|
183
|
+
conditions.push(`type = ?${params.length}`);
|
|
184
|
+
}
|
|
185
|
+
if (filters?.taskId) {
|
|
186
|
+
params.push(filters.taskId);
|
|
187
|
+
conditions.push(`task_id = ?${params.length}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const where =
|
|
191
|
+
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
192
|
+
const limit = filters?.limit ? `LIMIT ${filters.limit}` : "";
|
|
193
|
+
|
|
194
|
+
const rows = db
|
|
195
|
+
.query(
|
|
196
|
+
`SELECT * FROM threads ${where}
|
|
197
|
+
ORDER BY started_at DESC
|
|
198
|
+
${limit}`,
|
|
199
|
+
)
|
|
200
|
+
.all(...params) as ThreadRow[];
|
|
201
|
+
return rows.map(rowToThread);
|
|
202
|
+
}
|
package/src/db/uuid.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { v7 as uuidv7 } from "uuid";
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getBotholomewDir, getDbPath, getMcpxDir } from "../constants.ts";
|
|
4
|
+
import { getConnection } from "../db/connection.ts";
|
|
5
|
+
import { migrate } from "../db/schema.ts";
|
|
6
|
+
import { logger } from "../utils/logger.ts";
|
|
7
|
+
import {
|
|
8
|
+
BELIEFS_MD,
|
|
9
|
+
DEFAULT_CONFIG,
|
|
10
|
+
DEFAULT_MCPX_SERVERS,
|
|
11
|
+
GOALS_MD,
|
|
12
|
+
SOUL_MD,
|
|
13
|
+
} from "./templates.ts";
|
|
14
|
+
|
|
15
|
+
export async function initProject(
|
|
16
|
+
projectDir: string,
|
|
17
|
+
opts: { force?: boolean } = {},
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
const dotDir = getBotholomewDir(projectDir);
|
|
20
|
+
const mcpxDir = getMcpxDir(projectDir);
|
|
21
|
+
|
|
22
|
+
// Check if already initialized
|
|
23
|
+
const dirExists = await Bun.file(join(dotDir, "soul.md")).exists();
|
|
24
|
+
if (dirExists && !opts.force) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`.botholomew already initialized in ${projectDir}. Use --force to reinitialize.`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Create directories
|
|
31
|
+
await mkdir(dotDir, { recursive: true });
|
|
32
|
+
await mkdir(mcpxDir, { recursive: true });
|
|
33
|
+
|
|
34
|
+
// Write template files
|
|
35
|
+
await Bun.write(join(dotDir, "soul.md"), SOUL_MD);
|
|
36
|
+
await Bun.write(join(dotDir, "beliefs.md"), BELIEFS_MD);
|
|
37
|
+
await Bun.write(join(dotDir, "goals.md"), GOALS_MD);
|
|
38
|
+
|
|
39
|
+
// Write config (without API key)
|
|
40
|
+
await Bun.write(
|
|
41
|
+
join(dotDir, "config.json"),
|
|
42
|
+
`${JSON.stringify(DEFAULT_CONFIG, null, 2)}\n`,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// Write mcpx servers config
|
|
46
|
+
await Bun.write(
|
|
47
|
+
join(mcpxDir, "servers.json"),
|
|
48
|
+
`${JSON.stringify(DEFAULT_MCPX_SERVERS, null, 2)}\n`,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// Initialize database
|
|
52
|
+
const dbPath = getDbPath(projectDir);
|
|
53
|
+
const conn = getConnection(dbPath);
|
|
54
|
+
migrate(conn);
|
|
55
|
+
conn.close();
|
|
56
|
+
|
|
57
|
+
// Update .gitignore
|
|
58
|
+
await updateGitignore(projectDir);
|
|
59
|
+
|
|
60
|
+
logger.success("Initialized Botholomew project");
|
|
61
|
+
logger.dim(` Directory: ${dotDir}`);
|
|
62
|
+
logger.dim(` Database: ${dbPath}`);
|
|
63
|
+
logger.dim("");
|
|
64
|
+
logger.dim("Next steps:");
|
|
65
|
+
logger.dim(" 1. Set ANTHROPIC_API_KEY or add it to .botholomew/config.json");
|
|
66
|
+
logger.dim(" 2. Run 'botholomew task add' to create your first task");
|
|
67
|
+
logger.dim(" 3. Run 'botholomew daemon start' to start the daemon");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function updateGitignore(projectDir: string): Promise<void> {
|
|
71
|
+
const gitignorePath = join(projectDir, ".gitignore");
|
|
72
|
+
const file = Bun.file(gitignorePath);
|
|
73
|
+
|
|
74
|
+
let content = "";
|
|
75
|
+
if (await file.exists()) {
|
|
76
|
+
content = await file.text();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const entry = ".botholomew/";
|
|
80
|
+
if (content.includes(entry)) return;
|
|
81
|
+
|
|
82
|
+
const section = `\n# Botholomew (auto-generated)\n${entry}\n`;
|
|
83
|
+
await Bun.write(gitignorePath, `${content.trimEnd()}\n${section}`);
|
|
84
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export const SOUL_MD = `---
|
|
2
|
+
loading: always
|
|
3
|
+
agent-modification: false
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Soul
|
|
7
|
+
|
|
8
|
+
You are Botholomew, an AI agent for knowledge work. You help humans manage information, research topics, organize knowledge, and complete intellectual tasks.
|
|
9
|
+
|
|
10
|
+
You are thoughtful, thorough, and proactive. You work through your task queue methodically, prioritizing appropriately and asking for clarification when needed.
|
|
11
|
+
`;
|
|
12
|
+
|
|
13
|
+
export const BELIEFS_MD = `---
|
|
14
|
+
loading: always
|
|
15
|
+
agent-modification: true
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Beliefs
|
|
19
|
+
|
|
20
|
+
*These are things Botholomew has learned about the world and this project.*
|
|
21
|
+
*Botholomew updates this file as it learns.*
|
|
22
|
+
|
|
23
|
+
- I should be concise and clear in my work products.
|
|
24
|
+
- I should ask for help when I'm stuck rather than guessing.
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
export const GOALS_MD = `---
|
|
28
|
+
loading: always
|
|
29
|
+
agent-modification: true
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
# Goals
|
|
33
|
+
|
|
34
|
+
*These are the current goals for this project.*
|
|
35
|
+
*Botholomew updates this file as goals are completed or new ones are added.*
|
|
36
|
+
|
|
37
|
+
- Get set up and ready to help.
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
export const DEFAULT_CONFIG = {
|
|
41
|
+
model: "claude-sonnet-4-20250514",
|
|
42
|
+
tick_interval_seconds: 300,
|
|
43
|
+
max_tick_duration_seconds: 120,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const DEFAULT_MCPX_SERVERS = {
|
|
47
|
+
mcpServers: {},
|
|
48
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { contextPathExists, createContextItem } from "../../db/context.ts";
|
|
3
|
+
import type { ToolDefinition } from "../tool.ts";
|
|
4
|
+
|
|
5
|
+
const inputSchema = z.object({
|
|
6
|
+
path: z.string().describe("Directory path to create"),
|
|
7
|
+
parents: z
|
|
8
|
+
.boolean()
|
|
9
|
+
.optional()
|
|
10
|
+
.describe("Create parent directories as needed"),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const outputSchema = z.object({
|
|
14
|
+
created: z.boolean(),
|
|
15
|
+
path: z.string(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const dirCreateTool = {
|
|
19
|
+
name: "dir_create",
|
|
20
|
+
description: "Create a directory in the virtual filesystem.",
|
|
21
|
+
group: "dir",
|
|
22
|
+
inputSchema,
|
|
23
|
+
outputSchema,
|
|
24
|
+
execute: async (input, ctx) => {
|
|
25
|
+
const exists = await contextPathExists(ctx.conn, input.path);
|
|
26
|
+
if (exists) {
|
|
27
|
+
return { created: false, path: input.path };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
await createContextItem(ctx.conn, {
|
|
31
|
+
title: input.path.split("/").filter(Boolean).pop() ?? input.path,
|
|
32
|
+
contextPath: input.path,
|
|
33
|
+
mimeType: "inode/directory",
|
|
34
|
+
isTextual: false,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return { created: true, path: input.path };
|
|
38
|
+
},
|
|
39
|
+
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
getDistinctDirectories,
|
|
4
|
+
listContextItemsByPrefix,
|
|
5
|
+
} from "../../db/context.ts";
|
|
6
|
+
import type { ToolDefinition } from "../tool.ts";
|
|
7
|
+
|
|
8
|
+
const DirEntrySchema = z.object({
|
|
9
|
+
name: z.string(),
|
|
10
|
+
type: z.enum(["file", "directory"]),
|
|
11
|
+
size: z.number(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const inputSchema = z.object({
|
|
15
|
+
path: z.string().optional().describe("Directory path (defaults to /)"),
|
|
16
|
+
recursive: z
|
|
17
|
+
.boolean()
|
|
18
|
+
.optional()
|
|
19
|
+
.default(true)
|
|
20
|
+
.describe("Include contents of subdirectories (defaults to true)"),
|
|
21
|
+
limit: z
|
|
22
|
+
.number()
|
|
23
|
+
.optional()
|
|
24
|
+
.default(100)
|
|
25
|
+
.describe("Maximum number of entries to return (defaults to 100)"),
|
|
26
|
+
offset: z
|
|
27
|
+
.number()
|
|
28
|
+
.optional()
|
|
29
|
+
.default(0)
|
|
30
|
+
.describe("Number of entries to skip (defaults to 0)"),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const outputSchema = z.object({
|
|
34
|
+
entries: z.array(DirEntrySchema),
|
|
35
|
+
total: z.number(),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export const dirListTool = {
|
|
39
|
+
name: "dir_list",
|
|
40
|
+
description: "List directory contents in the virtual filesystem.",
|
|
41
|
+
group: "dir",
|
|
42
|
+
inputSchema,
|
|
43
|
+
outputSchema,
|
|
44
|
+
execute: async (input, ctx) => {
|
|
45
|
+
const path = input.path ?? "/";
|
|
46
|
+
const recursive = input.recursive ?? true;
|
|
47
|
+
const limit = input.limit ?? 100;
|
|
48
|
+
const offset = input.offset ?? 0;
|
|
49
|
+
const normalizedPath = path.endsWith("/") ? path : `${path}/`;
|
|
50
|
+
|
|
51
|
+
const allItems = await listContextItemsByPrefix(ctx.conn, path, {
|
|
52
|
+
recursive,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const entries: z.infer<typeof DirEntrySchema>[] = allItems.map((item) => ({
|
|
56
|
+
name: recursive
|
|
57
|
+
? item.context_path
|
|
58
|
+
: item.context_path.slice(normalizedPath.length),
|
|
59
|
+
type:
|
|
60
|
+
item.mime_type === "inode/directory"
|
|
61
|
+
? ("directory" as const)
|
|
62
|
+
: ("file" as const),
|
|
63
|
+
size: item.content?.length ?? 0,
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
// Add subdirectories (if not recursive, show immediate child dirs)
|
|
67
|
+
if (!recursive) {
|
|
68
|
+
const dirs = await getDistinctDirectories(ctx.conn, path);
|
|
69
|
+
for (const dir of dirs) {
|
|
70
|
+
const name = dir.slice(normalizedPath.length);
|
|
71
|
+
if (!entries.some((e) => e.name === name)) {
|
|
72
|
+
entries.push({ name, type: "directory", size: 0 });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
entries.sort((a, b) => {
|
|
78
|
+
if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
|
|
79
|
+
return a.name.localeCompare(b.name);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const total = entries.length;
|
|
83
|
+
const paginated = entries.slice(offset, offset + limit);
|
|
84
|
+
|
|
85
|
+
return { entries: paginated, total };
|
|
86
|
+
},
|
|
87
|
+
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { listContextItemsByPrefix } from "../../db/context.ts";
|
|
3
|
+
import type { ToolDefinition } from "../tool.ts";
|
|
4
|
+
|
|
5
|
+
function formatBytes(bytes: number): string {
|
|
6
|
+
if (bytes === 0) return "0 B";
|
|
7
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
8
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
9
|
+
const value = bytes / 1024 ** i;
|
|
10
|
+
return `${value.toFixed(i > 0 ? 1 : 0)} ${units[i]}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const inputSchema = z.object({
|
|
14
|
+
path: z.string().optional().describe("Directory path (defaults to /)"),
|
|
15
|
+
recursive: z
|
|
16
|
+
.boolean()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe("Include subdirectories (defaults to true)"),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const outputSchema = z.object({
|
|
22
|
+
bytes: z.number(),
|
|
23
|
+
formatted: z.string(),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const dirSizeTool = {
|
|
27
|
+
name: "dir_size",
|
|
28
|
+
description: "Get the total size of files in a directory.",
|
|
29
|
+
group: "dir",
|
|
30
|
+
inputSchema,
|
|
31
|
+
outputSchema,
|
|
32
|
+
execute: async (input, ctx) => {
|
|
33
|
+
const path = input.path ?? "/";
|
|
34
|
+
const items = await listContextItemsByPrefix(ctx.conn, path, {
|
|
35
|
+
recursive: input.recursive !== false,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
let bytes = 0;
|
|
39
|
+
for (const item of items) {
|
|
40
|
+
if (item.content != null) bytes += item.content.length;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { bytes, formatted: formatBytes(bytes) };
|
|
44
|
+
},
|
|
45
|
+
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|