redmine-context 1.0.0 → 1.2.0
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 +20 -5
- package/dist/bundle/journal-detail.d.ts +89 -0
- package/dist/bundle/journal-detail.js +260 -0
- package/dist/bundle/markdown.d.ts +7 -0
- package/dist/bundle/markdown.js +21 -12
- package/dist/client/enumerations.d.ts +39 -0
- package/dist/client/enumerations.js +76 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/fetch-issue-bundle.js +17 -2
- package/dist/fetch-issue-search.d.ts +10 -0
- package/dist/fetch-issue-search.js +1 -1
- package/dist/fetch-last-issues.d.ts +96 -0
- package/dist/fetch-last-issues.js +120 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +22 -2
- package/dist/normalize/issue.js +32 -3
- package/dist/surfaces/cli/commands.d.ts +17 -0
- package/dist/surfaces/cli/commands.js +105 -12
- package/dist/surfaces/cli/main.js +22 -3
- package/dist/surfaces/mcp/server.d.ts +22 -45
- package/dist/surfaces/mcp/server.js +67 -56
- package/dist/surfaces/mcp/tools.d.ts +109 -0
- package/dist/surfaces/mcp/tools.js +93 -0
- package/dist/surfaces/tui/app.d.ts +14 -0
- package/dist/surfaces/tui/app.js +40 -3
- package/dist/surfaces/tui/banner.d.ts +43 -0
- package/dist/surfaces/tui/banner.js +93 -0
- package/dist/surfaces/tui/components/gauge.d.ts +27 -0
- package/dist/surfaces/tui/components/gauge.js +53 -0
- package/dist/surfaces/tui/components/gradient-banner.d.ts +13 -0
- package/dist/surfaces/tui/components/gradient-banner.js +41 -0
- package/dist/surfaces/tui/components/text-input.js +23 -4
- package/dist/surfaces/tui/glyphs.d.ts +8 -0
- package/dist/surfaces/tui/glyphs.js +8 -0
- package/dist/surfaces/tui/hooks/use-issue-detail.d.ts +7 -1
- package/dist/surfaces/tui/hooks/use-issue-detail.js +15 -2
- package/dist/surfaces/tui/hooks/use-issue-search.d.ts +25 -2
- package/dist/surfaces/tui/hooks/use-issue-search.js +14 -2
- package/dist/surfaces/tui/hooks/use-list-navigation.d.ts +8 -0
- package/dist/surfaces/tui/hooks/use-list-navigation.js +12 -1
- package/dist/surfaces/tui/hooks/use-my-issues.d.ts +9 -0
- package/dist/surfaces/tui/hooks/use-my-issues.js +6 -2
- package/dist/surfaces/tui/hooks/use-status-options.d.ts +38 -0
- package/dist/surfaces/tui/hooks/use-status-options.js +86 -0
- package/dist/surfaces/tui/hooks/use-terminal-width.d.ts +16 -0
- package/dist/surfaces/tui/hooks/use-terminal-width.js +15 -0
- package/dist/surfaces/tui/hooks/use-typing-guard.d.ts +19 -0
- package/dist/surfaces/tui/hooks/use-typing-guard.js +58 -0
- package/dist/surfaces/tui/list-window.d.ts +39 -0
- package/dist/surfaces/tui/list-window.js +41 -0
- package/dist/surfaces/tui/screens/export.js +6 -2
- package/dist/surfaces/tui/screens/home.js +161 -33
- package/dist/surfaces/tui/screens/issue-detail.js +55 -16
- package/dist/surfaces/tui/screens/jobs.js +3 -2
- package/dist/surfaces/tui/screens/welcome.js +9 -1
- package/dist/surfaces/tui/status-color.d.ts +21 -0
- package/dist/surfaces/tui/status-color.js +33 -0
- package/dist/surfaces/tui/wrap.d.ts +38 -0
- package/dist/surfaces/tui/wrap.js +82 -0
- package/package.json +1 -1
|
@@ -16,45 +16,9 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
18
18
|
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
19
|
+
import { type GetAttachmentTextArgs, type GetIssueContextArgs, type GetLastArgs, type SearchIssuesArgs } from './tools.js';
|
|
19
20
|
import * as core from '../../index.js';
|
|
20
21
|
import type { AttachmentTextResult, FetchAttachmentTextOptions, resolveApiKey } from '../../index.js';
|
|
21
|
-
/** Formato aceito pela tool MCP (nomes amigáveis expostos ao cliente). */
|
|
22
|
-
export type McpFormat = 'markdown' | 'json';
|
|
23
|
-
/** Argumentos da tool `get_issue_context` já validados pelo schema zod. */
|
|
24
|
-
export interface GetIssueContextArgs {
|
|
25
|
-
/** Identificador numérico da issue no Redmine. */
|
|
26
|
-
issue_id: number;
|
|
27
|
-
/** Formato de saída: `markdown` (padrão) ou `json`. */
|
|
28
|
-
format?: McpFormat | undefined;
|
|
29
|
-
/**
|
|
30
|
-
* Extrai o texto dos anexos de imagem (OCR) e o embute no bundle. Default:
|
|
31
|
-
* `false` — a extração adiciona latência (download + OCR por anexo). O M4 trará
|
|
32
|
-
* o modo cache-first/processing assíncrono; no M3 a extração é síncrona.
|
|
33
|
-
*/
|
|
34
|
-
extract_attachments?: boolean | undefined;
|
|
35
|
-
}
|
|
36
|
-
/** Argumentos da tool `get_attachment_text` já validados pelo schema zod. */
|
|
37
|
-
export interface GetAttachmentTextArgs {
|
|
38
|
-
/** Identificador numérico da issue que contém o anexo. */
|
|
39
|
-
issue_id: number;
|
|
40
|
-
/** Identificador numérico do anexo cujo texto será extraído. */
|
|
41
|
-
attachment_id: number;
|
|
42
|
-
}
|
|
43
|
-
/** Argumentos da tool `search_issues` já validados pelo schema zod. */
|
|
44
|
-
export interface SearchIssuesArgs {
|
|
45
|
-
/** Termo full-text opcional (`/search.json`, best-effort). */
|
|
46
|
-
query?: string | undefined;
|
|
47
|
-
/** Filtro `project_id`. */
|
|
48
|
-
project_id?: number | undefined;
|
|
49
|
-
/** Filtro `status_id` (`'open'`, `'closed'`, `'*'` ou um id). */
|
|
50
|
-
status_id?: number | string | undefined;
|
|
51
|
-
/** Filtro `assigned_to_id` (um id ou `'me'`). */
|
|
52
|
-
assigned_to_id?: number | string | undefined;
|
|
53
|
-
/** Filtro `updated_on` no formato do Redmine (ex.: `>=2026-01-01`). */
|
|
54
|
-
updated_on?: string | undefined;
|
|
55
|
-
/** Máximo de resultados. Default: {@link SEARCH_DEFAULT_LIMIT}. */
|
|
56
|
-
limit?: number | undefined;
|
|
57
|
-
}
|
|
58
22
|
/**
|
|
59
23
|
* Dependências injetáveis do server MCP — permitem testar o handler sem tocar o
|
|
60
24
|
* processo real nem a rede. Os defaults (ver {@link defaultMcpDeps}) apontam para
|
|
@@ -65,6 +29,8 @@ export interface McpServerDeps {
|
|
|
65
29
|
fetchIssueBundle: typeof core.fetchIssueBundle;
|
|
66
30
|
/** Orquestração de busca (filtros + full-text best-effort) do core. */
|
|
67
31
|
searchIssues: typeof core.fetchIssueSearch;
|
|
32
|
+
/** Orquestração das últimas issues (ordem + bundle completo) do core. */
|
|
33
|
+
fetchLastIssues: typeof core.fetchLastIssues;
|
|
68
34
|
/**
|
|
69
35
|
* Orquestração get → normalize → extração CACHE-FIRST de UM anexo (M4-11 #70):
|
|
70
36
|
* devolve o texto já cacheado na hora e `processing` (sem bloquear) para mídia
|
|
@@ -95,14 +61,6 @@ export interface McpServerDeps {
|
|
|
95
61
|
/** Sink de diagnóstico (progresso/erros) — SEMPRE stderr no stdio transport. */
|
|
96
62
|
log?: (message: string) => void;
|
|
97
63
|
}
|
|
98
|
-
/** Nome canônico da tool de contexto de issue. */
|
|
99
|
-
export declare const TOOL_NAME = "get_issue_context";
|
|
100
|
-
/** Nome canônico da tool de busca de issues. */
|
|
101
|
-
export declare const SEARCH_TOOL_NAME = "search_issues";
|
|
102
|
-
/** Nome canônico da tool de texto de anexo. */
|
|
103
|
-
export declare const ATTACHMENT_TOOL_NAME = "get_attachment_text";
|
|
104
|
-
/** Limite default de resultados da tool `search_issues` (documentado no schema). */
|
|
105
|
-
export declare const SEARCH_DEFAULT_LIMIT = 25;
|
|
106
64
|
/**
|
|
107
65
|
* Cria o handler da tool `get_issue_context`, testável isoladamente.
|
|
108
66
|
*
|
|
@@ -132,6 +90,24 @@ export declare function createGetIssueContextHandler(deps: McpServerDeps): (args
|
|
|
132
90
|
* const result = await handler({ query: 'timeout', project_id: 5 });
|
|
133
91
|
*/
|
|
134
92
|
export declare function createSearchIssuesHandler(deps: McpServerDeps): (args: SearchIssuesArgs) => Promise<CallToolResult>;
|
|
93
|
+
/**
|
|
94
|
+
* Cria o handler da tool `get_last`, testável isoladamente.
|
|
95
|
+
*
|
|
96
|
+
* Resolve a instância/credencial da env (nunca de argumentos) e delega à
|
|
97
|
+
* orquestração `fetchLastIssues`: ordena por `updated`/`created`/`priority` e
|
|
98
|
+
* devolve o BUNDLE COMPLETO das mais recentes — o atalho de um passo para
|
|
99
|
+
* "me dá a última issue", sem exigir que o cliente descubra o id antes.
|
|
100
|
+
*
|
|
101
|
+
* Usa `cacheFirst: true` como as demais tools: responde na hora com o texto de
|
|
102
|
+
* anexo já cacheado e marca o restante como `processing`, sem bloquear no OCR.
|
|
103
|
+
*
|
|
104
|
+
* @param deps - Ver {@link McpServerDeps}.
|
|
105
|
+
* @returns Função assíncrona que recebe os argumentos e devolve um CallToolResult.
|
|
106
|
+
* @example
|
|
107
|
+
* const handler = createGetLastHandler(defaultMcpDeps());
|
|
108
|
+
* const result = await handler({ order: 'priority', count: 3 });
|
|
109
|
+
*/
|
|
110
|
+
export declare function createGetLastHandler(deps: McpServerDeps): (args: GetLastArgs) => Promise<CallToolResult>;
|
|
135
111
|
/**
|
|
136
112
|
* Cria o handler da tool `get_attachment_text`, testável isoladamente.
|
|
137
113
|
*
|
|
@@ -169,3 +145,4 @@ export declare function defaultMcpDeps(): McpServerDeps;
|
|
|
169
145
|
* @returns Promise que resolve quando o transporte encerra.
|
|
170
146
|
*/
|
|
171
147
|
export declare function runStdioServer(overrides?: Partial<McpServerDeps>): Promise<void>;
|
|
148
|
+
export { ATTACHMENT_TOOL_NAME, LAST_TOOL_NAME, SEARCH_DEFAULT_LIMIT, SEARCH_TOOL_NAME, TOOL_NAME, type GetAttachmentTextArgs, type GetIssueContextArgs, type GetLastArgs, type McpFormat, type SearchIssuesArgs, } from './tools.js';
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
18
18
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
19
|
-
import {
|
|
19
|
+
import { ATTACHMENT_INPUT_SCHEMA, ATTACHMENT_TOOL_NAME, INPUT_SCHEMA, LAST_INPUT_SCHEMA, LAST_TOOL_NAME, SEARCH_DEFAULT_LIMIT, SEARCH_INPUT_SCHEMA, SEARCH_TOOL_NAME, TOOL_NAME, } from './tools.js';
|
|
20
20
|
import * as core from '../../index.js';
|
|
21
21
|
import { fenceBlock } from '../../index.js';
|
|
22
22
|
/** Interpreta `REDMINE_INSECURE` (`1`/`true`, case-insensitive) como boolean. */
|
|
@@ -24,61 +24,6 @@ function parseInsecure(env) {
|
|
|
24
24
|
const raw = env.REDMINE_INSECURE;
|
|
25
25
|
return raw !== undefined && /^(1|true)$/i.test(raw.trim());
|
|
26
26
|
}
|
|
27
|
-
/** Nome canônico da tool de contexto de issue. */
|
|
28
|
-
export const TOOL_NAME = 'get_issue_context';
|
|
29
|
-
/** Nome canônico da tool de busca de issues. */
|
|
30
|
-
export const SEARCH_TOOL_NAME = 'search_issues';
|
|
31
|
-
/** Nome canônico da tool de texto de anexo. */
|
|
32
|
-
export const ATTACHMENT_TOOL_NAME = 'get_attachment_text';
|
|
33
|
-
/** Limite default de resultados da tool `search_issues` (documentado no schema). */
|
|
34
|
-
export const SEARCH_DEFAULT_LIMIT = 25;
|
|
35
|
-
/** Teto de resultados aceito pela tool `search_issues`. */
|
|
36
|
-
const SEARCH_MAX_LIMIT = 100;
|
|
37
|
-
/** Schema zod dos argumentos da tool (sem URL/host: a instância vem da env). */
|
|
38
|
-
const INPUT_SCHEMA = {
|
|
39
|
-
issue_id: z.number().int().positive().describe('Identificador numérico da issue no Redmine'),
|
|
40
|
-
format: z
|
|
41
|
-
.enum(['markdown', 'json'])
|
|
42
|
-
.optional()
|
|
43
|
-
.describe("Formato de saída: 'markdown' (padrão) ou 'json'"),
|
|
44
|
-
extract_attachments: z
|
|
45
|
-
.boolean()
|
|
46
|
-
.optional()
|
|
47
|
-
.describe('Extrai o texto (OCR) dos anexos de imagem e o embute no bundle. Default: false (adiciona latência de download+OCR). O M4 traz o modo cache-first/processing.'),
|
|
48
|
-
};
|
|
49
|
-
/** Schema zod da tool `get_attachment_text` (read-only, sem URL/host). */
|
|
50
|
-
const ATTACHMENT_INPUT_SCHEMA = {
|
|
51
|
-
issue_id: z.number().int().positive().describe('Identificador numérico da issue que contém o anexo'),
|
|
52
|
-
attachment_id: z.number().int().positive().describe('Identificador numérico do anexo a extrair'),
|
|
53
|
-
};
|
|
54
|
-
/** Schema zod da tool `search_issues` (read-only, sem URL/host). */
|
|
55
|
-
const SEARCH_INPUT_SCHEMA = {
|
|
56
|
-
query: z
|
|
57
|
-
.string()
|
|
58
|
-
.min(1)
|
|
59
|
-
.optional()
|
|
60
|
-
.describe('Termo full-text via /search.json (best-effort). Se a busca falhar, degrada para os filtros estruturados com aviso.'),
|
|
61
|
-
project_id: z.number().int().positive().optional().describe('Filtro estruturado project_id'),
|
|
62
|
-
status_id: z
|
|
63
|
-
.union([z.number().int(), z.string()])
|
|
64
|
-
.optional()
|
|
65
|
-
.describe("Filtro status_id: um id, 'open', 'closed' ou '*'"),
|
|
66
|
-
assigned_to_id: z
|
|
67
|
-
.union([z.number().int(), z.string()])
|
|
68
|
-
.optional()
|
|
69
|
-
.describe("Filtro assigned_to_id: um id ou 'me'"),
|
|
70
|
-
updated_on: z
|
|
71
|
-
.string()
|
|
72
|
-
.optional()
|
|
73
|
-
.describe('Filtro updated_on no formato do Redmine (ex.: >=2026-01-01, <=2026-12-31)'),
|
|
74
|
-
limit: z
|
|
75
|
-
.number()
|
|
76
|
-
.int()
|
|
77
|
-
.positive()
|
|
78
|
-
.max(SEARCH_MAX_LIMIT)
|
|
79
|
-
.optional()
|
|
80
|
-
.describe(`Máximo de resultados paginados (default ${SEARCH_DEFAULT_LIMIT}, teto ${SEARCH_MAX_LIMIT})`),
|
|
81
|
-
};
|
|
82
27
|
/** Extrai uma mensagem legível de um erro desconhecido. */
|
|
83
28
|
function messageOf(error) {
|
|
84
29
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -276,6 +221,60 @@ export function createSearchIssuesHandler(deps) {
|
|
|
276
221
|
}
|
|
277
222
|
};
|
|
278
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* Cria o handler da tool `get_last`, testável isoladamente.
|
|
226
|
+
*
|
|
227
|
+
* Resolve a instância/credencial da env (nunca de argumentos) e delega à
|
|
228
|
+
* orquestração `fetchLastIssues`: ordena por `updated`/`created`/`priority` e
|
|
229
|
+
* devolve o BUNDLE COMPLETO das mais recentes — o atalho de um passo para
|
|
230
|
+
* "me dá a última issue", sem exigir que o cliente descubra o id antes.
|
|
231
|
+
*
|
|
232
|
+
* Usa `cacheFirst: true` como as demais tools: responde na hora com o texto de
|
|
233
|
+
* anexo já cacheado e marca o restante como `processing`, sem bloquear no OCR.
|
|
234
|
+
*
|
|
235
|
+
* @param deps - Ver {@link McpServerDeps}.
|
|
236
|
+
* @returns Função assíncrona que recebe os argumentos e devolve um CallToolResult.
|
|
237
|
+
* @example
|
|
238
|
+
* const handler = createGetLastHandler(defaultMcpDeps());
|
|
239
|
+
* const result = await handler({ order: 'priority', count: 3 });
|
|
240
|
+
*/
|
|
241
|
+
export function createGetLastHandler(deps) {
|
|
242
|
+
return async (args) => {
|
|
243
|
+
const resolved = await resolveInstance(deps);
|
|
244
|
+
if (!isResolved(resolved))
|
|
245
|
+
return resolved;
|
|
246
|
+
const { baseUrl, apiKey } = resolved;
|
|
247
|
+
const format = args.format === 'json' ? 'json' : 'md';
|
|
248
|
+
try {
|
|
249
|
+
let content;
|
|
250
|
+
for await (const event of deps.fetchLastIssues({
|
|
251
|
+
baseUrl,
|
|
252
|
+
apiKey,
|
|
253
|
+
format,
|
|
254
|
+
order: args.order,
|
|
255
|
+
count: args.count,
|
|
256
|
+
toolVersion: deps.toolVersion,
|
|
257
|
+
insecure: deps.insecure ?? false,
|
|
258
|
+
extractAttachments: args.extract_attachments ?? false,
|
|
259
|
+
cacheFirst: true,
|
|
260
|
+
})) {
|
|
261
|
+
if (event.kind === 'progress') {
|
|
262
|
+
deps.log?.(event.message);
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
content = event.value.content;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (content === undefined) {
|
|
269
|
+
return errorResult('A operação não produziu um bundle.');
|
|
270
|
+
}
|
|
271
|
+
return textResult(content);
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
return errorResult(typedSearchErrorMessage(error));
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
}
|
|
279
278
|
/**
|
|
280
279
|
* Traduz erros da extração de anexo em mensagens claras e tipadas.
|
|
281
280
|
*
|
|
@@ -370,6 +369,7 @@ export function createMcpServer(deps) {
|
|
|
370
369
|
const handler = createGetIssueContextHandler(deps);
|
|
371
370
|
const searchHandler = createSearchIssuesHandler(deps);
|
|
372
371
|
const attachmentHandler = createGetAttachmentTextHandler(deps);
|
|
372
|
+
const lastHandler = createGetLastHandler(deps);
|
|
373
373
|
server.registerTool(TOOL_NAME, {
|
|
374
374
|
title: 'Contexto de issue do Redmine',
|
|
375
375
|
description: 'Busca uma issue na instância Redmine configurada (REDMINE_URL) e retorna seu contexto completo empacotado. Read-only.',
|
|
@@ -388,6 +388,12 @@ export function createMcpServer(deps) {
|
|
|
388
388
|
inputSchema: ATTACHMENT_INPUT_SCHEMA,
|
|
389
389
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
390
390
|
}, (args) => attachmentHandler(args));
|
|
391
|
+
server.registerTool(LAST_TOOL_NAME, {
|
|
392
|
+
title: 'Últimas issues do Redmine',
|
|
393
|
+
description: 'Retorna o contexto completo das issues mais recentes da instância configurada (REDMINE_URL), ordenadas por updated (padrão), created ou priority. Atalho de um passo quando o id ainda não é conhecido — considera apenas issues abertas; use search_issues para filtros (projeto, status, responsável). Read-only.',
|
|
394
|
+
inputSchema: LAST_INPUT_SCHEMA,
|
|
395
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
396
|
+
}, (args) => lastHandler(args));
|
|
391
397
|
return server;
|
|
392
398
|
}
|
|
393
399
|
/** Constrói as dependências default apontando para o core e o processo reais. */
|
|
@@ -395,6 +401,7 @@ export function defaultMcpDeps() {
|
|
|
395
401
|
return {
|
|
396
402
|
fetchIssueBundle: core.fetchIssueBundle,
|
|
397
403
|
searchIssues: core.fetchIssueSearch,
|
|
404
|
+
fetchLastIssues: core.fetchLastIssues,
|
|
398
405
|
fetchAttachmentText: core.fetchAttachmentTextCacheFirst,
|
|
399
406
|
resolveApiKey: core.resolveApiKey,
|
|
400
407
|
env: process.env,
|
|
@@ -425,3 +432,7 @@ export async function runStdioServer(overrides = {}) {
|
|
|
425
432
|
});
|
|
426
433
|
}
|
|
427
434
|
/* c8 ignore stop */
|
|
435
|
+
// Contrato de entrada das tools (nomes, tipos e schemas) — vive em `./tools.ts`
|
|
436
|
+
// desde a extração da RULES #24. Reexportado aqui para que os consumidores
|
|
437
|
+
// (CLI, testes) continuem tendo `./server.js` como porta única da superfície MCP.
|
|
438
|
+
export { ATTACHMENT_TOOL_NAME, LAST_TOOL_NAME, SEARCH_DEFAULT_LIMIT, SEARCH_TOOL_NAME, TOOL_NAME, } from './tools.js';
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contrato de ENTRADA das tools MCP: nomes canônicos, tipos de argumentos e
|
|
3
|
+
* schemas zod.
|
|
4
|
+
*
|
|
5
|
+
* Extraído de `./server.ts` (RULES #24 — arquivo não-teste perto de 700 linhas):
|
|
6
|
+
* o server passa a conter apenas o comportamento (resolução de instância,
|
|
7
|
+
* handlers e registro), enquanto a superfície declarativa — o que cada tool
|
|
8
|
+
* aceita — vive aqui. Um só motivo para mudar cada arquivo.
|
|
9
|
+
*
|
|
10
|
+
* Segurança: nenhum schema aceita URL/host — a instância vem sempre da config do
|
|
11
|
+
* processo (ver `resolveInstance` em `./server.ts`).
|
|
12
|
+
*/
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import * as core from '../../index.js';
|
|
15
|
+
/** Formato aceito pelas tools MCP (nomes amigáveis expostos ao cliente). */
|
|
16
|
+
export type McpFormat = 'markdown' | 'json';
|
|
17
|
+
/** Nome canônico da tool de contexto de issue. */
|
|
18
|
+
export declare const TOOL_NAME = "get_issue_context";
|
|
19
|
+
/** Nome canônico da tool de busca de issues. */
|
|
20
|
+
export declare const SEARCH_TOOL_NAME = "search_issues";
|
|
21
|
+
/** Nome canônico da tool de texto de anexo. */
|
|
22
|
+
export declare const ATTACHMENT_TOOL_NAME = "get_attachment_text";
|
|
23
|
+
/** Nome canônico da tool das últimas issues. */
|
|
24
|
+
export declare const LAST_TOOL_NAME = "get_last";
|
|
25
|
+
/** Limite default de resultados da tool `search_issues` (documentado no schema). */
|
|
26
|
+
export declare const SEARCH_DEFAULT_LIMIT = 25;
|
|
27
|
+
/** Argumentos da tool `get_issue_context` já validados pelo schema zod. */
|
|
28
|
+
export interface GetIssueContextArgs {
|
|
29
|
+
/** Identificador numérico da issue no Redmine. */
|
|
30
|
+
issue_id: number;
|
|
31
|
+
/** Formato de saída: `markdown` (padrão) ou `json`. */
|
|
32
|
+
format?: McpFormat | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Extrai o texto dos anexos de imagem (OCR) e o embute no bundle. Default:
|
|
35
|
+
* `false` — a extração adiciona latência (download + OCR por anexo). O M4 trará
|
|
36
|
+
* o modo cache-first/processing assíncrono; no M3 a extração é síncrona.
|
|
37
|
+
*/
|
|
38
|
+
extract_attachments?: boolean | undefined;
|
|
39
|
+
}
|
|
40
|
+
/** Argumentos da tool `get_attachment_text` já validados pelo schema zod. */
|
|
41
|
+
export interface GetAttachmentTextArgs {
|
|
42
|
+
/** Identificador numérico da issue que contém o anexo. */
|
|
43
|
+
issue_id: number;
|
|
44
|
+
/** Identificador numérico do anexo cujo texto será extraído. */
|
|
45
|
+
attachment_id: number;
|
|
46
|
+
}
|
|
47
|
+
/** Argumentos da tool `search_issues` já validados pelo schema zod. */
|
|
48
|
+
export interface SearchIssuesArgs {
|
|
49
|
+
/** Termo full-text opcional (`/search.json`, best-effort). */
|
|
50
|
+
query?: string | undefined;
|
|
51
|
+
/** Filtro `project_id`. */
|
|
52
|
+
project_id?: number | undefined;
|
|
53
|
+
/** Filtro `status_id` (`'open'`, `'closed'`, `'*'` ou um id). */
|
|
54
|
+
status_id?: number | string | undefined;
|
|
55
|
+
/** Filtro `assigned_to_id` (um id ou `'me'`). */
|
|
56
|
+
assigned_to_id?: number | string | undefined;
|
|
57
|
+
/** Filtro `updated_on` no formato do Redmine (ex.: `>=2026-01-01`). */
|
|
58
|
+
updated_on?: string | undefined;
|
|
59
|
+
/** Máximo de resultados. Default: {@link SEARCH_DEFAULT_LIMIT}. */
|
|
60
|
+
limit?: number | undefined;
|
|
61
|
+
}
|
|
62
|
+
/** Argumentos da tool `get_last` já validados pelo schema zod. */
|
|
63
|
+
export interface GetLastArgs {
|
|
64
|
+
/** Critério de ordenação. Default: `updated`. */
|
|
65
|
+
order?: core.LastIssuesOrder | undefined;
|
|
66
|
+
/** Quantas issues empacotar. Default: 1; teto {@link core.LAST_MAX_COUNT}. */
|
|
67
|
+
count?: number | undefined;
|
|
68
|
+
/** Formato de saída: `markdown` (padrão) ou `json`. */
|
|
69
|
+
format?: McpFormat | undefined;
|
|
70
|
+
/** Extrai o texto (OCR) dos anexos e o embute nos bundles. Default: `false`. */
|
|
71
|
+
extract_attachments?: boolean | undefined;
|
|
72
|
+
}
|
|
73
|
+
/** Schema zod da tool `get_issue_context` (sem URL/host: a instância vem da env). */
|
|
74
|
+
export declare const INPUT_SCHEMA: {
|
|
75
|
+
readonly issue_id: z.ZodNumber;
|
|
76
|
+
readonly format: z.ZodOptional<z.ZodEnum<{
|
|
77
|
+
json: "json";
|
|
78
|
+
markdown: "markdown";
|
|
79
|
+
}>>;
|
|
80
|
+
readonly extract_attachments: z.ZodOptional<z.ZodBoolean>;
|
|
81
|
+
};
|
|
82
|
+
/** Schema zod da tool `get_attachment_text` (read-only, sem URL/host). */
|
|
83
|
+
export declare const ATTACHMENT_INPUT_SCHEMA: {
|
|
84
|
+
readonly issue_id: z.ZodNumber;
|
|
85
|
+
readonly attachment_id: z.ZodNumber;
|
|
86
|
+
};
|
|
87
|
+
/** Schema zod da tool `search_issues` (read-only, sem URL/host). */
|
|
88
|
+
export declare const SEARCH_INPUT_SCHEMA: {
|
|
89
|
+
readonly query: z.ZodOptional<z.ZodString>;
|
|
90
|
+
readonly project_id: z.ZodOptional<z.ZodNumber>;
|
|
91
|
+
readonly status_id: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>;
|
|
92
|
+
readonly assigned_to_id: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>;
|
|
93
|
+
readonly updated_on: z.ZodOptional<z.ZodString>;
|
|
94
|
+
readonly limit: z.ZodOptional<z.ZodNumber>;
|
|
95
|
+
};
|
|
96
|
+
/** Schema zod da tool `get_last` (read-only, sem URL/host). */
|
|
97
|
+
export declare const LAST_INPUT_SCHEMA: {
|
|
98
|
+
readonly order: z.ZodOptional<z.ZodEnum<{
|
|
99
|
+
priority: "priority";
|
|
100
|
+
updated: "updated";
|
|
101
|
+
created: "created";
|
|
102
|
+
}>>;
|
|
103
|
+
readonly count: z.ZodOptional<z.ZodNumber>;
|
|
104
|
+
readonly format: z.ZodOptional<z.ZodEnum<{
|
|
105
|
+
json: "json";
|
|
106
|
+
markdown: "markdown";
|
|
107
|
+
}>>;
|
|
108
|
+
readonly extract_attachments: z.ZodOptional<z.ZodBoolean>;
|
|
109
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contrato de ENTRADA das tools MCP: nomes canônicos, tipos de argumentos e
|
|
3
|
+
* schemas zod.
|
|
4
|
+
*
|
|
5
|
+
* Extraído de `./server.ts` (RULES #24 — arquivo não-teste perto de 700 linhas):
|
|
6
|
+
* o server passa a conter apenas o comportamento (resolução de instância,
|
|
7
|
+
* handlers e registro), enquanto a superfície declarativa — o que cada tool
|
|
8
|
+
* aceita — vive aqui. Um só motivo para mudar cada arquivo.
|
|
9
|
+
*
|
|
10
|
+
* Segurança: nenhum schema aceita URL/host — a instância vem sempre da config do
|
|
11
|
+
* processo (ver `resolveInstance` em `./server.ts`).
|
|
12
|
+
*/
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import * as core from '../../index.js';
|
|
15
|
+
/** Nome canônico da tool de contexto de issue. */
|
|
16
|
+
export const TOOL_NAME = 'get_issue_context';
|
|
17
|
+
/** Nome canônico da tool de busca de issues. */
|
|
18
|
+
export const SEARCH_TOOL_NAME = 'search_issues';
|
|
19
|
+
/** Nome canônico da tool de texto de anexo. */
|
|
20
|
+
export const ATTACHMENT_TOOL_NAME = 'get_attachment_text';
|
|
21
|
+
/** Nome canônico da tool das últimas issues. */
|
|
22
|
+
export const LAST_TOOL_NAME = 'get_last';
|
|
23
|
+
/** Limite default de resultados da tool `search_issues` (documentado no schema). */
|
|
24
|
+
export const SEARCH_DEFAULT_LIMIT = 25;
|
|
25
|
+
/** Teto de resultados aceito pela tool `search_issues`. */
|
|
26
|
+
const SEARCH_MAX_LIMIT = 100;
|
|
27
|
+
/** Schema zod da tool `get_issue_context` (sem URL/host: a instância vem da env). */
|
|
28
|
+
export const INPUT_SCHEMA = {
|
|
29
|
+
issue_id: z.number().int().positive().describe('Identificador numérico da issue no Redmine'),
|
|
30
|
+
format: z
|
|
31
|
+
.enum(['markdown', 'json'])
|
|
32
|
+
.optional()
|
|
33
|
+
.describe("Formato de saída: 'markdown' (padrão) ou 'json'"),
|
|
34
|
+
extract_attachments: z
|
|
35
|
+
.boolean()
|
|
36
|
+
.optional()
|
|
37
|
+
.describe('Extrai o texto (OCR) dos anexos de imagem e o embute no bundle. Default: false (adiciona latência de download+OCR). O M4 traz o modo cache-first/processing.'),
|
|
38
|
+
};
|
|
39
|
+
/** Schema zod da tool `get_attachment_text` (read-only, sem URL/host). */
|
|
40
|
+
export const ATTACHMENT_INPUT_SCHEMA = {
|
|
41
|
+
issue_id: z.number().int().positive().describe('Identificador numérico da issue que contém o anexo'),
|
|
42
|
+
attachment_id: z.number().int().positive().describe('Identificador numérico do anexo a extrair'),
|
|
43
|
+
};
|
|
44
|
+
/** Schema zod da tool `search_issues` (read-only, sem URL/host). */
|
|
45
|
+
export const SEARCH_INPUT_SCHEMA = {
|
|
46
|
+
query: z
|
|
47
|
+
.string()
|
|
48
|
+
.min(1)
|
|
49
|
+
.optional()
|
|
50
|
+
.describe('Termo full-text via /search.json (best-effort). Se a busca falhar, degrada para os filtros estruturados com aviso.'),
|
|
51
|
+
project_id: z.number().int().positive().optional().describe('Filtro estruturado project_id'),
|
|
52
|
+
status_id: z
|
|
53
|
+
.union([z.number().int(), z.string()])
|
|
54
|
+
.optional()
|
|
55
|
+
.describe("Filtro status_id: um id, 'open', 'closed' ou '*'"),
|
|
56
|
+
assigned_to_id: z
|
|
57
|
+
.union([z.number().int(), z.string()])
|
|
58
|
+
.optional()
|
|
59
|
+
.describe("Filtro assigned_to_id: um id ou 'me'"),
|
|
60
|
+
updated_on: z
|
|
61
|
+
.string()
|
|
62
|
+
.optional()
|
|
63
|
+
.describe('Filtro updated_on no formato do Redmine (ex.: >=2026-01-01, <=2026-12-31)'),
|
|
64
|
+
limit: z
|
|
65
|
+
.number()
|
|
66
|
+
.int()
|
|
67
|
+
.positive()
|
|
68
|
+
.max(SEARCH_MAX_LIMIT)
|
|
69
|
+
.optional()
|
|
70
|
+
.describe(`Máximo de resultados paginados (default ${SEARCH_DEFAULT_LIMIT}, teto ${SEARCH_MAX_LIMIT})`),
|
|
71
|
+
};
|
|
72
|
+
/** Schema zod da tool `get_last` (read-only, sem URL/host). */
|
|
73
|
+
export const LAST_INPUT_SCHEMA = {
|
|
74
|
+
order: z
|
|
75
|
+
.enum(['updated', 'created', 'priority'])
|
|
76
|
+
.optional()
|
|
77
|
+
.describe(`Critério de ordenação: 'updated' (padrão — mexida mais recente), 'created' (entrada mais recente) ou 'priority' (mais urgente, desempatando pela mais recente)`),
|
|
78
|
+
count: z
|
|
79
|
+
.number()
|
|
80
|
+
.int()
|
|
81
|
+
.positive()
|
|
82
|
+
.max(core.LAST_MAX_COUNT)
|
|
83
|
+
.optional()
|
|
84
|
+
.describe(`Quantas issues empacotar (default ${core.LAST_DEFAULT_COUNT}, teto ${core.LAST_MAX_COUNT}). Cada item é um bundle COMPLETO — prefira search_issues para visões amplas.`),
|
|
85
|
+
format: z
|
|
86
|
+
.enum(['markdown', 'json'])
|
|
87
|
+
.optional()
|
|
88
|
+
.describe("Formato de saída: 'markdown' (padrão) ou 'json' (sempre um array)"),
|
|
89
|
+
extract_attachments: z
|
|
90
|
+
.boolean()
|
|
91
|
+
.optional()
|
|
92
|
+
.describe('Extrai o texto (OCR) dos anexos de imagem e o embute nos bundles. Default: false (adiciona latência de download+OCR).'),
|
|
93
|
+
};
|
|
@@ -9,6 +9,20 @@ import { type Theme, type ThemeController } from './theme.js';
|
|
|
9
9
|
* Ink nem depender de uma tela de dados real (#29+) para chegar no estado de
|
|
10
10
|
* re-auth ativo.
|
|
11
11
|
*/
|
|
12
|
+
/**
|
|
13
|
+
* Estilo da moldura da aplicação conforme o suporte a Unicode.
|
|
14
|
+
*
|
|
15
|
+
* `round` desenha com box-drawing (`╭ ─ │ ╯`); no terminal legado do Windows
|
|
16
|
+
* isso vira mojibake, do mesmo jeito que os frames braille do spinner (M5-09,
|
|
17
|
+
* #84). `classic` é o fallback ASCII do próprio Ink (`+ - |`).
|
|
18
|
+
*
|
|
19
|
+
* Função pura (em vez de só a constante) para o fallback ser testável sem
|
|
20
|
+
* depender do ambiente real — mesmo padrão de `./glyphs.ts`.
|
|
21
|
+
*
|
|
22
|
+
* @param unicode - `true` quando o terminal renderiza box-drawing.
|
|
23
|
+
* @returns O `borderStyle` a passar ao `Box` do Ink.
|
|
24
|
+
*/
|
|
25
|
+
export declare function borderStyleFor(unicode: boolean): 'round' | 'classic';
|
|
12
26
|
export type EscapeAction = {
|
|
13
27
|
kind: 'abort-reauth';
|
|
14
28
|
origin: ScreenName;
|
package/dist/surfaces/tui/app.js
CHANGED
|
@@ -34,12 +34,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
34
34
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
35
35
|
import { Box, Text, useApp, useInput, useStdout } from 'ink';
|
|
36
36
|
import { Breadcrumb } from './components/breadcrumb.js';
|
|
37
|
-
import {
|
|
37
|
+
import { isUnicodeSupported } from './glyphs.js';
|
|
38
|
+
import { TerminalHeightProvider, TerminalSizeProvider, useTerminalHeight, } from './hooks/use-terminal-width.js';
|
|
38
39
|
import { applyTerminalColors } from './terminal-colors.js';
|
|
39
40
|
import { ReAuthAbortedError } from './hooks/use-auth-guard.js';
|
|
40
41
|
import { consumeEscapeInterceptor } from './hooks/use-escape-interceptor.js';
|
|
41
42
|
import { useExitGuard } from './hooks/use-exit-guard.js';
|
|
42
43
|
import { useOnboardingCallbacks } from './hooks/use-onboarding-callbacks.js';
|
|
44
|
+
import { isTyping } from './hooks/use-typing-guard.js';
|
|
43
45
|
import { JobRegistryProvider } from './job-registry.js';
|
|
44
46
|
import { NavigationProvider, useNavigation, useNavigationStack } from './navigation.js';
|
|
45
47
|
import { HomeSelectionProvider } from './screens/home-selection.js';
|
|
@@ -49,6 +51,33 @@ import { DEFAULT_PALETTE_ID, resolvePalette } from './palettes.js';
|
|
|
49
51
|
import { INITIAL_SCREEN, SCREENS } from './screen.js';
|
|
50
52
|
import { symbols } from './symbols.js';
|
|
51
53
|
import { DEFAULT_THEME, ThemeControllerProvider, ThemeProvider, useTheme, } from './theme.js';
|
|
54
|
+
/**
|
|
55
|
+
* O que `Esc` deve fazer dado o estado atual — extraída como função pura
|
|
56
|
+
* (fix do review #119) para poder ser testada isoladamente
|
|
57
|
+
* (`tests/surfaces/tui/app.test.tsx`) sem precisar montar toda a árvore do
|
|
58
|
+
* Ink nem depender de uma tela de dados real (#29+) para chegar no estado de
|
|
59
|
+
* re-auth ativo.
|
|
60
|
+
*/
|
|
61
|
+
/**
|
|
62
|
+
* Estilo da moldura da aplicação conforme o suporte a Unicode.
|
|
63
|
+
*
|
|
64
|
+
* `round` desenha com box-drawing (`╭ ─ │ ╯`); no terminal legado do Windows
|
|
65
|
+
* isso vira mojibake, do mesmo jeito que os frames braille do spinner (M5-09,
|
|
66
|
+
* #84). `classic` é o fallback ASCII do próprio Ink (`+ - |`).
|
|
67
|
+
*
|
|
68
|
+
* Função pura (em vez de só a constante) para o fallback ser testável sem
|
|
69
|
+
* depender do ambiente real — mesmo padrão de `./glyphs.ts`.
|
|
70
|
+
*
|
|
71
|
+
* @param unicode - `true` quando o terminal renderiza box-drawing.
|
|
72
|
+
* @returns O `borderStyle` a passar ao `Box` do Ink.
|
|
73
|
+
*/
|
|
74
|
+
export function borderStyleFor(unicode) {
|
|
75
|
+
return unicode ? 'round' : 'classic';
|
|
76
|
+
}
|
|
77
|
+
/** Estilo resolvido para o ambiente atual — decidido uma vez, no import. */
|
|
78
|
+
const BORDER_STYLE = borderStyleFor(isUnicodeSupported());
|
|
79
|
+
/** Linhas consumidas pela moldura (topo + base). */
|
|
80
|
+
const BORDER_ROWS = 2;
|
|
52
81
|
/**
|
|
53
82
|
* Decide a ação de `Esc`: abandono do re-auth (fix do review #119) quando a
|
|
54
83
|
* tela atual é do fluxo de onboarding (`onboarding-*`) E há um `reAuth` em
|
|
@@ -139,7 +168,10 @@ function AppShell() {
|
|
|
139
168
|
const abortReAuthRef = useRef(abortReAuth);
|
|
140
169
|
abortReAuthRef.current = abortReAuth;
|
|
141
170
|
const handleGlobalInput = useCallback((input, key) => {
|
|
142
|
-
|
|
171
|
+
// `q` só sai FORA de campo de texto: o Ink entrega a tecla a todos os
|
|
172
|
+
// handlers, então sem esta guarda digitar uma URL com "q" (ou uma senha)
|
|
173
|
+
// fecharia a TUI no meio do onboarding (ver ./hooks/use-typing-guard.ts).
|
|
174
|
+
if (input === 'q' && !isTyping()) {
|
|
143
175
|
exitRef.current();
|
|
144
176
|
return;
|
|
145
177
|
}
|
|
@@ -176,5 +208,10 @@ function AppShell() {
|
|
|
176
208
|
// + este container com a ALTURA do terminal (`minHeight`). O breadcrumb fica no
|
|
177
209
|
// topo e a tela ocupa o resto (`flexGrow`); cada tela ancora seus atalhos no
|
|
178
210
|
// rodapé com um espaçador `flexGrow` (estilo nano/nvim/tmux).
|
|
179
|
-
return (
|
|
211
|
+
return (
|
|
212
|
+
// Moldura única da aplicação (#190 — pacote estético): a borda vive AQUI e
|
|
213
|
+
// não nas telas, então nenhuma tela precisa saber que existe uma. As duas
|
|
214
|
+
// linhas da borda saem do minHeight para o conteúdo não estourar a altura do
|
|
215
|
+
// terminal (o que empurraria o topo para fora no modo full-screen).
|
|
216
|
+
_jsxs(Box, { flexDirection: "column", minHeight: Math.max(1, rows - BORDER_ROWS), borderStyle: BORDER_STYLE, borderColor: theme.border, children: [_jsx(Breadcrumb, { stack: stack }), armed ? (_jsx(Box, { paddingX: 1, marginBottom: 1, children: _jsxs(Text, { color: theme.warning, children: [symbols.warning, " Pressione Ctrl+C de novo para sair."] }) })) : null, _jsx(Box, { flexGrow: 1, flexDirection: "column", children: _jsx(TerminalHeightProvider, { height: Math.max(1, rows - BORDER_ROWS), children: _jsx(Screen, {}) }) })] }));
|
|
180
217
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arte ASCII do banner da tela de abertura (#190 — pacote estético).
|
|
3
|
+
*
|
|
4
|
+
* As três variantes são CONSTANTES, não geradas em runtime: o texto é sempre
|
|
5
|
+
* `redmine-context`, então trazer uma dependência de fontes (figlet/cfonts) para
|
|
6
|
+
* recalcular a mesma string a cada boot não se paga — custaria peso no pacote e
|
|
7
|
+
* tempo de start. Para regenerar (fonte `ANSI Shadow` / `Small` do figlet):
|
|
8
|
+
*
|
|
9
|
+
* npx figlet -f "ANSI Shadow" redmine-context
|
|
10
|
+
* npx figlet -f "ANSI Shadow" redmine ; npx figlet -f "ANSI Shadow" context
|
|
11
|
+
* npx figlet -f Small redmine-context
|
|
12
|
+
*
|
|
13
|
+
* A seleção é PURA ({@link selectBanner}) e depende de dois sinais que a TUI já
|
|
14
|
+
* possui: a largura do terminal (`../hooks/use-terminal-width.js`) e o suporte a
|
|
15
|
+
* Unicode (`../glyphs.js`) — o mesmo sinal que degrada os frames braille do
|
|
16
|
+
* spinner no terminal legado do Windows (M5-09, #84). Sem essa degradação, os
|
|
17
|
+
* blocos `█`/`╗` do ANSI Shadow virariam mojibake.
|
|
18
|
+
*/
|
|
19
|
+
/** Variante escolhida por {@link selectBanner}. */
|
|
20
|
+
export type BannerVariant = 'wide' | 'stacked' | 'ascii' | 'plain';
|
|
21
|
+
/**
|
|
22
|
+
* Escolhe a variante do banner para o terminal atual.
|
|
23
|
+
*
|
|
24
|
+
* Ordem de preferência, sempre respeitando a largura disponível:
|
|
25
|
+
* `wide` (uma linha, Unicode) → `stacked` (duas linhas, Unicode) → `ascii`
|
|
26
|
+
* (ASCII puro) → `plain` (sem arte; a tela cai no nome em texto).
|
|
27
|
+
*
|
|
28
|
+
* @param width - Colunas disponíveis (ver `useTerminalWidth`).
|
|
29
|
+
* @param unicode - `true` quando o terminal renderiza os blocos do ANSI Shadow.
|
|
30
|
+
* @returns A variante a renderizar.
|
|
31
|
+
* @example
|
|
32
|
+
* selectBanner(140, true) // 'wide'
|
|
33
|
+
* selectBanner(80, true) // 'stacked' (não cabe a wide)
|
|
34
|
+
* selectBanner(80, false) // 'ascii' (sem Unicode)
|
|
35
|
+
*/
|
|
36
|
+
export declare function selectBanner(width: number, unicode: boolean): BannerVariant;
|
|
37
|
+
/**
|
|
38
|
+
* Linhas da variante escolhida.
|
|
39
|
+
*
|
|
40
|
+
* @param variant - Variante de {@link selectBanner}.
|
|
41
|
+
* @returns As linhas da arte; vazio para `plain`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function bannerLines(variant: BannerVariant): readonly string[];
|