becki-mcp 1.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.
@@ -0,0 +1,109 @@
1
+ // runner.ts — Becki Core daemon orchestration (#191 sub-tasks 3-4 glue)
2
+ //
3
+ // When becki-mcp runs as the stdio MCP server (the normal Claude Code /
4
+ // Cursor invocation), it can ALSO start background work:
5
+ // 1. Project activity watchers (chokidar per registered project)
6
+ // 2. A daily AI session digest sweep (runs once at startup if it's been
7
+ // >20h, then every 12h thereafter)
8
+ //
9
+ // Wired here rather than in index.ts so the existing 4K-line MCP file
10
+ // stays focused on tool dispatch; this module owns the "Core daemon"
11
+ // concerns.
12
+ //
13
+ // Extractor + ingester are injected — runner.ts doesn't know whether
14
+ // extraction goes to Anthropic directly or proxies through the Becki
15
+ // backend. index.ts wires the actual transport at startup time.
16
+ import { BeckiCache } from "./db.js";
17
+ import { ProjectActivityWatcher } from "./project-activity.js";
18
+ import { runDigest } from "./ai-sessions.js";
19
+ const DIGEST_INTERVAL_MS = 12 * 60 * 60 * 1000; // 12h
20
+ const DIGEST_STARTUP_GRACE_MS = 20 * 60 * 60 * 1000; // run at boot if last digest >20h ago
21
+ const CONFIG_LAST_DIGEST = "core.lastDigestAt";
22
+ export class CoreRunner {
23
+ opts;
24
+ cache;
25
+ watcher = null;
26
+ digestTimer = null;
27
+ log;
28
+ running = false;
29
+ constructor(opts) {
30
+ this.opts = opts;
31
+ this.cache = new BeckiCache(opts.beckiHome);
32
+ this.log = opts.logger ?? (() => { });
33
+ }
34
+ /** Start watchers + schedule digests. Idempotent. */
35
+ async start() {
36
+ if (this.running)
37
+ return;
38
+ this.running = true;
39
+ this.watcher = new ProjectActivityWatcher({
40
+ cache: this.cache,
41
+ ingest: this.opts.ingest,
42
+ logger: this.log,
43
+ });
44
+ await this.watcher.startAll();
45
+ const lastDigestStr = this.cache.getConfig(CONFIG_LAST_DIGEST);
46
+ const lastDigest = lastDigestStr ? Number(lastDigestStr) : 0;
47
+ const sinceLast = Date.now() - lastDigest;
48
+ if (sinceLast > DIGEST_STARTUP_GRACE_MS) {
49
+ this.log(`core-runner: last digest ${Math.round(sinceLast / 3.6e6)}h ago; running on startup`);
50
+ // Don't block daemon startup on the digest — kick it off async.
51
+ void this.runOneDigest("daily");
52
+ }
53
+ this.digestTimer = setInterval(() => {
54
+ void this.runOneDigest("interval");
55
+ }, DIGEST_INTERVAL_MS);
56
+ // unref so the daemon can exit cleanly when the MCP stdio closes.
57
+ this.digestTimer.unref?.();
58
+ }
59
+ /** Manually trigger a digest (used by `becki-mcp digest` CLI). */
60
+ async runOneDigest(reason) {
61
+ this.log(`core-runner: starting digest (${reason})`);
62
+ try {
63
+ const result = await runDigest({
64
+ cache: this.cache,
65
+ extract: this.opts.extract,
66
+ ingest: this.opts.ingest,
67
+ logger: this.log,
68
+ });
69
+ this.cache.setConfig(CONFIG_LAST_DIGEST, String(Date.now()));
70
+ this.log(`core-runner: digest done — processed=${result.processed} ingested=${result.ingested} ` +
71
+ `errors=${result.errors} skipped=${result.skippedSettled + result.skippedNoGrowth + result.skippedTooSmall + result.skippedTokenCap}`);
72
+ }
73
+ catch (err) {
74
+ this.log(`core-runner: digest failed: ${err.message}`);
75
+ }
76
+ }
77
+ /** Manually trigger a historical bootstrap (used by `becki-mcp bootstrap` CLI). */
78
+ async runBootstrap(ageWindowDays = 90) {
79
+ this.log(`core-runner: starting historical bootstrap (${ageWindowDays}d)`);
80
+ try {
81
+ const result = await runDigest({
82
+ cache: this.cache,
83
+ extract: this.opts.extract,
84
+ ingest: this.opts.ingest,
85
+ isBootstrap: true,
86
+ ageWindowDays,
87
+ logger: this.log,
88
+ });
89
+ this.log(`core-runner: bootstrap done — processed=${result.processed} ingested=${result.ingested} errors=${result.errors}`);
90
+ }
91
+ catch (err) {
92
+ this.log(`core-runner: bootstrap failed: ${err.message}`);
93
+ }
94
+ }
95
+ async stop() {
96
+ if (!this.running)
97
+ return;
98
+ this.running = false;
99
+ if (this.digestTimer) {
100
+ clearInterval(this.digestTimer);
101
+ this.digestTimer = null;
102
+ }
103
+ if (this.watcher) {
104
+ await this.watcher.stopAll();
105
+ this.watcher = null;
106
+ }
107
+ this.cache.close();
108
+ }
109
+ }