@sleep2agi/commhub-server 0.9.0-preview.90 → 0.9.0-preview.91
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 +1 -1
- package/src/auth.ts +40 -6
- package/src/department-heads-http.test.ts +329 -0
- package/src/department-heads.ts +226 -0
- package/src/departments.ts +7 -1
- package/src/mcp-context-cost-http.test.ts +117 -0
- package/src/network-token-membership-http.test.ts +139 -0
- package/src/observer-avatar-http.test.ts +7 -5
- package/src/requirement-notify.ts +31 -0
- package/src/requirements-errors.ts +2 -0
- package/src/requirements.ts +45 -4
- package/src/rest-explicit-columns-http.test.ts +2 -0
- package/src/server.ts +131 -37
- package/src/task-access.ts +68 -10
- package/src/tools.ts +47 -33
package/src/tools.ts
CHANGED
|
@@ -148,9 +148,9 @@ function scheduledReplyRecipient(taskId: string, networkId: string | null | unde
|
|
|
148
148
|
// 方无影响,只是错误形态更一致)。
|
|
149
149
|
const NODE_ID_ALIAS_FIELDS = {
|
|
150
150
|
node_id: z.string().min(1).max(200).optional()
|
|
151
|
-
.describe("
|
|
151
|
+
.describe("Child node id (as returned by create_node), in your network."),
|
|
152
152
|
child_node_id: z.string().min(1).max(200).optional()
|
|
153
|
-
.describe("
|
|
153
|
+
.describe("Deprecated alias of node_id."),
|
|
154
154
|
} as const;
|
|
155
155
|
|
|
156
156
|
/** #1281 — 把 node_id / child_node_id 收敛成单一内部 id。两个都传且不等 ⇒
|
|
@@ -1639,7 +1639,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
1639
1639
|
network_id: z.string().max(200).optional().describe("Network scope"),
|
|
1640
1640
|
parent_task_id: z.string().max(200).optional().describe("Parent task this dispatch is on behalf of. When the child task replies the hub will auto-chain the answer to the parent task's originator, so the user sees the final result even if the intermediate session ends."),
|
|
1641
1641
|
meta: z.any().optional().describe("Optional structured task metadata, e.g. { attachments: [{ type, path, url, mime, name, size }] }."),
|
|
1642
|
-
force: z.boolean().optional().describe("
|
|
1642
|
+
force: z.boolean().optional().describe("dispatch even if the target is degraded (user tokens only)"),
|
|
1643
1643
|
},
|
|
1644
1644
|
async ({ alias, task, priority, context, from_session: _fromIn, ttl_seconds, network_id: netId, parent_task_id: parentIn, meta, force }) => { const fromMismatch = fromIdentityMismatchReply(_fromIn); if (fromMismatch) return fromMismatch; const from_session = defaultFrom(_fromIn);
|
|
1645
1645
|
const effectiveNetId = getNetworkId(netId);
|
|
@@ -1972,7 +1972,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
1972
1972
|
// `attachments` before this schema entry existed would see `ok:true`
|
|
1973
1973
|
// and never learn the attachments were dropped. Validated by
|
|
1974
1974
|
// validateAttachments (uploads.ts) — the same helper the REST path uses.
|
|
1975
|
-
attachments: z.any().optional().describe("
|
|
1975
|
+
attachments: z.any().optional().describe("Attachments, like send_task's meta.attachments: [{type:'file', file_id, name?, mime?, size?}]."),
|
|
1976
1976
|
// #507 — optional structured metadata (parity with send_task L837). If
|
|
1977
1977
|
// both `attachments` (top-level) and `meta.attachments` are supplied,
|
|
1978
1978
|
// top-level wins (same rule as REST /api/task L2101).
|
|
@@ -2359,7 +2359,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
2359
2359
|
task_id: z.string().min(1).max(200).describe("Task ID to retry"),
|
|
2360
2360
|
from_session: z.string().max(200).optional(),
|
|
2361
2361
|
network_id: z.string().max(200).optional().describe("Network scope (auto-resolved for single-network user tokens)"),
|
|
2362
|
-
force: z.boolean().optional().describe("
|
|
2362
|
+
force: z.boolean().optional().describe("dispatch even if the target is degraded (user tokens only)"),
|
|
2363
2363
|
},
|
|
2364
2364
|
async ({ task_id, from_session: _fromIn, network_id: netId, force }) => { const fromMismatch = fromIdentityMismatchReply(_fromIn); if (fromMismatch) return fromMismatch; const from_session = defaultFrom(_fromIn);
|
|
2365
2365
|
const effectiveNetId = getNetworkId(netId);
|
|
@@ -2578,7 +2578,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
2578
2578
|
new_alias: z.string().min(1).max(200).describe("Target agent alias"),
|
|
2579
2579
|
from_session: z.string().max(200).optional(),
|
|
2580
2580
|
network_id: z.string().max(200).optional().describe("Network scope (auto-resolved for single-network user tokens)"),
|
|
2581
|
-
force: z.boolean().optional().describe("
|
|
2581
|
+
force: z.boolean().optional().describe("dispatch even if the target is degraded (user tokens only)"),
|
|
2582
2582
|
},
|
|
2583
2583
|
async ({ task_id, new_alias, from_session: _fromIn, network_id: netId, force }) => { const fromMismatch = fromIdentityMismatchReply(_fromIn); if (fromMismatch) return fromMismatch; const from_session = defaultFrom(_fromIn);
|
|
2584
2584
|
const effectiveNetId = getNetworkId(netId);
|
|
@@ -3509,7 +3509,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
3509
3509
|
"Ask a node to send back its rules file (CLAUDE.md for claude nodes, AGENTS.md otherwise) from its working directory. No path argument by design. Poll get_rules_file_result with the returned request_id. app#225.",
|
|
3510
3510
|
{
|
|
3511
3511
|
...NODE_ID_ALIAS_FIELDS,
|
|
3512
|
-
alias: z.string().min(1).max(200).optional().describe("
|
|
3512
|
+
alias: z.string().min(1).max(200).optional().describe("Alias instead of node_id (a node, or a session reporting rules_file_capable)."),
|
|
3513
3513
|
network_id: z.string().max(200).optional(),
|
|
3514
3514
|
},
|
|
3515
3515
|
async ({ node_id, child_node_id, alias, network_id }) => enqueueRulesFileRequest("read", { node_id, child_node_id, alias, network_id }),
|
|
@@ -3520,7 +3520,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
3520
3520
|
"Ask a node to overwrite its rules file (CLAUDE.md for claude nodes, AGENTS.md otherwise) in its working directory with `content`. No path argument by design. Poll get_rules_file_result with the returned request_id. app#225.",
|
|
3521
3521
|
{
|
|
3522
3522
|
...NODE_ID_ALIAS_FIELDS,
|
|
3523
|
-
alias: z.string().min(1).max(200).optional().describe("
|
|
3523
|
+
alias: z.string().min(1).max(200).optional().describe("Alias instead of node_id (a node, or a session reporting rules_file_capable)."),
|
|
3524
3524
|
content: z.string().max(RULES_FILE_MAX_BYTES).describe("Full new file content (UTF-8). The node writes it atomically."),
|
|
3525
3525
|
network_id: z.string().max(200).optional(),
|
|
3526
3526
|
},
|
|
@@ -3532,7 +3532,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
3532
3532
|
"Ask a node to list the skills its runtime loads (name, scope project|user|system, display path, frontmatter description). Read-only; no path argument. Poll get_rules_file_result — content is JSON {skills:[…]}.",
|
|
3533
3533
|
{
|
|
3534
3534
|
...NODE_ID_ALIAS_FIELDS,
|
|
3535
|
-
alias: z.string().min(1).max(200).optional().describe("
|
|
3535
|
+
alias: z.string().min(1).max(200).optional().describe("Alias instead of node_id (a node, or a session reporting skills_capable)."),
|
|
3536
3536
|
network_id: z.string().max(200).optional(),
|
|
3537
3537
|
},
|
|
3538
3538
|
async ({ node_id, child_node_id, alias, network_id }) => enqueueRulesFileRequest("skills_list", { node_id, child_node_id, alias, network_id }),
|
|
@@ -3543,7 +3543,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
3543
3543
|
"Ask a node for one skill's SKILL.md by name. Read-only; the node resolves the name under its own runtime's skills roots — no path argument. Poll get_rules_file_result — content is JSON {name, scope, path_rel, description, content}.",
|
|
3544
3544
|
{
|
|
3545
3545
|
...NODE_ID_ALIAS_FIELDS,
|
|
3546
|
-
alias: z.string().min(1).max(200).optional().describe("
|
|
3546
|
+
alias: z.string().min(1).max(200).optional().describe("Alias instead of node_id (a node, or a session reporting skills_capable)."),
|
|
3547
3547
|
name: z.string().min(1).max(64).describe("Skill directory name, [A-Za-z0-9._-]."),
|
|
3548
3548
|
network_id: z.string().max(200).optional(),
|
|
3549
3549
|
},
|
|
@@ -3555,7 +3555,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
3555
3555
|
"Ask a node to list one directory level of its work dir (project folder view, read-only). `path` is relative to the work dir (default: the root); absolute paths and .. are refused. Poll get_rules_file_result — content is JSON {path, entries:[{name,type,size?,mtime?,hidden_reason?,no_descend?}], truncated, total}. User logins only.",
|
|
3556
3556
|
{
|
|
3557
3557
|
...NODE_ID_ALIAS_FIELDS,
|
|
3558
|
-
alias: z.string().min(1).max(200).optional().describe("
|
|
3558
|
+
alias: z.string().min(1).max(200).optional().describe("Alias instead of node_id (a node, or a session reporting files_capable)."),
|
|
3559
3559
|
path: z.string().max(NODE_FILE_PATH_MAX).optional().describe("Directory relative to the node's work dir; omit or \"\" for the root."),
|
|
3560
3560
|
network_id: z.string().max(200).optional(),
|
|
3561
3561
|
},
|
|
@@ -3567,7 +3567,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
3567
3567
|
"Ask a node for one text file under its work dir (project folder view, read-only, 256 KiB cap; binary / too large → size only; secret-looking files → name only). `path` is relative to the work dir. Poll get_rules_file_result — content is JSON {path, name, kind, size?, mtime?, content?, hidden_reason?}. User logins only.",
|
|
3568
3568
|
{
|
|
3569
3569
|
...NODE_ID_ALIAS_FIELDS,
|
|
3570
|
-
alias: z.string().min(1).max(200).optional().describe("
|
|
3570
|
+
alias: z.string().min(1).max(200).optional().describe("Alias instead of node_id (a node, or a session reporting files_capable)."),
|
|
3571
3571
|
path: z.string().min(1).max(NODE_FILE_PATH_MAX).describe("File path relative to the node's work dir."),
|
|
3572
3572
|
network_id: z.string().max(200).optional(),
|
|
3573
3573
|
},
|
|
@@ -3579,7 +3579,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
3579
3579
|
"Ask a node for the tail of its own agent-node run log (read-only, redacted on the node before it leaves). No path argument: the node reads only its own log directory. User logins only; the node's owner or a network owner/admin. Poll get_rules_file_result — content is JSON {files, lines:[{ts, level, text, key}], truncated, matched, now_ts}; it is handed out once and then purged.",
|
|
3580
3580
|
{
|
|
3581
3581
|
...NODE_ID_ALIAS_FIELDS,
|
|
3582
|
-
alias: z.string().min(1).max(200).optional().describe("
|
|
3582
|
+
alias: z.string().min(1).max(200).optional().describe("Alias instead of node_id (a node, or a session reporting logs_capable)."),
|
|
3583
3583
|
lines: z.number().int().min(1).max(LOGS_MAX_LINES).optional().describe("How many of the newest matching lines (default 500, max 2000)."),
|
|
3584
3584
|
level: z.enum(["info", "warn", "error"]).optional().describe("Only lines of exactly this level."),
|
|
3585
3585
|
grep: z.string().min(1).max(LOGS_GREP_MAX).optional().describe("Case-insensitive substring, matched after redaction."),
|
|
@@ -5844,9 +5844,11 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
5844
5844
|
// (requirements.ts personRef;找不到 / 别名对上多个 → 400 + hint)。给了 id 就只看 id。
|
|
5845
5845
|
const reqPerson = z.object({
|
|
5846
5846
|
kind: z.enum(["user", "node"]),
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
5847
|
+
// #476 —— 这三格的说明不写在这里:reqPerson 在 3 个工具 × 3 个字段里展开,每份都复制一遍(~1.6 KB)。
|
|
5848
|
+
// 写法在 owner / agent_owner / participants 的 describe 里各说一次。
|
|
5849
|
+
id: z.string().min(1).max(200).optional(),
|
|
5850
|
+
username: z.string().min(1).max(100).optional(),
|
|
5851
|
+
alias: z.string().min(1).max(200).optional(),
|
|
5850
5852
|
});
|
|
5851
5853
|
const reqChecklistItem = z.object({ id: z.string().regex(/^[A-Za-z0-9_-]{1,40}$/).optional(), text: z.string().min(1).max(500), done: z.boolean().optional() });
|
|
5852
5854
|
const reqFields = {
|
|
@@ -5859,9 +5861,9 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
5859
5861
|
description: z.string().max(20_000).optional().describe("markdown"),
|
|
5860
5862
|
checklist: z.array(reqChecklistItem).max(100).optional().describe("replaces the whole list"),
|
|
5861
5863
|
project_id: z.string().max(200).nullable().optional(),
|
|
5862
|
-
owner: reqPerson.nullable().optional().describe("负责人 (a person): {kind:'user', id}
|
|
5863
|
-
agent_owner: reqPerson.nullable().optional().describe("负责 Agent: {kind:'node', id
|
|
5864
|
-
participants: z.array(reqPerson).max(100).optional().describe("REPLACES the whole
|
|
5864
|
+
owner: reqPerson.nullable().optional().describe("负责人 (a person): {kind:'user', id | username} (ids / usernames from requirements_people; names resolve in the task's network); a node is rejected with owner_must_be_human — use agent_owner"),
|
|
5865
|
+
agent_owner: reqPerson.nullable().optional().describe("负责 Agent: {kind:'node', id | alias}"),
|
|
5866
|
+
participants: z.array(reqPerson).max(100).optional().describe("REPLACES the whole list (written like owner / agent_owner; omitted people are removed, [] clears). To add or remove a few, use participants_add / participants_remove on requirements_update."),
|
|
5865
5867
|
external_url: z.string().max(500).nullable().optional(),
|
|
5866
5868
|
parent_id: z.string().regex(/^[A-Za-z0-9_-]{1,200}$/).nullable().optional().describe("parent requirement (same network, no cycles, ≤ 5 levels); null detaches"),
|
|
5867
5869
|
};
|
|
@@ -5869,14 +5871,14 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
5869
5871
|
const REQ_WRITE_KEYS = [...Object.keys(reqFields), "external_ref", "archived"];
|
|
5870
5872
|
// #475 —— status 是 column 的别名(列表的筛选参数就叫 status,Agent 写任务时也常这么写)。只在 MCP 这层换名,REST 不变;
|
|
5871
5873
|
// 两个都给且不同 → 400 status_conflicts_with_column,不替调用方挑一个。
|
|
5872
|
-
const reqStatus = z.enum(["pool", "doing", "done"]).optional().describe("alias of column
|
|
5874
|
+
const reqStatus = z.enum(["pool", "doing", "done"]).optional().describe("alias of column; must match it if both are sent");
|
|
5873
5875
|
const withColumn = (args: Record<string, unknown>, call: (args: Record<string, unknown>) => Promise<{ content: { type: "text"; text: string }[] }>) => {
|
|
5874
5876
|
if (args.status === undefined) return call(args);
|
|
5875
5877
|
if (args.column !== undefined && args.column !== args.status) return Promise.resolve({ content: [{ type: "text" as const, text: JSON.stringify({ ...errorBody("status_conflicts_with_column"), status: 400 }) }] });
|
|
5876
5878
|
return call({ ...args, column: args.status });
|
|
5877
5879
|
};
|
|
5878
|
-
const reqPeopleDelta = (verb: string) => z.array(reqPerson).max(100).optional().describe(`${verb
|
|
5879
|
-
const REQ_ID_DESC = "requirement id (req_…) or
|
|
5880
|
+
const reqPeopleDelta = (verb: string) => z.array(reqPerson).max(100).optional().describe(`${verb === "add" ? "Add to" : "Remove from"} the participant list, keeping the rest (atomic; not with participants).`);
|
|
5881
|
+
const REQ_ID_DESC = "requirement id (req_…) or short number \"#N\" (per network; with several networks also pass network_id)";
|
|
5880
5882
|
|
|
5881
5883
|
// #471 —— MCP 的列表默认省流:summary 视图 + 50 张一页(REST 的默认 full + 500 不变,App 靠它)。
|
|
5882
5884
|
// 生产实测 122 张卡:旧默认(full、500)148 KB,summary 81 KB —— 一次调用就塞满 Agent 的上下文。
|
|
@@ -5894,6 +5896,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
5894
5896
|
external_ref: z.string().max(200).optional(),
|
|
5895
5897
|
include_archived: z.boolean().optional(),
|
|
5896
5898
|
parent_id: z.string().max(200).optional().describe("children of this requirement ('none' = top level)"),
|
|
5899
|
+
department_id: z.string().max(64).optional().describe("owner (or its Agent's owner) in this department or below"),
|
|
5897
5900
|
top_level: z.boolean().optional(), q: z.string().max(200).optional(),
|
|
5898
5901
|
limit: z.number().int().min(1).max(1000).optional().describe(`page size, default ${REQ_LIST_MCP_DEFAULT_LIMIT}`),
|
|
5899
5902
|
cursor: z.string().max(600).optional().describe("next_cursor from the previous page"),
|
|
@@ -5904,7 +5907,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
5904
5907
|
server.registerTool(
|
|
5905
5908
|
"requirements_list",
|
|
5906
5909
|
{
|
|
5907
|
-
description: `List requirement tasks in your network. Defaults (MCP): view='summary'
|
|
5910
|
+
description: `List requirement tasks in your network, newest first. Defaults (MCP): view='summary' (no description / checklist bodies; has_description, checklist_count instead; ~0.5 KB per task) and ${REQ_LIST_MCP_DEFAULT_LIMIT} per page; has_more + next_cursor → pass cursor. Full text: requirements_get, or view='full' with a small limit (≤ 1000). Filters: seq (#N), status, project_id ('none'), owner / agent_owner ('user:<id>' / 'node:<id>' / 'none'), tag (exact), updated_since (ISO), external_ref, parent_id / top_level, department_id (owner in it or below), include_archived; q searches title / description / people / project / tags (terms ANDed). Rows carry children {total, done}. changes=true (with updated_since) returns only tasks changed since then, archived included, plus deleted ids and server_time for the next updated_since. Unknown parameters are rejected (-32602).`,
|
|
5908
5911
|
// Strict: registerTool honours a constructed object's unknownKeys (server.tool() would strip them silently).
|
|
5909
5912
|
inputSchema: z.strictObject(reqListShape, {
|
|
5910
5913
|
error: (iss: any) => iss.code === "unrecognized_keys"
|
|
@@ -5914,7 +5917,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
5914
5917
|
},
|
|
5915
5918
|
async (args: any) => {
|
|
5916
5919
|
const q = new URLSearchParams();
|
|
5917
|
-
for (const k of ["seq", "status", "project_id", "owner", "agent_owner", "tag", "updated_since", "external_ref", "parent_id", "q", "limit", "cursor", "view"] as const) if (args[k] !== undefined) q.set(k, String(args[k]));
|
|
5920
|
+
for (const k of ["seq", "status", "project_id", "owner", "agent_owner", "tag", "updated_since", "external_ref", "parent_id", "department_id", "q", "limit", "cursor", "view"] as const) if (args[k] !== undefined) q.set(k, String(args[k]));
|
|
5918
5921
|
if (!q.has("view")) q.set("view", "summary");
|
|
5919
5922
|
if (!q.has("limit")) q.set("limit", String(REQ_LIST_MCP_DEFAULT_LIMIT));
|
|
5920
5923
|
if (args.include_archived) q.set("include_archived", "1");
|
|
@@ -5935,7 +5938,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
5935
5938
|
server.registerTool(
|
|
5936
5939
|
"requirements_people",
|
|
5937
5940
|
{
|
|
5938
|
-
description: "Look up the people in your network and the Agents they own, to fill task person fields. Each row: user_id, username, display_name (\"\" if not set), role, department {id, name} or null, agents [{node_id, alias}]; Agents with no owner in this network are listed in agents_without_owner (first page only). q filters by username / display name / department / Agent alias (case-insensitive substring). 50 people per page (limit ≤ 200); has_more + next_offset → pass offset. Agents you are not granted are left out.
|
|
5941
|
+
description: "Look up the people in your network and the Agents they own, to fill task person fields. Each row: user_id, username, display_name (\"\" if not set), role, department {id, name} or null, agents [{node_id, alias}]; Agents with no owner in this network are listed in agents_without_owner (first page only). q filters by username / display name / department / Agent alias (case-insensitive substring). 50 people per page (limit ≤ 200); has_more + next_offset → pass offset. Agents you are not granted are left out. Task person fields take these ids, or {kind:'user', username} / {kind:'node', alias}.",
|
|
5939
5942
|
inputSchema: z.strictObject(reqPeopleShape, {
|
|
5940
5943
|
error: (iss: any) => iss.code === "unrecognized_keys"
|
|
5941
5944
|
? `unknown parameter(s): ${(iss.keys ?? []).join(", ")}; valid parameters: ${reqPeopleKeys.join(", ")}`
|
|
@@ -6020,17 +6023,27 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
6020
6023
|
async (args) => requirementsCall("PATCH", `/api/requirements/projects/${encodeURIComponent(args.id)}`, args.network_id, pick(args, ["name", "color", "sort", "archived"])),
|
|
6021
6024
|
);
|
|
6022
6025
|
|
|
6023
|
-
|
|
6026
|
+
// #476 —— 不带 requirement_id 的全网动态默认 50 条(REST 默认 200 不变,App 走 REST);严格参数同 #471。
|
|
6027
|
+
const REQ_EVENTS_NETWORK_DEFAULT_LIMIT = 50;
|
|
6028
|
+
const reqEventsShape = {
|
|
6029
|
+
network_id: z.string().max(200).optional(),
|
|
6030
|
+
requirement_id: z.string().min(1).max(200).optional().describe(REQ_ID_DESC),
|
|
6031
|
+
since: z.string().max(40).optional().describe("ISO; only events after it"),
|
|
6032
|
+
limit: z.number().int().min(1).max(500).optional(),
|
|
6033
|
+
cursor: z.string().max(20).optional().describe("next_cursor from the previous page (older events)"),
|
|
6034
|
+
};
|
|
6035
|
+
const reqEventsKeys = Object.keys(reqEventsShape);
|
|
6036
|
+
server.registerTool(
|
|
6024
6037
|
"requirements_events",
|
|
6025
|
-
"Read the task activity timeline (the app's 动态): field-level changes — who, when, old → new — and comments (kind=comment, text in new.text; add one with requirements_comment), newest first. Pass requirement_id (req_… or \"#N\") for one task, or omit it for the whole network. since (ISO) = only changes after it; limit 1–500 (default 200); next_cursor → pass as cursor for the next page; server_time → use as the next since. Same visibility as requirements_list: tasks you can't see (and hidden Agents) are left out.",
|
|
6026
6038
|
{
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6039
|
+
description: `Read the task activity timeline (the app's 动态), newest first: field-level changes (who, when, old → new) and comments (kind=comment, text in new.text; add one with requirements_comment). With requirement_id (req_… or "#N"): that task's events (limit default 200). Without it: the whole network, ${REQ_EVENTS_NETWORK_DEFAULT_LIMIT} per page by default. limit ≤ 500; next_cursor → pass as cursor for older events; server_time → use as the next since. Same visibility as requirements_list. Unknown parameters are rejected (-32602).`,
|
|
6040
|
+
inputSchema: z.strictObject(reqEventsShape, {
|
|
6041
|
+
error: (iss: any) => iss.code === "unrecognized_keys"
|
|
6042
|
+
? `unknown parameter(s): ${(iss.keys ?? []).join(", ")}; valid parameters: ${reqEventsKeys.join(", ")}`
|
|
6043
|
+
: undefined,
|
|
6044
|
+
}) as any,
|
|
6032
6045
|
},
|
|
6033
|
-
async (args) => {
|
|
6046
|
+
async (args: any) => {
|
|
6034
6047
|
let card = args.requirement_id;
|
|
6035
6048
|
// "#N" → the card's id, resolved exactly like requirements_get (same scope; a card you can't see is a 404 here too).
|
|
6036
6049
|
if (card && /^#\d+$/.test(card)) {
|
|
@@ -6043,6 +6056,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
6043
6056
|
if (card) q.set("requirement_id", card);
|
|
6044
6057
|
if (args.since) q.set("since", args.since);
|
|
6045
6058
|
if (args.limit !== undefined) q.set("limit", String(args.limit));
|
|
6059
|
+
else if (!card) q.set("limit", String(REQ_EVENTS_NETWORK_DEFAULT_LIMIT));
|
|
6046
6060
|
if (args.cursor) q.set("cursor", args.cursor);
|
|
6047
6061
|
return requirementsCall("GET", `/api/requirements/events${q.size ? `?${q}` : ""}`, args.network_id);
|
|
6048
6062
|
},
|