botholomew 0.1.1 → 0.2.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.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "An AI agent for knowledge work",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,6 +23,9 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "@anthropic-ai/sdk": "^0.88.0",
26
+ "@evantahler/mcpx": "0.18.3",
27
+ "@huggingface/transformers": "^4.0.1",
28
+ "@sqliteai/sqlite-vector": "^0.9.95",
26
29
  "ansis": "^4.2.0",
27
30
  "commander": "^14.0.0",
28
31
  "gray-matter": "^4.0.3",
package/src/cli.ts CHANGED
@@ -8,6 +8,8 @@ import { registerContextCommand } from "./commands/context.ts";
8
8
  import { registerDaemonCommand } from "./commands/daemon.ts";
9
9
  import { registerInitCommand } from "./commands/init.ts";
10
10
  import { registerMcpxCommand } from "./commands/mcpx.ts";
11
+ import { registerPrepareCommand } from "./commands/prepare.ts";
12
+ import { registerScheduleCommand } from "./commands/schedule.ts";
11
13
  import { registerTaskCommand } from "./commands/task.ts";
12
14
  import { registerToolCommands } from "./commands/tools.ts";
13
15
  import { registerUpgradeCommand } from "./commands/upgrade.ts";
@@ -31,10 +33,12 @@ program
31
33
  registerInitCommand(program);
32
34
  registerDaemonCommand(program);
33
35
  registerTaskCommand(program);
36
+ registerScheduleCommand(program);
34
37
  registerChatCommand(program);
35
38
  registerContextCommand(program);
36
39
  registerMcpxCommand(program);
37
40
  registerToolCommands(program);
41
+ registerPrepareCommand(program);
38
42
  registerCheckUpdateCommand(program);
39
43
  registerUpgradeCommand(program);
40
44
 
@@ -1,5 +1,20 @@
1
+ import { readdir, stat } from "node:fs/promises";
2
+ import { basename, join, resolve } from "node:path";
3
+ import ansis from "ansis";
1
4
  import type { Command } from "commander";
5
+ import { isText } from "istextorbinary";
6
+ import { loadConfig } from "../config/loader.ts";
7
+ import { embedSingle, warmupEmbedder } from "../context/embedder.ts";
8
+ import { ingestContextItem } from "../context/ingest.ts";
9
+ import type { DbConnection } from "../db/connection.ts";
10
+ import {
11
+ createContextItem,
12
+ listContextItems,
13
+ listContextItemsByPrefix,
14
+ } from "../db/context.ts";
15
+ import { hybridSearch, initVectorSearch } from "../db/embeddings.ts";
2
16
  import { logger } from "../utils/logger.ts";
17
+ import { withDb } from "./with-db.ts";
3
18
 
4
19
  export function registerContextCommand(program: Command) {
5
20
  const ctx = program.command("context").description("Manage context items");
@@ -7,21 +22,160 @@ export function registerContextCommand(program: Command) {
7
22
  ctx
8
23
  .command("list")
9
24
  .description("List context items")
10
- .action(async () => {
11
- logger.warn("Not yet implemented. Coming soon.");
12
- });
25
+ .option("--path <prefix>", "filter by path prefix")
26
+ .option("-l, --limit <n>", "max number of items", Number.parseInt)
27
+ .action((opts) =>
28
+ withDb(program, async (conn) => {
29
+ const items = opts.path
30
+ ? await listContextItemsByPrefix(conn, opts.path, {
31
+ recursive: true,
32
+ limit: opts.limit,
33
+ })
34
+ : await listContextItems(conn, { limit: opts.limit });
35
+
36
+ if (items.length === 0) {
37
+ logger.dim("No context items found.");
38
+ return;
39
+ }
40
+
41
+ const header = `${ansis.bold("Path".padEnd(40))} ${"Title".padEnd(25)} ${"Type".padEnd(20)} Indexed`;
42
+ console.log(header);
43
+ console.log("-".repeat(header.length));
44
+
45
+ for (const item of items) {
46
+ const indexed = item.indexed_at
47
+ ? ansis.green("yes")
48
+ : ansis.dim("no");
49
+ console.log(
50
+ `${item.context_path.padEnd(40)} ${item.title.slice(0, 24).padEnd(25)} ${item.mime_type.slice(0, 19).padEnd(20)} ${indexed}`,
51
+ );
52
+ }
53
+
54
+ console.log(`\n${ansis.dim(`${items.length} item(s)`)}`);
55
+ }),
56
+ );
13
57
 
14
58
  ctx
15
59
  .command("add <path>")
16
60
  .description("Add a file or directory to context")
17
- .action(async () => {
18
- logger.warn("Not yet implemented. Coming soon.");
19
- });
61
+ .option("--prefix <prefix>", "virtual path prefix", "/")
62
+ .action((path, opts) =>
63
+ withDb(program, async (conn, dir) => {
64
+ const config = await loadConfig(dir);
65
+ await warmupEmbedder();
66
+
67
+ const resolvedPath = resolve(path);
68
+ const info = await stat(resolvedPath);
69
+
70
+ let added = 0;
71
+ let chunks = 0;
72
+
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);
79
+ if (count >= 0) {
80
+ added++;
81
+ chunks += count;
82
+ }
83
+ }
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
+ }
92
+
93
+ logger.success(`Added ${added} file(s), ${chunks} chunk(s) indexed.`);
94
+ }),
95
+ );
20
96
 
21
97
  ctx
22
98
  .command("search <query>")
23
99
  .description("Search context items")
24
- .action(async () => {
25
- logger.warn("Not yet implemented. Coming soon.");
100
+ .option("-k, --top-k <n>", "max results", Number.parseInt, 10)
101
+ .action((query, opts) =>
102
+ withDb(program, async (conn) => {
103
+ await warmupEmbedder();
104
+ initVectorSearch(conn);
105
+ const queryVec = await embedSingle(query);
106
+ const results = hybridSearch(conn, query, queryVec, opts.topK);
107
+
108
+ if (results.length === 0) {
109
+ logger.dim("No results found.");
110
+ return;
111
+ }
112
+
113
+ for (const [i, r] of results.entries()) {
114
+ const score = (r.score * 100).toFixed(1);
115
+ console.log(
116
+ `${ansis.bold(`${i + 1}.`)} ${ansis.cyan(r.title)} ${ansis.dim(`(${score}%)`)}`,
117
+ );
118
+ console.log(` ${ansis.dim(r.source_path || r.context_item_id)}`);
119
+ if (r.chunk_content) {
120
+ const snippet = r.chunk_content.slice(0, 120).replace(/\n/g, " ");
121
+ console.log(` ${snippet}...`);
122
+ }
123
+ console.log("");
124
+ }
125
+ }),
126
+ );
127
+ }
128
+
129
+ async function addFile(
130
+ conn: DbConnection,
131
+ config: Awaited<ReturnType<typeof loadConfig>>,
132
+ filePath: string,
133
+ contextPath: string,
134
+ ): Promise<number> {
135
+ try {
136
+ const bunFile = Bun.file(filePath);
137
+ const mimeType = bunFile.type.split(";")[0] || "application/octet-stream";
138
+ const filename = basename(filePath);
139
+ const textual = isText(filename) !== false;
140
+
141
+ const content = textual ? await bunFile.text() : null;
142
+
143
+ const item = await createContextItem(conn, {
144
+ title: filename,
145
+ content: content ?? undefined,
146
+ mimeType,
147
+ sourcePath: filePath,
148
+ contextPath,
149
+ isTextual: textual,
26
150
  });
151
+
152
+ if (textual && content) {
153
+ const count = await ingestContextItem(conn, item.id, config);
154
+ console.log(` + ${contextPath} (${count} chunks)`);
155
+ return count;
156
+ }
157
+
158
+ console.log(` + ${contextPath} (binary, not indexed)`);
159
+ return 0;
160
+ } catch (err) {
161
+ logger.warn(` ! ${contextPath}: ${err}`);
162
+ return -1;
163
+ }
164
+ }
165
+
166
+ async function walkDirectory(dirPath: string): Promise<string[]> {
167
+ const files: string[] = [];
168
+ const entries = await readdir(dirPath, { withFileTypes: true });
169
+
170
+ for (const entry of entries) {
171
+ const fullPath = join(dirPath, entry.name);
172
+ if (entry.isDirectory()) {
173
+ if (entry.name.startsWith(".")) continue; // skip hidden dirs
174
+ files.push(...(await walkDirectory(fullPath)));
175
+ } else if (entry.isFile()) {
176
+ files.push(fullPath);
177
+ }
178
+ }
179
+
180
+ return files;
27
181
  }
@@ -1,29 +1,245 @@
1
+ import { copyFileSync, existsSync, mkdirSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
1
6
  import type { Command } from "commander";
7
+ import { getMcpxDir } from "../constants.ts";
2
8
  import { logger } from "../utils/logger.ts";
3
9
 
10
+ const require = createRequire(import.meta.url);
11
+ const ourPkg = require("../../package.json");
12
+ const mcpxPkg = require("@evantahler/mcpx/package.json");
13
+
14
+ if (mcpxPkg.version !== ourPkg.dependencies["@evantahler/mcpx"]) {
15
+ throw new Error(
16
+ `@evantahler/mcpx version mismatch: installed ${mcpxPkg.version}, expected ${ourPkg.dependencies["@evantahler/mcpx"]}`,
17
+ );
18
+ }
19
+
20
+ const MCPX_CLI = fileURLToPath(import.meta.resolve("@evantahler/mcpx/cli"));
21
+
22
+ export async function runMcpx(
23
+ projectDir: string,
24
+ args: (string | undefined)[],
25
+ opts?: { inherit?: boolean },
26
+ ): Promise<string> {
27
+ const mcpxDir = getMcpxDir(projectDir);
28
+ const filteredArgs = args.filter((a): a is string => a !== undefined);
29
+ const proc = Bun.spawn(["bun", MCPX_CLI, ...filteredArgs, "-c", mcpxDir], {
30
+ stdout: opts?.inherit ? "inherit" : "pipe",
31
+ stderr: opts?.inherit ? "inherit" : "pipe",
32
+ stdin: opts?.inherit ? "inherit" : undefined,
33
+ });
34
+ const exitCode = await proc.exited;
35
+
36
+ if (opts?.inherit) {
37
+ if (exitCode !== 0) process.exit(exitCode);
38
+ return "";
39
+ }
40
+
41
+ const stdout = await new Response(proc.stdout).text();
42
+ const stderr = await new Response(proc.stderr).text();
43
+ if (exitCode !== 0) {
44
+ logger.error(stderr.trim() || stdout.trim());
45
+ process.exit(exitCode);
46
+ }
47
+ return stdout;
48
+ }
49
+
50
+ function getDir(program: Command): string {
51
+ return program.opts().dir;
52
+ }
53
+
4
54
  export function registerMcpxCommand(program: Command) {
5
55
  const mcpx = program
6
56
  .command("mcpx")
7
57
  .description("Manage MCP servers via MCPX");
8
58
 
59
+ // --- servers ---
9
60
  mcpx
10
- .command("list")
11
- .description("List configured MCP servers")
61
+ .command("servers")
62
+ .description("List configured MCP server names")
12
63
  .action(async () => {
13
- logger.warn("Not yet implemented. Coming soon.");
64
+ const out = await runMcpx(getDir(program), ["servers"]);
65
+ process.stdout.write(out);
14
66
  });
15
67
 
68
+ // --- info ---
16
69
  mcpx
17
- .command("add <server>")
70
+ .command("info <first> [second]")
71
+ .description(
72
+ "Show server overview, or schema for a specific tool (server is optional if tool name is unambiguous)",
73
+ )
74
+ .action(async (first: string, second?: string) => {
75
+ const out = await runMcpx(getDir(program), ["info", first, second]);
76
+ process.stdout.write(out);
77
+ });
78
+
79
+ // --- search ---
80
+ mcpx
81
+ .command("search <terms...>")
82
+ .description("Search tools by keyword and/or semantic similarity")
83
+ .action(async (terms: string[]) => {
84
+ const out = await runMcpx(getDir(program), ["search", ...terms]);
85
+ process.stdout.write(out);
86
+ });
87
+
88
+ // --- exec ---
89
+ mcpx
90
+ .command("exec <first> [second] [third]")
91
+ .description(
92
+ "Execute a tool call (server is optional if tool name is unambiguous)",
93
+ )
94
+ .action(async (first: string, second?: string, third?: string) => {
95
+ const out = await runMcpx(getDir(program), [
96
+ "exec",
97
+ first,
98
+ second,
99
+ third,
100
+ ]);
101
+ process.stdout.write(out);
102
+ });
103
+
104
+ // --- add ---
105
+ mcpx
106
+ .command("add <name>")
18
107
  .description("Add an MCP server")
108
+ .option("--command <cmd>", "Stdio server command")
109
+ .option("--args <args...>", "Stdio server arguments")
110
+ .option("--url <url>", "HTTP server URL")
111
+ .option("--transport <type>", "HTTP transport: sse or streamable-http")
112
+ .option("--env <pairs...>", "Environment variables as KEY=VALUE pairs")
113
+ .action(
114
+ async (
115
+ name: string,
116
+ opts: {
117
+ command?: string;
118
+ args?: string[];
119
+ url?: string;
120
+ transport?: string;
121
+ env?: string[];
122
+ },
123
+ ) => {
124
+ const cliArgs: string[] = ["add", name];
125
+ if (opts.command) cliArgs.push("--command", opts.command);
126
+ if (opts.args) {
127
+ for (const a of opts.args) cliArgs.push("--args", a);
128
+ }
129
+ if (opts.url) cliArgs.push("--url", opts.url);
130
+ if (opts.transport) cliArgs.push("--transport", opts.transport);
131
+ if (opts.env) {
132
+ for (const e of opts.env) cliArgs.push("--env", e);
133
+ }
134
+ const out = await runMcpx(getDir(program), cliArgs);
135
+ process.stdout.write(out);
136
+ },
137
+ );
138
+
139
+ // --- remove ---
140
+ mcpx
141
+ .command("remove <name>")
142
+ .description("Remove an MCP server")
143
+ .action(async (name: string) => {
144
+ const out = await runMcpx(getDir(program), ["remove", name]);
145
+ process.stdout.write(out);
146
+ });
147
+
148
+ // --- ping ---
149
+ mcpx
150
+ .command("ping [servers...]")
151
+ .description("Check connectivity to MCP servers")
152
+ .action(async (servers: string[]) => {
153
+ const out = await runMcpx(getDir(program), ["ping", ...servers]);
154
+ process.stdout.write(out);
155
+ });
156
+
157
+ // --- auth ---
158
+ mcpx
159
+ .command("auth <server>")
160
+ .description("Authenticate with an HTTP MCP server")
161
+ .action(async (server: string) => {
162
+ await runMcpx(getDir(program), ["auth", server], { inherit: true });
163
+ });
164
+
165
+ // --- resource ---
166
+ mcpx
167
+ .command("resource [server] [uri]")
168
+ .description("List resources for a server, or read a specific resource")
169
+ .action(async (server?: string, uri?: string) => {
170
+ const out = await runMcpx(getDir(program), ["resource", server, uri]);
171
+ process.stdout.write(out);
172
+ });
173
+
174
+ // --- prompt ---
175
+ mcpx
176
+ .command("prompt [server] [name] [args]")
177
+ .description("List prompts for a server, or get a specific prompt")
178
+ .action(async (server?: string, name?: string, argsJson?: string) => {
179
+ const out = await runMcpx(getDir(program), [
180
+ "prompt",
181
+ server,
182
+ name,
183
+ argsJson,
184
+ ]);
185
+ process.stdout.write(out);
186
+ });
187
+
188
+ // --- task ---
189
+ mcpx
190
+ .command("task <action> <server> [taskId]")
191
+ .description("Manage async tasks (actions: list, get, result, cancel)")
192
+ .action(async (action: string, server: string, taskId?: string) => {
193
+ const out = await runMcpx(getDir(program), [
194
+ "task",
195
+ action,
196
+ server,
197
+ taskId,
198
+ ]);
199
+ process.stdout.write(out);
200
+ });
201
+
202
+ // --- import-global ---
203
+ mcpx
204
+ .command("import-global")
205
+ .description("Copy system-wide MCPX settings (~/.mcpx) into this project")
19
206
  .action(async () => {
20
- logger.warn("Not yet implemented. Coming soon.");
207
+ const globalDir = join(homedir(), ".mcpx");
208
+ if (!existsSync(globalDir)) {
209
+ logger.error("No global MCPX config found at ~/.mcpx");
210
+ process.exit(1);
211
+ }
212
+
213
+ const projectMcpxDir = getMcpxDir(getDir(program));
214
+ if (!existsSync(projectMcpxDir)) {
215
+ mkdirSync(projectMcpxDir, { recursive: true });
216
+ }
217
+
218
+ const filesToCopy = ["servers.json", "auth.json", "search.json"];
219
+ let copied = 0;
220
+ for (const file of filesToCopy) {
221
+ const src = join(globalDir, file);
222
+ if (!existsSync(src)) continue;
223
+ const dest = join(projectMcpxDir, file);
224
+ copyFileSync(src, dest);
225
+ logger.success(`Copied ${file}`);
226
+ copied++;
227
+ }
228
+
229
+ if (copied === 0) {
230
+ logger.warn("No config files found in ~/.mcpx to copy.");
231
+ } else {
232
+ logger.success(
233
+ `Imported ${copied} file(s) from ~/.mcpx into ${projectMcpxDir}`,
234
+ );
235
+ }
21
236
  });
22
237
 
238
+ // --- index ---
23
239
  mcpx
24
- .command("test <tool>")
25
- .description("Test a tool call")
240
+ .command("index")
241
+ .description("Build the search index from all configured servers")
26
242
  .action(async () => {
27
- logger.warn("Not yet implemented. Coming soon.");
243
+ await runMcpx(getDir(program), ["index"], { inherit: true });
28
244
  });
29
245
  }
@@ -0,0 +1,16 @@
1
+ import type { Command } from "commander";
2
+ import { warmupEmbedder } from "../context/embedder.ts";
3
+ import { logger } from "../utils/logger.ts";
4
+
5
+ export function registerPrepareCommand(program: Command) {
6
+ program
7
+ .command("prepare")
8
+ .description(
9
+ "Download and cache required models. Run this in CI or on first setup.",
10
+ )
11
+ .action(async () => {
12
+ logger.info("Preparing Botholomew...");
13
+ await warmupEmbedder();
14
+ logger.success("All models downloaded and ready.");
15
+ });
16
+ }
@@ -0,0 +1,186 @@
1
+ import ansis from "ansis";
2
+ import type { Command } from "commander";
3
+ import type { Schedule } from "../db/schedules.ts";
4
+ import {
5
+ createSchedule,
6
+ deleteSchedule,
7
+ getSchedule,
8
+ listSchedules,
9
+ updateSchedule,
10
+ } from "../db/schedules.ts";
11
+ import { logger } from "../utils/logger.ts";
12
+ import { withDb } from "./with-db.ts";
13
+
14
+ export function registerScheduleCommand(program: Command) {
15
+ const schedule = program.command("schedule").description("Manage schedules");
16
+
17
+ schedule
18
+ .command("list")
19
+ .description("List all schedules")
20
+ .option("--enabled", "show only enabled schedules")
21
+ .option("--disabled", "show only disabled schedules")
22
+ .action((opts) =>
23
+ withDb(program, async (conn) => {
24
+ const filters: { enabled?: boolean } = {};
25
+ if (opts.enabled) filters.enabled = true;
26
+ if (opts.disabled) filters.enabled = false;
27
+
28
+ const schedules = await listSchedules(conn, filters);
29
+
30
+ if (schedules.length === 0) {
31
+ logger.dim("No schedules found.");
32
+ return;
33
+ }
34
+
35
+ for (const s of schedules) {
36
+ printSchedule(s);
37
+ }
38
+ }),
39
+ );
40
+
41
+ schedule
42
+ .command("add <name>")
43
+ .description("Create a new schedule")
44
+ .requiredOption(
45
+ "-f, --frequency <text>",
46
+ "how often to run (e.g. 'every morning')",
47
+ )
48
+ .option("--description <text>", "schedule description", "")
49
+ .action((name, opts) =>
50
+ withDb(program, async (conn) => {
51
+ const s = await createSchedule(conn, {
52
+ name,
53
+ description: opts.description,
54
+ frequency: opts.frequency,
55
+ });
56
+ logger.success(`Created schedule: ${s.name} (${s.id})`);
57
+ }),
58
+ );
59
+
60
+ schedule
61
+ .command("view <id>")
62
+ .description("View schedule details")
63
+ .action((id) =>
64
+ withDb(program, async (conn) => {
65
+ const s = await getSchedule(conn, id);
66
+ if (!s) {
67
+ logger.error(`Schedule not found: ${id}`);
68
+ process.exit(1);
69
+ }
70
+ printScheduleDetail(s);
71
+ }),
72
+ );
73
+
74
+ schedule
75
+ .command("enable <id>")
76
+ .description("Enable a schedule")
77
+ .action((id) =>
78
+ withDb(program, async (conn) => {
79
+ const s = await updateSchedule(conn, id, { enabled: true });
80
+ if (!s) {
81
+ logger.error(`Schedule not found: ${id}`);
82
+ process.exit(1);
83
+ }
84
+ logger.success(`Enabled schedule: ${s.name}`);
85
+ }),
86
+ );
87
+
88
+ schedule
89
+ .command("disable <id>")
90
+ .description("Disable a schedule")
91
+ .action((id) =>
92
+ withDb(program, async (conn) => {
93
+ const s = await updateSchedule(conn, id, { enabled: false });
94
+ if (!s) {
95
+ logger.error(`Schedule not found: ${id}`);
96
+ process.exit(1);
97
+ }
98
+ logger.success(`Disabled schedule: ${s.name}`);
99
+ }),
100
+ );
101
+
102
+ schedule
103
+ .command("delete <id>")
104
+ .description("Delete a schedule")
105
+ .action((id) =>
106
+ withDb(program, async (conn) => {
107
+ const deleted = await deleteSchedule(conn, id);
108
+ if (!deleted) {
109
+ logger.error(`Schedule not found: ${id}`);
110
+ process.exit(1);
111
+ }
112
+ logger.success(`Deleted schedule: ${id}`);
113
+ }),
114
+ );
115
+
116
+ schedule
117
+ .command("trigger <id>")
118
+ .description("Manually trigger a schedule (creates tasks immediately)")
119
+ .action((id) =>
120
+ withDb(program, async (conn, dir) => {
121
+ const s = await getSchedule(conn, id);
122
+ if (!s) {
123
+ logger.error(`Schedule not found: ${id}`);
124
+ process.exit(1);
125
+ }
126
+
127
+ // Lazy import to avoid loading LLM deps for non-trigger commands
128
+ const { evaluateSchedule } = await import("../daemon/schedules.ts");
129
+ const { loadConfig } = await import("../config/loader.ts");
130
+ const { createTask } = await import("../db/tasks.ts");
131
+ const { markScheduleRun } = await import("../db/schedules.ts");
132
+
133
+ const config = await loadConfig(dir);
134
+ const evaluation = await evaluateSchedule(config, s);
135
+
136
+ if (evaluation.tasksToCreate.length === 0) {
137
+ logger.dim("Schedule evaluated but produced no tasks.");
138
+ } else {
139
+ const createdIds: string[] = [];
140
+ for (const taskDef of evaluation.tasksToCreate) {
141
+ const blockedBy = (taskDef.depends_on ?? [])
142
+ .map((i: number) => createdIds[i])
143
+ .filter(Boolean) as string[];
144
+ const t = await createTask(conn, {
145
+ name: taskDef.name,
146
+ description: taskDef.description,
147
+ priority: taskDef.priority,
148
+ blocked_by: blockedBy,
149
+ });
150
+ createdIds.push(t.id);
151
+ logger.success(`Created task: ${t.name} (${t.id})`);
152
+ }
153
+ }
154
+
155
+ await markScheduleRun(conn, s.id);
156
+ logger.info(`Marked schedule "${s.name}" as run.`);
157
+ }),
158
+ );
159
+ }
160
+
161
+ function enabledColor(enabled: boolean): string {
162
+ return enabled ? ansis.green("enabled") : ansis.dim("disabled");
163
+ }
164
+
165
+ function printSchedule(s: Schedule) {
166
+ const id = ansis.dim(s.id.slice(0, 8));
167
+ const lastRun = s.last_run_at
168
+ ? s.last_run_at.toISOString()
169
+ : ansis.dim("never");
170
+ console.log(
171
+ ` ${id} ${enabledColor(s.enabled)} ${s.frequency} ${s.name} (last: ${lastRun})`,
172
+ );
173
+ }
174
+
175
+ function printScheduleDetail(s: Schedule) {
176
+ console.log(ansis.bold(s.name));
177
+ console.log(` ID: ${s.id}`);
178
+ console.log(` Status: ${enabledColor(s.enabled)}`);
179
+ console.log(` Frequency: ${s.frequency}`);
180
+ if (s.description) console.log(` Description: ${s.description}`);
181
+ console.log(
182
+ ` Last run: ${s.last_run_at ? s.last_run_at.toISOString() : ansis.dim("never")}`,
183
+ );
184
+ console.log(` Created: ${s.created_at.toISOString()}`);
185
+ console.log(` Updated: ${s.updated_at.toISOString()}`);
186
+ }