@rune-kit/rune 2.3.3 → 2.4.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.
- package/README.md +51 -1
- package/compiler/__tests__/scripts-bundling.test.js +284 -0
- package/compiler/__tests__/tier-override.test.js +41 -0
- package/compiler/adapters/antigravity.js +4 -0
- package/compiler/adapters/codex.js +4 -0
- package/compiler/adapters/cursor.js +4 -0
- package/compiler/adapters/generic.js +4 -0
- package/compiler/adapters/openclaw.js +4 -0
- package/compiler/adapters/opencode.js +4 -0
- package/compiler/adapters/windsurf.js +4 -0
- package/compiler/emitter.js +85 -5
- package/compiler/transforms/scripts-path.js +18 -0
- package/extensions/zalo/PACK.md +20 -1
- package/extensions/zalo/references/conversation-management.md +214 -0
- package/extensions/zalo/references/eval-scenarios.md +157 -0
- package/extensions/zalo/references/listen-mode.md +237 -0
- package/extensions/zalo/references/mcp-production.md +274 -0
- package/extensions/zalo/references/multi-account-proxy.md +224 -0
- package/extensions/zalo/references/vietqr-banking.md +160 -0
- package/package.json +2 -3
- package/skills/marketing/SKILL.md +3 -0
- package/skills/sentinel/SKILL.md +4 -1
- package/skills/sentinel/references/auth-crypto-reference.md +192 -0
- package/skills/sentinel/references/desktop-security.md +201 -0
- package/skills/sentinel/references/supply-chain.md +160 -0
- package/skills/slides/SKILL.md +142 -0
- package/skills/slides/scripts/build-deck.js +158 -0
- package/docs/ANTIGRAVITY-GAP-ANALYSIS.md +0 -369
- package/docs/ARCHITECTURE.md +0 -332
- package/docs/COMMUNITY-PACKS.md +0 -109
- package/docs/CONTRIBUTING-L4.md +0 -215
- package/docs/CROSS-IDE-ANALYSIS.md +0 -164
- package/docs/EXTENSION-TEMPLATE.md +0 -126
- package/docs/MESH-RULES.md +0 -34
- package/docs/MULTI-PLATFORM.md +0 -804
- package/docs/SKILL-DEPTH-AUDIT.md +0 -191
- package/docs/SKILL-TEMPLATE.md +0 -118
- package/docs/TRADE-MATRIX.md +0 -327
- package/docs/VERSIONING.md +0 -91
- package/docs/VISION.md +0 -263
- package/docs/assets/demo-subtitles.srt +0 -215
- package/docs/assets/end-card.html +0 -276
- package/docs/assets/mesh-diagram.html +0 -654
- package/docs/assets/thumbnail.html +0 -295
- package/docs/guides/cli.md +0 -403
- package/docs/guides/index.html +0 -1450
- package/docs/index.html +0 -1005
- package/docs/references/claudekit-analysis.md +0 -414
- package/docs/references/voltagent-analysis.md +0 -189
- package/docs/script.js +0 -495
- package/docs/skills/index.html +0 -832
- package/docs/style.css +0 -958
- package/docs/video-demo-plan.md +0 -172
package/README.md
CHANGED
|
@@ -32,7 +32,57 @@ Mesh: A ↔ B ↔ C (B fails = A reaches C via D→E)
|
|
|
32
32
|
D ↔ E ↔ F
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
##
|
|
35
|
+
## Benchmark: With Rune vs Without Rune
|
|
36
|
+
|
|
37
|
+
We ran 10 standardized coding tasks on Claude Code — once **without** Rune (vanilla), once **with** Rune — and measured tokens, cost, duration, and correctness.
|
|
38
|
+
|
|
39
|
+
### Headline Results
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
Without Rune With Rune Delta
|
|
43
|
+
Avg Tokens: 541,400 454,491 ↓ 16%
|
|
44
|
+
Avg Cost: $0.69 $0.65 ↓ 6%
|
|
45
|
+
Avg Duration: 2.3 min 2.1 min ↓ 9%
|
|
46
|
+
Avg Tool Calls: 14 13 ↓ 7%
|
|
47
|
+
Correctness: 9/10 9/10 =
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Where Rune Shines: Complex Tasks
|
|
51
|
+
|
|
52
|
+
| Task | Difficulty | Tokens | Cost | Duration | Tools |
|
|
53
|
+
|------|-----------|--------|------|----------|-------|
|
|
54
|
+
| Refactor 450-line component | Medium | **-62%** | **-17%** | **-32%** | **-27%** |
|
|
55
|
+
| Full feature (auth + API + tests) | Complex | **-36%** | **-29%** | **-31%** | **-27%** |
|
|
56
|
+
| Add Zod validation | Easy | -9% | **-28%** | **-32%** | 0% |
|
|
57
|
+
| Dark mode across 6 components | Hard | ~0% | +10% | -7% | -6% |
|
|
58
|
+
|
|
59
|
+
Rune doesn't make Claude smarter — Claude already knows how to code. Rune makes Claude **disciplined**. The more complex the task, the more discipline matters.
|
|
60
|
+
|
|
61
|
+
> _"Without Rune, Claude writes code that works. With Rune, Claude writes code that lasts."_
|
|
62
|
+
|
|
63
|
+
<details>
|
|
64
|
+
<summary>Full 10-task breakdown</summary>
|
|
65
|
+
|
|
66
|
+
| # | Task | Diff | Tokens | Cost | Time | Correct |
|
|
67
|
+
|---|------|------|--------|------|------|---------|
|
|
68
|
+
| 1 | Zod Validation | Easy | -9% | -28% | -32% | ✅ → ✅ |
|
|
69
|
+
| 2 | Fix N+1 Query | Easy | +12% | +25% | +3% | ❌ → ❌ |
|
|
70
|
+
| 3 | Cursor Pagination | Med | +12% | +19% | -9% | ✅ → ✅ |
|
|
71
|
+
| 4 | Security Review | Med | +13% | +32% | +3% | ✅ → ✅ |
|
|
72
|
+
| 5 | Rate Limiting | Med | +12% | +5% | +5% | ✅ → ✅ |
|
|
73
|
+
| 6 | Refactor Component | Med | **-62%** | **-17%** | **-32%** | ✅ → ✅ |
|
|
74
|
+
| 7 | Dark Mode (6 files) | Hard | ~0% | +10% | -7% | ✅ → ✅ |
|
|
75
|
+
| 8 | DB Migration | Hard | +52% | +11% | +49% | ✅ → ✅ |
|
|
76
|
+
| 9 | Memory Leak Debug | Hard | +13% | +28% | -2% | ✅ → ✅ |
|
|
77
|
+
| 10 | Full Auth System | Complex | **-36%** | **-29%** | **-31%** | ✅ → ✅ |
|
|
78
|
+
|
|
79
|
+
_Methodology: Claude Code CLI headless mode (`claude -p --output-format json`), 10 tasks with fixture code, pattern-based correctness evaluation. Source: [`Benchmark/`](Benchmark/)_
|
|
80
|
+
|
|
81
|
+
</details>
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## What's New (v2.4.0)
|
|
36
86
|
|
|
37
87
|
- **UI/UX Pro Max Integration** — 161 palettes, 84 styles, 73 font pairings, 161 reasoning rules from [UI/UX Pro Max](https://github.com/nextlevelbuilder/ui-ux-pro-max-skill) (MIT, 42.8k★) meshed into `design` skill + `@rune/ui` pack
|
|
38
88
|
- **11 Core Skill Enrichments** — deviation rules, repair operators, evidence quality gate, decision classification, debug knowledge base, CSO discipline, YAGNI pushback, formal pause/resume
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scripts Bundling Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the scripts/ directory copy pipeline:
|
|
5
|
+
* - copyScriptsDir helper
|
|
6
|
+
* - scriptsDir adapter method
|
|
7
|
+
* - {scripts_dir} placeholder resolution
|
|
8
|
+
* - End-to-end buildAll with scripts
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import assert from 'node:assert';
|
|
12
|
+
import { existsSync } from 'node:fs';
|
|
13
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { describe, test } from 'node:test';
|
|
17
|
+
import { getAdapter } from '../adapters/index.js';
|
|
18
|
+
import { buildAll } from '../emitter.js';
|
|
19
|
+
import { resolveScriptsPath } from '../transforms/scripts-path.js';
|
|
20
|
+
|
|
21
|
+
// --- resolveScriptsPath transform ---
|
|
22
|
+
|
|
23
|
+
describe('resolveScriptsPath', () => {
|
|
24
|
+
test('replaces {scripts_dir} with resolved path', () => {
|
|
25
|
+
const body = 'Run: `node {scripts_dir}/build-deck.js`';
|
|
26
|
+
const result = resolveScriptsPath(body, '.cursor/rules/rune-slides-scripts');
|
|
27
|
+
assert.strictEqual(result, 'Run: `node .cursor/rules/rune-slides-scripts/build-deck.js`');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('replaces multiple occurrences', () => {
|
|
31
|
+
const body = '{scripts_dir}/a.js and {scripts_dir}/b.js';
|
|
32
|
+
const result = resolveScriptsPath(body, 'scripts');
|
|
33
|
+
assert.strictEqual(result, 'scripts/a.js and scripts/b.js');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('returns body unchanged when no placeholder', () => {
|
|
37
|
+
const body = 'No placeholders here';
|
|
38
|
+
const result = resolveScriptsPath(body, 'some/path');
|
|
39
|
+
assert.strictEqual(result, body);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('returns body unchanged when scriptsPath is null', () => {
|
|
43
|
+
const body = 'Has {scripts_dir} but no path';
|
|
44
|
+
const result = resolveScriptsPath(body, null);
|
|
45
|
+
assert.strictEqual(result, body);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('returns body unchanged when body is empty', () => {
|
|
49
|
+
assert.strictEqual(resolveScriptsPath('', 'path'), '');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// --- scriptsDir adapter method ---
|
|
54
|
+
|
|
55
|
+
describe('adapter scriptsDir', () => {
|
|
56
|
+
test('flat-file adapters return sibling dir pattern', () => {
|
|
57
|
+
for (const name of ['cursor', 'windsurf', 'antigravity', 'generic', 'openclaw']) {
|
|
58
|
+
const adapter = getAdapter(name);
|
|
59
|
+
assert.ok(adapter.scriptsDir, `${name} should have scriptsDir`);
|
|
60
|
+
assert.strictEqual(adapter.scriptsDir('slides'), 'rune-slides-scripts', `${name} scriptsDir mismatch`);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('directory-per-skill adapters return nested dir pattern', () => {
|
|
65
|
+
for (const name of ['codex', 'opencode']) {
|
|
66
|
+
const adapter = getAdapter(name);
|
|
67
|
+
assert.ok(adapter.scriptsDir, `${name} should have scriptsDir`);
|
|
68
|
+
assert.strictEqual(adapter.scriptsDir('slides'), 'rune-slides/scripts', `${name} scriptsDir mismatch`);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('claude adapter has no scriptsDir (passthrough)', () => {
|
|
73
|
+
const adapter = getAdapter('claude');
|
|
74
|
+
assert.ok(!adapter.scriptsDir, 'claude should not have scriptsDir');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// --- Integration: buildAll with scripts ---
|
|
79
|
+
|
|
80
|
+
describe('buildAll with scripts', () => {
|
|
81
|
+
async function createTempSkillTree() {
|
|
82
|
+
const tmp = path.join(tmpdir(), `rune-scripts-test-${Date.now()}`);
|
|
83
|
+
const skillsDir = path.join(tmp, 'skills');
|
|
84
|
+
const slideSkillDir = path.join(skillsDir, 'test-slide');
|
|
85
|
+
const scriptsDir = path.join(slideSkillDir, 'scripts');
|
|
86
|
+
const plainSkillDir = path.join(skillsDir, 'test-plain');
|
|
87
|
+
|
|
88
|
+
await mkdir(scriptsDir, { recursive: true });
|
|
89
|
+
await mkdir(plainSkillDir, { recursive: true });
|
|
90
|
+
|
|
91
|
+
// Skill with scripts/
|
|
92
|
+
await writeFile(
|
|
93
|
+
path.join(slideSkillDir, 'SKILL.md'),
|
|
94
|
+
[
|
|
95
|
+
'---',
|
|
96
|
+
'name: test-slide',
|
|
97
|
+
'description: "Test slide skill"',
|
|
98
|
+
'layer: L3',
|
|
99
|
+
'group: utility',
|
|
100
|
+
'connections: []',
|
|
101
|
+
'tags: [test]',
|
|
102
|
+
'---',
|
|
103
|
+
'',
|
|
104
|
+
'# test-slide',
|
|
105
|
+
'',
|
|
106
|
+
'Run: `node {scripts_dir}/build-deck.js --input slides.json`',
|
|
107
|
+
'',
|
|
108
|
+
'Also: `{scripts_dir}/helper.py`',
|
|
109
|
+
].join('\n'),
|
|
110
|
+
'utf-8',
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
await writeFile(path.join(scriptsDir, 'build-deck.js'), '// build deck script\nconsole.log("hello");\n', 'utf-8');
|
|
114
|
+
await writeFile(path.join(scriptsDir, 'helper.py'), '# helper\nprint("hi")\n', 'utf-8');
|
|
115
|
+
|
|
116
|
+
// Skill without scripts/
|
|
117
|
+
await writeFile(
|
|
118
|
+
path.join(plainSkillDir, 'SKILL.md'),
|
|
119
|
+
[
|
|
120
|
+
'---',
|
|
121
|
+
'name: test-plain',
|
|
122
|
+
'description: "Test plain skill"',
|
|
123
|
+
'layer: L3',
|
|
124
|
+
'group: utility',
|
|
125
|
+
'connections: []',
|
|
126
|
+
'tags: [test]',
|
|
127
|
+
'---',
|
|
128
|
+
'',
|
|
129
|
+
'# test-plain',
|
|
130
|
+
'',
|
|
131
|
+
'No scripts here.',
|
|
132
|
+
].join('\n'),
|
|
133
|
+
'utf-8',
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// Minimal extensions dir (empty — no packs needed for test)
|
|
137
|
+
await mkdir(path.join(tmp, 'extensions'), { recursive: true });
|
|
138
|
+
|
|
139
|
+
// Minimal .claude-plugin for openclaw
|
|
140
|
+
await mkdir(path.join(tmp, '.claude-plugin'), { recursive: true });
|
|
141
|
+
await writeFile(
|
|
142
|
+
path.join(tmp, '.claude-plugin', 'plugin.json'),
|
|
143
|
+
JSON.stringify({ version: '0.0.1' }),
|
|
144
|
+
'utf-8',
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
return tmp;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
test('cursor: scripts copied to sibling dir, placeholder resolved', async () => {
|
|
151
|
+
const tmp = await createTempSkillTree();
|
|
152
|
+
try {
|
|
153
|
+
const outputRoot = path.join(tmp, 'out');
|
|
154
|
+
const adapter = getAdapter('cursor');
|
|
155
|
+
const stats = await buildAll({ runeRoot: tmp, outputRoot, adapter });
|
|
156
|
+
|
|
157
|
+
// Scripts copied
|
|
158
|
+
assert.ok(stats.scriptsCopied >= 2, `expected 2+ scripts copied, got ${stats.scriptsCopied}`);
|
|
159
|
+
|
|
160
|
+
// Scripts dir exists with files
|
|
161
|
+
const scriptsOut = path.join(outputRoot, adapter.outputDir, 'rune-test-slide-scripts');
|
|
162
|
+
assert.ok(existsSync(path.join(scriptsOut, 'build-deck.js')), 'build-deck.js missing in output');
|
|
163
|
+
assert.ok(existsSync(path.join(scriptsOut, 'helper.py')), 'helper.py missing in output');
|
|
164
|
+
|
|
165
|
+
// Placeholder resolved in .mdc output
|
|
166
|
+
const mdc = await readFile(path.join(outputRoot, adapter.outputDir, 'rune-test-slide.mdc'), 'utf-8');
|
|
167
|
+
assert.ok(!mdc.includes('{scripts_dir}'), 'placeholder should be resolved');
|
|
168
|
+
assert.ok(mdc.includes('.cursor/rules/rune-test-slide-scripts/build-deck.js'), 'resolved path missing');
|
|
169
|
+
|
|
170
|
+
// Plain skill: no scripts dir created
|
|
171
|
+
assert.ok(!existsSync(path.join(outputRoot, adapter.outputDir, 'rune-test-plain-scripts')), 'plain skill should not have scripts dir');
|
|
172
|
+
} finally {
|
|
173
|
+
await rm(tmp, { recursive: true, force: true });
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('codex: scripts inside skill directory', async () => {
|
|
178
|
+
const tmp = await createTempSkillTree();
|
|
179
|
+
try {
|
|
180
|
+
const outputRoot = path.join(tmp, 'out');
|
|
181
|
+
const adapter = getAdapter('codex');
|
|
182
|
+
const stats = await buildAll({ runeRoot: tmp, outputRoot, adapter });
|
|
183
|
+
|
|
184
|
+
assert.ok(stats.scriptsCopied >= 2);
|
|
185
|
+
|
|
186
|
+
// Scripts inside skill dir: .codex/skills/rune-test-slide/scripts/
|
|
187
|
+
const scriptsOut = path.join(outputRoot, adapter.outputDir, 'rune-test-slide', 'scripts');
|
|
188
|
+
assert.ok(existsSync(path.join(scriptsOut, 'build-deck.js')), 'build-deck.js missing');
|
|
189
|
+
assert.ok(existsSync(path.join(scriptsOut, 'helper.py')), 'helper.py missing');
|
|
190
|
+
|
|
191
|
+
// Placeholder resolved
|
|
192
|
+
const md = await readFile(
|
|
193
|
+
path.join(outputRoot, adapter.outputDir, 'rune-test-slide', 'SKILL.md'),
|
|
194
|
+
'utf-8',
|
|
195
|
+
);
|
|
196
|
+
assert.ok(!md.includes('{scripts_dir}'), 'placeholder should be resolved');
|
|
197
|
+
assert.ok(md.includes('.codex/skills/rune-test-slide/scripts/build-deck.js'), 'resolved path missing');
|
|
198
|
+
} finally {
|
|
199
|
+
await rm(tmp, { recursive: true, force: true });
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('skill without scripts: no error, no empty dirs', async () => {
|
|
204
|
+
const tmp = await createTempSkillTree();
|
|
205
|
+
try {
|
|
206
|
+
const outputRoot = path.join(tmp, 'out');
|
|
207
|
+
const adapter = getAdapter('windsurf');
|
|
208
|
+
const stats = await buildAll({ runeRoot: tmp, outputRoot, adapter });
|
|
209
|
+
|
|
210
|
+
// Should still build both skills
|
|
211
|
+
assert.strictEqual(stats.skillCount, 2);
|
|
212
|
+
|
|
213
|
+
// No scripts dir for plain skill
|
|
214
|
+
assert.ok(!existsSync(path.join(outputRoot, adapter.outputDir, 'rune-test-plain-scripts')));
|
|
215
|
+
} finally {
|
|
216
|
+
await rm(tmp, { recursive: true, force: true });
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('stats include scriptsCopied count', async () => {
|
|
221
|
+
const tmp = await createTempSkillTree();
|
|
222
|
+
try {
|
|
223
|
+
const outputRoot = path.join(tmp, 'out');
|
|
224
|
+
const adapter = getAdapter('generic');
|
|
225
|
+
const stats = await buildAll({ runeRoot: tmp, outputRoot, adapter });
|
|
226
|
+
|
|
227
|
+
assert.strictEqual(typeof stats.scriptsCopied, 'number');
|
|
228
|
+
assert.strictEqual(stats.scriptsCopied, 2);
|
|
229
|
+
} finally {
|
|
230
|
+
await rm(tmp, { recursive: true, force: true });
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('claude adapter: passthrough, no scripts processing', async () => {
|
|
235
|
+
const tmp = await createTempSkillTree();
|
|
236
|
+
try {
|
|
237
|
+
const outputRoot = path.join(tmp, 'out');
|
|
238
|
+
const adapter = getAdapter('claude');
|
|
239
|
+
const stats = await buildAll({ runeRoot: tmp, outputRoot, adapter });
|
|
240
|
+
|
|
241
|
+
// Claude is passthrough — no build
|
|
242
|
+
assert.strictEqual(stats.platform, 'claude');
|
|
243
|
+
assert.strictEqual(stats.skillCount, 0);
|
|
244
|
+
} finally {
|
|
245
|
+
await rm(tmp, { recursive: true, force: true });
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('scripts content copied as-is (byte-for-byte)', async () => {
|
|
250
|
+
const tmp = await createTempSkillTree();
|
|
251
|
+
try {
|
|
252
|
+
const outputRoot = path.join(tmp, 'out');
|
|
253
|
+
const adapter = getAdapter('cursor');
|
|
254
|
+
await buildAll({ runeRoot: tmp, outputRoot, adapter });
|
|
255
|
+
|
|
256
|
+
const original = await readFile(path.join(tmp, 'skills', 'test-slide', 'scripts', 'build-deck.js'), 'utf-8');
|
|
257
|
+
const copied = await readFile(path.join(outputRoot, adapter.outputDir, 'rune-test-slide-scripts', 'build-deck.js'), 'utf-8');
|
|
258
|
+
assert.strictEqual(copied, original, 'script content should be identical');
|
|
259
|
+
} finally {
|
|
260
|
+
await rm(tmp, { recursive: true, force: true });
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test('multiple adapters resolve different paths', async () => {
|
|
265
|
+
const tmp = await createTempSkillTree();
|
|
266
|
+
try {
|
|
267
|
+
// Cursor (flat)
|
|
268
|
+
const cursorOut = path.join(tmp, 'out-cursor');
|
|
269
|
+
const cursorAdapter = getAdapter('cursor');
|
|
270
|
+
await buildAll({ runeRoot: tmp, outputRoot: cursorOut, adapter: cursorAdapter });
|
|
271
|
+
const cursorMdc = await readFile(path.join(cursorOut, '.cursor/rules/rune-test-slide.mdc'), 'utf-8');
|
|
272
|
+
assert.ok(cursorMdc.includes('.cursor/rules/rune-test-slide-scripts/'), 'cursor path');
|
|
273
|
+
|
|
274
|
+
// Codex (directory)
|
|
275
|
+
const codexOut = path.join(tmp, 'out-codex');
|
|
276
|
+
const codexAdapter = getAdapter('codex');
|
|
277
|
+
await buildAll({ runeRoot: tmp, outputRoot: codexOut, adapter: codexAdapter });
|
|
278
|
+
const codexMd = await readFile(path.join(codexOut, '.codex/skills/rune-test-slide/SKILL.md'), 'utf-8');
|
|
279
|
+
assert.ok(codexMd.includes('.codex/skills/rune-test-slide/scripts/'), 'codex path');
|
|
280
|
+
} finally {
|
|
281
|
+
await rm(tmp, { recursive: true, force: true });
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
});
|
|
@@ -155,4 +155,45 @@ describe('discoverTieredPacks', () => {
|
|
|
155
155
|
rmSync(root, { recursive: true, force: true });
|
|
156
156
|
}
|
|
157
157
|
});
|
|
158
|
+
|
|
159
|
+
test('pro override tracks free pack in overrides array', async () => {
|
|
160
|
+
const { freePacks, proPacks, root } = createTierFixture();
|
|
161
|
+
try {
|
|
162
|
+
const packs = await discoverTieredPacks(freePacks, { pro: proPacks });
|
|
163
|
+
const saasPack = packs.find((p) => p.dirName === 'pro-saas');
|
|
164
|
+
assert.ok(saasPack, 'pro-saas should exist');
|
|
165
|
+
assert.strictEqual(saasPack.overrides.length, 1);
|
|
166
|
+
assert.strictEqual(saasPack.overrides[0].tier, 'free');
|
|
167
|
+
assert.strictEqual(saasPack.overrides[0].dirName, 'saas');
|
|
168
|
+
} finally {
|
|
169
|
+
rmSync(root, { recursive: true, force: true });
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('business override tracks both pro and free in overrides array', async () => {
|
|
174
|
+
const { freePacks, proPacks, bizPacks, root } = createTierFixture();
|
|
175
|
+
try {
|
|
176
|
+
const packs = await discoverTieredPacks(freePacks, { pro: proPacks, business: bizPacks });
|
|
177
|
+
const saasPack = packs.find((p) => p.dirName === 'business-saas');
|
|
178
|
+
assert.ok(saasPack, 'business-saas should exist');
|
|
179
|
+
// Should have both free and pro overrides (free was overridden by pro, then pro by business)
|
|
180
|
+
assert.strictEqual(saasPack.overrides.length, 2);
|
|
181
|
+
const overrideTiers = saasPack.overrides.map((o) => o.tier).sort();
|
|
182
|
+
assert.deepStrictEqual(overrideTiers, ['free', 'pro']);
|
|
183
|
+
} finally {
|
|
184
|
+
rmSync(root, { recursive: true, force: true });
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('free-only packs have empty overrides array', async () => {
|
|
189
|
+
const { freePacks, proPacks, root } = createTierFixture();
|
|
190
|
+
try {
|
|
191
|
+
const packs = await discoverTieredPacks(freePacks, { pro: proPacks });
|
|
192
|
+
const tradingPack = packs.find((p) => p.dirName === 'trading');
|
|
193
|
+
assert.ok(tradingPack, 'trading should exist');
|
|
194
|
+
assert.deepStrictEqual(tradingPack.overrides, []);
|
|
195
|
+
} finally {
|
|
196
|
+
rmSync(root, { recursive: true, force: true });
|
|
197
|
+
}
|
|
198
|
+
});
|
|
158
199
|
});
|
package/compiler/emitter.js
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { existsSync } from 'node:fs';
|
|
9
|
-
import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
9
|
+
import { cp, mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { extractCrossRefs, extractToolRefs, parsePack, parseSkill } from './parser.js';
|
|
12
12
|
import { transformSkill } from './transformer.js';
|
|
13
|
+
import { resolveScriptsPath } from './transforms/scripts-path.js';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Discover all SKILL.md files in the skills directory
|
|
@@ -59,6 +60,29 @@ async function discoverPacks(extensionsDir, enabledPacks = null) {
|
|
|
59
60
|
return paths.sort();
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Copy scripts directory from skill source to output.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} sourceScriptsDir - e.g. skills/slides/scripts/
|
|
67
|
+
* @param {string} outputScriptsDir - e.g. .cursor/rules/rune-slides-scripts/
|
|
68
|
+
* @returns {Promise<string[]>} list of copied file paths
|
|
69
|
+
*/
|
|
70
|
+
async function copyScriptsDir(sourceScriptsDir, outputScriptsDir) {
|
|
71
|
+
if (!existsSync(sourceScriptsDir)) return [];
|
|
72
|
+
|
|
73
|
+
const entries = await readdir(sourceScriptsDir, { recursive: true, withFileTypes: true });
|
|
74
|
+
const files = entries.filter((e) => e.isFile());
|
|
75
|
+
if (entries.length === 0) return [];
|
|
76
|
+
|
|
77
|
+
await cp(sourceScriptsDir, outputScriptsDir, { recursive: true });
|
|
78
|
+
|
|
79
|
+
// Return relative paths within the scripts dir (same structure as source after recursive cp)
|
|
80
|
+
return files.map((e) => {
|
|
81
|
+
const parent = e.parentPath || e.path;
|
|
82
|
+
return path.relative(sourceScriptsDir, path.join(parent, e.name));
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
62
86
|
/**
|
|
63
87
|
* Tier priority: higher number = higher priority (wins override)
|
|
64
88
|
*/
|
|
@@ -104,9 +128,12 @@ export async function discoverTieredPacks(freeExtDir, tierSources = {}, enabledP
|
|
|
104
128
|
const priority = TIER_PRIORITY[tier] ?? 0;
|
|
105
129
|
const existing = packMap.get(normalized);
|
|
106
130
|
|
|
107
|
-
// Higher priority tier wins
|
|
131
|
+
// Higher priority tier wins — track overridden lower-tier entries for skill-level merging
|
|
108
132
|
if (!existing || priority > existing.priority) {
|
|
109
|
-
|
|
133
|
+
const overrides = existing
|
|
134
|
+
? [...(existing.overrides || []), { path: existing.path, tier: existing.tier, dirName: existing.dirName }]
|
|
135
|
+
: [];
|
|
136
|
+
packMap.set(normalized, { path: packFile, tier, priority, dirName: entry.name, overrides });
|
|
110
137
|
}
|
|
111
138
|
}
|
|
112
139
|
}
|
|
@@ -187,6 +214,7 @@ export async function buildAll({
|
|
|
187
214
|
packCount: 0,
|
|
188
215
|
crossRefsResolved: 0,
|
|
189
216
|
toolRefsResolved: 0,
|
|
217
|
+
scriptsCopied: 0,
|
|
190
218
|
files: [],
|
|
191
219
|
skipped: [],
|
|
192
220
|
errors: [],
|
|
@@ -205,7 +233,25 @@ export async function buildAll({
|
|
|
205
233
|
continue;
|
|
206
234
|
}
|
|
207
235
|
|
|
208
|
-
const { header, body, footer } = transformSkill(parsed, adapter);
|
|
236
|
+
const { header, body: rawBody, footer } = transformSkill(parsed, adapter);
|
|
237
|
+
|
|
238
|
+
// Resolve {scripts_dir} placeholder if adapter supports scripts
|
|
239
|
+
const skillSourceDir = path.dirname(skillPath);
|
|
240
|
+
const scriptsSource = path.join(skillSourceDir, 'scripts');
|
|
241
|
+
const hasScripts = existsSync(scriptsSource) && adapter.scriptsDir;
|
|
242
|
+
const scriptsRelPath = hasScripts
|
|
243
|
+
? path.join(adapter.outputDir, adapter.scriptsDir(parsed.name)).replaceAll('\\', '/')
|
|
244
|
+
: null;
|
|
245
|
+
const body = hasScripts ? resolveScriptsPath(rawBody, scriptsRelPath) : rawBody;
|
|
246
|
+
|
|
247
|
+
// Warn if {scripts_dir} placeholder exists but no scripts/ folder to resolve it
|
|
248
|
+
if (!hasScripts && rawBody.includes('{scripts_dir}')) {
|
|
249
|
+
stats.errors.push({
|
|
250
|
+
file: skillPath,
|
|
251
|
+
error: `{scripts_dir} placeholder found but no scripts/ directory exists for skill "${parsed.name}"`,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
209
255
|
const output = [header, body, footer].filter(Boolean).join('\n');
|
|
210
256
|
|
|
211
257
|
let outputPath;
|
|
@@ -226,6 +272,13 @@ export async function buildAll({
|
|
|
226
272
|
|
|
227
273
|
await writeFile(outputPath, output, 'utf-8');
|
|
228
274
|
|
|
275
|
+
// Copy scripts/ directory if present
|
|
276
|
+
if (hasScripts) {
|
|
277
|
+
const scriptsOutput = path.join(outputDir, adapter.scriptsDir(parsed.name));
|
|
278
|
+
const copied = await copyScriptsDir(scriptsSource, scriptsOutput);
|
|
279
|
+
stats.scriptsCopied += copied.length;
|
|
280
|
+
}
|
|
281
|
+
|
|
229
282
|
stats.skillCount++;
|
|
230
283
|
stats.crossRefsResolved += parsed.crossRefs.length;
|
|
231
284
|
stats.toolRefsResolved += parsed.toolRefs.length;
|
|
@@ -249,11 +302,38 @@ export async function buildAll({
|
|
|
249
302
|
stats.tierOverrides.push({ pack: packName, tier: packEntry.tier });
|
|
250
303
|
}
|
|
251
304
|
|
|
305
|
+
// Tier Override: merge skill manifests from lower tiers
|
|
306
|
+
// If a Pro/Business pack overrides a Free pack, inherit skills the higher tier doesn't provide
|
|
307
|
+
if (packEntry.overrides?.length > 0 && parsed.isSplit && parsed.skillManifest.length > 0) {
|
|
308
|
+
const winnerSkillNames = new Set(parsed.skillManifest.map((s) => s.name));
|
|
309
|
+
for (const lower of packEntry.overrides) {
|
|
310
|
+
try {
|
|
311
|
+
const lowerContent = await readFile(lower.path, 'utf-8');
|
|
312
|
+
const lowerParsed = parsePack(lowerContent, lower.path);
|
|
313
|
+
if (lowerParsed.isSplit) {
|
|
314
|
+
const lowerPackDir = path.dirname(lower.path);
|
|
315
|
+
for (const lowerSkill of lowerParsed.skillManifest) {
|
|
316
|
+
if (!winnerSkillNames.has(lowerSkill.name)) {
|
|
317
|
+
// Inherit skill from lower tier — track source directory for file resolution
|
|
318
|
+
parsed.skillManifest.push({ ...lowerSkill, _sourceDir: lowerPackDir });
|
|
319
|
+
winnerSkillNames.add(lowerSkill.name);
|
|
320
|
+
stats.tierOverrides.push({ pack: packName, skill: lowerSkill.name, inherited: lower.tier });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
} catch {
|
|
325
|
+
// Lower-tier pack unreadable — skip gracefully
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
252
330
|
// For split packs, load individual skill files and concatenate into body
|
|
253
331
|
if (parsed.isSplit && parsed.skillManifest.length > 0) {
|
|
254
332
|
const skillBodies = [];
|
|
255
333
|
for (const skill of parsed.skillManifest) {
|
|
256
|
-
|
|
334
|
+
// Resolve skill file path — use _sourceDir for inherited lower-tier skills
|
|
335
|
+
const sourceDir = skill._sourceDir || packDir;
|
|
336
|
+
const skillPath = path.join(sourceDir, skill.file);
|
|
257
337
|
if (existsSync(skillPath)) {
|
|
258
338
|
const skillContent = await readFile(skillPath, 'utf-8');
|
|
259
339
|
// Strip frontmatter from skill file — we only need the body
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scripts Path Transform
|
|
3
|
+
*
|
|
4
|
+
* Replaces {scripts_dir} placeholder in skill body with the
|
|
5
|
+
* platform-resolved scripts directory path.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Replace {scripts_dir} placeholder with platform-resolved path.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} body - skill body text
|
|
12
|
+
* @param {string} scriptsPath - resolved path for this platform
|
|
13
|
+
* @returns {string} body with placeholders resolved
|
|
14
|
+
*/
|
|
15
|
+
export function resolveScriptsPath(body, scriptsPath) {
|
|
16
|
+
if (!body || !scriptsPath) return body;
|
|
17
|
+
return body.replaceAll('{scripts_dir}', scriptsPath);
|
|
18
|
+
}
|
package/extensions/zalo/PACK.md
CHANGED
|
@@ -3,7 +3,7 @@ name: "@rune/zalo"
|
|
|
3
3
|
description: Zalo platform integration — Official Account API (OAuth2, messaging, webhooks, MCP server) and personal account automation (zca-js). Dual-track with explicit risk gating.
|
|
4
4
|
metadata:
|
|
5
5
|
author: runedev
|
|
6
|
-
version: "0.
|
|
6
|
+
version: "0.2.0"
|
|
7
7
|
layer: L4
|
|
8
8
|
price: free
|
|
9
9
|
target: Vietnamese developers building Zalo bots, OA automation, and AI agent integrations
|
|
@@ -116,6 +116,25 @@ Called By ← mcp-builder (L2): when building Zalo-specific MCP server
|
|
|
116
116
|
| Webhook signature verification skipped in development, then deployed to production unsigned | HIGH | Always validate `X-Zalo-Signature` header from first commit — skip in dev only via explicit `SKIP_WEBHOOK_VERIFY=true` env flag |
|
|
117
117
|
| Rate limit hit causes account ban with no warning (HTTP 429 mishandled as transient error) | HIGH | Implement token bucket per endpoint; treat sustained 429s as ban-risk signal and back off for 60+ seconds |
|
|
118
118
|
|
|
119
|
+
## References
|
|
120
|
+
|
|
121
|
+
| Reference | Trigger |
|
|
122
|
+
|-----------|---------|
|
|
123
|
+
| [VietQR & Banking](references/vietqr-banking.md) | Payment, bank transfer, QR code patterns detected |
|
|
124
|
+
| [Conversation Management](references/conversation-management.md) | Polls, auto-reply, mute/archive, advanced messaging |
|
|
125
|
+
| [MCP Production](references/mcp-production.md) | MCP server deployment, cursor pagination, pm2 setup |
|
|
126
|
+
| [Multi-Account & Proxy](references/multi-account-proxy.md) | Multi-account setup, proxy configuration, VPS deployment |
|
|
127
|
+
| [Listen Mode](references/listen-mode.md) | WebSocket listener, real-time events, webhook forwarding |
|
|
128
|
+
| [Eval Scenarios](references/eval-scenarios.md) | Quality gate — 24 test scenarios (functional + security) |
|
|
129
|
+
|
|
130
|
+
## Credits
|
|
131
|
+
|
|
132
|
+
This pack was originally inspired by and incorporates patterns from:
|
|
133
|
+
|
|
134
|
+
- **[zalo-agent-cli](https://github.com/PhucMPham/zalo-agent-cli)** by PhucMPham (MIT) — CLI tool for Zalo automation, 90+ commands, MCP server, VietQR banking integration
|
|
135
|
+
- **[openzalo](https://github.com/darkamenosa/openzalo)** by darkamenosa — OpenClaw channel plugin for Zalo personal accounts via openzca CLI
|
|
136
|
+
- **[zca-js](https://github.com/RFS-ADRENO/zca-js)** — Unofficial Zalo client library (reverse-engineered API)
|
|
137
|
+
|
|
119
138
|
## Done When
|
|
120
139
|
|
|
121
140
|
- OA OAuth2 flow working with auto-refresh
|