@taiga-ai/mcp-server 0.25.0-rc.474 → 0.26.0-dev.481

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 (55) hide show
  1. package/README.md +111 -0
  2. package/dist/index.js +292 -115
  3. package/package.json +15 -2
  4. package/scripts/build.mjs +0 -37
  5. package/scripts/prepare-publish.cjs +0 -49
  6. package/src/client.test.ts +0 -424
  7. package/src/client.ts +0 -977
  8. package/src/config.test.ts +0 -24
  9. package/src/config.ts +0 -28
  10. package/src/index.ts +0 -45
  11. package/src/modern.integration.test.ts +0 -153
  12. package/src/server.integration.test.ts +0 -9214
  13. package/src/server.ts +0 -18
  14. package/src/tools/annotations.ts +0 -38
  15. package/src/tools/autonomy.ts +0 -86
  16. package/src/tools/backlogs.ts +0 -802
  17. package/src/tools/builds.ts +0 -668
  18. package/src/tools/catalogue.test.ts +0 -170
  19. package/src/tools/catalogue.ts +0 -223
  20. package/src/tools/context.ts +0 -513
  21. package/src/tools/documents.ts +0 -375
  22. package/src/tools/errors.ts +0 -172
  23. package/src/tools/hints.ts +0 -99
  24. package/src/tools/inbox.ts +0 -546
  25. package/src/tools/index.ts +0 -239
  26. package/src/tools/inputs.ts +0 -47
  27. package/src/tools/instructions.ts +0 -41
  28. package/src/tools/jobs.ts +0 -90
  29. package/src/tools/knowledge.ts +0 -249
  30. package/src/tools/maintenance.ts +0 -547
  31. package/src/tools/markdown.test.ts +0 -249
  32. package/src/tools/markdown.ts +0 -151
  33. package/src/tools/observe.ts +0 -311
  34. package/src/tools/product-documents.ts +0 -184
  35. package/src/tools/products.ts +0 -529
  36. package/src/tools/prompts.ts +0 -265
  37. package/src/tools/queue.ts +0 -250
  38. package/src/tools/readiness.ts +0 -177
  39. package/src/tools/remote.test.ts +0 -44
  40. package/src/tools/remote.ts +0 -107
  41. package/src/tools/repositories.ts +0 -360
  42. package/src/tools/request-prompt.test.ts +0 -32
  43. package/src/tools/request-prompt.ts +0 -28
  44. package/src/tools/resources.ts +0 -217
  45. package/src/tools/sections.test.ts +0 -143
  46. package/src/tools/sections.ts +0 -287
  47. package/src/tools/setup.ts +0 -455
  48. package/src/tools/summarize.test.ts +0 -20
  49. package/src/tools/summarize.ts +0 -96
  50. package/src/tools/wait.ts +0 -490
  51. package/src/version.test.ts +0 -10
  52. package/src/version.ts +0 -14
  53. package/taiga-ai-mcp-server-0.25.0-rc.474.tgz +0 -0
  54. package/tsconfig.json +0 -21
  55. package/vitest.config.ts +0 -9
package/src/client.ts DELETED
@@ -1,977 +0,0 @@
1
- import type {
2
- ActivityEvent,
3
- AgentJob,
4
- AgentJobType,
5
- ApiKeyIntrospection,
6
- BacklogItem,
7
- BacklogItemPlanWithSteps,
8
- BuildRun,
9
- BuildRunStep,
10
- BuildRunWithContext,
11
- ClientHealthSummary,
12
- CurrentDeployments,
13
- CustomDocumentDownloadView,
14
- CustomDocumentDto,
15
- Deployment,
16
- DeploymentInsights,
17
- DeploymentSyncStatusView,
18
- DocumentReviewState,
19
- Environment,
20
- EnvironmentHealth,
21
- Factory,
22
- FactoryDocument,
23
- FactoryMovePreview,
24
- GithubInstallation,
25
- GithubRepository,
26
- InboxItem,
27
- MaintenanceFinding,
28
- MaintenanceFindingDetail,
29
- MaintenanceRun,
30
- MaintenanceSummary,
31
- MonitoringHistory,
32
- MoveProductResult,
33
- PolicyConformance,
34
- PolicyRemediationInput,
35
- PolicyRemediationResult,
36
- Product,
37
- ProductDocument,
38
- ProductDocumentListItem,
39
- ProductRepository,
40
- ProductRepositoryWithProtection,
41
- PublishProductDocumentResponse,
42
- QueueEntry,
43
- RepositoryBranch,
44
- RumSummary,
45
- SetupTaskListEntry,
46
- TenantDocument,
47
- UptimeReport,
48
- CreateFactoryInput,
49
- CreateProductInput,
50
- } from '@taiga/types';
51
- import { SERVER_VERSION } from './version.js';
52
-
53
- /** A response type as the tools see it: `withoutTenant` has removed every `tenantId`, at any depth. */
54
- export type WithoutTenant<T> = T extends readonly (infer Item)[]
55
- ? WithoutTenant<Item>[]
56
- : T extends object
57
- ? { [K in keyof T as K extends 'tenantId' ? never : K]: WithoutTenant<T[K]> }
58
- : T;
59
-
60
- /** What each monitoring signal route returns. */
61
- interface MonitoringSignals {
62
- uptime: UptimeReport;
63
- rum: RumSummary;
64
- 'client-health': ClientHealthSummary;
65
- history: MonitoringHistory;
66
- }
67
-
68
- /** The API's single-resource envelope. */
69
- type One<T> = { data: T };
70
- /** The API's paginated envelope. */
71
- type Page<T> = { data: T[]; nextCursor: string | null };
72
-
73
- /**
74
- * The same value without any `tenantId`, at any depth. Build run and step
75
- * rows carry one; the key is scoped to one tenant already, so the id is
76
- * never something the model acts on. Applied to every response here, once,
77
- * so no tool, resource or prompt can hand it over by forgetting to; only
78
- * the key's own introspection keeps it, typed and never handed over.
79
- *
80
- * An explicit stack rather than recursion: a document body a person stored
81
- * can nest as deep as JSON.parse allows, and a walk must not overflow where
82
- * the parse did not. Every key is defined as a data property, so an own
83
- * "__proto__" key stays a key instead of becoming the copy's prototype.
84
- */
85
- export function withoutTenant<T>(value: T): WithoutTenant<T> {
86
- const root: { out?: unknown } = {};
87
- // Siblings keep their order and nested values are copied in full: see the loop.
88
- const pending: { source: unknown; target: object; key: string | number }[] = [
89
- { source: value, target: root, key: 'out' },
90
- ];
91
- while (pending.length > 0) {
92
- const { source, target, key } = pending.pop()!;
93
- let copy: unknown = source;
94
- // Children are pushed last-first so they pop first-first: key order is
95
- // meaning in a section map (the order sections render in) and must survive.
96
- if (Array.isArray(source)) {
97
- copy = new Array(source.length);
98
- for (let index = source.length - 1; index >= 0; index -= 1) {
99
- pending.push({ source: source[index], target: copy as object, key: index });
100
- }
101
- } else if (source !== null && typeof source === 'object') {
102
- copy = {};
103
- const entries = Object.entries(source as Record<string, unknown>).filter(([inner]) => inner !== 'tenantId');
104
- for (let index = entries.length - 1; index >= 0; index -= 1) {
105
- pending.push({ source: entries[index]![1], target: copy as object, key: entries[index]![0] });
106
- }
107
- }
108
- Object.defineProperty(target, key, { value: copy, enumerable: true, writable: true, configurable: true });
109
- }
110
- return root.out as WithoutTenant<T>;
111
- }
112
-
113
- export class TaigaApiError extends Error {
114
- readonly status: number;
115
- readonly code: string | undefined;
116
- readonly apiMessage: string | undefined;
117
- /** A refusal's structured reason, when the API sent one: `{ reason, ... }` (a 409's discriminator and its data). */
118
- readonly details: Record<string, unknown> | undefined;
119
-
120
- constructor(method: string, path: string, status: number, body: string) {
121
- const parsed = parseErrorBody(body);
122
- super(`Taiga API ${method} ${path} failed (${status}): ${parsed.message ?? body}`);
123
- this.name = 'TaigaApiError';
124
- this.status = status;
125
- this.code = parsed.code;
126
- this.apiMessage = parsed.message;
127
- this.details = parsed.details;
128
- }
129
- }
130
-
131
- /**
132
- * One path segment. Every id and type a tool passes is validated at the
133
- * tool boundary; this is the second wall. Encoding keeps a slash inside the
134
- * segment, and the dot segments are refused outright: encodeURIComponent
135
- * leaves "." and ".." as they are, and the URL parser in fetch would then
136
- * collapse `/documents/../publish` into a different route.
137
- */
138
- function seg(value: string): string {
139
- if (value === '' || value === '.' || value === '..') {
140
- throw new TypeError(`Invalid path segment: ${JSON.stringify(value)}`);
141
- }
142
- return encodeURIComponent(value);
143
- }
144
-
145
- /**
146
- * A query string from the defined values only, prefixed with "?" when there
147
- * is one. Every value goes through URLSearchParams, so nothing a tool passes
148
- * can add a parameter of its own.
149
- */
150
- function query(params: Record<string, string | number | boolean | null | undefined>): string {
151
- const search = new URLSearchParams();
152
- for (const [key, value] of Object.entries(params)) {
153
- if (value !== undefined && value !== null && value !== '') search.set(key, String(value));
154
- }
155
- const text = search.toString();
156
- return text ? `?${text}` : '';
157
- }
158
-
159
- function parseErrorBody(body: string): { code?: string; message?: string; details?: Record<string, unknown> } {
160
- try {
161
- const json = JSON.parse(body) as { code?: unknown; message?: unknown; details?: unknown };
162
- const message = typeof json.message === 'string' ? json.message : undefined;
163
- // A 400 from route validation says only "Request validation failed";
164
- // the field and the reason are in `details`, and the model needs them.
165
- const details = Array.isArray(json.details)
166
- ? json.details
167
- .map((d: { instancePath?: unknown; message?: unknown }) =>
168
- [d.instancePath, d.message].filter((x) => typeof x === 'string' && x).join(' ')
169
- )
170
- .filter(Boolean)
171
- : [];
172
- // A service refusal names its gate in an object (`reason`, and what it
173
- // found, such as the documents still missing); the model needs both.
174
- const structured =
175
- typeof json.details === 'object' && json.details !== null && !Array.isArray(json.details)
176
- ? (json.details as Record<string, unknown>)
177
- : undefined;
178
- return {
179
- ...(typeof json.code === 'string' ? { code: json.code } : {}),
180
- ...(message ? { message: details.length ? `${message}: ${details.join('; ')}` : message } : {}),
181
- ...(structured ? { details: structured } : {}),
182
- };
183
- } catch {
184
- return {};
185
- }
186
- }
187
-
188
- /**
189
- * A tenant, factory or product document as the tools read it. `data` holds
190
- * the content; policies, instructions and product documents keep it under
191
- * `sections` (see tools/sections.ts). Only tenant and factory documents have a
192
- * `name`; a product document is named by its type.
193
- */
194
- export type TaigaDocument = WithoutTenant<TenantDocument | FactoryDocument | ProductDocument>;
195
-
196
- /**
197
- * HTTP client for the Taiga API.
198
- * Used by the MCP server to proxy tool calls to the backend.
199
- */
200
- export class TaigaClient {
201
- private baseUrl: string;
202
- private apiKey: string;
203
- private introspection: ApiKeyIntrospection | null = null;
204
-
205
- constructor(opts: { baseUrl: string; apiKey: string }) {
206
- this.baseUrl = opts.baseUrl.replace(/\/+$/, '');
207
- this.apiKey = opts.apiKey;
208
- }
209
-
210
- /**
211
- * One API call, its body typed as the route's response. The body is not
212
- * validated: the server trusts the API, and `T` names what the route
213
- * returns. Every `tenantId` is removed unless the caller keeps it.
214
- */
215
- private request<T>(
216
- method: string,
217
- path: string,
218
- body: unknown,
219
- options: { keepTenant: true; signal?: AbortSignal | undefined }
220
- ): Promise<T>;
221
- private request<T>(
222
- method: string,
223
- path: string,
224
- body?: unknown,
225
- options?: { signal?: AbortSignal | undefined }
226
- ): Promise<WithoutTenant<T>>;
227
- private async request<T>(
228
- method: string,
229
- path: string,
230
- body?: unknown,
231
- options: { keepTenant?: true; signal?: AbortSignal | undefined } = {}
232
- ): Promise<T | WithoutTenant<T>> {
233
- const url = `${this.baseUrl}/v1${path}`;
234
- const headers: Record<string, string> = {
235
- Authorization: `Bearer ${this.apiKey}`,
236
- 'User-Agent': `taiga-ai-mcp-server/${SERVER_VERSION}`,
237
- };
238
- // Only with a body: Fastify refuses an empty body under a JSON
239
- // content type, which is what a bodiless POST (publish) would send. A
240
- // form (a file upload) sets its own multipart type with its boundary.
241
- const form = body instanceof FormData;
242
- if (body !== undefined && !form) headers['Content-Type'] = 'application/json';
243
-
244
- const response = await fetch(url, {
245
- method,
246
- headers,
247
- body: body === undefined ? null : form ? body : JSON.stringify(body),
248
- // A caller that must stop waiting (a wait's deadline, a cancelled call) ends the read too.
249
- ...(options.signal ? { signal: options.signal } : {}),
250
- });
251
-
252
- if (!response.ok) {
253
- const text = await response.text().catch(() => '');
254
- throw new TaigaApiError(method, path, response.status, text);
255
- }
256
-
257
- // A delete answers 204 with no body: there is nothing to parse.
258
- if (response.status === 204) return null as T;
259
- const parsed = (await response.json()) as T;
260
- return options.keepTenant ? parsed : withoutTenant(parsed);
261
- }
262
-
263
- // ---------------------------------------------------------------------------
264
- // Auth
265
- // ---------------------------------------------------------------------------
266
-
267
- async introspect(): Promise<ApiKeyIntrospection> {
268
- // The key's own identity keeps its tenant: typed so, and never handed to the model.
269
- const result = await this.request<{ data: ApiKeyIntrospection }>('GET', '/auth/introspect', undefined, {
270
- keepTenant: true,
271
- });
272
- this.introspection = result.data;
273
- return result.data;
274
- }
275
-
276
- getIntrospection(): ApiKeyIntrospection | null {
277
- return this.introspection;
278
- }
279
-
280
- hasCapability(capability: string): boolean {
281
- if (!this.introspection) return false;
282
- return this.introspection.capabilities.includes(capability);
283
- }
284
-
285
- // ---------------------------------------------------------------------------
286
- // Products
287
- // ---------------------------------------------------------------------------
288
-
289
- async listProducts(opts?: {
290
- factoryId?: string | undefined;
291
- status?: string | undefined;
292
- slug?: string | undefined;
293
- repository?: string | undefined;
294
- repositoryHost?: string | undefined;
295
- limit?: number | undefined;
296
- cursor?: string | undefined;
297
- }) {
298
- const params = new URLSearchParams();
299
- if (opts?.factoryId) params.set('factoryId', opts.factoryId);
300
- if (opts?.status) params.set('status', opts.status);
301
- if (opts?.slug) params.set('slug', opts.slug);
302
- if (opts?.repository) params.set('repository', opts.repository);
303
- if (opts?.repositoryHost) params.set('repositoryHost', opts.repositoryHost);
304
- if (opts?.limit) params.set('limit', String(opts.limit));
305
- if (opts?.cursor) params.set('cursor', opts.cursor);
306
- const qs = params.toString();
307
- return this.request<Page<Product>>('GET', `/products${qs ? `?${qs}` : ''}`);
308
- }
309
-
310
- async getProduct(id: string, signal?: AbortSignal) {
311
- return this.request<One<Product>>('GET', `/products/${seg(id)}`, undefined, { signal });
312
- }
313
-
314
- async listProductRepositories(productId: string) {
315
- return this.request<Page<ProductRepository>>('GET', `/products/${seg(productId)}/repositories`);
316
- }
317
-
318
- // ---------------------------------------------------------------------------
319
- // Documents (tenant-level policies)
320
- // ---------------------------------------------------------------------------
321
-
322
- async listTenantDocuments(opts?: { limit?: number | undefined; cursor?: string | undefined }) {
323
- const params = new URLSearchParams();
324
- if (opts?.limit) params.set('limit', String(opts.limit));
325
- if (opts?.cursor) params.set('cursor', opts.cursor);
326
- const qs = params.toString();
327
- return this.request<Page<TenantDocument>>('GET', `/tenants/current/documents${qs ? `?${qs}` : ''}`);
328
- }
329
-
330
- async getTenantDocument(type: string) {
331
- return this.request<One<TenantDocument>>('GET', `/tenants/current/documents/${seg(type)}`);
332
- }
333
-
334
- /** The shared draft for a document type, or `{ data: null }` when none exists. */
335
- async getTenantDocumentDraft(type: string) {
336
- return this.request<One<TenantDocument | null>>('GET', `/tenants/current/documents/${seg(type)}/draft`);
337
- }
338
-
339
- async saveTenantDocumentDraft(type: string, body: { data: Record<string, unknown> }) {
340
- return this.request<One<TenantDocument>>('PUT', `/tenants/current/documents/${seg(type)}/draft`, body);
341
- }
342
-
343
- async publishTenantDocument(type: string) {
344
- return this.request<One<TenantDocument>>('POST', `/tenants/current/documents/${seg(type)}/publish`);
345
- }
346
-
347
- // ---------------------------------------------------------------------------
348
- // Factory Documents
349
- // ---------------------------------------------------------------------------
350
-
351
- async getFactoryDocument(factoryId: string, type: string) {
352
- return this.request<One<FactoryDocument>>('GET', `/factories/${seg(factoryId)}/documents/${seg(type)}`);
353
- }
354
-
355
- /** The shared draft for a factory document type, or `{ data: null }` when none exists. */
356
- async getFactoryDocumentDraft(factoryId: string, type: string) {
357
- return this.request<One<FactoryDocument | null>>(
358
- 'GET',
359
- `/factories/${seg(factoryId)}/documents/${seg(type)}/draft`
360
- );
361
- }
362
-
363
- async saveFactoryDocumentDraft(factoryId: string, type: string, body: { data: Record<string, unknown> }) {
364
- return this.request<One<FactoryDocument>>('PUT', `/factories/${seg(factoryId)}/documents/${seg(type)}/draft`, body);
365
- }
366
-
367
- async publishFactoryDocument(factoryId: string, type: string) {
368
- return this.request<One<FactoryDocument>>('POST', `/factories/${seg(factoryId)}/documents/${seg(type)}/publish`);
369
- }
370
-
371
- // ---------------------------------------------------------------------------
372
- // Product Documents
373
- // ---------------------------------------------------------------------------
374
-
375
- async listProductDocuments(productId: string, opts?: { origin?: string | undefined; limit?: number | undefined }) {
376
- const params = new URLSearchParams();
377
- if (opts?.origin) params.set('origin', opts.origin);
378
- if (opts?.limit) params.set('limit', String(opts.limit));
379
- const qs = params.toString();
380
- return this.request<Page<ProductDocumentListItem>>(
381
- 'GET',
382
- `/products/${seg(productId)}/documents${qs ? `?${qs}` : ''}`
383
- );
384
- }
385
-
386
- /**
387
- * Stop or start the product's line. Stopping halts pulls and builds and
388
- * stops the plan and build in flight; starting picks the interrupted work
389
- * back up.
390
- */
391
- async setLineStopped(productId: string, stopped: boolean) {
392
- return this.request<One<Product>>('PUT', `/products/${seg(productId)}/line`, { stopped });
393
- }
394
-
395
- // ---------------------------------------------------------------------------
396
- // Agent jobs
397
- // ---------------------------------------------------------------------------
398
-
399
- /** Ask an agent for work: the job is queued and runs in the background. */
400
- async createAgentJob(body: {
401
- scope: 'product' | 'tenant';
402
- type: AgentJobType;
403
- targetId: string;
404
- documentType?: string | undefined;
405
- metadata?: Record<string, unknown> | undefined;
406
- }) {
407
- return this.request<One<AgentJob>>('POST', '/agent-jobs', body);
408
- }
409
-
410
- async getAgentJob(id: string, signal?: AbortSignal) {
411
- return this.request<One<AgentJob>>('GET', `/agent-jobs/${seg(id)}`, undefined, { signal });
412
- }
413
-
414
- /** The plan-side draft of a product document, or `{ data: null }` when none exists. */
415
- async getProductDocumentDraft(productId: string, type: string) {
416
- return this.request<One<ProductDocument | null>>('GET', `/products/${seg(productId)}/documents/${seg(type)}/draft`);
417
- }
418
-
419
- async saveProductDocumentDraft(productId: string, type: string, body: { data: Record<string, unknown> }) {
420
- return this.request<One<ProductDocument>>('PUT', `/products/${seg(productId)}/documents/${seg(type)}/draft`, body);
421
- }
422
-
423
- async publishProductDocument(productId: string, type: string) {
424
- return this.request<PublishProductDocumentResponse>(
425
- 'POST',
426
- `/products/${seg(productId)}/documents/${seg(type)}/publish`
427
- );
428
- }
429
-
430
- /** Mark the published version reviewed by the caller, as the Documents page shows it. */
431
- async markProductDocumentReviewed(productId: string, type: string) {
432
- return this.request<One<DocumentReviewState>>('POST', `/products/${seg(productId)}/documents/${seg(type)}/review`);
433
- }
434
-
435
- /** Generate remaining: every missing and outdated document, in chain order. */
436
- async generateRemainingDocuments(productId: string) {
437
- return this.request<One<{ jobIds: string[] }>>('POST', `/products/${seg(productId)}/documents/generate-all`);
438
- }
439
-
440
- /** Cancel a Generate remaining run still in flight. */
441
- async stopGeneratingDocuments(productId: string) {
442
- return this.request<One<{ cancelledJobIds: string[] }>>(
443
- 'POST',
444
- `/products/${seg(productId)}/documents/generate-all/stop`
445
- );
446
- }
447
-
448
- async getProductDocument(productId: string, type: string, origin?: string) {
449
- const qs = origin ? `?${new URLSearchParams({ origin })}` : '';
450
- return this.request<One<ProductDocument>>('GET', `/products/${seg(productId)}/documents/${seg(type)}${qs}`);
451
- }
452
-
453
- // ---------------------------------------------------------------------------
454
- // Factory Documents
455
- // ---------------------------------------------------------------------------
456
-
457
- // ---------------------------------------------------------------------------
458
- // Backlog
459
- // ---------------------------------------------------------------------------
460
-
461
- async listBacklogItems(
462
- productId: string,
463
- opts?: {
464
- status?: string | undefined;
465
- planningStatus?: string | undefined;
466
- category?: string | undefined;
467
- includeArchived?: boolean | undefined;
468
- limit?: number | undefined;
469
- cursor?: string | undefined;
470
- }
471
- ) {
472
- const params = new URLSearchParams();
473
- if (opts?.status) params.set('status', opts.status);
474
- if (opts?.planningStatus) params.set('planningStatus', opts.planningStatus);
475
- if (opts?.category) params.set('category', opts.category);
476
- // The route reads the literal strings 'true' and 'false'.
477
- if (opts?.includeArchived) params.set('includeArchived', 'true');
478
- if (opts?.limit) params.set('limit', String(opts.limit));
479
- if (opts?.cursor) params.set('cursor', opts.cursor);
480
- const qs = params.toString();
481
- return this.request<Page<BacklogItem>>('GET', `/products/${seg(productId)}/backlog${qs ? `?${qs}` : ''}`);
482
- }
483
-
484
- async getBacklogItem(itemId: string, signal?: AbortSignal) {
485
- return this.request<One<BacklogItem>>('GET', `/backlog/${seg(itemId)}`, undefined, { signal });
486
- }
487
-
488
- /** The initiative's history, oldest first: every turn event and audited move, each with who did it. */
489
- async listBacklogItemActivity(itemId: string) {
490
- return this.request<{ data: ActivityEvent[] }>('GET', `/backlog/${seg(itemId)}/activity`);
491
- }
492
-
493
- /** A person's move on the line board: the column dropped into, and what to follow there. */
494
- async transitionInitiative(
495
- itemId: string,
496
- body: {
497
- state: string;
498
- afterId?: string | null | undefined;
499
- archived?: boolean | undefined;
500
- force?: boolean | undefined;
501
- }
502
- ) {
503
- return this.request<One<BacklogItem>>('PUT', `/backlog/${seg(itemId)}/state`, body);
504
- }
505
-
506
- /** Answer the needs-human flag: plan again, resume the run, or clear a refused start. */
507
- async resumeInitiative(itemId: string) {
508
- return this.request<One<BacklogItem>>('POST', `/backlog/${seg(itemId)}/resume`);
509
- }
510
-
511
- /** The initiative's own review gate: true builds a landed plan at once, false stops for a person, null follows the product. */
512
- async setAutoBuild(itemId: string, autoBuild: boolean | null) {
513
- return this.request<One<BacklogItem>>('PUT', `/backlog/${seg(itemId)}/auto-build`, { autoBuild });
514
- }
515
-
516
- /** The product's live queue, in order. */
517
- async listQueue(productId: string) {
518
- return this.request<{ data: QueueEntry[] }>('GET', `/products/${seg(productId)}/queue`);
519
- }
520
-
521
- async getActivePlan(itemId: string) {
522
- return this.request<One<BacklogItemPlanWithSteps>>('GET', `/backlog/${seg(itemId)}/plans/active`);
523
- }
524
-
525
- async approvePlan(planId: string) {
526
- return this.request<One<BacklogItem>>('POST', `/plans/${seg(planId)}/approve`);
527
- }
528
-
529
- async rejectPlan(planId: string, body: { comment: string }) {
530
- return this.request<One<BacklogItem>>('POST', `/plans/${seg(planId)}/reject`, body);
531
- }
532
-
533
- // ---------------------------------------------------------------------------
534
- // Build runs
535
- // ---------------------------------------------------------------------------
536
-
537
- async listBuildRuns(
538
- productId: string,
539
- opts?: { backlogItemId?: string | undefined; limit?: number | undefined; cursor?: string | undefined }
540
- ) {
541
- const params = new URLSearchParams();
542
- if (opts?.backlogItemId) params.set('backlogItemId', opts.backlogItemId);
543
- if (opts?.limit) params.set('limit', String(opts.limit));
544
- if (opts?.cursor) params.set('cursor', opts.cursor);
545
- const qs = params.toString();
546
- return this.request<Page<BuildRunWithContext>>('GET', `/products/${seg(productId)}/builds${qs ? `?${qs}` : ''}`);
547
- }
548
-
549
- /** The runs of every product the key can see, newest first; `active` narrows to the ones executing now. */
550
- async listBuildRunActivity(opts?: {
551
- active?: boolean | undefined;
552
- limit?: number | undefined;
553
- cursor?: string | undefined;
554
- }) {
555
- const params = new URLSearchParams();
556
- // The route reads the literal strings 'true' and 'false'.
557
- if (opts?.active) params.set('active', 'true');
558
- if (opts?.limit) params.set('limit', String(opts.limit));
559
- if (opts?.cursor) params.set('cursor', opts.cursor);
560
- const qs = params.toString();
561
- return this.request<Page<BuildRunWithContext>>('GET', `/builds${qs ? `?${qs}` : ''}`);
562
- }
563
-
564
- async getBuildRun(id: string, signal?: AbortSignal) {
565
- return this.request<One<BuildRunWithContext>>('GET', `/builds/${seg(id)}`, undefined, { signal });
566
- }
567
-
568
- async stopBuildRun(id: string) {
569
- return this.request<One<BuildRun>>('POST', `/builds/${seg(id)}/stop`);
570
- }
571
-
572
- async listBuildRunSteps(id: string) {
573
- return this.request<{ data: BuildRunStep[] }>('GET', `/builds/${seg(id)}/steps`);
574
- }
575
-
576
- // ---------------------------------------------------------------------------
577
- // Maintaining
578
- // ---------------------------------------------------------------------------
579
-
580
- async getMaintenanceSummary(productId: string) {
581
- return this.request<One<MaintenanceSummary>>('GET', `/products/${seg(productId)}/maintenance/summary`);
582
- }
583
-
584
- async listMaintenanceSweeps(productId: string, opts?: { limit?: number | undefined }) {
585
- return this.request<Page<MaintenanceRun>>(
586
- 'GET',
587
- `/products/${seg(productId)}/maintenance/runs${query({ limit: opts?.limit })}`
588
- );
589
- }
590
-
591
- async listFindings(
592
- productId: string,
593
- opts?: {
594
- status?: string | undefined;
595
- severity?: string | undefined;
596
- kind?: string | undefined;
597
- limit?: number | undefined;
598
- cursor?: string | undefined;
599
- }
600
- ) {
601
- return this.request<Page<MaintenanceFinding> & { totalCount?: number }>(
602
- 'GET',
603
- `/products/${seg(productId)}/maintenance/findings${query({ ...opts })}`
604
- );
605
- }
606
-
607
- async getFinding(id: string) {
608
- return this.request<One<MaintenanceFindingDetail>>('GET', `/maintenance/findings/${seg(id)}`);
609
- }
610
-
611
- async updateFinding(
612
- id: string,
613
- body: {
614
- status: 'open' | 'ignored';
615
- ignoreReason?: string | undefined;
616
- ignoreNote?: string | null | undefined;
617
- ignoreExpiresAt?: string | null | undefined;
618
- }
619
- ) {
620
- return this.request<One<MaintenanceFinding>>('PATCH', `/maintenance/findings/${seg(id)}`, body);
621
- }
622
-
623
- async remediateFinding(productId: string, findingId: string) {
624
- return this.request<One<{ finding: MaintenanceFinding; backlogItemId: string }>>(
625
- 'POST',
626
- `/products/${seg(productId)}/maintenance/findings/${seg(findingId)}/remediate`,
627
- {}
628
- );
629
- }
630
-
631
- async remediatePackage(
632
- productId: string,
633
- body: {
634
- ecosystem: string;
635
- packageName: string;
636
- currentVersion?: string | null | undefined;
637
- productRepositoryId?: string | null | undefined;
638
- }
639
- ) {
640
- return this.request<One<{ backlogItemId: string; findingIds: string[] }>>(
641
- 'POST',
642
- `/products/${seg(productId)}/maintenance/packages/remediate`,
643
- body
644
- );
645
- }
646
-
647
- async getPolicyStanding(productId: string) {
648
- return this.request<One<PolicyConformance>>('GET', `/products/${seg(productId)}/maintenance/policies`);
649
- }
650
-
651
- async remediatePolicyControl(productId: string, controlId: string, body: PolicyRemediationInput = {}) {
652
- return this.request<One<PolicyRemediationResult>>(
653
- 'POST',
654
- `/products/${seg(productId)}/maintenance/policies/controls/${seg(controlId)}/remediate`,
655
- body
656
- );
657
- }
658
-
659
- async remediatePolicyViolation(productId: string, divergenceId: string, body: PolicyRemediationInput = {}) {
660
- return this.request<One<PolicyRemediationResult>>(
661
- 'POST',
662
- `/products/${seg(productId)}/maintenance/policies/divergences/${seg(divergenceId)}/remediate`,
663
- body
664
- );
665
- }
666
-
667
- // ---------------------------------------------------------------------------
668
- // Action Required
669
- // ---------------------------------------------------------------------------
670
-
671
- async listInbox(opts: {
672
- productId?: string | undefined;
673
- category?: string | undefined;
674
- resolved?: boolean | undefined;
675
- withTotal?: boolean | undefined;
676
- limit?: number | undefined;
677
- cursor?: string | undefined;
678
- }) {
679
- const { productId, ...rest } = opts;
680
- const path = productId ? `/products/${seg(productId)}/inbox` : '/inbox';
681
- return this.request<Page<InboxItem> & { totalCount?: number }>('GET', `${path}${query({ ...rest })}`);
682
- }
683
-
684
- async updateInboxItem(
685
- id: string,
686
- body: { status?: 'in_progress' | 'done' | 'dismissed' | undefined; snoozedUntil?: string | null | undefined }
687
- ) {
688
- return this.request<One<InboxItem>>('PATCH', `/inbox/${seg(id)}`, body);
689
- }
690
-
691
- async listSetupTasks(opts: {
692
- productId?: string | undefined;
693
- openOnly?: boolean | undefined;
694
- withTotal?: boolean | undefined;
695
- limit?: number | undefined;
696
- cursor?: string | undefined;
697
- }) {
698
- const { productId, ...rest } = opts;
699
- const path = productId ? `/products/${seg(productId)}/setup-tasks` : '/setup-tasks';
700
- return this.request<Page<SetupTaskListEntry> & { totalCount?: number }>('GET', `${path}${query({ ...rest })}`);
701
- }
702
-
703
- // ---------------------------------------------------------------------------
704
- // Deployments, environments and monitoring (observed, never driven)
705
- // ---------------------------------------------------------------------------
706
-
707
- async listEnvironments(productId: string, opts?: { limit?: number | undefined; cursor?: string | undefined }) {
708
- return this.request<Page<Environment>>('GET', `/products/${seg(productId)}/environments${query({ ...opts })}`);
709
- }
710
-
711
- async createEnvironment(productId: string, body: { name: string; type: string }) {
712
- return this.request<One<Environment>>('POST', `/products/${seg(productId)}/environments`, body);
713
- }
714
-
715
- async updateEnvironment(
716
- id: string,
717
- body: { name?: string | undefined; type?: string | undefined; url?: string | null | undefined }
718
- ) {
719
- return this.request<One<Environment>>('PATCH', `/environments/${seg(id)}`, body);
720
- }
721
-
722
- async getCurrentDeployments(productId: string) {
723
- return this.request<One<CurrentDeployments>>('GET', `/products/${seg(productId)}/deployments/current`);
724
- }
725
-
726
- async listDeployments(
727
- productId: string,
728
- opts?: { environmentId?: string | undefined; limit?: number | undefined; cursor?: string | undefined }
729
- ) {
730
- return this.request<Page<Deployment>>('GET', `/products/${seg(productId)}/deployments${query({ ...opts })}`);
731
- }
732
-
733
- async getDeploymentSyncStatus(productId: string) {
734
- return this.request<One<DeploymentSyncStatusView>>('GET', `/products/${seg(productId)}/deployments/sync-status`);
735
- }
736
-
737
- async getDeliveryMetrics(productId: string, environmentId: string, windowDays: number) {
738
- return this.request<One<DeploymentInsights>>(
739
- 'GET',
740
- `/products/${seg(productId)}/deployments/insights${query({ environmentId, windowDays })}`
741
- );
742
- }
743
-
744
- async getMonitoringHealth(productId: string) {
745
- return this.request<{ data: EnvironmentHealth[] }>('GET', `/products/${seg(productId)}/monitoring/health`);
746
- }
747
-
748
- async getMonitoringSignal<S extends keyof MonitoringSignals>(
749
- productId: string,
750
- signal: S,
751
- environmentId: string,
752
- windowDays: number
753
- ) {
754
- return this.request<One<MonitoringSignals[S]>>(
755
- 'GET',
756
- `/products/${seg(productId)}/monitoring/${signal}${query({ environmentId, windowDays })}`
757
- );
758
- }
759
-
760
- // ---------------------------------------------------------------------------
761
- // Knowledge
762
- // ---------------------------------------------------------------------------
763
-
764
- async listKnowledge(opts: {
765
- factoryId?: string | null | undefined;
766
- productId?: string | null | undefined;
767
- limit?: number | undefined;
768
- cursor?: string | undefined;
769
- }) {
770
- // The route reads the literal 'null' as "this level only, not below it".
771
- const scope = (value: string | null | undefined) => (value === null ? 'null' : value);
772
- return this.request<Page<CustomDocumentDto>>(
773
- 'GET',
774
- `/custom-documents${query({ ...opts, factoryId: scope(opts.factoryId), productId: scope(opts.productId) })}`
775
- );
776
- }
777
-
778
- async getKnowledge(id: string) {
779
- return this.request<One<CustomDocumentDto>>('GET', `/custom-documents/${seg(id)}`);
780
- }
781
-
782
- async getKnowledgeDownload(id: string) {
783
- return this.request<One<CustomDocumentDownloadView>>('GET', `/custom-documents/${seg(id)}/download`);
784
- }
785
-
786
- /** A text file filed as knowledge at one level: the organization, a factory, or a product in its factory. */
787
- async addKnowledge(file: {
788
- fileName: string;
789
- mime: 'text/markdown' | 'text/plain';
790
- content: string;
791
- factoryId?: string | undefined;
792
- productId?: string | undefined;
793
- }) {
794
- const form = new FormData();
795
- // Fields before the file: the route reads them in any order, but the
796
- // order a person would read them in costs nothing.
797
- if (file.factoryId) form.set('factoryId', file.factoryId);
798
- if (file.productId) form.set('productId', file.productId);
799
- form.set('file', new Blob([file.content], { type: file.mime }), file.fileName);
800
- return this.request<{ data: CustomDocumentDto; deduplicated: boolean }>('POST', '/custom-documents', form);
801
- }
802
-
803
- async retryKnowledge(id: string) {
804
- return this.request<One<CustomDocumentDto>>('POST', `/custom-documents/${seg(id)}/retry`);
805
- }
806
-
807
- async deleteKnowledge(id: string) {
808
- return this.request<null>('DELETE', `/custom-documents/${seg(id)}`);
809
- }
810
-
811
- // ---------------------------------------------------------------------------
812
- // Autonomy
813
- // ---------------------------------------------------------------------------
814
-
815
- async updateProduct(
816
- productId: string,
817
- body: {
818
- autoMerge?: boolean | null | undefined;
819
- autoBuild?: boolean | undefined;
820
- name?: string | undefined;
821
- ownsInfrastructure?: boolean | null | undefined;
822
- ownsDeployment?: boolean | null | undefined;
823
- deploymentNotes?: string | null | undefined;
824
- emitDesignStandards?: boolean | null | undefined;
825
- }
826
- ) {
827
- return this.request<One<Product>>('PATCH', `/products/${seg(productId)}`, body);
828
- }
829
-
830
- async updateBacklogItem(itemId: string, body: { autoMerge?: boolean | null | undefined }) {
831
- return this.request<One<BacklogItem>>('PATCH', `/backlog/${seg(itemId)}`, body);
832
- }
833
-
834
- // ---------------------------------------------------------------------------
835
- // Factories
836
- // ---------------------------------------------------------------------------
837
-
838
- async getFactory(id: string) {
839
- return this.request<One<Factory>>('GET', `/factories/${seg(id)}`);
840
- }
841
-
842
- async listFactories(opts?: { limit?: number | undefined; cursor?: string | undefined }) {
843
- const params = new URLSearchParams();
844
- if (opts?.limit) params.set('limit', String(opts.limit));
845
- if (opts?.cursor) params.set('cursor', opts.cursor);
846
- const qs = params.toString();
847
- return this.request<Page<Factory>>('GET', `/factories${qs ? `?${qs}` : ''}`);
848
- }
849
-
850
- async createFactory(body: CreateFactoryInput) {
851
- return this.request<One<Factory>>('POST', '/factories', body);
852
- }
853
-
854
- async updateFactory(factoryId: string, body: { name?: string | undefined }) {
855
- return this.request<One<Factory>>('PATCH', `/factories/${seg(factoryId)}`, body);
856
- }
857
-
858
- // ---------------------------------------------------------------------------
859
- // Setting a product up
860
- // ---------------------------------------------------------------------------
861
-
862
- async createProduct(
863
- body: Pick<
864
- CreateProductInput,
865
- | 'name'
866
- | 'factoryId'
867
- | 'projectType'
868
- | 'ownsInfrastructure'
869
- | 'ownsDeployment'
870
- | 'deploymentNotes'
871
- | 'refuseDuplicateName'
872
- >
873
- ) {
874
- return this.request<One<Product>>('POST', '/products', body);
875
- }
876
-
877
- async previewProductMove(productId: string, factoryId: string) {
878
- return this.request<One<FactoryMovePreview>>(
879
- 'GET',
880
- `/products/${seg(productId)}/factory/preview${query({ factoryId })}`
881
- );
882
- }
883
-
884
- async moveProduct(productId: string, body: { factoryId: string; stopLine?: boolean | undefined }) {
885
- return this.request<MoveProductResult>('PUT', `/products/${seg(productId)}/factory`, body);
886
- }
887
-
888
- async startImport(productId: string) {
889
- return this.request<One<{ jobId: string }>>('POST', `/products/${seg(productId)}/brownfield-import`);
890
- }
891
-
892
- async refreshRealityDocuments(productId: string, docTypes?: readonly string[] | undefined) {
893
- return this.request<One<{ jobId: string }>>(
894
- 'POST',
895
- `/products/${seg(productId)}/documents/refresh-reality`,
896
- docTypes ? { docTypes } : undefined
897
- );
898
- }
899
-
900
- /** What still blocks finishing setup: required documents unpublished, required checks unstamped (PRO-2800). */
901
- async getProductSetupBlockers(productId: string) {
902
- return this.request<{
903
- data: {
904
- inSetup: boolean;
905
- missingDocuments: string[];
906
- openChecks: { templateName: string; itemKey: string; criterion: string }[];
907
- };
908
- }>('GET', `/products/${seg(productId)}/setup`);
909
- }
910
-
911
- async completeSetup(productId: string) {
912
- return this.request<One<Product>>('POST', `/products/${seg(productId)}/setup/complete`);
913
- }
914
-
915
- async reopenSetup(productId: string) {
916
- return this.request<One<Product>>('POST', `/products/${seg(productId)}/setup/reopen`);
917
- }
918
-
919
- // ---------------------------------------------------------------------------
920
- // Repositories
921
- // ---------------------------------------------------------------------------
922
-
923
- /** Every connected GitHub account, page by page: a later account must be findable by its id too. */
924
- async listGithubInstallations() {
925
- const data: WithoutTenant<GithubInstallation>[] = [];
926
- let cursor: string | undefined;
927
- do {
928
- const result = await this.request<Page<GithubInstallation>>(
929
- 'GET',
930
- `/github/installations${query({ limit: 100, cursor })}`
931
- );
932
- data.push(...result.data);
933
- cursor = result.nextCursor ?? undefined;
934
- } while (cursor);
935
- return { data };
936
- }
937
-
938
- async listInstallationRepositories(
939
- installationId: string,
940
- opts: { q?: string | undefined; limit?: number | undefined; cursor?: string | undefined }
941
- ) {
942
- return this.request<Page<GithubRepository>>(
943
- 'GET',
944
- `/github/installations/${seg(installationId)}/repositories${query({ ...opts })}`
945
- );
946
- }
947
-
948
- async linkRepository(
949
- productId: string,
950
- body: { githubRepositoryId: string; replacesRepositoryId?: string | undefined }
951
- ) {
952
- return this.request<One<ProductRepository>>('POST', `/products/${seg(productId)}/repositories`, body);
953
- }
954
-
955
- async updateRepository(
956
- productId: string,
957
- repositoryId: string,
958
- body: { baseBranch?: string | null | undefined; label?: string | null | undefined }
959
- ) {
960
- return this.request<One<ProductRepositoryWithProtection>>(
961
- 'PATCH',
962
- `/products/${seg(productId)}/repositories/${seg(repositoryId)}`,
963
- body
964
- );
965
- }
966
-
967
- async listRepositoryBranches(productId: string, repositoryId: string) {
968
- return this.request<{ data: RepositoryBranch[]; truncated: boolean }>(
969
- 'GET',
970
- `/products/${seg(productId)}/repositories/${seg(repositoryId)}/branches`
971
- );
972
- }
973
-
974
- async unlinkRepository(productId: string, repositoryId: string) {
975
- return this.request<null>('DELETE', `/products/${seg(productId)}/repositories/${seg(repositoryId)}`);
976
- }
977
- }