@unity-china/codely-cli 1.0.0-beta.52 → 1.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/builtin/skill-creator/SKILL.md +381 -0
- package/bundle/builtin/skill-creator/scripts/init_skill.cjs +237 -0
- package/bundle/builtin/skill-creator/scripts/package_skill.cjs +142 -0
- package/bundle/builtin/skill-creator/scripts/validate_skill.cjs +137 -0
- package/bundle/example-prompts/analyze.toml +2 -2
- package/bundle/gemini.js +1741 -1653
- package/bundle/gemini.js.LEGAL.txt +29 -30
- package/bundle/policies/plan.toml +26 -2
- package/bundle/policies/read-only.toml +17 -3
- package/bundle/web-ui/dist/public/app.css +682 -10
- package/bundle/web-ui/dist/public/app.js +62 -44
- package/package.json +5 -1
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Packager - Creates a distributable .skill file of a skill folder
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* node package_skill.js <path/to/skill-folder> [output-directory]
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const path = require('node:path');
|
|
9
|
+
const os = require('node:os');
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
const { spawnSync } = require('node:child_process');
|
|
12
|
+
const { validateSkill } = require('./validate_skill.cjs');
|
|
13
|
+
|
|
14
|
+
const DEFAULT_OUTPUT_DIR = path.join(os.tmpdir(), 'codely-skills');
|
|
15
|
+
|
|
16
|
+
async function main() {
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
if (args.length < 1) {
|
|
19
|
+
console.log(
|
|
20
|
+
'Usage: node package_skill.cjs <path/to/skill-folder> [output-directory]',
|
|
21
|
+
);
|
|
22
|
+
console.log(
|
|
23
|
+
` output-directory: optional, default: ${DEFAULT_OUTPUT_DIR}`,
|
|
24
|
+
);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const skillPathArg = args[0];
|
|
29
|
+
const outputDirArg = args[1];
|
|
30
|
+
|
|
31
|
+
if (
|
|
32
|
+
skillPathArg.includes('..') ||
|
|
33
|
+
(outputDirArg && outputDirArg.includes('..'))
|
|
34
|
+
) {
|
|
35
|
+
console.error('❌ Error: Path traversal detected in arguments.');
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const skillPath = path.resolve(skillPathArg);
|
|
40
|
+
const outputDir = outputDirArg
|
|
41
|
+
? path.resolve(outputDirArg)
|
|
42
|
+
: DEFAULT_OUTPUT_DIR;
|
|
43
|
+
|
|
44
|
+
if (!outputDirArg) {
|
|
45
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
const skillName = path.basename(skillPath);
|
|
48
|
+
|
|
49
|
+
// 1. Validate first
|
|
50
|
+
console.log('🔍 Validating skill...');
|
|
51
|
+
const result = validateSkill(skillPath);
|
|
52
|
+
if (!result.valid) {
|
|
53
|
+
console.error(`❌ Validation failed: ${result.message}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (result.warning) {
|
|
58
|
+
console.warn(`⚠️ ${result.warning}`);
|
|
59
|
+
console.log('Please resolve all TODOs before packaging.');
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
if (result.checks && result.checks.length > 0) {
|
|
63
|
+
for (const check of result.checks) {
|
|
64
|
+
console.log(`✓ ${check}`);
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
console.log('✅ Skill is valid!');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 2. Package
|
|
71
|
+
const outputFilename = path.join(outputDir, `${skillName}.skill`);
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
// Zip everything except junk, keeping the folder structure
|
|
75
|
+
// We'll use the native 'zip' command for simplicity in a CLI environment
|
|
76
|
+
// or we could use a JS library, but zip is ubiquitous on darwin/linux.
|
|
77
|
+
|
|
78
|
+
// Command to zip:
|
|
79
|
+
// -r: recursive
|
|
80
|
+
// -x: exclude patterns
|
|
81
|
+
// Run the zip command from within the directory to avoid parent folder nesting
|
|
82
|
+
let zipProcess = spawnSync('zip', ['-r', outputFilename, '.'], {
|
|
83
|
+
cwd: skillPath,
|
|
84
|
+
stdio: 'inherit',
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (zipProcess.error || zipProcess.status !== 0) {
|
|
88
|
+
if (process.platform === 'win32') {
|
|
89
|
+
// Fallback to PowerShell Compress-Archive on Windows
|
|
90
|
+
// Note: Compress-Archive only supports .zip extension, so we zip to .zip and rename
|
|
91
|
+
console.log('zip command not found, falling back to PowerShell...');
|
|
92
|
+
const tempZip = outputFilename + '.zip';
|
|
93
|
+
// Escape single quotes for PowerShell (replace ' with '') and use single quotes for the path
|
|
94
|
+
const safeTempZip = tempZip.replace(/'/g, "''");
|
|
95
|
+
zipProcess = spawnSync(
|
|
96
|
+
'powershell.exe',
|
|
97
|
+
[
|
|
98
|
+
'-NoProfile',
|
|
99
|
+
'-Command',
|
|
100
|
+
`Compress-Archive -Path .\\* -DestinationPath '${safeTempZip}' -Force`,
|
|
101
|
+
],
|
|
102
|
+
{
|
|
103
|
+
cwd: skillPath,
|
|
104
|
+
stdio: 'inherit',
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
if (zipProcess.status === 0 && fs.existsSync(tempZip)) {
|
|
109
|
+
fs.renameSync(tempZip, outputFilename);
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
// Fallback to tar on Unix-like systems
|
|
113
|
+
console.log('zip command not found, falling back to tar...');
|
|
114
|
+
zipProcess = spawnSync(
|
|
115
|
+
'tar',
|
|
116
|
+
['-a', '-c', '--format=zip', '-f', outputFilename, '.'],
|
|
117
|
+
{
|
|
118
|
+
cwd: skillPath,
|
|
119
|
+
stdio: 'inherit',
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (zipProcess.error) {
|
|
126
|
+
throw zipProcess.error;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (zipProcess.status !== 0) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`Packaging command failed with exit code ${zipProcess.status}`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log(`✅ Successfully packaged skill to: ${outputFilename}`);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
console.error(`❌ Error packaging: ${err.message}`);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
main();
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Quick validation logic for skills.
|
|
9
|
+
* Leveraging existing dependencies when possible or providing a zero-dep fallback.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('node:fs');
|
|
13
|
+
const path = require('node:path');
|
|
14
|
+
|
|
15
|
+
function validateSkill(skillPath) {
|
|
16
|
+
if (!fs.existsSync(skillPath) || !fs.statSync(skillPath).isDirectory()) {
|
|
17
|
+
return { valid: false, message: `Path is not a directory: ${skillPath}` };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const skillMdPath = path.join(skillPath, 'SKILL.md');
|
|
21
|
+
if (!fs.existsSync(skillMdPath)) {
|
|
22
|
+
return { valid: false, message: 'SKILL.md not found' };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const content = fs.readFileSync(skillMdPath, 'utf8');
|
|
26
|
+
if (!content.startsWith('---')) {
|
|
27
|
+
return { valid: false, message: 'No YAML frontmatter found' };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const parts = content.split('---');
|
|
31
|
+
if (parts.length < 3) {
|
|
32
|
+
return { valid: false, message: 'Invalid frontmatter format' };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const frontmatterText = parts[1];
|
|
36
|
+
|
|
37
|
+
const nameMatch = frontmatterText.match(/^name:\s*(.+)$/m);
|
|
38
|
+
// Match description: "text" or description: 'text' or description: text
|
|
39
|
+
const descMatch = frontmatterText.match(
|
|
40
|
+
/^description:\s*(?:'([^']*)'|"([^"]*)"|(.+))$/m,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
if (!nameMatch)
|
|
44
|
+
return { valid: false, message: 'Missing "name" in frontmatter' };
|
|
45
|
+
if (!descMatch)
|
|
46
|
+
return {
|
|
47
|
+
valid: false,
|
|
48
|
+
message: 'Description must be a single-line string: description: ...',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const name = nameMatch[1].trim();
|
|
52
|
+
const description = (
|
|
53
|
+
descMatch[1] !== undefined
|
|
54
|
+
? descMatch[1]
|
|
55
|
+
: descMatch[2] !== undefined
|
|
56
|
+
? descMatch[2]
|
|
57
|
+
: descMatch[3] || ''
|
|
58
|
+
).trim();
|
|
59
|
+
|
|
60
|
+
if (description.includes('\n')) {
|
|
61
|
+
return {
|
|
62
|
+
valid: false,
|
|
63
|
+
message: 'Description must be a single line (no newlines)',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!/^[a-z0-9-]+$/.test(name)) {
|
|
68
|
+
return { valid: false, message: `Name "${name}" should be hyphen-case` };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (description.length > 1024) {
|
|
72
|
+
return { valid: false, message: 'Description is too long (max 1024)' };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check for TODOs
|
|
76
|
+
const files = getAllFiles(skillPath);
|
|
77
|
+
for (const file of files) {
|
|
78
|
+
const fileContent = fs.readFileSync(file, 'utf8');
|
|
79
|
+
if (fileContent.includes('TODO:')) {
|
|
80
|
+
return {
|
|
81
|
+
valid: true,
|
|
82
|
+
message: 'Skill has unresolved TODOs',
|
|
83
|
+
warning: `Found unresolved TODO in ${path.relative(skillPath, file)}`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { valid: true, message: 'Skill is valid!' };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getAllFiles(dir, fileList = []) {
|
|
92
|
+
const files = fs.readdirSync(dir);
|
|
93
|
+
files.forEach((file) => {
|
|
94
|
+
const name = path.join(dir, file);
|
|
95
|
+
if (fs.statSync(name).isDirectory()) {
|
|
96
|
+
if (!['node_modules', '.git', '__pycache__'].includes(file)) {
|
|
97
|
+
getAllFiles(name, fileList);
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
fileList.push(name);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
return fileList;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (require.main === module) {
|
|
107
|
+
const args = process.argv.slice(2);
|
|
108
|
+
if (args.length !== 1) {
|
|
109
|
+
console.log('Usage: node validate_skill.js <skill_directory>');
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const skillDirArg = args[0];
|
|
114
|
+
if (skillDirArg.includes('..')) {
|
|
115
|
+
console.error('❌ Error: Path traversal detected in skill directory path.');
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const result = validateSkill(path.resolve(skillDirArg));
|
|
120
|
+
if (result.warning) {
|
|
121
|
+
console.warn(`⚠️ ${result.warning}`);
|
|
122
|
+
}
|
|
123
|
+
if (result.valid) {
|
|
124
|
+
if (result.checks && result.checks.length > 0) {
|
|
125
|
+
for (const check of result.checks) {
|
|
126
|
+
console.log(`✓ ${check}`);
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
console.log(`✅ ${result.message}`);
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
console.error(`❌ ${result.message}`);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = { validateSkill };
|
|
@@ -7,12 +7,12 @@ Analysis Topic: {input}
|
|
|
7
7
|
Topic-driven goals:
|
|
8
8
|
1. **Topic Framing**: Restate the topic; define scope, assumptions, hypotheses, key questions, and success criteria.
|
|
9
9
|
2. **Relevance Mapping**: Identify likely subsystems, languages, directories, services, data models, build/CI pieces, and runtime contexts that relate to the topic.
|
|
10
|
-
3. **Investigation Plan**: Break work into steps using sequential_think and create a plan with
|
|
10
|
+
3. **Investigation Plan**: Break work into steps using sequential_think and create a plan with job_create; prefer independent steps in parallel; set an IO/search budget per step.
|
|
11
11
|
4. **Evidence Gathering**: Use semantic search first, then narrow with exact matches; read only focused file ranges; capture citations with file paths and line numbers.
|
|
12
12
|
5. **Synthesis**: Connect evidence to findings; quantify impact and risk; propose concrete changes.
|
|
13
13
|
|
|
14
14
|
Search and tooling rules (MANDATORY):
|
|
15
|
-
- **MUST use sequential_think first** to define scope and create a plan with
|
|
15
|
+
- **MUST use sequential_think first** to define scope and create a plan with job_create before any search.
|
|
16
16
|
- **ALWAYS keep searches tightly scoped** to specific directories/files; avoid using project root "./" as the target.
|
|
17
17
|
- Prefer codebase_search for semantic discovery; use grep/glob only for exact symbols/strings within the scoped paths.
|
|
18
18
|
- Use read_file only for specific, bounded ranges; avoid opening entire large files unless necessary.
|