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.
Files changed (229) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +449 -0
  3. package/dist/bundle/index.d.ts +5 -0
  4. package/dist/bundle/index.js +5 -0
  5. package/dist/bundle/json.d.ts +90 -0
  6. package/dist/bundle/json.js +266 -0
  7. package/dist/bundle/markdown.d.ts +75 -0
  8. package/dist/bundle/markdown.js +294 -0
  9. package/dist/bundle/search-list.d.ts +43 -0
  10. package/dist/bundle/search-list.js +53 -0
  11. package/dist/bundle/stable-stringify.d.ts +26 -0
  12. package/dist/bundle/stable-stringify.js +50 -0
  13. package/dist/cache/contract.d.ts +157 -0
  14. package/dist/cache/contract.js +0 -0
  15. package/dist/cache/disk-index.d.ts +82 -0
  16. package/dist/cache/disk-index.js +220 -0
  17. package/dist/cache/disk.d.ts +133 -0
  18. package/dist/cache/disk.js +313 -0
  19. package/dist/cache/gc.d.ts +78 -0
  20. package/dist/cache/gc.js +123 -0
  21. package/dist/cache/get-or-compute.d.ts +36 -0
  22. package/dist/cache/get-or-compute.js +52 -0
  23. package/dist/cache/index.d.ts +9 -0
  24. package/dist/cache/index.js +8 -0
  25. package/dist/cache/keys.d.ts +76 -0
  26. package/dist/cache/keys.js +78 -0
  27. package/dist/cache/memory.d.ts +48 -0
  28. package/dist/cache/memory.js +110 -0
  29. package/dist/cache-first.d.ts +127 -0
  30. package/dist/cache-first.js +227 -0
  31. package/dist/client/errors.d.ts +33 -0
  32. package/dist/client/errors.js +49 -0
  33. package/dist/client/http.d.ts +110 -0
  34. package/dist/client/http.js +207 -0
  35. package/dist/client/index.d.ts +5 -0
  36. package/dist/client/index.js +5 -0
  37. package/dist/client/issues.d.ts +71 -0
  38. package/dist/client/issues.js +100 -0
  39. package/dist/client/search.d.ts +58 -0
  40. package/dist/client/search.js +81 -0
  41. package/dist/config/credentials.d.ts +247 -0
  42. package/dist/config/credentials.js +427 -0
  43. package/dist/config/doctor.d.ts +123 -0
  44. package/dist/config/doctor.js +260 -0
  45. package/dist/config/index.d.ts +6 -0
  46. package/dist/config/index.js +6 -0
  47. package/dist/config/keyring.d.ts +96 -0
  48. package/dist/config/keyring.js +158 -0
  49. package/dist/config/login.d.ts +97 -0
  50. package/dist/config/login.js +189 -0
  51. package/dist/config/settings.d.ts +94 -0
  52. package/dist/config/settings.js +140 -0
  53. package/dist/contract.d.ts +173 -0
  54. package/dist/contract.js +27 -0
  55. package/dist/core.d.ts +1 -0
  56. package/dist/core.js +8 -0
  57. package/dist/extract/audio-extractor.d.ts +105 -0
  58. package/dist/extract/audio-extractor.js +156 -0
  59. package/dist/extract/audio.d.ts +126 -0
  60. package/dist/extract/audio.js +184 -0
  61. package/dist/extract/dispatcher.d.ts +132 -0
  62. package/dist/extract/dispatcher.js +115 -0
  63. package/dist/extract/download.d.ts +111 -0
  64. package/dist/extract/download.js +261 -0
  65. package/dist/extract/duration.d.ts +106 -0
  66. package/dist/extract/duration.js +148 -0
  67. package/dist/extract/ffmpeg.d.ts +56 -0
  68. package/dist/extract/ffmpeg.js +95 -0
  69. package/dist/extract/gguf.d.ts +137 -0
  70. package/dist/extract/gguf.js +215 -0
  71. package/dist/extract/index.d.ts +19 -0
  72. package/dist/extract/index.js +19 -0
  73. package/dist/extract/magic.d.ts +80 -0
  74. package/dist/extract/magic.js +282 -0
  75. package/dist/extract/ooxml.d.ts +131 -0
  76. package/dist/extract/ooxml.js +336 -0
  77. package/dist/extract/pdf.d.ts +147 -0
  78. package/dist/extract/pdf.js +322 -0
  79. package/dist/extract/queue.d.ts +167 -0
  80. package/dist/extract/queue.js +217 -0
  81. package/dist/extract/subprocess.d.ts +145 -0
  82. package/dist/extract/subprocess.js +181 -0
  83. package/dist/extract/tesseract.d.ts +153 -0
  84. package/dist/extract/tesseract.js +321 -0
  85. package/dist/extract/video-extractor.d.ts +84 -0
  86. package/dist/extract/video-extractor.js +89 -0
  87. package/dist/extract/video.d.ts +198 -0
  88. package/dist/extract/video.js +418 -0
  89. package/dist/extract/which.d.ts +63 -0
  90. package/dist/extract/which.js +72 -0
  91. package/dist/extract/whisper-extract.d.ts +211 -0
  92. package/dist/extract/whisper-extract.js +323 -0
  93. package/dist/extract/whisper.d.ts +50 -0
  94. package/dist/extract/whisper.js +67 -0
  95. package/dist/extract/zip.d.ts +50 -0
  96. package/dist/extract/zip.js +165 -0
  97. package/dist/extract-issue-attachments.d.ts +82 -0
  98. package/dist/extract-issue-attachments.js +156 -0
  99. package/dist/fetch-attachment-text.d.ts +113 -0
  100. package/dist/fetch-attachment-text.js +155 -0
  101. package/dist/fetch-issue-bundle.d.ts +81 -0
  102. package/dist/fetch-issue-bundle.js +93 -0
  103. package/dist/fetch-issue-search.d.ts +74 -0
  104. package/dist/fetch-issue-search.js +120 -0
  105. package/dist/index.d.ts +21 -0
  106. package/dist/index.js +67 -0
  107. package/dist/normalize/collections.d.ts +52 -0
  108. package/dist/normalize/collections.js +170 -0
  109. package/dist/normalize/helpers.d.ts +35 -0
  110. package/dist/normalize/helpers.js +59 -0
  111. package/dist/normalize/index.d.ts +2 -0
  112. package/dist/normalize/index.js +2 -0
  113. package/dist/normalize/issue.d.ts +34 -0
  114. package/dist/normalize/issue.js +154 -0
  115. package/dist/surfaces/cli/commands.d.ts +61 -0
  116. package/dist/surfaces/cli/commands.js +262 -0
  117. package/dist/surfaces/cli/main.d.ts +32 -0
  118. package/dist/surfaces/cli/main.js +210 -0
  119. package/dist/surfaces/cli/prompts.d.ts +66 -0
  120. package/dist/surfaces/cli/prompts.js +147 -0
  121. package/dist/surfaces/cli/tty.d.ts +27 -0
  122. package/dist/surfaces/cli/tty.js +35 -0
  123. package/dist/surfaces/cli/types.d.ts +39 -0
  124. package/dist/surfaces/cli/types.js +7 -0
  125. package/dist/surfaces/mcp/server.d.ts +171 -0
  126. package/dist/surfaces/mcp/server.js +427 -0
  127. package/dist/surfaces/tui/app.d.ts +55 -0
  128. package/dist/surfaces/tui/app.js +180 -0
  129. package/dist/surfaces/tui/attachment-status.d.ts +79 -0
  130. package/dist/surfaces/tui/attachment-status.js +113 -0
  131. package/dist/surfaces/tui/components/breadcrumb.d.ts +7 -0
  132. package/dist/surfaces/tui/components/breadcrumb.js +31 -0
  133. package/dist/surfaces/tui/components/gradient-text.d.ts +23 -0
  134. package/dist/surfaces/tui/components/gradient-text.js +75 -0
  135. package/dist/surfaces/tui/components/scroll-view.d.ts +25 -0
  136. package/dist/surfaces/tui/components/scroll-view.js +77 -0
  137. package/dist/surfaces/tui/components/spinner.d.ts +12 -0
  138. package/dist/surfaces/tui/components/spinner.js +39 -0
  139. package/dist/surfaces/tui/components/text-input.d.ts +45 -0
  140. package/dist/surfaces/tui/components/text-input.js +114 -0
  141. package/dist/surfaces/tui/format-file-size.d.ts +24 -0
  142. package/dist/surfaces/tui/format-file-size.js +43 -0
  143. package/dist/surfaces/tui/glyphs.d.ts +47 -0
  144. package/dist/surfaces/tui/glyphs.js +84 -0
  145. package/dist/surfaces/tui/hooks/use-auth-guard.d.ts +39 -0
  146. package/dist/surfaces/tui/hooks/use-auth-guard.js +135 -0
  147. package/dist/surfaces/tui/hooks/use-doctor-status.d.ts +64 -0
  148. package/dist/surfaces/tui/hooks/use-doctor-status.js +123 -0
  149. package/dist/surfaces/tui/hooks/use-escape-interceptor.d.ts +25 -0
  150. package/dist/surfaces/tui/hooks/use-escape-interceptor.js +65 -0
  151. package/dist/surfaces/tui/hooks/use-exit-guard.d.ts +18 -0
  152. package/dist/surfaces/tui/hooks/use-exit-guard.js +66 -0
  153. package/dist/surfaces/tui/hooks/use-export-bundle.d.ts +62 -0
  154. package/dist/surfaces/tui/hooks/use-export-bundle.js +100 -0
  155. package/dist/surfaces/tui/hooks/use-issue-detail.d.ts +61 -0
  156. package/dist/surfaces/tui/hooks/use-issue-detail.js +132 -0
  157. package/dist/surfaces/tui/hooks/use-issue-search.d.ts +71 -0
  158. package/dist/surfaces/tui/hooks/use-issue-search.js +168 -0
  159. package/dist/surfaces/tui/hooks/use-list-navigation.d.ts +44 -0
  160. package/dist/surfaces/tui/hooks/use-list-navigation.js +82 -0
  161. package/dist/surfaces/tui/hooks/use-media-binaries.d.ts +24 -0
  162. package/dist/surfaces/tui/hooks/use-media-binaries.js +44 -0
  163. package/dist/surfaces/tui/hooks/use-my-issues.d.ts +66 -0
  164. package/dist/surfaces/tui/hooks/use-my-issues.js +151 -0
  165. package/dist/surfaces/tui/hooks/use-onboarding-callbacks.d.ts +11 -0
  166. package/dist/surfaces/tui/hooks/use-onboarding-callbacks.js +104 -0
  167. package/dist/surfaces/tui/hooks/use-terminal-width.d.ts +52 -0
  168. package/dist/surfaces/tui/hooks/use-terminal-width.js +90 -0
  169. package/dist/surfaces/tui/index.d.ts +50 -0
  170. package/dist/surfaces/tui/index.js +158 -0
  171. package/dist/surfaces/tui/instance.d.ts +37 -0
  172. package/dist/surfaces/tui/instance.js +36 -0
  173. package/dist/surfaces/tui/job-registry.d.ts +113 -0
  174. package/dist/surfaces/tui/job-registry.js +123 -0
  175. package/dist/surfaces/tui/job-status.d.ts +45 -0
  176. package/dist/surfaces/tui/job-status.js +81 -0
  177. package/dist/surfaces/tui/navigation.d.ts +74 -0
  178. package/dist/surfaces/tui/navigation.js +87 -0
  179. package/dist/surfaces/tui/palettes.d.ts +38 -0
  180. package/dist/surfaces/tui/palettes.js +244 -0
  181. package/dist/surfaces/tui/screen.d.ts +30 -0
  182. package/dist/surfaces/tui/screen.js +51 -0
  183. package/dist/surfaces/tui/screens/about.d.ts +2 -0
  184. package/dist/surfaces/tui/screens/about.js +35 -0
  185. package/dist/surfaces/tui/screens/appearance.d.ts +2 -0
  186. package/dist/surfaces/tui/screens/appearance.js +74 -0
  187. package/dist/surfaces/tui/screens/config.d.ts +2 -0
  188. package/dist/surfaces/tui/screens/config.js +82 -0
  189. package/dist/surfaces/tui/screens/doctor.d.ts +2 -0
  190. package/dist/surfaces/tui/screens/doctor.js +109 -0
  191. package/dist/surfaces/tui/screens/export.d.ts +2 -0
  192. package/dist/surfaces/tui/screens/export.js +168 -0
  193. package/dist/surfaces/tui/screens/home-selection.d.ts +70 -0
  194. package/dist/surfaces/tui/screens/home-selection.js +80 -0
  195. package/dist/surfaces/tui/screens/home.d.ts +2 -0
  196. package/dist/surfaces/tui/screens/home.js +200 -0
  197. package/dist/surfaces/tui/screens/issue-detail.d.ts +6 -0
  198. package/dist/surfaces/tui/screens/issue-detail.js +182 -0
  199. package/dist/surfaces/tui/screens/jobs.d.ts +7 -0
  200. package/dist/surfaces/tui/screens/jobs.js +89 -0
  201. package/dist/surfaces/tui/screens/loaded-issue-context.d.ts +49 -0
  202. package/dist/surfaces/tui/screens/loaded-issue-context.js +57 -0
  203. package/dist/surfaces/tui/screens/onboarding/api-key.d.ts +2 -0
  204. package/dist/surfaces/tui/screens/onboarding/api-key.js +69 -0
  205. package/dist/surfaces/tui/screens/onboarding/login.d.ts +2 -0
  206. package/dist/surfaces/tui/screens/onboarding/login.js +50 -0
  207. package/dist/surfaces/tui/screens/onboarding/mode.d.ts +2 -0
  208. package/dist/surfaces/tui/screens/onboarding/mode.js +42 -0
  209. package/dist/surfaces/tui/screens/onboarding/onboarding-context.d.ts +221 -0
  210. package/dist/surfaces/tui/screens/onboarding/onboarding-context.js +131 -0
  211. package/dist/surfaces/tui/screens/onboarding/success.d.ts +2 -0
  212. package/dist/surfaces/tui/screens/onboarding/success.js +41 -0
  213. package/dist/surfaces/tui/screens/onboarding/url.d.ts +24 -0
  214. package/dist/surfaces/tui/screens/onboarding/url.js +84 -0
  215. package/dist/surfaces/tui/screens/onboarding/validating.d.ts +1 -0
  216. package/dist/surfaces/tui/screens/onboarding/validating.js +86 -0
  217. package/dist/surfaces/tui/screens/welcome.d.ts +6 -0
  218. package/dist/surfaces/tui/screens/welcome.js +95 -0
  219. package/dist/surfaces/tui/status-color.d.ts +21 -0
  220. package/dist/surfaces/tui/status-color.js +23 -0
  221. package/dist/surfaces/tui/symbols.d.ts +231 -0
  222. package/dist/surfaces/tui/symbols.js +14 -0
  223. package/dist/surfaces/tui/terminal-colors.d.ts +29 -0
  224. package/dist/surfaces/tui/terminal-colors.js +39 -0
  225. package/dist/surfaces/tui/theme.d.ts +156 -0
  226. package/dist/surfaces/tui/theme.js +86 -0
  227. package/dist/surfaces/tui/truncate.d.ts +31 -0
  228. package/dist/surfaces/tui/truncate.js +81 -0
  229. package/package.json +93 -0
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Login por senha (Basic auth) para descobrir a api_key do usuário (M1-07).
3
+ *
4
+ * Fluxo: `GET {baseUrl}/users/current.json` com `Authorization: Basic
5
+ * base64(user:pass)`. A senha nunca é logada nem persistida; qualquer texto
6
+ * que possa vazar (mensagens de rede/parse) é redigido antes de compor um erro.
7
+ *
8
+ * A política de TLS (https obrigatório; `insecure` apenas com aviso ruidoso) é
9
+ * reutilizada de `client/http.ts` via {@link validateBaseUrl} — não duplicada.
10
+ * O fallback de 2FA (copiar a api_key em `/my/account`) segue o ADR-003.
11
+ */
12
+ import { Buffer } from 'node:buffer';
13
+ import { RedmineAuthError, redactSecret, validateBaseUrl } from '../client/index.js';
14
+ const noopLogger = { warn: () => undefined };
15
+ /**
16
+ * Erro de login por senha que não se enquadra num status HTTP tipado do client
17
+ * (entrada inválida, REST desabilitada, corpo inesperado, falha de rede/parse).
18
+ * Sempre construído com a mensagem já redigida — nunca carrega a senha.
19
+ */
20
+ export class RedmineLoginError extends Error {
21
+ constructor(message) {
22
+ super(message);
23
+ this.name = new.target.name;
24
+ }
25
+ }
26
+ /** Remove barras finais da baseUrl preservando o subpath. */
27
+ function trimTrailingSlash(baseUrl) {
28
+ return baseUrl.replace(/\/+$/, '');
29
+ }
30
+ /** Estreita `unknown` para um objeto indexável, ou `undefined` se não for. */
31
+ function asRecord(value) {
32
+ return typeof value === 'object' && value !== null ? value : undefined;
33
+ }
34
+ /** Retorna a string se o campo for uma string não vazia, senão `undefined`. */
35
+ function stringField(record, key) {
36
+ const value = record[key];
37
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
38
+ }
39
+ /**
40
+ * Autentica por senha e descobre a api_key do usuário.
41
+ *
42
+ * @param options - Ver {@link LoginOptions}.
43
+ * @returns {@link LoginResult} com a api_key e a identidade do usuário.
44
+ * @throws {RedmineLoginError} Entrada inválida, corpo sem usuário/api_key,
45
+ * REST desabilitada, falha de rede ou JSON inválido (senha sempre redigida).
46
+ * @throws {RedmineAuthError} Em 401 — com instrução de fallback 2FA (ADR-003).
47
+ * @throws {Error} Se a política de TLS for violada (via `validateBaseUrl`).
48
+ * @example
49
+ * const { apiKey, user } = await loginWithPassword({
50
+ * baseUrl: 'https://redmine.example',
51
+ * username: 'alice',
52
+ * password: '****',
53
+ * });
54
+ */
55
+ export async function loginWithPassword(options) {
56
+ const { baseUrl, username, password, insecure = false } = options;
57
+ const logger = options.logger ?? noopLogger;
58
+ if (username.length === 0) {
59
+ throw new RedmineLoginError('username é obrigatório e não pode ser vazio.');
60
+ }
61
+ if (password.length === 0) {
62
+ throw new RedmineLoginError('password é obrigatório e não pode ser vazio.');
63
+ }
64
+ // Reusa a política de TLS do client (ADR-003) — pode lançar antes de qualquer fetch.
65
+ validateBaseUrl(baseUrl, insecure, logger);
66
+ // Redige a senha de qualquer texto de baixo nível antes de compor uma mensagem.
67
+ const redact = (text) => redactSecret(text, password);
68
+ const origin = trimTrailingSlash(baseUrl);
69
+ const url = `${origin}/users/current.json`;
70
+ const accountUrl = `${origin}/my/account`;
71
+ const token = Buffer.from(`${username}:${password}`, 'utf8').toString('base64');
72
+ const headers = {
73
+ Accept: 'application/json',
74
+ Authorization: `Basic ${token}`,
75
+ };
76
+ let response;
77
+ try {
78
+ response = await fetch(url, { method: 'GET', headers });
79
+ }
80
+ catch (cause) {
81
+ const reason = cause instanceof Error ? cause.message : String(cause);
82
+ throw new RedmineLoginError(redact(`Falha de rede ao acessar ${url}: ${reason}`));
83
+ }
84
+ if (response.status === 401) {
85
+ throw new RedmineAuthError(`Autenticação falhou (401): usuário ou senha inválidos. ` +
86
+ `Se a conta usa autenticação de dois fatores (2FA), o login por senha não funciona — ` +
87
+ `copie sua api_key em ${accountUrl} e informe-a diretamente.`, 401, url);
88
+ }
89
+ if (!response.ok) {
90
+ throw new RedmineLoginError(redact(`GET ${url} respondeu ${response.status} ${response.statusText}.`));
91
+ }
92
+ let body;
93
+ try {
94
+ body = await response.json();
95
+ }
96
+ catch (cause) {
97
+ const reason = cause instanceof Error ? cause.message : String(cause);
98
+ throw new RedmineLoginError(redact(`Resposta de ${url} não é JSON válido: ${reason}`));
99
+ }
100
+ const rawUser = asRecord(asRecord(body)?.user);
101
+ const id = rawUser?.id;
102
+ const login = rawUser ? stringField(rawUser, 'login') : undefined;
103
+ if (rawUser === undefined || typeof id !== 'number' || login === undefined) {
104
+ throw new RedmineLoginError(`Resposta de ${url} não contém um usuário válido (campos id/login ausentes).`);
105
+ }
106
+ const apiKey = stringField(rawUser, 'api_key');
107
+ if (apiKey === undefined) {
108
+ throw new RedmineLoginError(`A resposta não contém api_key: a REST API pode estar desabilitada ou a conta não ter permissão. ` +
109
+ `Habilite a REST API e copie sua api_key em ${accountUrl}.`);
110
+ }
111
+ const firstname = stringField(rawUser, 'firstname');
112
+ const lastname = stringField(rawUser, 'lastname');
113
+ const name = [firstname, lastname].filter((part) => part !== undefined).join(' ') || login;
114
+ return { apiKey, user: { id, login, name } };
115
+ }
116
+ /**
117
+ * Valida uma api_key colada diretamente pelo usuário (fallback de 2FA do
118
+ * ADR-003, quando `loginWithPassword` falha com 401) contra `GET
119
+ * {baseUrl}/users/current.json`, autenticando com o header
120
+ * `X-Redmine-API-Key` em vez de Basic auth. A key nunca é logada; qualquer
121
+ * texto que possa vazar é redigido antes de compor um erro — mesma política
122
+ * de {@link loginWithPassword}.
123
+ *
124
+ * @param options - Ver {@link ValidateApiKeyOptions}.
125
+ * @returns {@link LoginResult} com a própria api_key (ecoada de volta, para
126
+ * simetria com {@link loginWithPassword}) e a identidade do usuário.
127
+ * @throws {RedmineLoginError} Entrada inválida, corpo sem usuário, REST
128
+ * desabilitada, falha de rede ou JSON inválido (key sempre redigida).
129
+ * @throws {RedmineAuthError} Em 401 — api_key inválida ou expirada.
130
+ * @throws {Error} Se a política de TLS for violada (via `validateBaseUrl`).
131
+ * @example
132
+ * const { apiKey, user } = await validateApiKey({
133
+ * baseUrl: 'https://redmine.example',
134
+ * apiKey: pastedKey,
135
+ * });
136
+ */
137
+ export async function validateApiKey(options) {
138
+ const { baseUrl, apiKey, insecure = false } = options;
139
+ const logger = options.logger ?? noopLogger;
140
+ if (apiKey.length === 0) {
141
+ throw new RedmineLoginError('api_key é obrigatória e não pode ser vazia.');
142
+ }
143
+ // Reusa a política de TLS do client (ADR-003) — pode lançar antes de qualquer fetch.
144
+ validateBaseUrl(baseUrl, insecure, logger);
145
+ // Redige a key de qualquer texto de baixo nível antes de compor uma mensagem.
146
+ const redact = (text) => redactSecret(text, apiKey);
147
+ const origin = trimTrailingSlash(baseUrl);
148
+ const url = `${origin}/users/current.json`;
149
+ const headers = {
150
+ Accept: 'application/json',
151
+ 'X-Redmine-API-Key': apiKey,
152
+ };
153
+ let response;
154
+ try {
155
+ response = await fetch(url, { method: 'GET', headers });
156
+ }
157
+ catch (cause) {
158
+ const reason = cause instanceof Error ? cause.message : String(cause);
159
+ throw new RedmineLoginError(redact(`Falha de rede ao acessar ${url}: ${reason}`));
160
+ }
161
+ if (response.status === 401) {
162
+ throw new RedmineAuthError(`Autenticação falhou (401): api_key inválida ou expirada. Copie a api_key atual em ${accountUrlFor(origin)}.`, 401, url);
163
+ }
164
+ if (!response.ok) {
165
+ throw new RedmineLoginError(redact(`GET ${url} respondeu ${response.status} ${response.statusText}.`));
166
+ }
167
+ let body;
168
+ try {
169
+ body = await response.json();
170
+ }
171
+ catch (cause) {
172
+ const reason = cause instanceof Error ? cause.message : String(cause);
173
+ throw new RedmineLoginError(redact(`Resposta de ${url} não é JSON válido: ${reason}`));
174
+ }
175
+ const rawUser = asRecord(asRecord(body)?.user);
176
+ const id = rawUser?.id;
177
+ const login = rawUser ? stringField(rawUser, 'login') : undefined;
178
+ if (rawUser === undefined || typeof id !== 'number' || login === undefined) {
179
+ throw new RedmineLoginError(`Resposta de ${url} não contém um usuário válido (campos id/login ausentes).`);
180
+ }
181
+ const firstname = stringField(rawUser, 'firstname');
182
+ const lastname = stringField(rawUser, 'lastname');
183
+ const name = [firstname, lastname].filter((part) => part !== undefined).join(' ') || login;
184
+ return { apiKey, user: { id, login, name } };
185
+ }
186
+ /** Monta a URL de `/my/account` a partir da origem já sem barra final. */
187
+ function accountUrlFor(origin) {
188
+ return `${origin}/my/account`;
189
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Store de CONFIGURAÇÕES NÃO SECRETAS do usuário (#187).
3
+ *
4
+ * Hoje guarda apenas a **URL da instância** "default/última usada", para que a
5
+ * TUI/CLI/MCP não dependam EXCLUSIVAMENTE de `REDMINE_URL` no ambiente: o `login`
6
+ * persiste a instância autenticada e as superfícies a usam como fallback.
7
+ *
8
+ * Diferente do credential store (ADR-003), aqui NÃO há segredo — a URL não é
9
+ * sensível — então o arquivo usa permissões default (sem `0600`). Fica em
10
+ * `env-paths('redmine-context').config/settings.json`. Parse defensivo: JSON
11
+ * inválido/ausente NUNCA quebra o boot (degrada para "sem configuração"), pois,
12
+ * ao contrário da api_key, nada aqui é irrecuperável.
13
+ *
14
+ * RESOLUÇÃO DA INSTÂNCIA ({@link resolveInstanceUrl}, pura): a ordem de precedência
15
+ * é `--url` (flag) → `REDMINE_URL` (env) → URL persistida. Isso preserva a
16
+ * retrocompatibilidade (flag/env sempre vencem) e a convenção env-first do MCP
17
+ * headless, adicionando a persistência só como último recurso.
18
+ */
19
+ /** Formato persistido (evolutivo). */
20
+ export interface Settings {
21
+ /** URL base da instância "default/última usada" (normalizada ao gravar). */
22
+ instanceUrl?: string;
23
+ /** Id da paleta de cores da TUI escolhida (#190). */
24
+ palette?: string;
25
+ }
26
+ /** Store de settings não-secretas: URL da instância + paleta da TUI. */
27
+ export interface SettingsStore {
28
+ /** URL da instância persistida, ou `undefined` se nenhuma. */
29
+ getInstanceUrl(): Promise<string | undefined>;
30
+ /** Persiste (normalizada) a URL da instância. */
31
+ setInstanceUrl(url: string): Promise<void>;
32
+ /** Remove a URL da instância persistida; no-op se não existir. */
33
+ clearInstanceUrl(): Promise<void>;
34
+ /** Id da paleta de cores da TUI persistida, ou `undefined` se nenhuma (#190). */
35
+ getPaletteId(): Promise<string | undefined>;
36
+ /** Persiste o id da paleta de cores da TUI (#190). */
37
+ setPaletteId(id: string): Promise<void>;
38
+ }
39
+ /** Caminho padrão do arquivo de settings (diretório de config do usuário). */
40
+ export declare function defaultSettingsPath(): string;
41
+ /** Opções do {@link FileSettingsStore}. */
42
+ export interface FileSettingsStoreOptions {
43
+ /** Caminho do arquivo; default via `env-paths`. Útil para testes. */
44
+ filePath?: string;
45
+ }
46
+ /**
47
+ * {@link SettingsStore} baseado num arquivo JSON não-secreto. Leitura defensiva
48
+ * (ENOENT/JSON inválido → sem configuração); escrita cria o diretório sob demanda.
49
+ */
50
+ export declare class FileSettingsStore implements SettingsStore {
51
+ private readonly filePath;
52
+ constructor(options?: FileSettingsStoreOptions);
53
+ /**
54
+ * Lê o objeto BRUTO do arquivo (preservando chaves desconhecidas para
55
+ * forward-compat); qualquer problema degrada para `{}` (nunca lança).
56
+ */
57
+ private readRaw;
58
+ /**
59
+ * Escreve o objeto ATOMICAMENTE (arquivo temp + `rename`), criando o diretório
60
+ * sob demanda. O `rename` no mesmo diretório é atômico nos SOs suportados —
61
+ * evita deixar um `settings.json` truncado se o processo morrer no meio da escrita.
62
+ */
63
+ private write;
64
+ getInstanceUrl(): Promise<string | undefined>;
65
+ setInstanceUrl(url: string): Promise<void>;
66
+ clearInstanceUrl(): Promise<void>;
67
+ getPaletteId(): Promise<string | undefined>;
68
+ setPaletteId(id: string): Promise<void>;
69
+ }
70
+ /** {@link SettingsStore} padrão (arquivo em `env-paths().config`). */
71
+ export declare function defaultSettingsStore(): SettingsStore;
72
+ /** Origem de onde a URL da instância foi resolvida (para diagnóstico/UX). */
73
+ export type InstanceUrlOrigin = 'flag' | 'env' | 'config';
74
+ /** URL da instância resolvida + de onde veio. */
75
+ export interface ResolvedInstanceUrl {
76
+ /** URL base da instância. */
77
+ url: string;
78
+ /** Origem: `flag` (--url), `env` (REDMINE_URL) ou `config` (persistida). */
79
+ origin: InstanceUrlOrigin;
80
+ }
81
+ /**
82
+ * Resolve a URL da instância por precedência: `--url` → `REDMINE_URL` → persistida.
83
+ * Função PURA — o chamador carrega a URL persistida (assíncrona) e a injeta aqui.
84
+ *
85
+ * @param opts - `flagUrl` (--url), `envUrl` (REDMINE_URL) e `persistedUrl` (settings).
86
+ * @returns A URL resolvida + origem, ou `undefined` se nenhuma fonte tiver valor.
87
+ * @example
88
+ * resolveInstanceUrl({ envUrl: 'https://r.example' }); // { url: 'https://r.example', origin: 'env' }
89
+ */
90
+ export declare function resolveInstanceUrl(opts: {
91
+ flagUrl?: string | undefined;
92
+ envUrl?: string | undefined;
93
+ persistedUrl?: string | undefined;
94
+ }): ResolvedInstanceUrl | undefined;
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Store de CONFIGURAÇÕES NÃO SECRETAS do usuário (#187).
3
+ *
4
+ * Hoje guarda apenas a **URL da instância** "default/última usada", para que a
5
+ * TUI/CLI/MCP não dependam EXCLUSIVAMENTE de `REDMINE_URL` no ambiente: o `login`
6
+ * persiste a instância autenticada e as superfícies a usam como fallback.
7
+ *
8
+ * Diferente do credential store (ADR-003), aqui NÃO há segredo — a URL não é
9
+ * sensível — então o arquivo usa permissões default (sem `0600`). Fica em
10
+ * `env-paths('redmine-context').config/settings.json`. Parse defensivo: JSON
11
+ * inválido/ausente NUNCA quebra o boot (degrada para "sem configuração"), pois,
12
+ * ao contrário da api_key, nada aqui é irrecuperável.
13
+ *
14
+ * RESOLUÇÃO DA INSTÂNCIA ({@link resolveInstanceUrl}, pura): a ordem de precedência
15
+ * é `--url` (flag) → `REDMINE_URL` (env) → URL persistida. Isso preserva a
16
+ * retrocompatibilidade (flag/env sempre vencem) e a convenção env-first do MCP
17
+ * headless, adicionando a persistência só como último recurso.
18
+ */
19
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
20
+ import { dirname, join } from 'node:path';
21
+ import envPaths from 'env-paths';
22
+ import { normalizeInstanceUrl } from './credentials.js';
23
+ /** Nome do arquivo de settings no diretório de config do usuário. */
24
+ const SETTINGS_FILENAME = 'settings.json';
25
+ /** Caminho padrão do arquivo de settings (diretório de config do usuário). */
26
+ export function defaultSettingsPath() {
27
+ return join(envPaths('redmine-context').config, SETTINGS_FILENAME);
28
+ }
29
+ /** Verifica se um erro do fs é "arquivo não encontrado" (ENOENT). */
30
+ function isNotFound(cause) {
31
+ return (typeof cause === 'object' &&
32
+ cause !== null &&
33
+ cause.code === 'ENOENT');
34
+ }
35
+ /**
36
+ * {@link SettingsStore} baseado num arquivo JSON não-secreto. Leitura defensiva
37
+ * (ENOENT/JSON inválido → sem configuração); escrita cria o diretório sob demanda.
38
+ */
39
+ export class FileSettingsStore {
40
+ filePath;
41
+ constructor(options = {}) {
42
+ this.filePath = options.filePath ?? defaultSettingsPath();
43
+ }
44
+ /**
45
+ * Lê o objeto BRUTO do arquivo (preservando chaves desconhecidas para
46
+ * forward-compat); qualquer problema degrada para `{}` (nunca lança).
47
+ */
48
+ async readRaw() {
49
+ let raw;
50
+ try {
51
+ raw = await readFile(this.filePath, 'utf8');
52
+ }
53
+ catch (cause) {
54
+ if (isNotFound(cause))
55
+ return {};
56
+ throw cause;
57
+ }
58
+ let parsed;
59
+ try {
60
+ parsed = JSON.parse(raw);
61
+ }
62
+ catch {
63
+ // Config não-secreta: JSON corrompido degrada para "sem config" (a URL é
64
+ // re-obtida no próximo login), em vez de travar o boot como no credential store.
65
+ return {};
66
+ }
67
+ return typeof parsed === 'object' && parsed !== null ? parsed : {};
68
+ }
69
+ /**
70
+ * Escreve o objeto ATOMICAMENTE (arquivo temp + `rename`), criando o diretório
71
+ * sob demanda. O `rename` no mesmo diretório é atômico nos SOs suportados —
72
+ * evita deixar um `settings.json` truncado se o processo morrer no meio da escrita.
73
+ */
74
+ async write(data) {
75
+ await mkdir(dirname(this.filePath), { recursive: true });
76
+ const tmp = `${this.filePath}.${process.pid}.tmp`;
77
+ await writeFile(tmp, `${JSON.stringify(data, null, 2)}\n`);
78
+ await rename(tmp, this.filePath);
79
+ }
80
+ async getInstanceUrl() {
81
+ const url = (await this.readRaw()).instanceUrl;
82
+ // Rejeita string vazia OU só-whitespace (ex.: settings.json editado à mão) —
83
+ // mesma semântica do `nonEmpty()` do resolvedor, evitando um baseUrl de espaços.
84
+ const trimmed = typeof url === 'string' ? url.trim() : '';
85
+ return trimmed.length > 0 ? trimmed : undefined;
86
+ }
87
+ async setInstanceUrl(url) {
88
+ const normalized = normalizeInstanceUrl(url);
89
+ const data = await this.readRaw();
90
+ data.instanceUrl = normalized;
91
+ await this.write(data);
92
+ }
93
+ async clearInstanceUrl() {
94
+ const data = await this.readRaw();
95
+ if (!('instanceUrl' in data))
96
+ return;
97
+ delete data.instanceUrl;
98
+ await this.write(data);
99
+ }
100
+ async getPaletteId() {
101
+ const id = (await this.readRaw()).palette;
102
+ const trimmed = typeof id === 'string' ? id.trim() : '';
103
+ return trimmed.length > 0 ? trimmed : undefined;
104
+ }
105
+ async setPaletteId(id) {
106
+ const data = await this.readRaw();
107
+ data.palette = id;
108
+ await this.write(data);
109
+ }
110
+ }
111
+ /** {@link SettingsStore} padrão (arquivo em `env-paths().config`). */
112
+ export function defaultSettingsStore() {
113
+ return new FileSettingsStore();
114
+ }
115
+ /** Retorna a string aparada se não-vazia, senão `undefined`. */
116
+ function nonEmpty(value) {
117
+ const trimmed = value?.trim();
118
+ return trimmed !== undefined && trimmed.length > 0 ? trimmed : undefined;
119
+ }
120
+ /**
121
+ * Resolve a URL da instância por precedência: `--url` → `REDMINE_URL` → persistida.
122
+ * Função PURA — o chamador carrega a URL persistida (assíncrona) e a injeta aqui.
123
+ *
124
+ * @param opts - `flagUrl` (--url), `envUrl` (REDMINE_URL) e `persistedUrl` (settings).
125
+ * @returns A URL resolvida + origem, ou `undefined` se nenhuma fonte tiver valor.
126
+ * @example
127
+ * resolveInstanceUrl({ envUrl: 'https://r.example' }); // { url: 'https://r.example', origin: 'env' }
128
+ */
129
+ export function resolveInstanceUrl(opts) {
130
+ const flag = nonEmpty(opts.flagUrl);
131
+ if (flag !== undefined)
132
+ return { url: flag, origin: 'flag' };
133
+ const env = nonEmpty(opts.envUrl);
134
+ if (env !== undefined)
135
+ return { url: env, origin: 'env' };
136
+ const persisted = nonEmpty(opts.persistedUrl);
137
+ if (persisted !== undefined)
138
+ return { url: persisted, origin: 'config' };
139
+ return undefined;
140
+ }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Contrato público do core (ADR-005).
3
+ *
4
+ * FRONTEIRA (boundary): este é o ÚNICO módulo que as superfícies (TUI, CLI, MCP)
5
+ * devem importar para consumir o core. Nenhuma superfície acessa os módulos
6
+ * internos (`src/client`, `src/normalize`, `src/extract`, `src/bundle`,
7
+ * `src/config`, `src/cache`) diretamente — o acoplamento se dá exclusivamente
8
+ * por estes tipos e pelo padrão `AsyncIterable<ProgressEvent | Result>`.
9
+ * A regra eslint `no-restricted-imports` (escopo `src/surfaces/**`) reforça isso.
10
+ */
11
+ /** Referência nomeada do Redmine (`{ id, name }`) — projeto, tracker, status, usuário, etc. */
12
+ export interface RedmineRef {
13
+ id: number;
14
+ name: string;
15
+ }
16
+ /**
17
+ * Custom field normalizado — SEM coerção de tipo (ADR-005).
18
+ * `raw_value` preserva o valor bruto exatamente como a API devolveu (incl. `null`
19
+ * ou `""` em campo vazio); em campos "multiple" é `string[]`.
20
+ * `value` normaliza apenas ausência: `""`/`null`/ausente → `null`. Nada além disso.
21
+ */
22
+ export interface CustomField {
23
+ id: number;
24
+ name: string;
25
+ value: string | string[] | null;
26
+ raw_value: string | string[] | null;
27
+ field_format?: string;
28
+ }
29
+ /** Detalhe bruto de um journal (histórico de status/atributos), sem interpretação. */
30
+ export interface JournalDetail {
31
+ property: string;
32
+ name: string;
33
+ old_value?: string | null;
34
+ new_value?: string | null;
35
+ }
36
+ /** Entrada de journal: nota opcional + `details[]` brutos. */
37
+ export interface Journal {
38
+ id: number;
39
+ notes?: string;
40
+ created_on: string;
41
+ user?: RedmineRef;
42
+ details: JournalDetail[];
43
+ }
44
+ /** Anexo normalizado; `digest` é opcional (nem sempre presente na API). */
45
+ export interface Attachment {
46
+ id: number;
47
+ filename: string;
48
+ filesize: number;
49
+ content_type?: string;
50
+ description?: string;
51
+ author?: RedmineRef;
52
+ created_on: string;
53
+ content_url: string;
54
+ digest?: string;
55
+ }
56
+ /** Relação entre issues: `relation_type` + `delay` (ADR-005). */
57
+ export interface IssueRelation {
58
+ id: number;
59
+ issue_id: number;
60
+ issue_to_id: number;
61
+ relation_type: string;
62
+ delay?: number | null;
63
+ }
64
+ /** Referência a uma issue-filha (parent/children). */
65
+ export interface IssueChild {
66
+ id: number;
67
+ tracker?: RedmineRef;
68
+ subject?: string;
69
+ }
70
+ /** Issue normalizada — modelo estável consumido pelas superfícies. */
71
+ export interface Issue {
72
+ id: number;
73
+ subject: string;
74
+ description?: string;
75
+ project: RedmineRef;
76
+ tracker: RedmineRef;
77
+ status: RedmineRef;
78
+ priority: RedmineRef;
79
+ author: RedmineRef;
80
+ assigned_to?: RedmineRef;
81
+ created_on: string;
82
+ updated_on: string;
83
+ done_ratio?: number;
84
+ start_date?: string;
85
+ due_date?: string;
86
+ custom_fields: CustomField[];
87
+ journals: Journal[];
88
+ attachments: Attachment[];
89
+ relations: IssueRelation[];
90
+ parent?: {
91
+ id: number;
92
+ };
93
+ children: IssueChild[];
94
+ /** Opcional: pode faltar por degradação em 403 (ADR-005). */
95
+ watchers?: RedmineRef[];
96
+ }
97
+ /**
98
+ * Status de uma extração de anexo (ADR-005 — pipeline de extração, M3/M4).
99
+ *
100
+ * Vocabulário único e canônico do core; as superfícies (TUI/CLI/MCP) apenas o
101
+ * consomem. Ver a migração documentada em `src/surfaces/tui/attachment-status.ts`.
102
+ *
103
+ * - `text` — anexo textual (o conteúdo já É o texto, sem OCR/ASR).
104
+ * - `pending` — enfileirado/aguardando, ainda não processado.
105
+ * - `processing` — extração em andamento (job assíncrono).
106
+ * - `done` — extração concluída; `text` (e/ou `artifacts`) disponível(is).
107
+ * - `failed` — extração tentada e falhou (ver `metadata.reason`).
108
+ * - `cancelled` — extração cancelada via `AbortSignal` (o subprocesso foi morto
109
+ * ou o job pendente não chegou a iniciar) (#69, ADR-005).
110
+ * - `unsupported` — não há extrator registrado para o MIME REAL do arquivo.
111
+ * - `skipped` — pulado deliberadamente (ex.: excede o limite de tamanho, ADR-002).
112
+ */
113
+ export type ExtractionStatus = 'text' | 'pending' | 'processing' | 'done' | 'failed' | 'cancelled' | 'unsupported' | 'skipped';
114
+ /**
115
+ * Artefato derivado de uma extração (arquivo produzido e cacheado ao lado do
116
+ * `original` do anexo, ADR-004) — ex.: transcrição, texto de página, thumbnail.
117
+ */
118
+ export interface ExtractionArtifact {
119
+ /** Tipo do artefato (ex.: `transcript`, `page-text`, `thumbnail`). */
120
+ kind: string;
121
+ /** Caminho absoluto do artefato no cache local. */
122
+ path: string;
123
+ /** MIME do artefato, quando conhecido. */
124
+ mime?: string;
125
+ }
126
+ /**
127
+ * Resultado de uma extração de anexo (ADR-005).
128
+ *
129
+ * `status` é sempre presente; `text`/`confidence`/`artifacts` são preenchidos
130
+ * conforme o extrator e o status (ex.: `done` traz `text`; `unsupported`/
131
+ * `skipped` trazem apenas `status` + `metadata`). `mime` carrega o MIME REAL
132
+ * detectado por magic bytes (NUNCA por extensão/Content-Type).
133
+ */
134
+ export interface ExtractionResult {
135
+ /** Estado final/atual da extração. */
136
+ status: ExtractionStatus;
137
+ /** Texto extraído (quando `status === 'text'` ou `'done'`). */
138
+ text?: string;
139
+ /** Confiança da extração em [0, 1] (ex.: score de OCR/ASR). */
140
+ confidence?: number;
141
+ /** Artefatos derivados produzidos pela extração. */
142
+ artifacts?: ExtractionArtifact[];
143
+ /** MIME REAL detectado por magic bytes do arquivo baixado. */
144
+ mime?: string;
145
+ /** Metadados livres (ex.: `extractorId`, `version`, `reason`, `declaredMime`). */
146
+ metadata?: Record<string, unknown>;
147
+ }
148
+ /** Evento de progresso incremental emitido durante uma operação longa. */
149
+ export interface ProgressEvent {
150
+ kind: 'progress';
151
+ stage: string;
152
+ message: string;
153
+ }
154
+ /** Resultado final de uma operação. Sempre o último item do iterable. */
155
+ export interface Result<T> {
156
+ kind: 'result';
157
+ value: T;
158
+ }
159
+ /** União discriminada consumida pelas superfícies. */
160
+ export type CoreEvent<T> = ProgressEvent | Result<T>;
161
+ /**
162
+ * Operação de exemplo que fixa o padrão de consumo do core.
163
+ *
164
+ * Não implementa nenhum client real — apenas demonstra o contrato:
165
+ * emite progresso incremental e finaliza com um único `Result`.
166
+ *
167
+ * @returns Sequência assíncrona de progresso terminada por um Result.
168
+ * @example
169
+ * for await (const event of demoOperation()) {
170
+ * if (event.kind === 'result') console.log(event.value);
171
+ * }
172
+ */
173
+ export declare function demoOperation(): AsyncIterable<ProgressEvent | Result<string>>;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Contrato público do core (ADR-005).
3
+ *
4
+ * FRONTEIRA (boundary): este é o ÚNICO módulo que as superfícies (TUI, CLI, MCP)
5
+ * devem importar para consumir o core. Nenhuma superfície acessa os módulos
6
+ * internos (`src/client`, `src/normalize`, `src/extract`, `src/bundle`,
7
+ * `src/config`, `src/cache`) diretamente — o acoplamento se dá exclusivamente
8
+ * por estes tipos e pelo padrão `AsyncIterable<ProgressEvent | Result>`.
9
+ * A regra eslint `no-restricted-imports` (escopo `src/surfaces/**`) reforça isso.
10
+ */
11
+ /**
12
+ * Operação de exemplo que fixa o padrão de consumo do core.
13
+ *
14
+ * Não implementa nenhum client real — apenas demonstra o contrato:
15
+ * emite progresso incremental e finaliza com um único `Result`.
16
+ *
17
+ * @returns Sequência assíncrona de progresso terminada por um Result.
18
+ * @example
19
+ * for await (const event of demoOperation()) {
20
+ * if (event.kind === 'result') console.log(event.value);
21
+ * }
22
+ */
23
+ export async function* demoOperation() {
24
+ yield { kind: 'progress', stage: 'start', message: 'iniciando operação' };
25
+ yield { kind: 'progress', stage: 'work', message: 'processando' };
26
+ yield { kind: 'result', value: 'done' };
27
+ }
package/dist/core.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare const CORE_MODULES: readonly ["client", "normalize", "extract", "bundle", "config", "cache"];
package/dist/core.js ADDED
@@ -0,0 +1,8 @@
1
+ import { MODULE_NAME as bundle } from './bundle/index.js';
2
+ import { MODULE_NAME as cache } from './cache/index.js';
3
+ import { MODULE_NAME as client } from './client/index.js';
4
+ import { MODULE_NAME as config } from './config/index.js';
5
+ import { MODULE_NAME as extract } from './extract/index.js';
6
+ import { MODULE_NAME as normalize } from './normalize/index.js';
7
+ // Os 6 módulos do core, na ordem das camadas do ADR-005.
8
+ export const CORE_MODULES = [client, normalize, extract, bundle, config, cache];