botholomew 0.15.3 → 0.15.5

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.15.3",
3
+ "version": "0.15.5",
4
4
  "description": "An autonomous AI agent for knowledge work — works your task queue while you sleep.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/chat/agent.ts CHANGED
@@ -35,11 +35,13 @@ import {
35
35
 
36
36
  registerAllTools();
37
37
 
38
- /** Tools available in chat mode — no worker terminal tools (complete/fail/wait), no bulk-destructive file tools (delete, copy/move, dir ops) */
38
+ /** Tools available in chat mode — no worker terminal tools (complete/fail/wait), no bulk-destructive file tools (copy/move, dir ops) */
39
39
  const CHAT_TOOL_NAMES = new Set([
40
40
  "create_task",
41
41
  "list_tasks",
42
42
  "view_task",
43
+ "update_task",
44
+ "delete_task",
43
45
  "context_info",
44
46
  "context_tree",
45
47
  "context_read",
@@ -50,9 +52,11 @@ const CHAT_TOOL_NAMES = new Set([
50
52
  "view_thread",
51
53
  "search_threads",
52
54
  "create_schedule",
55
+ "schedule_edit",
53
56
  "list_schedules",
54
- "update_beliefs",
55
- "update_goals",
57
+ "prompt_read",
58
+ "prompt_edit",
59
+ "task_edit",
56
60
  "capabilities_refresh",
57
61
  "mcp_list_tools",
58
62
  "mcp_search",
@@ -98,7 +102,7 @@ You do NOT execute long-running work directly — enqueue tasks for a background
98
102
  Use the available tools to look up tasks, threads, schedules, and context when the user asks about them. Files the agent can read and write live under \`context/\` as project-relative paths (e.g. \`notes/foo.md\`). Use \`context_tree\` to see what's there, \`search\` (hybrid regexp + semantic) to find content, then \`context_read\` / \`context_info\` to drill in.
99
103
  Past conversations live in CSV files under \`threads/\`; use \`list_threads\`, \`search_threads\`, and \`view_thread\` to find and page through them.
100
104
  When multiple tool calls are independent of each other (i.e., one does not depend on the result of another), call them all in a single response. They will be executed in parallel, which is faster than calling them one at a time.
101
- You can update the agent's beliefs and goals files when the user asks you to.
105
+ You can read and edit the agent's prompt files (beliefs, goals, capabilities, soul) under \`prompts/\` via \`prompt_read\` and \`prompt_edit\` (line-range patches). Files marked \`agent-modification: false\` (e.g. soul.md) are read-only.
102
106
  You can author and refine slash-command skills (reusable prompt templates stored in \`skills/\`) via \`skill_list\`, \`skill_search\`, \`skill_read\`, \`skill_write\`, \`skill_edit\`, and \`skill_delete\`. New or edited skills are usable as \`/<name>\` on the user's next message.
103
107
  Format your responses using Markdown. Use headings, bold, italic, lists, and code blocks to make your responses clear and well-structured.
104
108
  `;
@@ -156,6 +160,11 @@ export interface ChatTurnCallbacks {
156
160
  isError: boolean,
157
161
  meta?: ToolEndMeta,
158
162
  ) => void;
163
+ /** Side-effect notification from inside a tool ("Created subtask: …"). The
164
+ * TUI renders these inside the tool-call card so they stay anchored to the
165
+ * tool that produced them. Workers don't supply this; tools fall back to
166
+ * `logger.info`. */
167
+ onToolNotify?: (toolUseId: string, message: string) => void;
159
168
  /** Called between LLM turns. The TUI returns any queued user messages so
160
169
  * the agent can inject them into the running turn instead of waiting for
161
170
  * the entire tool loop to finish. Each returned message is logged + pushed
@@ -245,7 +254,7 @@ export async function runChatTurn(input: {
245
254
  // Rebuild the system prompt every iteration so that:
246
255
  // (1) `loading: contextual` files get matched against the latest user
247
256
  // message, and
248
- // (2) any update_beliefs / update_goals tool call in the previous
257
+ // (2) any prompt_edit tool call in the previous
249
258
  // iteration is reflected in the next LLM call.
250
259
  const keywordSource = findLastUserText(messages);
251
260
  const systemPrompt = await buildChatSystemPrompt(projectDir, {
@@ -406,6 +415,9 @@ export async function runChatTurn(input: {
406
415
  config,
407
416
  mcpxClient,
408
417
  shouldAbort: session ? () => session.aborted : undefined,
418
+ notify: callbacks.onToolNotify
419
+ ? (msg) => callbacks.onToolNotify?.(toolUse.id, msg)
420
+ : undefined,
409
421
  });
410
422
  const durationMs = Date.now() - start;
411
423
  const stored = maybeStoreResult(toolUse.name, result.output);
@@ -454,6 +466,7 @@ interface ChatToolCallCtx {
454
466
  config: Required<BotholomewConfig>;
455
467
  mcpxClient: McpxClient | null;
456
468
  shouldAbort?: () => boolean;
469
+ notify?: (message: string) => void;
457
470
  }
458
471
 
459
472
  async function executeChatToolCall(
@@ -8,8 +8,9 @@ export function registerChatCommand(program: Command) {
8
8
  "Open the interactive chat TUI\n\n" +
9
9
  " Tab navigation (Ctrl+<letter> from any tab):\n" +
10
10
  " Ctrl+a Chat Ctrl+t Tasks Ctrl+w Workers\n" +
11
- " Ctrl+o Tools Ctrl+r Threads Ctrl+g Help\n" +
11
+ " Ctrl+o Tools Ctrl+e Threads Ctrl+g Help\n" +
12
12
  " Ctrl+n Context Ctrl+s Schedules Esc Return to Chat\n\n" +
13
+ " Refresh: Ctrl+R refreshes Context · Tasks · Threads · Schedules · Workers\n\n" +
13
14
  " Chat input:\n" +
14
15
  " Enter Send message\n" +
15
16
  " ⌥+Enter Insert newline\n" +
@@ -13,6 +13,7 @@ import {
13
13
  import { dirname, join, posix, relative, sep } from "node:path";
14
14
  import { CONTEXT_DIR, PROTECTED_AREAS } from "../constants.ts";
15
15
  import { atomicWrite } from "../fs/atomic.ts";
16
+ import { applyLinePatches, type LinePatch } from "../fs/patches.ts";
16
17
  import {
17
18
  getCanonicalRoot,
18
19
  PathEscapeError,
@@ -59,11 +60,7 @@ export class PathConflictError extends Error {
59
60
  }
60
61
  }
61
62
 
62
- export interface Patch {
63
- start_line: number;
64
- end_line: number;
65
- content: string;
66
- }
63
+ export type Patch = LinePatch;
67
64
 
68
65
  export interface ContextEntry {
69
66
  /** Project-relative path under context/, e.g. "notes/foo.md". Forward-slashes. */
@@ -775,26 +772,11 @@ export async function applyPatches(
775
772
  patches: Patch[],
776
773
  ): Promise<{ applied: number; lines: number }> {
777
774
  const content = await readContextFile(projectDir, path);
778
- const lines = content.split("\n");
779
-
780
- const sorted = [...patches].sort((a, b) => b.start_line - a.start_line);
781
-
782
- for (const patch of sorted) {
783
- if (patch.end_line === 0) {
784
- const insertLines = patch.content === "" ? [] : patch.content.split("\n");
785
- lines.splice(patch.start_line - 1, 0, ...insertLines);
786
- } else {
787
- const deleteCount = patch.end_line - patch.start_line + 1;
788
- const insertLines = patch.content === "" ? [] : patch.content.split("\n");
789
- lines.splice(patch.start_line - 1, deleteCount, ...insertLines);
790
- }
791
- }
792
-
793
- const newContent = lines.join("\n");
775
+ const newContent = applyLinePatches(content, patches);
794
776
  await writeContextFile(projectDir, path, newContent, {
795
777
  onConflict: "overwrite",
796
778
  });
797
- return { applied: patches.length, lines: lines.length };
779
+ return { applied: patches.length, lines: newContent.split("\n").length };
798
780
  }
799
781
 
800
782
  /**
@@ -0,0 +1,39 @@
1
+ import { z } from "zod";
2
+
3
+ export interface LinePatch {
4
+ start_line: number;
5
+ end_line: number;
6
+ content: string;
7
+ }
8
+
9
+ export const LinePatchSchema = z.object({
10
+ start_line: z.number().describe("1-based inclusive start line"),
11
+ end_line: z
12
+ .number()
13
+ .describe("1-based inclusive end line (0 to insert without replacing)"),
14
+ content: z
15
+ .string()
16
+ .describe("Replacement text (empty string to delete lines)"),
17
+ });
18
+
19
+ /**
20
+ * Apply git-style line-range patches to a string. Patches are applied
21
+ * bottom-up so earlier line numbers stay stable. `end_line === 0` is an
22
+ * insert that doesn't replace; an empty `content` deletes.
23
+ */
24
+ export function applyLinePatches(raw: string, patches: LinePatch[]): string {
25
+ const lines = raw.split("\n");
26
+ const sorted = [...patches].sort((a, b) => b.start_line - a.start_line);
27
+
28
+ for (const patch of sorted) {
29
+ const insertLines = patch.content === "" ? [] : patch.content.split("\n");
30
+ if (patch.end_line === 0) {
31
+ lines.splice(patch.start_line - 1, 0, ...insertLines);
32
+ } else {
33
+ const deleteCount = patch.end_line - patch.start_line + 1;
34
+ lines.splice(patch.start_line - 1, deleteCount, ...insertLines);
35
+ }
36
+ }
37
+
38
+ return lines.join("\n");
39
+ }
@@ -19,7 +19,7 @@ import {
19
19
  ScheduleFrontmatterSchema,
20
20
  } from "./schema.ts";
21
21
 
22
- function scheduleFilePath(projectDir: string, id: string): string {
22
+ export function scheduleFilePath(projectDir: string, id: string): string {
23
23
  return join(getSchedulesDir(projectDir), `${id}.md`);
24
24
  }
25
25
 
@@ -27,20 +27,26 @@ function scheduleLockPath(projectDir: string, id: string): string {
27
27
  return join(getSchedulesLockDir(projectDir), `${id}.lock`);
28
28
  }
29
29
 
30
- function serializeSchedule(fm: ScheduleFrontmatter, body: string): string {
30
+ export function serializeSchedule(
31
+ fm: ScheduleFrontmatter,
32
+ body: string,
33
+ ): string {
31
34
  return matter.stringify(`\n${body.trim()}\n`, fm as Record<string, unknown>);
32
35
  }
33
36
 
34
- interface ParseOk {
37
+ export interface ScheduleParseOk {
35
38
  ok: true;
36
39
  schedule: Schedule;
37
40
  }
38
- interface ParseFail {
41
+ export interface ScheduleParseFail {
39
42
  ok: false;
40
43
  reason: string;
41
44
  }
42
45
 
43
- function parseScheduleFile(raw: string, mtimeMs: number): ParseOk | ParseFail {
46
+ export function parseScheduleFile(
47
+ raw: string,
48
+ mtimeMs: number,
49
+ ): ScheduleParseOk | ScheduleParseFail {
44
50
  let parsed: matter.GrayMatterFile<string>;
45
51
  try {
46
52
  parsed = matter(raw);
@@ -21,7 +21,7 @@ import {
21
21
  type TaskStatus,
22
22
  } from "./schema.ts";
23
23
 
24
- function taskFilePath(projectDir: string, id: string): string {
24
+ export function taskFilePath(projectDir: string, id: string): string {
25
25
  return join(getTasksDir(projectDir), `${id}.md`);
26
26
  }
27
27
 
@@ -33,23 +33,23 @@ function taskLockPath(projectDir: string, id: string): string {
33
33
  * Render a Task to its on-disk markdown form. Frontmatter contains every
34
34
  * field; the body is preserved as-is. Trailing newline keeps line count sane.
35
35
  */
36
- function serializeTask(fm: TaskFrontmatter, body: string): string {
36
+ export function serializeTask(fm: TaskFrontmatter, body: string): string {
37
37
  return matter.stringify(`\n${body.trim()}\n`, fm as Record<string, unknown>);
38
38
  }
39
39
 
40
- interface ParseResult {
40
+ export interface TaskParseOk {
41
41
  ok: true;
42
42
  task: Task;
43
43
  }
44
- interface ParseFailure {
44
+ export interface TaskParseFail {
45
45
  ok: false;
46
46
  reason: string;
47
47
  }
48
48
 
49
- function parseTaskFile(
49
+ export function parseTaskFile(
50
50
  raw: string,
51
51
  mtimeMs: number,
52
- ): ParseResult | ParseFailure {
52
+ ): TaskParseOk | TaskParseFail {
53
53
  let parsed: matter.GrayMatterFile<string>;
54
54
  try {
55
55
  parsed = matter(raw);
@@ -5,21 +5,12 @@ import {
5
5
  NotFoundError,
6
6
  readContextFile,
7
7
  } from "../../context/store.ts";
8
+ import { LinePatchSchema } from "../../fs/patches.ts";
8
9
  import type { ToolDefinition } from "../tool.ts";
9
10
 
10
- const PatchSchema = z.object({
11
- start_line: z.number().describe("1-based inclusive start line"),
12
- end_line: z
13
- .number()
14
- .describe("1-based inclusive end line (0 to insert without replacing)"),
15
- content: z
16
- .string()
17
- .describe("Replacement text (empty string to delete lines)"),
18
- });
19
-
20
11
  const inputSchema = z.object({
21
12
  path: z.string().describe("Project-relative path under context/"),
22
- patches: z.array(PatchSchema).describe("Patches to apply"),
13
+ patches: z.array(LinePatchSchema).describe("Patches to apply"),
23
14
  });
24
15
 
25
16
  const outputSchema = z.object({
@@ -0,0 +1,148 @@
1
+ import { join } from "node:path";
2
+ import { z } from "zod";
3
+ import { getPromptsDir } from "../../constants.ts";
4
+ import {
5
+ atomicWriteIfUnchanged,
6
+ MtimeConflictError,
7
+ readWithMtime,
8
+ } from "../../fs/atomic.ts";
9
+ import { applyLinePatches, LinePatchSchema } from "../../fs/patches.ts";
10
+ import {
11
+ parseContextFile,
12
+ serializeContextFile,
13
+ } from "../../utils/frontmatter.ts";
14
+ import type { ToolDefinition } from "../tool.ts";
15
+
16
+ const inputSchema = z.object({
17
+ name: z
18
+ .string()
19
+ .describe(
20
+ "Prompt name without extension (e.g. 'beliefs', 'goals', 'capabilities'). Resolves to prompts/<name>.md.",
21
+ ),
22
+ patches: z.array(LinePatchSchema).describe("Patches to apply"),
23
+ });
24
+
25
+ const outputSchema = z.object({
26
+ name: z.string(),
27
+ path: z.string().nullable(),
28
+ applied: z.number(),
29
+ content: z.string(),
30
+ is_error: z.boolean(),
31
+ error_type: z.string().optional(),
32
+ message: z.string().optional(),
33
+ next_action_hint: z.string().optional(),
34
+ });
35
+
36
+ export const promptEditTool = {
37
+ name: "prompt_edit",
38
+ description:
39
+ "[[ bash equivalent command: patch ]] Apply git-style line-range patches to a prompt file under prompts/. Operates on the whole file (frontmatter + body). Files marked `agent-modification: false` (e.g. soul.md) are protected. Use prompt_read first to inspect current line numbers.",
40
+ group: "context",
41
+ inputSchema,
42
+ outputSchema,
43
+ execute: async (input, ctx) => {
44
+ if (input.name.includes("/") || input.name.includes("..")) {
45
+ return {
46
+ name: input.name,
47
+ path: null,
48
+ applied: 0,
49
+ content: "",
50
+ is_error: true,
51
+ error_type: "invalid_name",
52
+ message: `Invalid prompt name: ${input.name}`,
53
+ next_action_hint:
54
+ "Use a basename without slashes or dots (e.g. 'beliefs').",
55
+ };
56
+ }
57
+ const filePath = join(getPromptsDir(ctx.projectDir), `${input.name}.md`);
58
+ const file = await readWithMtime(filePath);
59
+ if (!file) {
60
+ return {
61
+ name: input.name,
62
+ path: null,
63
+ applied: 0,
64
+ content: "",
65
+ is_error: true,
66
+ error_type: "not_found",
67
+ message: `Prompt not found: prompts/${input.name}.md`,
68
+ };
69
+ }
70
+
71
+ const original = file.content;
72
+ const preParsed = parseContextFile(original);
73
+ if (!preParsed.meta["agent-modification"]) {
74
+ return {
75
+ name: input.name,
76
+ path: filePath,
77
+ applied: 0,
78
+ content: original,
79
+ is_error: true,
80
+ error_type: "agent_modification_disabled",
81
+ message: `Agent modification not allowed for prompts/${input.name}.md`,
82
+ };
83
+ }
84
+
85
+ const updated = applyLinePatches(original, input.patches);
86
+ let postParsed: { meta: Record<string, unknown>; content: string };
87
+ try {
88
+ postParsed = parseContextFile(updated);
89
+ } catch (err) {
90
+ return {
91
+ name: input.name,
92
+ path: filePath,
93
+ applied: 0,
94
+ content: original,
95
+ is_error: true,
96
+ error_type: "invalid_frontmatter",
97
+ message: `Patched content failed to parse: ${err instanceof Error ? err.message : String(err)}`,
98
+ next_action_hint:
99
+ "Check that the frontmatter delimiters and YAML stay valid.",
100
+ };
101
+ }
102
+ if (!postParsed.meta["agent-modification"]) {
103
+ return {
104
+ name: input.name,
105
+ path: filePath,
106
+ applied: 0,
107
+ content: original,
108
+ is_error: true,
109
+ error_type: "agent_modification_disabled",
110
+ message: `Patch would clear agent-modification on prompts/${input.name}.md`,
111
+ next_action_hint:
112
+ "Don't change the agent-modification frontmatter flag.",
113
+ };
114
+ }
115
+
116
+ const serialized = serializeContextFile(
117
+ postParsed.meta,
118
+ postParsed.content,
119
+ );
120
+
121
+ try {
122
+ await atomicWriteIfUnchanged(filePath, serialized, file.mtimeMs);
123
+ } catch (err) {
124
+ if (err instanceof MtimeConflictError) {
125
+ return {
126
+ name: input.name,
127
+ path: filePath,
128
+ applied: 0,
129
+ content: original,
130
+ is_error: true,
131
+ error_type: "mtime_conflict",
132
+ message: `Prompt was modified concurrently: ${err.message}`,
133
+ next_action_hint:
134
+ "Re-read with prompt_read and recompute your patch line numbers before retrying.",
135
+ };
136
+ }
137
+ throw err;
138
+ }
139
+
140
+ return {
141
+ name: input.name,
142
+ path: filePath,
143
+ applied: input.patches.length,
144
+ content: serialized,
145
+ is_error: false,
146
+ };
147
+ },
148
+ } satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
@@ -0,0 +1,70 @@
1
+ import { join } from "node:path";
2
+ import { z } from "zod";
3
+ import { getPromptsDir } from "../../constants.ts";
4
+ import type { ToolDefinition } from "../tool.ts";
5
+
6
+ const inputSchema = z.object({
7
+ name: z
8
+ .string()
9
+ .describe(
10
+ "Prompt name without extension (e.g. 'beliefs', 'goals', 'capabilities'). Resolves to prompts/<name>.md.",
11
+ ),
12
+ });
13
+
14
+ const outputSchema = z.object({
15
+ name: z.string(),
16
+ path: z.string().nullable(),
17
+ content: z.string(),
18
+ agent_modification: z.boolean(),
19
+ is_error: z.boolean(),
20
+ error_type: z.string().optional(),
21
+ message: z.string().optional(),
22
+ });
23
+
24
+ export const promptReadTool = {
25
+ name: "prompt_read",
26
+ description:
27
+ "[[ bash equivalent command: cat ]] Read a prompt file under prompts/ (e.g. beliefs, goals, capabilities, soul). Returns the whole file (frontmatter + body) for use with prompt_edit.",
28
+ group: "context",
29
+ inputSchema,
30
+ outputSchema,
31
+ execute: async (input, ctx) => {
32
+ if (input.name.includes("/") || input.name.includes("..")) {
33
+ return {
34
+ name: input.name,
35
+ path: null,
36
+ content: "",
37
+ agent_modification: false,
38
+ is_error: true,
39
+ error_type: "invalid_name",
40
+ message: `Invalid prompt name: ${input.name}`,
41
+ };
42
+ }
43
+ const filePath = join(getPromptsDir(ctx.projectDir), `${input.name}.md`);
44
+ const file = Bun.file(filePath);
45
+ if (!(await file.exists())) {
46
+ return {
47
+ name: input.name,
48
+ path: null,
49
+ content: "",
50
+ agent_modification: false,
51
+ is_error: true,
52
+ error_type: "not_found",
53
+ message: `Prompt not found: prompts/${input.name}.md`,
54
+ };
55
+ }
56
+ const content = await file.text();
57
+ // Cheap header sniff so the agent knows whether prompt_edit will be
58
+ // accepted before it constructs patches.
59
+ const agent_modification = /agent-modification:\s*true/.test(
60
+ content.split("---", 3).slice(0, 3).join("---"),
61
+ );
62
+ return {
63
+ name: input.name,
64
+ path: filePath,
65
+ content,
66
+ agent_modification,
67
+ is_error: false,
68
+ };
69
+ },
70
+ } satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
@@ -3,8 +3,6 @@ import { capabilitiesRefreshTool } from "./capabilities/refresh.ts";
3
3
  // Context tools
4
4
  import { pipeToContextTool } from "./context/pipe.ts";
5
5
  import { readLargeResultTool } from "./context/read-large-result.ts";
6
- import { updateBeliefsTool } from "./context/update-beliefs.ts";
7
- import { updateGoalsTool } from "./context/update-goals.ts";
8
6
  // Context — directory operations
9
7
  import { contextCreateDirTool } from "./dir/create.ts";
10
8
  import { contextDirSizeTool } from "./dir/size.ts";
@@ -24,8 +22,12 @@ import { mcpExecTool } from "./mcp/exec.ts";
24
22
  import { mcpInfoTool } from "./mcp/info.ts";
25
23
  import { mcpListToolsTool } from "./mcp/list-tools.ts";
26
24
  import { mcpSearchTool } from "./mcp/search.ts";
25
+ // Prompt tools
26
+ import { promptEditTool } from "./prompt/edit.ts";
27
+ import { promptReadTool } from "./prompt/read.ts";
27
28
  // Schedule tools
28
29
  import { createScheduleTool } from "./schedule/create.ts";
30
+ import { scheduleEditTool } from "./schedule/edit.ts";
29
31
  import { listSchedulesTool } from "./schedule/list.ts";
30
32
  // Search tools
31
33
  import { searchTool } from "./search/index.ts";
@@ -40,6 +42,7 @@ import { skillWriteTool } from "./skill/write.ts";
40
42
  import { completeTaskTool } from "./task/complete.ts";
41
43
  import { createTaskTool } from "./task/create.ts";
42
44
  import { deleteTaskTool } from "./task/delete.ts";
45
+ import { taskEditTool } from "./task/edit.ts";
43
46
  import { failTaskTool } from "./task/fail.ts";
44
47
  import { listTasksTool } from "./task/list.ts";
45
48
  import { updateTaskTool } from "./task/update.ts";
@@ -62,6 +65,7 @@ export function registerAllTools(): void {
62
65
  registerTool(waitTaskTool);
63
66
  registerTool(createTaskTool);
64
67
  registerTool(updateTaskTool);
68
+ registerTool(taskEditTool);
65
69
  registerTool(deleteTaskTool);
66
70
  registerTool(listTasksTool);
67
71
  registerTool(viewTaskTool);
@@ -79,8 +83,8 @@ export function registerAllTools(): void {
79
83
  registerTool(contextInfoTool);
80
84
  registerTool(contextExistsTool);
81
85
  registerTool(contextCountLinesTool);
82
- registerTool(updateBeliefsTool);
83
- registerTool(updateGoalsTool);
86
+ registerTool(promptReadTool);
87
+ registerTool(promptEditTool);
84
88
  registerTool(readLargeResultTool);
85
89
  registerTool(pipeToContextTool);
86
90
 
@@ -89,6 +93,7 @@ export function registerAllTools(): void {
89
93
 
90
94
  // Schedule
91
95
  registerTool(createScheduleTool);
96
+ registerTool(scheduleEditTool);
92
97
  registerTool(listSchedulesTool);
93
98
 
94
99
  // Search
@@ -33,7 +33,9 @@ export const createScheduleTool = {
33
33
  description: input.description,
34
34
  frequency: input.frequency,
35
35
  });
36
- logger.info(`Created schedule: ${schedule.name} (${schedule.id})`);
36
+ const msg = `Created schedule: ${schedule.name} (${schedule.id})`;
37
+ if (ctx.notify) ctx.notify(msg);
38
+ else logger.info(msg);
37
39
  return {
38
40
  id: schedule.id,
39
41
  name: schedule.name,