@ai-setting/roy-task-plugin 0.6.1 → 0.7.1
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/dist/claude-plugin/hooks/stop-check.mjs +23 -19
- package/dist/claude-plugin/mcp/bridge.mjs +64 -443
- package/dist/claude-plugin/mcp/roy-task-mcp-server.mjs +75 -50
- package/dist/claude-plugin/mcp/tools.mjs +39 -225
- package/dist/cli.js +7 -6
- package/dist/index.js +525 -524
- package/dist/index.js.map +22 -21
- package/package.json +2 -2
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
* - exit 0 always: a hook that throws must not wedge the session.
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
|
-
import {
|
|
41
|
+
import { createServer, defaultTaskDbPath, BridgeError } from "../mcp/bridge.mjs";
|
|
42
42
|
|
|
43
43
|
/** Read stdin fully, with a hard cap so a huge transcript cannot stall us. */
|
|
44
44
|
async function readStdin(limitBytes = 2 * 1024 * 1024) {
|
|
@@ -132,15 +132,23 @@ function parseTranscript(raw) {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
/** The most recently updated task that is still open. */
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
135
|
+
/**
|
|
136
|
+
* Find the most recently touched task that is still open.
|
|
137
|
+
*
|
|
138
|
+
* Task #4619 — this used to be a hand-written SQL query against roy's own
|
|
139
|
+
* database (`SELECT id, title, status FROM task WHERE status IN ('todo',
|
|
140
|
+
* 'active') ORDER BY updated_at DESC LIMIT 1`). The *query* is task
|
|
141
|
+
* semantics, so it belongs to core, not to the plugin.
|
|
142
|
+
*
|
|
143
|
+
* It is now expressed against core's `TaskBackend`, which owns the status
|
|
144
|
+
* vocabulary and the ordering.
|
|
145
|
+
*/
|
|
146
|
+
async function findOpenTask(backend) {
|
|
147
|
+
const result = await backend.listTasks({ limit: 200 });
|
|
148
|
+
const open = result.tasks
|
|
149
|
+
.filter((t) => t.status === "todo" || t.status === "active")
|
|
150
|
+
.sort((a, b) => (b.updatedAt ?? b.updated_at ?? 0) - (a.updatedAt ?? a.updated_at ?? 0));
|
|
151
|
+
return open[0] ?? null;
|
|
144
152
|
}
|
|
145
153
|
|
|
146
154
|
function approve() {
|
|
@@ -185,9 +193,11 @@ async function main() {
|
|
|
185
193
|
return;
|
|
186
194
|
}
|
|
187
195
|
|
|
188
|
-
let
|
|
196
|
+
let backend;
|
|
189
197
|
try {
|
|
190
|
-
|
|
198
|
+
const server = createServer(process.env.ROY_TASK_DB_PATH || defaultTaskDbPath());
|
|
199
|
+
// The server owns the store; the backend is what the hook needs.
|
|
200
|
+
backend = server.backend;
|
|
191
201
|
} catch (err) {
|
|
192
202
|
// A store we cannot open is not a reason to block the user.
|
|
193
203
|
approve();
|
|
@@ -195,7 +205,7 @@ async function main() {
|
|
|
195
205
|
}
|
|
196
206
|
|
|
197
207
|
try {
|
|
198
|
-
const open = findOpenTask(
|
|
208
|
+
const open = await findOpenTask(backend);
|
|
199
209
|
if (!open) {
|
|
200
210
|
approve();
|
|
201
211
|
return;
|
|
@@ -210,12 +220,6 @@ async function main() {
|
|
|
210
220
|
return;
|
|
211
221
|
}
|
|
212
222
|
approve();
|
|
213
|
-
} finally {
|
|
214
|
-
try {
|
|
215
|
-
db.close();
|
|
216
|
-
} catch {
|
|
217
|
-
// already closed
|
|
218
|
-
}
|
|
219
223
|
}
|
|
220
224
|
}
|
|
221
225
|
|
|
@@ -1,39 +1,48 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Bridge:
|
|
2
|
+
* Bridge: roy's task store + roy's MCP server, wired together.
|
|
3
3
|
*
|
|
4
|
-
* ##
|
|
4
|
+
* ## What this file used to be
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
6
|
+
* Before Task #4619 this was a **second implementation** of roy's task
|
|
7
|
+
* semantics: 482 lines that created its own `task` / `task_operation`
|
|
8
|
+
* schema and implemented every tool as raw SQL — `taskTree` (BFS +
|
|
9
|
+
* `pruneDepth`), `taskCreate`, `taskGet`, `taskList` (COUNT for `total`),
|
|
10
|
+
* `taskUpdate`, `taskOperationCreate`, `taskComplete`, `taskSearch` — with
|
|
11
|
+
* **zero** imports from `@ai-setting/roy-agent-core`.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
13
|
+
* That was the same defect review #4371 found on the opencode side (an
|
|
14
|
+
* 869-line `facade.ts`), except it hid inside a `.mjs` file, so the reviews
|
|
15
|
+
* that counted TypeScript lines never saw it.
|
|
14
16
|
*
|
|
15
|
-
*
|
|
16
|
-
* roy agent writes and the progress card renders. Writing anywhere else
|
|
17
|
-
* would make Claude's tasks invisible to the card.
|
|
18
|
-
* 2. **`node:sqlite`, not a native addon.** Node 22+ ships SQLite in core.
|
|
19
|
-
* No `better-sqlite3` compile step, no `bun:sqlite` — the same file works
|
|
20
|
-
* under plain node, which is the whole point of the published package.
|
|
21
|
-
* (Measured: `better-sqlite3@11.10.0`'s native module aborts the process
|
|
22
|
-
* under node 24 with `Assertion failed: (env) != nullptr`.)
|
|
23
|
-
* 3. **Schema is stable and read-only by default.** We open the database in
|
|
24
|
-
* read-write mode, but every statement is explicit and parameterised.
|
|
17
|
+
* ## What it is now
|
|
25
18
|
*
|
|
26
|
-
*
|
|
19
|
+
* A thin adapter with exactly three responsibilities:
|
|
27
20
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
21
|
+
* 1. **where** the store lives (roy-agent's own `tasks.db`, not the
|
|
22
|
+
* plugin's private one — otherwise Claude would create tasks the
|
|
23
|
+
* progress card can never see);
|
|
24
|
+
* 2. **binding** that store to roy's semantics (`TaskStoreBackend` +
|
|
25
|
+
* `createSQLiteTaskPort`, both from core);
|
|
26
|
+
* 3. **transport** — handing core's MCP server the bytes.
|
|
27
|
+
*
|
|
28
|
+
* Every semantic decision (tree shape, depth, `total`, deliverable
|
|
29
|
+
* merging, milestone filtering, search filters) lives in core, once.
|
|
30
|
+
*
|
|
31
|
+
* ## Runtime
|
|
32
|
+
*
|
|
33
|
+
* Plain node (`#!/usr/bin/env node` in the server entry) — hence core's
|
|
34
|
+
* `node:sqlite` support (no native `better-sqlite3` needed) and core's
|
|
35
|
+
* `createCompatibleDatabase` compat shim.
|
|
31
36
|
*/
|
|
32
37
|
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
|
|
38
|
+
import { createTaskMcpServer } from "@ai-setting/roy-agent-core/env/task/mcp";
|
|
39
|
+
import {
|
|
40
|
+
TaskStoreBackend,
|
|
41
|
+
createSQLiteTaskPort,
|
|
42
|
+
} from "@ai-setting/roy-agent-core/env/task/backend";
|
|
43
|
+
import { SQLiteTaskStore } from "@ai-setting/roy-agent-core/env/task/storage";
|
|
36
44
|
import { homedir } from "node:os";
|
|
45
|
+
import { join } from "node:path";
|
|
37
46
|
|
|
38
47
|
/** Raised when a tool call cannot be served. */
|
|
39
48
|
export class BridgeError extends Error {
|
|
@@ -50,433 +59,45 @@ export function defaultTaskDbPath() {
|
|
|
50
59
|
return join(base, "roy-agent", "tasks.db");
|
|
51
60
|
}
|
|
52
61
|
|
|
53
|
-
/** The columns `task` exposes, in the order the CLI's JSON output uses. */
|
|
54
|
-
function rowToTask(row) {
|
|
55
|
-
return {
|
|
56
|
-
id: row.id,
|
|
57
|
-
title: row.title,
|
|
58
|
-
description: row.description ?? "",
|
|
59
|
-
status: row.status,
|
|
60
|
-
priority: row.priority,
|
|
61
|
-
progress: row.progress ?? 0,
|
|
62
|
-
current_status: row.current_status ?? "",
|
|
63
|
-
goals_and_expected_deliverables: row.goals_and_expected_deliverables ?? "",
|
|
64
|
-
parent_task_id: row.parent_task_id ?? null,
|
|
65
|
-
createdAt: toIso(row.created_at),
|
|
66
|
-
updatedAt: toIso(row.updated_at),
|
|
67
|
-
due_date: row.due_date ?? null,
|
|
68
|
-
tags: parseTags(row.tags),
|
|
69
|
-
project_path: row.project_path ?? "",
|
|
70
|
-
context: row.context ?? "",
|
|
71
|
-
type: row.type ?? "normal",
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function rowToOperation(row) {
|
|
76
|
-
return {
|
|
77
|
-
id: row.id,
|
|
78
|
-
task_id: row.task_id,
|
|
79
|
-
session_id: row.session_id ?? null,
|
|
80
|
-
milestone_type: row.milestone_type,
|
|
81
|
-
milestone_title: row.milestone_title,
|
|
82
|
-
milestone_description: row.milestone_description ?? "",
|
|
83
|
-
timestamp: toIso(row.timestamp),
|
|
84
|
-
md_path: row.md_path ?? null,
|
|
85
|
-
process_description: row.process_description ?? "",
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function toIso(value) {
|
|
90
|
-
if (value === null || value === undefined) return new Date().toISOString();
|
|
91
|
-
if (typeof value === "number") return new Date(value).toISOString();
|
|
92
|
-
const asNumber = Number(value);
|
|
93
|
-
if (Number.isFinite(asNumber) && String(value).trim() !== "") {
|
|
94
|
-
return new Date(asNumber).toISOString();
|
|
95
|
-
}
|
|
96
|
-
return String(value);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function parseTags(value) {
|
|
100
|
-
if (!value) return [];
|
|
101
|
-
try {
|
|
102
|
-
const parsed = JSON.parse(value);
|
|
103
|
-
return Array.isArray(parsed) ? parsed.map(String) : [];
|
|
104
|
-
} catch {
|
|
105
|
-
return String(value)
|
|
106
|
-
.split(",")
|
|
107
|
-
.map((t) => t.trim())
|
|
108
|
-
.filter(Boolean);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function requireString(args, key) {
|
|
113
|
-
const value = args[key];
|
|
114
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
115
|
-
throw new BridgeError(`missing required string argument "${key}"`);
|
|
116
|
-
}
|
|
117
|
-
return value;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function optionalString(args, key) {
|
|
121
|
-
const value = args[key];
|
|
122
|
-
if (value === undefined || value === null) return undefined;
|
|
123
|
-
if (typeof value !== "string") {
|
|
124
|
-
throw new BridgeError(`argument "${key}" must be a string`);
|
|
125
|
-
}
|
|
126
|
-
return value;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function optionalNumber(args, key) {
|
|
130
|
-
const value = args[key];
|
|
131
|
-
if (value === undefined || value === null) return undefined;
|
|
132
|
-
const num = typeof value === "number" ? value : Number(value);
|
|
133
|
-
if (!Number.isFinite(num)) {
|
|
134
|
-
throw new BridgeError(`argument "${key}" must be a number`);
|
|
135
|
-
}
|
|
136
|
-
return num;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function optionalStringArray(args, key) {
|
|
140
|
-
const value = args[key];
|
|
141
|
-
if (value === undefined || value === null) return undefined;
|
|
142
|
-
if (!Array.isArray(value)) {
|
|
143
|
-
throw new BridgeError(`argument "${key}" must be an array of strings`);
|
|
144
|
-
}
|
|
145
|
-
return value.map((v) => String(v));
|
|
146
|
-
}
|
|
147
|
-
|
|
148
62
|
/**
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* We do not run roy-agent's migrations — that would duplicate a versioned
|
|
152
|
-
* migration chain we do not own. Instead we create the tables only when they
|
|
153
|
-
* are **entirely absent**, using the same column set the current roy-agent
|
|
154
|
-
* schema has. If the tables exist we touch nothing, so an existing store is
|
|
155
|
-
* never rewritten by this server.
|
|
63
|
+
* Build the MCP server over roy's own task store.
|
|
156
64
|
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
65
|
+
* This is the whole "binding" the plugin does: core's `SQLiteTaskStore` →
|
|
66
|
+
* core's `createSQLiteTaskPort` → core's `TaskStoreBackend` (the semantics)
|
|
67
|
+
* → core's `createTaskMcpServer` (the transport-agnostic JSON-RPC surface).
|
|
160
68
|
*/
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
updated_at INTEGER,
|
|
174
|
-
due_date TEXT,
|
|
175
|
-
tags TEXT,
|
|
176
|
-
project_path TEXT,
|
|
177
|
-
context TEXT,
|
|
178
|
-
type TEXT NOT NULL DEFAULT 'normal'
|
|
179
|
-
);
|
|
180
|
-
CREATE TABLE IF NOT EXISTS task_operation (
|
|
181
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
182
|
-
task_id INTEGER,
|
|
183
|
-
session_id TEXT,
|
|
184
|
-
milestone_type TEXT,
|
|
185
|
-
milestone_title TEXT,
|
|
186
|
-
milestone_description TEXT,
|
|
187
|
-
timestamp INTEGER,
|
|
188
|
-
md_path TEXT,
|
|
189
|
-
process_description TEXT
|
|
190
|
-
);
|
|
191
|
-
CREATE INDEX IF NOT EXISTS task_operation_task_idx ON task_operation(task_id);
|
|
192
|
-
`;
|
|
193
|
-
|
|
194
|
-
/** Open (and create if needed) the task store. */
|
|
195
|
-
export function openStore(dbPath = defaultTaskDbPath()) {
|
|
196
|
-
if (dbPath !== ":memory:") {
|
|
197
|
-
try {
|
|
198
|
-
mkdirSync(dirname(dbPath), { recursive: true });
|
|
199
|
-
} catch {
|
|
200
|
-
// best effort — the open below will produce the real error
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
const db = new DatabaseSync(dbPath);
|
|
204
|
-
try {
|
|
205
|
-
db.exec(FALLBACK_SCHEMA);
|
|
206
|
-
} catch (err) {
|
|
207
|
-
// An existing store whose schema differs is roy-agent's business, not
|
|
208
|
-
// ours. Surface the problem instead of silently corrupting it.
|
|
209
|
-
process.stderr.write(
|
|
210
|
-
`roy-task mcp server: schema check failed on ${dbPath}: ${err && err.message}\n`,
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
return db;
|
|
69
|
+
export function createServer(dbPath = defaultTaskDbPath()) {
|
|
70
|
+
const store = new SQLiteTaskStore(dbPath);
|
|
71
|
+
const backend = new TaskStoreBackend(createSQLiteTaskPort(store));
|
|
72
|
+
const server = createTaskMcpServer(backend, {
|
|
73
|
+
name: "roy-task",
|
|
74
|
+
version: "1.0.0",
|
|
75
|
+
sessionId: process.env.ROY_TASK_SESSION_ID || "claude-mcp",
|
|
76
|
+
});
|
|
77
|
+
// The hooks (stop-check) need the *backend*, not the MCP surface: they
|
|
78
|
+
// ask "is there an open task?" — a task-semantics question, answered by
|
|
79
|
+
// core's backend, not by the JSON-RPC layer.
|
|
80
|
+
return { ...server, backend, store };
|
|
214
81
|
}
|
|
215
82
|
|
|
216
83
|
/**
|
|
217
84
|
* Serve one tool call.
|
|
218
85
|
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
86
|
+
* Kept as a **synchronous-looking** helper for the MCP entry point: the
|
|
87
|
+
* returned promise resolves to a JSON-serialisable value. Throws
|
|
88
|
+
* `BridgeError` for an unknown tool; other failures surface as tool errors
|
|
89
|
+
* inside the response (never as an unhandled rejection).
|
|
221
90
|
*/
|
|
222
|
-
export function callTool(
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
return taskTree(db, args);
|
|
226
|
-
case "task_create":
|
|
227
|
-
return taskCreate(db, args);
|
|
228
|
-
case "task_get":
|
|
229
|
-
return taskGet(db, args);
|
|
230
|
-
case "task_list":
|
|
231
|
-
return taskList(db, args);
|
|
232
|
-
case "task_update":
|
|
233
|
-
return taskUpdate(db, args);
|
|
234
|
-
case "task_operation_create":
|
|
235
|
-
return taskOperationCreate(db, args);
|
|
236
|
-
case "task_complete":
|
|
237
|
-
return taskComplete(db, args);
|
|
238
|
-
case "task_search":
|
|
239
|
-
return taskSearch(db, args);
|
|
240
|
-
default:
|
|
241
|
-
throw new BridgeError(`unknown tool "${name}"`);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function taskTree(db, args) {
|
|
246
|
-
const depth = optionalNumber(args, "depth");
|
|
247
|
-
const rootId = optionalNumber(args, "parent_task_id");
|
|
248
|
-
const rows = db.prepare("SELECT * FROM task ORDER BY id ASC").all();
|
|
249
|
-
const tasks = rows.map(rowToTask);
|
|
250
|
-
const byId = new Map(tasks.map((t) => [t.id, { ...t, children: [] }]));
|
|
251
|
-
const roots = [];
|
|
252
|
-
for (const node of byId.values()) {
|
|
253
|
-
const parent = node.parent_task_id ? byId.get(node.parent_task_id) : undefined;
|
|
254
|
-
if (parent) parent.children.push(node);
|
|
255
|
-
else roots.push(node);
|
|
256
|
-
}
|
|
257
|
-
const scope = rootId !== undefined ? byId.get(rootId) : undefined;
|
|
258
|
-
const selected = scope ? [scope] : roots;
|
|
259
|
-
const limited =
|
|
260
|
-
depth === undefined || depth < 0 ? selected : pruneDepth(selected, depth);
|
|
261
|
-
return { total: tasks.length, roots: limited.length, tasks: limited };
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function pruneDepth(nodes, depth) {
|
|
265
|
-
if (depth <= 0) return nodes.map((n) => ({ ...n, children: [] }));
|
|
266
|
-
return nodes.map((n) => ({ ...n, children: pruneDepth(n.children, depth - 1) }));
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function taskCreate(db, args) {
|
|
270
|
-
const title = requireString(args, "title");
|
|
271
|
-
const now = Date.now();
|
|
272
|
-
const description = optionalString(args, "description") ?? "";
|
|
273
|
-
const goals = optionalString(args, "goals_and_expected_deliverables") ?? "";
|
|
274
|
-
const priority = optionalString(args, "priority") ?? "medium";
|
|
275
|
-
const type = optionalString(args, "type") ?? "normal";
|
|
276
|
-
const parentId = optionalNumber(args, "parent_task_id") ?? null;
|
|
277
|
-
const projectPath = optionalString(args, "project_path") ?? "";
|
|
278
|
-
const context = optionalString(args, "context") ?? "";
|
|
279
|
-
const tags = optionalStringArray(args, "tags") ?? [];
|
|
280
|
-
|
|
281
|
-
const info = db
|
|
282
|
-
.prepare(
|
|
283
|
-
`INSERT INTO task
|
|
284
|
-
(title, description, status, priority, progress, current_status,
|
|
285
|
-
goals_and_expected_deliverables, parent_task_id, created_at, updated_at,
|
|
286
|
-
tags, project_path, context, type)
|
|
287
|
-
VALUES (?, ?, 'todo', ?, 0, '', ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
288
|
-
)
|
|
289
|
-
.run(
|
|
290
|
-
title,
|
|
291
|
-
description,
|
|
292
|
-
priority,
|
|
293
|
-
goals,
|
|
294
|
-
parentId,
|
|
295
|
-
now,
|
|
296
|
-
now,
|
|
297
|
-
JSON.stringify(tags),
|
|
298
|
-
projectPath,
|
|
299
|
-
context,
|
|
300
|
-
type,
|
|
301
|
-
);
|
|
302
|
-
const id = Number(info.lastInsertRowid);
|
|
303
|
-
const created = db.prepare("SELECT * FROM task WHERE id = ?").get(id);
|
|
304
|
-
return rowToTask(created);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function taskGet(db, args) {
|
|
308
|
-
const id = optionalNumber(args, "id");
|
|
309
|
-
if (id === undefined) throw new BridgeError('missing required number argument "id"');
|
|
310
|
-
const row = db.prepare("SELECT * FROM task WHERE id = ?").get(id);
|
|
311
|
-
if (!row) throw new BridgeError(`task ${id} not found`);
|
|
312
|
-
const task = rowToTask(row);
|
|
313
|
-
const ops = db
|
|
314
|
-
.prepare("SELECT * FROM task_operation WHERE task_id = ? ORDER BY id ASC")
|
|
315
|
-
.all(id)
|
|
316
|
-
.map(rowToOperation);
|
|
317
|
-
return { ...task, operations: ops };
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function taskList(db, args) {
|
|
321
|
-
const clauses = [];
|
|
322
|
-
const params = [];
|
|
323
|
-
const status = optionalString(args, "status");
|
|
324
|
-
if (status) {
|
|
325
|
-
clauses.push("status = ?");
|
|
326
|
-
params.push(status);
|
|
327
|
-
}
|
|
328
|
-
const priority = optionalString(args, "priority");
|
|
329
|
-
if (priority) {
|
|
330
|
-
clauses.push("priority = ?");
|
|
331
|
-
params.push(priority);
|
|
332
|
-
}
|
|
333
|
-
const type = optionalString(args, "type");
|
|
334
|
-
if (type) {
|
|
335
|
-
clauses.push("type = ?");
|
|
336
|
-
params.push(type);
|
|
91
|
+
export async function callTool(server, name, args = {}) {
|
|
92
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
93
|
+
throw new BridgeError("tools/call requires a tool name");
|
|
337
94
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
.all(...params, limit, offset);
|
|
345
|
-
return {
|
|
346
|
-
total,
|
|
347
|
-
count: rows.length,
|
|
348
|
-
truncated: total > rows.length,
|
|
349
|
-
limit,
|
|
350
|
-
offset,
|
|
351
|
-
tasks: rows.map(rowToTask),
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function taskUpdate(db, args) {
|
|
356
|
-
const id = optionalNumber(args, "id");
|
|
357
|
-
if (id === undefined) throw new BridgeError('missing required number argument "id"');
|
|
358
|
-
const existing = db.prepare("SELECT * FROM task WHERE id = ?").get(id);
|
|
359
|
-
if (!existing) throw new BridgeError(`task ${id} not found`);
|
|
360
|
-
|
|
361
|
-
const sets = [];
|
|
362
|
-
const params = [];
|
|
363
|
-
const push = (col, value) => {
|
|
364
|
-
sets.push(`${col} = ?`);
|
|
365
|
-
params.push(value);
|
|
366
|
-
};
|
|
367
|
-
|
|
368
|
-
const status = optionalString(args, "status");
|
|
369
|
-
if (status !== undefined) push("status", status);
|
|
370
|
-
const progress = optionalNumber(args, "progress");
|
|
371
|
-
if (progress !== undefined) push("progress", progress);
|
|
372
|
-
const currentStatus = optionalString(args, "current_status");
|
|
373
|
-
if (currentStatus !== undefined) push("current_status", currentStatus);
|
|
374
|
-
const description = optionalString(args, "description");
|
|
375
|
-
if (description !== undefined) push("description", description);
|
|
376
|
-
const goals = optionalString(args, "goals_and_expected_deliverables");
|
|
377
|
-
if (goals !== undefined) push("goals_and_expected_deliverables", goals);
|
|
378
|
-
const priority = optionalString(args, "priority");
|
|
379
|
-
if (priority !== undefined) push("priority", priority);
|
|
380
|
-
const type = optionalString(args, "type");
|
|
381
|
-
if (type !== undefined) push("type", type);
|
|
382
|
-
push("updated_at", Date.now());
|
|
383
|
-
|
|
384
|
-
params.push(id);
|
|
385
|
-
db.prepare(`UPDATE task SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
386
|
-
return rowToTask(db.prepare("SELECT * FROM task WHERE id = ?").get(id));
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function taskOperationCreate(db, args) {
|
|
390
|
-
const taskId = optionalNumber(args, "task_id");
|
|
391
|
-
if (taskId === undefined) {
|
|
392
|
-
throw new BridgeError('missing required number argument "task_id"');
|
|
393
|
-
}
|
|
394
|
-
const exists = db.prepare("SELECT id FROM task WHERE id = ?").get(taskId);
|
|
395
|
-
if (!exists) throw new BridgeError(`task ${taskId} not found`);
|
|
396
|
-
|
|
397
|
-
const milestoneType = requireString(args, "milestone_type");
|
|
398
|
-
const milestoneTitle = requireString(args, "milestone_title");
|
|
399
|
-
const milestoneDescription = optionalString(args, "milestone_description") ?? "";
|
|
400
|
-
const processDescription = optionalString(args, "process_description") ?? null;
|
|
401
|
-
const sessionId = optionalString(args, "session_id") ?? null;
|
|
402
|
-
|
|
403
|
-
// The 100-150 char rule is roy-agent's, not ours — but the CLI validates it
|
|
404
|
-
// on the way in, so the MCP tool has to as well or the two paths diverge.
|
|
405
|
-
if (processDescription !== null) {
|
|
406
|
-
const len = processDescription.length;
|
|
407
|
-
if (len > 0 && (len < 100 || len > 150)) {
|
|
408
|
-
throw new BridgeError(
|
|
409
|
-
`process_description must be 100-150 characters (got ${len})`,
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
const info = db
|
|
415
|
-
.prepare(
|
|
416
|
-
`INSERT INTO task_operation
|
|
417
|
-
(task_id, session_id, milestone_type, milestone_title,
|
|
418
|
-
milestone_description, timestamp, md_path, process_description)
|
|
419
|
-
VALUES (?, ?, ?, ?, ?, ?, NULL, ?)`,
|
|
420
|
-
)
|
|
421
|
-
.run(
|
|
422
|
-
taskId,
|
|
423
|
-
sessionId,
|
|
424
|
-
milestoneType,
|
|
425
|
-
milestoneTitle,
|
|
426
|
-
milestoneDescription,
|
|
427
|
-
Date.now(),
|
|
428
|
-
processDescription,
|
|
95
|
+
try {
|
|
96
|
+
return await server.callTool(name, args);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
if (err instanceof BridgeError) throw err;
|
|
99
|
+
throw new BridgeError(
|
|
100
|
+
err instanceof Error ? err.message : `tool "${name}" failed: ${String(err)}`,
|
|
429
101
|
);
|
|
430
|
-
const row = db
|
|
431
|
-
.prepare("SELECT * FROM task_operation WHERE id = ?")
|
|
432
|
-
.get(Number(info.lastInsertRowid));
|
|
433
|
-
return rowToOperation(row);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
function taskComplete(db, args) {
|
|
437
|
-
const id = optionalNumber(args, "id");
|
|
438
|
-
if (id === undefined) throw new BridgeError('missing required number argument "id"');
|
|
439
|
-
const existing = db.prepare("SELECT * FROM task WHERE id = ?").get(id);
|
|
440
|
-
if (!existing) throw new BridgeError(`task ${id} not found`);
|
|
441
|
-
|
|
442
|
-
const deliverable = optionalString(args, "deliverable_storage_path");
|
|
443
|
-
const now = Date.now();
|
|
444
|
-
const sets = ["status = ?", "progress = 100", "updated_at = ?"];
|
|
445
|
-
const params = ["completed", now];
|
|
446
|
-
if (deliverable) {
|
|
447
|
-
// Same convention roy-agent uses: the deliverable lives in the task's
|
|
448
|
-
// context JSON under `deliverable_storage_path`.
|
|
449
|
-
let context = {};
|
|
450
|
-
try {
|
|
451
|
-
context = existing.context ? JSON.parse(existing.context) : {};
|
|
452
|
-
} catch {
|
|
453
|
-
context = {};
|
|
454
|
-
}
|
|
455
|
-
context.deliverable_storage_path = deliverable;
|
|
456
|
-
sets.push("context = ?");
|
|
457
|
-
params.push(JSON.stringify(context));
|
|
458
|
-
}
|
|
459
|
-
params.push(id);
|
|
460
|
-
db.prepare(`UPDATE task SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
461
|
-
return rowToTask(db.prepare("SELECT * FROM task WHERE id = ?").get(id));
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
function taskSearch(db, args) {
|
|
465
|
-
const keywords = optionalStringArray(args, "keywords");
|
|
466
|
-
if (!keywords || keywords.length === 0) {
|
|
467
|
-
throw new BridgeError('missing required array argument "keywords"');
|
|
468
|
-
}
|
|
469
|
-
const limit = optionalNumber(args, "limit") ?? 20;
|
|
470
|
-
const where = keywords
|
|
471
|
-
.map(() => "(title LIKE ? OR description LIKE ? OR goals_and_expected_deliverables LIKE ? OR tags LIKE ?)")
|
|
472
|
-
.join(" AND ");
|
|
473
|
-
const params = [];
|
|
474
|
-
for (const kw of keywords) {
|
|
475
|
-
const like = `%${kw}%`;
|
|
476
|
-
params.push(like, like, like, like);
|
|
477
102
|
}
|
|
478
|
-
const rows = db
|
|
479
|
-
.prepare(`SELECT * FROM task WHERE ${where} ORDER BY id DESC LIMIT ?`)
|
|
480
|
-
.all(...params, limit);
|
|
481
|
-
return { total: rows.length, count: rows.length, tasks: rows.map(rowToTask) };
|
|
482
103
|
}
|