@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.
- package/dist/config/skills/git-commit/LICENSE.txt +18 -0
- package/dist/config/skills/git-commit/SKILL.md +258 -0
- package/dist/config/skills/git-commit/references/REFERENCE.md +249 -0
- package/dist/config/skills/git-commit/scripts/validate_commit_msg.py +163 -0
- package/dist/config/skills/git-pr/LICENSE.txt +18 -0
- package/dist/config/skills/git-pr/SKILL.md +293 -0
- package/dist/config/skills/git-pr/references/REFERENCE.md +256 -0
- package/dist/config/skills/git-pr/scripts/validate_commits.py +112 -0
- package/dist/config/skills/pdf/LICENSE.txt +26 -0
- package/dist/config/skills/pdf/SKILL.md +327 -0
- package/dist/config/skills/pdf/references/FORMS.md +69 -0
- package/dist/config/skills/pdf/references/REFERENCE.md +52 -0
- package/dist/config/skills/pdf/scripts/create_report.py +59 -0
- package/dist/config/skills/pdf/scripts/merge_pdfs.py +39 -0
- package/dist/config/skills/skill-creator/LICENSE.txt +26 -0
- package/dist/config/skills/skill-creator/SKILL.md +229 -0
- package/dist/config/skills/xlsx/LICENSE.txt +18 -0
- package/dist/config/skills/xlsx/SKILL.md +298 -0
- package/dist/config/skills/xlsx/references/REFERENCE.md +337 -0
- package/dist/config/skills/xlsx/scripts/office/__init__.py +2 -0
- package/dist/config/skills/xlsx/scripts/office/__pycache__/__init__.cpython-312.pyc +0 -0
- package/dist/config/skills/xlsx/scripts/office/__pycache__/pack.cpython-312.pyc +0 -0
- package/dist/config/skills/xlsx/scripts/office/__pycache__/soffice.cpython-312.pyc +0 -0
- package/dist/config/skills/xlsx/scripts/office/__pycache__/unpack.cpython-312.pyc +0 -0
- package/dist/config/skills/xlsx/scripts/office/__pycache__/validate.cpython-312.pyc +0 -0
- package/dist/config/skills/xlsx/scripts/office/pack.py +58 -0
- package/dist/config/skills/xlsx/scripts/office/soffice.py +180 -0
- package/dist/config/skills/xlsx/scripts/office/unpack.py +63 -0
- package/dist/config/skills/xlsx/scripts/office/validate.py +122 -0
- package/dist/config/skills/xlsx/scripts/recalc.py +143 -0
- package/dist/main.js +201 -50
- package/package.json +1 -1
- package/dist/config/example.bluma-mcp.json.txt +0 -14
- package/dist/config/models_config.json +0 -78
- package/dist/skills/git-conventional/LICENSE.txt +0 -3
- package/dist/skills/git-conventional/SKILL.md +0 -83
- package/dist/skills/skill-creator/SKILL.md +0 -495
- package/dist/skills/testing/LICENSE.txt +0 -3
- 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:
|
|
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
|
|
3413
|
-
const configPath = path10.resolve(
|
|
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
|
|
3504
|
-
const defaultConfigPath = path11.resolve(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3865
|
-
* Skills
|
|
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
|
|
3870
|
-
|
|
3871
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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 (
|
|
5218
|
-
const reasoningText = normalizedMessage.reasoning_content || normalizedMessage.reasoning;
|
|
5358
|
+
if (reasoningContent) {
|
|
5219
5359
|
this.eventBus.emit("backend_message", {
|
|
5220
5360
|
type: "reasoning",
|
|
5221
|
-
content:
|
|
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
|
|
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 =
|
|
6912
|
-
const
|
|
6913
|
-
pkg = findBlumaPackageJson(
|
|
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
|
-
|
|
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",
|