suite-timesheet-mcp 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/CONTRACT.md +143 -0
- package/LICENSE +99 -0
- package/README.md +202 -0
- package/SECURITY.md +80 -0
- package/lib/autoarranque.js +90 -0
- package/lib/cliente.js +30 -0
- package/lib/contexto.js +14 -0
- package/lib/ferramentas.js +188 -0
- package/lib/fila.js +193 -0
- package/lib/rotas.js +148 -0
- package/lib/validar.js +57 -0
- package/mcp.js +45 -0
- package/package.json +47 -0
- package/serve.js +182 -0
package/CONTRACT.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Contrato
|
|
2
|
+
|
|
3
|
+
A fonte deste contrato é o `CONTRACT.md` do repo da extensão (`suite-timesheet-importer`). Esta
|
|
4
|
+
cópia existe para quem só clona este pacote. Se divergirem, manda o da extensão.
|
|
5
|
+
|
|
6
|
+
## Ponte (opcional): o serviço manda comandos à extensão
|
|
7
|
+
|
|
8
|
+
Um serviço que implemente estas três rotas passa a poder pedir leituras e propor lançamentos
|
|
9
|
+
à extensão. Quem só quer entregar um ficheiro de horas não precisa delas.
|
|
10
|
+
|
|
11
|
+
| método | rota | corpo / resposta |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `POST` | `/bridge/contexto` | a extensão envia `{ano, mes, diasNoMes, projetos: [{option_id, nome, trancado}], lidoEm}` sempre que a Folha de Horas carrega ou muda de mês |
|
|
14
|
+
| `GET` | `/bridge/next?wait=25` | responde `204` sem corpo se não houver comando ao fim de `wait` segundos, ou `200` com um comando |
|
|
15
|
+
| `POST` | `/bridge/result/{id}` | a extensão publica o resultado do comando `id` |
|
|
16
|
+
|
|
17
|
+
Comando: `{"id": "texto", "tipo": "estado" | "projetos" | "ler" | "propor" | "aplicar", "ano": 2026, "mes": 9}`.
|
|
18
|
+
`propor` leva ainda `"linhas": [{option_id | project, date, hours}]`, `"espelho": false` e
|
|
19
|
+
`"nome": "texto"`. O mês tem de ser o que a página mostra; caso contrário a resposta é
|
|
20
|
+
`ERR_MES_DIFERENTE` e nada é lido nem escrito.
|
|
21
|
+
|
|
22
|
+
### `aplicar`: escreve uma proposta pendente sem o clique no painel
|
|
23
|
+
|
|
24
|
+
`aplicar` leva ainda `"proposta": "id"`, o `id` de um comando `propor` já entregue à extensão e
|
|
25
|
+
ainda sem `final` (ou seja, com o painel aberto à espera de decisão). Em vez de esperar que o
|
|
26
|
+
utilizador clique Aplicar, o cliente MCP (com a aprovação humana já dada no chat, e a aprovação
|
|
27
|
+
da própria chamada da tool) manda escrever essa proposta diretamente.
|
|
28
|
+
|
|
29
|
+
A extensão trata `aplicar` como um comando novo e independente (com o seu próprio `id`), que
|
|
30
|
+
resolve o `propor` referido em `proposta`:
|
|
31
|
+
|
|
32
|
+
- Escreve linha a linha, tal como faria depois do clique em Aplicar — mesmas validações (linhas
|
|
33
|
+
trancadas recusadas, sessão expirada para tudo), mesmo `report`.
|
|
34
|
+
- Responde ao comando `aplicar` com `{"ok": true, "dados": {"estado": "aplicado" | "erro",
|
|
35
|
+
"report"?, "stopped"?, "erro"?}}` (uma só fase, sem `previa`) ou `{"ok": false, "erro":
|
|
36
|
+
{"code", "message"}}`.
|
|
37
|
+
- Publica também o `final` do `propor` original (mesmo `{"fase": "final", "estado": ...}` de
|
|
38
|
+
sempre), para quem ainda estiver a chamar `resultado(<id do propor>)` continuar a funcionar.
|
|
39
|
+
|
|
40
|
+
`ERR_PROPOSTA_DESCONHECIDA`: não há nenhum `propor` pendente com o `id` indicado em `proposta`
|
|
41
|
+
(nunca existiu, já teve `final`, ou o `serve`/worker reiniciou e perdeu-o). `aplicar` nunca é
|
|
42
|
+
bloqueado por `ERR_OCUPADO` — não é um novo `propor` à espera de painel, é a resolução de um que
|
|
43
|
+
já existe.
|
|
44
|
+
|
|
45
|
+
Resultado: `{"ok": true, "dados": ...}` ou `{"ok": false, "erro": {"code", "message"}}`.
|
|
46
|
+
`propor` publica dois resultados com o mesmo `id`: primeiro `{"fase": "previa", "previa": {...}}`
|
|
47
|
+
com o plano calculado (o painel abre nesse momento), e depois, quando o utilizador decide,
|
|
48
|
+
`{"fase": "final", "estado": "aplicado" | "cancelado" | "erro", ...}`.
|
|
49
|
+
|
|
50
|
+
A extensão continua a ser a única coisa que escreve no Suite, sempre depois de o utilizador
|
|
51
|
+
clicar Aplicar no painel ou de aprovar a tool `aplicar` no cliente MCP (ver secção "aplicar"
|
|
52
|
+
abaixo). Nunca submete o mês, nunca muda o mês.
|
|
53
|
+
|
|
54
|
+
### Prazo de `/bridge/next`
|
|
55
|
+
|
|
56
|
+
O `serve` tem de responder a `GET /bridge/next?wait=25` dentro de, aproximadamente, 28 segundos
|
|
57
|
+
— mesmo sem comando (nesse caso, `204`). O worker pede `wait=25` (e este `serve` recusa um
|
|
58
|
+
`wait` maior que 25), mas o Chrome mata um service worker MV3 cujo `fetch` demora mais de 30 s a
|
|
59
|
+
resolver; um `serve` que ignore o `wait` ou que demore a responder além desse limite faz o
|
|
60
|
+
worker perder o ciclo (e reentrar só ao alarme de 30 s seguinte), mesmo que a resposta acabe por
|
|
61
|
+
chegar.
|
|
62
|
+
|
|
63
|
+
### Formato de `dados` por tipo de comando
|
|
64
|
+
|
|
65
|
+
Resultado com `"ok": true`, no campo `dados`:
|
|
66
|
+
|
|
67
|
+
| `tipo` | forma de `dados` |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `estado` | `{ano, mes, projetos: N, loteEmCurso: boolean}` — `projetos` é a contagem, não a lista |
|
|
70
|
+
| `projetos` | `[{option_id, nome, trancado}]` |
|
|
71
|
+
| `ler` | `{ano, mes, linhas: [{option_id, nome, status, dias: {D: horas}, total}], totaisPorDia: {D: horas}}` |
|
|
72
|
+
| `propor`, fase `previa` | `{fase: 'previa', previa: {matriz, criar, atualizar, apagar, ignoradas, avisos, erros, bloqueio}}` — `criar`/`atualizar`/`apagar` são listas de `{option_id, projeto, dias}`; `bloqueio` é a mensagem em português ou `null` |
|
|
73
|
+
| `propor`, fase `final` | `{fase: 'final', estado: 'aplicado' \| 'cancelado' \| 'erro', report?, motivo?, erro?}` — `report` só em `aplicado` (`{results, stopped, reconciliacao, skipped}`), `motivo` só em `cancelado`, `erro` (`{code, message}`) só em `erro` |
|
|
74
|
+
| `aplicar` (uma só fase) | `{estado: 'aplicado' \| 'erro', report?, stopped?, erro?}` — sem `fase: 'previa'`; ver secção "aplicar" acima |
|
|
75
|
+
|
|
76
|
+
### Códigos de erro
|
|
77
|
+
|
|
78
|
+
`{"ok": false, "erro": {"code", "message"}}`. Códigos que o worker pode devolver:
|
|
79
|
+
|
|
80
|
+
| código | motivo |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `ERR_COMANDO` | corpo do comando malformado (id/tipo/ano/mes/linhas inválidos) |
|
|
83
|
+
| `ERR_MES_DIFERENTE` | o comando pede um mês diferente do que a página do Suite tem aberto |
|
|
84
|
+
| `ERR_SEM_CONTEXTO` | a Folha de Horas do Suite ainda não foi aberta nesta sessão |
|
|
85
|
+
| `ERR_SEM_ABA` | não há nenhuma aba do Suite aberta (só em `propor`; sem aba o painel não abre) |
|
|
86
|
+
| `ERR_OCUPADO` | já há uma proposta do MCP à espera de confirmação no painel, ou (no APPLY) uma proposta a ser calculada |
|
|
87
|
+
| `ERR_LOTE_A_CORRER` | já está um lote a escrever no Suite (Aplicar), ou (no `propor`) já está um propor a ser calculado |
|
|
88
|
+
| `ERR_SESSION` | o Suite devolveu HTML de login: a sessão expirou |
|
|
89
|
+
| `ERR_MONTH_MISMATCH` | o read do Suite não cobre os projetos visíveis na página |
|
|
90
|
+
| `ERR_PROPOSTA_DESCONHECIDA` | `aplicar` referiu, em `proposta`, um `id` sem `propor` pendente (só em `aplicar`) |
|
|
91
|
+
| `ERR_DESCONHECIDO` | qualquer outro erro não classificado |
|
|
92
|
+
|
|
93
|
+
Este `serve` acrescenta o seu próprio `ERR_EXPIRADO` (ver secção seguinte) e usa
|
|
94
|
+
`ERR_SEM_CHROME`/`ERR_SEM_CONTEXTO` nas suas próprias rotas `/mcp/*` (ver "Rotas deste serve").
|
|
95
|
+
|
|
96
|
+
### Um comando sem resposta nunca é respondido
|
|
97
|
+
|
|
98
|
+
Se o service worker morrer (o Chrome pode matá-lo a qualquer momento fora de um fetch pendente)
|
|
99
|
+
depois de o `serve` entregar um comando em `/bridge/next` mas antes de ele publicar o resultado
|
|
100
|
+
em `/bridge/result/{id}`, esse comando fica sem resposta — o worker que reentra ao alarme
|
|
101
|
+
seguinte não retoma comandos a meio, só pede o próximo.
|
|
102
|
+
|
|
103
|
+
Este `serve` dá um timeout a cada comando pendente: expira com
|
|
104
|
+
`{"ok": false, "erro": {"code": "ERR_EXPIRADO", ...}}` se nunca chega a ser entregue dentro do
|
|
105
|
+
`timeout_s` do pedido que o enfileirou (defeito 30 s), ou se é um `propor` entregue mas que não
|
|
106
|
+
devolveu a previa em 120 s. Um comando expirado torna-se final, o que também liberta o próximo
|
|
107
|
+
`propor` bloqueado.
|
|
108
|
+
|
|
109
|
+
## CORS
|
|
110
|
+
|
|
111
|
+
A extensão chama o serviço a partir do service worker com `host_permissions` para
|
|
112
|
+
`http://127.0.0.1/*`; **não** é preciso enviar cabeçalhos CORS.
|
|
113
|
+
|
|
114
|
+
## Rotas deste `serve`
|
|
115
|
+
|
|
116
|
+
### `/mcp/*` — só o `mcp.js` chama
|
|
117
|
+
|
|
118
|
+
| método | rota | efeito |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| `GET` | `/mcp/estado` | contexto conhecido + `ponte: 'ligada' \| 'sem-chrome'` (ligada = long-poll ativo há menos de 40 s) + `ultimoMcp` (ISO do último pedido a `/mcp/*`, incluindo este) |
|
|
121
|
+
| `POST` | `/mcp/comando` | enfileira, espera até `timeout_s` s pelo resultado (defeito 30, máximo 290; `aplicar` tem defeito 120 — escreve linha a linha), devolve `{id, estado, resultado?}` |
|
|
122
|
+
| `GET` | `/mcp/comando/{id}?wait=N&fase=previa\|final` | espera pelo resultado de um comando já enfileirado; `fase` (defeito `final`) escolhe entre a pré-visualização ou o estado final de um `propor` |
|
|
123
|
+
|
|
124
|
+
`ERR_SEM_CHROME`: sem long-poll recente (o worker não está a correr, ou a extensão não tem a
|
|
125
|
+
permissão de `http://127.0.0.1/*`). `ERR_SEM_CONTEXTO`: a ponte está viva (o worker continua a
|
|
126
|
+
fazer long-poll) mas o `serve` reiniciou depois de a página já ter carregado — falta um novo
|
|
127
|
+
`POST /bridge/contexto`, que só volta a chegar quando a Folha de Horas recarregar.
|
|
128
|
+
|
|
129
|
+
### Contrato antigo (ficheiro/serviço simples, sem MCP)
|
|
130
|
+
|
|
131
|
+
Estas duas rotas são o mesmo contrato "serviço local" que o `CONTRACT.md` da extensão descreve
|
|
132
|
+
para quem não precisa da ponte nem do MCP — só entregar horas por HTTP.
|
|
133
|
+
|
|
134
|
+
| método | rota | efeito |
|
|
135
|
+
|---|---|---|
|
|
136
|
+
| `GET` | `/health` | `200 application/json` com `{nome: 'suite-timesheet-serve', versao, ultimoMcp, ponte}`; `ultimoMcp` é o ISO do último pedido a `/mcp/*` (ou `null` se nenhum desde o arranque) e `ponte` é o mesmo valor que `/mcp/estado`. A extensão usa `nome` para confirmar que é este serve (e não outro programa) a responder na porta — um corpo que não seja este JSON conta como "outro programa na porta". |
|
|
137
|
+
| `GET` | `/timesheet?year=YYYY&month=M` | `200 application/json` com `{year, month, generated_at, rows}`; `rows` vem da última proposta aceite (por `propor`) para esse mês, ou `[]` |
|
|
138
|
+
|
|
139
|
+
## O que este `serve` NÃO faz
|
|
140
|
+
|
|
141
|
+
Não escreve no Suite, não fala com o Suite. Toda a escrita é feita pela extensão, dentro do
|
|
142
|
+
browser, com a sessão do utilizador — sempre depois de o utilizador clicar Aplicar no painel ou
|
|
143
|
+
de aprovar a tool `aplicar` no cliente MCP.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
suite-timesheet-mcp — Source-Available License
|
|
2
|
+
Copyright (c) 2026 Willian Saez. All rights reserved.
|
|
3
|
+
|
|
4
|
+
The English text below is the governing version. The Portuguese translation that follows is
|
|
5
|
+
provided for convenience; if the two differ, the English text prevails.
|
|
6
|
+
|
|
7
|
+
-------------------------------------------------------------------------------
|
|
8
|
+
ENGLISH
|
|
9
|
+
-------------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
1. Definitions. "Software" means the source code, documentation and other files in this
|
|
12
|
+
repository and in any package built from it. "You" means the individual or legal entity
|
|
13
|
+
exercising the permissions granted by this license.
|
|
14
|
+
|
|
15
|
+
2. Permitted use. Subject to the conditions of this license, you may:
|
|
16
|
+
a) view and read the Software;
|
|
17
|
+
b) download, install and run unmodified copies of the Software, including through package
|
|
18
|
+
tools such as npm or npx, solely for your own personal use or for the internal business
|
|
19
|
+
purposes of your organization, by you and your organization's employees and contractors.
|
|
20
|
+
|
|
21
|
+
3. Restrictions. Except as expressly permitted in section 2, you may not, in whole or in part:
|
|
22
|
+
a) modify, adapt, translate or create derivative works of the Software;
|
|
23
|
+
b) copy the Software, except for the copies strictly necessary for the use permitted in
|
|
24
|
+
section 2 (including those made automatically by package managers and caches);
|
|
25
|
+
c) distribute, publish, sublicense, sell, rent, lend or otherwise make the Software available
|
|
26
|
+
to any third party, including by hosting copies, mirrors or forks of it on any platform or
|
|
27
|
+
offering it as a service;
|
|
28
|
+
d) remove or alter this license or any copyright notice.
|
|
29
|
+
|
|
30
|
+
A fork created through a code-hosting platform's own features (for example, under the terms
|
|
31
|
+
of service of GitHub) grants no rights beyond viewing it on that platform; any other use of
|
|
32
|
+
such a fork is subject to the restrictions above.
|
|
33
|
+
|
|
34
|
+
4. Reservation of rights. All rights not expressly granted are reserved by the copyright holder.
|
|
35
|
+
No right is granted to any trademark, name or logo.
|
|
36
|
+
|
|
37
|
+
5. Termination. This license terminates automatically if you breach any of its terms. Upon
|
|
38
|
+
termination you must stop using the Software and delete all copies in your possession.
|
|
39
|
+
|
|
40
|
+
6. No warranty. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
41
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
|
42
|
+
PARTICULAR PURPOSE AND NON-INFRINGEMENT.
|
|
43
|
+
|
|
44
|
+
7. Limitation of liability. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM,
|
|
45
|
+
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
46
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OF OR OTHER DEALINGS IN THE
|
|
47
|
+
SOFTWARE, INCLUDING ANY DATA ENTERED INTO THIRD-PARTY SYSTEMS THROUGH IT.
|
|
48
|
+
|
|
49
|
+
8. Other permissions. Any use not permitted by this license, including contributions,
|
|
50
|
+
modifications or redistribution, requires the prior written permission of the copyright
|
|
51
|
+
holder.
|
|
52
|
+
|
|
53
|
+
-------------------------------------------------------------------------------
|
|
54
|
+
PORTUGUÊS (tradução)
|
|
55
|
+
-------------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
1. Definições. "Software" designa o código-fonte, a documentação e os demais ficheiros deste
|
|
58
|
+
repositório e de qualquer pacote gerado a partir dele. "Tu" designa a pessoa singular ou
|
|
59
|
+
coletiva que exerce as permissões concedidas por esta licença.
|
|
60
|
+
|
|
61
|
+
2. Uso permitido. Nas condições desta licença, podes:
|
|
62
|
+
a) ver e ler o Software;
|
|
63
|
+
b) descarregar, instalar e executar cópias não modificadas do Software, incluindo através de
|
|
64
|
+
ferramentas de pacotes como npm ou npx, exclusivamente para uso pessoal ou para fins
|
|
65
|
+
internos da tua organização, por ti e pelos colaboradores e prestadores de serviços dessa
|
|
66
|
+
organização.
|
|
67
|
+
|
|
68
|
+
3. Restrições. Salvo o expressamente permitido na secção 2, não podes, no todo ou em parte:
|
|
69
|
+
a) modificar, adaptar, traduzir ou criar obras derivadas do Software;
|
|
70
|
+
b) copiar o Software, exceto as cópias estritamente necessárias ao uso permitido na secção 2
|
|
71
|
+
(incluindo as feitas automaticamente por gestores de pacotes e caches);
|
|
72
|
+
c) distribuir, publicar, sublicenciar, vender, alugar, emprestar ou de outra forma
|
|
73
|
+
disponibilizar o Software a terceiros, incluindo alojar cópias, espelhos ou forks dele em
|
|
74
|
+
qualquer plataforma ou oferecê-lo como serviço;
|
|
75
|
+
d) remover ou alterar esta licença ou qualquer aviso de direitos de autor.
|
|
76
|
+
|
|
77
|
+
Um fork criado através das funcionalidades de uma plataforma de alojamento de código (por
|
|
78
|
+
exemplo, ao abrigo dos termos de serviço do GitHub) não confere nenhum direito além de o
|
|
79
|
+
ver nessa plataforma; qualquer outro uso desse fork está sujeito às restrições acima.
|
|
80
|
+
|
|
81
|
+
4. Reserva de direitos. Todos os direitos não expressamente concedidos ficam reservados ao
|
|
82
|
+
titular dos direitos de autor. Não é concedido nenhum direito sobre marcas, nomes ou logótipos.
|
|
83
|
+
|
|
84
|
+
5. Cessação. Esta licença cessa automaticamente se violares qualquer dos seus termos. Com a
|
|
85
|
+
cessação, deves deixar de usar o Software e apagar todas as cópias em teu poder.
|
|
86
|
+
|
|
87
|
+
6. Sem garantia. O SOFTWARE É FORNECIDO "TAL COMO ESTÁ", SEM GARANTIA DE QUALQUER TIPO,
|
|
88
|
+
EXPRESSA OU IMPLÍCITA, INCLUINDO, SEM LIMITAÇÃO, AS GARANTIAS DE COMERCIABILIDADE, ADEQUAÇÃO
|
|
89
|
+
A UM FIM ESPECÍFICO E NÃO VIOLAÇÃO.
|
|
90
|
+
|
|
91
|
+
7. Limitação de responsabilidade. EM CASO ALGUM O TITULAR DOS DIREITOS DE AUTOR SERÁ
|
|
92
|
+
RESPONSÁVEL POR QUALQUER RECLAMAÇÃO, DANO OU OUTRA RESPONSABILIDADE, SEJA EM AÇÃO
|
|
93
|
+
CONTRATUAL, EXTRACONTRATUAL OU OUTRA, DECORRENTE DO SOFTWARE, DO SEU USO OU DE OUTRAS
|
|
94
|
+
OPERAÇÕES COM ELE, INCLUINDO QUAISQUER DADOS INTRODUZIDOS EM SISTEMAS DE TERCEIROS ATRAVÉS
|
|
95
|
+
DELE.
|
|
96
|
+
|
|
97
|
+
8. Outras permissões. Qualquer uso não permitido por esta licença, incluindo contribuições,
|
|
98
|
+
modificações ou redistribuição, requer autorização prévia e por escrito do titular dos
|
|
99
|
+
direitos de autor.
|
package/README.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# suite-timesheet-mcp
|
|
2
|
+
|
|
3
|
+
Servidor MCP que deixa um assistente (Claude, GitHub Copilot, ou outro cliente MCP) **ler e
|
|
4
|
+
propor horas** na Folha de Horas do Suite, através da extensão **Suite Timesheet Importer**.
|
|
5
|
+
|
|
6
|
+
- **Nunca fala com o Suite.** Quem lê e escreve é a extensão, no teu browser, com a tua sessão.
|
|
7
|
+
- **Nunca escreve sem ti.** Cada lançamento é uma proposta; só é escrito quando clicas Aplicar
|
|
8
|
+
no painel ou aprovas a tool `aplicar` no teu cliente MCP.
|
|
9
|
+
- **Nunca submete meses** nem muda o mês aberto na página.
|
|
10
|
+
- **Só local.** Escuta apenas em `127.0.0.1`, guarda tudo em memória, não tem telemetria.
|
|
11
|
+
|
|
12
|
+
Detalhes e forma de verificar cada afirmação: [SECURITY.md](SECURITY.md).
|
|
13
|
+
|
|
14
|
+
## Requisitos
|
|
15
|
+
|
|
16
|
+
- Node.js ≥ 20 (`node -v`).
|
|
17
|
+
- Chrome ou Edge com a extensão Suite Timesheet Importer instalada.
|
|
18
|
+
- A Folha de Horas do Suite aberta, com sessão válida.
|
|
19
|
+
|
|
20
|
+
## Instalação
|
|
21
|
+
|
|
22
|
+
### 1. Ligar a ponte na extensão
|
|
23
|
+
|
|
24
|
+
Nas **Opções** da extensão, liga **Ligação ao servidor local (ponte MCP)**, clica em
|
|
25
|
+
**Guardar** e aceita a permissão para `http://127.0.0.1/*`. Depois recarrega a Folha de Horas.
|
|
26
|
+
|
|
27
|
+
Este passo é manual e feito uma vez: sem ele a extensão nunca toca na rede local.
|
|
28
|
+
|
|
29
|
+
### 2. Registar o servidor no teu cliente MCP
|
|
30
|
+
|
|
31
|
+
Não é preciso clonar nada: o `npx` descarrega o pacote do npm e corre-o. `@1` fixa a versão
|
|
32
|
+
principal: recebes as correções 1.x, nunca uma versão incompatível. A primeira execução demora
|
|
33
|
+
alguns segundos; as seguintes usam a cache.
|
|
34
|
+
|
|
35
|
+
**Claude Code**
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
claude mcp add --scope user suite-timesheet -- npx -y suite-timesheet-mcp@1
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**GitHub Copilot (VS Code)**
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
code --add-mcp '{"name":"suite-timesheet","command":"npx","args":["-y","suite-timesheet-mcp@1"]}'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Depois recarrega a janela; as tools aparecem no modo **Agent** do Copilot.
|
|
48
|
+
|
|
49
|
+
**Claude Desktop (inclui Cowork)**
|
|
50
|
+
|
|
51
|
+
Em Definições → Programador → Editar configuração
|
|
52
|
+
(`~/Library/Application Support/Claude/claude_desktop_config.json` no macOS,
|
|
53
|
+
`%APPDATA%\Claude\claude_desktop_config.json` no Windows):
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"mcpServers": {
|
|
58
|
+
"suite-timesheet": {
|
|
59
|
+
"command": "npx",
|
|
60
|
+
"args": ["-y", "suite-timesheet-mcp@1"]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Reinicia o Claude Desktop.
|
|
67
|
+
|
|
68
|
+
**Outros clientes**
|
|
69
|
+
|
|
70
|
+
O mesmo comando: `npx -y suite-timesheet-mcp@1`, por stdio. Atenção à
|
|
71
|
+
chave do ficheiro de configuração: o VS Code usa `servers`, a maioria dos outros usa
|
|
72
|
+
`mcpServers`.
|
|
73
|
+
|
|
74
|
+
> Se o cliente não encontrar o `npx` (comum em apps abertas pelo Dock, que não herdam o PATH
|
|
75
|
+
> do terminal), usa o caminho absoluto dado por `which npx`. No Windows, usa
|
|
76
|
+
> `"command": "cmd"` com `"args": ["/c", "npx", "-y", "…"]`.
|
|
77
|
+
|
|
78
|
+
### 3. Verificar
|
|
79
|
+
|
|
80
|
+
Pede ao assistente: *"consulta o estado da ponte do Suite Timesheet"*. A tool `estado` deve
|
|
81
|
+
responder com a ponte ligada e o mês visível na página. As Opções da extensão passam a mostrar
|
|
82
|
+
"Claude ligado há N s" (o texto é o mesmo para qualquer cliente).
|
|
83
|
+
|
|
84
|
+
## Instalação feita por um agente
|
|
85
|
+
|
|
86
|
+
Se estás a pedir a um agente para instalar isto, ele deve seguir esta ordem e parar no primeiro
|
|
87
|
+
passo que falhe:
|
|
88
|
+
|
|
89
|
+
1. Confirmar `node -v` ≥ 20.
|
|
90
|
+
2. Correr o comando do passo 2 correspondente ao cliente em uso.
|
|
91
|
+
3. Pedir ao utilizador para fazer o passo 1 (Opções da extensão). O agente não o consegue fazer.
|
|
92
|
+
4. Pedir ao utilizador para recarregar o cliente MCP, se o cliente o exigir.
|
|
93
|
+
5. Chamar a tool `estado`. Sucesso = ponte ligada e mês visível. Qualquer `ERR_*`: ver
|
|
94
|
+
[Erros](#erros).
|
|
95
|
+
|
|
96
|
+
O agente **não** deve pôr a tool `aplicar` em aprovação automática.
|
|
97
|
+
|
|
98
|
+
## Usar
|
|
99
|
+
|
|
100
|
+
Exemplos de pedidos:
|
|
101
|
+
|
|
102
|
+
- *"Quantas horas tenho lançadas este mês no Suite?"* → `ler_mes`
|
|
103
|
+
- *"Que projetos tenho disponíveis?"* → `projetos`
|
|
104
|
+
- *"Lança 8 h no projeto X de segunda a sexta desta semana."* → `propor`, depois `aplicar`
|
|
105
|
+
|
|
106
|
+
Lançar horas tem sempre duas fases:
|
|
107
|
+
|
|
108
|
+
1. **`propor`** calcula o plano, abre o painel da extensão com a pré-visualização e devolve-a ao
|
|
109
|
+
assistente. Nada é escrito.
|
|
110
|
+
2. **Confirmação**, de uma de duas formas:
|
|
111
|
+
- clicas **Aplicar** no painel, ou
|
|
112
|
+
- dizes OK no chat e aprovas a chamada da tool **`aplicar`** no cliente MCP.
|
|
113
|
+
|
|
114
|
+
Por isso: **mantém a aprovação manual da tool `aplicar`**. Com ela em aprovação automática,
|
|
115
|
+
qualquer proposta é escrita sem mais nenhuma confirmação. Linhas trancadas e propostas
|
|
116
|
+
bloqueadas são sempre recusadas pela extensão.
|
|
117
|
+
|
|
118
|
+
## Tools
|
|
119
|
+
|
|
120
|
+
| tool | faz |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `estado` | ponte ligada?, mês visível, nº de projetos, lote a correr |
|
|
123
|
+
| `projetos` | projetos do dropdown da página: `option_id`, nome, trancado |
|
|
124
|
+
| `ler_mes` | horas já lançadas no mês visível |
|
|
125
|
+
| `propor` | calcula o plano, abre o painel, devolve a pré-visualização e um `id` |
|
|
126
|
+
| `aplicar` | escreve a proposta `id` no Suite através da extensão |
|
|
127
|
+
| `resultado` | espera pela decisão (painel ou `aplicar`): aplicado, cancelado, erro ou pendente |
|
|
128
|
+
|
|
129
|
+
## Erros
|
|
130
|
+
|
|
131
|
+
| erro | o que fazer |
|
|
132
|
+
|---|---|
|
|
133
|
+
| `ERR_SERVE_EM_BAIXO` | o serviço local não responde; reinicia o cliente MCP (ele arranca-o sozinho) |
|
|
134
|
+
| `ERR_SEM_CHROME` | abre a Folha de Horas e confirma que a ponte está ligada nas Opções |
|
|
135
|
+
| `ERR_SEM_CONTEXTO` | recarrega a Folha de Horas (o serviço reiniciou depois de a página carregar) |
|
|
136
|
+
| `ERR_MES_DIFERENTE` | muda o mês na página; o MCP nunca o muda por ti |
|
|
137
|
+
| `ERR_OCUPADO` | há uma proposta à espera no painel; aplica-a ou cancela-a primeiro |
|
|
138
|
+
| `ERR_PROPOSTA_DESCONHECIDA` | o `id` passado a `aplicar` não corresponde a nenhuma proposta pendente |
|
|
139
|
+
|
|
140
|
+
## Como funciona
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
cliente MCP ──stdio──▶ mcp.js ──HTTP──▶ serve.js ◀──long-poll── extensão ──▶ Suite
|
|
144
|
+
127.0.0.1 │
|
|
145
|
+
painel: Aplicar
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
1. O cliente MCP arranca o `mcp.js`. Se o serviço local (`serve.js`) não estiver a correr, o
|
|
149
|
+
`mcp.js` arranca-o em segundo plano, em `127.0.0.1:18765`.
|
|
150
|
+
2. A extensão, com a Folha de Horas aberta, faz long-poll ao serviço e publica o mês visível.
|
|
151
|
+
3. Cada tool enfileira um comando; a extensão executa-o no browser e devolve o resultado.
|
|
152
|
+
|
|
153
|
+
O protocolo entre o serviço e a extensão está em [CONTRACT.md](CONTRACT.md).
|
|
154
|
+
|
|
155
|
+
## Configuração avançada
|
|
156
|
+
|
|
157
|
+
Variáveis de ambiente, no bloco `env` da configuração do cliente MCP:
|
|
158
|
+
|
|
159
|
+
| variável | efeito |
|
|
160
|
+
|---|---|
|
|
161
|
+
| `SUITE_TIMESHEET_BASE` | outro endereço local, ex. `http://127.0.0.1:18770`; põe o mesmo nas Opções da extensão |
|
|
162
|
+
| `SUITE_TIMESHEET_SEM_AUTOARRANQUE=1` | o `mcp.js` não arranca o serviço; corres tu `node serve.js [porta]` |
|
|
163
|
+
|
|
164
|
+
Com o serviço gerido à mão, arranca-o **antes** de abrir a Folha de Horas, ou recarrega a página
|
|
165
|
+
depois: a extensão só publica o contexto quando a página carrega ou muda de mês.
|
|
166
|
+
|
|
167
|
+
As tools aceitam `timeout_s` até 290 s (útil em `resultado` com lotes grandes). Se o cliente
|
|
168
|
+
cortar a chamada antes disso, aumenta o timeout do cliente MCP (no Claude Code, 60 s por defeito).
|
|
169
|
+
|
|
170
|
+
## Atualizar
|
|
171
|
+
|
|
172
|
+
As correções 1.x chegam ao reiniciar o cliente MCP (o `npx` volta a consultar o npm). Para uma
|
|
173
|
+
versão principal nova, muda `@1` no comando. O serviço local fica a correr entre sessões; para
|
|
174
|
+
ele pegar na versão nova, termina-o uma vez
|
|
175
|
+
(`lsof -ti tcp:18765 | xargs kill`) e o `mcp.js` arranca o novo.
|
|
176
|
+
|
|
177
|
+
## Limitações
|
|
178
|
+
|
|
179
|
+
- Sem autenticação local: qualquer processo do teu utilizador consegue falar com o serviço.
|
|
180
|
+
Páginas web não conseguem. Ver [SECURITY.md](SECURITY.md).
|
|
181
|
+
- Tudo em memória: reiniciar o serviço esquece propostas e contexto (recarrega a Folha de Horas).
|
|
182
|
+
- Um mês de cada vez: o que está aberto na página.
|
|
183
|
+
|
|
184
|
+
## Correr a partir de um clone
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
git clone https://github.com/williansaez/suite-timesheet-mcp.git
|
|
188
|
+
cd suite-timesheet-mcp
|
|
189
|
+
npm ci
|
|
190
|
+
node --test
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Para usar a cópia local num cliente, troca o comando `npx …` por
|
|
194
|
+
`node /caminho/para/suite-timesheet-mcp/mcp.js`. Também dá para instalar diretamente de uma tag do
|
|
195
|
+
GitHub, sem npm: `npx -y github:williansaez/suite-timesheet-mcp#v1.0.0` (precisa de `git`).
|
|
196
|
+
|
|
197
|
+
## Licença
|
|
198
|
+
|
|
199
|
+
Código disponível para consulta, **não open source**. Podes instalar e usar o pacote sem
|
|
200
|
+
alterações, para uso pessoal ou interno da tua organização. Não é permitido alterá-lo,
|
|
201
|
+
redistribuí-lo nem publicar cópias ou forks. Os termos completos (em inglês, com tradução para
|
|
202
|
+
português) estão em [LICENSE](LICENSE).
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Segurança
|
|
2
|
+
|
|
3
|
+
Para quem precisa de decidir se pode correr este pacote numa máquina da empresa. Descreve o que
|
|
4
|
+
ele faz, o que nunca faz, e como verificar cada afirmação.
|
|
5
|
+
|
|
6
|
+
## Em uma frase
|
|
7
|
+
|
|
8
|
+
Um processo local (`serve.js`) que guarda em memória uma fila de comandos entre o Claude e a
|
|
9
|
+
extensão Suite Timesheet Importer, e um servidor MCP por stdio (`mcp.js`) que expõe seis tools ao
|
|
10
|
+
Claude. Este pacote **nunca fala com o Suite**: quem lê e escreve no Suite é a extensão, dentro do
|
|
11
|
+
browser, com a sessão do utilizador, e só depois de o utilizador clicar Aplicar no painel ou de
|
|
12
|
+
aprovar a tool `aplicar` no cliente MCP (ver a tabela abaixo).
|
|
13
|
+
|
|
14
|
+
## O que nunca faz (verificado pelo CI)
|
|
15
|
+
|
|
16
|
+
| regra | como é garantida |
|
|
17
|
+
|---|---|
|
|
18
|
+
| Nunca contacta o Suite nem qualquer host externo | o código não contém nenhum URL além de `127.0.0.1` (guard no CI); o único `listen` é em `127.0.0.1`; o `mcp.js` só chama o `serve` local |
|
|
19
|
+
| Nunca escreve no Suite, nem submete meses | não tem sessão, cookies nem endpoints do Suite; o endpoint de submissão não existe no código (guard no CI) |
|
|
20
|
+
| Nunca provoca uma escrita sem confirmação humana | a tool `propor` só enfileira; a escrita só acontece com o clique em Aplicar no painel **ou** com a aprovação da tool `aplicar` no cliente MCP (ex.: Claude Code a pedir confirmação antes de correr a tool) — uma das duas é sempre exigida |
|
|
21
|
+
| Nunca guarda dados em disco | fila, contexto e propostas vivem em memória; reiniciar o `serve` esquece tudo |
|
|
22
|
+
| Nunca aceita pedidos de páginas web | recusa `Origin` que não seja `chrome-extension://`, `Host` que não seja `127.0.0.1`/`localhost`, e POST sem `application/json` |
|
|
23
|
+
| Nunca contém dados reais | testes e exemplos só com projetos, ids e horas fictícios (guard de nomes de cliente no CI) |
|
|
24
|
+
|
|
25
|
+
## Superfície de rede
|
|
26
|
+
|
|
27
|
+
Só um socket: `127.0.0.1:18765` (porta configurável). Rotas:
|
|
28
|
+
|
|
29
|
+
- `/bridge/*`: usadas pela extensão (long-poll, contexto da página, resultados).
|
|
30
|
+
- `/mcp/*`: usadas pelo `mcp.js` na mesma máquina.
|
|
31
|
+
- `/health`, `/timesheet`: contrato antigo do serviço local, só leitura. `/health` devolve a
|
|
32
|
+
identidade do serve (`nome`, `versao`) mais `ultimoMcp` (a hora do último pedido a `/mcp/*`,
|
|
33
|
+
ou `null`) e `ponte`; não expõe nada além disso — nem contexto, nem propostas.
|
|
34
|
+
|
|
35
|
+
Corpo dos pedidos limitado a 1 MB. Ligações abortadas, URLs malformados e erros internos
|
|
36
|
+
respondem com erro e nunca derrubam o processo.
|
|
37
|
+
|
|
38
|
+
O `mcp.js` pode arrancar o `serve.js` (mesmo pacote, mesmo utilizador, só loopback) quando liga e
|
|
39
|
+
não encontra nada a responder em `/health`; nunca arranca nem chama nenhum outro programa
|
|
40
|
+
(`SUITE_TIMESHEET_SEM_AUTOARRANQUE=1` desliga isto).
|
|
41
|
+
|
|
42
|
+
## Dados
|
|
43
|
+
|
|
44
|
+
Em memória: o mês visível e a lista de projetos que a extensão publica, as propostas enviadas
|
|
45
|
+
pelo Claude, e os resultados que a extensão devolve (prévia e resultado final). Resultados expiram
|
|
46
|
+
ao fim de 1 h; comandos sem resposta expiram com `ERR_EXPIRADO`. Nada é registado além de uma
|
|
47
|
+
linha por comando em `stderr` (tipo e mês, sem horas).
|
|
48
|
+
|
|
49
|
+
## Dependências
|
|
50
|
+
|
|
51
|
+
Uma, declarada e fixada em `package-lock.json`: `@modelcontextprotocol/sdk` (só importada por
|
|
52
|
+
`mcp.js`). O `serve.js` usa apenas `node:http`. `npm audit` faz parte da verificação abaixo.
|
|
53
|
+
|
|
54
|
+
## Limitações conhecidas
|
|
55
|
+
|
|
56
|
+
- **Sem autenticação local.** Qualquer processo a correr com o teu utilizador consegue falar com
|
|
57
|
+
o `serve` (enfileirar um `propor`, ler o contexto). Páginas web não conseguem (ver acima). Um
|
|
58
|
+
`propor` malicioso ainda assim só chega ao Suite se clicares Aplicar no painel ou aprovares a
|
|
59
|
+
chamada da tool `aplicar` no cliente MCP.
|
|
60
|
+
- **Sem TLS.** Loopback apenas; nada sai da máquina.
|
|
61
|
+
- **Sem persistência.** Um reinício perde a fila; o MCP recebe `ERR_COMANDO_DESCONHECIDO` e repete.
|
|
62
|
+
|
|
63
|
+
## Como a tua equipa pode verificar
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm ci
|
|
67
|
+
node --test # 73 testes, sem rede além de loopback
|
|
68
|
+
npm audit --omit=dev # dependências conhecidas
|
|
69
|
+
grep -rnoE "https?://[a-zA-Z0-9.-]+" --include='*.js' --exclude-dir=node_modules --exclude-dir=test . # esperado: só 127.0.0.1
|
|
70
|
+
grep -rniE "submitaction" --include='*.js' . --exclude-dir=node_modules | grep -v '/test/' # esperado: vazio
|
|
71
|
+
grep -rn "listen(" serve.js # esperado: só 127.0.0.1
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Reportar um problema
|
|
75
|
+
|
|
76
|
+
Encontraste uma forma de este pacote contactar algo fora da máquina, de aceitar pedidos de uma
|
|
77
|
+
página web, ou de provocar uma escrita no Suite sem o clique em Aplicar nem a aprovação da tool
|
|
78
|
+
`aplicar`? Abre um issue privado
|
|
79
|
+
neste repositório com o rótulo `segurança` ou contacta o dono do repositório. Sem cookies, tokens
|
|
80
|
+
nem dados reais no relatório.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Auto-arranque do serve: o mcp.js chama isto antes de se ligar por stdio, para o utilizador
|
|
2
|
+
// não ter de arrancar o `serve` à mão. Puro-ish: fetch, spawn e pausa são injetáveis (só
|
|
3
|
+
// `spawnServeReal`, usado como valor por defeito, toca em node:child_process a sério).
|
|
4
|
+
|
|
5
|
+
import { spawn as spawnReal } from 'node:child_process';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const PORTA_POR_DEFEITO = 18765;
|
|
9
|
+
|
|
10
|
+
function portaDe(base) {
|
|
11
|
+
try {
|
|
12
|
+
const porta = new URL(base).port;
|
|
13
|
+
return porta ? Number(porta) : PORTA_POR_DEFEITO;
|
|
14
|
+
} catch {
|
|
15
|
+
return PORTA_POR_DEFEITO;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Só isto identifica um "serve" desta package: nome fixo devolvido por GET /health
|
|
20
|
+
// (ver lib/rotas.js). Qualquer outra coisa na porta (outro programa, um proxy, etc.) não passa.
|
|
21
|
+
function ehIdentidade(json) {
|
|
22
|
+
return Boolean(json) && typeof json === 'object' && json.nome === 'suite-timesheet-serve';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function pausaReal(ms) {
|
|
26
|
+
return new Promise((resolve) => { setTimeout(resolve, ms); });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Arranca o serve.js real, desligado deste processo: detached (não morre com o mcp.js),
|
|
30
|
+
// stdio 'ignore' (não herda nem escreve no stdio que o protocolo MCP usa) e unref() (não
|
|
31
|
+
// impede o mcp.js de terminar enquanto o serve continua a correr).
|
|
32
|
+
function spawnServeReal(porta) {
|
|
33
|
+
const caminhoServe = fileURLToPath(new URL('../serve.js', import.meta.url));
|
|
34
|
+
const filho = spawnReal(process.execPath, [caminhoServe, String(porta)], {
|
|
35
|
+
detached: true,
|
|
36
|
+
stdio: 'ignore',
|
|
37
|
+
});
|
|
38
|
+
filho.unref();
|
|
39
|
+
return filho;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// GET base/health, sem lançar: { ligou: false } se o fetch falhar (nada à escuta),
|
|
43
|
+
// { ligou: true, status, json } caso contrário (json pode ser null se o corpo não for JSON).
|
|
44
|
+
async function verHealth(base, fetchImpl) {
|
|
45
|
+
let resposta;
|
|
46
|
+
try {
|
|
47
|
+
resposta = await fetchImpl(`${base}/health`);
|
|
48
|
+
} catch {
|
|
49
|
+
return { ligou: false };
|
|
50
|
+
}
|
|
51
|
+
const json = await resposta.json().catch(() => null);
|
|
52
|
+
return { ligou: true, status: resposta.status, json };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Garante que há um `serve` desta package a responder em `base`. Nunca lança: devolve sempre
|
|
57
|
+
* { estado, detalhe }, com estado um de 'ja-corria' | 'arrancado' | 'porta-ocupada' | 'falhou'.
|
|
58
|
+
*/
|
|
59
|
+
export async function garantirServe({
|
|
60
|
+
base,
|
|
61
|
+
fetch: fetchImpl = globalThis.fetch,
|
|
62
|
+
spawn: spawnImpl,
|
|
63
|
+
pausa = pausaReal,
|
|
64
|
+
tentativas = 15,
|
|
65
|
+
} = {}) {
|
|
66
|
+
const porta = portaDe(base);
|
|
67
|
+
const arrancar = spawnImpl ?? (() => spawnServeReal(porta));
|
|
68
|
+
|
|
69
|
+
const primeira = await verHealth(base, fetchImpl);
|
|
70
|
+
|
|
71
|
+
if (primeira.ligou) {
|
|
72
|
+
if (primeira.status === 200 && ehIdentidade(primeira.json)) {
|
|
73
|
+
return { estado: 'ja-corria', detalhe: `Já havia um serve a responder em ${base}.` };
|
|
74
|
+
}
|
|
75
|
+
// Responde, mas não é este serve: outro programa qualquer está nesta porta. Nunca arrancar
|
|
76
|
+
// o serve por cima disso.
|
|
77
|
+
return { estado: 'porta-ocupada', detalhe: `Na porta ${porta} responde outro programa.` };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Nada à escuta: arrancar o serve e esperar que fique saudável.
|
|
81
|
+
arrancar();
|
|
82
|
+
for (let i = 0; i < tentativas; i += 1) {
|
|
83
|
+
await pausa(200);
|
|
84
|
+
const tentativa = await verHealth(base, fetchImpl);
|
|
85
|
+
if (tentativa.ligou && tentativa.status === 200 && ehIdentidade(tentativa.json)) {
|
|
86
|
+
return { estado: 'arrancado', detalhe: `Arranquei o serve em ${base}.` };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { estado: 'falhou', detalhe: `Não consegui arrancar o serve em ${base} (tenta "node serve.js").` };
|
|
90
|
+
}
|
package/lib/cliente.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Cliente HTTP do serve, usado pelo mcp.js. fetch injetável para os testes.
|
|
2
|
+
|
|
3
|
+
export function criarCliente({ base, fetch: fetchImpl = globalThis.fetch, comandoArranque = 'node serve.js' }) {
|
|
4
|
+
async function pedir(metodo, caminho, corpo) {
|
|
5
|
+
let resposta;
|
|
6
|
+
try {
|
|
7
|
+
resposta = await fetchImpl(`${base}${caminho}`, {
|
|
8
|
+
method: metodo,
|
|
9
|
+
headers: corpo ? { 'content-type': 'application/json' } : {},
|
|
10
|
+
body: corpo ? JSON.stringify(corpo) : undefined,
|
|
11
|
+
});
|
|
12
|
+
} catch (e) {
|
|
13
|
+
throw Object.assign(
|
|
14
|
+
new Error(`O serviço local não responde em ${base} (${e.message}). Arranca-o com: ${comandoArranque}`),
|
|
15
|
+
{ code: 'ERR_SERVE_EM_BAIXO' },
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
const json = await resposta.json().catch(() => null);
|
|
19
|
+
if (!resposta.ok) {
|
|
20
|
+
const erro = json?.erro ?? { code: 'ERR_SERVE', message: `O serviço local devolveu HTTP ${resposta.status}.` };
|
|
21
|
+
throw Object.assign(new Error(erro.message), { code: erro.code });
|
|
22
|
+
}
|
|
23
|
+
return json;
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
estado: () => pedir('GET', '/mcp/estado'),
|
|
27
|
+
comando: (corpo) => pedir('POST', '/mcp/comando', corpo),
|
|
28
|
+
resultado: (id, waitS, fase = 'final') => pedir('GET', `/mcp/comando/${encodeURIComponent(id)}?wait=${waitS}&fase=${fase}`),
|
|
29
|
+
};
|
|
30
|
+
}
|
package/lib/contexto.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Puro. O último contexto que a extensão publicou e a última proposta por mês.
|
|
2
|
+
|
|
3
|
+
export function criarContexto({ agora = () => Date.now() } = {}) {
|
|
4
|
+
let atual = null;
|
|
5
|
+
const propostas = new Map();
|
|
6
|
+
const chave = (ano, mes) => `${ano}-${mes}`;
|
|
7
|
+
return {
|
|
8
|
+
guardar(ctx) { atual = { ...ctx, recebidoEm: agora() }; },
|
|
9
|
+
atual: () => atual,
|
|
10
|
+
mesVisivel: () => (atual ? { ano: atual.ano, mes: atual.mes } : null),
|
|
11
|
+
guardarProposta(ano, mes, linhas) { propostas.set(chave(ano, mes), linhas); },
|
|
12
|
+
proposta: (ano, mes) => propostas.get(chave(ano, mes)) ?? [],
|
|
13
|
+
};
|
|
14
|
+
}
|