redmine-context 1.0.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/LICENSE +21 -0
- package/README.md +449 -0
- package/dist/bundle/index.d.ts +5 -0
- package/dist/bundle/index.js +5 -0
- package/dist/bundle/json.d.ts +90 -0
- package/dist/bundle/json.js +266 -0
- package/dist/bundle/markdown.d.ts +75 -0
- package/dist/bundle/markdown.js +294 -0
- package/dist/bundle/search-list.d.ts +43 -0
- package/dist/bundle/search-list.js +53 -0
- package/dist/bundle/stable-stringify.d.ts +26 -0
- package/dist/bundle/stable-stringify.js +50 -0
- package/dist/cache/contract.d.ts +157 -0
- package/dist/cache/contract.js +0 -0
- package/dist/cache/disk-index.d.ts +82 -0
- package/dist/cache/disk-index.js +220 -0
- package/dist/cache/disk.d.ts +133 -0
- package/dist/cache/disk.js +313 -0
- package/dist/cache/gc.d.ts +78 -0
- package/dist/cache/gc.js +123 -0
- package/dist/cache/get-or-compute.d.ts +36 -0
- package/dist/cache/get-or-compute.js +52 -0
- package/dist/cache/index.d.ts +9 -0
- package/dist/cache/index.js +8 -0
- package/dist/cache/keys.d.ts +76 -0
- package/dist/cache/keys.js +78 -0
- package/dist/cache/memory.d.ts +48 -0
- package/dist/cache/memory.js +110 -0
- package/dist/cache-first.d.ts +127 -0
- package/dist/cache-first.js +227 -0
- package/dist/client/errors.d.ts +33 -0
- package/dist/client/errors.js +49 -0
- package/dist/client/http.d.ts +110 -0
- package/dist/client/http.js +207 -0
- package/dist/client/index.d.ts +5 -0
- package/dist/client/index.js +5 -0
- package/dist/client/issues.d.ts +71 -0
- package/dist/client/issues.js +100 -0
- package/dist/client/search.d.ts +58 -0
- package/dist/client/search.js +81 -0
- package/dist/config/credentials.d.ts +247 -0
- package/dist/config/credentials.js +427 -0
- package/dist/config/doctor.d.ts +123 -0
- package/dist/config/doctor.js +260 -0
- package/dist/config/index.d.ts +6 -0
- package/dist/config/index.js +6 -0
- package/dist/config/keyring.d.ts +96 -0
- package/dist/config/keyring.js +158 -0
- package/dist/config/login.d.ts +97 -0
- package/dist/config/login.js +189 -0
- package/dist/config/settings.d.ts +94 -0
- package/dist/config/settings.js +140 -0
- package/dist/contract.d.ts +173 -0
- package/dist/contract.js +27 -0
- package/dist/core.d.ts +1 -0
- package/dist/core.js +8 -0
- package/dist/extract/audio-extractor.d.ts +105 -0
- package/dist/extract/audio-extractor.js +156 -0
- package/dist/extract/audio.d.ts +126 -0
- package/dist/extract/audio.js +184 -0
- package/dist/extract/dispatcher.d.ts +132 -0
- package/dist/extract/dispatcher.js +115 -0
- package/dist/extract/download.d.ts +111 -0
- package/dist/extract/download.js +261 -0
- package/dist/extract/duration.d.ts +106 -0
- package/dist/extract/duration.js +148 -0
- package/dist/extract/ffmpeg.d.ts +56 -0
- package/dist/extract/ffmpeg.js +95 -0
- package/dist/extract/gguf.d.ts +137 -0
- package/dist/extract/gguf.js +215 -0
- package/dist/extract/index.d.ts +19 -0
- package/dist/extract/index.js +19 -0
- package/dist/extract/magic.d.ts +80 -0
- package/dist/extract/magic.js +282 -0
- package/dist/extract/ooxml.d.ts +131 -0
- package/dist/extract/ooxml.js +336 -0
- package/dist/extract/pdf.d.ts +147 -0
- package/dist/extract/pdf.js +322 -0
- package/dist/extract/queue.d.ts +167 -0
- package/dist/extract/queue.js +217 -0
- package/dist/extract/subprocess.d.ts +145 -0
- package/dist/extract/subprocess.js +181 -0
- package/dist/extract/tesseract.d.ts +153 -0
- package/dist/extract/tesseract.js +321 -0
- package/dist/extract/video-extractor.d.ts +84 -0
- package/dist/extract/video-extractor.js +89 -0
- package/dist/extract/video.d.ts +198 -0
- package/dist/extract/video.js +418 -0
- package/dist/extract/which.d.ts +63 -0
- package/dist/extract/which.js +72 -0
- package/dist/extract/whisper-extract.d.ts +211 -0
- package/dist/extract/whisper-extract.js +323 -0
- package/dist/extract/whisper.d.ts +50 -0
- package/dist/extract/whisper.js +67 -0
- package/dist/extract/zip.d.ts +50 -0
- package/dist/extract/zip.js +165 -0
- package/dist/extract-issue-attachments.d.ts +82 -0
- package/dist/extract-issue-attachments.js +156 -0
- package/dist/fetch-attachment-text.d.ts +113 -0
- package/dist/fetch-attachment-text.js +155 -0
- package/dist/fetch-issue-bundle.d.ts +81 -0
- package/dist/fetch-issue-bundle.js +93 -0
- package/dist/fetch-issue-search.d.ts +74 -0
- package/dist/fetch-issue-search.js +120 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +67 -0
- package/dist/normalize/collections.d.ts +52 -0
- package/dist/normalize/collections.js +170 -0
- package/dist/normalize/helpers.d.ts +35 -0
- package/dist/normalize/helpers.js +59 -0
- package/dist/normalize/index.d.ts +2 -0
- package/dist/normalize/index.js +2 -0
- package/dist/normalize/issue.d.ts +34 -0
- package/dist/normalize/issue.js +154 -0
- package/dist/surfaces/cli/commands.d.ts +61 -0
- package/dist/surfaces/cli/commands.js +262 -0
- package/dist/surfaces/cli/main.d.ts +32 -0
- package/dist/surfaces/cli/main.js +210 -0
- package/dist/surfaces/cli/prompts.d.ts +66 -0
- package/dist/surfaces/cli/prompts.js +147 -0
- package/dist/surfaces/cli/tty.d.ts +27 -0
- package/dist/surfaces/cli/tty.js +35 -0
- package/dist/surfaces/cli/types.d.ts +39 -0
- package/dist/surfaces/cli/types.js +7 -0
- package/dist/surfaces/mcp/server.d.ts +171 -0
- package/dist/surfaces/mcp/server.js +427 -0
- package/dist/surfaces/tui/app.d.ts +55 -0
- package/dist/surfaces/tui/app.js +180 -0
- package/dist/surfaces/tui/attachment-status.d.ts +79 -0
- package/dist/surfaces/tui/attachment-status.js +113 -0
- package/dist/surfaces/tui/components/breadcrumb.d.ts +7 -0
- package/dist/surfaces/tui/components/breadcrumb.js +31 -0
- package/dist/surfaces/tui/components/gradient-text.d.ts +23 -0
- package/dist/surfaces/tui/components/gradient-text.js +75 -0
- package/dist/surfaces/tui/components/scroll-view.d.ts +25 -0
- package/dist/surfaces/tui/components/scroll-view.js +77 -0
- package/dist/surfaces/tui/components/spinner.d.ts +12 -0
- package/dist/surfaces/tui/components/spinner.js +39 -0
- package/dist/surfaces/tui/components/text-input.d.ts +45 -0
- package/dist/surfaces/tui/components/text-input.js +114 -0
- package/dist/surfaces/tui/format-file-size.d.ts +24 -0
- package/dist/surfaces/tui/format-file-size.js +43 -0
- package/dist/surfaces/tui/glyphs.d.ts +47 -0
- package/dist/surfaces/tui/glyphs.js +84 -0
- package/dist/surfaces/tui/hooks/use-auth-guard.d.ts +39 -0
- package/dist/surfaces/tui/hooks/use-auth-guard.js +135 -0
- package/dist/surfaces/tui/hooks/use-doctor-status.d.ts +64 -0
- package/dist/surfaces/tui/hooks/use-doctor-status.js +123 -0
- package/dist/surfaces/tui/hooks/use-escape-interceptor.d.ts +25 -0
- package/dist/surfaces/tui/hooks/use-escape-interceptor.js +65 -0
- package/dist/surfaces/tui/hooks/use-exit-guard.d.ts +18 -0
- package/dist/surfaces/tui/hooks/use-exit-guard.js +66 -0
- package/dist/surfaces/tui/hooks/use-export-bundle.d.ts +62 -0
- package/dist/surfaces/tui/hooks/use-export-bundle.js +100 -0
- package/dist/surfaces/tui/hooks/use-issue-detail.d.ts +61 -0
- package/dist/surfaces/tui/hooks/use-issue-detail.js +132 -0
- package/dist/surfaces/tui/hooks/use-issue-search.d.ts +71 -0
- package/dist/surfaces/tui/hooks/use-issue-search.js +168 -0
- package/dist/surfaces/tui/hooks/use-list-navigation.d.ts +44 -0
- package/dist/surfaces/tui/hooks/use-list-navigation.js +82 -0
- package/dist/surfaces/tui/hooks/use-media-binaries.d.ts +24 -0
- package/dist/surfaces/tui/hooks/use-media-binaries.js +44 -0
- package/dist/surfaces/tui/hooks/use-my-issues.d.ts +66 -0
- package/dist/surfaces/tui/hooks/use-my-issues.js +151 -0
- package/dist/surfaces/tui/hooks/use-onboarding-callbacks.d.ts +11 -0
- package/dist/surfaces/tui/hooks/use-onboarding-callbacks.js +104 -0
- package/dist/surfaces/tui/hooks/use-terminal-width.d.ts +52 -0
- package/dist/surfaces/tui/hooks/use-terminal-width.js +90 -0
- package/dist/surfaces/tui/index.d.ts +50 -0
- package/dist/surfaces/tui/index.js +158 -0
- package/dist/surfaces/tui/instance.d.ts +37 -0
- package/dist/surfaces/tui/instance.js +36 -0
- package/dist/surfaces/tui/job-registry.d.ts +113 -0
- package/dist/surfaces/tui/job-registry.js +123 -0
- package/dist/surfaces/tui/job-status.d.ts +45 -0
- package/dist/surfaces/tui/job-status.js +81 -0
- package/dist/surfaces/tui/navigation.d.ts +74 -0
- package/dist/surfaces/tui/navigation.js +87 -0
- package/dist/surfaces/tui/palettes.d.ts +38 -0
- package/dist/surfaces/tui/palettes.js +244 -0
- package/dist/surfaces/tui/screen.d.ts +30 -0
- package/dist/surfaces/tui/screen.js +51 -0
- package/dist/surfaces/tui/screens/about.d.ts +2 -0
- package/dist/surfaces/tui/screens/about.js +35 -0
- package/dist/surfaces/tui/screens/appearance.d.ts +2 -0
- package/dist/surfaces/tui/screens/appearance.js +74 -0
- package/dist/surfaces/tui/screens/config.d.ts +2 -0
- package/dist/surfaces/tui/screens/config.js +82 -0
- package/dist/surfaces/tui/screens/doctor.d.ts +2 -0
- package/dist/surfaces/tui/screens/doctor.js +109 -0
- package/dist/surfaces/tui/screens/export.d.ts +2 -0
- package/dist/surfaces/tui/screens/export.js +168 -0
- package/dist/surfaces/tui/screens/home-selection.d.ts +70 -0
- package/dist/surfaces/tui/screens/home-selection.js +80 -0
- package/dist/surfaces/tui/screens/home.d.ts +2 -0
- package/dist/surfaces/tui/screens/home.js +200 -0
- package/dist/surfaces/tui/screens/issue-detail.d.ts +6 -0
- package/dist/surfaces/tui/screens/issue-detail.js +182 -0
- package/dist/surfaces/tui/screens/jobs.d.ts +7 -0
- package/dist/surfaces/tui/screens/jobs.js +89 -0
- package/dist/surfaces/tui/screens/loaded-issue-context.d.ts +49 -0
- package/dist/surfaces/tui/screens/loaded-issue-context.js +57 -0
- package/dist/surfaces/tui/screens/onboarding/api-key.d.ts +2 -0
- package/dist/surfaces/tui/screens/onboarding/api-key.js +69 -0
- package/dist/surfaces/tui/screens/onboarding/login.d.ts +2 -0
- package/dist/surfaces/tui/screens/onboarding/login.js +50 -0
- package/dist/surfaces/tui/screens/onboarding/mode.d.ts +2 -0
- package/dist/surfaces/tui/screens/onboarding/mode.js +42 -0
- package/dist/surfaces/tui/screens/onboarding/onboarding-context.d.ts +221 -0
- package/dist/surfaces/tui/screens/onboarding/onboarding-context.js +131 -0
- package/dist/surfaces/tui/screens/onboarding/success.d.ts +2 -0
- package/dist/surfaces/tui/screens/onboarding/success.js +41 -0
- package/dist/surfaces/tui/screens/onboarding/url.d.ts +24 -0
- package/dist/surfaces/tui/screens/onboarding/url.js +84 -0
- package/dist/surfaces/tui/screens/onboarding/validating.d.ts +1 -0
- package/dist/surfaces/tui/screens/onboarding/validating.js +86 -0
- package/dist/surfaces/tui/screens/welcome.d.ts +6 -0
- package/dist/surfaces/tui/screens/welcome.js +95 -0
- package/dist/surfaces/tui/status-color.d.ts +21 -0
- package/dist/surfaces/tui/status-color.js +23 -0
- package/dist/surfaces/tui/symbols.d.ts +231 -0
- package/dist/surfaces/tui/symbols.js +14 -0
- package/dist/surfaces/tui/terminal-colors.d.ts +29 -0
- package/dist/surfaces/tui/terminal-colors.js +39 -0
- package/dist/surfaces/tui/theme.d.ts +156 -0
- package/dist/surfaces/tui/theme.js +86 -0
- package/dist/surfaces/tui/truncate.d.ts +31 -0
- package/dist/surfaces/tui/truncate.js +81 -0
- package/package.json +93 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orquestração get → normalize → extração de UM anexo (M3-13, ADR-002/ADR-005).
|
|
3
|
+
*
|
|
4
|
+
* Função fina do core que a superfície MCP (#55) reutiliza pela tool read-only
|
|
5
|
+
* `get_attachment_text`: recebe credenciais já resolvidas + `issueId`/`attachmentId`
|
|
6
|
+
* e devolve o {@link ExtractionResult} do anexo pedido, passando pelo MESMO
|
|
7
|
+
* pipeline com cache de {@link extractIssueAttachments} (`getOrCompute` garante o
|
|
8
|
+
* cache-hit quando o texto já foi extraído — a #55 não re-baixa nem re-roda o OCR).
|
|
9
|
+
*
|
|
10
|
+
* Só o anexo pedido entra no pipeline (a issue é filtrada a um único anexo), então
|
|
11
|
+
* o download/extração caro roda no máximo uma vez por chamada. Mantém a fronteira
|
|
12
|
+
* do ADR-005: encapsula `client`, `normalize`, `extract` e `cache`.
|
|
13
|
+
*/
|
|
14
|
+
import { type BackgroundExtractor } from './cache-first.js';
|
|
15
|
+
import { type CacheStore } from './cache/index.js';
|
|
16
|
+
import { type Logger } from './client/index.js';
|
|
17
|
+
import type { ExtractionResult } from './contract.js';
|
|
18
|
+
import { type ExtractorRegistry } from './extract/index.js';
|
|
19
|
+
/**
|
|
20
|
+
* Erro tipado: o anexo pedido não existe na issue (após o fetch bem-sucedido da
|
|
21
|
+
* issue). Distinto de um 404 do Redmine (que é a issue inexistente) — as
|
|
22
|
+
* superfícies o mapeiam para uma mensagem clara, sem cache indevido.
|
|
23
|
+
*/
|
|
24
|
+
export declare class AttachmentNotFoundError extends Error {
|
|
25
|
+
/** Id da issue consultada. */
|
|
26
|
+
readonly issueId: number;
|
|
27
|
+
/** Id do anexo ausente. */
|
|
28
|
+
readonly attachmentId: number;
|
|
29
|
+
/**
|
|
30
|
+
* @param issueId - Id da issue consultada.
|
|
31
|
+
* @param attachmentId - Id do anexo que não foi encontrado.
|
|
32
|
+
*/
|
|
33
|
+
constructor(issueId: number, attachmentId: number);
|
|
34
|
+
}
|
|
35
|
+
/** Opções de {@link fetchAttachmentText}: credenciais resolvidas + alvos. */
|
|
36
|
+
export interface FetchAttachmentTextOptions {
|
|
37
|
+
/** URL base da instância Redmine (ex.: `https://redmine.example`). */
|
|
38
|
+
baseUrl: string;
|
|
39
|
+
/** api_key já resolvida pela cascata da superfície. */
|
|
40
|
+
apiKey: string;
|
|
41
|
+
/** Identificador da issue que contém o anexo. */
|
|
42
|
+
issueId: number;
|
|
43
|
+
/** Identificador do anexo cujo texto será extraído. */
|
|
44
|
+
attachmentId: number;
|
|
45
|
+
/** Permite `http://` (sem TLS) com aviso ruidoso. Default: `false`. */
|
|
46
|
+
insecure?: boolean;
|
|
47
|
+
/** Raiz do cache em disco para downloads/extrações. Default do {@link DiskCacheStore}. */
|
|
48
|
+
cacheDir?: string;
|
|
49
|
+
}
|
|
50
|
+
/** Resultado de {@link fetchAttachmentText}: o anexo + sua extração. */
|
|
51
|
+
export interface AttachmentTextResult {
|
|
52
|
+
/** Id do anexo processado. */
|
|
53
|
+
attachmentId: number;
|
|
54
|
+
/** Resultado da extração (status + texto/metadata) — ver {@link ExtractionResult}. */
|
|
55
|
+
extraction: ExtractionResult;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Busca a issue, isola o anexo pedido e extrai seu texto pelo pipeline com cache.
|
|
59
|
+
*
|
|
60
|
+
* @param options - Ver {@link FetchAttachmentTextOptions}.
|
|
61
|
+
* @returns O {@link AttachmentTextResult} do anexo (texto quando `done`/`text`;
|
|
62
|
+
* caso contrário `skipped`/`unsupported`/`failed` com motivo/dica).
|
|
63
|
+
* @throws {RedmineAuthError} Em 401 (propagado do client).
|
|
64
|
+
* @throws {RedmineForbiddenError} Em 403 — sem permissão para a issue.
|
|
65
|
+
* @throws {RedmineNotFoundError} Em 404 — issue inexistente.
|
|
66
|
+
* @throws {AttachmentNotFoundError} Quando o anexo não existe na issue.
|
|
67
|
+
* @example
|
|
68
|
+
* const { extraction } = await fetchAttachmentText({
|
|
69
|
+
* baseUrl, apiKey, issueId: 42, attachmentId: 77,
|
|
70
|
+
* });
|
|
71
|
+
*/
|
|
72
|
+
export declare function fetchAttachmentText(options: FetchAttachmentTextOptions): Promise<AttachmentTextResult>;
|
|
73
|
+
/**
|
|
74
|
+
* Opções de {@link fetchAttachmentTextCacheFirst}: as de {@link fetchAttachmentText}
|
|
75
|
+
* mais seams INJETÁVEIS (registry/store/background/logger) com defaults reais —
|
|
76
|
+
* usados pelos testes para simular a extração lenta de forma determinística, sem
|
|
77
|
+
* tocar a rede/disco/subprocesso reais.
|
|
78
|
+
*/
|
|
79
|
+
export interface FetchAttachmentTextCacheFirstOptions extends FetchAttachmentTextOptions {
|
|
80
|
+
/** Registry de extratores; default: {@link createDefaultRegistry}. */
|
|
81
|
+
registry?: ExtractorRegistry;
|
|
82
|
+
/** Store attachment-level; default: {@link DiskCacheStore} sobre `cacheDir`. */
|
|
83
|
+
store?: CacheStore<ExtractionResult>;
|
|
84
|
+
/**
|
|
85
|
+
* Dispatcher do job em background (#71); default: fila (`runQueue`) que roda a
|
|
86
|
+
* extração real detached. Injetável para provar o não-bloqueio (< 5s) sem esperar.
|
|
87
|
+
*/
|
|
88
|
+
background?: BackgroundExtractor;
|
|
89
|
+
/** Logger para avisos; default no-op. Nunca `console.*`. */
|
|
90
|
+
logger?: Logger;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Variante CACHE-FIRST e NÃO-BLOQUEANTE de {@link fetchAttachmentText} (M4-11, #70).
|
|
94
|
+
*
|
|
95
|
+
* Busca a issue (rápido), isola o anexo pedido e LÊ o cache: se a extração já está
|
|
96
|
+
* pronta, devolve o texto IMEDIATAMENTE; se ainda não (anexo pesado), devolve
|
|
97
|
+
* `processing` na hora — SEM aguardar a extração — e dispara o job em segundo
|
|
98
|
+
* plano pela fila (fire-and-forget, forward-compatible com a continuação #71).
|
|
99
|
+
* Nenhuma chamada bloqueia aguardando mídia.
|
|
100
|
+
*
|
|
101
|
+
* @param options - Ver {@link FetchAttachmentTextCacheFirstOptions}.
|
|
102
|
+
* @returns O {@link AttachmentTextResult} do anexo (texto pronto, `processing` ou
|
|
103
|
+
* `unsupported`), sempre sem bloquear na extração cara.
|
|
104
|
+
* @throws {RedmineAuthError} Em 401 (propagado do client).
|
|
105
|
+
* @throws {RedmineForbiddenError} Em 403 — sem permissão para a issue.
|
|
106
|
+
* @throws {RedmineNotFoundError} Em 404 — issue inexistente.
|
|
107
|
+
* @throws {AttachmentNotFoundError} Quando o anexo não existe na issue.
|
|
108
|
+
* @example
|
|
109
|
+
* const { extraction } = await fetchAttachmentTextCacheFirst({
|
|
110
|
+
* baseUrl, apiKey, issueId: 42, attachmentId: 77,
|
|
111
|
+
* });
|
|
112
|
+
*/
|
|
113
|
+
export declare function fetchAttachmentTextCacheFirst(options: FetchAttachmentTextCacheFirstOptions): Promise<AttachmentTextResult>;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orquestração get → normalize → extração de UM anexo (M3-13, ADR-002/ADR-005).
|
|
3
|
+
*
|
|
4
|
+
* Função fina do core que a superfície MCP (#55) reutiliza pela tool read-only
|
|
5
|
+
* `get_attachment_text`: recebe credenciais já resolvidas + `issueId`/`attachmentId`
|
|
6
|
+
* e devolve o {@link ExtractionResult} do anexo pedido, passando pelo MESMO
|
|
7
|
+
* pipeline com cache de {@link extractIssueAttachments} (`getOrCompute` garante o
|
|
8
|
+
* cache-hit quando o texto já foi extraído — a #55 não re-baixa nem re-roda o OCR).
|
|
9
|
+
*
|
|
10
|
+
* Só o anexo pedido entra no pipeline (a issue é filtrada a um único anexo), então
|
|
11
|
+
* o download/extração caro roda no máximo uma vez por chamada. Mantém a fronteira
|
|
12
|
+
* do ADR-005: encapsula `client`, `normalize`, `extract` e `cache`.
|
|
13
|
+
*/
|
|
14
|
+
import { extractIssueAttachmentsCacheFirst, makeQueueBackgroundExtractor, processingResult, } from './cache-first.js';
|
|
15
|
+
import { DiskCacheStore } from './cache/index.js';
|
|
16
|
+
import { createHttpClient, getIssue } from './client/index.js';
|
|
17
|
+
import { extractIssueAttachments } from './extract-issue-attachments.js';
|
|
18
|
+
import { createDefaultRegistry } from './extract/index.js';
|
|
19
|
+
import { normalizeIssue } from './normalize/index.js';
|
|
20
|
+
/**
|
|
21
|
+
* Erro tipado: o anexo pedido não existe na issue (após o fetch bem-sucedido da
|
|
22
|
+
* issue). Distinto de um 404 do Redmine (que é a issue inexistente) — as
|
|
23
|
+
* superfícies o mapeiam para uma mensagem clara, sem cache indevido.
|
|
24
|
+
*/
|
|
25
|
+
export class AttachmentNotFoundError extends Error {
|
|
26
|
+
/** Id da issue consultada. */
|
|
27
|
+
issueId;
|
|
28
|
+
/** Id do anexo ausente. */
|
|
29
|
+
attachmentId;
|
|
30
|
+
/**
|
|
31
|
+
* @param issueId - Id da issue consultada.
|
|
32
|
+
* @param attachmentId - Id do anexo que não foi encontrado.
|
|
33
|
+
*/
|
|
34
|
+
constructor(issueId, attachmentId) {
|
|
35
|
+
super(`Anexo #${attachmentId} não encontrado na issue #${issueId}.`);
|
|
36
|
+
this.name = 'AttachmentNotFoundError';
|
|
37
|
+
this.issueId = issueId;
|
|
38
|
+
this.attachmentId = attachmentId;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Extração `unsupported` para anexo cujo MIME não tem extrator local registrado. */
|
|
42
|
+
function unsupportedResult() {
|
|
43
|
+
return {
|
|
44
|
+
status: 'unsupported',
|
|
45
|
+
metadata: {
|
|
46
|
+
reason: 'sem-extrator-registrado',
|
|
47
|
+
hint: 'nenhum extrator local suporta o tipo deste anexo (o OCR cobre apenas imagens)',
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Busca a issue, isola o anexo pedido e extrai seu texto pelo pipeline com cache.
|
|
53
|
+
*
|
|
54
|
+
* @param options - Ver {@link FetchAttachmentTextOptions}.
|
|
55
|
+
* @returns O {@link AttachmentTextResult} do anexo (texto quando `done`/`text`;
|
|
56
|
+
* caso contrário `skipped`/`unsupported`/`failed` com motivo/dica).
|
|
57
|
+
* @throws {RedmineAuthError} Em 401 (propagado do client).
|
|
58
|
+
* @throws {RedmineForbiddenError} Em 403 — sem permissão para a issue.
|
|
59
|
+
* @throws {RedmineNotFoundError} Em 404 — issue inexistente.
|
|
60
|
+
* @throws {AttachmentNotFoundError} Quando o anexo não existe na issue.
|
|
61
|
+
* @example
|
|
62
|
+
* const { extraction } = await fetchAttachmentText({
|
|
63
|
+
* baseUrl, apiKey, issueId: 42, attachmentId: 77,
|
|
64
|
+
* });
|
|
65
|
+
*/
|
|
66
|
+
export async function fetchAttachmentText(options) {
|
|
67
|
+
const { baseUrl, apiKey, issueId, attachmentId, insecure = false, cacheDir } = options;
|
|
68
|
+
const http = createHttpClient({ baseUrl, apiKey, insecure });
|
|
69
|
+
const payload = await getIssue(http, issueId);
|
|
70
|
+
const issue = normalizeIssue(payload);
|
|
71
|
+
const attachment = issue.attachments.find((a) => a.id === attachmentId);
|
|
72
|
+
if (attachment === undefined) {
|
|
73
|
+
throw new AttachmentNotFoundError(issueId, attachmentId);
|
|
74
|
+
}
|
|
75
|
+
const registry = await createDefaultRegistry();
|
|
76
|
+
const store = new DiskCacheStore(cacheDir !== undefined ? { cacheDir } : {});
|
|
77
|
+
// Filtra a issue a UM anexo: o pipeline pré-filtra por extrator e só baixa/roda
|
|
78
|
+
// o extrator caro do anexo pedido. `getOrCompute` serve do cache num re-hit.
|
|
79
|
+
const single = { ...issue, attachments: [attachment] };
|
|
80
|
+
const map = await extractIssueAttachments(http, single, {
|
|
81
|
+
instanceUrl: baseUrl,
|
|
82
|
+
registry,
|
|
83
|
+
store,
|
|
84
|
+
...(cacheDir !== undefined ? { cacheDir } : {}),
|
|
85
|
+
});
|
|
86
|
+
// Ausente no mapa = nenhum extrator para o MIME provável → unsupported legível.
|
|
87
|
+
const extraction = map.get(attachmentId) ?? unsupportedResult();
|
|
88
|
+
return { attachmentId, extraction };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Variante CACHE-FIRST e NÃO-BLOQUEANTE de {@link fetchAttachmentText} (M4-11, #70).
|
|
92
|
+
*
|
|
93
|
+
* Busca a issue (rápido), isola o anexo pedido e LÊ o cache: se a extração já está
|
|
94
|
+
* pronta, devolve o texto IMEDIATAMENTE; se ainda não (anexo pesado), devolve
|
|
95
|
+
* `processing` na hora — SEM aguardar a extração — e dispara o job em segundo
|
|
96
|
+
* plano pela fila (fire-and-forget, forward-compatible com a continuação #71).
|
|
97
|
+
* Nenhuma chamada bloqueia aguardando mídia.
|
|
98
|
+
*
|
|
99
|
+
* @param options - Ver {@link FetchAttachmentTextCacheFirstOptions}.
|
|
100
|
+
* @returns O {@link AttachmentTextResult} do anexo (texto pronto, `processing` ou
|
|
101
|
+
* `unsupported`), sempre sem bloquear na extração cara.
|
|
102
|
+
* @throws {RedmineAuthError} Em 401 (propagado do client).
|
|
103
|
+
* @throws {RedmineForbiddenError} Em 403 — sem permissão para a issue.
|
|
104
|
+
* @throws {RedmineNotFoundError} Em 404 — issue inexistente.
|
|
105
|
+
* @throws {AttachmentNotFoundError} Quando o anexo não existe na issue.
|
|
106
|
+
* @example
|
|
107
|
+
* const { extraction } = await fetchAttachmentTextCacheFirst({
|
|
108
|
+
* baseUrl, apiKey, issueId: 42, attachmentId: 77,
|
|
109
|
+
* });
|
|
110
|
+
*/
|
|
111
|
+
export async function fetchAttachmentTextCacheFirst(options) {
|
|
112
|
+
const { baseUrl, apiKey, issueId, attachmentId, insecure = false, cacheDir, logger } = options;
|
|
113
|
+
const http = createHttpClient({ baseUrl, apiKey, insecure });
|
|
114
|
+
const payload = await getIssue(http, issueId);
|
|
115
|
+
const issue = normalizeIssue(payload);
|
|
116
|
+
const attachment = issue.attachments.find((a) => a.id === attachmentId);
|
|
117
|
+
if (attachment === undefined) {
|
|
118
|
+
throw new AttachmentNotFoundError(issueId, attachmentId);
|
|
119
|
+
}
|
|
120
|
+
const registry = options.registry ?? (await createDefaultRegistry());
|
|
121
|
+
const store = options.store ?? new DiskCacheStore(cacheDir !== undefined ? { cacheDir } : {});
|
|
122
|
+
const single = { ...issue, attachments: [attachment] };
|
|
123
|
+
// Default: a extração cara roda em background pela fila; o compute reusa o
|
|
124
|
+
// pipeline testado `extractIssueAttachments` (mesma chave/store → o resultado
|
|
125
|
+
// fica pronto para a 2ª chamada, #71). Injetável nos testes por determinismo.
|
|
126
|
+
const background = options.background ??
|
|
127
|
+
makeQueueBackgroundExtractor(async (_target, signal) => {
|
|
128
|
+
// Repassa o `signal` da fila (#69/#73) ao pipeline: quando a fila abortar, o
|
|
129
|
+
// abort chega ao `runWithWatchdog` e MATA o ffmpeg/whisper ponta a ponta. Só
|
|
130
|
+
// inclui quando dado (exactOptionalPropertyTypes).
|
|
131
|
+
const computed = await extractIssueAttachments(http, single, {
|
|
132
|
+
instanceUrl: baseUrl,
|
|
133
|
+
registry,
|
|
134
|
+
store,
|
|
135
|
+
...(cacheDir !== undefined ? { cacheDir } : {}),
|
|
136
|
+
...(signal !== undefined ? { signal } : {}),
|
|
137
|
+
...(logger !== undefined ? { logger } : {}),
|
|
138
|
+
});
|
|
139
|
+
return computed.get(attachmentId) ?? processingResult();
|
|
140
|
+
},
|
|
141
|
+
// Passa o `store` para o dispatcher PERSISTIR o resultado de fechamento
|
|
142
|
+
// (done/failed) sob a mesma chave lida no cache-first (#71): a 2ª chamada
|
|
143
|
+
// acerta o cache e uma falha vira `failed` — nunca `processing` eterno.
|
|
144
|
+
{ store, ...(logger !== undefined ? { logger } : {}) });
|
|
145
|
+
const map = await extractIssueAttachmentsCacheFirst(single, {
|
|
146
|
+
instanceUrl: baseUrl,
|
|
147
|
+
registry,
|
|
148
|
+
store,
|
|
149
|
+
background,
|
|
150
|
+
...(logger !== undefined ? { logger } : {}),
|
|
151
|
+
});
|
|
152
|
+
// Ausente no mapa = nenhum extrator para o MIME provável → unsupported legível.
|
|
153
|
+
const extraction = map.get(attachmentId) ?? unsupportedResult();
|
|
154
|
+
return { attachmentId, extraction };
|
|
155
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orquestração get → normalize → bundle de uma issue (M1-11).
|
|
3
|
+
*
|
|
4
|
+
* Função fina do core que as superfícies (CLI #17, MCP #18) reutilizam: recebe
|
|
5
|
+
* as credenciais já resolvidas + o formato desejado e devolve uma
|
|
6
|
+
* `AsyncIterable<CoreEvent<IssueBundleResult>>` — emitindo progresso incremental
|
|
7
|
+
* e finalizando com um único {@link Result} contendo o bundle serializado.
|
|
8
|
+
*
|
|
9
|
+
* Mantém a fronteira do ADR-005: encapsula os módulos internos (`client`,
|
|
10
|
+
* `normalize`, `bundle`) para que a superfície permaneça fina e desacoplada.
|
|
11
|
+
*/
|
|
12
|
+
import { type BackgroundExtractor } from './cache-first.js';
|
|
13
|
+
import type { CoreEvent } from './contract.js';
|
|
14
|
+
/** Formato de saída do bundle: Markdown (default) ou JSON canônico. */
|
|
15
|
+
export type BundleFormat = 'md' | 'json';
|
|
16
|
+
/** Opções de {@link fetchIssueBundle}: credenciais já resolvidas + formato. */
|
|
17
|
+
export interface FetchIssueBundleOptions {
|
|
18
|
+
/** URL base da instância Redmine (ex.: `https://redmine.example`). */
|
|
19
|
+
baseUrl: string;
|
|
20
|
+
/** api_key já resolvida pela cascata da superfície. */
|
|
21
|
+
apiKey: string;
|
|
22
|
+
/** Identificador da issue a empacotar. */
|
|
23
|
+
issueId: number;
|
|
24
|
+
/** Formato de saída. Default (na superfície): `md`. */
|
|
25
|
+
format: BundleFormat;
|
|
26
|
+
/** Versão da ferramenta gravada no bundle. */
|
|
27
|
+
toolVersion: string;
|
|
28
|
+
/** Permite `http://` (sem TLS) com aviso ruidoso. Default: `false`. */
|
|
29
|
+
insecure?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Extrai o texto dos anexos de imagem e o embute no bundle (M3-10). Default:
|
|
32
|
+
* `false` no M3 — as superfícies (CLI/MCP) ligam a flag na #55. Requer o
|
|
33
|
+
* binário `tesseract`; ausente, o anexo apenas registra `failed` com a dica de
|
|
34
|
+
* instalação (o bundle sai mesmo assim — degradação graciosa, ADR-002).
|
|
35
|
+
*/
|
|
36
|
+
extractAttachments?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Raiz do cache em disco para downloads/extrações (M3-10). Default do
|
|
39
|
+
* {@link DiskCacheStore}. Só usado quando `extractAttachments` é `true`.
|
|
40
|
+
*/
|
|
41
|
+
cacheDir?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Modo CACHE-FIRST não-bloqueante (M4-11, #70). Quando `true` (e
|
|
44
|
+
* `extractAttachments` `true`), NÃO computa extração no caminho da resposta:
|
|
45
|
+
* embute o texto já cacheado e marca os anexos ainda não processados como
|
|
46
|
+
* `processing`, disparando a extração em background. Default: `false`
|
|
47
|
+
* (comportamento síncrono do M3, usado pela CLI de exportação em disco).
|
|
48
|
+
*/
|
|
49
|
+
cacheFirst?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Dispatcher do job em background do modo cache-first (seam de teste). Default:
|
|
52
|
+
* fila (`runQueue`) que roda a extração real detached. Injetável para provar o
|
|
53
|
+
* não-bloqueio de forma determinística. Só usado quando `cacheFirst` é `true`.
|
|
54
|
+
*/
|
|
55
|
+
background?: BackgroundExtractor;
|
|
56
|
+
}
|
|
57
|
+
/** Resultado final: conteúdo serializado pronto para stdout/arquivo. */
|
|
58
|
+
export interface IssueBundleResult {
|
|
59
|
+
/** Id da issue empacotada (compõe o nome do arquivo em `--out`). */
|
|
60
|
+
issueId: number;
|
|
61
|
+
/** Formato efetivo do conteúdo. */
|
|
62
|
+
format: BundleFormat;
|
|
63
|
+
/** Bundle serializado (Markdown ou JSON canônico). */
|
|
64
|
+
content: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Busca, normaliza e empacota uma issue, emitindo progresso incremental.
|
|
68
|
+
*
|
|
69
|
+
* @param options - Ver {@link FetchIssueBundleOptions}.
|
|
70
|
+
* @returns Sequência de {@link ProgressEvent} terminada por um {@link Result}
|
|
71
|
+
* com o {@link IssueBundleResult}.
|
|
72
|
+
* @throws {RedmineAuthError} Em 401 (propagado do client).
|
|
73
|
+
* @throws {RedmineNotFoundError} Em 404 — issue inexistente.
|
|
74
|
+
* @throws {RedmineHttpError} Em outros status ≥ 400.
|
|
75
|
+
* @throws {Error} Em falha de rede/TLS/JSON inválido.
|
|
76
|
+
* @example
|
|
77
|
+
* for await (const event of fetchIssueBundle(opts)) {
|
|
78
|
+
* if (event.kind === 'result') process.stdout.write(event.value.content);
|
|
79
|
+
* }
|
|
80
|
+
*/
|
|
81
|
+
export declare function fetchIssueBundle(options: FetchIssueBundleOptions): AsyncIterable<CoreEvent<IssueBundleResult>>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orquestração get → normalize → bundle de uma issue (M1-11).
|
|
3
|
+
*
|
|
4
|
+
* Função fina do core que as superfícies (CLI #17, MCP #18) reutilizam: recebe
|
|
5
|
+
* as credenciais já resolvidas + o formato desejado e devolve uma
|
|
6
|
+
* `AsyncIterable<CoreEvent<IssueBundleResult>>` — emitindo progresso incremental
|
|
7
|
+
* e finalizando com um único {@link Result} contendo o bundle serializado.
|
|
8
|
+
*
|
|
9
|
+
* Mantém a fronteira do ADR-005: encapsula os módulos internos (`client`,
|
|
10
|
+
* `normalize`, `bundle`) para que a superfície permaneça fina e desacoplada.
|
|
11
|
+
*/
|
|
12
|
+
import { buildJsonBundle } from './bundle/index.js';
|
|
13
|
+
import { buildMarkdownBundle } from './bundle/index.js';
|
|
14
|
+
import { extractIssueAttachmentsCacheFirst, makeQueueBackgroundExtractor, processingResult, } from './cache-first.js';
|
|
15
|
+
import { DiskCacheStore } from './cache/index.js';
|
|
16
|
+
import { createHttpClient, getIssue } from './client/index.js';
|
|
17
|
+
import { extractIssueAttachments } from './extract-issue-attachments.js';
|
|
18
|
+
import { createDefaultRegistry } from './extract/index.js';
|
|
19
|
+
import { normalizeIssue } from './normalize/index.js';
|
|
20
|
+
/** Helper: constrói um evento de progresso tipado. */
|
|
21
|
+
function progress(stage, message) {
|
|
22
|
+
return { kind: 'progress', stage, message };
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Busca, normaliza e empacota uma issue, emitindo progresso incremental.
|
|
26
|
+
*
|
|
27
|
+
* @param options - Ver {@link FetchIssueBundleOptions}.
|
|
28
|
+
* @returns Sequência de {@link ProgressEvent} terminada por um {@link Result}
|
|
29
|
+
* com o {@link IssueBundleResult}.
|
|
30
|
+
* @throws {RedmineAuthError} Em 401 (propagado do client).
|
|
31
|
+
* @throws {RedmineNotFoundError} Em 404 — issue inexistente.
|
|
32
|
+
* @throws {RedmineHttpError} Em outros status ≥ 400.
|
|
33
|
+
* @throws {Error} Em falha de rede/TLS/JSON inválido.
|
|
34
|
+
* @example
|
|
35
|
+
* for await (const event of fetchIssueBundle(opts)) {
|
|
36
|
+
* if (event.kind === 'result') process.stdout.write(event.value.content);
|
|
37
|
+
* }
|
|
38
|
+
*/
|
|
39
|
+
export async function* fetchIssueBundle(options) {
|
|
40
|
+
const { baseUrl, apiKey, issueId, format, toolVersion, insecure = false, extractAttachments = false } = options;
|
|
41
|
+
yield progress('connect', `Conectando a ${baseUrl}`);
|
|
42
|
+
const http = createHttpClient({ baseUrl, apiKey, insecure });
|
|
43
|
+
yield progress('fetch', `Buscando issue #${issueId}`);
|
|
44
|
+
const payload = await getIssue(http, issueId);
|
|
45
|
+
yield progress('normalize', 'Normalizando issue');
|
|
46
|
+
const issue = normalizeIssue(payload);
|
|
47
|
+
let extractions;
|
|
48
|
+
if (extractAttachments) {
|
|
49
|
+
yield progress('extract', 'Extraindo texto dos anexos');
|
|
50
|
+
const registry = await createDefaultRegistry();
|
|
51
|
+
const store = new DiskCacheStore(options.cacheDir !== undefined ? { cacheDir: options.cacheDir } : {});
|
|
52
|
+
if (options.cacheFirst === true) {
|
|
53
|
+
// Cache-first (#70): lê o que está pronto e marca o resto como `processing`,
|
|
54
|
+
// sem bloquear na extração cara; a computação real corre em background.
|
|
55
|
+
const background = options.background ??
|
|
56
|
+
makeQueueBackgroundExtractor(async (target, signal) => {
|
|
57
|
+
const single = { ...issue, attachments: [target.attachment] };
|
|
58
|
+
// Repassa o `signal` da fila (#69/#73): o abort chega ao `runWithWatchdog`
|
|
59
|
+
// e MATA o ffmpeg/whisper ponta a ponta. Só inclui quando dado.
|
|
60
|
+
const computed = await extractIssueAttachments(http, single, {
|
|
61
|
+
instanceUrl: baseUrl,
|
|
62
|
+
registry,
|
|
63
|
+
store,
|
|
64
|
+
...(options.cacheDir !== undefined ? { cacheDir: options.cacheDir } : {}),
|
|
65
|
+
...(signal !== undefined ? { signal } : {}),
|
|
66
|
+
});
|
|
67
|
+
return computed.get(target.attachment.id) ?? processingResult();
|
|
68
|
+
}, { store });
|
|
69
|
+
extractions = await extractIssueAttachmentsCacheFirst(issue, {
|
|
70
|
+
instanceUrl: baseUrl,
|
|
71
|
+
registry,
|
|
72
|
+
store,
|
|
73
|
+
background,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
extractions = await extractIssueAttachments(http, issue, {
|
|
78
|
+
instanceUrl: baseUrl,
|
|
79
|
+
registry,
|
|
80
|
+
store,
|
|
81
|
+
...(options.cacheDir !== undefined ? { cacheDir: options.cacheDir } : {}),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
yield progress('bundle', `Empacotando bundle (${format})`);
|
|
86
|
+
const meta = { baseUrl, toolVersion, ...(extractions !== undefined ? { extractions } : {}) };
|
|
87
|
+
const content = format === 'json' ? buildJsonBundle(issue, meta).canonical : buildMarkdownBundle(issue, meta);
|
|
88
|
+
const result = {
|
|
89
|
+
kind: 'result',
|
|
90
|
+
value: { issueId, format, content },
|
|
91
|
+
};
|
|
92
|
+
yield result;
|
|
93
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orquestração de busca de issues para a tool `search_issues` (M1-13).
|
|
3
|
+
*
|
|
4
|
+
* Função fina do core reutilizável pela superfície MCP (#19): recebe credenciais
|
|
5
|
+
* já resolvidas + filtros estruturados + termo full-text opcional e devolve uma
|
|
6
|
+
* lista COMPACTA em Markdown (id, assunto fenced, status, responsável) — nunca o
|
|
7
|
+
* bundle completo por item.
|
|
8
|
+
*
|
|
9
|
+
* Estratégia (best-effort no full-text):
|
|
10
|
+
* - Sem `query`: lista `/issues.json` só com os filtros estruturados.
|
|
11
|
+
* - Com `query`: busca `/search.json` para obter os ids que casam com o texto e
|
|
12
|
+
* então lista `/issues.json` restringindo por `issue_id` (interseção
|
|
13
|
+
* texto ∩ filtros). Se `/search` falhar (404/erro), DEGRADA para os filtros
|
|
14
|
+
* estruturados e adiciona um aviso ao payload — sem quebrar a tool.
|
|
15
|
+
*
|
|
16
|
+
* Mantém a fronteira do ADR-005: encapsula os módulos internos (`client`,
|
|
17
|
+
* `bundle`) para que a superfície permaneça fina e sem acesso a URL/host.
|
|
18
|
+
*/
|
|
19
|
+
/** Limite default de resultados quando a superfície não informa `limit`. */
|
|
20
|
+
export declare const SEARCH_DEFAULT_LIMIT = 25;
|
|
21
|
+
/**
|
|
22
|
+
* Filtros estruturados aceitos por `/issues.json`. Os valores seguem os formatos
|
|
23
|
+
* do Redmine (ex.: `status_id: 'open'`, `updated_on: '>=2026-01-01'`).
|
|
24
|
+
*/
|
|
25
|
+
export interface IssueSearchFilters {
|
|
26
|
+
/** Filtro `project_id`. */
|
|
27
|
+
project_id?: number | undefined;
|
|
28
|
+
/** Filtro `status_id` (`'open'`, `'closed'`, `'*'` ou um id). */
|
|
29
|
+
status_id?: number | string | undefined;
|
|
30
|
+
/** Filtro `assigned_to_id` (um id ou `'me'`). */
|
|
31
|
+
assigned_to_id?: number | string | undefined;
|
|
32
|
+
/** Filtro `updated_on` no formato do Redmine (ex.: `>=2026-01-01`). */
|
|
33
|
+
updated_on?: string | undefined;
|
|
34
|
+
}
|
|
35
|
+
/** Opções de {@link fetchIssueSearch}: credenciais + filtros + full-text. */
|
|
36
|
+
export interface FetchIssueSearchOptions {
|
|
37
|
+
/** URL base da instância Redmine (ex.: `https://redmine.example`). */
|
|
38
|
+
baseUrl: string;
|
|
39
|
+
/** api_key já resolvida pela cascata da superfície. */
|
|
40
|
+
apiKey: string;
|
|
41
|
+
/** Filtros estruturados de `/issues.json` (ver {@link IssueSearchFilters}). */
|
|
42
|
+
filters: IssueSearchFilters;
|
|
43
|
+
/** Termo full-text opcional (`/search.json`, best-effort). */
|
|
44
|
+
query?: string | undefined;
|
|
45
|
+
/** Máximo de resultados. Default: {@link SEARCH_DEFAULT_LIMIT}. */
|
|
46
|
+
limit?: number | undefined;
|
|
47
|
+
/** Permite `http://` (sem TLS) com aviso ruidoso. Default: `false`. */
|
|
48
|
+
insecure?: boolean | undefined;
|
|
49
|
+
}
|
|
50
|
+
/** Resultado da busca: markdown compacto + metadados de degradação. */
|
|
51
|
+
export interface IssueSearchResult {
|
|
52
|
+
/** Lista compacta em Markdown pronta para o CallToolResult. */
|
|
53
|
+
content: string;
|
|
54
|
+
/** Número de itens retornados. */
|
|
55
|
+
count: number;
|
|
56
|
+
/** Avisos de degradação (ex.: `/search` indisponível). Vazio no caminho feliz. */
|
|
57
|
+
warnings: string[];
|
|
58
|
+
/** `true` quando o full-text foi solicitado mas degradou para os filtros. */
|
|
59
|
+
degraded: boolean;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Busca issues combinando filtros estruturados e full-text best-effort.
|
|
63
|
+
*
|
|
64
|
+
* @param options - Ver {@link FetchIssueSearchOptions}.
|
|
65
|
+
* @returns Lista compacta em Markdown + metadados de degradação.
|
|
66
|
+
* @throws {RedmineAuthError} Em 401 (propagado do client).
|
|
67
|
+
* @throws {RedmineForbiddenError} Em 403.
|
|
68
|
+
* @throws {RedmineHttpError} Em outros status ≥ 400 de `/issues.json`.
|
|
69
|
+
* @example
|
|
70
|
+
* const res = await fetchIssueSearch({
|
|
71
|
+
* baseUrl, apiKey, filters: { project_id: 5 }, query: 'timeout', limit: 25,
|
|
72
|
+
* });
|
|
73
|
+
*/
|
|
74
|
+
export declare function fetchIssueSearch(options: FetchIssueSearchOptions): Promise<IssueSearchResult>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orquestração de busca de issues para a tool `search_issues` (M1-13).
|
|
3
|
+
*
|
|
4
|
+
* Função fina do core reutilizável pela superfície MCP (#19): recebe credenciais
|
|
5
|
+
* já resolvidas + filtros estruturados + termo full-text opcional e devolve uma
|
|
6
|
+
* lista COMPACTA em Markdown (id, assunto fenced, status, responsável) — nunca o
|
|
7
|
+
* bundle completo por item.
|
|
8
|
+
*
|
|
9
|
+
* Estratégia (best-effort no full-text):
|
|
10
|
+
* - Sem `query`: lista `/issues.json` só com os filtros estruturados.
|
|
11
|
+
* - Com `query`: busca `/search.json` para obter os ids que casam com o texto e
|
|
12
|
+
* então lista `/issues.json` restringindo por `issue_id` (interseção
|
|
13
|
+
* texto ∩ filtros). Se `/search` falhar (404/erro), DEGRADA para os filtros
|
|
14
|
+
* estruturados e adiciona um aviso ao payload — sem quebrar a tool.
|
|
15
|
+
*
|
|
16
|
+
* Mantém a fronteira do ADR-005: encapsula os módulos internos (`client`,
|
|
17
|
+
* `bundle`) para que a superfície permaneça fina e sem acesso a URL/host.
|
|
18
|
+
*/
|
|
19
|
+
import { buildSearchListMarkdown } from './bundle/index.js';
|
|
20
|
+
import { createHttpClient, listIssues, searchIssues } from './client/index.js';
|
|
21
|
+
/** Limite default de resultados quando a superfície não informa `limit`. */
|
|
22
|
+
export const SEARCH_DEFAULT_LIMIT = 25;
|
|
23
|
+
/** Placeholder para responsável ausente (issue sem `assigned_to`). */
|
|
24
|
+
const ABSENT_ASSIGNEE = '(nenhum)';
|
|
25
|
+
/** Placeholder para ref degradada/desconhecida (nome ausente). */
|
|
26
|
+
const UNKNOWN_REF = '(desconhecido)';
|
|
27
|
+
/** Extrai uma mensagem legível de um erro desconhecido. */
|
|
28
|
+
function messageOf(error) {
|
|
29
|
+
return error instanceof Error ? error.message : String(error);
|
|
30
|
+
}
|
|
31
|
+
/** Converte um valor desconhecido em objeto simples para acesso seguro. */
|
|
32
|
+
function asRecord(value) {
|
|
33
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
/** Nome legível de uma ref `{ id, name }` bruta; ausência → `undefined`. */
|
|
39
|
+
function refName(value) {
|
|
40
|
+
const rec = asRecord(value);
|
|
41
|
+
if (rec === undefined)
|
|
42
|
+
return undefined;
|
|
43
|
+
return typeof rec.name === 'string' ? rec.name : UNKNOWN_REF;
|
|
44
|
+
}
|
|
45
|
+
/** Monta os params de `/issues.json` a partir dos filtros definidos. */
|
|
46
|
+
function structuredParams(filters) {
|
|
47
|
+
const params = {};
|
|
48
|
+
if (filters.project_id !== undefined)
|
|
49
|
+
params.project_id = filters.project_id;
|
|
50
|
+
if (filters.status_id !== undefined)
|
|
51
|
+
params.status_id = filters.status_id;
|
|
52
|
+
if (filters.assigned_to_id !== undefined)
|
|
53
|
+
params.assigned_to_id = filters.assigned_to_id;
|
|
54
|
+
if (filters.updated_on !== undefined)
|
|
55
|
+
params.updated_on = filters.updated_on;
|
|
56
|
+
return params;
|
|
57
|
+
}
|
|
58
|
+
/** Extrai a linha compacta (assunto/status/responsável) de um payload bruto. */
|
|
59
|
+
function toSearchListItem(payload) {
|
|
60
|
+
return {
|
|
61
|
+
id: payload.id,
|
|
62
|
+
subject: typeof payload.subject === 'string' ? payload.subject : null,
|
|
63
|
+
status: refName(payload.status) ?? UNKNOWN_REF,
|
|
64
|
+
assignee: refName(payload.assigned_to) ?? ABSENT_ASSIGNEE,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Busca issues combinando filtros estruturados e full-text best-effort.
|
|
69
|
+
*
|
|
70
|
+
* @param options - Ver {@link FetchIssueSearchOptions}.
|
|
71
|
+
* @returns Lista compacta em Markdown + metadados de degradação.
|
|
72
|
+
* @throws {RedmineAuthError} Em 401 (propagado do client).
|
|
73
|
+
* @throws {RedmineForbiddenError} Em 403.
|
|
74
|
+
* @throws {RedmineHttpError} Em outros status ≥ 400 de `/issues.json`.
|
|
75
|
+
* @example
|
|
76
|
+
* const res = await fetchIssueSearch({
|
|
77
|
+
* baseUrl, apiKey, filters: { project_id: 5 }, query: 'timeout', limit: 25,
|
|
78
|
+
* });
|
|
79
|
+
*/
|
|
80
|
+
export async function fetchIssueSearch(options) {
|
|
81
|
+
const { baseUrl, apiKey, filters, insecure = false } = options;
|
|
82
|
+
const limit = options.limit ?? SEARCH_DEFAULT_LIMIT;
|
|
83
|
+
const query = options.query?.trim();
|
|
84
|
+
const http = createHttpClient({ baseUrl, apiKey, insecure });
|
|
85
|
+
const warnings = [];
|
|
86
|
+
let degraded = false;
|
|
87
|
+
let payloads;
|
|
88
|
+
if (query !== undefined && query.length > 0) {
|
|
89
|
+
let hitIds;
|
|
90
|
+
try {
|
|
91
|
+
const page = await searchIssues(http, { query, limit });
|
|
92
|
+
// Ids únicos preservando a ordem de relevância do /search.
|
|
93
|
+
hitIds = [...new Set(page.hits.map((hit) => hit.id))];
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
// Degradação best-effort: /search indisponível não quebra a tool.
|
|
97
|
+
degraded = true;
|
|
98
|
+
warnings.push(`Busca full-text indisponível (${messageOf(error)}); exibindo apenas os filtros estruturados.`);
|
|
99
|
+
}
|
|
100
|
+
if (hitIds !== undefined) {
|
|
101
|
+
// Interseção texto ∩ filtros: restringe /issues.json pelos ids do /search.
|
|
102
|
+
payloads =
|
|
103
|
+
hitIds.length === 0
|
|
104
|
+
? []
|
|
105
|
+
: await listIssues(http, {
|
|
106
|
+
filters: { ...structuredParams(filters), issue_id: hitIds.join(',') },
|
|
107
|
+
maxItems: limit,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
payloads = await listIssues(http, { filters: structuredParams(filters), maxItems: limit });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
payloads = await listIssues(http, { filters: structuredParams(filters), maxItems: limit });
|
|
116
|
+
}
|
|
117
|
+
const items = payloads.slice(0, limit).map(toSearchListItem);
|
|
118
|
+
const content = buildSearchListMarkdown(items, { query, warnings });
|
|
119
|
+
return { content, count: items.length, warnings, degraded };
|
|
120
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const TOOL_NAME = "redmine-context";
|
|
2
|
+
export declare const TOOL_VERSION = "1.0.0";
|
|
3
|
+
export * from './contract.js';
|
|
4
|
+
export { fetchIssueBundle, type BundleFormat, type FetchIssueBundleOptions, type IssueBundleResult, } from './fetch-issue-bundle.js';
|
|
5
|
+
export { extractIssueAttachments, type ExtractIssueAttachmentsOptions, } from './extract-issue-attachments.js';
|
|
6
|
+
export { fetchAttachmentText, fetchAttachmentTextCacheFirst, AttachmentNotFoundError, type FetchAttachmentTextOptions, type FetchAttachmentTextCacheFirstOptions, type AttachmentTextResult, } from './fetch-attachment-text.js';
|
|
7
|
+
export { extractIssueAttachmentsCacheFirst, makeQueueBackgroundExtractor, processingResult, type BackgroundExtractionTarget, type BackgroundExtractor, type BackgroundCompute, type CacheFirstExtractionOptions, type QueueBackgroundOptions, } from './cache-first.js';
|
|
8
|
+
export { fetchIssueSearch, SEARCH_DEFAULT_LIMIT, type FetchIssueSearchOptions, type IssueSearchFilters, type IssueSearchResult, } from './fetch-issue-search.js';
|
|
9
|
+
export { searchIssues, type SearchIssuesOptions, type SearchIssuesPage } from './client/index.js';
|
|
10
|
+
export { createHttpClient, type HttpClient, type HttpClientOptions, type QueryParams, } from './client/index.js';
|
|
11
|
+
export { listIssues, type ListIssuesOptions, type RedmineIssuePayload } from './client/index.js';
|
|
12
|
+
export { getIssue } from './client/index.js';
|
|
13
|
+
export { normalizeIssue } from './normalize/index.js';
|
|
14
|
+
export { buildMarkdownBundle, buildJsonBundle, fenceBlock, type MarkdownBundleMeta, type JsonBundle, type JsonBundleEnvelope, type JsonBundleMeta, type JsonBundleSource, } from './bundle/index.js';
|
|
15
|
+
export { RedmineHttpError, RedmineAuthError, RedmineForbiddenError, RedmineNotFoundError, } from './client/index.js';
|
|
16
|
+
export { loginWithPassword, validateApiKey, RedmineLoginError, type LoginOptions, type LoginResult, type ValidateApiKeyOptions, } from './config/index.js';
|
|
17
|
+
export { createCredentialCascade, resolveApiKey, describeCredentialSource, normalizeInstanceUrl, CredentialStoreError, type CredentialCascadeOptions, type CredentialSourceKind, } from './config/index.js';
|
|
18
|
+
export { defaultSettingsStore, resolveInstanceUrl, type InstanceUrlOrigin, type ResolvedInstanceUrl, type SettingsStore, } from './config/index.js';
|
|
19
|
+
export { diagnoseBinaries, tesseractInstallHint, pdftotextInstallHint, ffmpegInstallHint, whisperInstallHint, type BinaryDiagnosis, type DiagnoseBinariesOptions, } from './config/index.js';
|
|
20
|
+
export { findFfmpeg, detectFfmpegVersion, findWhisper, whisperModelDir, type FfmpegLocation, type WhisperLocation, } from './extract/index.js';
|
|
21
|
+
export { downloadGgufModel, GgufDownloadError, GGUF_MODEL_NAME, GGUF_MODEL_URL, GGUF_MODEL_SHA256, type DownloadGgufOptions, type GgufDownloadFailure, } from './extract/index.js';
|