@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.
- package/README.md +7 -7
- package/dist/index.js +215 -375
- 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.
|
|
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.
|
|
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.
|
|
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
|
+
$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.
|
|
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.
|
|
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.
|
|
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
|
-
*
|
|
23979
|
-
*
|
|
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
|
|
23984
|
-
return this.request("GET",
|
|
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
|
-
|
|
23987
|
-
|
|
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 =
|
|
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.")
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
38656
|
-
|
|
38657
|
-
|
|
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
|
-
|
|
38666
|
-
|
|
38667
|
-
|
|
38668
|
-
case "
|
|
38669
|
-
|
|
38670
|
-
|
|
38671
|
-
}
|
|
38672
|
-
case "
|
|
38673
|
-
case "
|
|
38674
|
-
|
|
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
|
-
|
|
38684
|
-
|
|
38685
|
-
|
|
38686
|
-
|
|
38687
|
-
const
|
|
38688
|
-
|
|
38689
|
-
|
|
38690
|
-
|
|
38691
|
-
|
|
38692
|
-
|
|
38693
|
-
|
|
38694
|
-
|
|
38695
|
-
}
|
|
38696
|
-
|
|
38697
|
-
|
|
38698
|
-
|
|
38699
|
-
|
|
38700
|
-
|
|
38701
|
-
|
|
38702
|
-
|
|
38703
|
-
|
|
38704
|
-
|
|
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
|
|
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
|
-
|
|
38761
|
-
|
|
38762
|
-
|
|
38763
|
-
|
|
38764
|
-
|
|
38765
|
-
...
|
|
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
|
-
...
|
|
38768
|
-
...
|
|
38769
|
-
...
|
|
38770
|
-
...
|
|
38771
|
-
|
|
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
|
-
|
|
38776
|
-
|
|
38777
|
-
|
|
38778
|
-
|
|
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
|
|
38782
|
-
const
|
|
38783
|
-
|
|
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
|
-
|
|
38793
|
-
|
|
38794
|
-
|
|
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
|
-
/**
|
|
38803
|
-
const
|
|
38804
|
-
/**
|
|
38805
|
-
|
|
38806
|
-
|
|
38807
|
-
|
|
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
|
-
...
|
|
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
|
-
"
|
|
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
|
-
|
|
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.
|
|
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(
|
|
39000
|
-
resolved: boolean().optional().describe("Read
|
|
39001
|
-
full: boolean().optional().describe("Carry each
|
|
39002
|
-
|
|
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,
|
|
39007
|
-
const
|
|
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
|
|
39022
|
-
const
|
|
39023
|
-
const
|
|
39024
|
-
const
|
|
39025
|
-
const
|
|
39026
|
-
const
|
|
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
|
-
...
|
|
39035
|
-
|
|
39036
|
-
|
|
39037
|
-
} : {},
|
|
39038
|
-
...
|
|
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: "
|
|
39086
|
-
description: "
|
|
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
|
|
39089
|
-
|
|
39090
|
-
"in_progress",
|
|
38937
|
+
actionId: idInput("Action Required request", "taiga_list_action_required gives it"),
|
|
38938
|
+
answer: _enum([
|
|
39091
38939
|
"done",
|
|
39092
|
-
"
|
|
39093
|
-
|
|
39094
|
-
|
|
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
|
-
|
|
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"
|
|
39422
|
-
*
|
|
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
|
-
|
|
39425
|
-
|
|
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.
|
|
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",
|