@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
@@ -0,0 +1,274 @@
1
+ # MCP Server Production Reference
2
+
3
+ > Loaded by `@rune/zalo` when MCP server setup, production deployment, or cursor-based pagination patterns detected.
4
+
5
+ ---
6
+
7
+ ## MCP Server Modes
8
+
9
+ ### Local (stdio)
10
+
11
+ ```bash
12
+ # Start MCP server in stdio mode (for local Claude Code / AI IDE)
13
+ zalo-agent mcp start
14
+
15
+ # Connects via stdin/stdout — AI agent calls tools directly
16
+ ```
17
+
18
+ ### Remote (HTTP)
19
+
20
+ ```bash
21
+ # Start MCP server in HTTP mode (for remote agents, multi-client)
22
+ zalo-agent mcp start --http 3847 --auth your-secret-token
23
+
24
+ # Clients connect via HTTP transport with Bearer auth
25
+ ```
26
+
27
+ ---
28
+
29
+ ## Enhanced Tool Definitions
30
+
31
+ ### zalo_get_messages — Cursor-Based Pagination
32
+
33
+ Replace simple queue polling with cursor-based pagination from a ring buffer:
34
+
35
+ ```typescript
36
+ {
37
+ name: 'zalo_get_messages',
38
+ description: 'Get messages from ring buffer with cursor-based pagination.',
39
+ inputSchema: {
40
+ type: 'object',
41
+ properties: {
42
+ cursor: {
43
+ type: 'string',
44
+ description: 'Opaque cursor from previous response. Omit for latest messages.'
45
+ },
46
+ limit: {
47
+ type: 'number',
48
+ default: 20,
49
+ maximum: 100,
50
+ description: 'Max messages to return'
51
+ },
52
+ thread_id: {
53
+ type: 'string',
54
+ description: 'Filter by specific thread. Omit for all threads.'
55
+ }
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ Returns:
62
+
63
+ ```json
64
+ {
65
+ "messages": [
66
+ {
67
+ "id": "msg_abc123",
68
+ "thread_id": "user_456",
69
+ "sender_name": "Nguyen Van A",
70
+ "text": "Hello",
71
+ "timestamp": 1711234567890,
72
+ "type": "text",
73
+ "attachments": []
74
+ }
75
+ ],
76
+ "next_cursor": "eyJ0IjoxNzExMjM0NTY3ODkwfQ==",
77
+ "has_more": true
78
+ }
79
+ ```
80
+
81
+ ### Implementation
82
+
83
+ ```typescript
84
+ class RingBuffer {
85
+ private buffer: Message[] = []
86
+ private maxSize: number
87
+
88
+ constructor(maxSize = 5000) {
89
+ this.maxSize = maxSize
90
+ }
91
+
92
+ push(msg: Message) {
93
+ this.buffer.push(msg)
94
+ if (this.buffer.length > this.maxSize) {
95
+ this.buffer = this.buffer.slice(-this.maxSize) // keep newest
96
+ }
97
+ }
98
+
99
+ query(cursor?: string, limit = 20, threadId?: string): QueryResult {
100
+ let startIdx = 0
101
+
102
+ if (cursor) {
103
+ const decoded = JSON.parse(Buffer.from(cursor, 'base64url').toString())
104
+ startIdx = this.buffer.findIndex(m => m.timestamp > decoded.t)
105
+ if (startIdx === -1) startIdx = this.buffer.length
106
+ }
107
+
108
+ let filtered = this.buffer.slice(startIdx)
109
+ if (threadId) {
110
+ filtered = filtered.filter(m => m.thread_id === threadId)
111
+ }
112
+
113
+ const page = filtered.slice(0, limit)
114
+ const hasMore = filtered.length > limit
115
+
116
+ const nextCursor = page.length > 0
117
+ ? Buffer.from(JSON.stringify({ t: page[page.length - 1].timestamp })).toString('base64url')
118
+ : cursor
119
+
120
+ return { messages: page, next_cursor: nextCursor, has_more: hasMore }
121
+ }
122
+ }
123
+ ```
124
+
125
+ ### zalo_list_threads — Active Conversations
126
+
127
+ ```typescript
128
+ {
129
+ name: 'zalo_list_threads',
130
+ description: 'List active conversations with unread counts.',
131
+ inputSchema: {
132
+ type: 'object',
133
+ properties: {
134
+ limit: { type: 'number', default: 20 },
135
+ unread_only: { type: 'boolean', default: false }
136
+ }
137
+ }
138
+ }
139
+ ```
140
+
141
+ Returns: `{ threads: [{ id, name, type, unread_count, last_message_at }] }`
142
+
143
+ ### zalo_mark_read — Clear Buffer
144
+
145
+ ```typescript
146
+ {
147
+ name: 'zalo_mark_read',
148
+ description: 'Mark messages as processed. Clears from unread count.',
149
+ inputSchema: {
150
+ type: 'object',
151
+ required: ['thread_id'],
152
+ properties: {
153
+ thread_id: { type: 'string' },
154
+ up_to_cursor: { type: 'string', description: 'Mark all messages up to this cursor as read' }
155
+ }
156
+ }
157
+ }
158
+ ```
159
+
160
+ ### zalo_view_image — Media Download
161
+
162
+ ```typescript
163
+ {
164
+ name: 'zalo_view_image',
165
+ description: 'Download and view an image from a Zalo message locally.',
166
+ inputSchema: {
167
+ type: 'object',
168
+ required: ['message_id'],
169
+ properties: {
170
+ message_id: { type: 'string' },
171
+ save_to: { type: 'string', description: 'Local path to save. Default: temp directory.' }
172
+ }
173
+ }
174
+ }
175
+ ```
176
+
177
+ ---
178
+
179
+ ## MCP Configuration
180
+
181
+ ```json
182
+ // mcp-config.json
183
+ {
184
+ "watchThreads": ["group_abc", "user_xyz"],
185
+ "keywords": ["help", "support", "bug"],
186
+ "buffer": {
187
+ "maxSize": 5000,
188
+ "persistPath": "./mcp-buffer.json"
189
+ },
190
+ "autoReconnect": true,
191
+ "reconnectDelayMs": 5000
192
+ }
193
+ ```
194
+
195
+ | Field | Description | Default |
196
+ |-------|-------------|---------|
197
+ | `watchThreads` | Only capture messages from these threads | All threads |
198
+ | `keywords` | Keyword triggers — only buffer matching messages | All messages |
199
+ | `buffer.maxSize` | Ring buffer capacity | 5000 |
200
+ | `buffer.persistPath` | Persist buffer to disk on shutdown | In-memory only |
201
+ | `autoReconnect` | Auto-reconnect WebSocket on disconnect | true |
202
+ | `reconnectDelayMs` | Delay before reconnect attempt | 5000 |
203
+
204
+ ---
205
+
206
+ ## Production Deployment
207
+
208
+ ### pm2 (Recommended)
209
+
210
+ ```bash
211
+ # ecosystem.config.js
212
+ module.exports = {
213
+ apps: [{
214
+ name: 'zalo-mcp',
215
+ script: 'zalo-agent',
216
+ args: 'mcp start --http 3847 --auth $MCP_AUTH_SECRET',
217
+ autorestart: true,
218
+ max_restarts: 10,
219
+ restart_delay: 5000,
220
+ env: {
221
+ NODE_ENV: 'production',
222
+ MCP_AUTH_SECRET: 'your-secret'
223
+ }
224
+ }]
225
+ }
226
+
227
+ # Start
228
+ pm2 start ecosystem.config.js
229
+ pm2 save
230
+ pm2 startup
231
+ ```
232
+
233
+ ### WebSocket Session Management
234
+
235
+ ```
236
+ Key constraints:
237
+ 1. ONE WebSocket connection per Zalo account — no parallelism
238
+ 2. Browser Zalo and bot CANNOT coexist on same account
239
+ 3. WebSocket drops on network change → auto-reconnect required
240
+ 4. Session cookie expires → re-login with stored credentials
241
+ ```
242
+
243
+ ### Health Check Pattern
244
+
245
+ ```typescript
246
+ // Periodic health check for MCP + WebSocket
247
+ setInterval(async () => {
248
+ const wsAlive = api.isConnected()
249
+ const lastMsg = ringBuffer.getLatest()?.timestamp ?? 0
250
+ const silentMinutes = (Date.now() - lastMsg) / 60_000
251
+
252
+ if (!wsAlive) {
253
+ logger.error('WebSocket disconnected — attempting reconnect')
254
+ await api.reconnect()
255
+ }
256
+
257
+ if (silentMinutes > 30 && wsAlive) {
258
+ logger.warn(`No messages for ${silentMinutes.toFixed(0)}m — possible silent disconnect`)
259
+ // Force reconnect if no messages for 60+ minutes
260
+ if (silentMinutes > 60) await api.reconnect()
261
+ }
262
+ }, 60_000) // check every minute
263
+ ```
264
+
265
+ ---
266
+
267
+ ## Sharp Edges
268
+
269
+ - **Ring buffer vs queue**: Ring buffer overwrites oldest when full (no data loss notification). Queue drops with warning. Choose based on whether losing old messages is acceptable.
270
+ - **Cursor encoding**: Use `base64url` not `base64` — cursors appear in URLs and JSON, `+` and `/` cause issues
271
+ - **HTTP MCP auth**: `--auth` token is sent as `Bearer` header — use HTTPS in production or attacker can sniff the token
272
+ - **pm2 + credentials**: Never pass credentials as CLI args (visible in `ps aux`) — use env vars or credential file
273
+ - **Buffer persistence**: If `persistPath` is set, buffer writes to disk on SIGTERM — unclean kill loses buffered messages
274
+ - **WebSocket single-session**: Starting MCP server kicks out browser Zalo. Warn user before starting.
@@ -0,0 +1,224 @@
1
+ # Multi-Account & Proxy Management Reference
2
+
3
+ > Loaded by `@rune/zalo` when multi-account setup, proxy configuration, or production account management patterns detected.
4
+
5
+ ---
6
+
7
+ ## Multi-Account Architecture
8
+
9
+ Run multiple Zalo accounts on a single server — each with its own session, proxy, and WebSocket connection.
10
+
11
+ ### Account Operations
12
+
13
+ | Command | Description |
14
+ |---------|-------------|
15
+ | `account list` | Show all registered accounts (ID, name, status, proxy) |
16
+ | `account login -n <name> -p <proxy>` | Login new account with proxy |
17
+ | `account switch <ownerId>` | Switch active account |
18
+ | `account remove <ownerId>` | Remove account and credentials |
19
+ | `account info <ownerId>` | Show account details |
20
+ | `account export -o <path>` | Export credentials for headless re-login |
21
+
22
+ ### Login Flow
23
+
24
+ ```bash
25
+ # First account — interactive QR login
26
+ zalo-agent login --qr-url
27
+ # Opens http://localhost:18927/qr — scan with Zalo app
28
+
29
+ # Additional accounts — with proxy
30
+ zalo-agent account login \
31
+ -n "support-bot" \
32
+ -p "socks5://user:pass@proxy1.example.com:1080"
33
+
34
+ # Headless re-login (VPS, CI/CD)
35
+ zalo-agent account export -o ./creds-support.json
36
+ zalo-agent login --credentials ./creds-support.json
37
+ ```
38
+
39
+ ### QR Server Ports
40
+
41
+ Port auto-tries in order: `18927 → 8080 → 3000 → 9000`. QR code expires after **60 seconds**.
42
+
43
+ For VPS without browser access:
44
+
45
+ ```bash
46
+ # Start QR server accessible from outside
47
+ zalo-agent login --qr-url
48
+ # Access http://<VPS_IP>:18927/qr from your phone/laptop browser
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Proxy Configuration
54
+
55
+ ### Why Proxies Are Required
56
+
57
+ Zalo detects multiple accounts from the same IP address. Without proxies:
58
+ - Accounts may be flagged for suspicious activity
59
+ - Rate limits are shared across all accounts on the same IP
60
+ - Ban on one account can cascade to others
61
+
62
+ ### Rules
63
+
64
+ 1. **1 unique proxy per account** — never share proxies between accounts
65
+ 2. **Residential proxies preferred** — datacenter IPs are flagged more aggressively
66
+ 3. **Vietnamese proxies recommended** — foreign IPs trigger additional verification
67
+ 4. **Consistent proxy** — don't rotate proxies for the same account (triggers re-verification)
68
+
69
+ ### Supported Formats
70
+
71
+ ```
72
+ http://host:port
73
+ http://user:pass@host:port
74
+ https://user:pass@host:port
75
+ socks5://host:port
76
+ socks5://user:pass@host:port
77
+ ```
78
+
79
+ ### Implementation Pattern
80
+
81
+ ```typescript
82
+ import { HttpsProxyAgent } from 'https-proxy-agent'
83
+ import { SocksProxyAgent } from 'socks-proxy-agent'
84
+
85
+ function createProxyAgent(proxyUrl: string) {
86
+ const url = new URL(proxyUrl)
87
+ if (url.protocol.startsWith('socks')) {
88
+ return new SocksProxyAgent(proxyUrl)
89
+ }
90
+ return new HttpsProxyAgent(proxyUrl)
91
+ }
92
+
93
+ // Per-account proxy assignment
94
+ const accounts: Map<string, { api: ZaloApi; proxy: string }> = new Map()
95
+
96
+ async function loginWithProxy(name: string, proxyUrl: string) {
97
+ const agent = createProxyAgent(proxyUrl)
98
+ const api = new ZaloApi({ proxy: agent, name })
99
+ await api.login()
100
+ accounts.set(api.ownerId, { api, proxy: proxyUrl })
101
+ }
102
+ ```
103
+
104
+ ---
105
+
106
+ ## Credential Storage
107
+
108
+ ```
109
+ ~/.zalo-agent-cli/
110
+ ├── credentials.json # Default account
111
+ ├── accounts/
112
+ │ ├── <ownerId1>.json # Account 1 credentials
113
+ │ └── <ownerId2>.json # Account 2 credentials
114
+ └── config.json # Global config (proxies, defaults)
115
+ ```
116
+
117
+ ### Security Rules
118
+
119
+ - File permissions: `0600` (owner read/write only)
120
+ - **Never** log credential file contents
121
+ - **Never** commit to git (add to `.gitignore`)
122
+ - **Never** pass credentials as CLI arguments (visible in `ps aux`)
123
+ - Use environment variables or credential files for headless login
124
+
125
+ ```bash
126
+ # .gitignore
127
+ .zalo-agent-cli/
128
+ *.zalo-creds.json
129
+ ```
130
+
131
+ ---
132
+
133
+ ## Account Switching in Code
134
+
135
+ ```typescript
136
+ class AccountManager {
137
+ private accounts = new Map<string, ZaloApi>()
138
+ private active: string | null = null
139
+
140
+ async switch(ownerId: string): Promise<ZaloApi> {
141
+ const api = this.accounts.get(ownerId)
142
+ if (!api) throw new Error(`Account ${ownerId} not found`)
143
+ this.active = ownerId
144
+ return api
145
+ }
146
+
147
+ getActive(): ZaloApi {
148
+ if (!this.active) throw new Error('No active account')
149
+ return this.accounts.get(this.active)!
150
+ }
151
+
152
+ // Route messages to correct account
153
+ async routeMessage(msg: InboundMessage) {
154
+ const targetAccount = this.resolveAccount(msg.recipientId)
155
+ const api = this.accounts.get(targetAccount)
156
+ if (!api) return // account not managed
157
+
158
+ await api.sendMessage(msg.response, msg.threadId, msg.type)
159
+ }
160
+ }
161
+ ```
162
+
163
+ ---
164
+
165
+ ## Production Multi-Account Setup
166
+
167
+ ### pm2 Ecosystem
168
+
169
+ ```javascript
170
+ // ecosystem.config.js — one process per account
171
+ module.exports = {
172
+ apps: [
173
+ {
174
+ name: 'zalo-bot-main',
175
+ script: 'zalo-agent',
176
+ args: 'listen -w http://localhost:3001/webhook',
177
+ env: { ZALO_ACCOUNT: 'main', ZALO_PROXY: 'socks5://user:pass@proxy1:1080' }
178
+ },
179
+ {
180
+ name: 'zalo-bot-support',
181
+ script: 'zalo-agent',
182
+ args: 'listen -w http://localhost:3001/webhook',
183
+ env: { ZALO_ACCOUNT: 'support', ZALO_PROXY: 'socks5://user:pass@proxy2:1080' }
184
+ }
185
+ ]
186
+ }
187
+ ```
188
+
189
+ ### Health Monitoring
190
+
191
+ ```typescript
192
+ // Check all accounts periodically
193
+ async function healthCheck() {
194
+ for (const [id, { api, proxy }] of accounts) {
195
+ const connected = api.isConnected()
196
+ const status = await api.getStatus().catch(() => 'error')
197
+
198
+ if (!connected) {
199
+ logger.error(`Account ${id} disconnected via ${proxy}`)
200
+ await api.reconnect()
201
+ }
202
+
203
+ if (status === 'banned') {
204
+ logger.critical(`Account ${id} BANNED — removing from rotation`)
205
+ accounts.delete(id)
206
+ // Alert operator
207
+ }
208
+ }
209
+ }
210
+
211
+ setInterval(healthCheck, 5 * 60_000) // every 5 minutes
212
+ ```
213
+
214
+ ---
215
+
216
+ ## Sharp Edges
217
+
218
+ - **Proxy per account**: Sharing proxies between accounts = ban risk for ALL accounts on that proxy
219
+ - **QR expiry**: 60 seconds — have Zalo app ready before starting login
220
+ - **Session conflicts**: Only ONE active session per account — starting a new login invalidates existing sessions
221
+ - **Credential export**: Exported JSON contains session cookies — treat as sensitive as passwords
222
+ - **VPS QR access**: Firewall must allow inbound on QR port (18927) — close after login
223
+ - **Proxy rotation**: Do NOT rotate proxies for established sessions — Zalo treats IP change as suspicious
224
+ - **Rate limits per account**: Each account has independent rate limits — but shared IP without proxy means shared rate limit bucket
@@ -0,0 +1,160 @@
1
+ # VietQR & Banking Reference
2
+
3
+ > Loaded by `@rune/zalo` when payment, bank transfer, QR code, or VietQR patterns detected.
4
+
5
+ ---
6
+
7
+ ## VietQR Transfer Messages
8
+
9
+ Send bank transfer QR codes directly in Zalo chat — recipient scans to pay via their banking app.
10
+
11
+ ### Send Bank Card
12
+
13
+ ```bash
14
+ # CLI: send bank card with account details
15
+ zalo-agent msg send-card -t <threadId> \
16
+ --bank vcb \
17
+ --account-number 1234567890 \
18
+ --account-name "NGUYEN VAN A"
19
+
20
+ # With custom amount and message
21
+ zalo-agent msg send-card -t <threadId> \
22
+ --bank mbbank \
23
+ --account-number 0987654321 \
24
+ --account-name "TRAN THI B" \
25
+ --amount 500000 \
26
+ --message "Thanh toan don hang #123"
27
+ ```
28
+
29
+ ### Send QR Transfer
30
+
31
+ ```bash
32
+ # Generate VietQR and send as image
33
+ zalo-agent msg send-qr-transfer -t <threadId> \
34
+ --bank techcombank \
35
+ --account-number 19033456789 \
36
+ --account-name "LE VAN C" \
37
+ --amount 1000000 \
38
+ --message "Tien thue thang 3"
39
+ ```
40
+
41
+ ### Implementation Pattern
42
+
43
+ ```typescript
44
+ import { generateVietQR } from './vietqr-utils'
45
+
46
+ interface BankTransfer {
47
+ bankCode: string // BIN code or alias (e.g., 'vcb', 'mbbank')
48
+ accountNumber: string
49
+ accountName: string
50
+ amount?: number
51
+ message?: string // Noi dung chuyen khoan
52
+ }
53
+
54
+ async function sendBankCard(api: ZaloApi, threadId: string, transfer: BankTransfer) {
55
+ // 1. Resolve bank alias to BIN code
56
+ const bin = BANK_ALIASES[transfer.bankCode] ?? transfer.bankCode
57
+
58
+ // 2. Generate VietQR URL (NAPAS standard)
59
+ const qrUrl = `https://img.vietqr.io/image/${bin}-${transfer.accountNumber}-compact.png`
60
+ + (transfer.amount ? `?amount=${transfer.amount}` : '')
61
+ + (transfer.message ? `&addInfo=${encodeURIComponent(transfer.message)}` : '')
62
+
63
+ // 3. Download QR image to temp file
64
+ const tempPath = await downloadToTemp(qrUrl)
65
+
66
+ // 4. Send as image with caption
67
+ const caption = [
68
+ `🏦 ${BANK_NAMES[bin] ?? bin}`,
69
+ `📋 STK: ${transfer.accountNumber}`,
70
+ `👤 ${transfer.accountName}`,
71
+ transfer.amount ? `💰 ${new Intl.NumberFormat('vi-VN').format(transfer.amount)} VND` : null,
72
+ transfer.message ? `📝 ${transfer.message}` : null,
73
+ ].filter(Boolean).join('\n')
74
+
75
+ await api.sendMessage({ body: caption, attachments: [tempPath] }, threadId, 'User')
76
+ }
77
+ ```
78
+
79
+ ---
80
+
81
+ ## Vietnamese Bank Aliases
82
+
83
+ 55+ banks supported via VietQR / NAPAS standard:
84
+
85
+ | Alias | Bank Name | BIN |
86
+ |-------|-----------|-----|
87
+ | `vcb`, `vietcombank` | Vietcombank | 970436 |
88
+ | `bidv` | BIDV | 970418 |
89
+ | `vietinbank`, `ctg` | VietinBank | 970415 |
90
+ | `agribank` | Agribank | 970405 |
91
+ | `mb`, `mbbank` | MB Bank | 970422 |
92
+ | `techcombank`, `tcb` | Techcombank | 970407 |
93
+ | `acb` | ACB | 970416 |
94
+ | `vpbank` | VPBank | 970432 |
95
+ | `tpbank` | TPBank | 970423 |
96
+ | `sacombank`, `stb` | Sacombank | 970403 |
97
+ | `hdbank` | HDBank | 970437 |
98
+ | `ocb` | OCB | 970448 |
99
+ | `msb` | MSB | 970426 |
100
+ | `seabank` | SeABank | 970440 |
101
+ | `shb` | SHB | 970443 |
102
+ | `eximbank` | Eximbank | 970431 |
103
+ | `vib` | VIB | 970441 |
104
+ | `scb` | SCB | 970429 |
105
+ | `ncb` | NCB | 970419 |
106
+ | `abbank` | ABBank | 970425 |
107
+ | `dongabank` | DongA Bank | 970406 |
108
+ | `kienlongbank` | Kien Long Bank | 970452 |
109
+ | `baovibank` | BaoViet Bank | 970438 |
110
+ | `pvcombank` | PVcomBank | 970412 |
111
+ | `lienvietpostbank`, `lpb` | LienVietPostBank | 970449 |
112
+ | `namabank` | Nam A Bank | 970428 |
113
+ | `pgbank` | PG Bank | 970430 |
114
+ | `vietabank` | VietA Bank | 970427 |
115
+ | `bacabank` | Bac A Bank | 970409 |
116
+ | `saigonbank` | Saigon Bank | 970400 |
117
+ | `gpbank` | GP Bank | 970408 |
118
+ | `cbbank` | CB Bank | 970444 |
119
+ | `oceanbank` | OceanBank | 970414 |
120
+ | `shinhan` | Shinhan Vietnam | 970424 |
121
+ | `hsbc` | HSBC Vietnam | 970443 |
122
+ | `cimb` | CIMB Vietnam | 422589 |
123
+ | `woori` | Woori Vietnam | 970457 |
124
+ | `uob` | UOB Vietnam | 970458 |
125
+ | `standardchartered`, `sc` | Standard Chartered VN | 970410 |
126
+ | `publicbank` | Public Bank VN | 970439 |
127
+ | `cake` | CAKE (VPBank Digital) | 546034 |
128
+ | `ubank` | Ubank (VPBank) | 546035 |
129
+ | `timo` | Timo (Ban Viet) | 963388 |
130
+ | `vnptmoney` | VNPT Money | 971011 |
131
+ | `momo` | MoMo (via NAPAS) | 999996 |
132
+ | `zalopay` | ZaloPay (via NAPAS) | 999997 |
133
+ | `viettelpay` | ViettelPay | 971005 |
134
+
135
+ ### Alias Lookup
136
+
137
+ ```typescript
138
+ const BANK_ALIASES: Record<string, string> = {
139
+ vcb: '970436', vietcombank: '970436',
140
+ bidv: '970418',
141
+ vietinbank: '970415', ctg: '970415',
142
+ agribank: '970405',
143
+ mb: '970422', mbbank: '970422',
144
+ techcombank: '970407', tcb: '970407',
145
+ acb: '970416',
146
+ vpbank: '970432',
147
+ tpbank: '970423',
148
+ // ... extend as needed
149
+ }
150
+ ```
151
+
152
+ ---
153
+
154
+ ## Sharp Edges
155
+
156
+ - **VietQR image service**: `img.vietqr.io` is free but rate-limited — cache generated QR images locally for repeated sends
157
+ - **Amount encoding**: Must be integer (VND has no decimal). `500000` not `500,000` or `500.000`
158
+ - **Message encoding**: `addInfo` must be URL-encoded, max 25 characters (NAPAS limit), ASCII only — no Vietnamese diacritics
159
+ - **Bank BIN codes**: Change rarely but verify against NAPAS registry for production use
160
+ - **QR expiry**: VietQR codes don't expire — they encode static account info. But amounts can become stale if prices change
package/hooks/hooks.json CHANGED
@@ -12,6 +12,18 @@
12
12
  ]
13
13
  }
14
14
  ],
15
+ "UserPromptSubmit": [
16
+ {
17
+ "matcher": ".*",
18
+ "hooks": [
19
+ {
20
+ "type": "command",
21
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cjs\" intent-router",
22
+ "async": true
23
+ }
24
+ ]
25
+ }
26
+ ],
15
27
  "PreToolUse": [
16
28
  {
17
29
  "matcher": "Read|Write|Edit",