botholomew 0.2.0 → 0.3.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botholomew",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "An AI agent for knowledge work",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,7 +18,7 @@
18
18
  "scripts": {
19
19
  "dev": "bun run src/cli.ts",
20
20
  "test": "bun test",
21
- "build": "bun build --compile --minify --sourcemap ./src/cli.ts --outfile dist/botholomew",
21
+ "build": "bun build --compile --minify --sourcemap --external react-devtools-core ./src/cli.ts --outfile dist/botholomew",
22
22
  "lint": "tsc --noEmit && biome check ."
23
23
  },
24
24
  "dependencies": {
@@ -30,6 +30,8 @@
30
30
  "commander": "^14.0.0",
31
31
  "gray-matter": "^4.0.3",
32
32
  "ink": "^6.0.0",
33
+ "ink-spinner": "^5.0.0",
34
+ "ink-text-input": "^6.0.0",
33
35
  "istextorbinary": "^9.5.0",
34
36
  "react": "^19.1.0",
35
37
  "uuid": "^13.0.0",
@@ -0,0 +1,216 @@
1
+ import Anthropic from "@anthropic-ai/sdk";
2
+ import type {
3
+ MessageParam,
4
+ ToolResultBlockParam,
5
+ ToolUseBlock,
6
+ } from "@anthropic-ai/sdk/resources/messages";
7
+ import type { BotholomewConfig } from "../config/schemas.ts";
8
+ import { buildMetaHeader, loadPersistentContext } from "../daemon/prompt.ts";
9
+ import type { DbConnection } from "../db/connection.ts";
10
+ import { logInteraction } from "../db/threads.ts";
11
+ import { registerAllTools } from "../tools/registry.ts";
12
+ import {
13
+ getAllTools,
14
+ getTool,
15
+ type ToolContext,
16
+ toAnthropicTool,
17
+ } from "../tools/tool.ts";
18
+
19
+ registerAllTools();
20
+
21
+ /** Tools available in chat mode — no daemon terminal tools, no destructive file tools */
22
+ const CHAT_TOOL_NAMES = new Set([
23
+ "create_task",
24
+ "list_tasks",
25
+ "view_task",
26
+ "search_context",
27
+ "search_grep",
28
+ "search_semantic",
29
+ "list_threads",
30
+ "view_thread",
31
+ "create_schedule",
32
+ "list_schedules",
33
+ "update_beliefs",
34
+ "update_goals",
35
+ "mcp_list_tools",
36
+ "mcp_search",
37
+ "mcp_info",
38
+ "mcp_exec",
39
+ ]);
40
+
41
+ export function getChatTools() {
42
+ return getAllTools()
43
+ .filter((t) => CHAT_TOOL_NAMES.has(t.name))
44
+ .map(toAnthropicTool);
45
+ }
46
+
47
+ export async function buildChatSystemPrompt(
48
+ projectDir: string,
49
+ ): Promise<string> {
50
+ const parts: string[] = [];
51
+
52
+ parts.push(...buildMetaHeader(projectDir));
53
+ parts.push(...(await loadPersistentContext(projectDir)));
54
+
55
+ parts.push("## Instructions");
56
+ 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.",
58
+ );
59
+ parts.push(
60
+ "You do NOT execute long-running work directly — enqueue tasks for the daemon instead using create_task.",
61
+ );
62
+ parts.push(
63
+ "Use the available tools to look up tasks, threads, schedules, and context when the user asks about them.",
64
+ );
65
+ parts.push(
66
+ "You can update the agent's beliefs and goals files when the user asks you to.",
67
+ );
68
+ parts.push(
69
+ "Format your responses using Markdown. Use headings, bold, italic, lists, and code blocks to make your responses clear and well-structured.",
70
+ );
71
+ parts.push("");
72
+
73
+ return parts.join("\n");
74
+ }
75
+
76
+ export interface ChatTurnCallbacks {
77
+ onToken: (text: string) => void;
78
+ onToolStart: (name: string, input: string) => void;
79
+ onToolEnd: (name: string, output: string) => void;
80
+ }
81
+
82
+ /**
83
+ * Run a single chat turn: stream the assistant response, execute any tool calls,
84
+ * and loop until the model produces end_turn with no tool calls.
85
+ * Mutates `messages` in-place by appending assistant/tool messages.
86
+ */
87
+ export async function runChatTurn(input: {
88
+ messages: MessageParam[];
89
+ systemPrompt: string;
90
+ config: Required<BotholomewConfig>;
91
+ conn: DbConnection;
92
+ threadId: string;
93
+ toolCtx: ToolContext;
94
+ callbacks: ChatTurnCallbacks;
95
+ }): Promise<void> {
96
+ const { messages, systemPrompt, config, conn, threadId, toolCtx, callbacks } =
97
+ input;
98
+
99
+ const client = new Anthropic({
100
+ apiKey: config.anthropic_api_key || undefined,
101
+ });
102
+
103
+ const chatTools = getChatTools();
104
+ const maxTurns = 10;
105
+
106
+ for (let turn = 0; turn < maxTurns; turn++) {
107
+ const startTime = Date.now();
108
+
109
+ const stream = client.messages.stream({
110
+ model: config.model,
111
+ max_tokens: 4096,
112
+ system: systemPrompt,
113
+ messages,
114
+ tools: chatTools,
115
+ });
116
+
117
+ // Collect the full response
118
+ let assistantText = "";
119
+
120
+ stream.on("text", (text) => {
121
+ assistantText += text;
122
+ callbacks.onToken(text);
123
+ });
124
+
125
+ const response = await stream.finalMessage();
126
+ const durationMs = Date.now() - startTime;
127
+ const tokenCount =
128
+ response.usage.input_tokens + response.usage.output_tokens;
129
+
130
+ // Log assistant text
131
+ if (assistantText) {
132
+ await logInteraction(conn, threadId, {
133
+ role: "assistant",
134
+ kind: "message",
135
+ content: assistantText,
136
+ durationMs,
137
+ tokenCount,
138
+ });
139
+ }
140
+
141
+ // Check for tool calls
142
+ const toolUseBlocks = response.content.filter(
143
+ (block): block is ToolUseBlock => block.type === "tool_use",
144
+ );
145
+
146
+ if (toolUseBlocks.length === 0) {
147
+ // No tool calls — turn is complete
148
+ messages.push({ role: "assistant", content: response.content });
149
+ return;
150
+ }
151
+
152
+ // Add assistant response to conversation
153
+ messages.push({ role: "assistant", content: response.content });
154
+
155
+ // Execute tool calls
156
+ const toolResults: ToolResultBlockParam[] = [];
157
+
158
+ for (const toolUse of toolUseBlocks) {
159
+ const toolInput = JSON.stringify(toolUse.input);
160
+ callbacks.onToolStart(toolUse.name, toolInput);
161
+
162
+ await logInteraction(conn, threadId, {
163
+ role: "assistant",
164
+ kind: "tool_use",
165
+ content: `Calling ${toolUse.name}`,
166
+ toolName: toolUse.name,
167
+ toolInput,
168
+ });
169
+
170
+ const toolStart = Date.now();
171
+ const result = await executeChatToolCall(toolUse, toolCtx);
172
+ const toolDuration = Date.now() - toolStart;
173
+
174
+ await logInteraction(conn, threadId, {
175
+ role: "tool",
176
+ kind: "tool_result",
177
+ content: result,
178
+ toolName: toolUse.name,
179
+ durationMs: toolDuration,
180
+ });
181
+
182
+ callbacks.onToolEnd(toolUse.name, result);
183
+
184
+ toolResults.push({
185
+ type: "tool_result",
186
+ tool_use_id: toolUse.id,
187
+ content: result,
188
+ });
189
+ }
190
+
191
+ messages.push({ role: "user", content: toolResults });
192
+ // Loop to get the model's next response after tool results
193
+ }
194
+ }
195
+
196
+ async function executeChatToolCall(
197
+ toolUse: ToolUseBlock,
198
+ ctx: ToolContext,
199
+ ): Promise<string> {
200
+ const tool = getTool(toolUse.name);
201
+ if (!tool) return `Unknown tool: ${toolUse.name}`;
202
+ if (!CHAT_TOOL_NAMES.has(tool.name))
203
+ return `Tool not available in chat mode: ${tool.name}`;
204
+
205
+ const parsed = tool.inputSchema.safeParse(toolUse.input);
206
+ if (!parsed.success) {
207
+ return `Invalid input: ${JSON.stringify(parsed.error)}`;
208
+ }
209
+
210
+ try {
211
+ const result = await tool.execute(parsed.data, ctx);
212
+ return typeof result === "string" ? result : JSON.stringify(result);
213
+ } catch (err) {
214
+ return `Tool error: ${err}`;
215
+ }
216
+ }
@@ -0,0 +1,136 @@
1
+ import type { MessageParam } from "@anthropic-ai/sdk/resources/messages";
2
+ import { loadConfig } from "../config/loader.ts";
3
+ import type { BotholomewConfig } from "../config/schemas.ts";
4
+ import { getDbPath } from "../constants.ts";
5
+ import type { DbConnection } from "../db/connection.ts";
6
+ import { getConnection } from "../db/connection.ts";
7
+ import { migrate } from "../db/schema.ts";
8
+ import {
9
+ createThread,
10
+ endThread,
11
+ getThread,
12
+ logInteraction,
13
+ reopenThread,
14
+ } from "../db/threads.ts";
15
+ import { createMcpxClient } from "../mcpx/client.ts";
16
+ import type { ToolContext } from "../tools/tool.ts";
17
+ import {
18
+ buildChatSystemPrompt,
19
+ type ChatTurnCallbacks,
20
+ runChatTurn,
21
+ } from "./agent.ts";
22
+
23
+ export interface ChatSession {
24
+ conn: DbConnection;
25
+ threadId: string;
26
+ projectDir: string;
27
+ config: Required<BotholomewConfig>;
28
+ messages: MessageParam[];
29
+ systemPrompt: string;
30
+ toolCtx: ToolContext;
31
+ cleanup: () => Promise<void>;
32
+ }
33
+
34
+ export async function startChatSession(
35
+ projectDir: string,
36
+ existingThreadId?: string,
37
+ ): Promise<ChatSession> {
38
+ const config = await loadConfig(projectDir);
39
+
40
+ if (!config.anthropic_api_key) {
41
+ throw new Error(
42
+ "no API key found. add anthropic_api_key to .botholomew/config.json",
43
+ );
44
+ }
45
+
46
+ const conn = getConnection(getDbPath(projectDir));
47
+ migrate(conn);
48
+
49
+ let threadId: string;
50
+ const messages: MessageParam[] = [];
51
+
52
+ if (existingThreadId) {
53
+ // Resume existing thread
54
+ const result = await getThread(conn, existingThreadId);
55
+ if (!result) {
56
+ conn.close();
57
+ throw new Error(`Thread not found: ${existingThreadId}`);
58
+ }
59
+ threadId = existingThreadId;
60
+ await reopenThread(conn, threadId);
61
+
62
+ // Rebuild message history from interactions
63
+ for (const interaction of result.interactions) {
64
+ if (interaction.kind !== "message") continue;
65
+ if (interaction.role === "user") {
66
+ messages.push({ role: "user", content: interaction.content });
67
+ } else if (interaction.role === "assistant") {
68
+ messages.push({ role: "assistant", content: interaction.content });
69
+ }
70
+ }
71
+ } else {
72
+ threadId = await createThread(
73
+ conn,
74
+ "chat_session",
75
+ undefined,
76
+ "Interactive chat",
77
+ );
78
+ }
79
+
80
+ const systemPrompt = await buildChatSystemPrompt(projectDir);
81
+
82
+ const mcpxClient = await createMcpxClient(projectDir);
83
+
84
+ const toolCtx: ToolContext = {
85
+ conn,
86
+ projectDir,
87
+ config,
88
+ mcpxClient,
89
+ };
90
+
91
+ const cleanup = async () => {
92
+ await mcpxClient?.close();
93
+ };
94
+
95
+ return {
96
+ conn,
97
+ threadId,
98
+ projectDir,
99
+ config,
100
+ messages,
101
+ systemPrompt,
102
+ toolCtx,
103
+ cleanup,
104
+ };
105
+ }
106
+
107
+ export async function sendMessage(
108
+ session: ChatSession,
109
+ userMessage: string,
110
+ callbacks: ChatTurnCallbacks,
111
+ ): Promise<void> {
112
+ // Log and append user message
113
+ await logInteraction(session.conn, session.threadId, {
114
+ role: "user",
115
+ kind: "message",
116
+ content: userMessage,
117
+ });
118
+
119
+ session.messages.push({ role: "user", content: userMessage });
120
+
121
+ await runChatTurn({
122
+ messages: session.messages,
123
+ systemPrompt: session.systemPrompt,
124
+ config: session.config,
125
+ conn: session.conn,
126
+ threadId: session.threadId,
127
+ toolCtx: session.toolCtx,
128
+ callbacks,
129
+ });
130
+ }
131
+
132
+ export async function endChatSession(session: ChatSession): Promise<void> {
133
+ await endThread(session.conn, session.threadId);
134
+ await session.cleanup();
135
+ session.conn.close();
136
+ }
@@ -1,11 +1,37 @@
1
1
  import type { Command } from "commander";
2
- import { logger } from "../utils/logger.ts";
3
2
 
4
3
  export function registerChatCommand(program: Command) {
5
4
  program
6
5
  .command("chat")
7
- .description("Open the interactive chat TUI")
8
- .action(async () => {
9
- logger.warn("Chat TUI not yet implemented. Coming soon.");
6
+ .description(
7
+ "Open the interactive chat TUI\n\n" +
8
+ " Keyboard shortcuts:\n" +
9
+ " Enter Send message\n" +
10
+ " ⌥+Enter Insert newline (multiline input)\n" +
11
+ " ↑/↓ Browse input history\n\n" +
12
+ " Commands:\n" +
13
+ " /help Show keyboard shortcuts\n" +
14
+ " /tools Open tool call inspector\n" +
15
+ " /quit, /exit End the chat session",
16
+ )
17
+ .option("--thread-id <id>", "Resume an existing chat thread")
18
+ .action(async (opts: { threadId?: string }) => {
19
+ const { render } = await import("ink");
20
+ const React = await import("react");
21
+ const { App } = await import("../tui/App.tsx");
22
+ const dir = program.opts().dir;
23
+ const instance = render(
24
+ React.createElement(App, {
25
+ projectDir: dir,
26
+ threadId: opts.threadId,
27
+ }),
28
+ {
29
+ kittyKeyboard: {
30
+ mode: "enabled",
31
+ flags: ["disambiguateEscapeCodes", "reportEventTypes"],
32
+ },
33
+ },
34
+ );
35
+ await instance.waitUntilExit();
10
36
  });
11
37
  }
@@ -16,6 +16,11 @@ import { hybridSearch, initVectorSearch } from "../db/embeddings.ts";
16
16
  import { logger } from "../utils/logger.ts";
17
17
  import { withDb } from "./with-db.ts";
18
18
 
19
+ function fmtDate(d: Date): string {
20
+ const pad = (n: number) => String(n).padStart(2, "0");
21
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
22
+ }
23
+
19
24
  export function registerContextCommand(program: Command) {
20
25
  const ctx = program.command("context").description("Manage context items");
21
26
 
@@ -38,7 +43,7 @@ export function registerContextCommand(program: Command) {
38
43
  return;
39
44
  }
40
45
 
41
- const header = `${ansis.bold("Path".padEnd(40))} ${"Title".padEnd(25)} ${"Type".padEnd(20)} Indexed`;
46
+ const header = `${ansis.bold("Path".padEnd(40))} ${"Title".padEnd(25)} ${"Type".padEnd(20)} ${"Updated".padEnd(18)} Indexed`;
42
47
  console.log(header);
43
48
  console.log("-".repeat(header.length));
44
49
 
@@ -46,8 +51,9 @@ export function registerContextCommand(program: Command) {
46
51
  const indexed = item.indexed_at
47
52
  ? ansis.green("yes")
48
53
  : ansis.dim("no");
54
+ const updated = ansis.dim(fmtDate(item.updated_at).padEnd(18));
49
55
  console.log(
50
- `${item.context_path.padEnd(40)} ${item.title.slice(0, 24).padEnd(25)} ${item.mime_type.slice(0, 19).padEnd(20)} ${indexed}`,
56
+ `${item.context_path.padEnd(40)} ${item.title.slice(0, 24).padEnd(25)} ${item.mime_type.slice(0, 19).padEnd(20)} ${updated} ${indexed}`,
51
57
  );
52
58
  }
53
59
 
@@ -56,38 +62,45 @@ export function registerContextCommand(program: Command) {
56
62
  );
57
63
 
58
64
  ctx
59
- .command("add <path>")
60
- .description("Add a file or directory to context")
65
+ .command("add <paths...>")
66
+ .description("Add files or directories to context")
61
67
  .option("--prefix <prefix>", "virtual path prefix", "/")
62
- .action((path, opts) =>
68
+ .action((paths: string[], opts) =>
63
69
  withDb(program, async (conn, dir) => {
64
70
  const config = await loadConfig(dir);
65
71
  await warmupEmbedder();
66
72
 
67
- const resolvedPath = resolve(path);
68
- const info = await stat(resolvedPath);
69
-
70
73
  let added = 0;
71
74
  let chunks = 0;
72
75
 
73
- if (info.isDirectory()) {
74
- const entries = await walkDirectory(resolvedPath);
75
- for (const filePath of entries) {
76
- const relativePath = filePath.slice(resolvedPath.length);
77
- const contextPath = join(opts.prefix, relativePath);
78
- const count = await addFile(conn, config, filePath, contextPath);
76
+ for (const path of paths) {
77
+ const resolvedPath = resolve(path);
78
+ const info = await stat(resolvedPath);
79
+
80
+ if (info.isDirectory()) {
81
+ const entries = await walkDirectory(resolvedPath);
82
+ for (const filePath of entries) {
83
+ 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
+ }
90
+ }
91
+ } else {
92
+ const contextPath = join(opts.prefix, basename(resolvedPath));
93
+ const count = await addFile(
94
+ conn,
95
+ config,
96
+ resolvedPath,
97
+ contextPath,
98
+ );
79
99
  if (count >= 0) {
80
100
  added++;
81
101
  chunks += count;
82
102
  }
83
103
  }
84
- } else {
85
- const contextPath = join(opts.prefix, basename(resolvedPath));
86
- const count = await addFile(conn, config, resolvedPath, contextPath);
87
- if (count >= 0) {
88
- added++;
89
- chunks += count;
90
- }
91
104
  }
92
105
 
93
106
  logger.success(`Added ${added} file(s), ${chunks} chunk(s) indexed.`);
@@ -115,7 +128,9 @@ export function registerContextCommand(program: Command) {
115
128
  console.log(
116
129
  `${ansis.bold(`${i + 1}.`)} ${ansis.cyan(r.title)} ${ansis.dim(`(${score}%)`)}`,
117
130
  );
118
- console.log(` ${ansis.dim(r.source_path || r.context_item_id)}`);
131
+ console.log(
132
+ ` ${ansis.dim(r.source_path || r.context_item_id)} ${ansis.dim(fmtDate(r.created_at))}`,
133
+ );
119
134
  if (r.chunk_content) {
120
135
  const snippet = r.chunk_content.slice(0, 120).replace(/\n/g, " ");
121
136
  console.log(` ${snippet}...`);
@@ -13,35 +13,18 @@ const pkg = await Bun.file(
13
13
  new URL("../../package.json", import.meta.url),
14
14
  ).json();
15
15
 
16
- export async function buildSystemPrompt(
16
+ /**
17
+ * Load persistent context files from .botholomew/ directory.
18
+ * Returns an array of formatted string sections for "always" loaded files.
19
+ * If taskKeywords are provided, also includes "contextual" files that match.
20
+ */
21
+ export async function loadPersistentContext(
17
22
  projectDir: string,
18
- task?: Task,
19
- conn?: DbConnection,
20
- _config?: Required<BotholomewConfig>,
21
- options?: { hasMcpTools?: boolean },
22
- ): Promise<string> {
23
+ taskKeywords?: Set<string> | null,
24
+ ): Promise<string[]> {
23
25
  const dotDir = getBotholomewDir(projectDir);
24
26
  const parts: string[] = [];
25
27
 
26
- // Meta information
27
- parts.push(`# Botholomew v${pkg.version}`);
28
- parts.push(`Current time: ${new Date().toISOString()}`);
29
- parts.push(`Project directory: ${projectDir}`);
30
- parts.push(`OS: ${process.platform} ${process.arch}`);
31
- parts.push(`User: ${process.env.USER || process.env.USERNAME || "unknown"}`);
32
- parts.push("");
33
-
34
- // Build keyword set from task for contextual loading
35
- const taskKeywords = task
36
- ? new Set(
37
- `${task.name} ${task.description}`
38
- .toLowerCase()
39
- .split(/\s+/)
40
- .filter((w) => w.length > 3),
41
- )
42
- : null;
43
-
44
- // Load context files from .botholomew/
45
28
  try {
46
29
  const files = await readdir(dotDir);
47
30
  const mdFiles = files.filter((f) => f.endsWith(".md"));
@@ -56,7 +39,6 @@ export async function buildSystemPrompt(
56
39
  parts.push(content);
57
40
  parts.push("");
58
41
  } else if (meta.loading === "contextual" && taskKeywords) {
59
- // Include contextual files if keywords overlap with task
60
42
  const contentLower = content.toLowerCase();
61
43
  const hasOverlap = [...taskKeywords].some((kw) =>
62
44
  contentLower.includes(kw),
@@ -72,6 +54,48 @@ export async function buildSystemPrompt(
72
54
  // .botholomew dir might not have md files yet
73
55
  }
74
56
 
57
+ return parts;
58
+ }
59
+
60
+ /**
61
+ * Build common meta header (version, time, OS, user).
62
+ */
63
+ export function buildMetaHeader(projectDir: string): string[] {
64
+ return [
65
+ `# Botholomew v${pkg.version}`,
66
+ `Current time: ${new Date().toISOString()}`,
67
+ `Project directory: ${projectDir}`,
68
+ `OS: ${process.platform} ${process.arch}`,
69
+ `User: ${process.env.USER || process.env.USERNAME || "unknown"}`,
70
+ "",
71
+ ];
72
+ }
73
+
74
+ export async function buildSystemPrompt(
75
+ projectDir: string,
76
+ task?: Task,
77
+ conn?: DbConnection,
78
+ _config?: Required<BotholomewConfig>,
79
+ options?: { hasMcpTools?: boolean },
80
+ ): Promise<string> {
81
+ const parts: string[] = [];
82
+
83
+ // Meta information
84
+ parts.push(...buildMetaHeader(projectDir));
85
+
86
+ // Build keyword set from task for contextual loading
87
+ const taskKeywords = task
88
+ ? new Set(
89
+ `${task.name} ${task.description}`
90
+ .toLowerCase()
91
+ .split(/\s+/)
92
+ .filter((w) => w.length > 3),
93
+ )
94
+ : null;
95
+
96
+ // Load context files from .botholomew/
97
+ parts.push(...(await loadPersistentContext(projectDir, taskKeywords)));
98
+
75
99
  // Relevant context from embeddings search
76
100
  if (task && conn) {
77
101
  try {
package/src/db/threads.ts CHANGED
@@ -147,6 +147,13 @@ export async function endThread(
147
147
  );
148
148
  }
149
149
 
150
+ export async function reopenThread(
151
+ db: DbConnection,
152
+ threadId: string,
153
+ ): Promise<void> {
154
+ db.query("UPDATE threads SET ended_at = NULL WHERE id = ?1").run(threadId);
155
+ }
156
+
150
157
  export async function getThread(
151
158
  db: DbConnection,
152
159
  threadId: string,
package/src/init/index.ts CHANGED
@@ -36,7 +36,7 @@ export async function initProject(
36
36
  await Bun.write(join(dotDir, "beliefs.md"), BELIEFS_MD);
37
37
  await Bun.write(join(dotDir, "goals.md"), GOALS_MD);
38
38
 
39
- // Write config (without API key)
39
+ // Write config (with placeholder API key)
40
40
  await Bun.write(
41
41
  join(dotDir, "config.json"),
42
42
  `${JSON.stringify(DEFAULT_CONFIG, null, 2)}\n`,
@@ -38,7 +38,8 @@ agent-modification: true
38
38
  `;
39
39
 
40
40
  export const DEFAULT_CONFIG = {
41
- model: "claude-sonnet-4-20250514",
41
+ anthropic_api_key: "your-api-key-here",
42
+ model: "claude-opus-4-20250514",
42
43
  tick_interval_seconds: 300,
43
44
  max_tick_duration_seconds: 120,
44
45
  };