botholomew 0.7.5 → 0.7.7

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 CHANGED
@@ -169,6 +169,9 @@ Topics worth understanding in detail:
169
169
 
170
170
  - **[Architecture](docs/architecture.md)** — daemon, chat, watchdog, and how
171
171
  they share a database.
172
+ - **[The TUI](docs/tui.md)** — the `botholomew chat` Ink/React terminal UI:
173
+ seven tabs, slash-command autocomplete, message queue, and tool-call
174
+ visualization.
172
175
  - **[The virtual filesystem](docs/virtual-filesystem.md)** — why the agent's
173
176
  "files" are actually DuckDB rows, and how `context_read`/`context_write` work.
174
177
  - **[Context & hybrid search](docs/context-and-search.md)** — LLM-driven
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botholomew",
3
- "version": "0.7.5",
3
+ "version": "0.7.7",
4
4
  "description": "Local, autonomous AI agent for knowledge work — works your task queue while you sleep.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -75,18 +75,30 @@ export async function startDaemon(
75
75
  ? buildForegroundCallbacks()
76
76
  : undefined;
77
77
 
78
- logger.info(`Daemon started for ${projectDir} (PID ${process.pid})`);
78
+ logger.info(
79
+ `Daemon started ${new Date().toISOString()} for ${projectDir} (PID ${process.pid})`,
80
+ );
79
81
  logger.info(`Tick interval: ${config.tick_interval_seconds}s`);
80
82
 
83
+ let tickNum = 0;
81
84
  while (true) {
85
+ tickNum++;
82
86
  let didWork = false;
83
87
  try {
84
- didWork = await tick(projectDir, conn, config, mcpxClient, callbacks);
88
+ didWork = await tick(
89
+ projectDir,
90
+ conn,
91
+ config,
92
+ mcpxClient,
93
+ callbacks,
94
+ tickNum,
95
+ );
85
96
  } catch (err) {
86
97
  logger.error(`Tick failed: ${err}`);
87
98
  }
88
99
 
89
100
  if (!didWork) {
101
+ logger.phase("sleeping", `${config.tick_interval_seconds}s`);
90
102
  await Bun.sleep(config.tick_interval_seconds * 1000);
91
103
  }
92
104
  }
@@ -1,6 +1,7 @@
1
1
  import type { McpxClient } from "@evantahler/mcpx";
2
2
  import type { BotholomewConfig } from "../config/schemas.ts";
3
3
  import type { DbConnection } from "../db/connection.ts";
4
+ import { listSchedules } from "../db/schedules.ts";
4
5
  import {
5
6
  claimNextTask,
6
7
  resetStaleTasks,
@@ -20,8 +21,10 @@ export async function tick(
20
21
  config: Required<BotholomewConfig>,
21
22
  mcpxClient?: McpxClient | null,
22
23
  callbacks?: DaemonStreamCallbacks,
24
+ tickNum = 1,
23
25
  ): Promise<boolean> {
24
- logger.debug("Tick starting...");
26
+ const tickStart = Date.now();
27
+ logger.phase("tick-start", `#${tickNum}`);
25
28
 
26
29
  // Reset stale tasks stuck in in_progress
27
30
  const resetIds = await resetStaleTasks(
@@ -35,20 +38,27 @@ export async function tick(
35
38
  }
36
39
 
37
40
  // Process schedules (may create new tasks)
38
- try {
39
- await processSchedules(conn, config);
40
- } catch (err) {
41
- logger.error(`Schedule processing failed: ${err}`);
41
+ const enabledSchedules = await listSchedules(conn, { enabled: true });
42
+ if (enabledSchedules.length > 0) {
43
+ logger.phase("evaluating-schedules", `${enabledSchedules.length} enabled`);
44
+ try {
45
+ await processSchedules(conn, config);
46
+ } catch (err) {
47
+ logger.error(`Schedule processing failed: ${err}`);
48
+ }
42
49
  }
43
50
 
44
51
  // Claim a task
52
+ logger.phase("claiming-task");
45
53
  const task = await claimNextTask(conn);
46
54
  if (!task) {
47
- logger.debug("No tasks to work on. Sleeping.");
55
+ logger.info("No task claimed (queue empty or all blocked)");
56
+ const elapsed = ((Date.now() - tickStart) / 1000).toFixed(1);
57
+ logger.phase("tick-end", `#${tickNum} ${elapsed}s didWork=false`);
48
58
  return false;
49
59
  }
50
60
 
51
- logger.info(`Working on task: ${task.name} (${task.id})`);
61
+ logger.info(`Claimed task: ${task.name} (${task.id})`);
52
62
  callbacks?.onTaskStart(task);
53
63
 
54
64
  // Create a thread for this tick
@@ -115,5 +125,8 @@ export async function tick(
115
125
  await endThread(conn, threadId);
116
126
  }
117
127
 
128
+ const elapsed = ((Date.now() - tickStart) / 1000).toFixed(1);
129
+ logger.phase("tick-end", `#${tickNum} ${elapsed}s didWork=true`);
130
+
118
131
  return true;
119
132
  }
@@ -1,29 +1,42 @@
1
1
  import ansis from "ansis";
2
2
 
3
+ function ts(): string {
4
+ return ansis.gray(new Date().toTimeString().slice(0, 8));
5
+ }
6
+
3
7
  export const logger = {
4
8
  info(msg: string) {
5
- console.log(ansis.blue("ℹ"), msg);
9
+ console.log(ts(), ansis.blue("ℹ"), msg);
6
10
  },
7
11
 
8
12
  success(msg: string) {
9
- console.log(ansis.green("✓"), msg);
13
+ console.log(ts(), ansis.green("✓"), msg);
10
14
  },
11
15
 
12
16
  warn(msg: string) {
13
- console.log(ansis.yellow("⚠"), msg);
17
+ console.log(ts(), ansis.yellow("⚠"), msg);
14
18
  },
15
19
 
16
20
  error(msg: string) {
17
- console.error(ansis.red("✗"), msg);
21
+ console.error(ts(), ansis.red("✗"), msg);
18
22
  },
19
23
 
20
24
  debug(msg: string) {
21
25
  if (process.env.BOTHOLOMEW_DEBUG) {
22
- console.log(ansis.gray("·"), ansis.gray(msg));
26
+ console.log(ts(), ansis.gray("·"), ansis.gray(msg));
23
27
  }
24
28
  },
25
29
 
26
30
  dim(msg: string) {
27
- console.log(ansis.dim(msg));
31
+ console.log(ts(), ansis.dim(msg));
32
+ },
33
+
34
+ phase(name: string, detail?: string) {
35
+ const tag = ansis.magenta.bold(`[[${name}]]`);
36
+ if (detail) {
37
+ console.log(ts(), tag, ansis.dim(detail));
38
+ } else {
39
+ console.log(ts(), tag);
40
+ }
28
41
  },
29
42
  };