@kodax-ai/kodax-cli 0.7.38
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/CHANGELOG.md +1304 -0
- package/LICENSE +191 -0
- package/README.md +1167 -0
- package/README_CN.md +631 -0
- package/dist/builtin/code-review/SKILL.md +63 -0
- package/dist/builtin/git-workflow/SKILL.md +84 -0
- package/dist/builtin/skill-creator/SKILL.md +122 -0
- package/dist/builtin/skill-creator/agents/analyzer.md +12 -0
- package/dist/builtin/skill-creator/agents/comparator.md +13 -0
- package/dist/builtin/skill-creator/agents/grader.md +13 -0
- package/dist/builtin/skill-creator/references/schemas.md +227 -0
- package/dist/builtin/skill-creator/scripts/aggregate-benchmark.d.ts +46 -0
- package/dist/builtin/skill-creator/scripts/aggregate-benchmark.js +209 -0
- package/dist/builtin/skill-creator/scripts/analyze-benchmark.d.ts +46 -0
- package/dist/builtin/skill-creator/scripts/analyze-benchmark.js +289 -0
- package/dist/builtin/skill-creator/scripts/compare-runs.d.ts +62 -0
- package/dist/builtin/skill-creator/scripts/compare-runs.js +333 -0
- package/dist/builtin/skill-creator/scripts/generate-review.d.ts +33 -0
- package/dist/builtin/skill-creator/scripts/generate-review.js +415 -0
- package/dist/builtin/skill-creator/scripts/grade-evals.d.ts +73 -0
- package/dist/builtin/skill-creator/scripts/grade-evals.js +405 -0
- package/dist/builtin/skill-creator/scripts/improve-description.d.ts +23 -0
- package/dist/builtin/skill-creator/scripts/improve-description.js +161 -0
- package/dist/builtin/skill-creator/scripts/init-skill.d.ts +14 -0
- package/dist/builtin/skill-creator/scripts/init-skill.js +153 -0
- package/dist/builtin/skill-creator/scripts/install-skill.d.ts +29 -0
- package/dist/builtin/skill-creator/scripts/install-skill.js +176 -0
- package/dist/builtin/skill-creator/scripts/package-skill.d.ts +38 -0
- package/dist/builtin/skill-creator/scripts/package-skill.js +124 -0
- package/dist/builtin/skill-creator/scripts/quick-validate.d.ts +8 -0
- package/dist/builtin/skill-creator/scripts/quick-validate.js +166 -0
- package/dist/builtin/skill-creator/scripts/run-eval.d.ts +66 -0
- package/dist/builtin/skill-creator/scripts/run-eval.js +356 -0
- package/dist/builtin/skill-creator/scripts/run-loop.d.ts +49 -0
- package/dist/builtin/skill-creator/scripts/run-loop.js +243 -0
- package/dist/builtin/skill-creator/scripts/run-trigger-eval.d.ts +58 -0
- package/dist/builtin/skill-creator/scripts/run-trigger-eval.js +225 -0
- package/dist/builtin/skill-creator/scripts/utils.js +278 -0
- package/dist/builtin/tdd/SKILL.md +56 -0
- package/dist/index.js +1717 -0
- package/dist/kodax_cli.js +1870 -0
- package/package.json +122 -0
- package/scripts/kodax-bin.cjs +27 -0
- package/scripts/production-env.cjs +16 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
8
|
+
import YAML from 'yaml';
|
|
9
|
+
|
|
10
|
+
// FEATURE_150 (v0.7.37, renamed v0.7.39) — KodaX SDK loader for builtin helper scripts.
|
|
11
|
+
//
|
|
12
|
+
// These helper scripts (run-eval.js / grade-evals.js / etc.) need access to
|
|
13
|
+
// the runKodaX / estimateTokens API. The SDK lives in different places
|
|
14
|
+
// depending on install mode:
|
|
15
|
+
//
|
|
16
|
+
// 1. Bundle-installed (npm install -g @kodax-ai/kodax-cli):
|
|
17
|
+
// this file → <prefix>/lib/node_modules/@kodax-ai/kodax-cli/dist/builtin-skills/skill-creator/scripts/utils.js
|
|
18
|
+
// SDK → <prefix>/lib/node_modules/@kodax-ai/kodax-cli/dist/index.js
|
|
19
|
+
// Resolution: relative path '../../../index.js' (3 levels up from scripts/ to dist/)
|
|
20
|
+
//
|
|
21
|
+
// 2. Dev monorepo (npm run dev / direct invocation against built sub-packages):
|
|
22
|
+
// this file → <repo>/packages/skills/dist/builtin/skill-creator/scripts/utils.js
|
|
23
|
+
// SDK → not at relative path; resolved via npm workspace symlink
|
|
24
|
+
// Resolution: bare-name `@kodax-ai/coding` (workspace alias works in this scope)
|
|
25
|
+
//
|
|
26
|
+
// 3. Path B SDK consumer (`npm install @kodax-ai/kodax-cli` in user project):
|
|
27
|
+
// this file would be inside their node_modules; same as case 1 layout.
|
|
28
|
+
//
|
|
29
|
+
// Legacy: v0.7.37/v0.7.38 published the bundle under `@kodax-ai/cli`. That
|
|
30
|
+
// package is deprecated as of v0.7.39 (renamed to `@kodax-ai/kodax-cli`).
|
|
31
|
+
// The Strategy 4 fallback below keeps `import('@kodax-ai/cli')` working for
|
|
32
|
+
// any user still on the legacy installed version; remove the fallback when
|
|
33
|
+
// no `@kodax-ai/cli` installs remain in the wild.
|
|
34
|
+
//
|
|
35
|
+
// See docs/HLD.md §12.4 risk 3 for the dist-layout contract.
|
|
36
|
+
|
|
37
|
+
let _cachedSdk = null;
|
|
38
|
+
|
|
39
|
+
export async function loadKodaXSDK() {
|
|
40
|
+
if (_cachedSdk) return _cachedSdk;
|
|
41
|
+
|
|
42
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
43
|
+
const errors = [];
|
|
44
|
+
|
|
45
|
+
// Strategy 1: bundled install — relative path to dist/index.js
|
|
46
|
+
// Layout: dist/builtin-skills/<skill>/scripts/utils.js → ../../../index.js
|
|
47
|
+
const relSdkPath = path.resolve(here, '../../../index.js');
|
|
48
|
+
if (existsSync(relSdkPath)) {
|
|
49
|
+
_cachedSdk = await import(pathToFileURL(relSdkPath).href);
|
|
50
|
+
return _cachedSdk;
|
|
51
|
+
}
|
|
52
|
+
errors.push(`relative SDK path not found: ${relSdkPath}`);
|
|
53
|
+
|
|
54
|
+
// Strategy 2: dev monorepo — workspace symlink resolves bare name
|
|
55
|
+
try {
|
|
56
|
+
_cachedSdk = await import('@kodax-ai/coding');
|
|
57
|
+
return _cachedSdk;
|
|
58
|
+
} catch (err) {
|
|
59
|
+
errors.push(`bare-name @kodax-ai/coding failed: ${err?.code ?? err?.message}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Strategy 3: bundled install via bare cli name (npm-installed CLI alongside)
|
|
63
|
+
try {
|
|
64
|
+
_cachedSdk = await import('@kodax-ai/kodax-cli');
|
|
65
|
+
return _cachedSdk;
|
|
66
|
+
} catch (err) {
|
|
67
|
+
errors.push(`bare-name @kodax-ai/kodax-cli failed: ${err?.code ?? err?.message}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Strategy 4: legacy fallback for users still on @kodax-ai/cli installs
|
|
71
|
+
// (v0.7.37/v0.7.38). Will be removed after a reasonable deprecation window.
|
|
72
|
+
try {
|
|
73
|
+
_cachedSdk = await import('@kodax-ai/cli');
|
|
74
|
+
return _cachedSdk;
|
|
75
|
+
} catch (err) {
|
|
76
|
+
errors.push(`bare-name @kodax-ai/cli (legacy) failed: ${err?.code ?? err?.message}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
throw new Error(
|
|
80
|
+
`Cannot locate KodaX SDK from helper script ${import.meta.url}.\nAttempted:\n - ${errors.join('\n - ')}`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function toPosixPath(filePath) {
|
|
85
|
+
return filePath.replace(/\\/g, '/');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function extractFrontmatter(rawContent) {
|
|
89
|
+
const normalized = rawContent
|
|
90
|
+
.replace(/^\uFEFF/, '')
|
|
91
|
+
.replace(/\r\n/g, '\n')
|
|
92
|
+
.replace(/\r/g, '\n')
|
|
93
|
+
.trimStart();
|
|
94
|
+
|
|
95
|
+
if (!normalized.startsWith('---\n')) {
|
|
96
|
+
throw new Error('SKILL.md missing YAML frontmatter');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const closeIndex = normalized.indexOf('\n---\n', 4);
|
|
100
|
+
if (closeIndex === -1) {
|
|
101
|
+
throw new Error('SKILL.md has unclosed YAML frontmatter');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
yamlText: normalized.slice(4, closeIndex),
|
|
106
|
+
body: normalized.slice(closeIndex + 5).trim(),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function parseSkillMarkdown(rawContent) {
|
|
111
|
+
const { yamlText, body } = extractFrontmatter(rawContent);
|
|
112
|
+
const frontmatter = YAML.parse(yamlText);
|
|
113
|
+
|
|
114
|
+
if (!frontmatter || typeof frontmatter !== 'object' || Array.isArray(frontmatter)) {
|
|
115
|
+
throw new Error('Frontmatter must be a YAML object');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
frontmatter,
|
|
120
|
+
body,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function loadSkill(skillDir) {
|
|
125
|
+
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
|
126
|
+
const info = await stat(skillMdPath).catch(() => null);
|
|
127
|
+
if (!info?.isFile()) {
|
|
128
|
+
throw new Error(`SKILL.md not found in ${skillDir}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const rawContent = await readFile(skillMdPath, 'utf8');
|
|
132
|
+
const { frontmatter, body } = parseSkillMarkdown(rawContent);
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
skillDir: path.resolve(skillDir),
|
|
136
|
+
skillMdPath,
|
|
137
|
+
rawContent,
|
|
138
|
+
body,
|
|
139
|
+
frontmatter,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function writeSkill(skillDir, frontmatter, body) {
|
|
144
|
+
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
|
145
|
+
const yamlText = YAML.stringify(frontmatter).trimEnd();
|
|
146
|
+
const content = `---\n${yamlText}\n---\n\n${body.trim()}\n`;
|
|
147
|
+
await writeFile(skillMdPath, content, 'utf8');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function pathExists(targetPath) {
|
|
151
|
+
try {
|
|
152
|
+
await stat(targetPath);
|
|
153
|
+
return true;
|
|
154
|
+
} catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function calculateStats(values) {
|
|
160
|
+
if (!values.length) {
|
|
161
|
+
return { mean: 0, stddev: 0, min: 0, max: 0 };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const mean = values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
165
|
+
const variance = values.length > 1
|
|
166
|
+
? values.reduce((sum, value) => sum + ((value - mean) ** 2), 0) / (values.length - 1)
|
|
167
|
+
: 0;
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
mean: roundNumber(mean),
|
|
171
|
+
stddev: roundNumber(Math.sqrt(variance)),
|
|
172
|
+
min: roundNumber(Math.min(...values)),
|
|
173
|
+
max: roundNumber(Math.max(...values)),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function roundNumber(value, digits = 4) {
|
|
178
|
+
return Number(value.toFixed(digits));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function formatDelta(value) {
|
|
182
|
+
return value >= 0 ? `+${value.toFixed(4)}` : value.toFixed(4);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function extractTaggedText(text, tagName) {
|
|
186
|
+
const pattern = new RegExp(`<${tagName}>([\\s\\S]*?)<\\/${tagName}>`, 'i');
|
|
187
|
+
const match = text.match(pattern);
|
|
188
|
+
return match?.[1]?.trim() ?? null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function safeJsonParse(text) {
|
|
192
|
+
try {
|
|
193
|
+
return JSON.parse(text);
|
|
194
|
+
} catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function extractJsonObject(text) {
|
|
200
|
+
const direct = safeJsonParse(text.trim());
|
|
201
|
+
if (direct && typeof direct === 'object') {
|
|
202
|
+
return direct;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const firstBrace = text.indexOf('{');
|
|
206
|
+
const lastBrace = text.lastIndexOf('}');
|
|
207
|
+
if (firstBrace === -1 || lastBrace === -1 || lastBrace <= firstBrace) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return safeJsonParse(text.slice(firstBrace, lastBrace + 1));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function readJsonFile(filePath, fallback = null) {
|
|
215
|
+
try {
|
|
216
|
+
return JSON.parse(await readFile(filePath, 'utf8'));
|
|
217
|
+
} catch {
|
|
218
|
+
return fallback;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function loadRelativeText(moduleUrl, relativePath) {
|
|
223
|
+
const moduleDir = path.dirname(fileURLToPath(moduleUrl));
|
|
224
|
+
return readFile(path.resolve(moduleDir, relativePath), 'utf8');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function truncateText(text, maxChars = 12000) {
|
|
228
|
+
const value = typeof text === 'string' ? text : String(text ?? '');
|
|
229
|
+
if (value.length <= maxChars) {
|
|
230
|
+
return value;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const hiddenChars = value.length - maxChars;
|
|
234
|
+
return `${value.slice(0, maxChars)}\n\n[truncated ${hiddenChars} chars]`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function computePassSummary(expectations) {
|
|
238
|
+
const total = Array.isArray(expectations) ? expectations.length : 0;
|
|
239
|
+
const passed = Array.isArray(expectations)
|
|
240
|
+
? expectations.filter((item) => item?.passed === true).length
|
|
241
|
+
: 0;
|
|
242
|
+
const failed = Math.max(total - passed, 0);
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
passed,
|
|
246
|
+
failed,
|
|
247
|
+
total,
|
|
248
|
+
pass_rate: total > 0 ? roundNumber(passed / total) : 0,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export async function collectFiles(rootDir, currentDir = rootDir, files = []) {
|
|
253
|
+
const entries = await readdir(currentDir, { withFileTypes: true }).catch(() => []);
|
|
254
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
255
|
+
const absolutePath = path.join(currentDir, entry.name);
|
|
256
|
+
if (entry.isDirectory()) {
|
|
257
|
+
await collectFiles(rootDir, absolutePath, files);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (!entry.isFile()) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
files.push({
|
|
264
|
+
absolutePath,
|
|
265
|
+
relativePath: path.relative(rootDir, absolutePath).replace(/\\/g, '/'),
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return files;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function getDefaultSkillsDir() {
|
|
272
|
+
return path.join(os.homedir(), '.kodax', 'skills');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export async function ensureDirectory(targetPath) {
|
|
276
|
+
await mkdir(targetPath, { recursive: true });
|
|
277
|
+
return targetPath;
|
|
278
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tdd
|
|
3
|
+
description: 用测试驱动开发方式实现或修复功能:先写失败测试,再做最小实现,最后重构。只在用户明确要求 TDD、先写测试、补回归测试或按 Red-Green-Refactor 工作时使用;不要用于单纯解释测试报错或泛泛讨论测试概念。
|
|
4
|
+
user-invocable: true
|
|
5
|
+
disable-model-invocation: true
|
|
6
|
+
allowed-tools: "Read, Grep, Glob, Write, Edit, Bash(npm:*, node:*, npx:*, vitest:*, jest:*, pytest:*)"
|
|
7
|
+
argument-hint: "[file-or-description]"
|
|
8
|
+
compatibility: "Works best in repositories with an existing test runner and established test conventions."
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# TDD (Test-Driven Development) Skill
|
|
12
|
+
|
|
13
|
+
按小步快跑的 Red -> Green -> Refactor 循环工作,优先建立可复现的行为保护,再修改实现。
|
|
14
|
+
|
|
15
|
+
## 开始前
|
|
16
|
+
|
|
17
|
+
- 根据 `$ARGUMENTS` 明确目标行为、bug 场景或待实现功能。
|
|
18
|
+
- 先读现有实现、相邻测试和项目约定,优先沿用仓库已有的测试框架、fixture 和命名方式。
|
|
19
|
+
- 如果用户只要求“补测试”而没有要求改实现,就停在测试层,不主动改生产代码。
|
|
20
|
+
|
|
21
|
+
## RED
|
|
22
|
+
|
|
23
|
+
1. 写能暴露目标行为的最小失败测试。
|
|
24
|
+
2. 对 bug 修复,先写复现 bug 的回归测试。
|
|
25
|
+
3. 优先运行最小测试范围,确认它确实失败,并说明失败证明了什么。
|
|
26
|
+
|
|
27
|
+
## GREEN
|
|
28
|
+
|
|
29
|
+
1. 只做让新测试通过所需的最小实现改动。
|
|
30
|
+
2. 先重跑刚刚失败的测试;必要时再补跑相邻测试。
|
|
31
|
+
3. 不为了“顺手优化”扩大改动面。
|
|
32
|
+
|
|
33
|
+
## REFACTOR
|
|
34
|
+
|
|
35
|
+
1. 在测试保护下整理命名、消除重复、简化实现。
|
|
36
|
+
2. 任何重构后都重新运行相关测试,确保行为不变。
|
|
37
|
+
3. 只有在改动面较大或用户明确要求时,才扩大到更完整的测试集。
|
|
38
|
+
|
|
39
|
+
## 工作准则
|
|
40
|
+
|
|
41
|
+
- 优先断言对外可观察行为,而不是内部实现细节。
|
|
42
|
+
- 新增测试尽量贴近现有测试文件;只有在必要时才创建新文件。
|
|
43
|
+
- 如果仓库没有测试基础设施,先说明现状,再决定是补最小配置还是只给出建议。
|
|
44
|
+
- 不要把覆盖率数字当成目标;以行为信心和回归保护为准。
|
|
45
|
+
|
|
46
|
+
## 汇报方式
|
|
47
|
+
|
|
48
|
+
- 使用 `## RED`、`## GREEN`、`## REFACTOR` 三段汇报。
|
|
49
|
+
- 说明修改了哪些测试文件、哪些实现文件,以及运行了哪些测试命令。
|
|
50
|
+
- 最后补充剩余风险、未覆盖场景或后续建议。
|
|
51
|
+
|
|
52
|
+
## 使用示例
|
|
53
|
+
|
|
54
|
+
- `/tdd src/utils/format.ts` - 为 format.ts 编写测试
|
|
55
|
+
- `/tdd add user validation` - 实现用户验证功能 (TDD 方式)
|
|
56
|
+
- `/tdd` - 为当前 git 变更编写测试
|