suite-timesheet-mcp 1.0.0 → 1.1.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 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,44 @@
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="docs/media/lancamento-1.1.0.mp4">
23
+ <img src="docs/media/agente.gif" alt="Um agente recebe um pedido, propõe o lançamento e pede aprovação" width="820">
24
+ </a>
25
+ <br>
26
+ <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>
27
+ </p>
28
+
29
+ ## Porquê
30
+
31
+ O Suite não tem API para agentes. Este pacote dá-lhe uma, sem tocar no Suite:
32
+
33
+ - **Pedes em linguagem natural.** "Lança 4 h de hoje no Alfa: migração de dados."
34
+ - **O agente lê e propõe.** Vê o mês aberto, os projetos do teu dropdown e o que já está
35
+ lançado, e monta a proposta.
36
+ - **Tu decides.** A proposta abre no painel da extensão com a pré-visualização. Só é escrita
37
+ depois de aprovares.
38
+
39
+ Funciona com qualquer agente que fale MCP. O servidor não sabe nem precisa de saber qual é.
40
+
41
+ ## Garantias
5
42
 
6
43
  - **Nunca fala com o Suite.** Quem lê e escreve é a extensão, no teu browser, com a tua sessão.
7
44
  - **Nunca escreve sem ti.** Cada lançamento é uma proposta; só é escrito quando clicas Aplicar
@@ -26,18 +63,12 @@ Nas **Opções** da extensão, liga **Ligação ao servidor local (ponte MCP)**,
26
63
 
27
64
  Este passo é manual e feito uma vez: sem ele a extensão nunca toca na rede local.
28
65
 
29
- ### 2. Registar o servidor no teu cliente MCP
66
+ ### 2. Registar o servidor no teu agente
30
67
 
31
68
  Não é preciso clonar nada: o `npx` descarrega o pacote do npm e corre-o. `@1` fixa a versão
32
69
  principal: recebes as correções 1.x, nunca uma versão incompatível. A primeira execução demora
33
70
  alguns segundos; as seguintes usam a cache.
34
71
 
35
- **Claude Code**
36
-
37
- ```bash
38
- claude mcp add --scope user suite-timesheet -- npx -y suite-timesheet-mcp@1
39
- ```
40
-
41
72
  **GitHub Copilot (VS Code)**
42
73
 
43
74
  ```bash
@@ -46,6 +77,12 @@ code --add-mcp '{"name":"suite-timesheet","command":"npx","args":["-y","suite-ti
46
77
 
47
78
  Depois recarrega a janela; as tools aparecem no modo **Agent** do Copilot.
48
79
 
80
+ **Claude Code**
81
+
82
+ ```bash
83
+ claude mcp add --scope user suite-timesheet -- npx -y suite-timesheet-mcp@1
84
+ ```
85
+
49
86
  **Claude Desktop (inclui Cowork)**
50
87
 
51
88
  Em Definições → Programador → Editar configuração
@@ -65,7 +102,7 @@ Em Definições → Programador → Editar configuração
65
102
 
66
103
  Reinicia o Claude Desktop.
67
104
 
68
- **Outros clientes**
105
+ **Outros agentes**
69
106
 
70
107
  O mesmo comando: `npx -y suite-timesheet-mcp@1`, por stdio. Atenção à
71
108
  chave do ficheiro de configuração: o VS Code usa `servers`, a maioria dos outros usa
@@ -77,9 +114,9 @@ chave do ficheiro de configuração: o VS Code usa `servers`, a maioria dos outr
77
114
 
78
115
  ### 3. Verificar
79
116
 
80
- Pede ao assistente: *"consulta o estado da ponte do Suite Timesheet"*. A tool `estado` deve
117
+ Pede ao agente: *"consulta o estado da ponte do Suite Timesheet"*. A tool `estado` deve
81
118
  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).
119
+ "Agente ligado há N s" (o texto é o mesmo para qualquer agente).
83
120
 
84
121
  ## Instalação feita por um agente
85
122
 
@@ -102,18 +139,22 @@ Exemplos de pedidos:
102
139
  - *"Quantas horas tenho lançadas este mês no Suite?"* → `ler_mes`
103
140
  - *"Que projetos tenho disponíveis?"* → `projetos`
104
141
  - *"Lança 8 h no projeto X de segunda a sexta desta semana."* → `propor`, depois `aplicar`
142
+ - *"Soma 2 h ao dia de hoje no projeto X, com a nota 'reunião de arranque'."* → `propor` em
143
+ modo `somar`, com `comment`
105
144
 
106
145
  Lançar horas tem sempre duas fases:
107
146
 
108
147
  1. **`propor`** calcula o plano, abre o painel da extensão com a pré-visualização e devolve-a ao
109
- assistente. Nada é escrito.
148
+ agente. Nada é escrito.
110
149
  2. **Confirmação**, de uma de duas formas:
111
150
  - clicas **Aplicar** no painel, ou
112
151
  - dizes OK no chat e aprovas a chamada da tool **`aplicar`** no cliente MCP.
113
152
 
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.
153
+ > [!IMPORTANT]
154
+ > Mantém a aprovação manual da tool `aplicar`. Com ela em aprovação automática, qualquer
155
+ > proposta é escrita sem mais nenhuma confirmação.
156
+
157
+ Linhas trancadas e propostas bloqueadas são sempre recusadas pela extensão.
117
158
 
118
159
  ## Tools
119
160
 
@@ -121,11 +162,56 @@ bloqueadas são sempre recusadas pela extensão.
121
162
  |---|---|
122
163
  | `estado` | ponte ligada?, mês visível, nº de projetos, lote a correr |
123
164
  | `projetos` | projetos do dropdown da página: `option_id`, nome, trancado |
124
- | `ler_mes` | horas já lançadas no mês visível |
165
+ | `ler_mes` | horas e Observações já lançadas no mês visível |
125
166
  | `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 |
167
+ | `aplicar` | escreve a proposta `id` no Suite através da extensão, sem esperar pelo clique no painel |
127
168
  | `resultado` | espera pela decisão (painel ou `aplicar`): aplicado, cancelado, erro ou pendente |
128
169
 
170
+ ### `propor`
171
+
172
+ ```json
173
+ {
174
+ "modo": "somar",
175
+ "linhas": [
176
+ {
177
+ "project": "Alfa - Projeto SAP",
178
+ "date": "2026-10-14",
179
+ "hours": 4,
180
+ "comment": "Migração de dados"
181
+ }
182
+ ]
183
+ }
184
+ ```
185
+
186
+ | campo | notas |
187
+ |---|---|
188
+ | `linhas[].option_id` ou `project` | um dos dois: id do dropdown ou nome tal como lá aparece |
189
+ | `linhas[].date` | `YYYY-MM-DD`, dentro do mês visível |
190
+ | `linhas[].hours` | 0,5 a 23,5 em passos de 0,5; em `somar` pode ser negativo |
191
+ | `linhas[].comment` | opcional; vai para as Observações do projeto no mês |
192
+ | `modo` | `normal` (defeito), `somar` ou `sobregravar` |
193
+ | `nome` | opcional; rótulo que aparece no painel |
194
+
195
+ | modo | o que vem nas linhas | o que não vem |
196
+ |---|---|---|
197
+ | `normal` | fica com o valor pedido | fica como está |
198
+ | `somar` | Suite + pedido (negativos subtraem) | fica como está |
199
+ | `sobregravar` | fica com o valor pedido | é apagado |
200
+
201
+ Propor duas vezes em `somar` duplica. Mais de 8 h num dia é só aviso.
202
+
203
+ ## Novidades da 1.1.0
204
+
205
+ - **`propor` aceita `modo`**: `normal`, `somar` ou `sobregravar`, no lugar do antigo `espelho`
206
+ (que continua aceite).
207
+ - **`propor` aceita `comment` por linha**, que vai para as Observações do projeto.
208
+ - **`ler_mes` devolve as Observações.**
209
+ - **Comentários que o Suite ignoraria em silêncio são recusados** antes de propor.
210
+ - **`ERR_EXTENSAO_ANTIGA`**: com uma extensão anterior à 1.1.0, os modos `normal` e `somar` são
211
+ recusados, porque a extensão antiga faria outra coisa.
212
+
213
+ Os modos `normal` e `somar` e os comentários precisam da extensão ≥ 1.1.0.
214
+
129
215
  ## Erros
130
216
 
131
217
  | erro | o que fazer |
@@ -133,16 +219,25 @@ bloqueadas são sempre recusadas pela extensão.
133
219
  | `ERR_SERVE_EM_BAIXO` | o serviço local não responde; reinicia o cliente MCP (ele arranca-o sozinho) |
134
220
  | `ERR_SEM_CHROME` | abre a Folha de Horas e confirma que a ponte está ligada nas Opções |
135
221
  | `ERR_SEM_CONTEXTO` | recarrega a Folha de Horas (o serviço reiniciou depois de a página carregar) |
222
+ | `ERR_SEM_ABA` | não há nenhuma aba do Suite aberta; abre-a ou recarrega-a |
136
223
  | `ERR_MES_DIFERENTE` | muda o mês na página; o MCP nunca o muda por ti |
137
224
  | `ERR_OCUPADO` | há uma proposta à espera no painel; aplica-a ou cancela-a primeiro |
225
+ | `ERR_LOTE_A_CORRER` | já está um lote a escrever no Suite; espera que termine |
226
+ | `ERR_SESSION` | a sessão do Suite expirou; entra outra vez |
138
227
  | `ERR_PROPOSTA_DESCONHECIDA` | o `id` passado a `aplicar` não corresponde a nenhuma proposta pendente |
228
+ | `ERR_EXTENSAO_ANTIGA` | atualiza a extensão para a 1.1.0, ou usa `modo: "sobregravar"` |
229
+
230
+ Lista completa em [CONTRACT.md](CONTRACT.md).
139
231
 
140
232
  ## Como funciona
141
233
 
142
- ```
143
- cliente MCP ──stdio──▶ mcp.js ──HTTP──▶ serve.js ◀──long-poll── extensão ──▶ Suite
144
- 127.0.0.1 │
145
- painel: Aplicar
234
+ ```mermaid
235
+ flowchart LR
236
+ C["Agente<br>cliente MCP"] -->|stdio| M["mcp.js"]
237
+ M -->|HTTP local| S["serve.js<br>127.0.0.1:18765"]
238
+ E["Extensão<br>no browser"] -->|long-poll| S
239
+ E -->|"lê e escreve<br>com a tua sessão"| T["Folha de Horas<br>do Suite"]
240
+ E --- P["Painel: pré-visualização<br>e Aplicar"]
146
241
  ```
147
242
 
148
243
  1. O cliente MCP arranca o `mcp.js`. Se o serviço local (`serve.js`) não estiver a correr, o
@@ -165,7 +260,8 @@ Com o serviço gerido à mão, arranca-o **antes** de abrir a Folha de Horas, ou
165
260
  depois: a extensão só publica o contexto quando a página carrega ou muda de mês.
166
261
 
167
262
  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).
263
+ cortar a chamada antes disso, aumenta o timeout do cliente MCP (muitos cortam aos 60 s por
264
+ defeito).
169
265
 
170
266
  ## Atualizar
171
267
 
@@ -192,7 +288,7 @@ node --test
192
288
 
193
289
  Para usar a cópia local num cliente, troca o comando `npx …` por
194
290
  `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`).
291
+ GitHub, sem npm: `npx -y github:williansaez/suite-timesheet-mcp#v1.1.0` (precisa de `git`).
196
292
 
197
293
  ## Licença
198
294
 
@@ -200,3 +296,7 @@ Código disponível para consulta, **não open source**. Podes instalar e usar o
200
296
  alterações, para uso pessoal ou interno da tua organização. Não é permitido alterá-lo,
201
297
  redistribuí-lo nem publicar cópias ou forks. Os termos completos (em inglês, com tradução para
202
298
  português) estão em [LICENSE](LICENSE).
299
+
300
+ <p align="center">
301
+ <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">
302
+ </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 Chrome. 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;
@@ -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
  },
@@ -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,7 +20,7 @@ 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
 
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.0' },
31
31
  { capabilities: { tools: {} } },
32
32
  );
33
33
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "suite-timesheet-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
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
8
  "homepage": "https://github.com/williansaez/suite-timesheet-mcp#readme",