@taiga-ai/mcp-server 0.26.0-rc.500 → 0.26.0-rc.504
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +128 -48
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1919,6 +1919,7 @@ const ENVIRONMENT_TYPES = [
|
|
|
1919
1919
|
const MAINTENANCE_FINDING_KINDS = [
|
|
1920
1920
|
"vulnerable_dependency",
|
|
1921
1921
|
"outdated_dependency",
|
|
1922
|
+
"license_violation",
|
|
1922
1923
|
"secret",
|
|
1923
1924
|
"sast",
|
|
1924
1925
|
"iac_misconfig"
|
|
@@ -1943,7 +1944,11 @@ const CODE_REMEDIATION_KINDS = [
|
|
|
1943
1944
|
"iac_misconfig"
|
|
1944
1945
|
];
|
|
1945
1946
|
new Map(Object.entries({
|
|
1946
|
-
deps: [
|
|
1947
|
+
deps: [
|
|
1948
|
+
"vulnerable_dependency",
|
|
1949
|
+
"outdated_dependency",
|
|
1950
|
+
"license_violation"
|
|
1951
|
+
],
|
|
1947
1952
|
secrets: ["secret"],
|
|
1948
1953
|
sast: ["sast"],
|
|
1949
1954
|
iac: ["iac_misconfig"]
|
|
@@ -2030,6 +2035,19 @@ function lockfileScope(path, groups) {
|
|
|
2030
2035
|
}
|
|
2031
2036
|
}
|
|
2032
2037
|
/**
|
|
2038
|
+
* The kinds that are a package in a lockfile (PRO-2862): the ones with a
|
|
2039
|
+
* dependency scope, a relation and an inventory entry. A code kind reuses the
|
|
2040
|
+
* dependency columns for its rule and category and has none of those.
|
|
2041
|
+
*/
|
|
2042
|
+
const DEPENDENCY_FINDING_KINDS = [
|
|
2043
|
+
"vulnerable_dependency",
|
|
2044
|
+
"outdated_dependency",
|
|
2045
|
+
"license_violation"
|
|
2046
|
+
];
|
|
2047
|
+
function isDependencyFindingKind(kind) {
|
|
2048
|
+
return DEPENDENCY_FINDING_KINDS.includes(kind);
|
|
2049
|
+
}
|
|
2050
|
+
/**
|
|
2033
2051
|
* Whether a dependency finding sits only in development dependencies, in at
|
|
2034
2052
|
* least one runtime dependency set, or in lockfiles that do not say. Decided
|
|
2035
2053
|
* per lockfile (`lockfileScope`) and combined pessimistically: one runtime
|
|
@@ -2040,8 +2058,12 @@ function lockfileScope(path, groups) {
|
|
|
2040
2058
|
* install script in any group.
|
|
2041
2059
|
*/
|
|
2042
2060
|
function dependencyScope(finding) {
|
|
2043
|
-
if (finding.kind
|
|
2044
|
-
|
|
2061
|
+
if (!isDependencyFindingKind(finding.kind)) return "unknown";
|
|
2062
|
+
return scopeFromGroups(finding.dependencyGroups);
|
|
2063
|
+
}
|
|
2064
|
+
/** The scope read from per-lockfile groups alone: for an inventory record, which is a package by construction. */
|
|
2065
|
+
function scopeFromGroups(dependencyGroups) {
|
|
2066
|
+
const entries = Object.entries(dependencyGroups ?? {});
|
|
2045
2067
|
if (entries.length === 0) return "unknown";
|
|
2046
2068
|
return combineScopes(entries.map(([path, groups]) => lockfileScope(path, Array.isArray(groups) ? groups : [])));
|
|
2047
2069
|
}
|
|
@@ -2059,8 +2081,12 @@ function combineScopes(scopes) {
|
|
|
2059
2081
|
* remediation step already finds every resolved copy and its parent itself.
|
|
2060
2082
|
*/
|
|
2061
2083
|
function dependencyRelation(finding) {
|
|
2062
|
-
if (finding.kind
|
|
2063
|
-
|
|
2084
|
+
if (!isDependencyFindingKind(finding.kind)) return "unknown";
|
|
2085
|
+
return relationFromLockfiles(finding.dependencyRelation);
|
|
2086
|
+
}
|
|
2087
|
+
/** The relation read from per-lockfile answers alone: for an inventory record, which is a package by construction. */
|
|
2088
|
+
function relationFromLockfiles(dependencyRelation) {
|
|
2089
|
+
const values = Object.values(dependencyRelation ?? {});
|
|
2064
2090
|
if (values.includes("direct")) return "direct";
|
|
2065
2091
|
if (values.length > 0 && values.every((v) => v === "transitive")) return "transitive";
|
|
2066
2092
|
return "unknown";
|
|
@@ -2604,7 +2630,7 @@ function importStatusOf(product) {
|
|
|
2604
2630
|
/** Why a side of the timeline is empty, for a list or a miss that shows one. */
|
|
2605
2631
|
function emptyOriginNote(origin, product) {
|
|
2606
2632
|
const imported = isImportedProduct(product);
|
|
2607
|
-
if (origin === "plan") return imported ? `This product
|
|
2633
|
+
if (origin === "plan") return imported ? `This product is set up from its code (an import), so no discovery chain is generated for it: its derived documents are on the reality side. Only the documents a person writes (${PLAN_ONLY_DOCUMENT_TYPES.map(productDocumentName).join(", ")}) would appear here.` : "No plan documents are published yet: they are written during discovery in Taiga.";
|
|
2608
2634
|
if (!imported) return "No reality documents yet: they are derived from the code once the first initiative is done (a merged pull request marks it done) or the repository is pushed to. Until then read the plan side.";
|
|
2609
2635
|
switch (importStatusOf(product)) {
|
|
2610
2636
|
case "pending": return "No reality documents yet: the import has not started. taiga_start_import starts it once a repository is linked; taiga_get_product's readiness says whether one is.";
|
|
@@ -2674,7 +2700,7 @@ const BY_ARGUMENT = {
|
|
|
2674
2700
|
/** Where an argument means something else on one tool, or the tool's 404 needs its own word. */
|
|
2675
2701
|
const BY_TOOL = {
|
|
2676
2702
|
taiga_resolve_action: () => "taiga_list_action_required lists the items with their IDs.",
|
|
2677
|
-
taiga_get_plan: (said) => /\bplan\b/i.test(said) ? "taiga_get_initiative's situation says whether one is being planned or failed, and
|
|
2703
|
+
taiga_get_plan: (said) => /\bplan\b/i.test(said) ? "taiga_get_initiative's situation says whether one is being planned or failed, and names the call that asks for one (taiga_queue_initiative off the line, taiga_plan_again for a failed plan)." : ITEM_ID_HINT
|
|
2678
2704
|
};
|
|
2679
2705
|
/**
|
|
2680
2706
|
* What the API calls the thing each ID names, as its 404 says it ("Environment
|
|
@@ -3313,7 +3339,7 @@ const SEARCH_PAGE_LIMIT = 10;
|
|
|
3313
3339
|
* the whole list to find its ID. `more` is true when it stopped before the end.
|
|
3314
3340
|
*/
|
|
3315
3341
|
async function searchPages(fetchPage, query, text, limit, keep = () => true, nameOf) {
|
|
3316
|
-
const needle = query
|
|
3342
|
+
const needle = searchable(query);
|
|
3317
3343
|
const rows = [];
|
|
3318
3344
|
const names = [];
|
|
3319
3345
|
let cursor;
|
|
@@ -3323,7 +3349,7 @@ async function searchPages(fetchPage, query, text, limit, keep = () => true, nam
|
|
|
3323
3349
|
for (const [at, row] of page.entries()) {
|
|
3324
3350
|
if (!keep(row)) continue;
|
|
3325
3351
|
if (nameOf) names.push(nameOf(row));
|
|
3326
|
-
if (!text(row).some((value) => typeof value === "string" && value
|
|
3352
|
+
if (!text(row).some((value) => typeof value === "string" && searchable(value).includes(needle))) continue;
|
|
3327
3353
|
rows.push(row);
|
|
3328
3354
|
if (rows.length === limit) return {
|
|
3329
3355
|
rows,
|
|
@@ -3346,12 +3372,18 @@ async function searchPages(fetchPage, query, text, limit, keep = () => true, nam
|
|
|
3346
3372
|
};
|
|
3347
3373
|
}
|
|
3348
3374
|
/**
|
|
3375
|
+
* Text as a search compares it: lower case, punctuation as spaces, so "sign in" finds "Staff Sign-In" (PRO-2823).
|
|
3376
|
+
*/
|
|
3377
|
+
function searchable(text) {
|
|
3378
|
+
return text.normalize("NFC").toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim() || text.toLowerCase();
|
|
3379
|
+
}
|
|
3380
|
+
/**
|
|
3349
3381
|
* Up to three names close to a query that matched none: a typo of the name or
|
|
3350
3382
|
* of one of its words, within a third of the query's length ("tiaga", "rally"
|
|
3351
3383
|
* for ref-rallly), or a shared word of three letters or more. Closest first.
|
|
3352
3384
|
*/
|
|
3353
3385
|
function closestNames(query, names) {
|
|
3354
|
-
const words = (text) => text
|
|
3386
|
+
const words = (text) => searchable(text).split(" ").filter((word) => word.length >= 3);
|
|
3355
3387
|
const wanted = new Set(words(query));
|
|
3356
3388
|
const ranked = names.map((name) => ({
|
|
3357
3389
|
name,
|
|
@@ -4232,15 +4264,28 @@ function runOutcome(run) {
|
|
|
4232
4264
|
}[disposition] ?? status
|
|
4233
4265
|
};
|
|
4234
4266
|
}
|
|
4267
|
+
/** Words a sentence does not end at: "e.g." and the like, and a lone initial. */
|
|
4268
|
+
const ABBREVIATION = /(?:\b(?:e\.g|i\.e|etc|vs|cf|incl|approx)|(?:^|\s)[A-Za-z])\.$/i;
|
|
4269
|
+
/** A finding's first sentence, capped: it ends at . ! or ? before white space, but not at an abbreviation. */
|
|
4270
|
+
function firstSentence$1(text) {
|
|
4271
|
+
let sentence = text;
|
|
4272
|
+
for (const match of text.matchAll(/[.!?](?=\s|$)/g)) {
|
|
4273
|
+
const end = match.index + 1;
|
|
4274
|
+
if (ABBREVIATION.test(text.slice(0, end))) continue;
|
|
4275
|
+
sentence = text.slice(0, end);
|
|
4276
|
+
break;
|
|
4277
|
+
}
|
|
4278
|
+
return sentence.length > 160 ? `${sentence.slice(0, 157)}...` : sentence;
|
|
4279
|
+
}
|
|
4235
4280
|
/**
|
|
4236
4281
|
* What hardening left blocking, in a sentence: the findings the run names (PRO-2817), or, for a run reported before
|
|
4237
4282
|
* it recorded them, how many.
|
|
4238
4283
|
*/
|
|
4239
4284
|
function hardeningSaid(blocking, named) {
|
|
4240
4285
|
const count = `Hardening ended with ${blocking} blocking finding${blocking === 1 ? "" : "s"} it could not fix`;
|
|
4241
|
-
if (named.length === 0) return `${count};
|
|
4286
|
+
if (named.length === 0) return `${count}; this run predates recording which.`;
|
|
4242
4287
|
const shown = named.slice(0, 3).map((finding) => {
|
|
4243
|
-
const message = String(finding["message"]);
|
|
4288
|
+
const message = firstSentence$1(String(finding["message"]));
|
|
4244
4289
|
const where = finding["line"] ? `${String(finding["path"])}:${String(finding["line"])}` : String(finding["path"]);
|
|
4245
4290
|
return `${message} (${String(finding["tool"])}, ${where})`;
|
|
4246
4291
|
});
|
|
@@ -5075,7 +5120,7 @@ function registerBacklogReads(server, client) {
|
|
|
5075
5120
|
}
|
|
5076
5121
|
if (params.cursor !== void 0) return toolError("VALIDATION_ERROR", "A title search answers one page; narrow the title, or leave query out to page through a column.");
|
|
5077
5122
|
let closedSeen = 0;
|
|
5078
|
-
const needle = query
|
|
5123
|
+
const needle = searchable(query);
|
|
5079
5124
|
const found = await searchPages((cursor) => client.listBacklogItems(id, {
|
|
5080
5125
|
...apiFilters,
|
|
5081
5126
|
...column === "open" ? { open: void 0 } : {},
|
|
@@ -5083,14 +5128,15 @@ function registerBacklogReads(server, client) {
|
|
|
5083
5128
|
cursor
|
|
5084
5129
|
}), query, (row) => [row.title], params.limit ?? 25, (row) => {
|
|
5085
5130
|
if (column !== "open" || !["done", "canceled"].includes(String(row.state))) return true;
|
|
5086
|
-
if (String(row.title)
|
|
5131
|
+
if (searchable(String(row.title)).includes(needle)) closedSeen += 1;
|
|
5087
5132
|
return false;
|
|
5088
|
-
});
|
|
5133
|
+
}, (row) => String(row.title));
|
|
5089
5134
|
const hidden = column === "open" && !found.more ? closedSeen : 0;
|
|
5090
5135
|
return toolResult({
|
|
5091
5136
|
...page(situatedRows(found.rows), null),
|
|
5092
5137
|
...hidden > 0 ? { closedMatches: `${hidden} done or canceled initiative${hidden === 1 ? " also matches" : "s also match"}; column: 'all' lists them.` } : {},
|
|
5093
|
-
...found.more ? { more: "There may be more than the limit carries: narrow the title, or pick one column." } : {}
|
|
5138
|
+
...found.more ? { more: "There may be more than the limit carries: narrow the title, or pick one column." } : {},
|
|
5139
|
+
...hidden === 0 ? nearNote(found.near) : {}
|
|
5094
5140
|
});
|
|
5095
5141
|
}));
|
|
5096
5142
|
server.registerTool("taiga_get_initiative", {
|
|
@@ -5346,6 +5392,11 @@ const FINDING_ROW_FIELDS = [
|
|
|
5346
5392
|
* read off the row; remediableBy stays as the sweep recorded it.
|
|
5347
5393
|
*/
|
|
5348
5394
|
function withFixability(row) {
|
|
5395
|
+
if (row["kind"] === "license_violation") return {
|
|
5396
|
+
...row,
|
|
5397
|
+
fixable: false,
|
|
5398
|
+
fixNote: "A license violation has no fix version: review the license or replace the package; taiga_fix_finding refuses it."
|
|
5399
|
+
};
|
|
5349
5400
|
if ((row["kind"] === "vulnerable_dependency" || row["kind"] === "outdated_dependency") && !row["recommendedVersion"] && row["remediableBy"] === "coding_agent") return {
|
|
5350
5401
|
...row,
|
|
5351
5402
|
fixable: false,
|
|
@@ -5363,7 +5414,7 @@ function withDependencyScope(row, { keepGroups = false } = {}) {
|
|
|
5363
5414
|
...row,
|
|
5364
5415
|
dependencyRelation: void 0
|
|
5365
5416
|
} : lean;
|
|
5366
|
-
if (!(row["kind"]
|
|
5417
|
+
if (!isDependencyFindingKind(String(row["kind"]))) return rest;
|
|
5367
5418
|
const kind = row["kind"];
|
|
5368
5419
|
return {
|
|
5369
5420
|
...rest,
|
|
@@ -5555,7 +5606,7 @@ function registerMaintenanceTools(server, client) {
|
|
|
5555
5606
|
const summary = standingSummary(standing);
|
|
5556
5607
|
const assessment = standing.assessment;
|
|
5557
5608
|
const assessedAt = typeof assessment?.assessedAt === "string" ? assessment.assessedAt : null;
|
|
5558
|
-
const note = assessment?.state === "never" ? "Never assessed: the documents have not been checked against the policies yet, so nothing here is an all-clear. An assessment runs when the reality documents are derived or refreshed (taiga_refresh_reality_documents, paid work: ask first)." : summary.controlsMet + summary.controlsNotApplicable === 0 && assessedAt ? `No control is recorded as met or not applicable: the last assessment (${assessedAt}) either ran before assessments recorded those verdicts, or found none. Until the next one, only violations are known; the rest are not assessed, which is no pass.` : null;
|
|
5609
|
+
const note = assessment?.state === "never" ? "Never assessed: the documents have not been checked against the policies yet, so nothing here is an all-clear. " + (standing.canAssess === false && standing.hasPolicies !== false ? "It has no reality documents to assess yet: an import derives them (taiga_start_import for a product set up from its code, paid work: ask first), and the assessment follows." : "An assessment runs when the reality documents are derived or refreshed (taiga_refresh_reality_documents, paid work: ask first).") : summary.controlsMet + summary.controlsNotApplicable === 0 && assessedAt ? `No control is recorded as met or not applicable: the last assessment (${assessedAt}) either ran before assessments recorded those verdicts, or found none. Until the next one, only violations are known; the rest are not assessed, which is no pass.` : null;
|
|
5559
5610
|
const resolvedAny = Array.isArray(standing.resolvedDivergences) && standing.resolvedDivergences.length > 0;
|
|
5560
5611
|
const wording = violations.length > 0 || resolvedAny ? { wordingNote: "A violation's evidence is from its assessedAt; its title and summary may be from when it was first recorded. Where they disagree, the evidence is current." } : {};
|
|
5561
5612
|
if (params.controlId && params.violationId) return toolError("VALIDATION_ERROR", "Name either a control (controlId) or one violation (violationId), not both.");
|
|
@@ -5827,7 +5878,7 @@ const STALE_CHECKS = 10;
|
|
|
5827
5878
|
async function staleReviews(client, rows) {
|
|
5828
5879
|
const byId = new Map(rows.filter((row) => row["kind"] === "review_pr" && row["subjectType"] === "build_run").map((row) => [String(row["id"]), row]));
|
|
5829
5880
|
const candidates = [...byId.values()].slice(0, STALE_CHECKS);
|
|
5830
|
-
|
|
5881
|
+
const uncheckedIds = new Set([...byId.keys()].slice(STALE_CHECKS));
|
|
5831
5882
|
const out = /* @__PURE__ */ new Map();
|
|
5832
5883
|
await Promise.all(candidates.map(async (row) => {
|
|
5833
5884
|
try {
|
|
@@ -5835,19 +5886,20 @@ async function staleReviews(client, rows) {
|
|
|
5835
5886
|
if (run["prMerged"] === true) return void out.set(String(row["id"]), "its pull request has merged");
|
|
5836
5887
|
if (run["prState"] === "closed") return void out.set(String(row["id"]), "its pull request was closed");
|
|
5837
5888
|
if (typeof run["backlogItemId"] !== "string") return;
|
|
5838
|
-
if (!client.hasCapability("backlog:read")) return void (
|
|
5889
|
+
if (!client.hasCapability("backlog:read")) return void uncheckedIds.add(String(row["id"]));
|
|
5839
5890
|
const item = (await client.getBacklogItem(run["backlogItemId"])).data;
|
|
5840
5891
|
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");
|
|
5841
5892
|
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");
|
|
5842
5893
|
} catch (error) {
|
|
5843
5894
|
if (!(error instanceof TaigaApiError || isNetworkFailure(error))) throw error;
|
|
5844
|
-
|
|
5895
|
+
uncheckedIds.add(String(row["id"]));
|
|
5845
5896
|
}
|
|
5846
5897
|
}));
|
|
5847
|
-
|
|
5898
|
+
await staleBacklogReviews(client, rows, out, uncheckedIds);
|
|
5848
5899
|
return {
|
|
5849
5900
|
stale: out,
|
|
5850
|
-
unchecked
|
|
5901
|
+
unchecked: uncheckedIds.size,
|
|
5902
|
+
uncheckedIds
|
|
5851
5903
|
};
|
|
5852
5904
|
}
|
|
5853
5905
|
/**
|
|
@@ -5855,12 +5907,13 @@ async function staleReviews(client, rows) {
|
|
|
5855
5907
|
* waiting 15 days on a product whose initiatives had all moved on, with nothing an agent could act on. Best effort,
|
|
5856
5908
|
* a handful of products; returns how many could not be checked.
|
|
5857
5909
|
*/
|
|
5858
|
-
async function staleBacklogReviews(client, rows, out) {
|
|
5910
|
+
async function staleBacklogReviews(client, rows, out, unchecked) {
|
|
5859
5911
|
const asks = [...new Map(rows.filter((row) => row["kind"] === "review_backlog" && typeof row["productId"] === "string").map((row) => [String(row["id"]), row])).values()];
|
|
5860
|
-
|
|
5861
|
-
if (
|
|
5912
|
+
const skip = (which) => which.forEach((row) => unchecked.add(String(row["id"])));
|
|
5913
|
+
if (asks.length === 0) return;
|
|
5914
|
+
if (!client.hasCapability("backlog:read")) return skip(asks);
|
|
5862
5915
|
const checked = [...new Set(asks.map((row) => String(row["productId"])))].slice(0, STALE_CHECKS);
|
|
5863
|
-
|
|
5916
|
+
skip(asks.filter((row) => !checked.includes(String(row["productId"]))));
|
|
5864
5917
|
const empty = /* @__PURE__ */ new Set();
|
|
5865
5918
|
await Promise.all(checked.map(async (productId) => {
|
|
5866
5919
|
try {
|
|
@@ -5871,11 +5924,10 @@ async function staleBacklogReviews(client, rows, out) {
|
|
|
5871
5924
|
empty.add(productId);
|
|
5872
5925
|
} catch (error) {
|
|
5873
5926
|
if (!(error instanceof TaigaApiError || isNetworkFailure(error))) throw error;
|
|
5874
|
-
|
|
5927
|
+
skip(asks.filter((row) => row["productId"] === productId));
|
|
5875
5928
|
}
|
|
5876
5929
|
}));
|
|
5877
5930
|
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");
|
|
5878
|
-
return unchecked;
|
|
5879
5931
|
}
|
|
5880
5932
|
/**
|
|
5881
5933
|
* One product's Action Required as counts, the way the list's summary counts them: open is what still stands and is
|
|
@@ -5901,6 +5953,35 @@ function isDue(row, now) {
|
|
|
5901
5953
|
}
|
|
5902
5954
|
/** Pages read for the summary; past this it says it counted only those. */
|
|
5903
5955
|
const SUMMARY_PAGE = 100;
|
|
5956
|
+
/** Pages the summary reads before it says it stopped: 1,000 items. */
|
|
5957
|
+
const SUMMARY_PAGES = 10;
|
|
5958
|
+
/** Every item the summary counts, up to SUMMARY_PAGES pages; nextCursor set when it stopped short. Null on failure. */
|
|
5959
|
+
async function everyItem(client, query, orNull = true) {
|
|
5960
|
+
const data = [];
|
|
5961
|
+
let cursor;
|
|
5962
|
+
try {
|
|
5963
|
+
for (let index = 0; index < SUMMARY_PAGES; index += 1) {
|
|
5964
|
+
const page = await client.listInbox({
|
|
5965
|
+
...query,
|
|
5966
|
+
limit: SUMMARY_PAGE,
|
|
5967
|
+
cursor
|
|
5968
|
+
});
|
|
5969
|
+
data.push(...page.data);
|
|
5970
|
+
if (!page.nextCursor) return {
|
|
5971
|
+
data,
|
|
5972
|
+
nextCursor: null
|
|
5973
|
+
};
|
|
5974
|
+
cursor = page.nextCursor;
|
|
5975
|
+
}
|
|
5976
|
+
return {
|
|
5977
|
+
data,
|
|
5978
|
+
nextCursor: cursor ?? null
|
|
5979
|
+
};
|
|
5980
|
+
} catch (error) {
|
|
5981
|
+
if (orNull && (error instanceof TaigaApiError || isNetworkFailure(error))) return null;
|
|
5982
|
+
throw error;
|
|
5983
|
+
}
|
|
5984
|
+
}
|
|
5904
5985
|
const SETUP_TASK_FIELDS = [
|
|
5905
5986
|
"id",
|
|
5906
5987
|
"productId",
|
|
@@ -5995,14 +6076,7 @@ function registerInboxTools(server, client) {
|
|
|
5995
6076
|
...query,
|
|
5996
6077
|
...first ? { withTotal: true } : {}
|
|
5997
6078
|
});
|
|
5998
|
-
const overview = !first || resolved ? null : !result.nextCursor ? result : await client
|
|
5999
|
-
...query,
|
|
6000
|
-
limit: SUMMARY_PAGE,
|
|
6001
|
-
cursor: void 0
|
|
6002
|
-
}).catch((error) => {
|
|
6003
|
-
if (error instanceof TaigaApiError || isNetworkFailure(error)) return null;
|
|
6004
|
-
throw error;
|
|
6005
|
-
});
|
|
6079
|
+
const overview = !first || resolved ? null : !result.nextCursor ? result : await everyItem(client, query);
|
|
6006
6080
|
const now = Date.now();
|
|
6007
6081
|
const due_ = (row) => isDue(row, now);
|
|
6008
6082
|
const rows = result.data;
|
|
@@ -6011,26 +6085,35 @@ function registerInboxTools(server, client) {
|
|
|
6011
6085
|
const overviewDue = overview ? overview.data.filter(due_) : [];
|
|
6012
6086
|
const checked = resolved ? {
|
|
6013
6087
|
stale: /* @__PURE__ */ new Map(),
|
|
6014
|
-
unchecked: 0
|
|
6088
|
+
unchecked: 0,
|
|
6089
|
+
uncheckedIds: /* @__PURE__ */ new Set()
|
|
6015
6090
|
} : await staleReviews(client, [...due, ...overviewDue]);
|
|
6016
6091
|
const stale = checked.stale;
|
|
6017
6092
|
return toolResult({
|
|
6018
6093
|
...overview ? {
|
|
6019
6094
|
summary: summaryOf(overviewDue, stale),
|
|
6020
|
-
...overview.nextCursor ? { summaryNote: `The summary counts the first
|
|
6095
|
+
...overview.nextCursor ? { summaryNote: `The summary counts the first 1000 items` + (params.productId ? "." : "; productId narrows it to one product.") } : {}
|
|
6021
6096
|
} : {},
|
|
6022
6097
|
...page(due.map((row) => {
|
|
6023
6098
|
const item = itemRow(row, now, full === true, client.hasCapability("backlog:read"));
|
|
6024
6099
|
const why = stale.get(String(row["id"]));
|
|
6025
|
-
|
|
6100
|
+
if (why) return {
|
|
6026
6101
|
...item,
|
|
6027
6102
|
stale: why
|
|
6103
|
+
};
|
|
6104
|
+
return checked.uncheckedIds.has(String(row["id"])) ? {
|
|
6105
|
+
...item,
|
|
6106
|
+
staleChecked: false
|
|
6028
6107
|
} : item;
|
|
6029
6108
|
}), result.nextCursor),
|
|
6030
6109
|
...totalOf(result),
|
|
6031
|
-
...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
|
|
6110
|
+
...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.` : ".") } : {},
|
|
6032
6111
|
...hidden > 0 ? { snoozedHidden: hidden } : {},
|
|
6033
|
-
...checked.unchecked > 0 ?
|
|
6112
|
+
...checked.unchecked > 0 ? (() => {
|
|
6113
|
+
const onPage = due.filter((row) => checked.uncheckedIds.has(String(row["id"]))).length;
|
|
6114
|
+
const beyond = checked.unchecked - onPage;
|
|
6115
|
+
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"} counted in the summary but not on this page could not be checked either, so the summary counts them open`] : []].join("; ")}.` };
|
|
6116
|
+
})() : {}
|
|
6034
6117
|
});
|
|
6035
6118
|
}));
|
|
6036
6119
|
server.registerTool("taiga_list_setup_tasks", {
|
|
@@ -6395,13 +6478,10 @@ const CONTEXT_RUN_SAID = [
|
|
|
6395
6478
|
* the list's disagree (18 open here, 16 there), because this one counted stale reviews and put-off items as open.
|
|
6396
6479
|
*/
|
|
6397
6480
|
async function attentionPart(client, productId) {
|
|
6398
|
-
const result = await client
|
|
6399
|
-
productId,
|
|
6400
|
-
limit: 100
|
|
6401
|
-
});
|
|
6481
|
+
const result = await everyItem(client, { productId }, false);
|
|
6402
6482
|
return {
|
|
6403
6483
|
...await attentionCounts(client, result.data),
|
|
6404
|
-
...result.nextCursor ? { note: "These count the first
|
|
6484
|
+
...result.nextCursor ? { note: "These count the first 1000 items; taiga_list_action_required pages the rest." } : {}
|
|
6405
6485
|
};
|
|
6406
6486
|
}
|
|
6407
6487
|
/** The latest sweep and what it left open, as counts; taiga_list_findings lists them. */
|
|
@@ -22976,7 +23056,7 @@ function registerAllTools(server, client, options = {}) {
|
|
|
22976
23056
|
function registerWhoami(server, client, registered) {
|
|
22977
23057
|
server.registerTool("taiga_whoami", {
|
|
22978
23058
|
title: "Who am I",
|
|
22979
|
-
description: "Read who this API key acts as: the API it talks to (api
|
|
23059
|
+
description: "Read who this API key acts as: the API host it talks to (api), the person's email and organization role, the key's scopes (null means every capability the person has), its capabilities, and its tools split into reads, writes, and ask-first writes (paid agent work, changes others see). A tool the key cannot use is not offered at all.",
|
|
22980
23060
|
inputSchema: z.object({}),
|
|
22981
23061
|
annotations: READ_ONLY
|
|
22982
23062
|
}, () => runTool(async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taiga-ai/mcp-server",
|
|
3
|
-
"version": "0.26.0-rc.
|
|
3
|
+
"version": "0.26.0-rc.504",
|
|
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
|
"type": "module",
|