@taiga-ai/mcp-server 0.36.0-rc.933 → 0.37.0-dev.940

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.
Files changed (3) hide show
  1. package/README.md +7 -7
  2. package/dist/index.js +215 -375
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -14,7 +14,7 @@ You need an access key, made in Taiga under **Settings > Account > MCP**, where
14
14
 
15
15
  Create the key first, and choose when it expires: in 30 days, 90 days, a year, or never. Taiga copies it to your clipboard, shows it once, and keeps it on the page until you say you have saved it, so copying the setup next does not lose it.
16
16
 
17
- The same page shows the setup for your client and environment, with the key already filled in where a config file needs it, and a one-click install for Cursor. The examples here pin a release (`@taiga-ai/mcp-server@0.36.0`), so an install runs the version you chose rather than whatever `latest` is at the next start.
17
+ The same page shows the setup for your client and environment, with the key already filled in where a config file needs it, and a one-click install for Cursor. The examples here pin a release (`@taiga-ai/mcp-server@0.37.0`), so an install runs the version you chose rather than whatever `latest` is at the next start.
18
18
 
19
19
  ## Install
20
20
 
@@ -23,21 +23,21 @@ The same page shows the setup for your client and environment, with the key alre
23
23
  Run the command. When it asks for your access key, paste it once and press Enter. Nothing appears as you paste (PowerShell shows asterisks), and the key stays out of your shell history:
24
24
 
25
25
  ```sh
26
- printf 'Taiga access key: ' && read -rs TAIGA_API_KEY && echo && claude mcp add taiga --scope user -e TAIGA_API_KEY="$TAIGA_API_KEY" -- npx -y @taiga-ai/mcp-server@0.36.0
26
+ printf 'Taiga access key: ' && read -rs TAIGA_API_KEY && echo && claude mcp add taiga --scope user -e TAIGA_API_KEY="$TAIGA_API_KEY" -- npx -y @taiga-ai/mcp-server@0.37.0
27
27
  ```
28
28
 
29
29
  ```sh
30
- printf 'Taiga access key: ' && read -rs TAIGA_API_KEY && echo && codex mcp add taiga --env TAIGA_API_KEY="$TAIGA_API_KEY" -- npx -y @taiga-ai/mcp-server@0.36.0
30
+ printf 'Taiga access key: ' && read -rs TAIGA_API_KEY && echo && codex mcp add taiga --env TAIGA_API_KEY="$TAIGA_API_KEY" -- npx -y @taiga-ai/mcp-server@0.37.0
31
31
  ```
32
32
 
33
33
  On Windows, in PowerShell (Claude Code needs `cmd /c` to start `npx`; the quoted `'--'` survives npm's PowerShell shims):
34
34
 
35
35
  ```powershell
36
- $k = Read-Host 'Taiga access key' -AsSecureString; claude mcp add taiga --scope user -e "TAIGA_API_KEY=$([Net.NetworkCredential]::new('', $k).Password)" '--' cmd /c npx -y @taiga-ai/mcp-server@0.36.0
36
+ $k = Read-Host 'Taiga access key' -AsSecureString; claude mcp add taiga --scope user -e "TAIGA_API_KEY=$([Net.NetworkCredential]::new('', $k).Password)" '--' cmd /c npx -y @taiga-ai/mcp-server@0.37.0
37
37
  ```
38
38
 
39
39
  ```powershell
40
- $k = Read-Host 'Taiga access key' -AsSecureString; codex mcp add taiga --env "TAIGA_API_KEY=$([Net.NetworkCredential]::new('', $k).Password)" '--' npx -y @taiga-ai/mcp-server@0.36.0
40
+ $k = Read-Host 'Taiga access key' -AsSecureString; codex mcp add taiga --env "TAIGA_API_KEY=$([Net.NetworkCredential]::new('', $k).Password)" '--' npx -y @taiga-ai/mcp-server@0.37.0
41
41
  ```
42
42
 
43
43
  ### Cursor, Windsurf, Claude Desktop and other MCP clients
@@ -49,7 +49,7 @@ Most clients read the same `mcpServers` block. Add it with your key in place of
49
49
  "mcpServers": {
50
50
  "taiga": {
51
51
  "command": "npx",
52
- "args": ["-y", "@taiga-ai/mcp-server@0.36.0"],
52
+ "args": ["-y", "@taiga-ai/mcp-server@0.37.0"],
53
53
  "env": { "TAIGA_API_KEY": "YOUR_ACCESS_KEY" }
54
54
  }
55
55
  }
@@ -78,7 +78,7 @@ In `.vscode/mcp.json`. VS Code reads `servers`, and prompts for the key once and
78
78
  "taiga": {
79
79
  "type": "stdio",
80
80
  "command": "npx",
81
- "args": ["-y", "@taiga-ai/mcp-server@0.36.0"],
81
+ "args": ["-y", "@taiga-ai/mcp-server@0.37.0"],
82
82
  "env": { "TAIGA_API_KEY": "${input:taiga-api-key}" }
83
83
  }
84
84
  }
package/dist/index.js CHANGED
@@ -23969,22 +23969,20 @@ var TaigaClient = class {
23969
23969
  async remediatePolicyViolation(productId, divergenceId, body = {}) {
23970
23970
  return this.request("POST", `/products/${seg(productId)}/maintenance/policies/divergences/${seg(divergenceId)}/remediate`, body);
23971
23971
  }
23972
- async listInbox(opts) {
23973
- const { productId, ...rest } = opts;
23974
- const path = productId ? `/products/${seg(productId)}/inbox` : "/inbox";
23975
- return this.request("GET", `${path}${query$1({ ...rest })}`);
23976
- }
23977
23972
  /**
23978
- * The requests waiting on the caller (GET /users/me/requests), the read the
23979
- * web's Action required page merges with the inbox: a licence nobody
23980
- * approved (license_review, PRO-3313) is one, listed only for a caller who
23981
- * can change the list.
23973
+ * What waits on a person (GET /users/me/requests), up to the API's cap, with
23974
+ * what is assigned to someone else: a key speaks for the team's work.
23982
23975
  */
23983
- async listMyRequests() {
23984
- return this.request("GET", "/users/me/requests");
23976
+ async listRequests() {
23977
+ return this.request("GET", `/users/me/requests${query$1({ everyone: "true" })}`);
23978
+ }
23979
+ /** What closed in the last two weeks (GET /users/me/requests/done), by the same rules. */
23980
+ async listDoneRequests() {
23981
+ return this.request("GET", `/users/me/requests/done${query$1({ everyone: "true" })}`);
23985
23982
  }
23986
- async updateInboxItem(id, body) {
23987
- return this.request("PATCH", `/inbox/${seg(id)}`, body);
23983
+ /** Answer a request with one of the answers it offers. */
23984
+ async answerRequest(id, exit) {
23985
+ return this.request("POST", `/requests/${seg(id)}/answer`, { exit });
23988
23986
  }
23989
23987
  async listSetupTasks(opts) {
23990
23988
  const { productId, ...rest } = opts;
@@ -25370,23 +25368,6 @@ function hardeningCoverage(result) {
25370
25368
  }
25371
25369
  //#endregion
25372
25370
  //#region ../../packages/types/dist/inbox.js
25373
- /** What action is required. Drives icon, copy, and completion logic. */
25374
- const INBOX_ITEM_KINDS = [
25375
- "review_plan",
25376
- "review_backlog",
25377
- "review_doc",
25378
- "review_pr",
25379
- "review_maintenance",
25380
- "review_policy",
25381
- "attest_policy",
25382
- "setup_env_var",
25383
- "setup_secret",
25384
- "setup_third_party",
25385
- "setup_dns",
25386
- "setup_billing",
25387
- "setup_cloud_permission",
25388
- "setup_manual"
25389
- ];
25390
25371
  /** The kind of manual setup the agents identified. */
25391
25372
  const INBOX_TASK_CATEGORIES = [
25392
25373
  "env_var",
@@ -26236,11 +26217,72 @@ Object.freeze({
26236
26217
  gracePeriodDays: 0,
26237
26218
  requiredSince: null
26238
26219
  });
26220
+ //#endregion
26221
+ //#region ../../packages/types/dist/notifications.js
26222
+ /**
26223
+ * What the contact is FOR, in the AWS alternate-contacts sense.
26224
+ *
26225
+ * Only `default` is populated today. The column exists from day one anyway,
26226
+ * because retrofitting a type onto rows already in the wild means guessing what
26227
+ * each one was for. `security`, `operations` and `billing` become meaningful
26228
+ * once there are event classes to route to them.
26229
+ */
26230
+ const NOTIFICATION_CONTACT_TYPES = [
26231
+ "default",
26232
+ "security",
26233
+ "operations",
26234
+ "billing"
26235
+ ];
26239
26236
  [...TURN_EVENT_TRIGGERS];
26240
26237
  /** Longest request for changes from Taiga (PRO-3035): about four pages of prose. */
26241
26238
  const CHANGE_REQUEST_MAX_CHARS = 1e4;
26242
26239
  //#endregion
26243
26240
  //#region ../../packages/types/dist/requests.js
26241
+ /**
26242
+ * What a request asks for. Mirrored by a database CHECK on `requests.kind`.
26243
+ *
26244
+ * - `stop` the line stopped on an initiative and waits on a person
26245
+ * to choose a way on (PRO-2616): the needs-human flag, as a
26246
+ * request.
26247
+ * - `plan_review` a plan landed with the build gate closed and waits on a
26248
+ * person's verdict before anything is built (PRO-2617).
26249
+ * - `pr_review` a build's pull request waits on a person to review and
26250
+ * merge it, auto-merge not owning it (PRO-2655).
26251
+ * - `setup` a manual setup task an agent identified (a variable, a
26252
+ * secret, a DNS record...) waits on a person (PRO-2700).
26253
+ * Advisory: nothing waits on it.
26254
+ * - `document_review` a published document version waits on a person's
26255
+ * sign-off (PRO-2701). Advisory: nothing waits on it.
26256
+ * - `maintenance_review` a product's maintenance findings wait on a
26257
+ * person's review (PRO-2702): one per product, raised when
26258
+ * a sweep brings news. Advisory: nothing waits on it.
26259
+ * - `policy_review` a product's policy divergences wait on a person's
26260
+ * review (PRO-2702): one per product, raised when an
26261
+ * assessment after a refresh finds new ones. Advisory.
26262
+ * - `document_check` something the document pipeline found in one of a
26263
+ * product's documents waited on a person (PRO-2703): a
26264
+ * reference or fact it could not fix, or a product decision
26265
+ * only a person can make. One per check, under its key
26266
+ * (`<documentType>:doc-lint`, `:facts`,
26267
+ * `:decision:<hash>`), on the product. Retired: no longer
26268
+ * raised, and none is open -- a document asks for a review
26269
+ * and nothing else. Its closed rows remain.
26270
+ * - `license_review` a licence nobody approved is in use in a product
26271
+ * (PRO-3313): one per product and licence, under the
26272
+ * licence's SPDX id as its key, for whoever can change the
26273
+ * organization's approved list. Advisory.
26274
+ */
26275
+ const REQUEST_KINDS = [
26276
+ "stop",
26277
+ "plan_review",
26278
+ "pr_review",
26279
+ "setup",
26280
+ "document_review",
26281
+ "maintenance_review",
26282
+ "policy_review",
26283
+ "document_check",
26284
+ "license_review"
26285
+ ];
26244
26286
  /** Every way a request can be answered, whatever its kind. */
26245
26287
  const REQUEST_EXITS = [
26246
26288
  "resume_run",
@@ -27194,6 +27236,14 @@ function isSharedHostingDomain(host) {
27194
27236
  return SHARED_HOSTING_DOMAINS.includes(host);
27195
27237
  }
27196
27238
  strictObject({});
27239
+ strictObject({
27240
+ address: string$2().trim().toLowerCase().email("Enter a valid email address").max(320),
27241
+ contactType: _enum(NOTIFICATION_CONTACT_TYPES).optional()
27242
+ });
27243
+ object({
27244
+ verificationSent: boolean(),
27245
+ verificationError: string$2().nullable()
27246
+ });
27197
27247
  const uuid = string$2().uuid();
27198
27248
  const sha256 = string$2().regex(/^[0-9a-f]{64}$/, "sha256 must be 64 lowercase hex characters");
27199
27249
  object({ jobId: string$2() });
@@ -27839,7 +27889,7 @@ Object.freeze({
27839
27889
  * reads as deleted) and fields they explain would simply be missing. So it
27840
27890
  * refuses to start, and says what to do.
27841
27891
  */
27842
- function apiLevelProblem(introspection, apiUrl, required = 8, version = SERVER_VERSION) {
27892
+ function apiLevelProblem(introspection, apiUrl, required = 9, version = SERVER_VERSION) {
27843
27893
  const offered = introspection.mcpApiLevel ?? 0;
27844
27894
  if (offered >= required) return null;
27845
27895
  return `The Taiga API at ${apiUrl} is older than @taiga-ai/mcp-server ${version} needs (API level ${offered}, needs ${required}). Run the version published for it, or wait for that API to be updated; an earlier release of this package works against it.`;
@@ -28406,7 +28456,7 @@ function inProductWords(value) {
28406
28456
  }
28407
28457
  /** The API's 404 sentence in the product's words: "Backlog item not found" is an initiative not found. */
28408
28458
  function inProductWording(said) {
28409
- return said.replace(/^Backlog item not found/i, "Initiative not found").replace(/^Build run step not found/i, "Run step not found").replace(/^Build run not found/i, "Run not found").replace(/^Custom document not found/i, "Knowledge file not found").replace(/^(Branch "[^"]*" does not exist in \S+?)\.?$/i, "$1. taiga_list_repository_branches lists the branches it has.").replace(/^This product already has a repository\. Change it instead of linking another\./i, "This product already has a repository. Pass replacesLinkId to swap it in one step, or taiga_unlink_repository first (taiga_list_product_repositories gives its ID).");
28459
+ return said.replace(/^Backlog item not found/i, "Initiative not found").replace(/^Build run step not found/i, "Run step not found").replace(/^Build run not found/i, "Run not found").replace(/^Custom document not found/i, "Knowledge file not found").replace(/^(Branch "[^"]*" does not exist in \S+?)\.?$/i, "$1. taiga_list_repository_branches lists the branches it has.");
28410
28460
  }
28411
28461
  /**
28412
28462
  * A float without its binary noise: six significant digits. Delivery metrics
@@ -36733,7 +36783,7 @@ const PLANNER_REASON_MAX = 600;
36733
36783
  /** Words a sentence does not end at: "e.g." and the like, and a lone initial. */
36734
36784
  const ABBREVIATION = /(?:\b(?:e\.g|i\.e|etc|vs|cf|incl|approx)|(?:^|\s)[A-Za-z])\.$/i;
36735
36785
  /** A finding's first sentence, capped: it ends at . ! or ? before white space, but not at an abbreviation. */
36736
- function firstSentence$1(text) {
36786
+ function firstSentence(text) {
36737
36787
  let sentence = text;
36738
36788
  for (const match of text.matchAll(/[.!?](?=\s|$)/g)) {
36739
36789
  const end = match.index + 1;
@@ -36751,7 +36801,7 @@ function hardeningSaid(blocking, named) {
36751
36801
  const count = `Hardening ended with ${blocking} blocking finding${blocking === 1 ? "" : "s"} it could not fix`;
36752
36802
  if (named.length === 0) return `${count}; this run predates recording which.`;
36753
36803
  const shown = named.slice(0, 3).map((finding) => {
36754
- const message = firstSentence$1(String(finding["message"]));
36804
+ const message = firstSentence(String(finding["message"]));
36755
36805
  const where = finding["line"] ? `${String(finding["path"])}:${String(finding["line"])}` : String(finding["path"]);
36756
36806
  return `${message} (${String(finding["tool"])}, ${where})`;
36757
36807
  });
@@ -38630,294 +38680,122 @@ function registerDesignTokenTools(server, client, wait) {
38630
38680
  }
38631
38681
  //#endregion
38632
38682
  //#region src/tools/inbox.ts
38633
- /**
38634
- * An item as a row: what it is, whose product, how long it has waited. Not
38635
- * its metadata by default: a document decision carried its whole text there,
38636
- * and fifty rows came to 57 KB (PRO-2800); full: true brings it.
38637
- */
38638
- const ITEM_FIELDS = [
38639
- "id",
38640
- "productId",
38641
- "productName",
38642
- "category",
38643
- "kind",
38644
- "subjectType",
38645
- "subjectId",
38646
- "title",
38647
- "summary",
38648
- "status",
38649
- "blocking",
38650
- "snoozedUntil",
38651
- "createdAt",
38652
- "resolvedAt"
38653
- ];
38654
38683
  const DAY_MS = 864e5;
38655
- /** When an item stopped waiting: its resolution, or now while it is open. */
38656
- function waitedUntil(resolvedAt, now) {
38657
- const at = typeof resolvedAt === "string" ? Date.parse(resolvedAt) : NaN;
38658
- return Number.isNaN(at) ? now : at;
38659
- }
38660
- /**
38661
- * The read that opens an item's subject (PRO-2800): rows carried subjectType and subjectId, and the reviewing agent
38662
- * had to infer which tool takes them.
38663
- */
38684
+ /** The kinds a row can be: a document check is retired, and listed no more. */
38685
+ const LISTED_KINDS = REQUEST_KINDS.filter((kind) => kind !== "document_check");
38686
+ /** The read that opens a request's subject (PRO-2800): what is on an initiative needs backlog:read. */
38664
38687
  function readWith(row, backlogRead) {
38665
- switch (row["subjectType"]) {
38666
- case "build_run": return backlogRead ? "taiga_get_run (runId: subjectId)" : void 0;
38667
- case "backlog_item": return backlogRead ? "taiga_get_plan (initiativeId: subjectId)" : void 0;
38668
- case "product_document": {
38669
- const type = /^[^:]+:([a-z_]+):decision:/.exec(String(row["subjectId"]))?.[1];
38670
- return type ? `taiga_get_product_document (productId, type: ${type}); the decision's text is this row with full: true` : "taiga_get_product_document (productId, no type)";
38671
- }
38672
- case "inbox_task": return "taiga_list_setup_tasks (productId)";
38673
- case "product": return {
38674
- review_backlog: backlogRead ? "taiga_list_initiatives (productId)" : void 0,
38675
- review_maintenance: "taiga_list_findings (productId)",
38676
- review_policy: "taiga_get_policy_standing (productId)",
38677
- review_doc: "taiga_get_product_document (productId, no type)"
38678
- }[String(row["kind"])];
38688
+ const product = row["product"] ?? {};
38689
+ const productRead = `productId: ${String(product["id"])}`;
38690
+ switch (row["kind"]) {
38691
+ case "stop": return backlogRead ? `taiga_get_initiative (initiativeId: ${String(subjectIdOf(row))})` : void 0;
38692
+ case "plan_review": return backlogRead ? `taiga_get_plan (initiativeId: ${String(subjectIdOf(row))})` : void 0;
38693
+ case "pr_review": return backlogRead ? `taiga_get_run (runId: ${String(dataOf(row)["buildRunId"])})` : void 0;
38694
+ case "setup": return `taiga_list_setup_tasks (${productRead})`;
38695
+ case "document_review": return `taiga_get_product_document (${productRead}, type: ${String(dataOf(row)["documentType"])})`;
38696
+ case "maintenance_review": return `taiga_list_findings (${productRead})`;
38697
+ case "policy_review": return `taiga_get_policy_standing (${productRead})`;
38679
38698
  default: return;
38680
38699
  }
38681
38700
  }
38682
- /**
38683
- * The glossary's words for what the API names after its tables (PRO-2823): a reviewer met kind review_backlog,
38684
- * subjectType backlog_item and inbox_task beside a glossary that says initiative and run, and had to infer they
38685
- * were the same things.
38686
- */
38687
- const KIND_SAID = { review_backlog: "review_initiatives" };
38688
- /** The kinds as the rows say them, for the kind filter, and the way back to the API's word. */
38689
- const KINDS_SAID = INBOX_ITEM_KINDS.map((kind) => KIND_SAID[kind] ?? kind);
38690
- const KIND_FROM_SAID = Object.fromEntries(Object.entries(KIND_SAID).map(([api, said]) => [said, api]));
38691
- const SUBJECT_SAID = {
38692
- backlog_item: "initiative",
38693
- build_run: "run",
38694
- inbox_task: "setup_task"
38695
- };
38696
- const kindSaid = (kind) => KIND_SAID[String(kind)] ?? kind;
38697
- /** A setup task row in the glossary's words: the Action Required item it is, by the name taiga_resolve_action takes. */
38698
- function setupTaskRow(row) {
38699
- const { inboxItemId, inboxItemStatus, alsoFiledAs, ...rest } = row;
38700
- return {
38701
- ...rest,
38702
- ...inboxItemId !== void 0 ? { actionId: inboxItemId } : {},
38703
- ...inboxItemStatus !== void 0 ? { actionStatus: inboxItemStatus } : {},
38704
- ...Array.isArray(alsoFiledAs) ? { alsoFiledAs: alsoFiledAs.map(setupTaskRow) } : {}
38705
- };
38706
- }
38707
- /**
38708
- * The licences nobody approved, in use in a product (PRO-3313, PRO-3396): requests rather than inbox items, so
38709
- * the inbox read never listed them, though the web's Action required page does. Read for a key that can change
38710
- * the list (the API lists them to nobody else), on the first page, and best effort: a failed read leaves them
38711
- * out, not the page.
38712
- */
38713
- async function licenseReviews(client, productId, now) {
38714
- if (!client.hasCapability("tenant:settings")) return null;
38715
- try {
38716
- const result = await client.listMyRequests();
38717
- return {
38718
- rows: result.data.filter((r) => r["kind"] === "license_review").map((r) => {
38719
- const product = r["product"] ?? {};
38720
- const data = r["data"] ?? {};
38721
- const created = typeof r["createdAt"] === "string" ? Date.parse(r["createdAt"]) : NaN;
38722
- return {
38723
- id: r["id"],
38724
- productId: product["id"],
38725
- productName: product["name"],
38726
- kind: "review_license",
38727
- license: data["license"],
38728
- packages: data["packages"],
38729
- ...Number.isNaN(created) ? {} : { waitingDays: Math.floor((now - created) / DAY_MS) },
38730
- readWith: "taiga_list_licenses (license)",
38731
- decideWith: "taiga_change_license (license, action: 'approve') for every product" + (client.hasCapability("products:write") ? ", or taiga_allow_license (productId, license) for this one" : "") + "; dismissing it is only in the web"
38732
- };
38733
- }).filter((r) => productId === void 0 || r.productId === productId),
38734
- truncated: result.truncated === true
38735
- };
38736
- } catch (err) {
38737
- if (err instanceof TaigaApiError || isNetworkFailure(err)) return null;
38738
- throw err;
38739
- }
38740
- }
38741
- /** A summary past its first sentence: review_doc rows quoted two documents, 800 to 1,200 characters each. */
38742
- const SUMMARY_LIMIT = 200;
38743
- function firstSentence(text) {
38744
- if (typeof text !== "string") return text;
38745
- const head = text.split(/\n/)[0].trim();
38746
- if (head.length <= SUMMARY_LIMIT) return head.length < text.trim().length ? `${head} (full: true has the rest)` : head;
38747
- const cut = head.slice(0, SUMMARY_LIMIT);
38748
- const end = Math.max(cut.lastIndexOf(". "), cut.lastIndexOf("; "));
38749
- return `${end > 40 ? cut.slice(0, end + 1) : cut}... (full: true has the rest)`;
38750
- }
38751
- /** A row as an agent reads it: the pull request to open when there is one, and how many days it has waited. */
38752
- function itemRow(row, now, full, backlogRead) {
38753
- const { snoozedUntil, resolvedAt, ...rest } = pickFields([row], ITEM_FIELDS)[0];
38754
- const metadata = row["metadata"] ?? {};
38701
+ const dataOf = (row) => row["data"] ?? {};
38702
+ const subjectIdOf = (row) => (row["subject"] ?? {})["id"];
38703
+ /** What a request asks, in a line: a request holds no title of its own. */
38704
+ function titleOf(row) {
38705
+ const data = dataOf(row);
38706
+ const initiative = row["initiative"] ?? {};
38707
+ const named = typeof initiative["title"] === "string" ? initiative["title"] : "an initiative";
38708
+ switch (row["kind"]) {
38709
+ case "stop": return `Stopped: ${named}`;
38710
+ case "plan_review": return `Review plan: ${named}`;
38711
+ case "pr_review": return `Review pull request #${String(data["prNumber"])}: ${named}`;
38712
+ case "setup": return String(data["title"]);
38713
+ case "document_review": return `Review ${String(data["documentType"])}, version ${String(data["version"])}`;
38714
+ case "maintenance_review": return `Review ${String(data["openFindings"])} findings`;
38715
+ case "policy_review": return `Review ${String(data["openDivergences"])} policy violations`;
38716
+ case "license_review": return `Review license ${String(data["license"])}`;
38717
+ default: return String(row["kind"]);
38718
+ }
38719
+ }
38720
+ /** A request as an agent reads it: what it asks, how long it has waited, and which read opens it. */
38721
+ function requestRow(row, now, backlogRead, full) {
38722
+ const product = row["product"] ?? {};
38723
+ const pullRequest = row["pullRequest"];
38724
+ const closed = row["closed"];
38755
38725
  const created = typeof row["createdAt"] === "string" ? Date.parse(row["createdAt"]) : NaN;
38756
- const { summary, status, blocking, ...lean } = rest;
38726
+ const until = typeof closed?.["closedAt"] === "string" ? Date.parse(closed["closedAt"]) : now;
38757
38727
  const read = readWith(row, backlogRead);
38758
- const saysNothing = row["kind"] === "setup_manual" && String(summary).trim().toLowerCase() === "manual config";
38759
38728
  return {
38760
- ...lean,
38761
- kind: kindSaid(lean["kind"]),
38762
- ...typeof lean["subjectType"] === "string" ? { subjectType: SUBJECT_SAID[lean["subjectType"]] ?? lean["subjectType"] } : {},
38763
- ...saysNothing ? {} : full ? { summary } : row["kind"] === "review_pr" ? {} : { summary: firstSentence(summary) },
38764
- ...full || status !== "open" ? { status } : {},
38765
- ...full || blocking === true ? { blocking } : {},
38729
+ id: row["id"],
38730
+ productId: product["id"],
38731
+ productName: product["name"],
38732
+ kind: row["kind"],
38733
+ title: titleOf(row),
38734
+ ...row["blocking"] === true ? { blocking: true } : {},
38735
+ ...row["kind"] === "stop" ? {
38736
+ cause: dataOf(row)["cause"],
38737
+ detail: dataOf(row)["detail"]
38738
+ } : {},
38766
38739
  ...read ? { readWith: read } : {},
38767
- ...snoozedUntil ? { snoozedUntil } : {},
38768
- ...resolvedAt ? { resolvedAt } : {},
38769
- ...typeof metadata["prUrl"] === "string" ? { prUrl: metadata["prUrl"] } : {},
38770
- ...Number.isNaN(created) ? {} : { waitingDays: Math.floor((waitedUntil(resolvedAt, now) - created) / DAY_MS) },
38771
- ...full ? { metadata: row["metadata"] } : {}
38740
+ ...typeof pullRequest?.["url"] === "string" ? { prUrl: pullRequest["url"] } : {},
38741
+ ...closed ? { closed } : {},
38742
+ ...Number.isNaN(created) ? {} : { waitingDays: Math.floor((until - created) / DAY_MS) },
38743
+ ...full ? {
38744
+ data: row["data"],
38745
+ answers: row["exits"]
38746
+ } : {}
38772
38747
  };
38773
38748
  }
38774
- /**
38775
- * What is open, counted by product and kind: the answer to "what needs attention" without reading every row. An item
38776
- * that no longer stands is counted under stale, not open or byKind, so nobody subtracts by hand (PRO-2800).
38777
- */
38778
- function summaryOf(rows, stale) {
38749
+ /** The licence reviews, read apart: what decides one is another tool's, for every product or one. */
38750
+ function licenseRow(client, row, now) {
38751
+ const product = row["product"] ?? {};
38752
+ const data = dataOf(row);
38753
+ const created = typeof row["createdAt"] === "string" ? Date.parse(row["createdAt"]) : NaN;
38754
+ return {
38755
+ id: row["id"],
38756
+ productId: product["id"],
38757
+ productName: product["name"],
38758
+ kind: "license_review",
38759
+ license: data["license"],
38760
+ packages: data["packages"],
38761
+ ...Number.isNaN(created) ? {} : { waitingDays: Math.floor((now - created) / DAY_MS) },
38762
+ readWith: "taiga_list_licenses (license)",
38763
+ decideWith: "taiga_change_license (license, action: 'approve') for every product" + (client.hasCapability("products:write") ? ", taiga_allow_license (productId, license, allowed: true) for this one, or taiga_resolve_action (actionId: id, answer: 'dismiss')" : "")
38764
+ };
38765
+ }
38766
+ /** What is open, counted by product and kind: the answer to "what needs attention" without reading every row. */
38767
+ function summaryOf(rows) {
38779
38768
  const byProduct = /* @__PURE__ */ new Map();
38780
38769
  for (const row of rows) {
38781
- const key = String(row["productId"]);
38782
- const entry = byProduct.get(key) ?? (row["productName"] ? {
38783
- productId: key,
38784
- product: row["productName"],
38785
- open: 0,
38786
- byKind: {}
38787
- } : {
38770
+ const product = row["product"] ?? {};
38771
+ const key = String(product["id"]);
38772
+ const entry = byProduct.get(key) ?? {
38788
38773
  productId: key,
38774
+ product: product["name"],
38789
38775
  open: 0,
38790
38776
  byKind: {}
38791
- });
38792
- if (stale.has(String(row["id"]))) entry.stale = (entry.stale ?? 0) + 1;
38793
- else {
38794
- entry.open += 1;
38795
- const kind = String(kindSaid(row["kind"]));
38796
- entry.byKind[kind] = (entry.byKind[kind] ?? 0) + 1;
38797
- }
38777
+ };
38778
+ entry.open += 1;
38779
+ const kind = String(row["kind"]);
38780
+ entry.byKind[kind] = (entry.byKind[kind] ?? 0) + 1;
38798
38781
  byProduct.set(key, entry);
38799
38782
  }
38800
38783
  return [...byProduct.values()].sort((a, b) => b.open - a.open);
38801
38784
  }
38802
- /** Pull request reviews checked against their run and initiative; past this a row is left as it is. */
38803
- const STALE_CHECKS = 10;
38804
- /**
38805
- * Which "Review & merge" items no longer stand, and why (PRO-2800): the run's
38806
- * pull request closed or merged, or its initiative was canceled or finished
38807
- * without it. Two reviewing agents counted such leftovers as live gates; the
38808
- * row still is open in Action Required, so this says what it is now rather
38809
- * than hiding it. Best effort, a handful of runs: a failed read leaves the
38810
- * row as it was.
38811
- */
38812
- async function staleReviews(client, rows) {
38813
- const byId = new Map(rows.filter((row) => row["kind"] === "review_pr" && row["subjectType"] === "build_run").map((row) => [String(row["id"]), row]));
38814
- const candidates = [...byId.values()].slice(0, STALE_CHECKS);
38815
- const uncheckedIds = new Set([...byId.keys()].slice(STALE_CHECKS));
38816
- const out = /* @__PURE__ */ new Map();
38817
- await Promise.all(candidates.map(async (row) => {
38818
- if (!client.hasCapability("backlog:read")) return void uncheckedIds.add(String(row["id"]));
38819
- try {
38820
- const run = (await client.getBuildRun(String(row["subjectId"]))).data;
38821
- if (run["prMerged"] === true) return void out.set(String(row["id"]), "its pull request has merged");
38822
- if (run["prState"] === "closed") return void out.set(String(row["id"]), "its pull request was closed");
38823
- if (typeof run["backlogItemId"] !== "string") return;
38824
- const item = (await client.getBacklogItem(run["backlogItemId"])).data;
38825
- if (item["state"] === "canceled" || item["status"] === "canceled") out.set(String(row["id"]), "its initiative was canceled; the pull request is still open on the forge");
38826
- else if (item["state"] === "done" || item["status"] === "done") out.set(String(row["id"]), "its initiative is done; this run is not the one that landed it");
38827
- } catch (error) {
38828
- if (!(error instanceof TaigaApiError || isNetworkFailure(error))) throw error;
38829
- uncheckedIds.add(String(row["id"]));
38830
- }
38831
- }));
38832
- await staleBacklogReviews(client, rows, out, uncheckedIds);
38833
- return {
38834
- stale: out,
38835
- unchecked: uncheckedIds.size,
38836
- uncheckedIds
38837
- };
38838
- }
38839
- /**
38840
- * "Review Initiatives" items whose product has no initiative left in its backlog or todo column (PRO-2800): two reviews found one
38841
- * waiting 15 days on a product whose initiatives had all moved on, with nothing an agent could act on. Best effort,
38842
- * a handful of products; returns how many could not be checked.
38843
- */
38844
- async function staleBacklogReviews(client, rows, out, unchecked) {
38845
- const asks = [...new Map(rows.filter((row) => row["kind"] === "review_backlog" && typeof row["productId"] === "string").map((row) => [String(row["id"]), row])).values()];
38846
- const skip = (which) => which.forEach((row) => unchecked.add(String(row["id"])));
38847
- if (asks.length === 0) return;
38848
- if (!client.hasCapability("backlog:read")) return skip(asks);
38849
- const checked = [...new Set(asks.map((row) => String(row["productId"])))].slice(0, STALE_CHECKS);
38850
- skip(asks.filter((row) => !checked.includes(String(row["productId"]))));
38851
- const empty = /* @__PURE__ */ new Set();
38852
- await Promise.all(checked.map(async (productId) => {
38853
- try {
38854
- if ((await Promise.all(["backlog", "todo"].map((state) => client.listBacklogItems(productId, {
38855
- state,
38856
- limit: 1
38857
- })))).some((page) => page.data.length > 0)) return;
38858
- empty.add(productId);
38859
- } catch (error) {
38860
- if (!(error instanceof TaigaApiError || isNetworkFailure(error))) throw error;
38861
- skip(asks.filter((row) => row["productId"] === productId));
38862
- }
38863
- }));
38864
- for (const row of asks) if (empty.has(String(row["productId"]))) out.set(String(row["id"]), "no initiative is in the backlog or todo now: the ones it asked about have moved on");
38865
- }
38866
- /**
38867
- * One product's Action Required as counts, the way the list's summary counts them: open is what still stands and is
38868
- * not put off, stale and snoozed are counted apart. The product context reports these, so the two reads agree.
38869
- */
38870
- async function attentionCounts(client, rows) {
38871
- const now = Date.now();
38872
- const due = rows.filter((row) => isDue(row, now));
38873
- const { stale, unchecked } = await staleReviews(client, due);
38874
- const [entry] = summaryOf(due, stale);
38875
- const snoozed = rows.length - due.length;
38785
+ /** The request's product, when the tool asks about one. */
38786
+ const inProduct = (productIdWanted) => (row) => productIdWanted === void 0 || (row["product"] ?? {})["id"] === productIdWanted;
38787
+ /** One product's Action Required as counts, as the list's summary counts them. */
38788
+ async function attentionCounts(client, productIdWanted) {
38789
+ const result = await client.listRequests();
38790
+ const [entry] = summaryOf(result.data.filter(inProduct(productIdWanted)).filter((row) => row["kind"] !== "license_review"));
38876
38791
  return {
38877
38792
  open: entry ? Number(entry["open"]) : 0,
38878
38793
  byKind: entry ? entry["byKind"] : {},
38879
- ...entry && typeof entry["stale"] === "number" ? { stale: entry["stale"] } : {},
38880
- ...snoozed > 0 ? { snoozed } : {},
38881
- ...unchecked > 0 ? { staleNote: `${unchecked} review${unchecked === 1 ? " was" : "s were"} not checked for staleness and count as open.` } : {}
38794
+ ...result.truncated ? { note: "The read stopped at its cap, so these may undercount." } : {}
38882
38795
  };
38883
38796
  }
38884
- /** An item the team has not put off: the page hides one until its snooze ends, and so does this. */
38885
- function isDue(row, now) {
38886
- return !(row["status"] === "snoozed" && typeof row["snoozedUntil"] === "string" && Date.parse(row["snoozedUntil"]) > now);
38887
- }
38888
- /** Pages read for the summary; past this it says it counted only those. */
38889
- const SUMMARY_PAGE = 100;
38890
38797
  /** Rows an org-wide first page carries when the caller names no limit: the summary answers the rest. */
38891
38798
  const ORG_WIDE_ROWS = 10;
38892
- /** Pages the summary reads before it says it stopped: 1,000 items. */
38893
- const SUMMARY_PAGES = 10;
38894
- /** Every item the summary counts, up to SUMMARY_PAGES pages; nextCursor set when it stopped short. Null on failure. */
38895
- async function everyItem(client, query, orNull = true) {
38896
- const data = [];
38897
- let cursor;
38898
- try {
38899
- for (let index = 0; index < SUMMARY_PAGES; index += 1) {
38900
- const page = await client.listInbox({
38901
- ...query,
38902
- limit: SUMMARY_PAGE,
38903
- cursor
38904
- });
38905
- data.push(...page.data);
38906
- if (!page.nextCursor) return {
38907
- data,
38908
- nextCursor: null
38909
- };
38910
- cursor = page.nextCursor;
38911
- }
38912
- return {
38913
- data,
38914
- nextCursor: cursor ?? null
38915
- };
38916
- } catch (error) {
38917
- if (orNull && (error instanceof TaigaApiError || isNetworkFailure(error))) return null;
38918
- throw error;
38919
- }
38920
- }
38921
38799
  const SETUP_TASK_FIELDS = [
38922
38800
  "id",
38923
38801
  "productId",
@@ -38930,7 +38808,7 @@ const SETUP_TASK_FIELDS = [
38930
38808
  "title",
38931
38809
  "providerDocUrl",
38932
38810
  "providerActionUrl",
38933
- "inboxItemId",
38811
+ "requestId",
38934
38812
  "inboxItemStatus",
38935
38813
  "createdAt",
38936
38814
  "stale"
@@ -38939,6 +38817,16 @@ const SETUP_TASK_FIELDS = [
38939
38817
  function totalOf(result) {
38940
38818
  return typeof result.totalCount === "number" ? { totalCount: result.totalCount } : {};
38941
38819
  }
38820
+ /** A setup task row in the glossary's words: the Action Required item it is, by the name taiga_resolve_action takes. */
38821
+ function setupTaskRow(row) {
38822
+ const { requestId, inboxItemStatus, alsoFiledAs, ...rest } = row;
38823
+ return {
38824
+ ...rest,
38825
+ ...requestId ? { actionId: requestId } : {},
38826
+ ...inboxItemStatus !== void 0 ? { actionStatus: inboxItemStatus } : {},
38827
+ ...Array.isArray(alsoFiledAs) ? { alsoFiledAs: alsoFiledAs.map(setupTaskRow) } : {}
38828
+ };
38829
+ }
38942
38830
  /**
38943
38831
  * One row per variable: tasks filed before the API merged a key's categories
38944
38832
  * (PRO-2782) still list JWT_SECRET once as a secret and once as an env_var.
@@ -38967,7 +38855,7 @@ function sameKeyOnce(rows) {
38967
38855
  id: from["id"],
38968
38856
  category: from["category"],
38969
38857
  title: from["title"],
38970
- inboxItemId: from["inboxItemId"],
38858
+ requestId: from["requestId"],
38971
38859
  inboxItemStatus: from["inboxItemStatus"]
38972
38860
  });
38973
38861
  if (!isOpen(kept) && isOpen(row)) {
@@ -38992,71 +38880,32 @@ function registerInboxTools(server, client) {
38992
38880
  if (!client.hasCapability("products:read")) return;
38993
38881
  server.registerTool("taiga_list_action_required", {
38994
38882
  title: "List what needs a person",
38995
- description: "List what waits on a person (Action Required): reviews to make and setup only a person can do. The first page opens with a summary by product and kind; rows say how long each has waited and which read opens it (readWith). Across every product (10 rows by default) or one; kind and category narrow it, resolved: true reads what was done. For a key that can change the approved license list, the first page also lists licenseReviews: licenses in use in a product that nobody approved.",
38883
+ description: "List what waits on a person (Action Required): reviews to make and setup only a person can do. Opens with a summary by product and kind; rows say how long each has waited and which read opens it (readWith). Across every product (10 rows by default) or one; kind and category narrow it, resolved: true reads what closed in the last two weeks. For a key that can change the approved license list, it also lists licenseReviews: licenses in use in a product that nobody approved.",
38996
38884
  inputSchema: object({
38997
38885
  productId: productId.optional(),
38998
38886
  category: _enum(["gate", "setup"]).optional().describe("gate: things to review; setup: things to set up"),
38999
- kind: _enum(KINDS_SAID).optional().describe("Only this kind, such as review_plan or review_pr, as the rows name it"),
39000
- resolved: boolean().optional().describe("Read the resolved archive (done and dismissed) instead"),
39001
- full: boolean().optional().describe("Carry each item's metadata, such as a document decision's whole text (default false)"),
39002
- ...pagination
38887
+ kind: _enum(LISTED_KINDS).optional().describe("Only this kind, such as plan_review or setup"),
38888
+ resolved: boolean().optional().describe("Read what closed in the last two weeks instead"),
38889
+ full: boolean().optional().describe("Carry each request's facts and its answers (default false)"),
38890
+ limit: number$1().int().min(1).max(200).optional().describe("Max rows (default 10 across products, every row for one product)")
39003
38891
  }),
39004
38892
  annotations: READ_ONLY
39005
38893
  }, (params) => runTool(async () => {
39006
- const { resolved, full, kind, ...rest } = params;
39007
- const query = {
39008
- ...rest,
39009
- ...kind ? { kind: KIND_FROM_SAID[kind] ?? kind } : {},
39010
- ...resolved ? { resolved: true } : {}
39011
- };
39012
- const first = params.cursor === void 0;
39013
- const limit = params.limit ?? (params.productId ? void 0 : ORG_WIDE_ROWS);
39014
- const result = await client.listInbox({
39015
- ...query,
39016
- ...limit !== void 0 ? { limit } : {},
39017
- ...first ? { withTotal: true } : {}
39018
- });
39019
- const overview = !first || resolved ? null : !result.nextCursor ? result : await everyItem(client, query);
38894
+ const { resolved, full, kind, category, limit } = params;
38895
+ const result = resolved ? await client.listDoneRequests() : await client.listRequests();
39020
38896
  const now = Date.now();
39021
- const due_ = (row) => isDue(row, now);
39022
- const rows = result.data;
39023
- const due = rows.filter(due_);
39024
- const hidden = rows.length - due.length;
39025
- const overviewDue = overview ? overview.data.filter(due_) : [];
39026
- const checked = resolved ? {
39027
- stale: /* @__PURE__ */ new Map(),
39028
- unchecked: 0,
39029
- uncheckedIds: /* @__PURE__ */ new Set()
39030
- } : await staleReviews(client, [...due, ...overviewDue]);
39031
- const stale = checked.stale;
39032
- const licences = first && !resolved && !kind && !params.category ? await licenseReviews(client, params.productId, now) : null;
38897
+ const here = result.data.filter(inProduct(params.productId));
38898
+ const licences = here.filter((row) => row["kind"] === "license_review");
38899
+ const asRows = resolved || kind === "license_review";
38900
+ const rows = here.filter((row) => asRows || row["kind"] !== "license_review").filter((row) => kind === void 0 || row["kind"] === kind).filter((row) => category === void 0 || row["kind"] === "setup" === (category === "setup"));
38901
+ const shown = limit ?? (params.productId ? void 0 : ORG_WIDE_ROWS);
38902
+ const backlogRead = client.hasCapability("backlog:read");
39033
38903
  return toolResult({
39034
- ...overview ? {
39035
- summary: summaryOf(overviewDue, stale),
39036
- ...overview.nextCursor ? { summaryNote: `The summary counts the first 1000 items` + (params.productId ? "." : "; productId narrows it to one product.") } : {}
39037
- } : {},
39038
- ...page(due.map((row) => {
39039
- const item = itemRow(row, now, full === true, client.hasCapability("backlog:read"));
39040
- const why = stale.get(String(row["id"]));
39041
- if (why) return {
39042
- ...item,
39043
- stale: why
39044
- };
39045
- return checked.uncheckedIds.has(String(row["id"])) ? {
39046
- ...item,
39047
- staleChecked: false
39048
- } : item;
39049
- }), result.nextCursor),
39050
- ...totalOf(result),
39051
- ...overview && typeof result.totalCount === "number" && (stale.size > 0 || overview.data.length > overviewDue.length || overview.nextCursor) ? { totalNote: "totalCount counts every item, stale and snoozed ones too; summary.open does not" + (overview.nextCursor ? `, and counts only the first 1000.` : ".") } : {},
39052
- ...hidden > 0 ? { snoozedHidden: hidden } : {},
39053
- ...licences && licences.rows.length > 0 ? { licenseReviews: licences.rows } : {},
39054
- ...licences?.truncated ? { licenseReviewsNote: "The requests read stopped at its cap, so some licence reviews may be missing here; the web's Action required lists them all." } : {},
39055
- ...checked.unchecked > 0 ? (() => {
39056
- const onPage = due.filter((row) => checked.uncheckedIds.has(String(row["id"]))).length;
39057
- const beyond = checked.unchecked - onPage;
39058
- return { staleNote: `${[...onPage > 0 ? [`${onPage} review${onPage === 1 ? "" : "s"} on this page could not be checked for whether the subject moved on: those rows say staleChecked: false, and their readWith read tells`] : [], ...beyond > 0 ? [`${beyond} review${beyond === 1 ? "" : "s"} past this page could not be checked, so the summary counts them as open`] : []].join("; ")}.` };
39059
- })() : {}
38904
+ ...resolved ? {} : { summary: summaryOf(rows) },
38905
+ data: rows.slice(0, shown).map((row) => requestRow(row, now, backlogRead, full === true)),
38906
+ totalCount: rows.length,
38907
+ ...!resolved && !kind && !category && licences.length > 0 ? { licenseReviews: licences.map((row) => licenseRow(client, row, now)) } : {},
38908
+ ...result.truncated ? { truncatedNote: "The read stopped at its cap of 200, so some requests may be missing here and totalCount may undercount." } : {}
39060
38909
  });
39061
38910
  }));
39062
38911
  server.registerTool("taiga_list_setup_tasks", {
@@ -39082,24 +38931,19 @@ function registerInboxTools(server, client) {
39082
38931
  }));
39083
38932
  if (!client.hasCapability("products:write")) return;
39084
38933
  server.registerTool("taiga_resolve_action", {
39085
- title: "Resolve or snooze an Action Required item",
39086
- description: "Mark an Action Required item done, dismissed or in progress, or snooze it, for everybody. Resolving a review approves or merges nothing: answer the plan or pull request itself. Marking the documents review done records, in your name, that a person read them: only when they say so.",
38934
+ title: "Answer an Action Required request",
38935
+ description: "Answer a request for everybody: a setup task as done or dismiss, a document, findings or violations as reviewed, a license review as dismiss. Answering approves or merges nothing: answer the plan or pull request itself, and resume a stopped initiative with taiga_resume_initiative. reviewed records, in your name, that a person read it: only when they say so.",
39087
38936
  inputSchema: object({
39088
- actionId: idInput("Action Required item", "taiga_list_action_required gives it"),
39089
- status: _enum([
39090
- "in_progress",
38937
+ actionId: idInput("Action Required request", "taiga_list_action_required gives it"),
38938
+ answer: _enum([
39091
38939
  "done",
39092
- "dismissed"
39093
- ]).optional().describe("What became of it"),
39094
- snoozedUntil: string$2().datetime().meta({ pattern: void 0 }).nullable().optional().describe("Hide it until this time (ISO 8601 in UTC, ending in Z); null wakes it now")
38940
+ "dismiss",
38941
+ "reviewed"
38942
+ ]).describe("The answer, among those the request offers")
39095
38943
  }),
39096
38944
  annotations: OPERATE
39097
38945
  }, (params) => runTool(async () => {
39098
- if (params.status === void 0 && params.snoozedUntil === void 0) return toolError("VALIDATION_ERROR", "Say what became of the item (status) or until when to snooze it.");
39099
- return toolResult(pickFields([(await client.updateInboxItem(params.actionId, {
39100
- ...params.status !== void 0 ? { status: params.status } : {},
39101
- ...params.snoozedUntil !== void 0 ? { snoozedUntil: params.snoozedUntil } : {}
39102
- })).data], ITEM_FIELDS)[0]);
38946
+ return toolResult((await client.answerRequest(params.actionId, params.answer)).data);
39103
38947
  }));
39104
38948
  }
39105
38949
  //#endregion
@@ -39418,15 +39262,11 @@ const CONTEXT_RUN_SAID = [
39418
39262
  ];
39419
39263
  /**
39420
39264
  * What waits on a person for this product, counted as taiga_list_action_required's summary counts it (PRO-2800):
39421
- * the reviewing agent called four tools to answer "how does it stand", and two later reviews found this count and
39422
- * the list's disagree (18 open here, 16 there), because this one counted stale reviews and put-off items as open.
39265
+ * the reviewing agent called four tools to answer "how does it stand". A failed read throws, so the part is listed
39266
+ * under unavailable like every other part.
39423
39267
  */
39424
- async function attentionPart(client, productId) {
39425
- const result = await everyItem(client, { productId }, false);
39426
- return {
39427
- ...await attentionCounts(client, result.data),
39428
- ...result.nextCursor ? { note: "These count the first 1000 items; taiga_list_action_required pages the rest." } : {}
39429
- };
39268
+ function attentionPart(client, productId) {
39269
+ return attentionCounts(client, productId);
39430
39270
  }
39431
39271
  /** The latest sweep and what it left open, as counts; taiga_list_findings lists them. */
39432
39272
  async function maintenancePart(client, productId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taiga-ai/mcp-server",
3
- "version": "0.36.0-rc.933",
3
+ "version": "0.37.0-dev.940",
4
4
  "description": "Taiga for AI coding assistants: read products, policies, initiatives and runs, and operate the line, over MCP",
5
5
  "mcpName": "ga.tai/mcp-server",
6
6
  "license": "UNLICENSED",