@nomad-e/bluma-cli 0.1.18 → 0.1.20

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.
Files changed (39) hide show
  1. package/dist/config/skills/git-commit/LICENSE.txt +18 -0
  2. package/dist/config/skills/git-commit/SKILL.md +258 -0
  3. package/dist/config/skills/git-commit/references/REFERENCE.md +249 -0
  4. package/dist/config/skills/git-commit/scripts/validate_commit_msg.py +163 -0
  5. package/dist/config/skills/git-pr/LICENSE.txt +18 -0
  6. package/dist/config/skills/git-pr/SKILL.md +293 -0
  7. package/dist/config/skills/git-pr/references/REFERENCE.md +256 -0
  8. package/dist/config/skills/git-pr/scripts/validate_commits.py +112 -0
  9. package/dist/config/skills/pdf/LICENSE.txt +26 -0
  10. package/dist/config/skills/pdf/SKILL.md +327 -0
  11. package/dist/config/skills/pdf/references/FORMS.md +69 -0
  12. package/dist/config/skills/pdf/references/REFERENCE.md +52 -0
  13. package/dist/config/skills/pdf/scripts/create_report.py +59 -0
  14. package/dist/config/skills/pdf/scripts/merge_pdfs.py +39 -0
  15. package/dist/config/skills/skill-creator/LICENSE.txt +26 -0
  16. package/dist/config/skills/skill-creator/SKILL.md +229 -0
  17. package/dist/config/skills/xlsx/LICENSE.txt +18 -0
  18. package/dist/config/skills/xlsx/SKILL.md +298 -0
  19. package/dist/config/skills/xlsx/references/REFERENCE.md +337 -0
  20. package/dist/config/skills/xlsx/scripts/office/__init__.py +2 -0
  21. package/dist/config/skills/xlsx/scripts/office/__pycache__/__init__.cpython-312.pyc +0 -0
  22. package/dist/config/skills/xlsx/scripts/office/__pycache__/pack.cpython-312.pyc +0 -0
  23. package/dist/config/skills/xlsx/scripts/office/__pycache__/soffice.cpython-312.pyc +0 -0
  24. package/dist/config/skills/xlsx/scripts/office/__pycache__/unpack.cpython-312.pyc +0 -0
  25. package/dist/config/skills/xlsx/scripts/office/__pycache__/validate.cpython-312.pyc +0 -0
  26. package/dist/config/skills/xlsx/scripts/office/pack.py +58 -0
  27. package/dist/config/skills/xlsx/scripts/office/soffice.py +180 -0
  28. package/dist/config/skills/xlsx/scripts/office/unpack.py +63 -0
  29. package/dist/config/skills/xlsx/scripts/office/validate.py +122 -0
  30. package/dist/config/skills/xlsx/scripts/recalc.py +143 -0
  31. package/dist/main.js +201 -50
  32. package/package.json +1 -1
  33. package/dist/config/example.bluma-mcp.json.txt +0 -14
  34. package/dist/config/models_config.json +0 -78
  35. package/dist/skills/git-conventional/LICENSE.txt +0 -3
  36. package/dist/skills/git-conventional/SKILL.md +0 -83
  37. package/dist/skills/skill-creator/SKILL.md +0 -495
  38. package/dist/skills/testing/LICENSE.txt +0 -3
  39. package/dist/skills/testing/SKILL.md +0 -114
@@ -0,0 +1,122 @@
1
+ """
2
+ Validate the internal structure of Office Open XML files.
3
+
4
+ Checks that required parts exist, relationships are consistent,
5
+ and the ZIP structure is well-formed. Does NOT validate against
6
+ full ISO/IEC 29500 schemas (that would require lxml + full schema set).
7
+ """
8
+
9
+ import os
10
+ import zipfile
11
+
12
+ REQUIRED_PARTS_XLSX = [
13
+ '[Content_Types].xml',
14
+ '_rels/.rels',
15
+ 'xl/workbook.xml',
16
+ 'xl/_rels/workbook.xml.rels',
17
+ 'xl/styles.xml',
18
+ ]
19
+
20
+ REQUIRED_PARTS_DOCX = [
21
+ '[Content_Types].xml',
22
+ '_rels/.rels',
23
+ 'word/document.xml',
24
+ 'word/_rels/document.xml.rels',
25
+ 'word/styles.xml',
26
+ ]
27
+
28
+ REQUIRED_PARTS_PPTX = [
29
+ '[Content_Types].xml',
30
+ '_rels/.rels',
31
+ 'ppt/presentation.xml',
32
+ 'ppt/_rels/presentation.xml.rels',
33
+ ]
34
+
35
+ FILE_TYPE_MAP = {
36
+ '.xlsx': ('xlsx', REQUIRED_PARTS_XLSX),
37
+ '.xlsm': ('xlsx', REQUIRED_PARTS_XLSX),
38
+ '.docx': ('docx', REQUIRED_PARTS_DOCX),
39
+ '.pptx': ('pptx', REQUIRED_PARTS_PPTX),
40
+ }
41
+
42
+
43
+ def validate(filepath: str) -> dict:
44
+ """
45
+ Validate an Office file's internal structure.
46
+
47
+ Returns a dict with:
48
+ - valid (bool): Whether the file passes validation
49
+ - file_type (str): Detected type (xlsx/docx/pptx)
50
+ - parts_count (int): Number of parts in the archive
51
+ - missing_parts (list): Required parts that are missing
52
+ - warnings (list): Non-critical issues
53
+ - errors (list): Critical issues
54
+ """
55
+ abs_path = os.path.abspath(filepath)
56
+ ext = os.path.splitext(abs_path)[1].lower()
57
+
58
+ result = {
59
+ 'valid': True,
60
+ 'file_type': None,
61
+ 'parts_count': 0,
62
+ 'missing_parts': [],
63
+ 'warnings': [],
64
+ 'errors': [],
65
+ }
66
+
67
+ if not os.path.isfile(abs_path):
68
+ result['valid'] = False
69
+ result['errors'].append(f'File not found: {abs_path}')
70
+ return result
71
+
72
+ if ext not in FILE_TYPE_MAP:
73
+ result['valid'] = False
74
+ result['errors'].append(f'Unsupported extension: {ext}')
75
+ return result
76
+
77
+ file_type, required_parts = FILE_TYPE_MAP[ext]
78
+ result['file_type'] = file_type
79
+
80
+ try:
81
+ with zipfile.ZipFile(abs_path, 'r') as zf:
82
+ names = set(zf.namelist())
83
+ result['parts_count'] = len(names)
84
+
85
+ for part in required_parts:
86
+ if part not in names:
87
+ result['missing_parts'].append(part)
88
+
89
+ corrupt = zf.testzip()
90
+ if corrupt:
91
+ result['errors'].append(f'Corrupt entry: {corrupt}')
92
+
93
+ except zipfile.BadZipFile:
94
+ result['valid'] = False
95
+ result['errors'].append('Not a valid ZIP file')
96
+ return result
97
+ except Exception as e:
98
+ result['valid'] = False
99
+ result['errors'].append(f'Error reading file: {str(e)}')
100
+ return result
101
+
102
+ if result['missing_parts']:
103
+ result['valid'] = False
104
+ result['errors'].append(
105
+ f"Missing {len(result['missing_parts'])} required part(s)"
106
+ )
107
+
108
+ for name in names:
109
+ if name.startswith('..') or '/../' in name:
110
+ result['valid'] = False
111
+ result['errors'].append(f'Path traversal detected: {name}')
112
+
113
+ if result['parts_count'] == 0:
114
+ result['valid'] = False
115
+ result['errors'].append('Archive is empty')
116
+
117
+ if result['parts_count'] > 10000:
118
+ result['warnings'].append(
119
+ f'Unusually large archive: {result["parts_count"]} entries'
120
+ )
121
+
122
+ return result
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Recalculate all formulas in an Excel file using LibreOffice and scan
4
+ for formula errors.
5
+
6
+ Usage:
7
+ python recalc.py <excel_file> [timeout_seconds]
8
+
9
+ Returns JSON to stdout with recalculation status and any errors found.
10
+ Requires LibreOffice installed and openpyxl for error scanning.
11
+
12
+ Exit codes:
13
+ 0 Recalculated successfully (may still have formula errors)
14
+ 1 Recalculation failed
15
+ """
16
+
17
+ import json
18
+ import os
19
+ import sys
20
+ import time
21
+
22
+ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
23
+ sys.path.insert(0, SCRIPT_DIR)
24
+
25
+ from office.soffice import find_soffice, run_soffice_recalc
26
+
27
+
28
+ ERROR_VALUES = frozenset({
29
+ '#REF!', '#DIV/0!', '#VALUE!', '#N/A', '#NAME?',
30
+ '#NULL!', '#NUM!', '#GETTING_DATA', '#SPILL!', '#CALC!',
31
+ })
32
+
33
+
34
+ def scan_errors_fast(filepath: str) -> dict:
35
+ """Optimized error scanning — loads formula workbook only once."""
36
+ try:
37
+ from openpyxl import load_workbook
38
+ except ImportError:
39
+ return {
40
+ 'status': 'error',
41
+ 'message': 'openpyxl not installed — cannot scan for formula errors',
42
+ }
43
+
44
+ wb_data = load_workbook(filepath, data_only=True)
45
+ wb_formula = load_workbook(filepath)
46
+
47
+ errors = {}
48
+ total_formulas = 0
49
+ sheets_processed = []
50
+
51
+ for sheet_name in wb_data.sheetnames:
52
+ ws_data = wb_data[sheet_name]
53
+ ws_form = wb_formula[sheet_name]
54
+ sheets_processed.append(sheet_name)
55
+
56
+ for row_data, row_form in zip(ws_data.iter_rows(), ws_form.iter_rows()):
57
+ for cell_data, cell_form in zip(row_data, row_form):
58
+ if isinstance(cell_form.value, str) and cell_form.value.startswith('='):
59
+ total_formulas += 1
60
+
61
+ if isinstance(cell_data.value, str) and cell_data.value in ERROR_VALUES:
62
+ err_type = cell_data.value
63
+ if err_type not in errors:
64
+ errors[err_type] = {'count': 0, 'locations': []}
65
+ errors[err_type]['count'] += 1
66
+ loc = f"{sheet_name}!{cell_data.coordinate}"
67
+ if len(errors[err_type]['locations']) < 20:
68
+ errors[err_type]['locations'].append(loc)
69
+
70
+ wb_data.close()
71
+ wb_formula.close()
72
+
73
+ total_errors = sum(e['count'] for e in errors.values())
74
+
75
+ result = {
76
+ 'total_formulas': total_formulas,
77
+ 'total_errors': total_errors,
78
+ 'sheets_processed': sheets_processed,
79
+ }
80
+
81
+ if total_errors > 0:
82
+ result['status'] = 'errors_found'
83
+ result['error_summary'] = errors
84
+ else:
85
+ result['status'] = 'success'
86
+
87
+ return result
88
+
89
+
90
+ def main():
91
+ if len(sys.argv) < 2:
92
+ print(json.dumps({
93
+ 'status': 'error',
94
+ 'message': 'Usage: python recalc.py <excel_file> [timeout_seconds]',
95
+ }))
96
+ sys.exit(1)
97
+
98
+ filepath = os.path.abspath(sys.argv[1])
99
+ timeout = int(sys.argv[2]) if len(sys.argv) > 2 else 60
100
+
101
+ if not os.path.isfile(filepath):
102
+ print(json.dumps({
103
+ 'status': 'error',
104
+ 'message': f'File not found: {filepath}',
105
+ }))
106
+ sys.exit(1)
107
+
108
+ soffice_path = find_soffice()
109
+ if not soffice_path:
110
+ print(json.dumps({
111
+ 'status': 'error',
112
+ 'message': (
113
+ 'LibreOffice not found. Install it with: '
114
+ 'sudo apt install libreoffice-calc (Linux) or '
115
+ 'brew install --cask libreoffice (macOS)'
116
+ ),
117
+ }))
118
+ sys.exit(1)
119
+
120
+ start = time.time()
121
+
122
+ success, message = run_soffice_recalc(soffice_path, filepath, timeout)
123
+
124
+ elapsed = round(time.time() - start, 2)
125
+
126
+ if not success:
127
+ print(json.dumps({
128
+ 'status': 'error',
129
+ 'message': message,
130
+ 'elapsed_seconds': elapsed,
131
+ }))
132
+ sys.exit(1)
133
+
134
+ result = scan_errors_fast(filepath)
135
+ result['elapsed_seconds'] = elapsed
136
+ result['recalc_engine'] = 'LibreOffice'
137
+
138
+ print(json.dumps(result, indent=2))
139
+ sys.exit(0)
140
+
141
+
142
+ if __name__ == '__main__':
143
+ main()
package/dist/main.js CHANGED
@@ -3376,19 +3376,51 @@ async function loadSkill(args) {
3376
3376
  message: `Skill "${skill_name}" not found. Available skills: ${availableNames}`
3377
3377
  };
3378
3378
  }
3379
+ const warnings = [];
3380
+ const conflicts = globalContext.skillLoader.getConflicts();
3381
+ const thisConflict = conflicts.find((c) => c.name === skill_name);
3382
+ if (thisConflict) {
3383
+ warnings.push(
3384
+ `Skill "${skill_name}" exists as a native BluMa skill AND as a user skill at "${thisConflict.userPath}" (${thisConflict.userSource}). The native (bundled) version was loaded. Please rename your custom skill to avoid this conflict.`
3385
+ );
3386
+ }
3379
3387
  if (globalContext.history) {
3388
+ let injected = `[SKILL:${skill.name}]
3389
+
3390
+ ${skill.content}`;
3391
+ if (skill.references.length > 0 || skill.scripts.length > 0) {
3392
+ injected += "\n\n---\n";
3393
+ }
3394
+ if (skill.references.length > 0) {
3395
+ injected += "\n## Available References\n";
3396
+ for (const ref of skill.references) {
3397
+ injected += `- ${ref.name}: ${ref.path}
3398
+ `;
3399
+ }
3400
+ injected += "\nTo read a reference: use read_file_lines with the path above.\n";
3401
+ }
3402
+ if (skill.scripts.length > 0) {
3403
+ injected += "\n## Available Scripts\n";
3404
+ for (const s of skill.scripts) {
3405
+ injected += `- ${s.name}: ${s.path}
3406
+ `;
3407
+ }
3408
+ injected += '\nTo run a script: use shell_command with "python <path> [args]".\n';
3409
+ }
3380
3410
  globalContext.history.push({
3381
3411
  role: "user",
3382
- content: `[SKILL:${skill.name}]
3383
-
3384
- ${skill.content}`
3412
+ content: injected
3385
3413
  });
3386
3414
  }
3387
3415
  return {
3388
3416
  success: true,
3389
- message: `Skill "${skill_name}" loaded successfully. Follow the instructions in the skill content above.`,
3417
+ message: warnings.length > 0 ? `Skill "${skill_name}" loaded (native). WARNING: ${warnings[0]}` : `Skill "${skill_name}" loaded successfully (${skill.source}). Follow the instructions in the skill content above.`,
3390
3418
  skill_name: skill.name,
3391
- description: skill.description
3419
+ description: skill.description,
3420
+ source: skill.source,
3421
+ warnings: warnings.length > 0 ? warnings : void 0,
3422
+ references: skill.references.length > 0 ? skill.references : void 0,
3423
+ scripts: skill.scripts.length > 0 ? skill.scripts : void 0
3392
3424
  };
3393
3425
  }
3394
3426
 
@@ -3409,8 +3441,8 @@ var ToolInvoker = class {
3409
3441
  async initialize() {
3410
3442
  try {
3411
3443
  const __filename = fileURLToPath(import.meta.url);
3412
- const __dirname = path10.dirname(__filename);
3413
- const configPath = path10.resolve(__dirname, "config", "native_tools.json");
3444
+ const __dirname2 = path10.dirname(__filename);
3445
+ const configPath = path10.resolve(__dirname2, "config", "native_tools.json");
3414
3446
  const fileContent = await fs8.readFile(configPath, "utf-8");
3415
3447
  const config2 = JSON.parse(fileContent);
3416
3448
  this.toolDefinitions = config2.nativeTools;
@@ -3500,8 +3532,8 @@ var MCPClient = class {
3500
3532
  });
3501
3533
  }
3502
3534
  const __filename = fileURLToPath2(import.meta.url);
3503
- const __dirname = path11.dirname(__filename);
3504
- const defaultConfigPath = path11.resolve(__dirname, "config", "bluma-mcp.json");
3535
+ const __dirname2 = path11.dirname(__filename);
3536
+ const defaultConfigPath = path11.resolve(__dirname2, "config", "bluma-mcp.json");
3505
3537
  const userConfigPath = path11.join(os4.homedir(), ".bluma", "bluma-mcp.json");
3506
3538
  const defaultConfig = await this.loadMcpConfig(defaultConfigPath, "Default");
3507
3539
  const userConfig = await this.loadMcpConfig(userConfigPath, "User");
@@ -3852,33 +3884,71 @@ import { execSync } from "child_process";
3852
3884
  import fs11 from "fs";
3853
3885
  import path13 from "path";
3854
3886
  import os6 from "os";
3855
- var SkillLoader = class {
3887
+ import { fileURLToPath as fileURLToPath3 } from "url";
3888
+ var SkillLoader = class _SkillLoader {
3889
+ bundledSkillsDir;
3856
3890
  projectSkillsDir;
3857
3891
  globalSkillsDir;
3858
3892
  cache = /* @__PURE__ */ new Map();
3859
- constructor(projectRoot) {
3893
+ conflicts = [];
3894
+ constructor(projectRoot, bundledDir) {
3860
3895
  this.projectSkillsDir = path13.join(projectRoot, ".bluma", "skills");
3861
3896
  this.globalSkillsDir = path13.join(os6.homedir(), ".bluma", "skills");
3897
+ this.bundledSkillsDir = bundledDir || _SkillLoader.resolveBundledDir();
3898
+ }
3899
+ /**
3900
+ * Resolve o diretório de skills nativas relativo ao binário (dist/config/skills).
3901
+ * Funciona tanto em ESM como quando executado a partir de dist/.
3902
+ */
3903
+ static resolveBundledDir() {
3904
+ if (process.env.NODE_ENV === "test" || process.env.JEST_WORKER_ID !== void 0) {
3905
+ if (typeof __dirname !== "undefined") {
3906
+ return path13.join(__dirname, "config", "skills");
3907
+ }
3908
+ return path13.join(process.cwd(), "dist", "config", "skills");
3909
+ }
3910
+ try {
3911
+ const currentFile = fileURLToPath3(import.meta.url);
3912
+ const distDir = path13.dirname(currentFile);
3913
+ return path13.join(distDir, "config", "skills");
3914
+ } catch {
3915
+ return path13.join(process.cwd(), "dist", "config", "skills");
3916
+ }
3862
3917
  }
3863
3918
  /**
3864
- * Lista skills disponíveis de ambas as fontes
3865
- * Skills de projeto têm prioridade sobre globais com mesmo nome
3919
+ * Lista skills disponíveis de todas as fontes.
3920
+ * Skills nativas sempre presentes; skills do utilizador com nomes conflitantes são excluídas.
3866
3921
  */
3867
3922
  listAvailable() {
3923
+ this.conflicts = [];
3868
3924
  const skills = /* @__PURE__ */ new Map();
3869
- const globalSkills = this.listFromDir(this.globalSkillsDir, "global");
3870
- for (const skill of globalSkills) {
3871
- skills.set(skill.name, skill);
3872
- }
3873
- const projectSkills = this.listFromDir(this.projectSkillsDir, "project");
3874
- for (const skill of projectSkills) {
3925
+ const bundledSkills = this.listFromDir(this.bundledSkillsDir, "bundled");
3926
+ const bundledNames = new Set(bundledSkills.map((s) => s.name));
3927
+ for (const skill of bundledSkills) {
3875
3928
  skills.set(skill.name, skill);
3876
3929
  }
3930
+ this.mergeUserSkills(skills, bundledNames, this.globalSkillsDir, "global");
3931
+ this.mergeUserSkills(skills, bundledNames, this.projectSkillsDir, "project");
3877
3932
  return Array.from(skills.values());
3878
3933
  }
3879
3934
  /**
3880
- * Lista skills de um diretório específico
3935
+ * Adiciona skills do utilizador ao mapa, detetando conflitos com nativas.
3881
3936
  */
3937
+ mergeUserSkills(skills, bundledNames, dir, source) {
3938
+ const userSkills = this.listFromDir(dir, source);
3939
+ for (const skill of userSkills) {
3940
+ if (bundledNames.has(skill.name)) {
3941
+ this.conflicts.push({
3942
+ name: skill.name,
3943
+ userSource: source,
3944
+ userPath: path13.join(dir, skill.name, "SKILL.md"),
3945
+ bundledPath: path13.join(this.bundledSkillsDir, skill.name, "SKILL.md")
3946
+ });
3947
+ continue;
3948
+ }
3949
+ skills.set(skill.name, skill);
3950
+ }
3951
+ }
3882
3952
  listFromDir(dir, source) {
3883
3953
  if (!fs11.existsSync(dir)) return [];
3884
3954
  try {
@@ -3890,9 +3960,6 @@ var SkillLoader = class {
3890
3960
  return [];
3891
3961
  }
3892
3962
  }
3893
- /**
3894
- * Carrega metadata de um path específico
3895
- */
3896
3963
  loadMetadataFromPath(skillPath, skillName, source) {
3897
3964
  if (!fs11.existsSync(skillPath)) return null;
3898
3965
  try {
@@ -3911,20 +3978,47 @@ var SkillLoader = class {
3911
3978
  }
3912
3979
  }
3913
3980
  /**
3914
- * Carrega skill completa - procura primeiro em projeto, depois global
3981
+ * Carrega skill completa.
3982
+ * Ordem: bundled > project > global.
3983
+ * Retorna null se não encontrada.
3984
+ * Lança erro se o nome existir como nativa E como skill do utilizador (conflito).
3915
3985
  */
3916
3986
  load(name) {
3917
3987
  if (this.cache.has(name)) return this.cache.get(name);
3988
+ const bundledPath = path13.join(this.bundledSkillsDir, name, "SKILL.md");
3918
3989
  const projectPath = path13.join(this.projectSkillsDir, name, "SKILL.md");
3919
- if (fs11.existsSync(projectPath)) {
3990
+ const globalPath = path13.join(this.globalSkillsDir, name, "SKILL.md");
3991
+ const existsBundled = fs11.existsSync(bundledPath);
3992
+ const existsProject = fs11.existsSync(projectPath);
3993
+ const existsGlobal = fs11.existsSync(globalPath);
3994
+ if (existsBundled && (existsProject || existsGlobal)) {
3995
+ const conflictSource = existsProject ? "project" : "global";
3996
+ const conflictPath = existsProject ? projectPath : globalPath;
3997
+ const conflict = {
3998
+ name,
3999
+ userSource: conflictSource,
4000
+ userPath: conflictPath,
4001
+ bundledPath
4002
+ };
4003
+ if (!this.conflicts.find((c) => c.name === name)) {
4004
+ this.conflicts.push(conflict);
4005
+ }
4006
+ }
4007
+ if (existsBundled) {
4008
+ const skill = this.loadFromPath(bundledPath, name, "bundled");
4009
+ if (skill) {
4010
+ this.cache.set(name, skill);
4011
+ return skill;
4012
+ }
4013
+ }
4014
+ if (existsProject) {
3920
4015
  const skill = this.loadFromPath(projectPath, name, "project");
3921
4016
  if (skill) {
3922
4017
  this.cache.set(name, skill);
3923
4018
  return skill;
3924
4019
  }
3925
4020
  }
3926
- const globalPath = path13.join(this.globalSkillsDir, name, "SKILL.md");
3927
- if (fs11.existsSync(globalPath)) {
4021
+ if (existsGlobal) {
3928
4022
  const skill = this.loadFromPath(globalPath, name, "global");
3929
4023
  if (skill) {
3930
4024
  this.cache.set(name, skill);
@@ -3933,13 +4027,11 @@ var SkillLoader = class {
3933
4027
  }
3934
4028
  return null;
3935
4029
  }
3936
- /**
3937
- * Carrega skill de um path específico
3938
- */
3939
4030
  loadFromPath(skillPath, name, source) {
3940
4031
  try {
3941
4032
  const raw = fs11.readFileSync(skillPath, "utf-8");
3942
4033
  const parsed = this.parseFrontmatter(raw);
4034
+ const skillDir = path13.dirname(skillPath);
3943
4035
  return {
3944
4036
  name: parsed.name || name,
3945
4037
  description: parsed.description || "",
@@ -3947,15 +4039,28 @@ var SkillLoader = class {
3947
4039
  source,
3948
4040
  version: parsed.version,
3949
4041
  author: parsed.author,
3950
- license: parsed.license
4042
+ license: parsed.license,
4043
+ references: this.scanAssets(path13.join(skillDir, "references")),
4044
+ scripts: this.scanAssets(path13.join(skillDir, "scripts"))
3951
4045
  };
3952
4046
  } catch {
3953
4047
  return null;
3954
4048
  }
3955
4049
  }
3956
- /**
3957
- * Parse simples de YAML frontmatter
3958
- */
4050
+ scanAssets(dir) {
4051
+ if (!fs11.existsSync(dir)) return [];
4052
+ try {
4053
+ return fs11.readdirSync(dir).filter((f) => {
4054
+ const fp = path13.join(dir, f);
4055
+ return fs11.statSync(fp).isFile();
4056
+ }).map((f) => ({
4057
+ name: f,
4058
+ path: path13.resolve(dir, f)
4059
+ }));
4060
+ } catch {
4061
+ return [];
4062
+ }
4063
+ }
3959
4064
  parseFrontmatter(raw) {
3960
4065
  const lines = raw.split("\n");
3961
4066
  if (lines[0]?.trim() !== "---") {
@@ -3991,25 +4096,38 @@ var SkillLoader = class {
3991
4096
  content
3992
4097
  };
3993
4098
  }
3994
- /**
3995
- * Limpa o cache
3996
- */
3997
4099
  clearCache() {
3998
4100
  this.cache.clear();
3999
4101
  }
4000
- /**
4001
- * Verifica se uma skill existe (em projeto ou global)
4002
- */
4003
4102
  exists(name) {
4103
+ const bundledPath = path13.join(this.bundledSkillsDir, name, "SKILL.md");
4004
4104
  const projectPath = path13.join(this.projectSkillsDir, name, "SKILL.md");
4005
4105
  const globalPath = path13.join(this.globalSkillsDir, name, "SKILL.md");
4006
- return fs11.existsSync(projectPath) || fs11.existsSync(globalPath);
4106
+ return fs11.existsSync(bundledPath) || fs11.existsSync(projectPath) || fs11.existsSync(globalPath);
4107
+ }
4108
+ /**
4109
+ * Retorna conflitos detetados (skills do utilizador com mesmo nome de nativas).
4110
+ */
4111
+ getConflicts() {
4112
+ return [...this.conflicts];
4113
+ }
4114
+ /**
4115
+ * Verifica se existem conflitos (útil para logs/warnings rápidos).
4116
+ */
4117
+ hasConflicts() {
4118
+ return this.conflicts.length > 0;
4007
4119
  }
4008
4120
  /**
4009
- * Retorna os diretórios de skills
4121
+ * Formata mensagens de conflito legíveis para o utilizador.
4010
4122
  */
4123
+ formatConflictWarnings() {
4124
+ return this.conflicts.map(
4125
+ (c) => `Skill "${c.name}" already exists as a native BluMa skill. Your skill at "${c.userPath}" (${c.userSource}) was ignored. Please rename your skill to avoid this conflict.`
4126
+ );
4127
+ }
4011
4128
  getSkillsDirs() {
4012
4129
  return {
4130
+ bundled: this.bundledSkillsDir,
4013
4131
  project: this.projectSkillsDir,
4014
4132
  global: this.globalSkillsDir
4015
4133
  };
@@ -4156,6 +4274,21 @@ var SYSTEM_PROMPT = `
4156
4274
  - NEVER invent skill names that aren't in \`<available_skills>\`
4157
4275
  - NEVER try to \`load_skill\` with a name that isn't in \`<available_skills>\`
4158
4276
  - Your base knowledge (testing, git, docker...) is NOT a skill - it's just knowledge you have
4277
+
4278
+ **Progressive Disclosure (Skills with references and scripts):**
4279
+
4280
+ Skills may include additional assets beyond the SKILL.md body:
4281
+ - \`references/\` \u2014 extra documentation files (e.g. REFERENCE.md, FORMS.md) loaded on-demand.
4282
+ - \`scripts/\` \u2014 ready-made executable scripts (e.g. Python) you can run directly.
4283
+
4284
+ When you load a skill via \`load_skill\`, the result includes a manifest listing available references and scripts with their absolute paths.
4285
+
4286
+ Rules:
4287
+ - **Do NOT read references or run scripts unless the task specifically requires them.** The SKILL.md body is often sufficient.
4288
+ - To read a reference: use \`read_file_lines\` with the absolute path from the manifest.
4289
+ - To run a script: use \`shell_command\` with \`python <absolute_path> [args]\`.
4290
+ - The SKILL.md body tells you WHEN to consult each reference or script (e.g. "for forms, read references/FORMS.md").
4291
+ - This keeps your context lean \u2014 only load what you need.
4159
4292
  </skills_knowledge>
4160
4293
 
4161
4294
  ---
@@ -4970,6 +5103,14 @@ var BluMaAgent = class {
4970
5103
  });
4971
5104
  if (this.history.length === 0) {
4972
5105
  const availableSkills = this.skillLoader.listAvailable();
5106
+ if (this.skillLoader.hasConflicts()) {
5107
+ for (const warning of this.skillLoader.formatConflictWarnings()) {
5108
+ this.eventBus.emit("backend_message", {
5109
+ type: "warning",
5110
+ message: warning
5111
+ });
5112
+ }
5113
+ }
4973
5114
  const systemPrompt = getUnifiedSystemPrompt(availableSkills);
4974
5115
  this.history.push({ role: "system", content: systemPrompt });
4975
5116
  await saveSessionHistory(this.sessionFile, this.history);
@@ -5214,11 +5355,10 @@ ${editData.error.display}`;
5214
5355
  message2.tool_calls = toolCalls;
5215
5356
  }
5216
5357
  const normalizedMessage = ToolCallNormalizer.normalizeAssistantMessage(message2);
5217
- if (normalizedMessage.reasoning_content || normalizedMessage.reasoning) {
5218
- const reasoningText = normalizedMessage.reasoning_content || normalizedMessage.reasoning;
5358
+ if (reasoningContent) {
5219
5359
  this.eventBus.emit("backend_message", {
5220
5360
  type: "reasoning",
5221
- content: typeof reasoningText === "string" ? reasoningText : JSON.stringify(reasoningText)
5361
+ content: reasoningContent
5222
5362
  });
5223
5363
  }
5224
5364
  this.history.push(normalizedMessage);
@@ -6873,7 +7013,7 @@ var SlashCommands_default = SlashCommands;
6873
7013
 
6874
7014
  // src/app/agent/utils/update_check.ts
6875
7015
  import updateNotifier from "update-notifier";
6876
- import { fileURLToPath as fileURLToPath3 } from "url";
7016
+ import { fileURLToPath as fileURLToPath4 } from "url";
6877
7017
  import path17 from "path";
6878
7018
  import fs13 from "fs";
6879
7019
  var BLUMA_PACKAGE_NAME = "@nomad-e/bluma-cli";
@@ -6908,9 +7048,9 @@ async function checkForUpdates() {
6908
7048
  pkg = findBlumaPackageJson(path17.dirname(binPath));
6909
7049
  }
6910
7050
  if (!pkg) {
6911
- const __filename = fileURLToPath3(import.meta.url);
6912
- const __dirname = path17.dirname(__filename);
6913
- pkg = findBlumaPackageJson(__dirname);
7051
+ const __filename = fileURLToPath4(import.meta.url);
7052
+ const __dirname2 = path17.dirname(__filename);
7053
+ pkg = findBlumaPackageJson(__dirname2);
6914
7054
  }
6915
7055
  if (!pkg) {
6916
7056
  return null;
@@ -7613,7 +7753,9 @@ async function runAgentMode() {
7613
7753
  lastAssistantMessage = payload.content;
7614
7754
  }
7615
7755
  if (payload?.type === "reasoning" && typeof payload.content === "string") {
7616
- reasoningBuffer = (reasoningBuffer || "") + payload.content;
7756
+ if (!reasoningBuffer) {
7757
+ reasoningBuffer = payload.content;
7758
+ }
7617
7759
  }
7618
7760
  if (payload?.type === "tool_result" && payload.tool_name === "message") {
7619
7761
  try {
@@ -7654,6 +7796,15 @@ async function runAgentMode() {
7654
7796
  payload
7655
7797
  });
7656
7798
  });
7799
+ eventBus.on("stream_reasoning_chunk", (payload) => {
7800
+ writeJsonl({
7801
+ event_type: "backend_message",
7802
+ backend_type: "reasoning_delta",
7803
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7804
+ payload: { type: "reasoning_delta", delta: payload?.delta }
7805
+ });
7806
+ reasoningBuffer = (reasoningBuffer || "") + (payload?.delta || "");
7807
+ });
7657
7808
  writeJsonl({
7658
7809
  event_type: "log",
7659
7810
  level: "info",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nomad-e/bluma-cli",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "BluMa independent agent for automation and advanced software engineering.",
5
5
  "author": "Alex Fonseca",
6
6
  "license": "Apache-2.0",