suite-timesheet-mcp 1.0.0 → 1.1.1

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 CHANGED
@@ -10,12 +10,12 @@ Um serviço que implemente estas três rotas passa a poder pedir leituras e prop
10
10
 
11
11
  | método | rota | corpo / resposta |
12
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 |
13
+ | `POST` | `/bridge/contexto` | a extensão envia `{ano, mes, diasNoMes, projetos: [{option_id, nome, trancado}], lidoEm, versaoExtensao}` (`versaoExtensao` a partir da extensão 1.1.0) sempre que a Folha de Horas carrega ou muda de mês |
14
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
15
  | `POST` | `/bridge/result/{id}` | a extensão publica o resultado do comando `id` |
16
16
 
17
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
18
+ `propor` leva ainda `"linhas": [{option_id | project, date, hours, comment?}]` (`comment` vai para as Observações do projeto), `"modo": "normal" | "somar" | "sobregravar"` (defeito `normal`; ver a tabela de modos no `CONTRACT.md` da extensão; `"espelho": true`, da 1.0, vale como `sobregravar` e é sempre enviado junto para uma extensão 1.0 perceber o sobregravar) e
19
19
  `"nome": "texto"`. O mês tem de ser o que a página mostra; caso contrário a resposta é
20
20
  `ERR_MES_DIFERENTE` e nada é lido nem escrito.
21
21
 
@@ -49,7 +49,7 @@ com o plano calculado (o painel abre nesse momento), e depois, quando o utilizad
49
49
 
50
50
  A extensão continua a ser a única coisa que escreve no Suite, sempre depois de o utilizador
51
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.
52
+ abaixo), e só no mês aberto na página. Nunca submete as horas, nunca troca o mês selecionado.
53
53
 
54
54
  ### Prazo de `/bridge/next`
55
55
 
@@ -68,7 +68,7 @@ Resultado com `"ok": true`, no campo `dados`:
68
68
  |---|---|
69
69
  | `estado` | `{ano, mes, projetos: N, loteEmCurso: boolean}` — `projetos` é a contagem, não a lista |
70
70
  | `projetos` | `[{option_id, nome, trancado}]` |
71
- | `ler` | `{ano, mes, linhas: [{option_id, nome, status, dias: {D: horas}, total}], totaisPorDia: {D: horas}}` |
71
+ | `ler` | `{ano, mes, linhas: [{option_id, nome, status, dias: {D: horas}, total, comentario}], totaisPorDia: {D: horas}}` — `comentario` a partir da extensão 1.1.0 |
72
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
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
74
  | `aplicar` (uma só fase) | `{estado: 'aplicado' \| 'erro', report?, stopped?, erro?}` — sem `fase: 'previa'`; ver secção "aplicar" acima |
@@ -79,6 +79,7 @@ Resultado com `"ok": true`, no campo `dados`:
79
79
 
80
80
  | código | motivo |
81
81
  |---|---|
82
+ | `ERR_EXTENSAO_ANTIGA` | (só na tool `propor`) a extensão é anterior à 1.1.0 e o modo pedido não é `sobregravar`: a 1.0 faria outra coisa |
82
83
  | `ERR_COMANDO` | corpo do comando malformado (id/tipo/ano/mes/linhas inválidos) |
83
84
  | `ERR_MES_DIFERENTE` | o comando pede um mês diferente do que a página do Suite tem aberto |
84
85
  | `ERR_SEM_CONTEXTO` | a Folha de Horas do Suite ainda não foi aberta nesta sessão |
package/README.md CHANGED
@@ -1,7 +1,48 @@
1
- # suite-timesheet-mcp
1
+ <p align="center">
2
+ <img src="docs/media/icone.png" alt="Ícone do Suite Timesheet Importer" width="96">
3
+ </p>
2
4
 
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
+ <h1 align="center">suite-timesheet-mcp</h1>
6
+
7
+ <p align="center">
8
+ <strong>O agente propõe. Tu aprovas.</strong><br>
9
+ Servidor MCP que deixa um agente de IA <strong>ler e propor horas</strong> na Folha de Horas do
10
+ Suite, através da extensão <strong>Suite Timesheet Importer</strong>.
11
+ </p>
12
+
13
+ <p align="center">
14
+ <a href="https://www.npmjs.com/package/suite-timesheet-mcp"><img alt="versão no npm" src="https://img.shields.io/npm/v/suite-timesheet-mcp?color=1f6699&label=npm"></a>
15
+ <img alt="Model Context Protocol" src="https://img.shields.io/badge/MCP-stdio-2a7fba">
16
+ <img alt="Node 20 ou superior" src="https://img.shields.io/badge/node-%E2%89%A5%2020-1e7a34">
17
+ <img alt="só loopback" src="https://img.shields.io/badge/rede-s%C3%B3%20127.0.0.1-1e7a34">
18
+ <img alt="sem telemetria" src="https://img.shields.io/badge/telemetria-nenhuma-1e7a34">
19
+ </p>
20
+
21
+ <p align="center">
22
+ <a href="https://williansaez.github.io/suite-timesheet-mcp/"><strong>Site do projeto</strong></a>
23
+ </p>
24
+
25
+ <p align="center">
26
+ <a href="docs/media/lancamento-1.1.0.mp4">
27
+ <img src="docs/media/agente.gif" alt="Um agente recebe um pedido, propõe o lançamento e pede aprovação" width="820">
28
+ </a>
29
+ <br>
30
+ <sub>Excerto do vídeo de lançamento da 1.1.0, com o Copilot como exemplo de agente. Clica para ver o vídeo completo. Dados fictícios.</sub>
31
+ </p>
32
+
33
+ ## Porquê
34
+
35
+ O Suite não tem API para agentes. Este pacote dá-lhe uma, sem tocar no Suite:
36
+
37
+ - **Pedes em linguagem natural.** "Lança 4 h de hoje no Alfa: migração de dados."
38
+ - **O agente lê e propõe.** Vê o mês aberto, os projetos do teu dropdown e o que já está
39
+ lançado, e monta a proposta.
40
+ - **Tu decides.** A proposta abre no painel da extensão com a pré-visualização. Só é escrita
41
+ depois de aprovares.
42
+
43
+ Funciona com qualquer agente que fale MCP. O servidor não sabe nem precisa de saber qual é.
44
+
45
+ ## Garantias
5
46
 
6
47
  - **Nunca fala com o Suite.** Quem lê e escreve é a extensão, no teu browser, com a tua sessão.
7
48
  - **Nunca escreve sem ti.** Cada lançamento é uma proposta; só é escrito quando clicas Aplicar
@@ -14,30 +55,25 @@ Detalhes e forma de verificar cada afirmação: [SECURITY.md](SECURITY.md).
14
55
  ## Requisitos
15
56
 
16
57
  - Node.js ≥ 20 (`node -v`).
17
- - Chrome ou Edge com a extensão Suite Timesheet Importer instalada.
58
+ - Microsoft Edge com a extensão [Suite Timesheet Importer](https://microsoftedge.microsoft.com/addons/detail/suite-timesheet-importer/ccmnogdllbomjogpddfedgolgeobhdef) instalada.
18
59
  - A Folha de Horas do Suite aberta, com sessão válida.
19
60
 
20
61
  ## Instalação
21
62
 
22
63
  ### 1. Ligar a ponte na extensão
23
64
 
24
- Nas **Opções** da extensão, liga **Ligação ao servidor local (ponte MCP)**, clica em
65
+ Abre `edge://extensions`, clica em **Detalhes** na Suite Timesheet Importer e depois em **Opções da
66
+ extensão**. Aí, liga **Ligação ao servidor local (ponte MCP)**, clica em
25
67
  **Guardar** e aceita a permissão para `http://127.0.0.1/*`. Depois recarrega a Folha de Horas.
26
68
 
27
69
  Este passo é manual e feito uma vez: sem ele a extensão nunca toca na rede local.
28
70
 
29
- ### 2. Registar o servidor no teu cliente MCP
71
+ ### 2. Registar o servidor no teu agente
30
72
 
31
73
  Não é preciso clonar nada: o `npx` descarrega o pacote do npm e corre-o. `@1` fixa a versão
32
74
  principal: recebes as correções 1.x, nunca uma versão incompatível. A primeira execução demora
33
75
  alguns segundos; as seguintes usam a cache.
34
76
 
35
- **Claude Code**
36
-
37
- ```bash
38
- claude mcp add --scope user suite-timesheet -- npx -y suite-timesheet-mcp@1
39
- ```
40
-
41
77
  **GitHub Copilot (VS Code)**
42
78
 
43
79
  ```bash
@@ -46,6 +82,12 @@ code --add-mcp '{"name":"suite-timesheet","command":"npx","args":["-y","suite-ti
46
82
 
47
83
  Depois recarrega a janela; as tools aparecem no modo **Agent** do Copilot.
48
84
 
85
+ **Claude Code**
86
+
87
+ ```bash
88
+ claude mcp add --scope user suite-timesheet -- npx -y suite-timesheet-mcp@1
89
+ ```
90
+
49
91
  **Claude Desktop (inclui Cowork)**
50
92
 
51
93
  Em Definições → Programador → Editar configuração
@@ -65,7 +107,7 @@ Em Definições → Programador → Editar configuração
65
107
 
66
108
  Reinicia o Claude Desktop.
67
109
 
68
- **Outros clientes**
110
+ **Outros agentes**
69
111
 
70
112
  O mesmo comando: `npx -y suite-timesheet-mcp@1`, por stdio. Atenção à
71
113
  chave do ficheiro de configuração: o VS Code usa `servers`, a maioria dos outros usa
@@ -77,9 +119,9 @@ chave do ficheiro de configuração: o VS Code usa `servers`, a maioria dos outr
77
119
 
78
120
  ### 3. Verificar
79
121
 
80
- Pede ao assistente: *"consulta o estado da ponte do Suite Timesheet"*. A tool `estado` deve
122
+ Pede ao agente: *"consulta o estado da ponte do Suite Timesheet"*. A tool `estado` deve
81
123
  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).
124
+ "Agente ligado há N s" (o texto é o mesmo para qualquer agente).
83
125
 
84
126
  ## Instalação feita por um agente
85
127
 
@@ -102,18 +144,22 @@ Exemplos de pedidos:
102
144
  - *"Quantas horas tenho lançadas este mês no Suite?"* → `ler_mes`
103
145
  - *"Que projetos tenho disponíveis?"* → `projetos`
104
146
  - *"Lança 8 h no projeto X de segunda a sexta desta semana."* → `propor`, depois `aplicar`
147
+ - *"Soma 2 h ao dia de hoje no projeto X, com a nota 'reunião de arranque'."* → `propor` em
148
+ modo `somar`, com `comment`
105
149
 
106
150
  Lançar horas tem sempre duas fases:
107
151
 
108
152
  1. **`propor`** calcula o plano, abre o painel da extensão com a pré-visualização e devolve-a ao
109
- assistente. Nada é escrito.
153
+ agente. Nada é escrito.
110
154
  2. **Confirmação**, de uma de duas formas:
111
155
  - clicas **Aplicar** no painel, ou
112
156
  - dizes OK no chat e aprovas a chamada da tool **`aplicar`** no cliente MCP.
113
157
 
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.
158
+ > [!IMPORTANT]
159
+ > Mantém a aprovação manual da tool `aplicar`. Com ela em aprovação automática, qualquer
160
+ > proposta é escrita sem mais nenhuma confirmação.
161
+
162
+ Linhas trancadas e propostas bloqueadas são sempre recusadas pela extensão.
117
163
 
118
164
  ## Tools
119
165
 
@@ -121,11 +167,56 @@ bloqueadas são sempre recusadas pela extensão.
121
167
  |---|---|
122
168
  | `estado` | ponte ligada?, mês visível, nº de projetos, lote a correr |
123
169
  | `projetos` | projetos do dropdown da página: `option_id`, nome, trancado |
124
- | `ler_mes` | horas já lançadas no mês visível |
170
+ | `ler_mes` | horas e Observações já lançadas no mês visível |
125
171
  | `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 |
172
+ | `aplicar` | escreve a proposta `id` no Suite através da extensão, sem esperar pelo clique no painel |
127
173
  | `resultado` | espera pela decisão (painel ou `aplicar`): aplicado, cancelado, erro ou pendente |
128
174
 
175
+ ### `propor`
176
+
177
+ ```json
178
+ {
179
+ "modo": "somar",
180
+ "linhas": [
181
+ {
182
+ "project": "Alfa - Projeto SAP",
183
+ "date": "2026-10-14",
184
+ "hours": 4,
185
+ "comment": "Migração de dados"
186
+ }
187
+ ]
188
+ }
189
+ ```
190
+
191
+ | campo | notas |
192
+ |---|---|
193
+ | `linhas[].option_id` ou `project` | um dos dois: id do dropdown ou nome tal como lá aparece |
194
+ | `linhas[].date` | `YYYY-MM-DD`, dentro do mês visível |
195
+ | `linhas[].hours` | 0,5 a 23,5 em passos de 0,5; em `somar` pode ser negativo |
196
+ | `linhas[].comment` | opcional; vai para as Observações do projeto no mês |
197
+ | `modo` | `normal` (defeito), `somar` ou `sobregravar` |
198
+ | `nome` | opcional; rótulo que aparece no painel |
199
+
200
+ | modo | o que vem nas linhas | o que não vem |
201
+ |---|---|---|
202
+ | `normal` | fica com o valor pedido | fica como está |
203
+ | `somar` | Suite + pedido (negativos subtraem) | fica como está |
204
+ | `sobregravar` | fica com o valor pedido | é apagado |
205
+
206
+ Propor duas vezes em `somar` duplica. Mais de 8 h num dia é só aviso.
207
+
208
+ ## Novidades da 1.1.0
209
+
210
+ - **`propor` aceita `modo`**: `normal`, `somar` ou `sobregravar`, no lugar do antigo `espelho`
211
+ (que continua aceite).
212
+ - **`propor` aceita `comment` por linha**, que vai para as Observações do projeto.
213
+ - **`ler_mes` devolve as Observações.**
214
+ - **Comentários que o Suite ignoraria em silêncio são recusados** antes de propor.
215
+ - **`ERR_EXTENSAO_ANTIGA`**: com uma extensão anterior à 1.1.0, os modos `normal` e `somar` são
216
+ recusados, porque a extensão antiga faria outra coisa.
217
+
218
+ Os modos `normal` e `somar` e os comentários precisam da extensão ≥ 1.1.0.
219
+
129
220
  ## Erros
130
221
 
131
222
  | erro | o que fazer |
@@ -133,16 +224,25 @@ bloqueadas são sempre recusadas pela extensão.
133
224
  | `ERR_SERVE_EM_BAIXO` | o serviço local não responde; reinicia o cliente MCP (ele arranca-o sozinho) |
134
225
  | `ERR_SEM_CHROME` | abre a Folha de Horas e confirma que a ponte está ligada nas Opções |
135
226
  | `ERR_SEM_CONTEXTO` | recarrega a Folha de Horas (o serviço reiniciou depois de a página carregar) |
227
+ | `ERR_SEM_ABA` | não há nenhum separador do Suite aberto; abre-o ou recarrega-o |
136
228
  | `ERR_MES_DIFERENTE` | muda o mês na página; o MCP nunca o muda por ti |
137
229
  | `ERR_OCUPADO` | há uma proposta à espera no painel; aplica-a ou cancela-a primeiro |
230
+ | `ERR_LOTE_A_CORRER` | já está um lote a escrever no Suite; espera que termine |
231
+ | `ERR_SESSION` | a sessão do Suite expirou; entra outra vez |
138
232
  | `ERR_PROPOSTA_DESCONHECIDA` | o `id` passado a `aplicar` não corresponde a nenhuma proposta pendente |
233
+ | `ERR_EXTENSAO_ANTIGA` | atualiza a extensão para a 1.1.0, ou usa `modo: "sobregravar"` |
234
+
235
+ Lista completa em [CONTRACT.md](CONTRACT.md).
139
236
 
140
237
  ## Como funciona
141
238
 
142
- ```
143
- cliente MCP ──stdio──▶ mcp.js ──HTTP──▶ serve.js ◀──long-poll── extensão ──▶ Suite
144
- 127.0.0.1 │
145
- painel: Aplicar
239
+ ```mermaid
240
+ flowchart LR
241
+ C["Agente<br>cliente MCP"] -->|stdio| M["mcp.js"]
242
+ M -->|HTTP local| S["serve.js<br>127.0.0.1:18765"]
243
+ E["Extensão<br>no browser"] -->|long-poll| S
244
+ E -->|"lê e escreve<br>com a tua sessão"| T["Folha de Horas<br>do Suite"]
245
+ E --- P["Painel: pré-visualização<br>e Aplicar"]
146
246
  ```
147
247
 
148
248
  1. O cliente MCP arranca o `mcp.js`. Se o serviço local (`serve.js`) não estiver a correr, o
@@ -165,7 +265,8 @@ Com o serviço gerido à mão, arranca-o **antes** de abrir a Folha de Horas, ou
165
265
  depois: a extensão só publica o contexto quando a página carrega ou muda de mês.
166
266
 
167
267
  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).
268
+ cortar a chamada antes disso, aumenta o timeout do cliente MCP (muitos cortam aos 60 s por
269
+ defeito).
169
270
 
170
271
  ## Atualizar
171
272
 
@@ -192,7 +293,7 @@ node --test
192
293
 
193
294
  Para usar a cópia local num cliente, troca o comando `npx …` por
194
295
  `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`).
296
+ GitHub, sem npm: `npx -y github:williansaez/suite-timesheet-mcp#v1.1.1` (precisa de `git`).
196
297
 
197
298
  ## Licença
198
299
 
@@ -200,3 +301,7 @@ Código disponível para consulta, **não open source**. Podes instalar e usar o
200
301
  alterações, para uso pessoal ou interno da tua organização. Não é permitido alterá-lo,
201
302
  redistribuí-lo nem publicar cópias ou forks. Os termos completos (em inglês, com tradução para
202
303
  português) estão em [LICENSE](LICENSE).
304
+
305
+ <p align="center">
306
+ <img src="docs/media/fecho.png" alt="Suite Timesheet Importer 1.1.0 e suite-timesheet-mcp 1.1.0. O Suite de sempre, a tecnologia de hoje." width="820">
307
+ </p>
package/SECURITY.md CHANGED
@@ -5,9 +5,9 @@ ele faz, o que nunca faz, e como verificar cada afirmação.
5
5
 
6
6
  ## Em uma frase
7
7
 
8
- Um processo local (`serve.js`) que guarda em memória uma fila de comandos entre o Claude e a
8
+ Um processo local (`serve.js`) que guarda em memória uma fila de comandos entre o agente e a
9
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
10
+ agente. Este pacote **nunca fala com o Suite**: quem lê e escreve no Suite é a extensão, dentro do
11
11
  browser, com a sessão do utilizador, e só depois de o utilizador clicar Aplicar no painel ou de
12
12
  aprovar a tool `aplicar` no cliente MCP (ver a tabela abaixo).
13
13
 
@@ -17,7 +17,7 @@ aprovar a tool `aplicar` no cliente MCP (ver a tabela abaixo).
17
17
  |---|---|
18
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
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 |
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 (o cliente a pedir confirmação antes de correr a tool) — uma das duas é sempre exigida |
21
21
  | Nunca guarda dados em disco | fila, contexto e propostas vivem em memória; reiniciar o `serve` esquece tudo |
22
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
23
  | Nunca contém dados reais | testes e exemplos só com projetos, ids e horas fictícios (guard de nomes de cliente no CI) |
@@ -42,7 +42,7 @@ não encontra nada a responder em `/health`; nunca arranca nem chama nenhum outr
42
42
  ## Dados
43
43
 
44
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
45
+ pelo agente, e os resultados que a extensão devolve (prévia e resultado final). Resultados expiram
46
46
  ao fim de 1 h; comandos sem resposta expiram com `ERR_EXPIRADO`. Nada é registado além de uma
47
47
  linha por comando em `stderr` (tipo e mês, sem horas).
48
48
 
@@ -64,7 +64,7 @@ Uma, declarada e fixada em `package-lock.json`: `@modelcontextprotocol/sdk` (só
64
64
 
65
65
  ```bash
66
66
  npm ci
67
- node --test # 73 testes, sem rede além de loopback
67
+ node --test # 85 testes, sem rede além de loopback
68
68
  npm audit --omit=dev # dependências conhecidas
69
69
  grep -rnoE "https?://[a-zA-Z0-9.-]+" --include='*.js' --exclude-dir=node_modules --exclude-dir=test . # esperado: só 127.0.0.1
70
70
  grep -rniE "submitaction" --include='*.js' . --exclude-dir=node_modules | grep -v '/test/' # esperado: vazio
@@ -2,11 +2,11 @@
2
2
 
3
3
  import { validarProposta } from './validar.js';
4
4
 
5
- const AVISO = 'Opera só no mês que a Folha de Horas mostra no Chrome. Nunca submete o mês, nunca muda o mês.';
5
+ const AVISO = 'Escreve horas só no mês que a Folha de Horas mostra no Edge. Nunca submete as horas, nunca troca o mês selecionado.';
6
6
  const WAIT_MAX_S = 290;
7
7
  // O cliente por defeito do SDK do MCP corta a chamada aos 60 s; 50 dá margem para a
8
- // tool responder antes disso. timeout_s continua a poder ser pedido até WAIT_MAX_S (o
9
- // Claude Code permite subir o timeout do cliente MCP quando isso for preciso).
8
+ // tool responder antes disso. timeout_s continua a poder ser pedido até WAIT_MAX_S (alguns
9
+ // clientes MCP deixam subir esse timeout quando isso for preciso).
10
10
  const TIMEOUT_TOOL_S = 50;
11
11
  // aplicar escreve linha a linha no Suite: pode demorar bem mais do que as outras tools.
12
12
  const TIMEOUT_APLICAR_S = 120;
@@ -14,7 +14,7 @@ const TIMEOUT_APLICAR_S = 120;
14
14
  export const FERRAMENTAS = [
15
15
  {
16
16
  name: 'estado',
17
- description: `Estado da ponte com o Chrome: se a Folha de Horas está aberta, que mês mostra, quantos projetos tem e se há um lote a correr. ${AVISO}`,
17
+ description: `Estado da ponte com o Edge: se a Folha de Horas está aberta, que mês mostra, quantos projetos tem e se há um lote a correr. ${AVISO}`,
18
18
  inputSchema: { type: 'object', properties: {}, additionalProperties: false },
19
19
  },
20
20
  {
@@ -24,12 +24,12 @@ export const FERRAMENTAS = [
24
24
  },
25
25
  {
26
26
  name: 'ler_mes',
27
- description: `Lê as horas já lançadas no mês visível: uma linha por projeto com os dias preenchidos, o status e os totais por dia. ${AVISO}`,
27
+ description: `Lê as horas já lançadas no mês visível: uma linha por projeto com os dias preenchidos, o status, as Observações (comentario) e os totais por dia. ${AVISO}`,
28
28
  inputSchema: { type: 'object', properties: {}, additionalProperties: false },
29
29
  },
30
30
  {
31
31
  name: 'propor',
32
- description: `Propõe lançamentos para o mês visível. A extensão calcula o plano (criar/atualizar/apagar), abre o painel com a pré-visualização e devolve-a aqui; nada é escrito até o utilizador clicar Aplicar no painel. Depois chama "resultado" com o id. Com espelho: true, as linhas do Suite que não vierem na proposta são apagadas. ${AVISO}`,
32
+ description: `Propõe lançamentos para o mês visível. A extensão calcula o plano (criar/atualizar/apagar), abre o painel com a pré-visualização e devolve-a aqui; nada é escrito até o utilizador clicar Aplicar no painel. Depois chama "resultado" com o id. Modo (defeito "normal"): normal escreve os valores das linhas (preenche ou substitui) e nunca apaga o que não vier; somar soma às horas do Suite (negativos subtraem; propor duas vezes duplica); sobregravar deixa o mês igual às linhas (apaga dias e projetos que não vierem). Mais de 8 h num dia é só aviso. ${AVISO}`,
33
33
  inputSchema: {
34
34
  type: 'object',
35
35
  required: ['linhas'],
@@ -46,11 +46,20 @@ export const FERRAMENTAS = [
46
46
  option_id: { type: 'integer', description: 'id do projeto no dropdown (usa "projetos"). Exclusivo com project.' },
47
47
  project: { type: 'string', description: 'nome do projeto como aparece no dropdown. Exclusivo com option_id.' },
48
48
  date: { type: 'string', description: 'YYYY-MM-DD, dentro do mês visível' },
49
- hours: { type: 'number', description: 'de 0,5 a 23,5 em passo de 0,5' },
49
+ hours: { type: 'number', description: 'de 0,5 a 23,5 em passo de 0,5 (em modo somar pode ser negativo, para subtrair)' },
50
+ comment: {
51
+ type: 'string',
52
+ description: 'opcional: vai para as Observações do projeto no mês (uma por projeto). Comentários diferentes do mesmo projeto são juntos com "; ". Sem comment, as Observações que o Suite já tem ficam.',
53
+ },
50
54
  },
51
55
  },
52
56
  },
53
- espelho: { type: 'boolean', description: 'apaga do Suite o que não vier nas linhas (defeito: false)' },
57
+ modo: {
58
+ type: 'string',
59
+ enum: ['normal', 'somar', 'sobregravar'],
60
+ description: 'normal (defeito): escreve as células das linhas, nunca apaga; somar: soma ao Suite, negativos subtraem; sobregravar: o mês fica igual às linhas',
61
+ },
62
+ espelho: { type: 'boolean', description: 'obsoleto: espelho: true é o mesmo que modo: "sobregravar"' },
54
63
  nome: { type: 'string', description: 'rótulo que aparece no painel (ex.: "sprint 12")' },
55
64
  },
56
65
  },
@@ -98,7 +107,7 @@ const erro = (code, message) => Object.assign(new Error(message), { code });
98
107
  // Um comando síncrono do ponto de vista da tool: ou há dados, ou é erro.
99
108
  async function dadosDe(cliente, corpo) {
100
109
  const r = await cliente.comando(corpo);
101
- if (!r.resultado) throw erro('ERR_PENDENTE', `O Chrome não respondeu a tempo ao comando ${r.id}. Confirma que a Folha de Horas está aberta e tenta de novo.`);
110
+ if (!r.resultado) throw erro('ERR_PENDENTE', `O Edge não respondeu a tempo ao comando ${r.id}. Confirma que a Folha de Horas está aberta e tenta de novo.`);
102
111
  if (!r.resultado.ok) throw erro(r.resultado.erro.code, r.resultado.erro.message);
103
112
  return { id: r.id, dados: r.resultado.dados };
104
113
  }
@@ -111,6 +120,27 @@ const finalDe = (id, dados) => {
111
120
  return { id, estado, report, motivo };
112
121
  };
113
122
 
123
+ // A extensão 1.0 descarta o comment sem avisar: antes de propor comentários, confirma a versão
124
+ // que ela publicou no contexto. Nunca falha por isto — no pior caso avisa sem precisar.
125
+ const menorQue = (a, b) => {
126
+ const [x, y] = [a, b].map((v) => String(v).split('.').map(Number));
127
+ for (let i = 0; i < 3; i += 1) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) < (y[i] ?? 0);
128
+ return false;
129
+ };
130
+
131
+ // A versão vem do contexto que a extensão publicou. Sem contexto (serve reiniciado, estado em
132
+ // baixo) não se sabe: deixa seguir, o comando falha sozinho se não houver ponte. Com contexto
133
+ // sem versaoExtensao é uma extensão 1.0.
134
+ async function extensaoAntiga(cliente) {
135
+ let contexto = null;
136
+ try { contexto = (await cliente.estado())?.contexto ?? null; } catch { contexto = null; }
137
+ if (!contexto) return { antiga: false, versao: null };
138
+ const versao = contexto.versaoExtensao ?? null;
139
+ return { antiga: !versao || menorQue(versao, '1.1.0'), versao };
140
+ }
141
+
142
+ const textoVersao = (versao) => (versao ? `está na ${versao}` : 'é anterior à 1.1.0');
143
+
114
144
  export async function executar(nome, args = {}, cliente, { estadoServe } = {}) {
115
145
  switch (nome) {
116
146
  case 'estado': {
@@ -121,7 +151,7 @@ export async function executar(nome, args = {}, cliente, { estadoServe } = {}) {
121
151
  // O auto-arranque do mcp.js (ver lib/autoarranque.js) já tentou pôr o serve a
122
152
  // responder antes disto. Se não conseguiu, por a porta estar ocupada por outro
123
153
  // programa ou por o arranque ter falhado, esta tool não deve lançar: devolve o
124
- // porquê para o Claude poder explicar ao utilizador em vez de um erro seco.
154
+ // porquê para o agente poder explicar ao utilizador em vez de um erro seco.
125
155
  if (err.code === 'ERR_SERVE_EM_BAIXO' && (estadoServe?.estado === 'porta-ocupada' || estadoServe?.estado === 'falhou')) {
126
156
  return { ponte: 'sem-serve', serve: estadoServe.estado, detalhe: estadoServe.detalhe };
127
157
  }
@@ -148,15 +178,24 @@ export async function executar(nome, args = {}, cliente, { estadoServe } = {}) {
148
178
  case 'ler_mes': return (await dadosDe(cliente, { tipo: 'ler', timeout_s: TIMEOUT_TOOL_S })).dados;
149
179
  case 'propor': {
150
180
  const proposta = validarProposta(args); // erro imediato e legível; o serve valida com o mês
181
+ const { antiga, versao } = await extensaoAntiga(cliente);
182
+ // A 1.0 não conhece normal nem somar: trataria as linhas como "substituir o projeto".
183
+ if (antiga && proposta.modo !== 'sobregravar') {
184
+ throw erro('ERR_EXTENSAO_ANTIGA', `A extensão ${textoVersao(versao)} e só conhece o modo sobregravar. Atualiza a extensão para a 1.1.0 (ou usa modo: "sobregravar").`);
185
+ }
186
+ const aviso = antiga && proposta.linhas.some((l) => l.comment)
187
+ ? `A extensão ${textoVersao(versao)}: os comentários (Observações) só são escritos a partir da 1.1.0 e vão ser ignorados.`
188
+ : null;
189
+ const comAviso = (resposta) => (aviso ? { ...resposta, aviso } : resposta);
151
190
  const r = await cliente.comando({ tipo: 'propor', ...proposta, timeout_s: TIMEOUT_TOOL_S });
152
- if (!r.resultado) return { id: r.id, estado: 'pendente' };
191
+ if (!r.resultado) return comAviso({ id: r.id, estado: 'pendente' });
153
192
  if (!r.resultado.ok) throw erro(r.resultado.erro.code, r.resultado.erro.message);
154
193
  const { dados } = r.resultado;
155
194
  // Normalmente chega logo a previa; se a previa se perdeu (ex.: o worker publicou logo o
156
195
  // final) dados.fase já vem 'final' — devolve o estado final em vez de fingir "previa".
157
- if (dados.fase !== 'previa') return finalDe(r.id, dados);
196
+ if (dados.fase !== 'previa') return comAviso(finalDe(r.id, dados));
158
197
  const { previa } = dados;
159
- return { id: r.id, estado: 'previa', previa, bloqueio: previa?.bloqueio ?? null };
198
+ return comAviso({ id: r.id, estado: 'previa', previa, bloqueio: previa?.bloqueio ?? null });
160
199
  }
161
200
  case 'aplicar': {
162
201
  const id = String(args.id);
package/lib/rotas.js CHANGED
@@ -20,20 +20,20 @@ const limitar = (v, max) => Math.max(0, Math.min(inteiro(v, max), max));
20
20
  export function criarRotas({ fila, contexto, log = () => {}, versao, agora = Date.now }) {
21
21
  // Timestamp ISO do último pedido a qualquer rota /mcp/*, ou null se ainda não houve nenhum
22
22
  // desde que este `serve` arrancou. É o que /health e /mcp/estado mostram como "ultimoMcp",
23
- // para a extensão (painel) saber se o Claude ainda está a falar com o serviço.
23
+ // para a extensão (painel) saber se o agente ainda está a falar com o serviço.
24
24
  let ultimoMcp = null;
25
25
  const marcarMcp = () => { ultimoMcp = new Date(agora()).toISOString(); };
26
26
 
27
27
  async function comando(corpo) {
28
28
  if (!corpo || !TIPOS.includes(corpo.tipo)) return erro(400, 'ERR_COMANDO', `tipo tem de ser um de ${TIPOS.join(', ')}.`);
29
29
  if (!fila.ponteViva()) {
30
- return erro(409, 'ERR_SEM_CHROME', 'Abre a Folha de Horas do Suite no Chrome e espera uns segundos.');
30
+ return erro(409, 'ERR_SEM_CHROME', 'Abre a Folha de Horas do Suite no Edge e espera uns segundos.');
31
31
  }
32
32
  const visivel = contexto.mesVisivel();
33
33
  if (!visivel) {
34
34
  // A ponte está viva (o worker está a fazer long-poll), mas o serve reiniciou depois de a
35
35
  // página ter carregado: perdeu o contexto e ainda não houve recarregamento para o repor.
36
- return erro(409, 'ERR_SEM_CONTEXTO', 'A ponte está ligada mas ainda não recebi o mês da página. Recarrega a Folha de Horas no Chrome.');
36
+ return erro(409, 'ERR_SEM_CONTEXTO', 'A ponte está ligada mas ainda não recebi o mês da página. Recarrega a Folha de Horas no Edge.');
37
37
  }
38
38
  const ano = inteiro(corpo.ano, visivel.ano);
39
39
  const mes = inteiro(corpo.mes, visivel.mes);
package/lib/validar.js CHANGED
@@ -1,4 +1,4 @@
1
- // Puro. Primeira validação de uma proposta, para o Claude ter um erro imediato e legível.
1
+ // Puro. Primeira validação de uma proposta, para o agente ter um erro imediato e legível.
2
2
  // A extensão volta a validar (normalize/match): esta é a rede, aquela é a verdade.
3
3
 
4
4
  const DATA_ISO = /^(\d{4})-(\d{2})-(\d{2})$/;
@@ -17,11 +17,16 @@ function dataValida(texto) {
17
17
  return { ano, mes, dia };
18
18
  }
19
19
 
20
- export function validarProposta({ linhas, espelho = false, nome } = {}, mesVisivel = null) {
20
+ export const MODOS = ['normal', 'somar', 'sobregravar'];
21
+
22
+ // modo (extensão 1.1): normal | somar | sobregravar. espelho: true é o nome da 1.0 para sobregravar.
23
+ export function validarProposta({ linhas, espelho = false, modo: pedido, nome } = {}, mesVisivel = null) {
24
+ const modo = pedido ?? (espelho === true ? 'sobregravar' : 'normal');
21
25
  const problemas = [];
26
+ if (!MODOS.includes(modo)) problemas.push(`modo tem de ser ${MODOS.join(', ')}.`);
22
27
  const lista = Array.isArray(linhas) ? linhas : [];
23
28
  if (!Array.isArray(linhas)) problemas.push('"linhas" tem de ser um array.');
24
- else if (linhas.length === 0 && espelho !== true) problemas.push('Sem linhas. Para limpar o mês usa espelho: true.');
29
+ else if (linhas.length === 0 && modo !== 'sobregravar') problemas.push('Sem linhas. Para limpar o mês usa modo: "sobregravar".');
25
30
 
26
31
  const normalizadas = lista.map((linha, i) => {
27
32
  const n = i + 1;
@@ -37,21 +42,40 @@ export function validarProposta({ linhas, espelho = false, nome } = {}, mesVisiv
37
42
  problemas.push(`linha ${n}: ${linha.date} está fora do mês visível ${mesVisivel.ano}-${String(mesVisivel.mes).padStart(2, '0')}.`);
38
43
  }
39
44
  const horas = Number(linha?.hours);
40
- if (!Number.isFinite(horas) || horas <= 0 || horas > 23.5 || Math.round(horas * 2) !== horas * 2) {
41
- problemas.push(`linha ${n}: hours tem de estar entre 0,5 e 23,5 em passo de 0,5.`);
45
+ // Em somar um negativo subtrai: a extensão valida o resultado (0 a 23,5) contra o Suite.
46
+ const noIntervalo = modo === 'somar'
47
+ ? horas !== 0 && Math.abs(horas) <= 23.5
48
+ : horas > 0 && horas <= 23.5;
49
+ if (!Number.isFinite(horas) || !noIntervalo || Math.round(horas * 2) !== horas * 2) {
50
+ problemas.push(modo === 'somar'
51
+ ? `linha ${n}: hours em somar tem de ser diferente de 0, entre -23,5 e 23,5, em passo de 0,5.`
52
+ : `linha ${n}: hours tem de estar entre 0,5 e 23,5 em passo de 0,5.`);
53
+ }
54
+ // comment (Observações do Suite) é opcional; vazio fica de fora e o Suite mantém o que tem.
55
+ const comentario = linha?.comment;
56
+ if (comentario !== undefined && comentario !== null && typeof comentario !== 'string') {
57
+ problemas.push(`linha ${n}: comment tem de ser texto.`);
58
+ }
59
+ const textoComentario = typeof comentario === 'string' ? comentario.trim() : '';
60
+ // O Suite (validação de pedidos do ASP.NET) responde 200 sem gravar estes padrões.
61
+ if (/<[A-Za-z!/?]|&#/.test(textoComentario)) {
62
+ problemas.push(`linha ${n}: comment não pode ter "<" seguido de letra nem "&#" (o Suite ignora-o sem avisar).`);
42
63
  }
43
64
  return {
44
65
  option_id: temId ? Number(linha.option_id) : '',
45
66
  project: temNome ? linha.project.trim() : '',
46
67
  date: String(linha?.date ?? ''),
47
68
  hours: horas,
69
+ ...(textoComentario ? { comment: textoComentario } : {}),
48
70
  };
49
71
  });
50
72
 
51
73
  if (problemas.length > 0) throw erroProposta(problemas);
52
74
  return {
53
75
  linhas: normalizadas,
54
- espelho: espelho === true,
76
+ modo,
77
+ // Enviado também para uma extensão 1.0, que só conhece espelho.
78
+ espelho: modo === 'sobregravar',
55
79
  nome: typeof nome === 'string' && nome.trim() ? nome.trim() : 'proposta do Claude',
56
80
  };
57
81
  }
package/mcp.js CHANGED
@@ -27,7 +27,7 @@ if (process.env.SUITE_TIMESHEET_SEM_AUTOARRANQUE !== '1') {
27
27
  }
28
28
 
29
29
  const server = new Server(
30
- { name: 'suite-timesheet', version: '1.0.0' },
30
+ { name: 'suite-timesheet', version: '1.1.1' },
31
31
  { capabilities: { tools: {} } },
32
32
  );
33
33
 
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "suite-timesheet-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "type": "module",
5
- "description": "Servidor MCP para o Suite Timesheet Importer: um assistente (Claude, Copilot…) lê e propõe horas; a extensão escreve depois de confirmação humana.",
5
+ "description": "Servidor MCP para o Suite Timesheet Importer: um agente de IA lê e propõe horas; a extensão escreve depois de confirmação humana.",
6
6
  "license": "SEE LICENSE IN LICENSE",
7
7
  "author": "Willian Saez",
8
- "homepage": "https://github.com/williansaez/suite-timesheet-mcp#readme",
8
+ "homepage": "https://williansaez.github.io/suite-timesheet-mcp/",
9
9
  "repository": {
10
10
  "type": "git",
11
11
  "url": "git+https://github.com/williansaez/suite-timesheet-mcp.git"