@eximia-ventures/claude-code-toolkit 3.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.
@@ -0,0 +1,169 @@
1
+ # /aios-integrate — Gerenciador de Artefatos AIOS
2
+
3
+ Você é um assistente especializado em integrar, listar e exportar artefatos AIOS (agents e squads).
4
+
5
+ ## Instruções
6
+
7
+ Quando o usuário executar `/aios-integrate`, siga este fluxo:
8
+
9
+ ### 1. Pergunte a ação desejada
10
+
11
+ Apresente as opções:
12
+ 1. **Integrar artefato externo** — Importar um agent ou squad de um caminho/URL
13
+ 2. **Listar artefatos instalados** — Mostrar agents e squads registrados
14
+ 3. **Exportar artefatos** — Empacotar artefatos para compartilhamento
15
+
16
+ ---
17
+
18
+ ### 2. Integrar Artefato Externo
19
+
20
+ Quando o usuário escolher integrar:
21
+
22
+ 1. Peça o **caminho ou URL** do artefato (arquivo `.md`/`.yaml` para agent, ou pasta para squad)
23
+ 2. Leia e analise o artefato para detectar o tipo:
24
+ - Se contém `agent:` com campos `name`, `id`, `persona` → é um **Agent**
25
+ - Se contém `name`, `version`, `components`, `slashPrefix` → é um **Squad**
26
+
27
+ #### Para Agent:
28
+
29
+ Crie automaticamente todos os arquivos de integração:
30
+
31
+ ```
32
+ .codex/agents/{id}.md — Cópia do agent para IDE discovery
33
+ .antigravity/agents/{id}.md — Workflow activation file
34
+ .agent/workflows/{id}.md — Workflow file
35
+ ```
36
+
37
+ Registre no manifest `.aios-core/manifests/agents.csv` adicionando uma linha:
38
+ ```csv
39
+ {id},{name},{title},{icon},{status}
40
+ ```
41
+
42
+ Para cada `command` definido no agent (na seção `commands`), crie um slash command em `.claude/commands/`:
43
+ - Nome do arquivo: `{agent-id}-{command-name}.md`
44
+ - Conteúdo: Instruções para ativar o agent e executar o comando específico
45
+
46
+ Exemplo de slash command gerado:
47
+ ```markdown
48
+ # /{agent-id}-{command-name}
49
+
50
+ Ative o agent @{agent-name} e execute o comando `*{command-name}`.
51
+
52
+ ## Contexto
53
+ - Agent: {agent-name} ({agent-title})
54
+ - Comando: *{command-name}
55
+ - Descrição: {command-description}
56
+
57
+ ## Instruções
58
+ 1. Leia o agent definition em `.codex/agents/{id}.md`
59
+ 2. Assuma a persona do agent
60
+ 3. Execute o comando `*{command-name}` conforme definido no agent
61
+ ```
62
+
63
+ #### Para Squad:
64
+
65
+ 1. Copie a pasta completa do squad para `./squads/{squad-name}/`
66
+ 2. Leia o `squad.yaml` para identificar os agents do squad
67
+ 3. Para cada agent listado em `components.agents`:
68
+ - Registre no `.aios-core/manifests/agents.csv`
69
+ - Crie os arquivos de integração (mesmos do fluxo agent acima)
70
+ 4. Para cada task listada em `components.tasks`, crie slash commands baseados no `slashPrefix`:
71
+ - Nome: `{slashPrefix}-{task-name}.md`
72
+ - Conteúdo: Instruções para executar a task do squad
73
+
74
+ Ao finalizar, mostre um relatório:
75
+ ```
76
+ ✓ Artefato integrado: {name}
77
+ Tipo: Agent/Squad
78
+ Arquivos criados:
79
+ - .codex/agents/{id}.md
80
+ - .antigravity/agents/{id}.md
81
+ - .agent/workflows/{id}.md
82
+ - .claude/commands/{command}.md (x N)
83
+ Manifest atualizado: agents.csv
84
+ ```
85
+
86
+ ---
87
+
88
+ ### 3. Listar Artefatos Instalados
89
+
90
+ 1. Leia `.aios-core/manifests/agents.csv` e liste todos os agents registrados
91
+ 2. Verifique a pasta `./squads/` e liste todos os squads instalados
92
+ 3. Para cada artefato, mostre:
93
+ - Nome e ID
94
+ - Tipo (Agent standalone / Agent de Squad / Squad)
95
+ - Status (ativo/desativado)
96
+ - Comandos disponíveis
97
+ - Versão (se disponível)
98
+
99
+ Formato de saída:
100
+ ```
101
+ 📋 Artefatos AIOS Instalados
102
+
103
+ Agents:
104
+ 🤖 dev (Dex) — Development Agent — ativo
105
+ Comandos: *develop, *implement, *fix
106
+ 🤖 qa (Quinn) — QA Agent — ativo
107
+ Comandos: *qa-gate, *review
108
+
109
+ Squads:
110
+ 🎯 etl-squad v1.2.0 — ETL Pipeline Squad
111
+ Agents: extractor, transformer, loader
112
+ Prefix: /etl-*
113
+
114
+ Total: N agents, M squads
115
+ ```
116
+
117
+ ---
118
+
119
+ ### 4. Exportar Artefatos
120
+
121
+ 1. Liste todos os artefatos disponíveis e pergunte qual exportar
122
+ 2. Para o artefato selecionado:
123
+
124
+ #### Agent:
125
+ - Colete todos os arquivos relacionados:
126
+ - Agent definition (`.codex/agents/{id}.md`)
127
+ - Tasks referenciadas
128
+ - Templates referenciados
129
+ - Checklists referenciados
130
+ - Use `tar` para criar um bundle: `{agent-id}-export.tar.gz`
131
+
132
+ #### Squad:
133
+ - Empacote a pasta completa do squad: `./squads/{squad-name}/`
134
+ - Use `tar` para criar: `{squad-name}-export.tar.gz`
135
+
136
+ Salve o export na pasta atual e informe o caminho:
137
+ ```
138
+ ✓ Exportado: ./{artifact-name}-export.tar.gz
139
+ Conteúdo: N arquivos, M KB
140
+ Para importar em outro projeto: /aios-integrate → Integrar → caminho do arquivo
141
+ ```
142
+
143
+ ---
144
+
145
+ ## Validações
146
+
147
+ Antes de integrar qualquer artefato, valide:
148
+
149
+ 1. **Estrutura do agent** deve conter pelo menos: `agent.name`, `agent.id`, `persona.role`
150
+ 2. **Estrutura do squad** deve conter pelo menos: `name`, `version`, `components`
151
+ 3. **IDs não duplicados** — Verifique se o ID já existe no manifest antes de registrar
152
+ 4. **Dependências** — Se o agent/squad referencia tasks ou templates, verifique se existem
153
+
154
+ Se a validação falhar, mostre o erro e pergunte se o usuário quer continuar mesmo assim.
155
+
156
+ ---
157
+
158
+ ## Paths de Referência
159
+
160
+ | Artefato | Path |
161
+ |----------|------|
162
+ | Agent definitions | `.codex/agents/` |
163
+ | Agent workflows | `.agent/workflows/` |
164
+ | Agent activation | `.antigravity/agents/` |
165
+ | Agent manifest | `.aios-core/manifests/agents.csv` |
166
+ | Squad folders | `./squads/` |
167
+ | Slash commands | `.claude/commands/` |
168
+ | Agent schema | `.aios-core/schemas/agent-v3-schema.json` |
169
+ | Squad schema | `.aios-core/schemas/squad-schema.json` |
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # claude-code-toolkit — Statusline Script v1.0
4
+ #
5
+ # Receives JSON via stdin from Claude Code's statusLine configuration.
6
+ # Outputs a formatted status bar with model, context, cost, time, git info.
7
+ #
8
+ # Required: jq (https://jqlang.github.io/jq/)
9
+ #
10
+ # Environment variables (optional):
11
+ # CLAUDE_TOOLKIT_CURRENCY — Currency symbol (default: $)
12
+ # CLAUDE_TOOLKIT_CURRENCY_RATE — USD conversion rate (default: 1)
13
+ #
14
+
15
+ set -euo pipefail
16
+
17
+ # --- Read JSON from stdin ---
18
+ INPUT=$(cat)
19
+
20
+ # --- Check jq availability ---
21
+ if ! command -v jq &>/dev/null; then
22
+ echo "jq not found"
23
+ exit 0
24
+ fi
25
+
26
+ # --- ANSI Colors ---
27
+ RESET='\033[0m'
28
+ BOLD='\033[1m'
29
+ DIM='\033[2m'
30
+ RED='\033[31m'
31
+ GREEN='\033[32m'
32
+ YELLOW='\033[33m'
33
+ BLUE='\033[34m'
34
+ MAGENTA='\033[35m'
35
+ CYAN='\033[36m'
36
+ WHITE='\033[37m'
37
+ BOLD_BLUE="${BOLD}${BLUE}"
38
+ BOLD_RED="${BOLD}${RED}"
39
+
40
+ # --- Currency config ---
41
+ CURRENCY="${CLAUDE_TOOLKIT_CURRENCY:-\$}"
42
+ RATE="${CLAUDE_TOOLKIT_CURRENCY_RATE:-1}"
43
+
44
+ # --- Parse JSON fields ---
45
+ model_id=$(echo "$INPUT" | jq -r '.model.id // ""')
46
+ used_pct=$(echo "$INPUT" | jq -r '.context_window.used_percentage // 0')
47
+ total_input=$(echo "$INPUT" | jq -r '.context_window.total_input_tokens // 0')
48
+ total_output=$(echo "$INPUT" | jq -r '.context_window.total_output_tokens // 0')
49
+ total_cost=$(echo "$INPUT" | jq -r '.cost.total_cost_usd // 0')
50
+ total_duration=$(echo "$INPUT" | jq -r '.cost.total_duration_ms // 0')
51
+ session_id=$(echo "$INPUT" | jq -r '.session_id // ""')
52
+
53
+ # --- Helper: format model name ---
54
+ format_model() {
55
+ local id="$1"
56
+ case "$id" in
57
+ *opus*) echo "Opus 4.6" ;;
58
+ *sonnet*) echo "Sonnet 4.5" ;;
59
+ *haiku*) echo "Haiku 4.5" ;;
60
+ *) echo "$id" ;;
61
+ esac
62
+ }
63
+
64
+ # --- Helper: format tokens ---
65
+ fmt_tokens() {
66
+ local n="$1"
67
+ if (( n >= 1000000 )); then
68
+ printf "%.1fM" "$(echo "scale=1; $n / 1000000" | bc)"
69
+ elif (( n >= 1000 )); then
70
+ echo "$((n / 1000))K"
71
+ else
72
+ echo "$n"
73
+ fi
74
+ }
75
+
76
+ # --- Helper: progress bar ---
77
+ progress_bar() {
78
+ local pct="$1"
79
+ local width=10
80
+ local filled=$(( (pct * width + 50) / 100 ))
81
+ local empty=$(( width - filled ))
82
+
83
+ local color
84
+ if (( pct > 80 )); then color="$RED"
85
+ elif (( pct > 60 )); then color="$YELLOW"
86
+ else color="$GREEN"
87
+ fi
88
+
89
+ local bar=""
90
+ for ((i=0; i<filled; i++)); do bar+="█"; done
91
+ for ((i=0; i<empty; i++)); do bar+="░"; done
92
+
93
+ printf "${color}%s${RESET}" "$bar"
94
+ }
95
+
96
+ # --- Helper: format duration ---
97
+ fmt_duration() {
98
+ local ms="$1"
99
+ local total_sec=$(( ms / 1000 ))
100
+ local min=$(( total_sec / 60 ))
101
+ local sec=$(( total_sec % 60 ))
102
+ if (( min > 0 )); then
103
+ printf "%dm%02ds" "$min" "$sec"
104
+ else
105
+ printf "%ds" "$sec"
106
+ fi
107
+ }
108
+
109
+ # --- Helper: format cost with currency conversion ---
110
+ fmt_cost() {
111
+ local usd="$1"
112
+ local converted
113
+ converted=$(LC_NUMERIC=C awk "BEGIN { printf \"%.2f\", $usd * $RATE }")
114
+ printf "%s%s" "$CURRENCY" "$converted"
115
+ }
116
+
117
+ # --- Helper: get git branch (cached 5s) ---
118
+ GIT_CACHE_DIR="${HOME}/.claude/session-cache"
119
+ GIT_CACHE_FILE="${GIT_CACHE_DIR}/git-branch-cache"
120
+
121
+ get_git_branch() {
122
+ mkdir -p "$GIT_CACHE_DIR" 2>/dev/null || true
123
+
124
+ if [[ -f "$GIT_CACHE_FILE" ]]; then
125
+ local cache_age
126
+ if [[ "$(uname)" == "Darwin" ]]; then
127
+ cache_age=$(( $(date +%s) - $(stat -f %m "$GIT_CACHE_FILE") ))
128
+ else
129
+ cache_age=$(( $(date +%s) - $(stat -c %Y "$GIT_CACHE_FILE") ))
130
+ fi
131
+ if (( cache_age < 5 )); then
132
+ cat "$GIT_CACHE_FILE"
133
+ return
134
+ fi
135
+ fi
136
+
137
+ local branch
138
+ branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
139
+ if [[ -n "$branch" ]]; then
140
+ echo "$branch" > "$GIT_CACHE_FILE"
141
+ echo "$branch"
142
+ fi
143
+ }
144
+
145
+ # --- Helper: count user messages from transcript ---
146
+ count_messages() {
147
+ local transcript_path
148
+ transcript_path=$(echo "$INPUT" | jq -r '.transcript_path // ""')
149
+ if [[ -z "$transcript_path" || ! -f "$transcript_path" ]]; then
150
+ echo "0"
151
+ return
152
+ fi
153
+ grep -c '"type"\s*:\s*"human"\|"type"\s*:\s*"user"' "$transcript_path" 2>/dev/null || echo "0"
154
+ }
155
+
156
+ # --- Build segments ---
157
+ PARTS=()
158
+
159
+ # 1. Model with color indicator
160
+ model_name=$(format_model "$model_id")
161
+ if [[ -n "$model_name" ]]; then
162
+ model_color="$GREEN"
163
+ case "$model_name" in
164
+ "Opus 4.6") model_color="$MAGENTA" ;;
165
+ "Sonnet 4.5") model_color="$BLUE" ;;
166
+ "Haiku 4.5") model_color="$CYAN" ;;
167
+ esac
168
+ PARTS+=("$(printf "${model_color}●${RESET} ${BOLD_BLUE}%s${RESET}" "$model_name")")
169
+ fi
170
+
171
+ # 2. Date
172
+ current_date=$(date +%d/%m/%Y)
173
+ PARTS+=("$(printf "📅 %s" "$current_date")")
174
+
175
+ # 3. Context (total tokens in human format)
176
+ total_tokens=$(( total_input + total_output ))
177
+ if (( total_tokens > 0 )); then
178
+ PARTS+=("$(printf "📊 %s" "$(fmt_tokens "$total_tokens")")")
179
+ fi
180
+
181
+ # 4. Message count
182
+ msg_count=$(count_messages)
183
+ PARTS+=("$(printf "⚡ %s" "$msg_count")")
184
+
185
+ # 5. Progress bar + percentage
186
+ pct_int=${used_pct%.*}
187
+ pct_int=${pct_int:-0}
188
+ bar=$(progress_bar "$pct_int")
189
+ if (( pct_int > 80 )); then pct_color="$RED"
190
+ elif (( pct_int > 60 )); then pct_color="$YELLOW"
191
+ else pct_color="$GREEN"
192
+ fi
193
+ PARTS+=("$(printf "%s ${pct_color}%s%%${RESET}" "$bar" "$pct_int")")
194
+
195
+ # 6. Cost
196
+ if [[ "$total_cost" != "0" ]]; then
197
+ PARTS+=("$(printf "💰 %s" "$(fmt_cost "$total_cost")")")
198
+ fi
199
+
200
+ # 7. Duration
201
+ if (( total_duration > 0 )); then
202
+ PARTS+=("$(printf "⏱ %s" "$(fmt_duration "$total_duration")")")
203
+ fi
204
+
205
+ # 8. Git branch
206
+ git_branch=$(get_git_branch)
207
+ if [[ -n "$git_branch" ]]; then
208
+ PARTS+=("$(printf "🔀 ${MAGENTA}%s${RESET}" "$git_branch")")
209
+ fi
210
+
211
+ # --- Join parts with separator ---
212
+ output=""
213
+ for i in "${!PARTS[@]}"; do
214
+ if (( i > 0 )); then
215
+ output+=" | "
216
+ fi
217
+ output+="${PARTS[$i]}"
218
+ done
219
+
220
+ echo -e "$output"
package/bin/cli.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict';
4
+
5
+ require('../src/index.js');
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@eximia-ventures/claude-code-toolkit",
3
+ "version": "3.0.0",
4
+ "description": "Setup completo para Claude Code: statusline, session handoff, AIOS integration",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "claude-code-toolkit": "bin/cli.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "src/",
12
+ "assets/"
13
+ ],
14
+ "scripts": {
15
+ "start": "node bin/cli.js",
16
+ "test": "echo \"No tests yet\" && exit 0"
17
+ },
18
+ "keywords": [
19
+ "claude",
20
+ "claude-code",
21
+ "statusline",
22
+ "toolkit",
23
+ "aios",
24
+ "developer-tools"
25
+ ],
26
+ "author": "Hugo Capitelli",
27
+ "license": "MIT",
28
+ "dependencies": {
29
+ "chalk": "^4.1.2",
30
+ "claude-code-handoff": "^2.1.0",
31
+ "commander": "^12.1.0",
32
+ "fs-extra": "^11.3.0",
33
+ "inquirer": "^8.2.6"
34
+ }
35
+ }
@@ -0,0 +1,166 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs-extra');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const { execSync } = require('child_process');
7
+ const logger = require('../utils/logger');
8
+ const { MANIFEST_PATH } = require('../installer');
9
+ const { SETTINGS_PATH, readSettings } = require('../utils/settings-merger');
10
+
11
+ const CLAUDE_DIR = path.join(os.homedir(), '.claude');
12
+ const STATUSLINE_PATH = path.join(CLAUDE_DIR, 'statusline.sh');
13
+
14
+ async function runDoctor() {
15
+ logger.section('Diagnóstico');
16
+
17
+ let issues = 0;
18
+ let ok = 0;
19
+
20
+ // 1. Check manifest
21
+ if (await fs.pathExists(MANIFEST_PATH)) {
22
+ const manifest = await fs.readJson(MANIFEST_PATH);
23
+ check(`Manifest encontrado (v${manifest.version})`, true);
24
+ ok++;
25
+ } else {
26
+ check('Manifest não encontrado — toolkit não instalado?', false);
27
+ issues++;
28
+ }
29
+
30
+ // 2. Check ~/.claude/ directory
31
+ if (await fs.pathExists(CLAUDE_DIR)) {
32
+ check('Diretório ~/.claude/ existe', true);
33
+ ok++;
34
+ } else {
35
+ check('Diretório ~/.claude/ não encontrado', false);
36
+ issues++;
37
+ }
38
+
39
+ // 3. Check settings.json
40
+ if (await fs.pathExists(SETTINGS_PATH)) {
41
+ const settings = await readSettings();
42
+ if (settings.statusLine) {
43
+ check('settings.json contém statusLine', true);
44
+ ok++;
45
+ } else {
46
+ check('settings.json não contém statusLine', false);
47
+ issues++;
48
+ }
49
+ } else {
50
+ check('settings.json não encontrado', false);
51
+ issues++;
52
+ }
53
+
54
+ // 4. Check statusline.sh
55
+ if (await fs.pathExists(STATUSLINE_PATH)) {
56
+ const stats = await fs.stat(STATUSLINE_PATH);
57
+ const isExecutable = (stats.mode & 0o111) !== 0;
58
+ if (isExecutable) {
59
+ check('statusline.sh existe e é executável', true);
60
+ ok++;
61
+ } else {
62
+ check('statusline.sh existe mas NÃO é executável', false);
63
+ issues++;
64
+ }
65
+ } else {
66
+ check('statusline.sh não encontrado', false);
67
+ issues++;
68
+ }
69
+
70
+ // 5. Check jq
71
+ try {
72
+ const jqVersion = execSync('jq --version 2>&1', { encoding: 'utf8' }).trim();
73
+ check(`jq instalado (${jqVersion})`, true);
74
+ ok++;
75
+ } catch {
76
+ check('jq não encontrado — necessário para statusline', false);
77
+ issues++;
78
+ }
79
+
80
+ // 6. Check git
81
+ try {
82
+ const gitVersion = execSync('git --version 2>&1', { encoding: 'utf8' }).trim();
83
+ check(`${gitVersion}`, true);
84
+ ok++;
85
+ } catch {
86
+ check('git não encontrado', false);
87
+ issues++;
88
+ }
89
+
90
+ // 7. Check Node.js version
91
+ const nodeVersion = process.version;
92
+ const major = parseInt(nodeVersion.slice(1), 10);
93
+ if (major >= 18) {
94
+ check(`Node.js ${nodeVersion}`, true);
95
+ ok++;
96
+ } else {
97
+ check(`Node.js ${nodeVersion} (recomendado >= 18)`, false);
98
+ issues++;
99
+ }
100
+
101
+ // 8. Check handoff commands
102
+ const commandsDir = path.join(process.cwd(), '.claude', 'commands');
103
+ if (await fs.pathExists(commandsDir)) {
104
+ const commands = await fs.readdir(commandsDir);
105
+ const handoffCommands = commands.filter(f =>
106
+ ['resume.md', 'handoff.md', 'save-handoff.md'].includes(f)
107
+ );
108
+ if (handoffCommands.length > 0) {
109
+ check(`Handoff commands encontrados (${handoffCommands.length} de 7)`, true);
110
+ ok++;
111
+ } else {
112
+ check('Handoff commands não encontrados no projeto atual', false);
113
+ issues++;
114
+ }
115
+ } else {
116
+ check('.claude/commands/ não encontrado no projeto atual', false);
117
+ issues++;
118
+ }
119
+
120
+ // 9. Check AIOS
121
+ const aiosCoreDir = path.join(process.cwd(), '.aios-core');
122
+ if (await fs.pathExists(aiosCoreDir)) {
123
+ check('AIOS Core instalado no projeto', true);
124
+ ok++;
125
+
126
+ // Check aios-integrate skill
127
+ const skillPath = path.join(process.cwd(), '.claude', 'commands', 'aios-integrate.md');
128
+ if (await fs.pathExists(skillPath)) {
129
+ check('Skill /aios-integrate instalada', true);
130
+ ok++;
131
+ } else {
132
+ check('Skill /aios-integrate não encontrada', false);
133
+ issues++;
134
+ }
135
+ } else {
136
+ logger.dim(' - AIOS Core não instalado (opcional)');
137
+ }
138
+
139
+ // 10. Check env vars
140
+ const currency = process.env.CLAUDE_TOOLKIT_CURRENCY;
141
+ const rate = process.env.CLAUDE_TOOLKIT_CURRENCY_RATE;
142
+ if (currency || rate) {
143
+ check(`Moeda configurada: ${currency || '$'} (taxa: ${rate || '1'})`, true);
144
+ ok++;
145
+ } else {
146
+ logger.dim(' - Variáveis de moeda não definidas (usando defaults)');
147
+ }
148
+
149
+ // Summary
150
+ logger.blank();
151
+ if (issues === 0) {
152
+ logger.success(`Tudo OK! (${ok} verificações passaram)`);
153
+ } else {
154
+ logger.warn(`${issues} problema(s) encontrado(s), ${ok} OK`);
155
+ }
156
+ }
157
+
158
+ function check(msg, passed) {
159
+ if (passed) {
160
+ logger.success(msg);
161
+ } else {
162
+ logger.error(msg);
163
+ }
164
+ }
165
+
166
+ module.exports = { runDoctor };
@@ -0,0 +1,91 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs-extra');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const inquirer = require('inquirer');
7
+ const logger = require('../utils/logger');
8
+ const { MANIFEST_PATH } = require('../installer');
9
+ const { removeSettingsKey, SETTINGS_PATH } = require('../utils/settings-merger');
10
+
11
+ async function runUninstall(options = {}) {
12
+ logger.section('Desinstalação');
13
+
14
+ // Check manifest
15
+ if (!(await fs.pathExists(MANIFEST_PATH))) {
16
+ logger.warn('Nenhuma instalação encontrada (manifest ausente).');
17
+ logger.dim(`Esperado em: ${MANIFEST_PATH}`);
18
+ return;
19
+ }
20
+
21
+ const manifest = await fs.readJson(MANIFEST_PATH);
22
+
23
+ logger.info(`Instalação encontrada (v${manifest.version}, ${manifest.installedAt})`);
24
+ logger.info(`Módulos: ${manifest.modules.join(', ')}`);
25
+ logger.blank();
26
+
27
+ // Confirm
28
+ if (!options.force) {
29
+ const { confirm } = await inquirer.prompt([
30
+ {
31
+ type: 'confirm',
32
+ name: 'confirm',
33
+ message: 'Remover tudo que foi instalado pelo toolkit?',
34
+ default: false
35
+ }
36
+ ]);
37
+
38
+ if (!confirm) {
39
+ logger.info('Cancelado.');
40
+ return;
41
+ }
42
+ }
43
+
44
+ const removed = [];
45
+
46
+ // Remove statusline
47
+ if (manifest.modules.includes('statusline')) {
48
+ const statusline = require('../modules/statusline');
49
+ const files = await statusline.uninstall();
50
+ removed.push(...files);
51
+ logger.success('Statusline removida');
52
+ }
53
+
54
+ // Remove handoff
55
+ if (manifest.modules.includes('handoff')) {
56
+ const handoff = require('../modules/handoff');
57
+ const files = await handoff.uninstall();
58
+ removed.push(...files);
59
+ logger.success('Session Handoff removido');
60
+ }
61
+
62
+ // Remove AIOS skill
63
+ if (manifest.modules.includes('aios')) {
64
+ const aios = require('../modules/aios');
65
+ const files = await aios.uninstall();
66
+ removed.push(...files);
67
+ logger.success('AIOS Skill removida');
68
+ }
69
+
70
+ // Remove settings.json backup
71
+ const backupPath = SETTINGS_PATH + '.backup';
72
+ if (await fs.pathExists(backupPath)) {
73
+ await fs.remove(backupPath);
74
+ removed.push(backupPath);
75
+ }
76
+
77
+ // Remove manifest
78
+ await fs.remove(MANIFEST_PATH);
79
+ removed.push(MANIFEST_PATH);
80
+
81
+ logger.blank();
82
+ logger.success(`Desinstalação completa (${removed.length} itens removidos)`);
83
+
84
+ if (manifest.aiosCore) {
85
+ logger.blank();
86
+ logger.info('AIOS Core NÃO foi removido (gerenciado separadamente).');
87
+ logger.dim('Para remover: npx aios-core uninstall');
88
+ }
89
+ }
90
+
91
+ module.exports = { runUninstall };
package/src/index.js ADDED
@@ -0,0 +1,67 @@
1
+ 'use strict';
2
+
3
+ const { Command } = require('commander');
4
+ const pkg = require('../package.json');
5
+ const logger = require('./utils/logger');
6
+ const { runInstaller } = require('./installer');
7
+ const { runUninstall } = require('./commands/uninstall');
8
+ const { runDoctor } = require('./commands/doctor');
9
+
10
+ const program = new Command();
11
+
12
+ program
13
+ .name('claude-code-toolkit')
14
+ .description('Setup completo para Claude Code: statusline, session handoff, AIOS')
15
+ .version(pkg.version);
16
+
17
+ program
18
+ .command('install', { isDefault: true })
19
+ .description('Instala o toolkit (wizard interativo)')
20
+ .option('--statusline-only', 'Instala apenas a statusline')
21
+ .option('--handoff-only', 'Instala apenas o session handoff')
22
+ .option('--no-aios', 'Pula a configuração do AIOS')
23
+ .option('--currency <symbol>', 'Símbolo da moeda (ex: R$, $, €)')
24
+ .option('--currency-rate <rate>', 'Taxa de conversão USD→moeda', parseFloat)
25
+ .action(async (options) => {
26
+ logger.banner(pkg.version);
27
+ try {
28
+ await runInstaller(options);
29
+ } catch (err) {
30
+ if (err.name === 'ExitPromptError' || err.message?.includes('prompt')) {
31
+ logger.blank();
32
+ logger.info('Instalação cancelada.');
33
+ process.exit(0);
34
+ }
35
+ logger.error(`Erro: ${err.message}`);
36
+ process.exit(1);
37
+ }
38
+ });
39
+
40
+ program
41
+ .command('uninstall')
42
+ .description('Remove tudo que foi instalado pelo toolkit')
43
+ .option('--force', 'Remove sem confirmação')
44
+ .action(async (options) => {
45
+ logger.banner(pkg.version);
46
+ try {
47
+ await runUninstall(options);
48
+ } catch (err) {
49
+ logger.error(`Erro: ${err.message}`);
50
+ process.exit(1);
51
+ }
52
+ });
53
+
54
+ program
55
+ .command('doctor')
56
+ .description('Verifica integridade da instalação')
57
+ .action(async () => {
58
+ logger.banner(pkg.version);
59
+ try {
60
+ await runDoctor();
61
+ } catch (err) {
62
+ logger.error(`Erro: ${err.message}`);
63
+ process.exit(1);
64
+ }
65
+ });
66
+
67
+ program.parse();
@@ -0,0 +1,196 @@
1
+ 'use strict';
2
+
3
+ const inquirer = require('inquirer');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const fs = require('fs-extra');
7
+ const logger = require('./utils/logger');
8
+ const modules = require('./modules');
9
+
10
+ const MANIFEST_PATH = path.join(os.homedir(), '.claude', '.toolkit-manifest.json');
11
+
12
+ const CURRENCY_PRESETS = [
13
+ { name: 'BRL (R$) — Taxa: 6.20', value: { symbol: 'R$', rate: 6.20 } },
14
+ { name: 'USD ($) — Sem conversão', value: { symbol: '$', rate: 1 } },
15
+ { name: 'EUR (€) — Taxa: 0.92', value: { symbol: '€', rate: 0.92 } },
16
+ { name: 'Personalizada — Definir símbolo e taxa', value: null }
17
+ ];
18
+
19
+ async function runInstaller(cliOptions = {}) {
20
+ let selectedModules;
21
+ let currencyConfig;
22
+ let aiosOptions = {};
23
+
24
+ // --- Short-circuit for CLI flags ---
25
+ if (cliOptions.statuslineOnly) {
26
+ selectedModules = ['statusline'];
27
+ currencyConfig = {
28
+ symbol: cliOptions.currency || '$',
29
+ rate: cliOptions.currencyRate || 1
30
+ };
31
+ } else if (cliOptions.handoffOnly) {
32
+ selectedModules = ['handoff'];
33
+ } else {
34
+ // --- Interactive wizard ---
35
+
36
+ // Step 1: Module selection
37
+ const { selected } = await inquirer.prompt([
38
+ {
39
+ type: 'checkbox',
40
+ name: 'selected',
41
+ message: 'Selecione o que deseja instalar:',
42
+ choices: [
43
+ {
44
+ name: `${modules.statusline.name} — ${modules.statusline.description}`,
45
+ value: 'statusline',
46
+ checked: true
47
+ },
48
+ {
49
+ name: `${modules.handoff.name} — ${modules.handoff.description}`,
50
+ value: 'handoff',
51
+ checked: true
52
+ }
53
+ ]
54
+ }
55
+ ]);
56
+ selectedModules = selected;
57
+
58
+ if (selectedModules.length === 0) {
59
+ logger.warn('Nenhum módulo selecionado. Saindo.');
60
+ return;
61
+ }
62
+
63
+ // Step 2: Currency config (if statusline selected)
64
+ if (selectedModules.includes('statusline')) {
65
+ logger.section('Configuração da Statusline');
66
+
67
+ const { currencyPreset } = await inquirer.prompt([
68
+ {
69
+ type: 'list',
70
+ name: 'currencyPreset',
71
+ message: 'Moeda para exibição de custo:',
72
+ choices: CURRENCY_PRESETS
73
+ }
74
+ ]);
75
+
76
+ if (currencyPreset === null) {
77
+ const custom = await inquirer.prompt([
78
+ {
79
+ type: 'input',
80
+ name: 'symbol',
81
+ message: 'Símbolo da moeda:',
82
+ default: '$'
83
+ },
84
+ {
85
+ type: 'number',
86
+ name: 'rate',
87
+ message: 'Taxa de conversão USD→moeda:',
88
+ default: 1
89
+ }
90
+ ]);
91
+ currencyConfig = custom;
92
+ } else {
93
+ currencyConfig = currencyPreset;
94
+ }
95
+ }
96
+
97
+ // Step 3: AIOS (opt-in)
98
+ if (cliOptions.aios !== false) {
99
+ logger.section('AIOS Framework');
100
+
101
+ const { useAios } = await inquirer.prompt([
102
+ {
103
+ type: 'confirm',
104
+ name: 'useAios',
105
+ message: 'Vai utilizar o AIOS?',
106
+ default: false
107
+ }
108
+ ]);
109
+
110
+ if (useAios) {
111
+ const { aiosChoices } = await inquirer.prompt([
112
+ {
113
+ type: 'checkbox',
114
+ name: 'aiosChoices',
115
+ message: 'Selecione as opções AIOS:',
116
+ choices: [
117
+ {
118
+ name: 'Instalar/Atualizar AIOS Core',
119
+ value: 'core',
120
+ checked: true
121
+ },
122
+ {
123
+ name: 'Instalar Skill /aios-integrate (integrar e gerenciar artefatos)',
124
+ value: 'skill',
125
+ checked: true
126
+ }
127
+ ]
128
+ }
129
+ ]);
130
+
131
+ selectedModules.push('aios');
132
+ aiosOptions = {
133
+ installAiosCore: aiosChoices.includes('core'),
134
+ installAiosSkill: aiosChoices.includes('skill')
135
+ };
136
+ }
137
+ }
138
+ }
139
+
140
+ // --- Execute installation ---
141
+ logger.section('Instalando...');
142
+
143
+ const manifest = {
144
+ version: require('../package.json').version,
145
+ installedAt: new Date().toISOString(),
146
+ modules: selectedModules,
147
+ files: []
148
+ };
149
+
150
+ // Install statusline
151
+ if (selectedModules.includes('statusline')) {
152
+ const result = await modules.statusline.module.install({
153
+ currency: currencyConfig?.symbol,
154
+ currencyRate: currencyConfig?.rate
155
+ });
156
+ if (result.installed) {
157
+ manifest.files.push(...modules.statusline.module.getManifestEntries());
158
+ }
159
+ }
160
+
161
+ // Install handoff
162
+ if (selectedModules.includes('handoff')) {
163
+ const result = await modules.handoff.module.install();
164
+ if (result.installed) {
165
+ manifest.files.push(...modules.handoff.module.getManifestEntries());
166
+ }
167
+ }
168
+
169
+ // Install AIOS
170
+ if (selectedModules.includes('aios')) {
171
+ const result = await modules.aios.module.install(aiosOptions);
172
+ manifest.files.push(...modules.aios.module.getManifestEntries());
173
+ if (aiosOptions.installAiosCore && result.core?.installed) {
174
+ manifest.aiosCore = true;
175
+ }
176
+ }
177
+
178
+ // Save manifest
179
+ await fs.ensureDir(path.dirname(MANIFEST_PATH));
180
+ await fs.writeJson(MANIFEST_PATH, manifest, { spaces: 2 });
181
+
182
+ // Final message
183
+ logger.blank();
184
+ logger.success('Pronto!');
185
+ logger.blank();
186
+
187
+ if (selectedModules.includes('statusline')) {
188
+ logger.info('Reinicie o Claude Code para ver a statusline.');
189
+ }
190
+ if (selectedModules.includes('aios') && aiosOptions.installAiosSkill) {
191
+ logger.info('Use /aios-integrate para integrar e gerenciar artefatos AIOS.');
192
+ }
193
+ logger.blank();
194
+ }
195
+
196
+ module.exports = { runInstaller, MANIFEST_PATH };
@@ -0,0 +1,84 @@
1
+ 'use strict';
2
+
3
+ const { execSync } = require('child_process');
4
+ const path = require('path');
5
+ const fs = require('fs-extra');
6
+ const { installFile } = require('../utils/file-installer');
7
+ const logger = require('../utils/logger');
8
+
9
+ const SKILL_SRC = path.join(__dirname, '..', '..', 'assets', 'skill', 'aios-integrate.md');
10
+
11
+ async function installCore({ targetDir } = {}) {
12
+ const cwd = targetDir || process.cwd();
13
+
14
+ logger.dim('Executando aios-core install...');
15
+ try {
16
+ execSync('npx aios-core@latest install', {
17
+ cwd,
18
+ stdio: 'inherit',
19
+ timeout: 180000
20
+ });
21
+ logger.success('AIOS Core instalado/atualizado');
22
+ return { installed: true };
23
+ } catch (err) {
24
+ logger.warn(`AIOS Core falhou: ${err.message}`);
25
+ logger.dim('Você pode instalar manualmente depois: npx aios-core install');
26
+ return { installed: false, error: err.message };
27
+ }
28
+ }
29
+
30
+ async function installSkill({ targetDir } = {}) {
31
+ const cwd = targetDir || process.cwd();
32
+ const commandsDir = path.join(cwd, '.claude', 'commands');
33
+ const dest = path.join(commandsDir, 'aios-integrate.md');
34
+
35
+ await fs.ensureDir(commandsDir);
36
+
37
+ const result = await installFile(SKILL_SRC, dest);
38
+
39
+ if (result.action === 'installed') {
40
+ logger.success('Skill /aios-integrate instalada');
41
+ } else {
42
+ logger.success('Skill /aios-integrate já atualizada');
43
+ }
44
+
45
+ return { installed: true, path: dest };
46
+ }
47
+
48
+ async function install({ installAiosCore = true, installAiosSkill = true, targetDir } = {}) {
49
+ logger.section('AIOS Framework');
50
+
51
+ const results = {};
52
+
53
+ if (installAiosCore) {
54
+ results.core = await installCore({ targetDir });
55
+ }
56
+
57
+ if (installAiosSkill) {
58
+ results.skill = await installSkill({ targetDir });
59
+ }
60
+
61
+ return results;
62
+ }
63
+
64
+ async function uninstall({ targetDir } = {}) {
65
+ const cwd = targetDir || process.cwd();
66
+ const removed = [];
67
+
68
+ const skillPath = path.join(cwd, '.claude', 'commands', 'aios-integrate.md');
69
+ if (await fs.pathExists(skillPath)) {
70
+ await fs.remove(skillPath);
71
+ removed.push(skillPath);
72
+ }
73
+
74
+ return removed;
75
+ }
76
+
77
+ function getManifestEntries({ targetDir } = {}) {
78
+ const cwd = targetDir || process.cwd();
79
+ return [
80
+ path.join(cwd, '.claude', 'commands', 'aios-integrate.md')
81
+ ];
82
+ }
83
+
84
+ module.exports = { install, uninstall, getManifestEntries };
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ const { execSync } = require('child_process');
4
+ const logger = require('../utils/logger');
5
+
6
+ async function install({ targetDir } = {}) {
7
+ logger.section('Session Handoff');
8
+
9
+ const cwd = targetDir || process.cwd();
10
+
11
+ try {
12
+ logger.dim('Executando claude-code-handoff...');
13
+ execSync('npx claude-code-handoff@latest', {
14
+ cwd,
15
+ stdio: 'inherit',
16
+ timeout: 120000
17
+ });
18
+ logger.success('claude-code-handoff instalado (7 commands, 2 hooks, 2 rules)');
19
+ return { installed: true };
20
+ } catch (err) {
21
+ logger.warn(`claude-code-handoff falhou: ${err.message}`);
22
+ logger.dim('Você pode instalar manualmente depois: npx claude-code-handoff');
23
+ return { installed: false, error: err.message };
24
+ }
25
+ }
26
+
27
+ async function uninstall({ targetDir } = {}) {
28
+ const cwd = targetDir || process.cwd();
29
+
30
+ try {
31
+ execSync('npx claude-code-handoff@latest uninstall', {
32
+ cwd,
33
+ stdio: 'inherit',
34
+ timeout: 60000
35
+ });
36
+ return ['claude-code-handoff (commands, hooks, rules)'];
37
+ } catch {
38
+ logger.dim('claude-code-handoff uninstall não disponível — remova manualmente .claude/commands/');
39
+ return [];
40
+ }
41
+ }
42
+
43
+ function getManifestEntries() {
44
+ return ['claude-code-handoff'];
45
+ }
46
+
47
+ module.exports = { install, uninstall, getManifestEntries };
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ const statusline = require('./statusline');
4
+ const handoff = require('./handoff');
5
+ const aios = require('./aios');
6
+
7
+ const modules = {
8
+ statusline: {
9
+ name: 'Statusline',
10
+ description: 'Barra de status: modelo, contexto %, custo, tempo, git',
11
+ module: statusline,
12
+ default: true
13
+ },
14
+ handoff: {
15
+ name: 'Session Handoff',
16
+ description: 'Continuidade de sessão (via claude-code-handoff)',
17
+ module: handoff,
18
+ default: true
19
+ },
20
+ aios: {
21
+ name: 'AIOS Framework',
22
+ description: 'Framework de orquestração de agentes AI',
23
+ module: aios,
24
+ default: false
25
+ }
26
+ };
27
+
28
+ module.exports = modules;
@@ -0,0 +1,86 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const os = require('os');
5
+ const fs = require('fs-extra');
6
+ const { installFile } = require('../utils/file-installer');
7
+ const { mergeSettings, removeSettingsKey } = require('../utils/settings-merger');
8
+ const logger = require('../utils/logger');
9
+
10
+ const CLAUDE_DIR = path.join(os.homedir(), '.claude');
11
+ const STATUSLINE_DEST = path.join(CLAUDE_DIR, 'statusline.sh');
12
+ const STATUSLINE_SRC = path.join(__dirname, '..', '..', 'assets', 'statusline', 'statusline.sh');
13
+
14
+ const STATUSLINE_SETTINGS = {
15
+ statusLine: {
16
+ command: `bash ${STATUSLINE_DEST}`
17
+ }
18
+ };
19
+
20
+ async function install({ currency, currencyRate } = {}) {
21
+ logger.section('Statusline');
22
+
23
+ // Install statusline.sh
24
+ const result = await installFile(STATUSLINE_SRC, STATUSLINE_DEST, { executable: true });
25
+
26
+ if (result.action === 'installed') {
27
+ logger.success(`Statusline instalada em ${STATUSLINE_DEST}`);
28
+ } else {
29
+ logger.success(`Statusline já atualizada em ${STATUSLINE_DEST}`);
30
+ }
31
+
32
+ // Merge settings.json
33
+ await mergeSettings(STATUSLINE_SETTINGS);
34
+ logger.success('settings.json atualizado com statusLine');
35
+
36
+ // Create env hint file if currency was configured
37
+ if (currency || currencyRate) {
38
+ const envHints = [];
39
+ if (currency) envHints.push(`export CLAUDE_TOOLKIT_CURRENCY="${currency}"`);
40
+ if (currencyRate) envHints.push(`export CLAUDE_TOOLKIT_CURRENCY_RATE="${currencyRate}"`);
41
+
42
+ const envFile = path.join(CLAUDE_DIR, 'toolkit-env.sh');
43
+ await fs.writeFile(envFile, envHints.join('\n') + '\n');
44
+ logger.dim(`Variáveis de moeda salvas em ${envFile}`);
45
+ logger.dim('Adicione ao seu .zshrc/.bashrc: source ~/.claude/toolkit-env.sh');
46
+ }
47
+
48
+ // Check jq
49
+ const { execSync } = require('child_process');
50
+ try {
51
+ execSync('which jq', { stdio: 'ignore' });
52
+ } catch {
53
+ logger.warn('jq não encontrado. A statusline requer jq para funcionar.');
54
+ logger.dim('Instale com: brew install jq (macOS) ou apt install jq (Linux)');
55
+ }
56
+
57
+ return { installed: true };
58
+ }
59
+
60
+ async function uninstall() {
61
+ const removed = [];
62
+
63
+ if (await fs.pathExists(STATUSLINE_DEST)) {
64
+ await fs.remove(STATUSLINE_DEST);
65
+ removed.push(STATUSLINE_DEST);
66
+ }
67
+
68
+ await removeSettingsKey('statusLine');
69
+
70
+ const envFile = path.join(CLAUDE_DIR, 'toolkit-env.sh');
71
+ if (await fs.pathExists(envFile)) {
72
+ await fs.remove(envFile);
73
+ removed.push(envFile);
74
+ }
75
+
76
+ return removed;
77
+ }
78
+
79
+ function getManifestEntries() {
80
+ return [
81
+ STATUSLINE_DEST,
82
+ path.join(CLAUDE_DIR, 'toolkit-env.sh')
83
+ ];
84
+ }
85
+
86
+ module.exports = { install, uninstall, getManifestEntries };
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs-extra');
4
+ const path = require('path');
5
+ const crypto = require('crypto');
6
+ const logger = require('./logger');
7
+
8
+ function fileHash(filePath) {
9
+ const content = fs.readFileSync(filePath);
10
+ return crypto.createHash('md5').update(content).digest('hex');
11
+ }
12
+
13
+ async function installFile(src, dest, { executable = false } = {}) {
14
+ await fs.ensureDir(path.dirname(dest));
15
+
16
+ if (await fs.pathExists(dest)) {
17
+ const srcHash = fileHash(src);
18
+ const destHash = fileHash(dest);
19
+ if (srcHash === destHash) {
20
+ logger.dim(`Já atualizado: ${dest}`);
21
+ return { action: 'skipped', path: dest };
22
+ }
23
+ logger.dim(`Atualizando: ${dest}`);
24
+ }
25
+
26
+ await fs.copy(src, dest);
27
+
28
+ if (executable) {
29
+ await fs.chmod(dest, 0o755);
30
+ }
31
+
32
+ return { action: 'installed', path: dest };
33
+ }
34
+
35
+ async function uninstallFile(filePath) {
36
+ if (await fs.pathExists(filePath)) {
37
+ await fs.remove(filePath);
38
+ return { action: 'removed', path: filePath };
39
+ }
40
+ return { action: 'not_found', path: filePath };
41
+ }
42
+
43
+ module.exports = { installFile, uninstallFile, fileHash };
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+
5
+ const logger = {
6
+ banner(version) {
7
+ console.log('');
8
+ console.log(chalk.cyan(' ╔══════════════════════════════════════╗'));
9
+ console.log(chalk.cyan(' ║') + chalk.bold.white(` claude-code-toolkit v${version}`.padEnd(38)) + chalk.cyan('║'));
10
+ console.log(chalk.cyan(' ║') + chalk.gray(' Setup completo para Claude Code ') + chalk.cyan('║'));
11
+ console.log(chalk.cyan(' ╚══════════════════════════════════════╝'));
12
+ console.log('');
13
+ },
14
+
15
+ section(title) {
16
+ console.log('');
17
+ console.log(chalk.cyan(` ─── ${title} ───`));
18
+ console.log('');
19
+ },
20
+
21
+ success(msg) {
22
+ console.log(chalk.green(' ✓ ') + msg);
23
+ },
24
+
25
+ warn(msg) {
26
+ console.log(chalk.yellow(' ⚠ ') + msg);
27
+ },
28
+
29
+ error(msg) {
30
+ console.log(chalk.red(' ✗ ') + msg);
31
+ },
32
+
33
+ info(msg) {
34
+ console.log(chalk.blue(' ℹ ') + msg);
35
+ },
36
+
37
+ dim(msg) {
38
+ console.log(chalk.dim(` ${msg}`));
39
+ },
40
+
41
+ blank() {
42
+ console.log('');
43
+ }
44
+ };
45
+
46
+ module.exports = logger;
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs-extra');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const logger = require('./logger');
7
+
8
+ const SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
9
+ const BACKUP_SUFFIX = '.backup';
10
+
11
+ function deepMerge(target, source) {
12
+ const result = { ...target };
13
+ for (const key of Object.keys(source)) {
14
+ if (
15
+ source[key] &&
16
+ typeof source[key] === 'object' &&
17
+ !Array.isArray(source[key]) &&
18
+ target[key] &&
19
+ typeof target[key] === 'object' &&
20
+ !Array.isArray(target[key])
21
+ ) {
22
+ result[key] = deepMerge(target[key], source[key]);
23
+ } else {
24
+ result[key] = source[key];
25
+ }
26
+ }
27
+ return result;
28
+ }
29
+
30
+ async function mergeSettings(newSettings) {
31
+ await fs.ensureDir(path.dirname(SETTINGS_PATH));
32
+
33
+ let existing = {};
34
+ if (await fs.pathExists(SETTINGS_PATH)) {
35
+ const backupPath = SETTINGS_PATH + BACKUP_SUFFIX;
36
+ await fs.copy(SETTINGS_PATH, backupPath);
37
+ logger.dim(`Backup criado: ${backupPath}`);
38
+ existing = await fs.readJson(SETTINGS_PATH);
39
+ }
40
+
41
+ const merged = deepMerge(existing, newSettings);
42
+ await fs.writeJson(SETTINGS_PATH, merged, { spaces: 2 });
43
+ return merged;
44
+ }
45
+
46
+ async function removeSettingsKey(key) {
47
+ if (!(await fs.pathExists(SETTINGS_PATH))) return;
48
+
49
+ const settings = await fs.readJson(SETTINGS_PATH);
50
+ if (key in settings) {
51
+ delete settings[key];
52
+ await fs.writeJson(SETTINGS_PATH, settings, { spaces: 2 });
53
+ }
54
+ }
55
+
56
+ async function readSettings() {
57
+ if (!(await fs.pathExists(SETTINGS_PATH))) return {};
58
+ return fs.readJson(SETTINGS_PATH);
59
+ }
60
+
61
+ module.exports = { mergeSettings, removeSettingsKey, readSettings, SETTINGS_PATH, deepMerge };