@rune-kit/rune 2.3.3 → 2.6.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.
Files changed (77) hide show
  1. package/README.md +86 -17
  2. package/compiler/__tests__/pack-split.test.js +141 -1
  3. package/compiler/__tests__/parser.test.js +147 -55
  4. package/compiler/__tests__/scripts-bundling.test.js +283 -0
  5. package/compiler/__tests__/skill-index.test.js +218 -0
  6. package/compiler/__tests__/tier-override.test.js +41 -0
  7. package/compiler/adapters/antigravity.js +71 -53
  8. package/compiler/adapters/codex.js +4 -0
  9. package/compiler/adapters/cursor.js +4 -0
  10. package/compiler/adapters/generic.js +4 -0
  11. package/compiler/adapters/openclaw.js +4 -0
  12. package/compiler/adapters/opencode.js +4 -0
  13. package/compiler/adapters/windsurf.js +4 -0
  14. package/compiler/bin/rune.js +355 -355
  15. package/compiler/doctor.js +11 -1
  16. package/compiler/emitter.js +678 -386
  17. package/compiler/parser.js +267 -247
  18. package/compiler/transforms/scripts-path.js +18 -0
  19. package/extensions/zalo/PACK.md +20 -1
  20. package/extensions/zalo/references/conversation-management.md +214 -0
  21. package/extensions/zalo/references/eval-scenarios.md +157 -0
  22. package/extensions/zalo/references/listen-mode.md +237 -0
  23. package/extensions/zalo/references/mcp-production.md +274 -0
  24. package/extensions/zalo/references/multi-account-proxy.md +224 -0
  25. package/extensions/zalo/references/vietqr-banking.md +160 -0
  26. package/hooks/hooks.json +12 -0
  27. package/hooks/intent-router/index.cjs +108 -0
  28. package/hooks/pre-tool-guard/index.cjs +177 -68
  29. package/package.json +63 -64
  30. package/skills/brainstorm/SKILL.md +2 -0
  31. package/skills/cook/SKILL.md +661 -648
  32. package/skills/debug/SKILL.md +394 -392
  33. package/skills/deploy/SKILL.md +2 -0
  34. package/skills/fix/SKILL.md +283 -281
  35. package/skills/marketing/SKILL.md +3 -0
  36. package/skills/onboard/SKILL.md +7 -0
  37. package/skills/plan/SKILL.md +344 -342
  38. package/skills/preflight/SKILL.md +362 -360
  39. package/skills/review/SKILL.md +491 -489
  40. package/skills/scout/SKILL.md +1 -0
  41. package/skills/sentinel/SKILL.md +319 -296
  42. package/skills/sentinel/references/auth-crypto-reference.md +192 -0
  43. package/skills/sentinel/references/desktop-security.md +201 -0
  44. package/skills/sentinel/references/supply-chain.md +160 -0
  45. package/skills/session-bridge/SKILL.md +1 -0
  46. package/skills/slides/SKILL.md +142 -0
  47. package/skills/slides/scripts/build-deck.js +158 -0
  48. package/skills/team/SKILL.md +1 -0
  49. package/skills/test/SKILL.md +587 -585
  50. package/skills/verification/SKILL.md +1 -0
  51. package/skills/watchdog/SKILL.md +2 -0
  52. package/docs/ANTIGRAVITY-GAP-ANALYSIS.md +0 -369
  53. package/docs/ARCHITECTURE.md +0 -332
  54. package/docs/COMMUNITY-PACKS.md +0 -109
  55. package/docs/CONTRIBUTING-L4.md +0 -215
  56. package/docs/CROSS-IDE-ANALYSIS.md +0 -164
  57. package/docs/EXTENSION-TEMPLATE.md +0 -126
  58. package/docs/MESH-RULES.md +0 -34
  59. package/docs/MULTI-PLATFORM.md +0 -804
  60. package/docs/SKILL-DEPTH-AUDIT.md +0 -191
  61. package/docs/SKILL-TEMPLATE.md +0 -118
  62. package/docs/TRADE-MATRIX.md +0 -327
  63. package/docs/VERSIONING.md +0 -91
  64. package/docs/VISION.md +0 -263
  65. package/docs/assets/demo-subtitles.srt +0 -215
  66. package/docs/assets/end-card.html +0 -276
  67. package/docs/assets/mesh-diagram.html +0 -654
  68. package/docs/assets/thumbnail.html +0 -295
  69. package/docs/guides/cli.md +0 -403
  70. package/docs/guides/index.html +0 -1450
  71. package/docs/index.html +0 -1005
  72. package/docs/references/claudekit-analysis.md +0 -414
  73. package/docs/references/voltagent-analysis.md +0 -189
  74. package/docs/script.js +0 -495
  75. package/docs/skills/index.html +0 -832
  76. package/docs/style.css +0 -958
  77. package/docs/video-demo-plan.md +0 -172
@@ -1,247 +1,267 @@
1
- /**
2
- * SKILL.md Parser
3
- *
4
- * Parses SKILL.md files into a structured intermediate representation (IR).
5
- * Extracts frontmatter, cross-references, tool references, HARD-GATE blocks, and sections.
6
- */
7
-
8
- const CROSS_REF_PATTERN = /`?rune:([a-z][\w-]*)`?/g;
9
- const TOOL_REF_PATTERN = /`(Read|Write|Edit|Glob|Grep|Bash|TodoWrite|Skill|Agent)`/g;
10
- const HARD_GATE_PATTERN = /<HARD-GATE>([\s\S]*?)<\/HARD-GATE>/g;
11
- const _SECTION_PATTERN = /^## (.+)$/gm;
12
-
13
- /**
14
- * Parse YAML-like frontmatter from SKILL.md
15
- * Handles nested metadata block
16
- */
17
- function parseFrontmatter(content) {
18
- // Normalize line endings to \n for cross-platform compatibility
19
- const normalized = content.replace(/\r\n/g, '\n');
20
- const match = normalized.match(/^---\n([\s\S]*?)\n---/);
21
- if (!match) return { frontmatter: {}, body: normalized };
22
-
23
- const raw = match[1];
24
- const body = normalized.slice(match[0].length).trim();
25
- const frontmatter = {};
26
-
27
- let currentIndent = null;
28
- let nestedKey = null;
29
- const _nestedObj = {};
30
-
31
- for (const line of raw.split('\n')) {
32
- const trimmed = line.trim();
33
- if (!trimmed) continue;
34
-
35
- // Detect nested block (e.g., metadata:)
36
- if (/^\w+:\s*$/.test(trimmed)) {
37
- nestedKey = trimmed.replace(':', '').trim();
38
- frontmatter[nestedKey] = {};
39
- currentIndent = 'nested';
40
- continue;
41
- }
42
-
43
- if (currentIndent === 'nested' && line.startsWith(' ')) {
44
- const kvMatch = trimmed.match(/^(\w+):\s*(.+)$/);
45
- if (kvMatch) {
46
- const value = kvMatch[2].replace(/^["']|["']$/g, '');
47
- frontmatter[nestedKey][kvMatch[1]] = value;
48
- }
49
- continue;
50
- }
51
-
52
- // Top-level key-value
53
- currentIndent = null;
54
- nestedKey = null;
55
- const kvMatch = trimmed.match(/^(\w[\w-]*):\s*(.+)$/);
56
- if (kvMatch) {
57
- const value = kvMatch[2].replace(/^["']|["']$/g, '');
58
- frontmatter[kvMatch[1]] = value;
59
- }
60
- }
61
-
62
- return { frontmatter, body };
63
- }
64
-
65
- /**
66
- * Extract all rune:<name> cross-references from body
67
- */
68
- export function extractCrossRefs(body) {
69
- const refs = [];
70
- const lines = body.split('\n');
71
-
72
- for (let i = 0; i < lines.length; i++) {
73
- const line = lines[i];
74
- let match;
75
- const regex = new RegExp(CROSS_REF_PATTERN.source, 'g');
76
-
77
- while ((match = regex.exec(line)) !== null) {
78
- refs.push({
79
- raw: match[0],
80
- skillName: match[1],
81
- line: i + 1,
82
- context: line.trim(),
83
- });
84
- }
85
- }
86
-
87
- return refs;
88
- }
89
-
90
- /**
91
- * Extract all Claude Code tool references from body
92
- */
93
- export function extractToolRefs(body) {
94
- const refs = [];
95
- const lines = body.split('\n');
96
-
97
- for (let i = 0; i < lines.length; i++) {
98
- const line = lines[i];
99
- let match;
100
- const regex = new RegExp(TOOL_REF_PATTERN.source, 'g');
101
-
102
- while ((match = regex.exec(line)) !== null) {
103
- refs.push({
104
- raw: match[0],
105
- toolName: match[1],
106
- line: i + 1,
107
- context: line.trim(),
108
- });
109
- }
110
- }
111
-
112
- return refs;
113
- }
114
-
115
- /**
116
- * Extract HARD-GATE blocks
117
- */
118
- function extractHardGates(body) {
119
- const gates = [];
120
- let match;
121
- const regex = new RegExp(HARD_GATE_PATTERN.source, 'gs');
122
-
123
- while ((match = regex.exec(body)) !== null) {
124
- gates.push(match[1].trim());
125
- }
126
-
127
- return gates;
128
- }
129
-
130
- /**
131
- * Extract ## sections as a Map<sectionName, content>
132
- */
133
- function extractSections(body) {
134
- const sections = new Map();
135
- const lines = body.split('\n');
136
- let currentSection = null;
137
- let currentContent = [];
138
-
139
- for (const line of lines) {
140
- const sectionMatch = line.match(/^## (.+)$/);
141
- if (sectionMatch) {
142
- if (currentSection) {
143
- sections.set(currentSection, currentContent.join('\n').trim());
144
- }
145
- currentSection = sectionMatch[1];
146
- currentContent = [];
147
- } else if (currentSection) {
148
- currentContent.push(line);
149
- }
150
- }
151
-
152
- if (currentSection) {
153
- sections.set(currentSection, currentContent.join('\n').trim());
154
- }
155
-
156
- return sections;
157
- }
158
-
159
- /**
160
- * Parse a SKILL.md file content into structured IR
161
- *
162
- * @param {string} content - raw SKILL.md content
163
- * @param {string} [filePath] - optional file path for error reporting
164
- * @returns {ParsedSkill}
165
- */
166
- export function parseSkill(content, filePath = '') {
167
- const { frontmatter, body } = parseFrontmatter(content);
168
-
169
- const metadata = frontmatter.metadata || {};
170
-
171
- return {
172
- name: frontmatter.name || '',
173
- description: frontmatter.description || '',
174
- layer: metadata.layer || 'L2',
175
- model: metadata.model || 'sonnet',
176
- group: metadata.group || 'general',
177
- contextFork: frontmatter.context === 'fork',
178
- agentType: frontmatter.agent || null,
179
- body,
180
- crossRefs: extractCrossRefs(body),
181
- toolRefs: extractToolRefs(body),
182
- hardGates: extractHardGates(body),
183
- sections: extractSections(body),
184
- filePath,
185
- frontmatter,
186
- };
187
- }
188
-
189
- /**
190
- * Parse a PACK.md extension file (same format, slightly different metadata)
191
- * Supports both monolith format (all skills inline) and split format (skills in separate files)
192
- *
193
- * Split format detection: metadata.format === "split" OR metadata.skills array present
194
- */
195
- export function parsePack(content, filePath = '') {
196
- const { frontmatter, body } = parseFrontmatter(content);
197
- const metadata = frontmatter.metadata || {};
198
-
199
- // Detect split format
200
- const isSplit = metadata.format === 'split' || Array.isArray(metadata.skills);
201
-
202
- return {
203
- name: frontmatter.name || '',
204
- description: frontmatter.description || '',
205
- version: metadata.version || frontmatter.version || '1.0.0',
206
- layer: 'L4',
207
- group: 'extension',
208
- isSplit,
209
- skillManifest: isSplit ? parseSkillManifest(metadata.skills || []) : [],
210
- body,
211
- crossRefs: extractCrossRefs(body),
212
- toolRefs: extractToolRefs(body),
213
- hardGates: extractHardGates(body),
214
- sections: extractSections(body),
215
- filePath,
216
- frontmatter,
217
- };
218
- }
219
-
220
- /**
221
- * Parse the skills manifest from split PACK.md frontmatter
222
- *
223
- * @param {Array} skills - array of skill entries from frontmatter metadata.skills
224
- * @returns {Array<{name: string, file: string, model: string, description: string}>}
225
- */
226
- function parseSkillManifest(skills) {
227
- if (!Array.isArray(skills)) return [];
228
-
229
- return skills.map((skill) => {
230
- // Handle both string format ("skill-name") and object format ({name, file, model, description})
231
- if (typeof skill === 'string') {
232
- return {
233
- name: skill,
234
- file: `skills/${skill}.md`,
235
- model: 'sonnet',
236
- description: '',
237
- };
238
- }
239
-
240
- return {
241
- name: skill.name || '',
242
- file: skill.file || `skills/${skill.name}.md`,
243
- model: skill.model || 'sonnet',
244
- description: skill.description || '',
245
- };
246
- });
247
- }
1
+ /**
2
+ * SKILL.md Parser
3
+ *
4
+ * Parses SKILL.md files into a structured intermediate representation (IR).
5
+ * Extracts frontmatter, cross-references, tool references, HARD-GATE blocks, and sections.
6
+ */
7
+
8
+ /**
9
+ * Metadata fields that should be parsed as comma-separated arrays
10
+ * e.g. emit: code.changed, tests.passed → ["code.changed", "tests.passed"]
11
+ */
12
+ const COMMA_LIST_FIELDS = new Set(['emit', 'listen']);
13
+
14
+ const CROSS_REF_PATTERN = /`?rune:([a-z][\w-]*)`?/g;
15
+ const TOOL_REF_PATTERN = /`(Read|Write|Edit|Glob|Grep|Bash|TodoWrite|Skill|Agent)`/g;
16
+ const HARD_GATE_PATTERN = /<HARD-GATE>([\s\S]*?)<\/HARD-GATE>/g;
17
+ const _SECTION_PATTERN = /^## (.+)$/gm;
18
+
19
+ /**
20
+ * Parse YAML-like frontmatter from SKILL.md
21
+ * Handles nested metadata block
22
+ */
23
+ function parseFrontmatter(content) {
24
+ // Normalize line endings to \n for cross-platform compatibility
25
+ const normalized = content.replace(/\r\n/g, '\n');
26
+ const match = normalized.match(/^---\n([\s\S]*?)\n---/);
27
+ if (!match) return { frontmatter: {}, body: normalized };
28
+
29
+ const raw = match[1];
30
+ const body = normalized.slice(match[0].length).trim();
31
+ const frontmatter = {};
32
+
33
+ let currentIndent = null;
34
+ let nestedKey = null;
35
+ const _nestedObj = {};
36
+
37
+ for (const line of raw.split('\n')) {
38
+ const trimmed = line.trim();
39
+ if (!trimmed) continue;
40
+
41
+ // Detect nested block (e.g., metadata:)
42
+ if (/^\w+:\s*$/.test(trimmed)) {
43
+ nestedKey = trimmed.replace(':', '').trim();
44
+ frontmatter[nestedKey] = {};
45
+ currentIndent = 'nested';
46
+ continue;
47
+ }
48
+
49
+ if (currentIndent === 'nested' && line.startsWith(' ')) {
50
+ const kvMatch = trimmed.match(/^(\w[\w-]*):\s*(.+)$/);
51
+ if (kvMatch) {
52
+ const rawValue = kvMatch[2].replace(/^["']|["']$/g, '');
53
+ // Comma-separated list fields → parse as array
54
+ if (COMMA_LIST_FIELDS.has(kvMatch[1])) {
55
+ frontmatter[nestedKey][kvMatch[1]] = rawValue
56
+ .split(',')
57
+ .map((s) => s.trim())
58
+ .filter(Boolean);
59
+ } else {
60
+ frontmatter[nestedKey][kvMatch[1]] = rawValue;
61
+ }
62
+ }
63
+ continue;
64
+ }
65
+
66
+ // Top-level key-value
67
+ currentIndent = null;
68
+ nestedKey = null;
69
+ const kvMatch = trimmed.match(/^(\w[\w-]*):\s*(.+)$/);
70
+ if (kvMatch) {
71
+ const value = kvMatch[2].replace(/^["']|["']$/g, '');
72
+ frontmatter[kvMatch[1]] = value;
73
+ }
74
+ }
75
+
76
+ return { frontmatter, body };
77
+ }
78
+
79
+ /**
80
+ * Extract all rune:<name> cross-references from body
81
+ */
82
+ export function extractCrossRefs(body) {
83
+ const refs = [];
84
+ const lines = body.split('\n');
85
+
86
+ for (let i = 0; i < lines.length; i++) {
87
+ const line = lines[i];
88
+ let match;
89
+ const regex = new RegExp(CROSS_REF_PATTERN.source, 'g');
90
+
91
+ while ((match = regex.exec(line)) !== null) {
92
+ refs.push({
93
+ raw: match[0],
94
+ skillName: match[1],
95
+ line: i + 1,
96
+ context: line.trim(),
97
+ });
98
+ }
99
+ }
100
+
101
+ return refs;
102
+ }
103
+
104
+ /**
105
+ * Extract all Claude Code tool references from body
106
+ */
107
+ export function extractToolRefs(body) {
108
+ const refs = [];
109
+ const lines = body.split('\n');
110
+
111
+ for (let i = 0; i < lines.length; i++) {
112
+ const line = lines[i];
113
+ let match;
114
+ const regex = new RegExp(TOOL_REF_PATTERN.source, 'g');
115
+
116
+ while ((match = regex.exec(line)) !== null) {
117
+ refs.push({
118
+ raw: match[0],
119
+ toolName: match[1],
120
+ line: i + 1,
121
+ context: line.trim(),
122
+ });
123
+ }
124
+ }
125
+
126
+ return refs;
127
+ }
128
+
129
+ /**
130
+ * Extract HARD-GATE blocks
131
+ */
132
+ function extractHardGates(body) {
133
+ const gates = [];
134
+ let match;
135
+ const regex = new RegExp(HARD_GATE_PATTERN.source, 'gs');
136
+
137
+ while ((match = regex.exec(body)) !== null) {
138
+ gates.push(match[1].trim());
139
+ }
140
+
141
+ return gates;
142
+ }
143
+
144
+ /**
145
+ * Extract ## sections as a Map<sectionName, content>
146
+ */
147
+ function extractSections(body) {
148
+ const sections = new Map();
149
+ const lines = body.split('\n');
150
+ let currentSection = null;
151
+ let currentContent = [];
152
+
153
+ for (const line of lines) {
154
+ const sectionMatch = line.match(/^## (.+)$/);
155
+ if (sectionMatch) {
156
+ if (currentSection) {
157
+ sections.set(currentSection, currentContent.join('\n').trim());
158
+ }
159
+ currentSection = sectionMatch[1];
160
+ currentContent = [];
161
+ } else if (currentSection) {
162
+ currentContent.push(line);
163
+ }
164
+ }
165
+
166
+ if (currentSection) {
167
+ sections.set(currentSection, currentContent.join('\n').trim());
168
+ }
169
+
170
+ return sections;
171
+ }
172
+
173
+ /**
174
+ * Parse a SKILL.md file content into structured IR
175
+ *
176
+ * @param {string} content - raw SKILL.md content
177
+ * @param {string} [filePath] - optional file path for error reporting
178
+ * @returns {ParsedSkill}
179
+ */
180
+ export function parseSkill(content, filePath = '') {
181
+ const { frontmatter, body } = parseFrontmatter(content);
182
+
183
+ const metadata = frontmatter.metadata || {};
184
+
185
+ // Extract signals — emit/listen arrays from metadata
186
+ const emit = Array.isArray(metadata.emit) ? metadata.emit : [];
187
+ const listen = Array.isArray(metadata.listen) ? metadata.listen : [];
188
+ const signals = emit.length > 0 || listen.length > 0 ? { emit, listen } : null;
189
+
190
+ return {
191
+ name: frontmatter.name || '',
192
+ description: frontmatter.description || '',
193
+ layer: metadata.layer || 'L2',
194
+ model: metadata.model || 'sonnet',
195
+ group: metadata.group || 'general',
196
+ signals,
197
+ contextFork: frontmatter.context === 'fork',
198
+ agentType: frontmatter.agent || null,
199
+ body,
200
+ crossRefs: extractCrossRefs(body),
201
+ toolRefs: extractToolRefs(body),
202
+ hardGates: extractHardGates(body),
203
+ sections: extractSections(body),
204
+ filePath,
205
+ frontmatter,
206
+ };
207
+ }
208
+
209
+ /**
210
+ * Parse a PACK.md extension file (same format, slightly different metadata)
211
+ * Supports both monolith format (all skills inline) and split format (skills in separate files)
212
+ *
213
+ * Split format detection: metadata.format === "split" OR metadata.skills array present
214
+ */
215
+ export function parsePack(content, filePath = '') {
216
+ const { frontmatter, body } = parseFrontmatter(content);
217
+ const metadata = frontmatter.metadata || {};
218
+
219
+ // Detect split format
220
+ const isSplit = metadata.format === 'split' || Array.isArray(metadata.skills);
221
+
222
+ return {
223
+ name: frontmatter.name || '',
224
+ description: frontmatter.description || '',
225
+ version: metadata.version || frontmatter.version || '1.0.0',
226
+ layer: 'L4',
227
+ group: 'extension',
228
+ isSplit,
229
+ skillManifest: isSplit ? parseSkillManifest(metadata.skills || []) : [],
230
+ body,
231
+ crossRefs: extractCrossRefs(body),
232
+ toolRefs: extractToolRefs(body),
233
+ hardGates: extractHardGates(body),
234
+ sections: extractSections(body),
235
+ filePath,
236
+ frontmatter,
237
+ };
238
+ }
239
+
240
+ /**
241
+ * Parse the skills manifest from split PACK.md frontmatter
242
+ *
243
+ * @param {Array} skills - array of skill entries from frontmatter metadata.skills
244
+ * @returns {Array<{name: string, file: string, model: string, description: string}>}
245
+ */
246
+ function parseSkillManifest(skills) {
247
+ if (!Array.isArray(skills)) return [];
248
+
249
+ return skills.map((skill) => {
250
+ // Handle both string format ("skill-name") and object format ({name, file, model, description})
251
+ if (typeof skill === 'string') {
252
+ return {
253
+ name: skill,
254
+ file: `skills/${skill}.md`,
255
+ model: 'sonnet',
256
+ description: '',
257
+ };
258
+ }
259
+
260
+ return {
261
+ name: skill.name || '',
262
+ file: skill.file || `skills/${skill.name}.md`,
263
+ model: skill.model || 'sonnet',
264
+ description: skill.description || '',
265
+ };
266
+ });
267
+ }
@@ -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
+ }
@@ -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.1.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