altis-claude-harness 1.4.0 → 1.4.2

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/bin/cli.js CHANGED
@@ -24,8 +24,10 @@ function parseArgs(argv) {
24
24
  return { cmd, target };
25
25
  }
26
26
 
27
- function loadFragment() {
28
- return JSON.parse(fs.readFileSync(path.join(PAYLOAD_DIR, 'settings-fragment.json'), 'utf8'));
27
+ function loadFragment(target) {
28
+ const raw = fs.readFileSync(path.join(PAYLOAD_DIR, 'settings-fragment.json'), 'utf8');
29
+ const home = target.split(path.sep).join('/');
30
+ return JSON.parse(raw.split('__ALTIS_HOME__').join(home));
29
31
  }
30
32
 
31
33
  function loadMcpFragment() {
@@ -54,7 +56,7 @@ function msgNaoLiberada() {
54
56
  function runSync(target) {
55
57
  const old = readManifest(target);
56
58
  const result = syncPayload(PAYLOAD_DIR, target, (old && old.files) || {});
57
- mergeSettings(path.join(target, 'settings.json'), loadFragment());
59
+ mergeSettings(path.join(target, 'settings.json'), loadFragment(target));
58
60
  mergeMcpConfig(mcpConfigPath(target), loadMcpFragment());
59
61
  const now = new Date().toISOString();
60
62
  const manifest = {
package/lib/settings.js CHANGED
@@ -3,6 +3,7 @@ const fs = require('fs');
3
3
  const path = require('path');
4
4
 
5
5
  const ALTIS_MARKER = 'hooks/altis/';
6
+ const STATUSLINE_MARKER = 'statusline-command.ps1';
6
7
 
7
8
  function readSettings(settingsPath) {
8
9
  if (!fs.existsSync(settingsPath)) return {};
@@ -34,6 +35,15 @@ function writeSettings(settingsPath, settings) {
34
35
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
35
36
  }
36
37
 
38
+ function aplicarStatusLine(settings, statusLine) {
39
+ if (!statusLine) return;
40
+ const atual = settings.statusLine;
41
+ const comandoAtual = atual && typeof atual.command === 'string' ? atual.command : '';
42
+ if (!atual || comandoAtual.includes(STATUSLINE_MARKER)) {
43
+ settings.statusLine = statusLine;
44
+ }
45
+ }
46
+
37
47
  function mergeSettings(settingsPath, fragment) {
38
48
  const settings = readSettings(settingsPath);
39
49
  const backedUp = backupOnce(settingsPath);
@@ -48,6 +58,7 @@ function mergeSettings(settingsPath, fragment) {
48
58
  const existing = Array.isArray(settings.hooks[event]) ? settings.hooks[event] : [];
49
59
  settings.hooks[event] = existing.concat(groups);
50
60
  }
61
+ aplicarStatusLine(settings, fragment.statusLine);
51
62
  writeSettings(settingsPath, settings);
52
63
  return { changed: true, backedUp };
53
64
  }
@@ -55,18 +66,23 @@ function mergeSettings(settingsPath, fragment) {
55
66
  function removeAltisEntries(settingsPath) {
56
67
  if (!fs.existsSync(settingsPath)) return { changed: false };
57
68
  const settings = readSettings(settingsPath);
58
- if (!settings.hooks) return { changed: false };
59
69
  let changed = false;
60
- for (const event of Object.keys(settings.hooks)) {
61
- const groups = settings.hooks[event];
62
- if (!Array.isArray(groups)) continue;
63
- const kept = groups.filter(g => !isAltisGroup(g));
64
- if (kept.length !== groups.length) changed = true;
65
- if (kept.length === 0) delete settings.hooks[event]; else settings.hooks[event] = kept;
70
+ if (settings.hooks) {
71
+ for (const event of Object.keys(settings.hooks)) {
72
+ const groups = settings.hooks[event];
73
+ if (!Array.isArray(groups)) continue;
74
+ const kept = groups.filter(g => !isAltisGroup(g));
75
+ if (kept.length !== groups.length) changed = true;
76
+ if (kept.length === 0) delete settings.hooks[event]; else settings.hooks[event] = kept;
77
+ }
78
+ if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
79
+ }
80
+ if (settings.statusLine && typeof settings.statusLine.command === 'string' && settings.statusLine.command.includes(STATUSLINE_MARKER)) {
81
+ delete settings.statusLine;
82
+ changed = true;
66
83
  }
67
- if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
68
84
  if (changed) writeSettings(settingsPath, settings);
69
85
  return { changed };
70
86
  }
71
87
 
72
- module.exports = { ALTIS_MARKER, mergeSettings, removeAltisEntries, readSettings };
88
+ module.exports = { ALTIS_MARKER, STATUSLINE_MARKER, mergeSettings, removeAltisEntries, readSettings };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "altis-claude-harness",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "description": "Padroes Claude Code da Altis Sistemas - instalador, auto-update e liberacao por estacao",
5
5
  "bin": {
6
6
  "altis-claude-harness": "bin/cli.js"
@@ -96,12 +96,39 @@ Mapeie o que foi adicionado (use os tipos/definições **exatamente** como estã
96
96
 
97
97
  | Mudança detectada nas linhas `+` | Comando gerado |
98
98
  |---|---|
99
- | **Coluna nova** | `alter table T`<br>`add COLUNA <tipo e cláusulas exatas>;` |
99
+ | **Coluna(s) nova(s)** | **UMA só** `alter table T` agrupando **todas** as colunas novas dessa tabela — ver "Agrupamento de colunas novas" abaixo. |
100
100
  | **Constraint ALTERADA** (mesma constraint mudou de definição) | `alter table T`<br>`drop constraint C;`<br><br>`alter table T`<br>`add constraint C`<br>`<definição nova exata>;` |
101
101
  | **Constraint NOVA** (não existia antes) | `alter table T`<br>`add constraint C`<br>`<definição exata>;` (sem drop) |
102
102
 
103
103
  Para saber se a constraint é **nova** ou **alterada**: é **alterada** se o nome já existia na versão base (`git show "$BASE:<arquivo>"`) com definição diferente; é **nova** se o nome não existia na base.
104
104
 
105
+ #### Agrupamento de colunas novas — UMA só `alter table` por tabela
106
+
107
+ Quando o diff adiciona **várias colunas à mesma tabela**, gere **um único** `alter table` com todas elas juntas — **nunca** um `alter table ... add` por coluna. Mantenha o tipo e as cláusulas de cada coluna **exatamente** como no script e preserve a ordem em que aparecem no diff.
108
+
109
+ - **Oracle** — sintaxe `add ( ... )`, colunas separadas por vírgula, uma por linha (indentação de 2 espaços), fechando com `);`:
110
+ ```sql
111
+ alter table FUNCIONARIOS
112
+ add (
113
+ VALOR_COMISSAO_HORA number(7,2) default 0 not null,
114
+ HORA_INICIO_EXPEDIENTE varchar2(5),
115
+ HORA_INICIO_ALMOCO varchar2(5)
116
+ );
117
+ ```
118
+ - **PostgreSQL** — **não** aceita `add ( ... )`; agrupe com várias cláusulas `add column` separadas por vírgula dentro do **mesmo** `alter table`:
119
+ ```sql
120
+ alter table funcionarios
121
+ add column valor_comissao_hora numeric(7,2) not null default 0,
122
+ add column hora_inicio_expediente varchar(5),
123
+ add column hora_inicio_almoco varchar(5);
124
+ ```
125
+ - **Uma única coluna nova** → forma simples, sem parênteses (Oracle) / sem repetir `add` (PostgreSQL):
126
+ ```sql
127
+ alter table T
128
+ add COLUNA <tipo e cláusulas exatas>;
129
+ ```
130
+ - O agrupamento vale **só para colunas**. Constraints continuam **cada uma em seu próprio** `alter table` (pois podem exigir `drop`+`add`) — não as junte no bloco de colunas.
131
+
105
132
  ### Caso C — Arquivos compartilhados (sempre existentes → trate pelo diff)
106
133
 
107
134
  - **`Sequenciais.sql`** → diff e extraia o **comando `create sequence` COMPLETO** de cada `SEQ_*` adicionada (linhas `+`), exatamente como está no arquivo (incluindo `nocycle nocache noorder` e demais cláusulas). Ex.: `create sequence SEQ_ALERTA_ID nocycle nocache noorder;`
@@ -169,7 +196,7 @@ Um bloco SQL por banco, em **ordem de aplicação**:
169
196
 
170
197
  1. `create sequence` das sequences novas
171
198
  2. **Tabelas novas, na ordem topológica calculada no Passo 3** (`NOME_TABELA;`) — referenciada antes de quem referencia
172
- 3. `alter table` (colunas novas em tabelas existentes)
199
+ 3. `alter table` de colunas novas em tabelas existentes — **uma única `alter table` por tabela**, agrupando todas as colunas novas dela (Oracle: `add ( ... )`; PostgreSQL: várias `add column` na mesma `alter`)
173
200
  4. Constraints alteradas/novas em tabelas existentes (`drop` + `add`, ou só `add`) — incluindo FKs de tabela existente que apontam para tabela nova (já garantidamente criada no passo 2)
174
201
  5. Índices novos (`IDX_*;`)
175
202
  6. Triggers / Views / Procedures / Functions novos (`NOME;`)
@@ -189,7 +216,10 @@ ALERTAS_CARGOS;
189
216
  ALERTAS_IU_BR;
190
217
 
191
218
  alter table CONEXAO_WHATSAPP
192
- add LIMITE_DIARIO number(10);
219
+ add (
220
+ LIMITE_DIARIO number(10),
221
+ MENSAGEM_PADRAO varchar2(200)
222
+ );
193
223
 
194
224
  alter table CONEXAO_WHATSAPP
195
225
  drop constraint CK_CONEXAO_WHATSAPP_ATIVO;
@@ -204,7 +234,7 @@ check (ATIVO in ('S','N'));
204
234
  logs_exemplo;
205
235
  ```
206
236
 
207
- > `ALERTAS` aparece **antes** de `ALERTAS_CARGOS` porque esta tem FK para aquela — ordem topológica, não de descoberta. Tabelas novas aparecem **só pelo nome** — sem `alter table`. Os `alter table` são **só** de tabelas que já existiam (ex.: `CONEXAO_WHATSAPP`). Itens "avulsos" e "revisar manualmente", se houver, vão em texto após os blocos, **nunca** dentro do SQL. Se um banco não tiver itens, omita a seção dele.
237
+ > `ALERTAS` aparece **antes** de `ALERTAS_CARGOS` porque esta tem FK para aquela — ordem topológica, não de descoberta. Tabelas novas aparecem **só pelo nome** — sem `alter table`. Os `alter table` são **só** de tabelas que já existiam (ex.: `CONEXAO_WHATSAPP`), e **todas as colunas novas de uma mesma tabela entram num único `alter table ... add ( ... )`** (Oracle) — nunca um `alter` por coluna. Itens "avulsos" e "revisar manualmente", se houver, vão em texto após os blocos, **nunca** dentro do SQL. Se um banco não tiver itens, omita a seção dele.
208
238
 
209
239
  ---
210
240
 
@@ -225,7 +255,7 @@ logs_exemplo;
225
255
  - **Sequence nova** → comando `create sequence ...;` completo (não só o nome).
226
256
  - **Objeto novo** (tabela, trigger, view, procedure, function, índice) → **só o nome `;`**.
227
257
  - **Tabelas novas são ordenadas topologicamente por dependência de FK** (referenciada antes de quem referencia), via helper do Passo 3 — nunca pela ordem de descoberta. FK circular entre tabelas novas é reportada para tratamento manual.
228
- - **Tabela já existente** com **coluna nova** → `alter table ... add ...;`.
258
+ - **Tabela já existente** com **coluna(s) nova(s)** → **uma única** `alter table` por tabela agrupando todas as colunas (Oracle: `add ( col1, col2, ... )`; PostgreSQL: várias `add column` na mesma `alter`) — nunca um `alter` por coluna.
229
259
  - **Constraint alterada** → `drop constraint` + recriação; **constraint nova** → só `add constraint`.
230
260
  - Mudanças destrutivas/ambíguas e scripts avulsos → reportados em texto no chat, fora do bloco SQL.
231
261
  - **Bloco SQL sem comentários.** Exibe no chat e, com confirmação, anexa ao arquivo de update.
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+
7
+ const ALTIS_MARKER = 'hooks/altis/';
8
+ const LEGACY_SCRIPTS = ['check_delphi_encoding.py', 'delphi_guard.py', 'license_guard.cjs', 'rag_telemetria.cjs', 'auto_update.cjs'];
9
+ const SETTINGS_FILES = ['settings.json', 'settings.local.json'];
10
+
11
+ function removerBom(texto) {
12
+ return typeof texto === 'string' && texto.charCodeAt(0) === 0xfeff ? texto.slice(1) : texto;
13
+ }
14
+
15
+ function ehHookAltisLegado(comando) {
16
+ if (typeof comando !== 'string') return false;
17
+ if (comando.includes(ALTIS_MARKER)) return false;
18
+ return LEGACY_SCRIPTS.some(script => comando.includes(script));
19
+ }
20
+
21
+ function limparSettings(settings) {
22
+ let alterado = false;
23
+ if (!settings || typeof settings !== 'object' || !settings.hooks || typeof settings.hooks !== 'object') {
24
+ return { settings, alterado };
25
+ }
26
+ for (const evento of Object.keys(settings.hooks)) {
27
+ const grupos = settings.hooks[evento];
28
+ if (!Array.isArray(grupos)) continue;
29
+ const gruposMantidos = [];
30
+ for (const grupo of grupos) {
31
+ if (!grupo || !Array.isArray(grupo.hooks)) {
32
+ gruposMantidos.push(grupo);
33
+ continue;
34
+ }
35
+ const hooksMantidos = grupo.hooks.filter(hook => !ehHookAltisLegado(hook && hook.command));
36
+ if (hooksMantidos.length !== grupo.hooks.length) alterado = true;
37
+ if (hooksMantidos.length > 0) gruposMantidos.push({ ...grupo, hooks: hooksMantidos });
38
+ }
39
+ if (gruposMantidos.length === 0) delete settings.hooks[evento];
40
+ else settings.hooks[evento] = gruposMantidos;
41
+ }
42
+ if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
43
+ return { settings, alterado };
44
+ }
45
+
46
+ function alvoClaude() {
47
+ return process.env.ALTIS_CLAUDE_TARGET || path.join(os.homedir(), '.claude');
48
+ }
49
+
50
+ function logar(mensagem) {
51
+ try {
52
+ fs.appendFileSync(path.join(alvoClaude(), 'hooks', 'altis', 'legacy_cleanup.log'), `${new Date().toISOString()} ${mensagem}\n`);
53
+ } catch {}
54
+ }
55
+
56
+ function curarArquivo(caminho) {
57
+ let bruto;
58
+ try {
59
+ bruto = fs.readFileSync(caminho, 'utf8');
60
+ } catch {
61
+ return false;
62
+ }
63
+ let settings;
64
+ try {
65
+ settings = JSON.parse(removerBom(bruto));
66
+ } catch {
67
+ return false;
68
+ }
69
+ const { alterado } = limparSettings(settings);
70
+ if (!alterado) return false;
71
+ try {
72
+ const bak = caminho + '.altis-legacy-bak';
73
+ if (!fs.existsSync(bak)) fs.writeFileSync(bak, bruto, 'utf8');
74
+ fs.writeFileSync(caminho, JSON.stringify(settings, null, 2) + '\n', 'utf8');
75
+ logar(`sobra pre-npx removida de ${caminho}`);
76
+ return true;
77
+ } catch {
78
+ return false;
79
+ }
80
+ }
81
+
82
+ function dirProjeto() {
83
+ try {
84
+ const dados = JSON.parse(removerBom(fs.readFileSync(0, 'utf8')));
85
+ if (dados && typeof dados.cwd === 'string' && dados.cwd) return dados.cwd;
86
+ } catch {}
87
+ return process.cwd();
88
+ }
89
+
90
+ function principal() {
91
+ try {
92
+ const dirClaude = path.join(dirProjeto(), '.claude');
93
+ for (const nome of SETTINGS_FILES) curarArquivo(path.join(dirClaude, nome));
94
+ } catch {}
95
+ process.exit(0);
96
+ }
97
+
98
+ module.exports = { ehHookAltisLegado, limparSettings, LEGACY_SCRIPTS, ALTIS_MARKER };
99
+
100
+ if (require.main === module) principal();
@@ -0,0 +1,115 @@
1
+ # Altis Claude Code status line
2
+ # Segments: subprojeto | branch git | modelo | contexto restante | tokens da sessao | uso da assinatura
3
+ $ErrorActionPreference = 'SilentlyContinue'
4
+
5
+ try {
6
+ $raw = [Console]::In.ReadToEnd()
7
+ $data = $raw | ConvertFrom-Json
8
+ } catch {
9
+ $data = $null
10
+ }
11
+
12
+ $cwd = $null
13
+ if ($data) {
14
+ if ($data.workspace -and $data.workspace.current_dir) { $cwd = $data.workspace.current_dir }
15
+ elseif ($data.cwd) { $cwd = $data.cwd }
16
+ }
17
+ if (-not $cwd) { $cwd = (Get-Location).Path }
18
+
19
+ $esc = [char]27
20
+ $reset = "$esc[0m"
21
+ $cyan = "$esc[36m"
22
+ $green = "$esc[32m"
23
+ $yellow = "$esc[33m"
24
+ $blue = "$esc[34m"
25
+ $magenta = "$esc[35m"
26
+ $red = "$esc[31m"
27
+ $dim = "$esc[2m"
28
+
29
+ function Get-AltisProjectLabel([string]$path) {
30
+ if (-not $path) { return "?" }
31
+ $parts = $path -split '[\\/]' | Where-Object { $_ -ne "" }
32
+ $idx = -1
33
+ for ($i = 0; $i -lt $parts.Length; $i++) {
34
+ if ($parts[$i] -ieq "Orientado a Objetos") { $idx = $i; break }
35
+ }
36
+ $containers = @("SpringBoot", "Angular", "ReactNative", "Android", "Python")
37
+ if ($idx -ge 0 -and ($idx + 1) -lt $parts.Length) {
38
+ $seg = $parts[$idx + 1]
39
+ if ($containers -contains $seg -and ($idx + 2) -lt $parts.Length) {
40
+ return $parts[$idx + 2]
41
+ }
42
+ return $seg
43
+ }
44
+ if ($parts.Length -gt 0) { return $parts[$parts.Length - 1] }
45
+ return "?"
46
+ }
47
+
48
+ function Format-Tokens([double]$n) {
49
+ if ($n -ge 1000000) { return ("{0:0.0}M" -f ($n / 1000000)) }
50
+ if ($n -ge 1000) { return ("{0}k" -f [math]::Round($n / 1000)) }
51
+ return [string][int]$n
52
+ }
53
+
54
+ $projectLabel = Get-AltisProjectLabel $cwd
55
+
56
+ $branch = $null
57
+ try {
58
+ $branchRaw = git --no-optional-locks -C "$cwd" branch --show-current 2>$null
59
+ if ($LASTEXITCODE -eq 0 -and $branchRaw) { $branch = $branchRaw.Trim() }
60
+ } catch {
61
+ $branch = $null
62
+ }
63
+
64
+ $modelName = "?"
65
+ if ($data -and $data.model -and $data.model.display_name) { $modelName = $data.model.display_name }
66
+
67
+ $ctxPct = $null
68
+ if ($data -and $data.context_window -and $null -ne $data.context_window.remaining_percentage) {
69
+ $ctxPct = [math]::Round([double]$data.context_window.remaining_percentage)
70
+ }
71
+
72
+ $sessTokens = $null
73
+ if ($data -and $data.context_window) {
74
+ $ti = $data.context_window.total_input_tokens
75
+ $to = $data.context_window.total_output_tokens
76
+ if ($null -ne $ti -or $null -ne $to) {
77
+ $sessTokens = [double]0
78
+ if ($null -ne $ti) { $sessTokens += [double]$ti }
79
+ if ($null -ne $to) { $sessTokens += [double]$to }
80
+ }
81
+ }
82
+
83
+ $sub5h = $null
84
+ $sub7d = $null
85
+ $subMax = $null
86
+ if ($data -and $data.rate_limits) {
87
+ if ($data.rate_limits.five_hour -and $null -ne $data.rate_limits.five_hour.used_percentage) {
88
+ $sub5h = [math]::Round([double]$data.rate_limits.five_hour.used_percentage)
89
+ }
90
+ if ($data.rate_limits.seven_day -and $null -ne $data.rate_limits.seven_day.used_percentage) {
91
+ $sub7d = [math]::Round([double]$data.rate_limits.seven_day.used_percentage)
92
+ }
93
+ $vals = @()
94
+ if ($null -ne $sub5h) { $vals += $sub5h }
95
+ if ($null -ne $sub7d) { $vals += $sub7d }
96
+ if ($vals.Count -gt 0) { $subMax = ($vals | Measure-Object -Maximum).Maximum }
97
+ }
98
+
99
+ $segments = New-Object System.Collections.Generic.List[string]
100
+ $segments.Add("$cyan$projectLabel$reset")
101
+ if ($branch) { $segments.Add("$green$branch$reset") }
102
+ $segments.Add("$yellow$modelName$reset")
103
+ if ($null -ne $ctxPct) { $segments.Add("$blue$ctxPct% ctx$reset") }
104
+ if ($null -ne $sessTokens) { $segments.Add("$magenta$(Format-Tokens $sessTokens) tok$reset") }
105
+ if ($null -ne $subMax) {
106
+ $subColor = $green
107
+ if ($subMax -ge 85) { $subColor = $red } elseif ($subMax -ge 60) { $subColor = $yellow }
108
+ $subParts = @()
109
+ if ($null -ne $sub5h) { $subParts += "5h $sub5h%" }
110
+ if ($null -ne $sub7d) { $subParts += "7d $sub7d%" }
111
+ $segments.Add("$subColor$([string]::Join(' ', $subParts))$reset")
112
+ }
113
+
114
+ $sep = "$dim |$reset "
115
+ Write-Output ([string]::Join($sep, $segments))
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "altis-docsai": {
11
11
  "type": "http",
12
- "url": "http://192.168.44.126:8765/mcp",
12
+ "url": "http://192.168.44.241:8765/mcp",
13
13
  "headers": {
14
14
  "X-Api-Key": "2ZCXMhBzJoleWdtHAIEwyfvU5r0QVPO4T6xR9SK1"
15
15
  }
@@ -63,6 +63,16 @@
63
63
  "timeout": 10
64
64
  }
65
65
  ]
66
+ },
67
+ {
68
+ "hooks": [
69
+ {
70
+ "type": "command",
71
+ "command": "node \"$HOME/.claude/hooks/altis/legacy_cleanup.cjs\"",
72
+ "shell": "bash",
73
+ "timeout": 10
74
+ }
75
+ ]
66
76
  }
67
77
  ],
68
78
  "Stop": [
@@ -89,5 +99,9 @@
89
99
  ]
90
100
  }
91
101
  ]
102
+ },
103
+ "statusLine": {
104
+ "type": "command",
105
+ "command": "powershell.exe -NoProfile -ExecutionPolicy Bypass -File \"__ALTIS_HOME__/hooks/altis/statusline-command.ps1\""
92
106
  }
93
107
  }