prodboard 0.3.0 → 0.5.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # prodboard
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#23](https://github.com/G4brym/prodboard/pull/23) [`146b3b5`](https://github.com/G4brym/prodboard/commit/146b3b5105ad127c3eb31f4e8c6ea19918329380) Thanks [@G4brym](https://github.com/G4brym)! - Optimize MCP list handlers: trim prompt from list_schedules, add get_schedule tool, exclude stderr_tail from list_runs defaults
8
+
9
+ ## 0.4.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [#21](https://github.com/G4brym/prodboard/pull/21) [`761e8fc`](https://github.com/G4brym/prodboard/commit/761e8fce4cd370424903753952835f7a399730bb) Thanks [@G4brym](https://github.com/G4brym)! - Add per-schedule and global model selection for Claude and OpenCode agents
14
+
3
15
  ## 0.3.0
4
16
 
5
17
  ### Minor Changes
package/README.md CHANGED
@@ -150,8 +150,11 @@ prodboard comments <id> # List comments
150
150
 
151
151
  ```bash
152
152
  prodboard schedule add --name "job" --cron "0 9 * * *" --prompt "Do X"
153
+ prodboard schedule add --name "fast" --cron "*/30 * * * *" --prompt "Do Y" --model claude-sonnet-4-6
153
154
  prodboard schedule ls # List schedules
154
155
  prodboard schedule edit <id> --cron "0 10 * * *" # Edit
156
+ prodboard schedule edit <id> --model claude-opus-4-6 # Set model
157
+ prodboard schedule edit <id> --model "" # Clear model override
155
158
  prodboard schedule enable <id> # Enable
156
159
  prodboard schedule disable <id> # Disable
157
160
  prodboard schedule rm <id> --force # Delete
@@ -193,7 +196,8 @@ These are the tools Claude Code sees when connected to the board:
193
196
  | `add_comment` | Leave notes on issues (default author: "claude") |
194
197
  | `pick_next_issue` | Claim the oldest todo, move to in-progress |
195
198
  | `complete_issue` | Mark done with an optional summary comment |
196
- | `list_schedules` | See scheduled jobs |
199
+ | `list_schedules` | See scheduled jobs (compact, excludes prompt) |
200
+ | `get_schedule` | Read full schedule details including prompt |
197
201
  | `create_schedule` | Set up a new cron job |
198
202
  | `update_schedule` | Modify a schedule |
199
203
  | `delete_schedule` | Remove a schedule |
@@ -226,6 +230,34 @@ OpenCode-specific settings:
226
230
  }
227
231
  ```
228
232
 
233
+ ## Model Selection
234
+
235
+ You can control which model is used for scheduled runs at two levels:
236
+
237
+ **Global default** — Set `daemon.model` in your config to apply to all schedules:
238
+
239
+ ```jsonc
240
+ {
241
+ "daemon": {
242
+ "model": "claude-sonnet-4-6"
243
+ }
244
+ }
245
+ ```
246
+
247
+ **Per-schedule override** — Set `--model` when creating or editing a schedule:
248
+
249
+ ```bash
250
+ prodboard schedule add --name "triage" --cron "0 9 * * *" --prompt "Triage the board" --model claude-opus-4-6
251
+ prodboard schedule edit <id> --model claude-haiku-4-5-20251001
252
+ prodboard schedule edit <id> --model "" # clear override, fall back to global
253
+ ```
254
+
255
+ **Resolution order:** schedule `--model` > `daemon.model` > agent's built-in default. For OpenCode, `daemon.opencode.model` sits between the global `daemon.model` and the agent default.
256
+
257
+ Example model IDs:
258
+ - Claude Code: `claude-sonnet-4-6`, `claude-opus-4-6`, `claude-haiku-4-5-20251001`
259
+ - OpenCode: `anthropic/claude-sonnet-4-20250514`, etc.
260
+
229
261
  ## Configuration
230
262
 
231
263
  Config file: `~/.prodboard/config.jsonc`
@@ -239,6 +271,7 @@ Config file: `~/.prodboard/config.jsonc`
239
271
  },
240
272
  "daemon": {
241
273
  "agent": "claude", // "claude" or "opencode"
274
+ "model": null, // default model for runs (null = agent's default)
242
275
  "basePath": null, // base path for worktrees and runs (null = use schedule workdir)
243
276
  "useTmux": true, // wrap agent runs in tmux sessions
244
277
  "maxConcurrentRuns": 2,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prodboard",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Self-hosted, CLI-first issue tracker and cron scheduler for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -53,6 +53,11 @@ export class ClaudeDriver implements AgentDriver {
53
53
  args.push("--agents", schedule.agents_json);
54
54
  }
55
55
 
56
+ const model = schedule.model ?? config.daemon.model;
57
+ if (model) {
58
+ args.push("--model", model);
59
+ }
60
+
56
61
  return args;
57
62
  }
58
63
 
@@ -17,8 +17,9 @@ export class OpenCodeDriver implements AgentDriver {
17
17
  args.push("--attach", opencode.serverUrl);
18
18
  }
19
19
 
20
- if (opencode.model) {
21
- args.push("--model", opencode.model);
20
+ const model = schedule.model ?? config.daemon.opencode.model ?? config.daemon.model;
21
+ if (model) {
22
+ args.push("--model", model);
22
23
  }
23
24
 
24
25
  if (opencode.agent) {
@@ -77,6 +77,7 @@ export async function scheduleAdd(args: string[], dbOverride?: Database): Promis
77
77
  use_worktree: !flags["no-worktree"],
78
78
  inject_context: !flags["no-context"],
79
79
  persist_session: !!flags["persist-session"],
80
+ model: flags.model as string | undefined,
80
81
  });
81
82
 
82
83
  console.log(`Created schedule ${schedule.id}: ${schedule.name} [${schedule.cron}]`);
@@ -101,16 +102,16 @@ export async function scheduleLs(args: string[], dbOverride?: Database): Promise
101
102
  }
102
103
 
103
104
  const table = renderTable(
104
- ["ID", "Name", "Cron", "Enabled", "Next Fire"],
105
+ ["ID", "Name", "Cron", "Model", "Enabled", "Next Fire"],
105
106
  schedules.map((s) => {
106
107
  let nextFire = "";
107
108
  try {
108
109
  const next = getNextFire(s.cron, new Date());
109
110
  nextFire = formatDate(next.toISOString());
110
111
  } catch {}
111
- return [s.id, s.name, s.cron, s.enabled ? "yes" : "no", nextFire];
112
+ return [s.id, s.name, s.cron, s.model ?? "-", s.enabled ? "yes" : "no", nextFire];
112
113
  }),
113
- { maxWidths: [10, 30, 20, 8, 18] }
114
+ { maxWidths: [10, 30, 20, 20, 8, 18] }
114
115
  );
115
116
  console.log(table);
116
117
  console.log(`${schedules.length} schedule${schedules.length === 1 ? "" : "s"}`);
@@ -140,6 +141,7 @@ export async function scheduleEdit(args: string[], dbOverride?: Database): Promi
140
141
  }
141
142
  if (flags.prompt || flags.p) fields.prompt = flags.prompt ?? flags.p;
142
143
  if (flags["max-turns"]) fields.max_turns = parseInt(flags["max-turns"] as string, 10);
144
+ if (flags.model !== undefined) fields.model = flags.model === "" ? null : flags.model;
143
145
 
144
146
  const updated = updateSchedule(db, schedule.id, fields);
145
147
  console.log(`Updated schedule ${updated.id}: ${updated.name}`);
package/src/config.ts CHANGED
@@ -73,6 +73,7 @@ export function getDefaults(): Config {
73
73
  },
74
74
  daemon: {
75
75
  agent: "claude",
76
+ model: null,
76
77
  basePath: null,
77
78
  useTmux: true,
78
79
  opencode: {
package/src/db.ts CHANGED
@@ -108,6 +108,10 @@ const MIGRATIONS: Migration[] = [
108
108
  ALTER TABLE runs ADD COLUMN agent TEXT NOT NULL DEFAULT 'claude';
109
109
  `,
110
110
  },
111
+ {
112
+ version: 3,
113
+ sql: `ALTER TABLE schedules ADD COLUMN model TEXT;`,
114
+ },
111
115
  ];
112
116
 
113
117
  export { MIGRATIONS };
package/src/mcp.ts CHANGED
@@ -149,7 +149,7 @@ const TOOLS = [
149
149
  },
150
150
  {
151
151
  name: "list_schedules",
152
- description: "List scheduled tasks with their status and last run info.",
152
+ description: "List scheduled tasks (compact: excludes prompt, agents_json, allowed_tools). Use get_schedule for full details.",
153
153
  inputSchema: {
154
154
  type: "object" as const,
155
155
  properties: {
@@ -157,6 +157,17 @@ const TOOLS = [
157
157
  },
158
158
  },
159
159
  },
160
+ {
161
+ name: "get_schedule",
162
+ description: "Get full details of a scheduled task, including prompt text.",
163
+ inputSchema: {
164
+ type: "object" as const,
165
+ properties: {
166
+ id: { type: "string" as const, description: "Schedule ID or prefix" },
167
+ },
168
+ required: ["id"],
169
+ },
170
+ },
160
171
  {
161
172
  name: "create_schedule",
162
173
  description: "Create a new scheduled task.",
@@ -168,6 +179,7 @@ const TOOLS = [
168
179
  prompt: { type: "string" as const, description: "Prompt to send to Claude" },
169
180
  workdir: { type: "string" as const, description: "Working directory" },
170
181
  max_turns: { type: "number" as const, description: "Max turns per run" },
182
+ model: { type: "string" as const, description: "Model to use for this schedule (e.g. claude-sonnet-4-6)" },
171
183
  },
172
184
  required: ["name", "cron", "prompt"],
173
185
  },
@@ -183,6 +195,7 @@ const TOOLS = [
183
195
  cron: { type: "string" as const },
184
196
  prompt: { type: "string" as const },
185
197
  enabled: { type: "boolean" as const },
198
+ model: { type: "string" as const, description: "Model override (empty string to clear)" },
186
199
  },
187
200
  required: ["id"],
188
201
  },
@@ -378,13 +391,29 @@ export async function handleListSchedules(db: Database, params: any) {
378
391
  lastRun = rq.getLastRun(db, s.id);
379
392
  }
380
393
  result.push({
381
- ...s,
394
+ id: s.id,
395
+ name: s.name,
396
+ cron: s.cron,
397
+ workdir: s.workdir,
398
+ enabled: s.enabled,
399
+ model: s.model,
400
+ max_turns: s.max_turns,
401
+ use_worktree: s.use_worktree,
402
+ source: s.source,
403
+ created_at: s.created_at,
404
+ updated_at: s.updated_at,
382
405
  last_run: lastRun ? { status: lastRun.status, finished_at: lastRun.finished_at } : null,
383
406
  });
384
407
  }
385
408
  return result;
386
409
  }
387
410
 
411
+ export async function handleGetSchedule(db: Database, params: any) {
412
+ const sq = await getScheduleQueries();
413
+ if (!sq) throw new Error("Schedule module not available");
414
+ return sq.getScheduleByPrefix(db, params.id);
415
+ }
416
+
388
417
  export async function handleCreateSchedule(db: Database, params: any) {
389
418
  const sq = await getScheduleQueries();
390
419
  if (!sq) throw new Error("Schedule module not available");
@@ -401,6 +430,7 @@ export async function handleCreateSchedule(db: Database, params: any) {
401
430
  prompt: params.prompt,
402
431
  workdir: params.workdir,
403
432
  max_turns: params.max_turns,
433
+ model: params.model,
404
434
  source: "mcp",
405
435
  });
406
436
  }
@@ -420,6 +450,7 @@ export async function handleUpdateSchedule(db: Database, params: any) {
420
450
  }
421
451
  if (params.prompt !== undefined) fields.prompt = params.prompt;
422
452
  if (params.enabled !== undefined) fields.enabled = params.enabled ? 1 : 0;
453
+ if (params.model !== undefined) fields.model = params.model === "" ? null : params.model;
423
454
 
424
455
  return sq.updateSchedule(db, schedule.id, fields);
425
456
  }
@@ -540,6 +571,9 @@ export async function startMcpServer(): Promise<void> {
540
571
  case "list_schedules":
541
572
  result = await handleListSchedules(db, params ?? {});
542
573
  break;
574
+ case "get_schedule":
575
+ result = await handleGetSchedule(db, params ?? {});
576
+ break;
543
577
  case "create_schedule":
544
578
  result = await handleCreateSchedule(db, params ?? {});
545
579
  break;
@@ -70,7 +70,7 @@ export function listRuns(
70
70
  const columns = opts?.include_output
71
71
  ? "r.*"
72
72
  : `r.id, r.schedule_id, r.status, r.pid, r.started_at, r.finished_at,
73
- r.exit_code, r.stderr_tail, r.session_id, r.worktree_path,
73
+ r.exit_code, r.session_id, r.worktree_path,
74
74
  r.tokens_in, r.tokens_out, r.cost_usd, r.tools_used,
75
75
  r.issues_touched, r.tmux_session, r.agent`;
76
76
 
@@ -16,6 +16,7 @@ export function createSchedule(
16
16
  persist_session?: boolean;
17
17
  agents_json?: string;
18
18
  source?: string;
19
+ model?: string;
19
20
  }
20
21
  ): Schedule {
21
22
  const id = generateId();
@@ -23,8 +24,8 @@ export function createSchedule(
23
24
 
24
25
  db.query(`
25
26
  INSERT INTO schedules (id, name, cron, prompt, workdir, max_turns, allowed_tools,
26
- use_worktree, inject_context, persist_session, agents_json, source, created_at, updated_at)
27
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
27
+ use_worktree, inject_context, persist_session, agents_json, source, model, created_at, updated_at)
28
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
28
29
  `).run(
29
30
  id, opts.name, opts.cron, opts.prompt,
30
31
  opts.workdir ?? ".",
@@ -35,6 +36,7 @@ export function createSchedule(
35
36
  opts.persist_session ? 1 : 0,
36
37
  opts.agents_json ?? null,
37
38
  opts.source ?? "cli",
39
+ opts.model ?? null,
38
40
  now, now
39
41
  );
40
42
 
@@ -83,6 +85,7 @@ export function updateSchedule(
83
85
  enabled: "enabled", max_turns: "max_turns", allowed_tools: "allowed_tools",
84
86
  use_worktree: "use_worktree", inject_context: "inject_context",
85
87
  persist_session: "persist_session", agents_json: "agents_json",
88
+ model: "model",
86
89
  };
87
90
 
88
91
  let hasRealFields = false;
package/src/types.ts CHANGED
@@ -6,6 +6,7 @@ export interface Config {
6
6
  };
7
7
  daemon: {
8
8
  agent: "claude" | "opencode";
9
+ model: string | null;
9
10
  basePath: string | null;
10
11
  useTmux: boolean;
11
12
  opencode: {
@@ -62,6 +63,7 @@ export interface Schedule {
62
63
  use_worktree: number;
63
64
  inject_context: number;
64
65
  persist_session: number;
66
+ model: string | null;
65
67
  agents_json: string | null;
66
68
  source: string;
67
69
  created_at: string;
@@ -17,6 +17,12 @@
17
17
  // Agent to use: "claude" or "opencode"
18
18
  // "agent": "claude",
19
19
 
20
+ // Default model for agent runs (null = agent's default)
21
+ // For Claude: "claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5-20251001"
22
+ // For OpenCode: "anthropic/claude-sonnet-4-20250514", etc.
23
+ // Can be overridden per-schedule with --model
24
+ // "model": null,
25
+
20
26
  // Base path for worktrees and agent runs (null = use schedule workdir)
21
27
  // "basePath": null,
22
28