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,6 +16,7 @@
|
|
|
16
16
|
* Mantém a fronteira do ADR-005: encapsula os módulos internos (`client`,
|
|
17
17
|
* `bundle`) para que a superfície permaneça fina e sem acesso a URL/host.
|
|
18
18
|
*/
|
|
19
|
+
import { type SearchListItem } from './bundle/index.js';
|
|
19
20
|
/** Limite default de resultados quando a superfície não informa `limit`. */
|
|
20
21
|
export declare const SEARCH_DEFAULT_LIMIT = 25;
|
|
21
22
|
/**
|
|
@@ -51,6 +52,15 @@ export interface FetchIssueSearchOptions {
|
|
|
51
52
|
export interface IssueSearchResult {
|
|
52
53
|
/** Lista compacta em Markdown pronta para o CallToolResult. */
|
|
53
54
|
content: string;
|
|
55
|
+
/**
|
|
56
|
+
* Os mesmos itens em forma ESTRUTURADA.
|
|
57
|
+
*
|
|
58
|
+
* O `content` é Markdown com fences `<untrusted-content>` — marcação
|
|
59
|
+
* anti prompt-injection destinada ao LLM. Uma interface que o exiba mostra
|
|
60
|
+
* essas tags ao usuário, que é ruído: a TUI renderiza a partir daqui e aplica
|
|
61
|
+
* sua própria apresentação.
|
|
62
|
+
*/
|
|
63
|
+
items: readonly SearchListItem[];
|
|
54
64
|
/** Número de itens retornados. */
|
|
55
65
|
count: number;
|
|
56
66
|
/** Avisos de degradação (ex.: `/search` indisponível). Vazio no caminho feliz. */
|
|
@@ -116,5 +116,5 @@ export async function fetchIssueSearch(options) {
|
|
|
116
116
|
}
|
|
117
117
|
const items = payloads.slice(0, limit).map(toSearchListItem);
|
|
118
118
|
const content = buildSearchListMarkdown(items, { query, warnings });
|
|
119
|
-
return { content, count: items.length, warnings, degraded };
|
|
119
|
+
return { content, items, count: items.length, warnings, degraded };
|
|
120
120
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orquestração "últimas issues" — tool MCP `get_last` e comando CLI `last`.
|
|
3
|
+
*
|
|
4
|
+
* Função fina do core reutilizável pelas superfícies (CLI e MCP): lista as
|
|
5
|
+
* issues mais recentes segundo uma ORDEM escolhida e devolve o BUNDLE COMPLETO
|
|
6
|
+
* de cada uma — o atalho de um passo para "me dá a última issue", sem exigir
|
|
7
|
+
* que o chamador descubra o id antes.
|
|
8
|
+
*
|
|
9
|
+
* Diferença para `fetchIssueSearch`: aquela responde "quais issues casam com
|
|
10
|
+
* estes filtros?" com uma lista compacta; esta responde "qual é a mais recente?"
|
|
11
|
+
* com o contexto inteiro (descrição, histórico, custom fields, anexos, relações).
|
|
12
|
+
*
|
|
13
|
+
* Ordem (parâmetro, com default):
|
|
14
|
+
* - `updated` → `sort=updated_on:desc` (DEFAULT — "no que se mexeu por último")
|
|
15
|
+
* - `created` → `sort=created_on:desc` ("o que entrou de novo", triagem)
|
|
16
|
+
* - `priority` → `sort=priority:desc,updated_on:desc` ("o que é mais urgente",
|
|
17
|
+
* desempatando pela mais recente)
|
|
18
|
+
*
|
|
19
|
+
* ESCOPO: não passa `status_id`, então vale o default do Redmine em
|
|
20
|
+
* `/issues.json` — apenas issues ABERTAS. Quem precisa de fechadas/filtros usa
|
|
21
|
+
* `fetchIssueSearch`, que expõe `status_id` e os demais filtros estruturados.
|
|
22
|
+
*
|
|
23
|
+
* Mantém a fronteira do ADR-005: encapsula os módulos internos (`client`) e
|
|
24
|
+
* reutiliza `fetchIssueBundle` para o empacotamento, sem duplicar a pipeline.
|
|
25
|
+
*/
|
|
26
|
+
import type { CoreEvent } from './contract.js';
|
|
27
|
+
import { type BundleFormat } from './fetch-issue-bundle.js';
|
|
28
|
+
/** Critério de ordenação aceito por {@link fetchLastIssues}. */
|
|
29
|
+
export type LastIssuesOrder = 'updated' | 'created' | 'priority';
|
|
30
|
+
/** Ordem usada quando a superfície não informa `order`. */
|
|
31
|
+
export declare const LAST_DEFAULT_ORDER: LastIssuesOrder;
|
|
32
|
+
/** Quantidade default de issues: só a última. */
|
|
33
|
+
export declare const LAST_DEFAULT_COUNT = 1;
|
|
34
|
+
/**
|
|
35
|
+
* Teto de issues por chamada. Baixo de propósito: cada item é um bundle
|
|
36
|
+
* COMPLETO (histórico + anexos), então o custo em tokens/latência cresce rápido.
|
|
37
|
+
* Quem quer uma visão ampla usa `fetchIssueSearch` (lista compacta).
|
|
38
|
+
*/
|
|
39
|
+
export declare const LAST_MAX_COUNT = 5;
|
|
40
|
+
/** Opções de {@link fetchLastIssues}: credenciais já resolvidas + ordem + formato. */
|
|
41
|
+
export interface FetchLastIssuesOptions {
|
|
42
|
+
/** URL base da instância Redmine (ex.: `https://redmine.example`). */
|
|
43
|
+
baseUrl: string;
|
|
44
|
+
/** api_key já resolvida pela cascata da superfície. */
|
|
45
|
+
apiKey: string;
|
|
46
|
+
/** Critério de ordenação. Default: {@link LAST_DEFAULT_ORDER}. */
|
|
47
|
+
order?: LastIssuesOrder | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Quantas issues empacotar. Default: {@link LAST_DEFAULT_COUNT}. Valores fora
|
|
50
|
+
* de `[1, LAST_MAX_COUNT]` são reduzidos ao intervalo (sem erro).
|
|
51
|
+
*/
|
|
52
|
+
count?: number | undefined;
|
|
53
|
+
/** Formato de saída de cada bundle. */
|
|
54
|
+
format: BundleFormat;
|
|
55
|
+
/** Versão da ferramenta gravada nos bundles. */
|
|
56
|
+
toolVersion: string;
|
|
57
|
+
/** Permite `http://` (sem TLS) com aviso ruidoso. Default: `false`. */
|
|
58
|
+
insecure?: boolean | undefined;
|
|
59
|
+
/** Extrai o texto (OCR) dos anexos e o embute nos bundles. Default: `false`. */
|
|
60
|
+
extractAttachments?: boolean | undefined;
|
|
61
|
+
/** Modo cache-first não-bloqueante (M4-11) repassado a `fetchIssueBundle`. */
|
|
62
|
+
cacheFirst?: boolean | undefined;
|
|
63
|
+
/** Raiz do cache em disco repassada a `fetchIssueBundle`. */
|
|
64
|
+
cacheDir?: string | undefined;
|
|
65
|
+
}
|
|
66
|
+
/** Resultado final: bundles das últimas issues, prontos para stdout/CallToolResult. */
|
|
67
|
+
export interface LastIssuesResult {
|
|
68
|
+
/** Ids empacotados, na ordem de saída (mais recente primeiro). */
|
|
69
|
+
issueIds: number[];
|
|
70
|
+
/** Ordem efetivamente aplicada (útil quando a superfície omitiu `order`). */
|
|
71
|
+
order: LastIssuesOrder;
|
|
72
|
+
/** Formato efetivo do conteúdo. */
|
|
73
|
+
format: BundleFormat;
|
|
74
|
+
/**
|
|
75
|
+
* Bundles serializados. Em `md`, concatenados e separados por `---`. Em
|
|
76
|
+
* `json`, SEMPRE um array JSON válido (inclusive com um único item ou nenhum)
|
|
77
|
+
* — `get_last` devolve uma coleção, diferente de `fetchIssueBundle`, que
|
|
78
|
+
* devolve o bundle de uma issue.
|
|
79
|
+
*/
|
|
80
|
+
content: string;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Lista as issues mais recentes e empacota o bundle completo de cada uma.
|
|
84
|
+
*
|
|
85
|
+
* @param options - Ver {@link FetchLastIssuesOptions}.
|
|
86
|
+
* @returns Sequência de {@link ProgressEvent} terminada por um {@link Result}
|
|
87
|
+
* com o {@link LastIssuesResult}.
|
|
88
|
+
* @throws {RedmineAuthError} Em 401 (propagado do client).
|
|
89
|
+
* @throws {RedmineHttpError} Em outros status ≥ 400.
|
|
90
|
+
* @throws {Error} Em falha de rede/TLS/JSON inválido.
|
|
91
|
+
* @example
|
|
92
|
+
* for await (const event of fetchLastIssues({ ...creds, order: 'priority', format: 'md' })) {
|
|
93
|
+
* if (event.kind === 'result') process.stdout.write(event.value.content);
|
|
94
|
+
* }
|
|
95
|
+
*/
|
|
96
|
+
export declare function fetchLastIssues(options: FetchLastIssuesOptions): AsyncIterable<CoreEvent<LastIssuesResult>>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orquestração "últimas issues" — tool MCP `get_last` e comando CLI `last`.
|
|
3
|
+
*
|
|
4
|
+
* Função fina do core reutilizável pelas superfícies (CLI e MCP): lista as
|
|
5
|
+
* issues mais recentes segundo uma ORDEM escolhida e devolve o BUNDLE COMPLETO
|
|
6
|
+
* de cada uma — o atalho de um passo para "me dá a última issue", sem exigir
|
|
7
|
+
* que o chamador descubra o id antes.
|
|
8
|
+
*
|
|
9
|
+
* Diferença para `fetchIssueSearch`: aquela responde "quais issues casam com
|
|
10
|
+
* estes filtros?" com uma lista compacta; esta responde "qual é a mais recente?"
|
|
11
|
+
* com o contexto inteiro (descrição, histórico, custom fields, anexos, relações).
|
|
12
|
+
*
|
|
13
|
+
* Ordem (parâmetro, com default):
|
|
14
|
+
* - `updated` → `sort=updated_on:desc` (DEFAULT — "no que se mexeu por último")
|
|
15
|
+
* - `created` → `sort=created_on:desc` ("o que entrou de novo", triagem)
|
|
16
|
+
* - `priority` → `sort=priority:desc,updated_on:desc` ("o que é mais urgente",
|
|
17
|
+
* desempatando pela mais recente)
|
|
18
|
+
*
|
|
19
|
+
* ESCOPO: não passa `status_id`, então vale o default do Redmine em
|
|
20
|
+
* `/issues.json` — apenas issues ABERTAS. Quem precisa de fechadas/filtros usa
|
|
21
|
+
* `fetchIssueSearch`, que expõe `status_id` e os demais filtros estruturados.
|
|
22
|
+
*
|
|
23
|
+
* Mantém a fronteira do ADR-005: encapsula os módulos internos (`client`) e
|
|
24
|
+
* reutiliza `fetchIssueBundle` para o empacotamento, sem duplicar a pipeline.
|
|
25
|
+
*/
|
|
26
|
+
import { createHttpClient, listIssues } from './client/index.js';
|
|
27
|
+
import { fetchIssueBundle } from './fetch-issue-bundle.js';
|
|
28
|
+
/** Ordem usada quando a superfície não informa `order`. */
|
|
29
|
+
export const LAST_DEFAULT_ORDER = 'updated';
|
|
30
|
+
/** Quantidade default de issues: só a última. */
|
|
31
|
+
export const LAST_DEFAULT_COUNT = 1;
|
|
32
|
+
/**
|
|
33
|
+
* Teto de issues por chamada. Baixo de propósito: cada item é um bundle
|
|
34
|
+
* COMPLETO (histórico + anexos), então o custo em tokens/latência cresce rápido.
|
|
35
|
+
* Quem quer uma visão ampla usa `fetchIssueSearch` (lista compacta).
|
|
36
|
+
*/
|
|
37
|
+
export const LAST_MAX_COUNT = 5;
|
|
38
|
+
/** Tradução ordem → parâmetro `sort` do `/issues.json`. */
|
|
39
|
+
const SORT_BY_ORDER = {
|
|
40
|
+
updated: 'updated_on:desc',
|
|
41
|
+
created: 'created_on:desc',
|
|
42
|
+
// Desempate explícito: mesma prioridade → a mexida mais recente vem primeiro.
|
|
43
|
+
priority: 'priority:desc,updated_on:desc',
|
|
44
|
+
};
|
|
45
|
+
/** Separador entre bundles Markdown consecutivos. */
|
|
46
|
+
const MARKDOWN_SEPARATOR = '\n\n---\n\n';
|
|
47
|
+
/** Conteúdo devolvido quando a instância não tem nenhuma issue aberta. */
|
|
48
|
+
const EMPTY_MARKDOWN = '_(nenhuma issue encontrada)_';
|
|
49
|
+
/** Helper: constrói um evento de progresso tipado. */
|
|
50
|
+
function progress(stage, message) {
|
|
51
|
+
return { kind: 'progress', stage, message };
|
|
52
|
+
}
|
|
53
|
+
/** Reduz `count` ao intervalo `[1, LAST_MAX_COUNT]`. */
|
|
54
|
+
function clampCount(count) {
|
|
55
|
+
if (count === undefined || !Number.isFinite(count))
|
|
56
|
+
return LAST_DEFAULT_COUNT;
|
|
57
|
+
return Math.min(Math.max(Math.trunc(count), 1), LAST_MAX_COUNT);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Lista as issues mais recentes e empacota o bundle completo de cada uma.
|
|
61
|
+
*
|
|
62
|
+
* @param options - Ver {@link FetchLastIssuesOptions}.
|
|
63
|
+
* @returns Sequência de {@link ProgressEvent} terminada por um {@link Result}
|
|
64
|
+
* com o {@link LastIssuesResult}.
|
|
65
|
+
* @throws {RedmineAuthError} Em 401 (propagado do client).
|
|
66
|
+
* @throws {RedmineHttpError} Em outros status ≥ 400.
|
|
67
|
+
* @throws {Error} Em falha de rede/TLS/JSON inválido.
|
|
68
|
+
* @example
|
|
69
|
+
* for await (const event of fetchLastIssues({ ...creds, order: 'priority', format: 'md' })) {
|
|
70
|
+
* if (event.kind === 'result') process.stdout.write(event.value.content);
|
|
71
|
+
* }
|
|
72
|
+
*/
|
|
73
|
+
export async function* fetchLastIssues(options) {
|
|
74
|
+
const { baseUrl, apiKey, format, toolVersion, insecure = false } = options;
|
|
75
|
+
const order = options.order ?? LAST_DEFAULT_ORDER;
|
|
76
|
+
const count = clampCount(options.count);
|
|
77
|
+
yield progress('connect', `Conectando a ${baseUrl}`);
|
|
78
|
+
const http = createHttpClient({ baseUrl, apiKey, insecure });
|
|
79
|
+
yield progress('list', `Listando as ${count} issue(s) mais recentes (ordem: ${order})`);
|
|
80
|
+
const filters = { sort: SORT_BY_ORDER[order] };
|
|
81
|
+
const payloads = await listIssues(http, { filters, pageSize: count, maxItems: count });
|
|
82
|
+
const issueIds = payloads.map((payload) => payload.id);
|
|
83
|
+
if (issueIds.length === 0) {
|
|
84
|
+
const empty = {
|
|
85
|
+
kind: 'result',
|
|
86
|
+
value: { issueIds, order, format, content: format === 'json' ? '[]' : EMPTY_MARKDOWN },
|
|
87
|
+
};
|
|
88
|
+
yield empty;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Reuso da pipeline de bundle (uma issue por vez), repassando o progresso de
|
|
92
|
+
// cada uma para a superfície — nada de duplicar get → normalize → bundle.
|
|
93
|
+
const contents = [];
|
|
94
|
+
for (const issueId of issueIds) {
|
|
95
|
+
for await (const event of fetchIssueBundle({
|
|
96
|
+
baseUrl,
|
|
97
|
+
apiKey,
|
|
98
|
+
issueId,
|
|
99
|
+
format,
|
|
100
|
+
toolVersion,
|
|
101
|
+
insecure,
|
|
102
|
+
extractAttachments: options.extractAttachments ?? false,
|
|
103
|
+
...(options.cacheFirst !== undefined ? { cacheFirst: options.cacheFirst } : {}),
|
|
104
|
+
...(options.cacheDir !== undefined ? { cacheDir: options.cacheDir } : {}),
|
|
105
|
+
})) {
|
|
106
|
+
if (event.kind === 'progress') {
|
|
107
|
+
yield event;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
contents.push(event.value.content);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const content = format === 'json' ? `[${contents.join(',')}]` : contents.join(MARKDOWN_SEPARATOR);
|
|
115
|
+
const result = {
|
|
116
|
+
kind: 'result',
|
|
117
|
+
value: { issueIds, order, format, content },
|
|
118
|
+
};
|
|
119
|
+
yield result;
|
|
120
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
export declare const TOOL_NAME = "redmine-context";
|
|
2
|
-
|
|
2
|
+
/** Versão da ferramenta, sempre igual à `version` do package.json. */
|
|
3
|
+
export declare const TOOL_VERSION: string;
|
|
3
4
|
export * from './contract.js';
|
|
4
5
|
export { fetchIssueBundle, type BundleFormat, type FetchIssueBundleOptions, type IssueBundleResult, } from './fetch-issue-bundle.js';
|
|
5
6
|
export { extractIssueAttachments, type ExtractIssueAttachmentsOptions, } from './extract-issue-attachments.js';
|
|
6
7
|
export { fetchAttachmentText, fetchAttachmentTextCacheFirst, AttachmentNotFoundError, type FetchAttachmentTextOptions, type FetchAttachmentTextCacheFirstOptions, type AttachmentTextResult, } from './fetch-attachment-text.js';
|
|
7
8
|
export { extractIssueAttachmentsCacheFirst, makeQueueBackgroundExtractor, processingResult, type BackgroundExtractionTarget, type BackgroundExtractor, type BackgroundCompute, type CacheFirstExtractionOptions, type QueueBackgroundOptions, } from './cache-first.js';
|
|
8
9
|
export { fetchIssueSearch, SEARCH_DEFAULT_LIMIT, type FetchIssueSearchOptions, type IssueSearchFilters, type IssueSearchResult, } from './fetch-issue-search.js';
|
|
10
|
+
export type { SearchListItem } from './bundle/index.js';
|
|
9
11
|
export { searchIssues, type SearchIssuesOptions, type SearchIssuesPage } from './client/index.js';
|
|
12
|
+
export { fetchLastIssues, LAST_DEFAULT_ORDER, LAST_DEFAULT_COUNT, LAST_MAX_COUNT, type LastIssuesOrder, type FetchLastIssuesOptions, type LastIssuesResult, } from './fetch-last-issues.js';
|
|
10
13
|
export { createHttpClient, type HttpClient, type HttpClientOptions, type QueryParams, } from './client/index.js';
|
|
11
14
|
export { listIssues, type ListIssuesOptions, type RedmineIssuePayload } from './client/index.js';
|
|
12
15
|
export { getIssue } from './client/index.js';
|
|
13
16
|
export { normalizeIssue } from './normalize/index.js';
|
|
14
17
|
export { buildMarkdownBundle, buildJsonBundle, fenceBlock, type MarkdownBundleMeta, type JsonBundle, type JsonBundleEnvelope, type JsonBundleMeta, type JsonBundleSource, } from './bundle/index.js';
|
|
18
|
+
export { collectUsers, journalDetailLabel, journalDetailValue, type DetailLookups, type DetailPart, } from './bundle/journal-detail.js';
|
|
19
|
+
export { fetchEnumerations, clearEnumerationsCache, type RedmineEnumerations, } from './client/index.js';
|
|
15
20
|
export { RedmineHttpError, RedmineAuthError, RedmineForbiddenError, RedmineNotFoundError, } from './client/index.js';
|
|
16
21
|
export { loginWithPassword, validateApiKey, RedmineLoginError, type LoginOptions, type LoginResult, type ValidateApiKeyOptions, } from './config/index.js';
|
|
17
22
|
export { createCredentialCascade, resolveApiKey, describeCredentialSource, normalizeInstanceUrl, CredentialStoreError, type CredentialCascadeOptions, type CredentialSourceKind, } from './config/index.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
1
2
|
export const TOOL_NAME = 'redmine-context';
|
|
2
|
-
//
|
|
3
|
-
|
|
3
|
+
// A versão é LIDA do package.json, nunca copiada à mão: o bump do changesets
|
|
4
|
+
// altera só o manifesto, e uma constante literal aqui ficava para trás a cada
|
|
5
|
+
// release (foi o que aconteceu no 1.0.0, com o gate de empacotamento reprovando).
|
|
6
|
+
// `../package.json` resolve tanto de `src/` (dev/testes) quanto de `dist/` (pacote
|
|
7
|
+
// publicado — o npm sempre inclui o manifesto no tarball).
|
|
8
|
+
const requireJson = createRequire(import.meta.url);
|
|
9
|
+
const pkg = requireJson('../package.json');
|
|
10
|
+
/** Versão da ferramenta, sempre igual à `version` do package.json. */
|
|
11
|
+
export const TOOL_VERSION = pkg.version;
|
|
4
12
|
// Superfície pública do core: contrato de tipos + padrão de progresso (ADR-005).
|
|
5
13
|
// As superfícies devem consumir o core somente por aqui / por ./contract.js.
|
|
6
14
|
export * from './contract.js';
|
|
@@ -22,6 +30,10 @@ export { extractIssueAttachmentsCacheFirst, makeQueueBackgroundExtractor, proces
|
|
|
22
30
|
export { fetchIssueSearch, SEARCH_DEFAULT_LIMIT, } from './fetch-issue-search.js';
|
|
23
31
|
// Primitiva full-text `/search.json` (usada pela orquestração acima).
|
|
24
32
|
export { searchIssues } from './client/index.js';
|
|
33
|
+
// Orquestração "últimas issues" (tool MCP `get_last` e comando CLI `last`):
|
|
34
|
+
// ordena por updated/created/priority e devolve o BUNDLE COMPLETO das mais
|
|
35
|
+
// recentes — atalho de um passo, sem exigir o id antes.
|
|
36
|
+
export { fetchLastIssues, LAST_DEFAULT_ORDER, LAST_DEFAULT_COUNT, LAST_MAX_COUNT, } from './fetch-last-issues.js';
|
|
25
37
|
// Client HTTP base (auth por api_key + retry) — usado por telas que precisam
|
|
26
38
|
// montar suas próprias chamadas ao core sem uma orquestração pronta (ex.: a
|
|
27
39
|
// home da TUI, #29, que lista "minhas issues" via `listIssues` abaixo).
|
|
@@ -46,6 +58,14 @@ export { normalizeIssue } from './normalize/index.js';
|
|
|
46
58
|
// #31), então gravar o bundle não precisa refazer a busca+normalização via
|
|
47
59
|
// `fetchIssueBundle` — só empacotar o que já foi carregado.
|
|
48
60
|
export { buildMarkdownBundle, buildJsonBundle, fenceBlock, } from './bundle/index.js';
|
|
61
|
+
// Semântica dos journal details (rótulos legíveis + ids resolvidos) — UMA fonte
|
|
62
|
+
// de verdade para o bundle Markdown e para a tela de detalhe da TUI, que antes
|
|
63
|
+
// mostrava os ids crus (`status_id: 12 → 7`). Cada superfície aplica sua própria
|
|
64
|
+
// política de confiança sobre as partes devolvidas.
|
|
65
|
+
export { collectUsers, journalDetailLabel, journalDetailValue, } from './bundle/journal-detail.js';
|
|
66
|
+
// Enumerações da instância (`id → nome`) — resolvem os ids HISTÓRICOS que o
|
|
67
|
+
// estado atual da issue não cobre. Memoizado por instância; degrada em silêncio.
|
|
68
|
+
export { fetchEnumerations, clearEnumerationsCache, } from './client/index.js';
|
|
49
69
|
// Erros HTTP tipados — usados pelas superfícies para mapear exit codes (ADR-005).
|
|
50
70
|
export { RedmineHttpError, RedmineAuthError, RedmineForbiddenError, RedmineNotFoundError, } from './client/index.js';
|
|
51
71
|
// Login por senha (M1-07) e cascata de credenciais (M1-08) para as superfícies.
|
package/dist/normalize/issue.js
CHANGED
|
@@ -13,6 +13,22 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { normalizeChildren, normalizeCustomFields, normalizeParent, normalizeRelations, normalizeWatchers, } from './collections.js';
|
|
15
15
|
import { asArray, asNumber, asRecord, asString, asStringOrNull, isDefined, normalizeRef, } from './helpers.js';
|
|
16
|
+
/**
|
|
17
|
+
* Normaliza quebras de linha para `\n`.
|
|
18
|
+
*
|
|
19
|
+
* O Redmine devolve texto com CRLF (o editor web grava assim). Um `\r` que
|
|
20
|
+
* sobrevive até a renderização é ATIVO, não decorativo: no terminal ele devolve
|
|
21
|
+
* o cursor ao início da linha e o texto seguinte SOBRESCREVE o anterior — foi o
|
|
22
|
+
* que comia o começo dos parágrafos na tela de detalhe ("Solicita-se que a
|
|
23
|
+
* área..." aparecia como "que a área..."). Em Markdown/JSON o `\r` também é
|
|
24
|
+
* ruído que não representa nada do conteúdo.
|
|
25
|
+
*
|
|
26
|
+
* @param text - Texto derivado do Redmine (descrição, nota de journal).
|
|
27
|
+
* @returns O mesmo texto com `\r\n` e `\r` isolados convertidos em `\n`.
|
|
28
|
+
*/
|
|
29
|
+
function normalizeNewlines(text) {
|
|
30
|
+
return text.replace(/\r\n?/g, '\n');
|
|
31
|
+
}
|
|
16
32
|
/** Ref placeholder para campos obrigatórios do contrato ausentes no payload. */
|
|
17
33
|
// Congelado: instância compartilhada entre todas as issues degradadas — mutação
|
|
18
34
|
// acidental corromperia o placeholder globalmente (modo strict lança).
|
|
@@ -62,7 +78,7 @@ function normalizeJournal(value) {
|
|
|
62
78
|
// notes só é significativa quando não-vazia (Redmine devolve "" em journal de detalhe).
|
|
63
79
|
const notes = asString(record.notes);
|
|
64
80
|
if (notes !== undefined && notes !== '')
|
|
65
|
-
journal.notes = notes;
|
|
81
|
+
journal.notes = normalizeNewlines(notes);
|
|
66
82
|
const user = normalizeRef(record.user);
|
|
67
83
|
if (user !== undefined)
|
|
68
84
|
journal.user = user;
|
|
@@ -139,11 +155,24 @@ export function normalizeIssue(payload, fieldFormats) {
|
|
|
139
155
|
children: normalizeChildren(record.children),
|
|
140
156
|
};
|
|
141
157
|
const description = asString(record.description);
|
|
142
|
-
if (description !== undefined && description !== '')
|
|
143
|
-
issue.description = description;
|
|
158
|
+
if (description !== undefined && description !== '') {
|
|
159
|
+
issue.description = normalizeNewlines(description);
|
|
160
|
+
}
|
|
144
161
|
const assignedTo = normalizeRef(record.assigned_to);
|
|
145
162
|
if (assignedTo !== undefined)
|
|
146
163
|
issue.assigned_to = assignedTo;
|
|
164
|
+
// Planejamento (contrato: done_ratio/start_date/due_date). O Redmine devolve
|
|
165
|
+
// `null` nas datas não preenchidas — `asString` já as descarta, mantendo o
|
|
166
|
+
// campo AUSENTE em vez de vazio, como o resto do normalize.
|
|
167
|
+
const doneRatio = asNumber(record.done_ratio);
|
|
168
|
+
if (doneRatio !== undefined)
|
|
169
|
+
issue.done_ratio = doneRatio;
|
|
170
|
+
const startDate = asString(record.start_date);
|
|
171
|
+
if (startDate !== undefined && startDate !== '')
|
|
172
|
+
issue.start_date = startDate;
|
|
173
|
+
const dueDate = asString(record.due_date);
|
|
174
|
+
if (dueDate !== undefined && dueDate !== '')
|
|
175
|
+
issue.due_date = dueDate;
|
|
147
176
|
const parent = normalizeParent(record.parent);
|
|
148
177
|
if (parent !== undefined)
|
|
149
178
|
issue.parent = parent;
|
|
@@ -39,6 +39,23 @@ export declare function exitCodeForError(error: unknown): number;
|
|
|
39
39
|
* @returns Exit code do processo.
|
|
40
40
|
*/
|
|
41
41
|
export declare function runIssue(parsed: ParsedArgs, deps: RunDeps): Promise<number>;
|
|
42
|
+
/**
|
|
43
|
+
* Comando `last`: emite o bundle completo das issues mais recentes, sem exigir
|
|
44
|
+
* que o usuário saiba o id.
|
|
45
|
+
*
|
|
46
|
+
* A ordem é um parâmetro com default: `updated` (mexida mais recente), `created`
|
|
47
|
+
* (entrada mais recente) ou `priority` (mais urgente, desempatando pela mais
|
|
48
|
+
* recente). Considera apenas issues ABERTAS — o `search_issues` do MCP é a
|
|
49
|
+
* superfície com filtros (projeto, status, responsável).
|
|
50
|
+
*
|
|
51
|
+
* Sem `--out`: o conteúdo (potencialmente vários bundles) vai para stdout, e o
|
|
52
|
+
* progresso para stderr, como no `issue`.
|
|
53
|
+
*
|
|
54
|
+
* @param parsed - Argumentos parseados (`--order`, `--count`, `--json`, `--extract`).
|
|
55
|
+
* @param deps - Dependências injetáveis (I/O, env).
|
|
56
|
+
* @returns Exit code do processo.
|
|
57
|
+
*/
|
|
58
|
+
export declare function runLast(parsed: ParsedArgs, deps: RunDeps): Promise<number>;
|
|
42
59
|
/**
|
|
43
60
|
* Comando `doctor`: diagnostica os binários de mídia (hoje o `tesseract`) e
|
|
44
61
|
* imprime um relatório em TEXTO PURO no stdout. Degrada naturalmente em
|
|
@@ -55,20 +55,17 @@ function stringFlag(parsed, name) {
|
|
|
55
55
|
return typeof value === 'string' ? value : undefined;
|
|
56
56
|
}
|
|
57
57
|
/**
|
|
58
|
-
*
|
|
59
|
-
* bundle (stdout ou `--out <dir>`), com progresso em stderr.
|
|
58
|
+
* Resolve instância e credencial para os comandos que falam com o Redmine.
|
|
60
59
|
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
60
|
+
* Ponto ÚNICO dessa cascata na CLI: `issue` e `last` a compartilham para que a
|
|
61
|
+
* regra de segurança do `allowEnvFallback` (#187) não possa divergir entre
|
|
62
|
+
* comandos.
|
|
63
|
+
*
|
|
64
|
+
* @param parsed - Argumentos parseados (usa `--url` e `--insecure`).
|
|
65
|
+
* @param deps - Dependências injetáveis (I/O, env, settings).
|
|
66
|
+
* @returns A instância resolvida, ou o exit code a devolver em caso de falha.
|
|
64
67
|
*/
|
|
65
|
-
|
|
66
|
-
const idRaw = parsed.positionals[1];
|
|
67
|
-
const issueId = Number(idRaw);
|
|
68
|
-
if (idRaw === undefined || !Number.isInteger(issueId) || issueId <= 0) {
|
|
69
|
-
deps.stderr(`Id de issue inválido: ${idRaw ?? '(ausente)'}. Uso: redmine-context issue <id>\n`);
|
|
70
|
-
return EXIT.GENERIC;
|
|
71
|
-
}
|
|
68
|
+
async function resolveInstanceAndKey(parsed, deps) {
|
|
72
69
|
// Instância: --url → REDMINE_URL → URL persistida no login (#187).
|
|
73
70
|
const persistedUrl = deps.settings ? await deps.settings.getInstanceUrl() : undefined;
|
|
74
71
|
const resolved = core.resolveInstanceUrl({
|
|
@@ -102,6 +99,27 @@ export async function runIssue(parsed, deps) {
|
|
|
102
99
|
deps.stderr(`Nenhuma credencial encontrada para ${baseUrl}.\nRode: redmine-context login\n`);
|
|
103
100
|
return EXIT.AUTH;
|
|
104
101
|
}
|
|
102
|
+
return { baseUrl, apiKey, insecure };
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Comando `issue <id>`: resolve credencial pela cascata, empacota e emite o
|
|
106
|
+
* bundle (stdout ou `--out <dir>`), com progresso em stderr.
|
|
107
|
+
*
|
|
108
|
+
* @param parsed - Argumentos parseados (posicional `<id>` + flags).
|
|
109
|
+
* @param deps - Dependências injetáveis (I/O, env).
|
|
110
|
+
* @returns Exit code do processo.
|
|
111
|
+
*/
|
|
112
|
+
export async function runIssue(parsed, deps) {
|
|
113
|
+
const idRaw = parsed.positionals[1];
|
|
114
|
+
const issueId = Number(idRaw);
|
|
115
|
+
if (idRaw === undefined || !Number.isInteger(issueId) || issueId <= 0) {
|
|
116
|
+
deps.stderr(`Id de issue inválido: ${idRaw ?? '(ausente)'}. Uso: redmine-context issue <id>\n`);
|
|
117
|
+
return EXIT.GENERIC;
|
|
118
|
+
}
|
|
119
|
+
const instance = await resolveInstanceAndKey(parsed, deps);
|
|
120
|
+
if (typeof instance === 'number')
|
|
121
|
+
return instance;
|
|
122
|
+
const { baseUrl, apiKey, insecure } = instance;
|
|
105
123
|
const format = parsed.flags.get('json') === true ? 'json' : 'md';
|
|
106
124
|
const outDir = stringFlag(parsed, 'out');
|
|
107
125
|
// --extract liga a extração de texto dos anexos (OCR) no bundle (M3-13).
|
|
@@ -145,6 +163,81 @@ export async function runIssue(parsed, deps) {
|
|
|
145
163
|
return exitCodeForError(error);
|
|
146
164
|
}
|
|
147
165
|
}
|
|
166
|
+
/** Ordens aceitas pelo `--order` do comando `last` (espelha `LastIssuesOrder`). */
|
|
167
|
+
const LAST_ORDERS = ['updated', 'created', 'priority'];
|
|
168
|
+
/** Type guard de `--order` contra {@link LAST_ORDERS}. */
|
|
169
|
+
function isLastOrder(value) {
|
|
170
|
+
return LAST_ORDERS.includes(value);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Comando `last`: emite o bundle completo das issues mais recentes, sem exigir
|
|
174
|
+
* que o usuário saiba o id.
|
|
175
|
+
*
|
|
176
|
+
* A ordem é um parâmetro com default: `updated` (mexida mais recente), `created`
|
|
177
|
+
* (entrada mais recente) ou `priority` (mais urgente, desempatando pela mais
|
|
178
|
+
* recente). Considera apenas issues ABERTAS — o `search_issues` do MCP é a
|
|
179
|
+
* superfície com filtros (projeto, status, responsável).
|
|
180
|
+
*
|
|
181
|
+
* Sem `--out`: o conteúdo (potencialmente vários bundles) vai para stdout, e o
|
|
182
|
+
* progresso para stderr, como no `issue`.
|
|
183
|
+
*
|
|
184
|
+
* @param parsed - Argumentos parseados (`--order`, `--count`, `--json`, `--extract`).
|
|
185
|
+
* @param deps - Dependências injetáveis (I/O, env).
|
|
186
|
+
* @returns Exit code do processo.
|
|
187
|
+
*/
|
|
188
|
+
export async function runLast(parsed, deps) {
|
|
189
|
+
const orderRaw = stringFlag(parsed, 'order');
|
|
190
|
+
if (orderRaw !== undefined && !isLastOrder(orderRaw)) {
|
|
191
|
+
deps.stderr(`Ordem inválida: ${orderRaw}. Use: ${LAST_ORDERS.join(', ')}.\n`);
|
|
192
|
+
return EXIT.GENERIC;
|
|
193
|
+
}
|
|
194
|
+
const countRaw = stringFlag(parsed, 'count');
|
|
195
|
+
let count;
|
|
196
|
+
if (countRaw !== undefined) {
|
|
197
|
+
const value = Number(countRaw);
|
|
198
|
+
if (!Number.isInteger(value) || value < 1 || value > core.LAST_MAX_COUNT) {
|
|
199
|
+
deps.stderr(`Valor inválido para --count: ${countRaw}. Use um inteiro entre 1 e ${core.LAST_MAX_COUNT}.\n`);
|
|
200
|
+
return EXIT.GENERIC;
|
|
201
|
+
}
|
|
202
|
+
count = value;
|
|
203
|
+
}
|
|
204
|
+
const instance = await resolveInstanceAndKey(parsed, deps);
|
|
205
|
+
if (typeof instance === 'number')
|
|
206
|
+
return instance;
|
|
207
|
+
const { baseUrl, apiKey, insecure } = instance;
|
|
208
|
+
const format = parsed.flags.get('json') === true ? 'json' : 'md';
|
|
209
|
+
const extractAttachments = parsed.flags.get('extract') === true;
|
|
210
|
+
try {
|
|
211
|
+
let content;
|
|
212
|
+
for await (const event of core.fetchLastIssues({
|
|
213
|
+
baseUrl,
|
|
214
|
+
apiKey,
|
|
215
|
+
format,
|
|
216
|
+
order: orderRaw,
|
|
217
|
+
count,
|
|
218
|
+
toolVersion: core.TOOL_VERSION,
|
|
219
|
+
insecure,
|
|
220
|
+
extractAttachments,
|
|
221
|
+
})) {
|
|
222
|
+
if (event.kind === 'progress') {
|
|
223
|
+
deps.stderr(`... ${event.message}\n`);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
content = event.value.content;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (content === undefined) {
|
|
230
|
+
deps.stderr('Operação não produziu um bundle.\n');
|
|
231
|
+
return EXIT.GENERIC;
|
|
232
|
+
}
|
|
233
|
+
deps.stdout(content);
|
|
234
|
+
return 0;
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
deps.stderr(`${messageOf(error)}\n`);
|
|
238
|
+
return exitCodeForError(error);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
148
241
|
/**
|
|
149
242
|
* Comando `doctor`: diagnostica os binários de mídia (hoje o `tesseract`) e
|
|
150
243
|
* imprime um relatório em TEXTO PURO no stdout. Degrada naturalmente em
|
|
@@ -11,19 +11,20 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { realpathSync } from 'node:fs';
|
|
13
13
|
import { pathToFileURL } from 'node:url';
|
|
14
|
-
import { runDoctor, runIssue, runLogin } from './commands.js';
|
|
14
|
+
import { runDoctor, runIssue, runLast, runLogin } from './commands.js';
|
|
15
15
|
import { createPromptSession } from './prompts.js';
|
|
16
16
|
import { shouldRenderTui } from './tty.js';
|
|
17
17
|
import { TOOL_VERSION, defaultSettingsStore } from '../../index.js';
|
|
18
18
|
import { runStdioServer } from '../mcp/server.js';
|
|
19
19
|
import { runTui } from '../tui/index.js';
|
|
20
20
|
/** Flags que consomem o próximo token como valor (as demais são booleanas). */
|
|
21
|
-
const VALUE_FLAGS = new Set(['out', 'url', 'api-key']);
|
|
21
|
+
const VALUE_FLAGS = new Set(['out', 'url', 'api-key', 'order', 'count']);
|
|
22
22
|
/** Texto do `--help` — limpo, sem referências a ferramentas de IA. */
|
|
23
23
|
const HELP = `redmine-context — contexto completo de issues do Redmine para LLMs
|
|
24
24
|
|
|
25
25
|
Uso:
|
|
26
26
|
redmine-context issue <id> [opções]
|
|
27
|
+
redmine-context last [opções]
|
|
27
28
|
redmine-context login [opções]
|
|
28
29
|
redmine-context doctor
|
|
29
30
|
redmine-context mcp
|
|
@@ -41,6 +42,21 @@ Comando issue:
|
|
|
41
42
|
--url <url> URL da instância Redmine (ou defina REDMINE_URL).
|
|
42
43
|
--insecure Permite http:// sem TLS (não recomendado).
|
|
43
44
|
|
|
45
|
+
Comando last:
|
|
46
|
+
Imprime o bundle completo das issues mais recentes, sem precisar do id.
|
|
47
|
+
Considera apenas issues abertas.
|
|
48
|
+
|
|
49
|
+
--order <ordem> updated (padrão) | created | priority
|
|
50
|
+
updated = mexida mais recente
|
|
51
|
+
created = entrada mais recente (triagem)
|
|
52
|
+
priority = mais urgente, desempatando pela mais recente
|
|
53
|
+
--count <n> Quantas issues empacotar (padrão 1, teto 5). Cada item é um
|
|
54
|
+
bundle completo.
|
|
55
|
+
--json Emite os bundles como um array JSON.
|
|
56
|
+
--extract Extrai o texto (OCR) dos anexos de imagem.
|
|
57
|
+
--url <url> URL da instância Redmine (ou defina REDMINE_URL).
|
|
58
|
+
--insecure Permite http:// sem TLS (não recomendado).
|
|
59
|
+
|
|
44
60
|
Comando login:
|
|
45
61
|
Autentica por usuário/senha e salva a api_key para a instância.
|
|
46
62
|
Em contas com 2FA, cole a api_key quando solicitado (ou use --api-key).
|
|
@@ -57,7 +73,7 @@ Comando doctor:
|
|
|
57
73
|
|
|
58
74
|
Comando mcp:
|
|
59
75
|
Sobe um servidor MCP (stdio) expondo as tools read-only get_issue_context,
|
|
60
|
-
search_issues e
|
|
76
|
+
search_issues, get_attachment_text e get_last. A instância vem de REDMINE_URL +
|
|
61
77
|
credencial da cascata (REDMINE_API_KEY); nenhuma tool aceita URL/host
|
|
62
78
|
arbitrário. Logs vão para stderr.
|
|
63
79
|
|
|
@@ -180,6 +196,9 @@ async function dispatch(argv, deps) {
|
|
|
180
196
|
if (command === 'issue') {
|
|
181
197
|
return runIssue(parsed, deps);
|
|
182
198
|
}
|
|
199
|
+
if (command === 'last') {
|
|
200
|
+
return runLast(parsed, deps);
|
|
201
|
+
}
|
|
183
202
|
if (command === 'login') {
|
|
184
203
|
return runLogin(parsed, deps);
|
|
185
204
|
}
|