@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/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("Target child node ID (canonical, #1281). The value create_node returns. Must be in caller's network."),
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("DEPRECATED alias for node_id (#1281): kept for back-compat with pre-unification clients; prefer node_id."),
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("#460 — dispatch even when the target reports a degraded health layer (user tokens only; ignored for node tokens)"),
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("Optional attachment array; parity with send_task's meta.attachments. Each item: {type:'file', file_id, name?, mime?, size?}. Persisted into tasks.meta_json + inbox.meta_json."),
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("#460 — dispatch even when the target reports a degraded health layer (user tokens only; ignored for node tokens)"),
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("#460 — dispatch even when the target reports a degraded health layer (user tokens only; ignored for node tokens)"),
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("Target by alias instead of node_id — resolves to the node row, or to a session that reported rules_file_capable (claude-code sessions)."),
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("Target by alias instead of node_id — resolves to the node row, or to a session that reported rules_file_capable (claude-code sessions)."),
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("Target by alias instead of node_id — resolves to the node row, or to a session that reported skills_capable."),
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("Target by alias instead of node_id — resolves to the node row, or to a session that reported skills_capable."),
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("Target by alias instead of node_id — resolves to the node row, or to a session that reported files_capable."),
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("Target by alias instead of node_id — resolves to the node row, or to a session that reported files_capable."),
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("Target by alias instead of node_id — resolves to the node row, or to a session that reported logs_capable."),
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
- id: z.string().min(1).max(200).optional().describe("user_id / node_id (from requirements_people)"),
5848
- username: z.string().min(1).max(100).optional().describe("kind 'user' only: the member's username, resolved in the task's network"),
5849
- alias: z.string().min(1).max(200).optional().describe("kind 'node' only: the Agent's alias, resolved in the task's network"),
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} or {kind:'user', username}; a node here is rejected with 400 owner_must_be_human — use agent_owner for nodes"),
5863
- agent_owner: reqPerson.nullable().optional().describe("负责 Agent: {kind:'node', id} or {kind:'node', alias}"),
5864
- participants: z.array(reqPerson).max(100).optional().describe("REPLACES the whole participant list ({kind, id}, {kind:'user', username} or {kind:'node', alias}) (omitted people are removed; [] clears it). To add or remove a few people without touching the others, use participants_add / participants_remove on requirements_update."),
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 (same values); if both are sent they must match");
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} these people ${verb === "add" ? "to" : "from"} the participant list, keeping everyone else (atomic; ${verb === "add" ? "already present = no change" : "not present = no change"}). Cannot be combined with participants.`);
5879
- const REQ_ID_DESC = "requirement id (req_…) or its short number \"#N\" (e.g. \"#42\", per network; a user token that sees several networks must also pass network_id, else 409 ambiguous_seq)";
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' and ${REQ_LIST_MCP_DEFAULT_LIMIT} per page, newest first — about 0.5 KB per task, so a page stays small. has_more + next_cursor → pass cursor for the next page. Summary rows drop description and checklist bodies (has_description, checklist_count instead); for full text read one task with requirements_get, or pass view='full' (several KB per task — keep limit small). Filters: seq (the per-network short number shown as #N), status (pool/doing/done), project_id ('none' = no project), owner / agent_owner ('user:<id>' / 'node:<id>' / 'none'), tag (exact label), updated_since (ISO), external_ref, parent_id / top_level, include_archived; q = search title / description / owner, agent and participant names / project name / tags (space-separated terms are ANDed). limit ≤ 1000. Each task carries children {total, done}. changes=true (needs updated_since) returns only tasks changed since then, archived ones included, plus deleted ids and server_time to pass as the next updated_since. Unknown parameters are rejected (-32602) with the list of valid ones.`,
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. Person fields on requirements_create / requirements_update / requirements_upsert_by_external_ref accept {kind:'user', id} or {kind:'user', username} (owner, participants) and {kind:'node', id} or {kind:'node', alias} (agent_owner, participants), resolved in the task's own network only.",
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
- server.tool(
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
- network_id: z.string().max(200).optional(),
6028
- requirement_id: z.string().min(1).max(200).optional(),
6029
- since: z.string().max(40).optional(),
6030
- limit: z.number().int().min(1).max(500).optional(),
6031
- cursor: z.string().max(20).optional(),
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
  },