botholomew 0.23.0 → 0.24.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 CHANGED
@@ -233,12 +233,13 @@ semantic search, append-only versioning, and URL refresh all live there.
233
233
  |---|---|
234
234
  | `botholomew init` | Initialize the current directory as a project (refuses on iCloud/Dropbox/NFS without `--force`) |
235
235
  | `botholomew status` | One-command dashboard: workers, task counts/claims, schedules (with a live "due?" check), quarantined files, store info. `--json` for scripting, `--no-evaluate` to skip the LLM schedule check |
236
- | `botholomew worker run\|start` | Run a worker (foreground or background); `--persist` for long-running, `--task-id <id>` to target one task |
236
+ | `botholomew worker run\|start` | Run a worker (foreground or background); `--persist` for long-running, `--task-id <id>` to target one task, `--unsafe` to bypass the tool-approval gate |
237
237
  | `botholomew worker list\|status\|stop\|kill\|reap` | Inspect and manage running workers |
238
- | `botholomew chat` | Interactive Ink/React TUI |
238
+ | `botholomew chat` | Interactive Ink/React TUI (`--unsafe` to bypass the tool-approval gate) |
239
239
  | `botholomew dream` | Reflect on recent threads — consolidate learnings into the knowledge store and update beliefs/goals (`--since`, `--dry-run`); also `/dream` in chat |
240
240
  | `botholomew task list\|add\|view\|update\|reset\|delete` | Manage the task queue (markdown files in `tasks/`) |
241
241
  | `botholomew schedule list\|add\|view\|enable\|disable\|trigger\|delete` | Recurring work (markdown files in `schedules/`) |
242
+ | `botholomew approval list\|view\|approve\|deny` | Review and decide pending outbound-tool approvals (markdown files in `approvals/`) |
242
243
  | `botholomew membot add\|ls\|tree\|read\|write\|search\|info\|versions\|diff\|refresh\|…` | Knowledge-store passthrough to [`membot`](https://github.com/evantahler/membot) — `--config` is resolved from `membot_scope` (default `~/.membot`) |
243
244
  | `botholomew membot import-global` | Seed the project from `~/.membot` (copies `index.duckdb` + `config.json` in) |
244
245
  | `botholomew capabilities` | Rescan built-in + MCPX tools and rewrite `prompts/capabilities.md` |
@@ -322,6 +323,9 @@ Topics worth understanding in detail:
322
323
  create, edit, and search them at runtime.
323
324
  - **[MCPX integration](docs/mcpx.md)** — configuring external servers and
324
325
  how MCP tools are merged into the agent's toolset.
326
+ - **[Approvals](docs/approvals.md)** — the human-in-the-loop gate on outbound
327
+ mcpx tool calls: default deny, the allowlist, the worker approval queue,
328
+ and `--unsafe`.
325
329
  - **[Configuration](docs/configuration.md)** — every key in `config.json`
326
330
  and its default.
327
331
  - **[Doc captures](docs/captures.md)** — how the screenshots and GIFs in
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botholomew",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "An autonomous AI agent for knowledge work — works your task queue while you sleep.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,7 +31,7 @@
31
31
  "dependencies": {
32
32
  "@ai-sdk/anthropic": "^3.0.81",
33
33
  "@ai-sdk/openai-compatible": "^2.0.48",
34
- "@evantahler/mcpx": "0.21.11",
34
+ "@evantahler/mcpx": "0.22.2",
35
35
  "ai": "^6.0.197",
36
36
  "ansis": "^4.3.1",
37
37
  "commander": "^15.0.0",
@@ -0,0 +1,36 @@
1
+ import { getTask, updateTaskStatus } from "../tasks/store.ts";
2
+ import { logger } from "../utils/logger.ts";
3
+ import type { Approval } from "./schema.ts";
4
+ import { decideApproval, getApproval } from "./store.ts";
5
+
6
+ /**
7
+ * Apply a human decision to a pending approval and re-queue its originating
8
+ * task. Shared by the CLI (`botholomew approval approve|deny`) and the chat
9
+ * TUI approvals panel so both behave identically:
10
+ * - mark the record approved/denied,
11
+ * - flip the parked task back to `pending` so a worker re-claims it; on the
12
+ * re-run the recorded decision short-circuits the gated call.
13
+ *
14
+ * Returns the updated approval, or null if it didn't exist / wasn't pending.
15
+ */
16
+ export async function decideAndRequeue(
17
+ projectDir: string,
18
+ id: string,
19
+ decision: "approved" | "denied",
20
+ decidedBy: string,
21
+ ): Promise<Approval | null> {
22
+ const existing = await getApproval(projectDir, id);
23
+ if (!existing || existing.status !== "pending") return null;
24
+ const decided = await decideApproval(projectDir, id, decision, decidedBy);
25
+ if (decided.task_id) {
26
+ const task = await getTask(projectDir, decided.task_id);
27
+ if (task) {
28
+ await updateTaskStatus(projectDir, decided.task_id, "pending");
29
+ } else {
30
+ logger.warn(
31
+ `Originating task ${decided.task_id} no longer exists; not re-queued.`,
32
+ );
33
+ }
34
+ }
35
+ return decided;
36
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Thrown by a worker's `onApprovalRequired` callback when a gated mcpx call has
3
+ * no decision yet (a fresh `approvals/<id>.md` was just written, or an existing
4
+ * one is still pending). It propagates out of `McpxClient.exec()` — unlike a
5
+ * `false` return, which mcpx turns into `ToolApprovalDeniedError`. `mcp_exec`
6
+ * catches it, signals the worker loop to park the task as `waiting`, and returns
7
+ * a structured "awaiting approval" result to the agent.
8
+ */
9
+ export class ApprovalPendingError extends Error {
10
+ readonly approvalId: string;
11
+ readonly server: string;
12
+ readonly tool: string;
13
+ constructor(approvalId: string, server: string, tool: string) {
14
+ super(
15
+ `Tool "${server}/${tool}" is awaiting human approval (${approvalId}).`,
16
+ );
17
+ this.name = "ApprovalPendingError";
18
+ this.approvalId = approvalId;
19
+ this.server = server;
20
+ this.tool = tool;
21
+ }
22
+ }
@@ -0,0 +1,48 @@
1
+ import { z } from "zod";
2
+
3
+ export const APPROVAL_STATUSES = ["pending", "approved", "denied"] as const;
4
+
5
+ export type ApprovalStatus = (typeof APPROVAL_STATUSES)[number];
6
+
7
+ /**
8
+ * Frontmatter validator for `approvals/<id>.md`. An approval record describes a
9
+ * single gated mcpx tool call a worker wanted to make. Strict so a hand-edited
10
+ * or stale file doesn't silently round-trip with bad data; a parse failure
11
+ * quarantines the file (skip, log) the same way tasks/schedules do.
12
+ *
13
+ * `call_key` is a stable hash of (server, tool, args) so a re-run of the same
14
+ * task can match the human's decision to the concrete call that was approved.
15
+ */
16
+ export const ApprovalFrontmatterSchema = z.object({
17
+ id: z.string().min(1),
18
+ status: z.enum(APPROVAL_STATUSES).default("pending"),
19
+ server: z.string(),
20
+ tool: z.string(),
21
+ /** JSON-encoded tool arguments (kept as a string to avoid nested-YAML quirks). */
22
+ args: z.string().default("{}"),
23
+ /** Stable hash of server+tool+args; how a re-run matches a prior decision. */
24
+ call_key: z.string(),
25
+ /** Originating task/thread/worker — null when the request came from chat. */
26
+ task_id: z.string().nullable().default(null),
27
+ thread_id: z.string().nullable().default(null),
28
+ worker_id: z.string().nullable().default(null),
29
+ /** Human-readable label for why the gate fired (e.g. "not-allowlisted"). */
30
+ reason: z.string().default(""),
31
+ created_at: z.string(),
32
+ updated_at: z.string(),
33
+ decided_at: z.string().nullable().default(null),
34
+ decided_by: z.string().nullable().default(null),
35
+ });
36
+
37
+ export type ApprovalFrontmatter = z.infer<typeof ApprovalFrontmatterSchema>;
38
+
39
+ /**
40
+ * In-memory approval representation: frontmatter parsed + filesystem mtime so
41
+ * callers can detect concurrent edits before committing a write.
42
+ */
43
+ export interface Approval extends ApprovalFrontmatter {
44
+ /** Filesystem mtime in epoch ms, used for atomic-write-if-unchanged. */
45
+ mtimeMs: number;
46
+ /** Markdown body (everything after the frontmatter). */
47
+ body: string;
48
+ }
@@ -0,0 +1,276 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readdir, unlink } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import matter from "gray-matter";
5
+ import { getApprovalsDir } from "../constants.ts";
6
+ import {
7
+ atomicWrite,
8
+ atomicWriteIfUnchanged,
9
+ readWithMtime,
10
+ } from "../fs/atomic.ts";
11
+ import { logger } from "../utils/logger.ts";
12
+ import { uuidv7 } from "../utils/uuid.ts";
13
+ import {
14
+ type Approval,
15
+ type ApprovalFrontmatter,
16
+ ApprovalFrontmatterSchema,
17
+ type ApprovalStatus,
18
+ } from "./schema.ts";
19
+
20
+ export function approvalFilePath(projectDir: string, id: string): string {
21
+ return join(getApprovalsDir(projectDir), `${id}.md`);
22
+ }
23
+
24
+ /**
25
+ * Stable key for an mcpx call: a hash over server + tool + canonicalized args
26
+ * (object keys sorted recursively) so the same logical call always produces
27
+ * the same key regardless of argument ordering.
28
+ */
29
+ export function callKey(
30
+ server: string,
31
+ tool: string,
32
+ args: Record<string, unknown> | undefined,
33
+ ): string {
34
+ const canonical = canonicalize(args ?? {});
35
+ const payload = JSON.stringify({ server, tool, args: canonical });
36
+ return createHash("sha256").update(payload).digest("hex").slice(0, 32);
37
+ }
38
+
39
+ function canonicalize(value: unknown): unknown {
40
+ if (Array.isArray(value)) return value.map(canonicalize);
41
+ if (value && typeof value === "object") {
42
+ const out: Record<string, unknown> = {};
43
+ for (const k of Object.keys(value as Record<string, unknown>).sort()) {
44
+ out[k] = canonicalize((value as Record<string, unknown>)[k]);
45
+ }
46
+ return out;
47
+ }
48
+ return value;
49
+ }
50
+
51
+ function approvalBody(fm: ApprovalFrontmatter): string {
52
+ let argsPretty = fm.args;
53
+ try {
54
+ argsPretty = JSON.stringify(JSON.parse(fm.args), null, 2);
55
+ } catch {
56
+ // leave as-is
57
+ }
58
+ return [
59
+ `# Approval: ${fm.server}/${fm.tool}`,
60
+ "",
61
+ `Status: **${fm.status}**`,
62
+ "",
63
+ "## Arguments",
64
+ "",
65
+ "```json",
66
+ argsPretty,
67
+ "```",
68
+ ].join("\n");
69
+ }
70
+
71
+ export function serializeApproval(
72
+ fm: ApprovalFrontmatter,
73
+ body?: string,
74
+ ): string {
75
+ const content = body ?? approvalBody(fm);
76
+ return matter.stringify(
77
+ `\n${content.trim()}\n`,
78
+ fm as Record<string, unknown>,
79
+ );
80
+ }
81
+
82
+ export interface ApprovalParseOk {
83
+ ok: true;
84
+ approval: Approval;
85
+ }
86
+ export interface ApprovalParseFail {
87
+ ok: false;
88
+ reason: string;
89
+ }
90
+
91
+ export function parseApprovalFile(
92
+ raw: string,
93
+ mtimeMs: number,
94
+ ): ApprovalParseOk | ApprovalParseFail {
95
+ let parsed: matter.GrayMatterFile<string>;
96
+ try {
97
+ parsed = matter(raw);
98
+ } catch (err) {
99
+ return { ok: false, reason: `frontmatter parse error: ${err}` };
100
+ }
101
+ const result = ApprovalFrontmatterSchema.safeParse(parsed.data);
102
+ if (!result.success) {
103
+ return {
104
+ ok: false,
105
+ reason: `frontmatter validation failed: ${result.error.message}`,
106
+ };
107
+ }
108
+ return {
109
+ ok: true,
110
+ approval: { ...result.data, mtimeMs, body: parsed.content.trim() },
111
+ };
112
+ }
113
+
114
+ export async function listApprovalFiles(projectDir: string): Promise<string[]> {
115
+ const dir = getApprovalsDir(projectDir);
116
+ try {
117
+ const names = await readdir(dir);
118
+ return names.filter((n) => n.endsWith(".md")).map((n) => n.slice(0, -3));
119
+ } catch (err) {
120
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") return [];
121
+ throw err;
122
+ }
123
+ }
124
+
125
+ export async function getApproval(
126
+ projectDir: string,
127
+ id: string,
128
+ ): Promise<Approval | null> {
129
+ const file = await readWithMtime(approvalFilePath(projectDir, id));
130
+ if (!file) return null;
131
+ const parsed = parseApprovalFile(file.content, file.mtimeMs);
132
+ if (!parsed.ok) {
133
+ logger.warn(`Approval ${id} is malformed: ${parsed.reason}`);
134
+ return null;
135
+ }
136
+ return parsed.approval;
137
+ }
138
+
139
+ export async function listApprovals(
140
+ projectDir: string,
141
+ filters?: { status?: ApprovalStatus; limit?: number; offset?: number },
142
+ ): Promise<Approval[]> {
143
+ const ids = await listApprovalFiles(projectDir);
144
+ const approvals: Approval[] = [];
145
+ for (const id of ids) {
146
+ const a = await getApproval(projectDir, id);
147
+ if (!a) continue;
148
+ if (filters?.status && a.status !== filters.status) continue;
149
+ approvals.push(a);
150
+ }
151
+ // Newest-first by id (uuidv7 is time-ordered) for deterministic pagination.
152
+ approvals.sort((a, b) => (a.id < b.id ? 1 : -1));
153
+ const offset = filters?.offset ?? 0;
154
+ const limit = filters?.limit ?? approvals.length;
155
+ return approvals.slice(offset, offset + limit);
156
+ }
157
+
158
+ export async function createApproval(
159
+ projectDir: string,
160
+ params: {
161
+ server: string;
162
+ tool: string;
163
+ args?: Record<string, unknown>;
164
+ reason?: string;
165
+ task_id?: string | null;
166
+ thread_id?: string | null;
167
+ worker_id?: string | null;
168
+ },
169
+ ): Promise<Approval> {
170
+ const id = uuidv7();
171
+ const now = new Date().toISOString();
172
+ const fm: ApprovalFrontmatter = {
173
+ id,
174
+ status: "pending",
175
+ server: params.server,
176
+ tool: params.tool,
177
+ args: JSON.stringify(params.args ?? {}),
178
+ call_key: callKey(params.server, params.tool, params.args),
179
+ task_id: params.task_id ?? null,
180
+ thread_id: params.thread_id ?? null,
181
+ worker_id: params.worker_id ?? null,
182
+ reason: params.reason ?? "",
183
+ created_at: now,
184
+ updated_at: now,
185
+ decided_at: null,
186
+ decided_by: null,
187
+ };
188
+ await atomicWrite(approvalFilePath(projectDir, id), serializeApproval(fm));
189
+ const fresh = await getApproval(projectDir, id);
190
+ if (!fresh) throw new Error(`Failed to read freshly created approval ${id}`);
191
+ return fresh;
192
+ }
193
+
194
+ export class ApprovalNotFoundError extends Error {
195
+ constructor(readonly id: string) {
196
+ super(`Approval not found: ${id}`);
197
+ this.name = "ApprovalNotFoundError";
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Record a human decision (approve/deny) on a pending approval. Atomic
203
+ * write-if-unchanged so a concurrent edit doesn't get clobbered.
204
+ */
205
+ export async function decideApproval(
206
+ projectDir: string,
207
+ id: string,
208
+ decision: "approved" | "denied",
209
+ decidedBy: string,
210
+ ): Promise<Approval> {
211
+ const a = await getApproval(projectDir, id);
212
+ if (!a) throw new ApprovalNotFoundError(id);
213
+ const fm: ApprovalFrontmatter = {
214
+ ...stripRuntime(a),
215
+ status: decision,
216
+ decided_at: new Date().toISOString(),
217
+ decided_by: decidedBy,
218
+ updated_at: new Date().toISOString(),
219
+ };
220
+ await atomicWriteIfUnchanged(
221
+ approvalFilePath(projectDir, id),
222
+ serializeApproval(fm),
223
+ a.mtimeMs,
224
+ );
225
+ const fresh = await getApproval(projectDir, id);
226
+ if (!fresh) throw new ApprovalNotFoundError(id);
227
+ return fresh;
228
+ }
229
+
230
+ /**
231
+ * Find the most-recent approval record for a call key (any status), or null.
232
+ * Used by the worker callback to resolve a gated call against a prior decision.
233
+ */
234
+ export async function findByCallKey(
235
+ projectDir: string,
236
+ key: string,
237
+ ): Promise<Approval | null> {
238
+ const all = await listApprovals(projectDir);
239
+ for (const a of all) {
240
+ // listApprovals is newest-first, so the first match is the latest.
241
+ if (a.call_key === key) return a;
242
+ }
243
+ return null;
244
+ }
245
+
246
+ /**
247
+ * Delete an approval record. Approvals are single-use: once an approved record
248
+ * has authorized its call, it's consumed so a later identical call re-prompts.
249
+ */
250
+ export async function consumeApproval(
251
+ projectDir: string,
252
+ id: string,
253
+ ): Promise<boolean> {
254
+ try {
255
+ await unlink(approvalFilePath(projectDir, id));
256
+ return true;
257
+ } catch (err) {
258
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") return false;
259
+ throw err;
260
+ }
261
+ }
262
+
263
+ export async function deleteAllApprovals(projectDir: string): Promise<number> {
264
+ const ids = await listApprovalFiles(projectDir);
265
+ let n = 0;
266
+ for (const id of ids) {
267
+ if (await consumeApproval(projectDir, id)) n++;
268
+ }
269
+ return n;
270
+ }
271
+
272
+ /** Drop the in-memory-only fields before writing frontmatter back to disk. */
273
+ function stripRuntime(a: Approval): ApprovalFrontmatter {
274
+ const { mtimeMs: _m, body: _b, ...fm } = a;
275
+ return fm;
276
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Bridge between the mcpx approval callback (which runs deep inside a chat turn,
3
+ * awaiting a boolean) and the Ink TUI (which renders a prompt and resolves it on
4
+ * a keypress). The mcpx client is constructed before the TUI mounts, so the
5
+ * callback can't reference React state directly — it talks to this bridge, and
6
+ * a TUI hook subscribes to drive the prompt.
7
+ *
8
+ * Gated tool calls within a single turn run in parallel (`Promise.all`), so the
9
+ * bridge holds a FIFO queue and surfaces one request at a time.
10
+ */
11
+ export interface ChatApprovalRequest {
12
+ server: string;
13
+ tool: string;
14
+ args: Record<string, unknown>;
15
+ reason: string;
16
+ }
17
+
18
+ interface PendingApproval {
19
+ req: ChatApprovalRequest;
20
+ resolve: (approved: boolean) => void;
21
+ }
22
+
23
+ export interface ChatApprovalBridge {
24
+ /** Called by the mcpx callback; resolves once the user decides. */
25
+ request(req: ChatApprovalRequest): Promise<boolean>;
26
+ /** The request currently awaiting a decision (head of the queue), or null. */
27
+ current(): ChatApprovalRequest | null;
28
+ /** Resolve the head request with the user's decision. */
29
+ resolve(approved: boolean): void;
30
+ /** Subscribe to queue changes (UI re-render). Returns an unsubscribe fn. */
31
+ subscribe(cb: () => void): () => void;
32
+ }
33
+
34
+ export function createApprovalBridge(): ChatApprovalBridge {
35
+ const queue: PendingApproval[] = [];
36
+ const listeners = new Set<() => void>();
37
+ const notify = () => {
38
+ for (const l of listeners) l();
39
+ };
40
+ return {
41
+ request(req) {
42
+ return new Promise<boolean>((resolve) => {
43
+ queue.push({ req, resolve });
44
+ notify();
45
+ });
46
+ },
47
+ current() {
48
+ return queue[0]?.req ?? null;
49
+ },
50
+ resolve(approved) {
51
+ const head = queue.shift();
52
+ head?.resolve(approved);
53
+ notify();
54
+ },
55
+ subscribe(cb) {
56
+ listeners.add(cb);
57
+ return () => {
58
+ listeners.delete(cb);
59
+ };
60
+ },
61
+ };
62
+ }
@@ -3,7 +3,11 @@ import { loadConfig } from "../config/loader.ts";
3
3
  import type { BotholomewConfig } from "../config/schemas.ts";
4
4
  import type { AbortHandle } from "../llm/abort.ts";
5
5
  import { BotholomewLlmError } from "../llm/types.ts";
6
- import { createMcpxClient, resolveMcpxDir } from "../mcpx/client.ts";
6
+ import {
7
+ buildApprovalPolicy,
8
+ createMcpxClient,
9
+ resolveMcpxDir,
10
+ } from "../mcpx/client.ts";
7
11
  import { loadSkills } from "../skills/loader.ts";
8
12
  import type { SkillDefinition } from "../skills/parser.ts";
9
13
  import {
@@ -16,6 +20,7 @@ import {
16
20
  } from "../threads/store.ts";
17
21
  import { generateThreadTitle } from "../utils/title.ts";
18
22
  import { type ChatTurnCallbacks, runChatTurn } from "./agent.ts";
23
+ import { type ChatApprovalBridge, createApprovalBridge } from "./approval.ts";
19
24
 
20
25
  export interface ChatSession {
21
26
  threadId: string;
@@ -25,6 +30,8 @@ export interface ChatSession {
25
30
  skills: Map<string, SkillDefinition>;
26
31
  // biome-ignore lint/suspicious/noExplicitAny: mcpx client
27
32
  mcpxClient: any;
33
+ /** Drives the inline tool-approval prompt in the TUI. */
34
+ approvalBridge: ChatApprovalBridge;
28
35
  cleanup: () => Promise<void>;
29
36
  /** Set by `runChatTurn` while a `streamText(...)` is in flight. */
30
37
  activeAbort: AbortHandle | null;
@@ -65,6 +72,7 @@ export function requireProviderCreds(config: BotholomewConfig): void {
65
72
  export async function startChatSession(
66
73
  projectDir: string,
67
74
  existingThreadId?: string,
75
+ opts: { unsafe?: boolean } = {},
68
76
  ): Promise<ChatSession> {
69
77
  const config = await loadConfig(projectDir);
70
78
 
@@ -106,7 +114,27 @@ export async function startChatSession(
106
114
  );
107
115
  }
108
116
 
109
- const mcpxClient = await createMcpxClient(resolveMcpxDir(projectDir, config));
117
+ // The approval gate. The bridge must exist before the mcpx client so the
118
+ // client's callback can reference it. When the gate is off (`--unsafe` or
119
+ // `approvals.enabled: false`) the policy is undefined and the callback is
120
+ // never invoked.
121
+ const approvalBridge = createApprovalBridge();
122
+ const approvalPolicy = buildApprovalPolicy(config, { unsafe: opts.unsafe });
123
+ const mcpxClient = await createMcpxClient(
124
+ resolveMcpxDir(projectDir, config),
125
+ {
126
+ approvalPolicy,
127
+ onApprovalRequired: approvalPolicy
128
+ ? (req) =>
129
+ approvalBridge.request({
130
+ server: req.server,
131
+ tool: req.tool,
132
+ args: req.args,
133
+ reason: req.reason,
134
+ })
135
+ : undefined,
136
+ },
137
+ );
110
138
  const skills = await loadSkills(projectDir);
111
139
 
112
140
  const cleanup = async () => {
@@ -120,6 +148,7 @@ export async function startChatSession(
120
148
  messages,
121
149
  skills,
122
150
  mcpxClient,
151
+ approvalBridge,
123
152
  cleanup,
124
153
  activeAbort: null,
125
154
  aborted: false,
package/src/cli.ts CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  import ansis from "ansis";
4
4
  import { program } from "commander";
5
+ import { registerApprovalCommand } from "./commands/approval.ts";
5
6
  import { registerCapabilitiesCommand } from "./commands/capabilities.ts";
6
7
  import { registerChatCommand } from "./commands/chat.ts";
7
8
  import { registerCheckUpdateCommand } from "./commands/check-update.ts";
@@ -64,6 +65,7 @@ registerWorkerCommand(program);
64
65
  registerTaskCommand(program);
65
66
  registerThreadCommand(program);
66
67
  registerScheduleCommand(program);
68
+ registerApprovalCommand(program);
67
69
  registerChatCommand(program);
68
70
  registerDreamCommand(program);
69
71
  registerMembotCommand(program);