@rune-kit/rune 2.2.0 ā 2.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -40
- package/compiler/__tests__/pack-split.test.js +145 -0
- package/compiler/bin/rune.js +26 -9
- package/compiler/doctor.js +42 -0
- package/compiler/emitter.js +27 -4
- package/compiler/parser.js +41 -3
- package/compiler/transformer.js +10 -6
- package/compiler/transforms/compliance.js +40 -0
- package/docs/ANTIGRAVITY-GAP-ANALYSIS.md +369 -0
- package/docs/ARCHITECTURE.md +332 -0
- package/docs/COMMUNITY-PACKS.md +109 -0
- package/docs/CONTRIBUTING-L4.md +215 -0
- package/docs/CROSS-IDE-ANALYSIS.md +164 -0
- package/docs/EXTENSION-TEMPLATE.md +108 -0
- package/docs/MESH-RULES.md +34 -0
- package/docs/MULTI-PLATFORM.md +804 -0
- package/docs/SKILL-DEPTH-AUDIT.md +191 -0
- package/docs/SKILL-TEMPLATE.md +72 -0
- package/docs/TRADE-MATRIX.md +327 -0
- package/docs/VERSIONING.md +91 -0
- package/docs/VISION.md +263 -0
- package/docs/assets/demo-subtitles.srt +215 -0
- package/docs/assets/end-card.html +276 -0
- package/docs/assets/mesh-diagram.html +654 -0
- package/docs/assets/thumbnail.html +295 -0
- package/docs/guides/cli.md +403 -0
- package/docs/guides/index.html +1346 -0
- package/docs/index.html +674 -0
- package/docs/references/claudekit-analysis.md +414 -0
- package/docs/references/voltagent-analysis.md +189 -0
- package/docs/script.js +277 -0
- package/docs/skills/index.html +832 -0
- package/docs/style.css +583 -0
- package/docs/video-demo-plan.md +172 -0
- package/extensions/ai-ml/PACK.md +38 -474
- package/extensions/ai-ml/skills/ai-agents.md +172 -0
- package/extensions/ai-ml/skills/code-sandbox.md +187 -0
- package/extensions/ai-ml/skills/deep-research.md +146 -0
- package/extensions/ai-ml/skills/embedding-search.md +66 -0
- package/extensions/ai-ml/skills/fine-tuning-guide.md +74 -0
- package/extensions/ai-ml/skills/llm-architect.md +125 -0
- package/extensions/ai-ml/skills/llm-integration.md +64 -0
- package/extensions/ai-ml/skills/prompt-patterns.md +72 -0
- package/extensions/ai-ml/skills/rag-patterns.md +66 -0
- package/extensions/ai-ml/skills/web-extraction.md +114 -0
- package/extensions/analytics/PACK.md +19 -484
- package/extensions/analytics/skills/ab-testing.md +72 -0
- package/extensions/analytics/skills/dashboard-patterns.md +83 -0
- package/extensions/analytics/skills/data-validation.md +68 -0
- package/extensions/analytics/skills/funnel-analysis.md +81 -0
- package/extensions/analytics/skills/sql-patterns.md +57 -0
- package/extensions/analytics/skills/statistical-analysis.md +79 -0
- package/extensions/analytics/skills/tracking-setup.md +71 -0
- package/extensions/backend/PACK.md +44 -618
- package/extensions/backend/skills/api-patterns.md +84 -0
- package/extensions/backend/skills/async-pipeline.md +193 -0
- package/extensions/backend/skills/auth-patterns.md +97 -0
- package/extensions/backend/skills/background-jobs.md +133 -0
- package/extensions/backend/skills/caching-patterns.md +108 -0
- package/extensions/backend/skills/cli-generation.md +133 -0
- package/extensions/backend/skills/database-patterns.md +87 -0
- package/extensions/backend/skills/middleware-patterns.md +104 -0
- package/extensions/chrome-ext/PACK.md +19 -921
- package/extensions/chrome-ext/skills/cws-preflight.md +143 -0
- package/extensions/chrome-ext/skills/cws-publish.md +104 -0
- package/extensions/chrome-ext/skills/ext-ai-integration.md +251 -0
- package/extensions/chrome-ext/skills/ext-messaging.md +139 -0
- package/extensions/chrome-ext/skills/ext-storage.md +133 -0
- package/extensions/chrome-ext/skills/mv3-scaffold.md +164 -0
- package/extensions/content/PACK.md +43 -335
- package/extensions/content/skills/blog-patterns.md +88 -0
- package/extensions/content/skills/cms-integration.md +131 -0
- package/extensions/content/skills/content-scoring.md +107 -0
- package/extensions/content/skills/i18n.md +83 -0
- package/extensions/content/skills/mdx-authoring.md +137 -0
- package/extensions/content/skills/reference.md +1014 -0
- package/extensions/content/skills/seo-patterns.md +67 -0
- package/extensions/content/skills/video-repurpose.md +153 -0
- package/extensions/devops/PACK.md +38 -457
- package/extensions/devops/skills/chaos-testing.md +67 -0
- package/extensions/devops/skills/ci-cd.md +75 -0
- package/extensions/devops/skills/docker.md +58 -0
- package/extensions/devops/skills/edge-serverless.md +163 -0
- package/extensions/devops/skills/infra-as-code.md +158 -0
- package/extensions/devops/skills/kubernetes.md +110 -0
- package/extensions/devops/skills/monitoring.md +57 -0
- package/extensions/devops/skills/server-setup.md +64 -0
- package/extensions/devops/skills/ssl-domain.md +42 -0
- package/extensions/ecommerce/PACK.md +62 -226
- package/extensions/ecommerce/skills/cart-system.md +79 -0
- package/extensions/ecommerce/skills/inventory-mgmt.md +102 -0
- package/extensions/ecommerce/skills/order-management.md +126 -0
- package/extensions/ecommerce/skills/payment-integration.md +472 -0
- package/extensions/ecommerce/skills/shopify-dev.md +69 -0
- package/extensions/ecommerce/skills/subscription-billing.md +93 -0
- package/extensions/ecommerce/skills/tax-compliance.md +117 -0
- package/extensions/gamedev/PACK.md +66 -317
- package/extensions/gamedev/skills/asset-pipeline.md +74 -0
- package/extensions/gamedev/skills/audio-system.md +129 -0
- package/extensions/gamedev/skills/camera-system.md +87 -0
- package/extensions/gamedev/skills/ecs.md +98 -0
- package/extensions/gamedev/skills/game-loops.md +72 -0
- package/extensions/gamedev/skills/input-system.md +199 -0
- package/extensions/gamedev/skills/multiplayer.md +180 -0
- package/extensions/gamedev/skills/particles.md +105 -0
- package/extensions/gamedev/skills/physics-engine.md +89 -0
- package/extensions/gamedev/skills/scene-management.md +146 -0
- package/extensions/gamedev/skills/threejs-patterns.md +90 -0
- package/extensions/gamedev/skills/webgl.md +71 -0
- package/extensions/mobile/PACK.md +56 -223
- package/extensions/mobile/skills/app-store-connect.md +152 -0
- package/extensions/mobile/skills/app-store-prep.md +66 -0
- package/extensions/mobile/skills/deep-linking.md +109 -0
- package/extensions/mobile/skills/flutter.md +60 -0
- package/extensions/mobile/skills/ios-build-pipeline.md +142 -0
- package/extensions/mobile/skills/native-bridge.md +66 -0
- package/extensions/mobile/skills/ota-updates.md +97 -0
- package/extensions/mobile/skills/push-notifications.md +111 -0
- package/extensions/mobile/skills/react-native.md +82 -0
- package/extensions/saas/PACK.md +26 -720
- package/extensions/saas/skills/billing-integration.md +121 -0
- package/extensions/saas/skills/feature-flags.md +130 -0
- package/extensions/saas/skills/multi-tenant.md +103 -0
- package/extensions/saas/skills/onboarding-flow.md +139 -0
- package/extensions/saas/skills/subscription-flow.md +95 -0
- package/extensions/saas/skills/team-management.md +144 -0
- package/extensions/security/PACK.md +10 -448
- package/extensions/security/skills/api-security.md +140 -0
- package/extensions/security/skills/compliance.md +68 -0
- package/extensions/security/skills/owasp-audit.md +64 -0
- package/extensions/security/skills/pentest-patterns.md +77 -0
- package/extensions/security/skills/secret-mgmt.md +65 -0
- package/extensions/security/skills/supply-chain.md +65 -0
- package/extensions/trading/PACK.md +18 -535
- package/extensions/trading/skills/chart-components.md +55 -0
- package/extensions/trading/skills/experiment-loop.md +125 -0
- package/extensions/trading/skills/fintech-patterns.md +47 -0
- package/extensions/trading/skills/indicator-library.md +58 -0
- package/extensions/trading/skills/quant-analysis.md +111 -0
- package/extensions/trading/skills/realtime-data.md +58 -0
- package/extensions/trading/skills/trade-logic.md +104 -0
- package/extensions/ui/PACK.md +36 -853
- package/extensions/ui/skills/a11y-audit.md +91 -0
- package/extensions/ui/skills/animation-patterns.md +106 -0
- package/extensions/ui/skills/component-patterns.md +75 -0
- package/extensions/ui/skills/design-decision.md +108 -0
- package/extensions/ui/skills/design-system.md +68 -0
- package/extensions/ui/skills/landing-patterns.md +155 -0
- package/extensions/ui/skills/palette-picker.md +173 -0
- package/extensions/ui/skills/react-health.md +90 -0
- package/extensions/ui/skills/type-system.md +125 -0
- package/extensions/ui/skills/web-vitals.md +153 -0
- package/extensions/zalo/PACK.md +117 -0
- package/extensions/zalo/skills/zalo-oa-mcp.md +317 -0
- package/extensions/zalo/skills/zalo-oa-messaging.md +429 -0
- package/extensions/zalo/skills/zalo-oa-setup.md +236 -0
- package/extensions/zalo/skills/zalo-oa-webhook.md +189 -0
- package/extensions/zalo/skills/zalo-personal-messaging.md +194 -0
- package/extensions/zalo/skills/zalo-personal-setup.md +153 -0
- package/extensions/zalo/skills/zalo-rate-guard.md +219 -0
- package/hooks/.gitkeep +0 -0
- package/hooks/auto-format/index.cjs +48 -0
- package/hooks/context-watch/index.cjs +68 -0
- package/hooks/hooks.json +99 -0
- package/hooks/metrics-collector/index.cjs +42 -0
- package/hooks/post-session-reflect/index.cjs +153 -0
- package/hooks/pre-compact/index.cjs +95 -0
- package/hooks/pre-tool-guard/index.cjs +68 -0
- package/hooks/run-hook +17 -0
- package/hooks/run-hook.cjs +16 -0
- package/hooks/run-hook.cmd +1 -0
- package/hooks/secrets-scan/index.cjs +100 -0
- package/hooks/session-start/index.cjs +65 -0
- package/hooks/typecheck/index.cjs +65 -0
- package/package.json +9 -4
- package/references/ui-pro-max-data/LICENSE-UI-PRO-MAX +21 -0
- package/references/ui-pro-max-data/charts.csv +26 -0
- package/references/ui-pro-max-data/colors.csv +162 -0
- package/references/ui-pro-max-data/styles.csv +85 -0
- package/references/ui-pro-max-data/typography.csv +74 -0
- package/references/ui-pro-max-data/ui-reasoning.csv +162 -0
- package/references/ui-pro-max-data/ux-guidelines.csv +100 -0
- package/skills/ba/SKILL.md +10 -0
- package/skills/brainstorm/SKILL.md +63 -1
- package/skills/completion-gate/SKILL.md +34 -1
- package/skills/context-engine/SKILL.md +13 -0
- package/skills/cook/SKILL.md +155 -5
- package/skills/debug/SKILL.md +56 -1
- package/skills/design/SKILL.md +11 -0
- package/skills/fix/SKILL.md +26 -1
- package/skills/mcp-builder/SKILL.md +48 -1
- package/skills/plan/SKILL.md +23 -6
- package/skills/review/SKILL.md +44 -5
- package/skills/review-intake/SKILL.md +17 -1
- package/skills/skill-forge/SKILL.md +38 -3
- package/skills/skill-router/SKILL.md +89 -7
- package/skills/team/SKILL.md +24 -1
- package/skills/test/SKILL.md +28 -1
- package/skills/verification/SKILL.md +98 -1
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: zalo-rate-guard
|
|
3
|
+
pack: "@rune/zalo"
|
|
4
|
+
description: Rate limiting patterns for Zalo OA and personal APIs ā token bucket per endpoint, exponential backoff, queue management, quota monitoring, anti-ban strategies.
|
|
5
|
+
model: sonnet
|
|
6
|
+
tools: "Read, Glob, Grep, Bash, Write, Edit"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# zalo-rate-guard
|
|
10
|
+
|
|
11
|
+
Shared rate limiting layer for both Track A (OA API) and Track B (Personal via zca-js).
|
|
12
|
+
Zalo has **undocumented rate limits** ā no official RPM/QPM numbers published.
|
|
13
|
+
Exceeding limits: throttled (429) ā warned ā OA suspended / account banned.
|
|
14
|
+
Neither `zalo-php-sdk`, `zalo-java-sdk`, nor `zca-js` implement any rate limiting.
|
|
15
|
+
This skill fills that gap.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Estimated Safe Limits
|
|
20
|
+
|
|
21
|
+
**Track A ā OA API:**
|
|
22
|
+
|
|
23
|
+
| Endpoint | Safe RPM | Burst | Notes |
|
|
24
|
+
|----------|----------|-------|-------|
|
|
25
|
+
| Send CS message | 200 | 10 | Per OA, includes all message types |
|
|
26
|
+
| Send broadcast | 50 | 5 | Monthly quota based on follower count |
|
|
27
|
+
| Get user profile | 300 | 20 | Cacheable ā use name cache |
|
|
28
|
+
| Get follower list | 100 | 10 | Paginated, cache results |
|
|
29
|
+
| Upload media | 60 | 5 | Large payloads, slower |
|
|
30
|
+
| Global | 500 | 30 | Total across all endpoints |
|
|
31
|
+
|
|
32
|
+
**Track B ā Personal (zca-js):**
|
|
33
|
+
|
|
34
|
+
| Action | Safe RPM | Burst | Notes |
|
|
35
|
+
|--------|----------|-------|-------|
|
|
36
|
+
| Send message (DM) | 30 | 5 | Much lower than OA ā personal account |
|
|
37
|
+
| Send message (group) | 20 | 3 | Groups are more scrutinized |
|
|
38
|
+
| Friend operations | 10 | 2 | Add/remove friend is very sensitive |
|
|
39
|
+
| Profile lookups | 60 | 10 | Less sensitive, still cache |
|
|
40
|
+
| Global | 100 | 15 | Err on side of caution |
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Token Bucket Implementation
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import PQueue from 'p-queue'
|
|
48
|
+
|
|
49
|
+
interface RateLimitConfig {
|
|
50
|
+
rpm: number // requests per minute
|
|
51
|
+
burst: number // max concurrent
|
|
52
|
+
retryAfter: number // ms to wait on 429
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const LIMITS: Record<string, RateLimitConfig> = {
|
|
56
|
+
'oa:send_message': { rpm: 200, burst: 10, retryAfter: 5000 },
|
|
57
|
+
'oa:broadcast': { rpm: 50, burst: 5, retryAfter: 10000 },
|
|
58
|
+
'oa:get_profile': { rpm: 300, burst: 20, retryAfter: 3000 },
|
|
59
|
+
'oa:upload': { rpm: 60, burst: 5, retryAfter: 5000 },
|
|
60
|
+
'personal:send_dm': { rpm: 30, burst: 5, retryAfter: 10000 },
|
|
61
|
+
'personal:send_grp': { rpm: 20, burst: 3, retryAfter: 15000 },
|
|
62
|
+
'personal:friend': { rpm: 10, burst: 2, retryAfter: 30000 },
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class ZaloRateLimiter {
|
|
66
|
+
private queues = new Map<string, PQueue>()
|
|
67
|
+
|
|
68
|
+
constructor() {
|
|
69
|
+
for (const [key, config] of Object.entries(LIMITS)) {
|
|
70
|
+
this.queues.set(key, new PQueue({
|
|
71
|
+
concurrency: config.burst,
|
|
72
|
+
intervalCap: config.rpm,
|
|
73
|
+
interval: 60_000, // per minute window
|
|
74
|
+
}))
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async execute<T>(endpoint: string, fn: () => Promise<T>): Promise<T> {
|
|
79
|
+
const queue = this.queues.get(endpoint)
|
|
80
|
+
if (!queue) throw new Error(`Unknown endpoint: ${endpoint}`)
|
|
81
|
+
return queue.add(fn) as Promise<T>
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
queueSize(endpoint: string): number {
|
|
85
|
+
return this.queues.get(endpoint)?.size ?? 0
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
pending(endpoint: string): number {
|
|
89
|
+
return this.queues.get(endpoint)?.pending ?? 0
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Exponential Backoff on 429
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
export async function withBackoff<T>(
|
|
100
|
+
fn: () => Promise<T>,
|
|
101
|
+
maxRetries = 3,
|
|
102
|
+
baseDelay = 1000
|
|
103
|
+
): Promise<T> {
|
|
104
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
105
|
+
try {
|
|
106
|
+
return await fn()
|
|
107
|
+
} catch (error: any) {
|
|
108
|
+
const isRateLimit = error?.status === 429 || error?.error_code === 429
|
|
109
|
+
if (!isRateLimit || attempt === maxRetries) throw error
|
|
110
|
+
const delay = baseDelay * Math.pow(2, attempt) + Math.random() * 1000
|
|
111
|
+
console.warn(`[zalo-rate-guard] Rate limited. Retry ${attempt + 1}/${maxRetries} in ${Math.round(delay)}ms`)
|
|
112
|
+
await new Promise(r => setTimeout(r, delay))
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
throw new Error('Unreachable')
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Quota Monitoring (OA Broadcast)
|
|
122
|
+
|
|
123
|
+
Broadcast quota is a **hard monthly limit** ā exceeding it silently drops messages, no error returned.
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
interface QuotaTracker {
|
|
127
|
+
monthly_limit: number // based on follower count + OA level
|
|
128
|
+
used: number
|
|
129
|
+
resets_at: Date // 1st of each month
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function canBroadcast(tracker: QuotaTracker, recipientCount: number): boolean {
|
|
133
|
+
const remaining = tracker.monthly_limit - tracker.used
|
|
134
|
+
if (recipientCount > remaining) {
|
|
135
|
+
console.error(
|
|
136
|
+
`[zalo-rate-guard] Broadcast quota insufficient: need ${recipientCount}, have ${remaining}/${tracker.monthly_limit}`
|
|
137
|
+
)
|
|
138
|
+
return false
|
|
139
|
+
}
|
|
140
|
+
return true
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function trackBroadcastUsed(tracker: QuotaTracker, sent: number): QuotaTracker {
|
|
144
|
+
return { ...tracker, used: tracker.used + sent }
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Integration Pattern
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// Singleton ā shared across the app
|
|
154
|
+
export const limiter = new ZaloRateLimiter()
|
|
155
|
+
|
|
156
|
+
// Track A: OA message send with rate limiting
|
|
157
|
+
export async function sendOaMessage(userId: string, text: string) {
|
|
158
|
+
return limiter.execute('oa:send_message', () =>
|
|
159
|
+
withBackoff(() =>
|
|
160
|
+
oaApiCall('/message/cs', {
|
|
161
|
+
recipient: { user_id: userId },
|
|
162
|
+
message: { text },
|
|
163
|
+
})
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Track B: Personal DM with rate limiting + human jitter
|
|
169
|
+
export async function sendPersonalMessage(threadId: string, text: string) {
|
|
170
|
+
const jitter = 500 + Math.random() * 1500 // 500ā2000ms
|
|
171
|
+
await new Promise(r => setTimeout(r, jitter))
|
|
172
|
+
return limiter.execute('personal:send_dm', () =>
|
|
173
|
+
withBackoff(() => api.sendMessage(text, threadId, 'User'))
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Track B: Friend operation ā highest-risk, extra jitter
|
|
178
|
+
export async function addFriend(userId: string) {
|
|
179
|
+
const jitter = 2000 + Math.random() * 3000 // 2ā5s
|
|
180
|
+
await new Promise(r => setTimeout(r, jitter))
|
|
181
|
+
return limiter.execute('personal:friend', () =>
|
|
182
|
+
withBackoff(() => api.sendFriendRequest(userId), 2, 5000)
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Anti-Ban Strategies
|
|
190
|
+
|
|
191
|
+
**Track A (OA):**
|
|
192
|
+
1. Stay under safe RPM limits (table above)
|
|
193
|
+
2. Exponential backoff on ALL 429 responses ā never retry immediately
|
|
194
|
+
3. Cache user profiles ā avoid repeated lookups for the same user
|
|
195
|
+
4. Spread broadcasts over time ā don't burst the entire follower list at once
|
|
196
|
+
5. Monitor quota before each broadcast batch ā stop before hitting monthly limit
|
|
197
|
+
6. Use `appsecret_proof` on all requests ā proves you're the legitimate app owner
|
|
198
|
+
|
|
199
|
+
**Track B (Personal):**
|
|
200
|
+
1. Much lower limits than OA ā personal accounts are watched more closely
|
|
201
|
+
2. Add human-like jitter: 500ā2000ms random delay between messages (not optional)
|
|
202
|
+
3. Avoid 3ā6 AM (VN timezone) ā traffic at those hours flags automated activity
|
|
203
|
+
4. Never change profile info programmatically ā triggers manual review
|
|
204
|
+
5. Friend operations are highest-risk ā max 10 RPM, prefer lower in practice
|
|
205
|
+
6. Keep sessions long-lived ā repeated login/logout is a strong ban signal
|
|
206
|
+
7. Use a consistent device fingerprint (`userAgent` + `IMEI`) per account
|
|
207
|
+
8. On `DuplicateConnection` (error 3000): wait 30s before reconnecting, never spam reconnects
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Sharp Edges
|
|
212
|
+
|
|
213
|
+
- Rate limits are **estimated** ā Zalo does not publish official numbers; treat all figures as conservative targets
|
|
214
|
+
- `p-queue` `intervalCap` applies per window, not per request ā test behavior under burst
|
|
215
|
+
- 429 without backoff = accelerating toward ban, not slowing down
|
|
216
|
+
- Broadcast quota overflow **silently drops messages** ā no 429, no error, just lost sends
|
|
217
|
+
- Stale cached profile data is acceptable; hitting rate limits for fresh data is not
|
|
218
|
+
- Personal account friend operations are the single highest-risk action ā handle with care
|
|
219
|
+
- Human jitter for personal track is a survival strategy, not a nice-to-have
|
package/hooks/.gitkeep
ADDED
|
File without changes
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Rune Auto-Format Hook
|
|
2
|
+
// PostToolUse hook on Edit/Write ā runs Prettier on JS/TS files after modification
|
|
3
|
+
//
|
|
4
|
+
// Only runs if Prettier is available in the project.
|
|
5
|
+
// Silent pass-through if not applicable.
|
|
6
|
+
|
|
7
|
+
const { execSync } = require('child_process');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const input = JSON.parse(process.env.CLAUDE_TOOL_INPUT || '{}');
|
|
11
|
+
const filePath = input.file_path || input.filePath || '';
|
|
12
|
+
|
|
13
|
+
// Only format JS/TS/JSON/CSS files
|
|
14
|
+
if (!/\.(js|jsx|ts|tsx|json|css|scss|md|html|yaml|yml)$/i.test(filePath)) {
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Check if file exists and is within a project with Prettier
|
|
19
|
+
const dir = path.dirname(filePath);
|
|
20
|
+
|
|
21
|
+
// Try to find prettier in the project
|
|
22
|
+
let hasPrettier = false;
|
|
23
|
+
try {
|
|
24
|
+
execSync('npx prettier --version', { cwd: dir, encoding: 'utf-8', stdio: 'pipe', timeout: 5000 });
|
|
25
|
+
hasPrettier = true;
|
|
26
|
+
} catch {
|
|
27
|
+
// No Prettier available ā skip silently
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!hasPrettier) {
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Run Prettier on the file
|
|
35
|
+
try {
|
|
36
|
+
execSync(`npx prettier --write "${filePath}"`, {
|
|
37
|
+
cwd: dir,
|
|
38
|
+
encoding: 'utf-8',
|
|
39
|
+
stdio: 'pipe',
|
|
40
|
+
timeout: 10000,
|
|
41
|
+
});
|
|
42
|
+
// Silent success ā formatted files are seamless
|
|
43
|
+
} catch (e) {
|
|
44
|
+
// Prettier failed ā non-critical, just warn
|
|
45
|
+
console.log(`[Rune auto-format] Prettier failed on ${path.basename(filePath)}: ${e.message.split('\n')[0]}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
process.exit(0);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Rune Context Watch Hook
|
|
2
|
+
// Lightweight tool call counter ā detects context pressure and suggests rune:context-engine
|
|
3
|
+
// Runs as PreToolUse hook on Edit/Write (high-cost operations)
|
|
4
|
+
//
|
|
5
|
+
// H3 Intelligence: also tracks tool type distribution and session start timestamp
|
|
6
|
+
// for metrics aggregation at session end.
|
|
7
|
+
//
|
|
8
|
+
// Uses a temp file counter (survives across hook invocations within same session).
|
|
9
|
+
// Zero overhead: just reads/increments a number. No token cost.
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
|
|
15
|
+
// Counter file scoped to current working directory (hash of cwd)
|
|
16
|
+
const cwd = process.cwd();
|
|
17
|
+
const hash = Buffer.from(cwd).toString('base64url').slice(0, 16);
|
|
18
|
+
const counterFile = path.join(os.tmpdir(), `rune-context-watch-${hash}.json`);
|
|
19
|
+
|
|
20
|
+
// Thresholds
|
|
21
|
+
const FIRST_WARNING = 40;
|
|
22
|
+
const REPEAT_INTERVAL = 20;
|
|
23
|
+
const CRITICAL_THRESHOLD = 80;
|
|
24
|
+
|
|
25
|
+
// Detect tool type from environment
|
|
26
|
+
const toolName = process.env.CLAUDE_TOOL_NAME || 'unknown';
|
|
27
|
+
|
|
28
|
+
// Read current state
|
|
29
|
+
let state = { count: 0, lastWarning: 0, sessionStart: null, toolCounts: {} };
|
|
30
|
+
try {
|
|
31
|
+
const raw = fs.readFileSync(counterFile, 'utf-8');
|
|
32
|
+
state = JSON.parse(raw);
|
|
33
|
+
// Ensure toolCounts exists (upgrade from old format)
|
|
34
|
+
if (!state.toolCounts) state.toolCounts = {};
|
|
35
|
+
if (!state.sessionStart) state.sessionStart = new Date().toISOString();
|
|
36
|
+
} catch {
|
|
37
|
+
// First run or corrupted ā start fresh
|
|
38
|
+
state.sessionStart = new Date().toISOString();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Increment total and per-tool counters
|
|
42
|
+
state.count += 1;
|
|
43
|
+
state.toolCounts[toolName] = (state.toolCounts[toolName] || 0) + 1;
|
|
44
|
+
|
|
45
|
+
// Check thresholds
|
|
46
|
+
const count = state.count;
|
|
47
|
+
const sinceLast = count - state.lastWarning;
|
|
48
|
+
|
|
49
|
+
if (count >= CRITICAL_THRESHOLD && sinceLast >= REPEAT_INTERVAL) {
|
|
50
|
+
console.log(`\nš“ [Rune context-watch] ${count} tool calls ā context likely RED (>85%).`);
|
|
51
|
+
console.log(' RECOMMENDED: Invoke rune:context-engine for state save + /compact.');
|
|
52
|
+
console.log(' Risk: auto-compaction may lose critical decisions without state save.\n');
|
|
53
|
+
state.lastWarning = count;
|
|
54
|
+
} else if (count >= FIRST_WARNING && sinceLast >= REPEAT_INTERVAL) {
|
|
55
|
+
console.log(`\nš” [Rune context-watch] ${count} tool calls ā context filling up.`);
|
|
56
|
+
console.log(' Consider invoking rune:context-engine at the next logical boundary.');
|
|
57
|
+
console.log(' Or run /compact manually if at a good stopping point.\n');
|
|
58
|
+
state.lastWarning = count;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Persist
|
|
62
|
+
try {
|
|
63
|
+
fs.writeFileSync(counterFile, JSON.stringify(state));
|
|
64
|
+
} catch {
|
|
65
|
+
// Non-critical ā counter resets next run
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
process.exit(0);
|
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "startup|resume|clear|compact",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cjs\" session-start",
|
|
10
|
+
"async": false
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"PreToolUse": [
|
|
16
|
+
{
|
|
17
|
+
"matcher": "Read|Write|Edit",
|
|
18
|
+
"hooks": [
|
|
19
|
+
{
|
|
20
|
+
"type": "command",
|
|
21
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cjs\" pre-tool-guard",
|
|
22
|
+
"async": false
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"matcher": "Edit|Write",
|
|
28
|
+
"hooks": [
|
|
29
|
+
{
|
|
30
|
+
"type": "command",
|
|
31
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cjs\" context-watch",
|
|
32
|
+
"async": true
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"matcher": "Bash",
|
|
38
|
+
"hooks": [
|
|
39
|
+
{
|
|
40
|
+
"type": "command",
|
|
41
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cjs\" secrets-scan",
|
|
42
|
+
"async": false
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"PostToolUse": [
|
|
48
|
+
{
|
|
49
|
+
"matcher": "Edit|Write",
|
|
50
|
+
"hooks": [
|
|
51
|
+
{
|
|
52
|
+
"type": "command",
|
|
53
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cjs\" auto-format",
|
|
54
|
+
"async": true
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"type": "command",
|
|
58
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cjs\" typecheck",
|
|
59
|
+
"async": true
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"matcher": "Skill",
|
|
65
|
+
"hooks": [
|
|
66
|
+
{
|
|
67
|
+
"type": "command",
|
|
68
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cjs\" metrics-collector",
|
|
69
|
+
"async": true
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
],
|
|
74
|
+
"PreCompact": [
|
|
75
|
+
{
|
|
76
|
+
"matcher": ".*",
|
|
77
|
+
"hooks": [
|
|
78
|
+
{
|
|
79
|
+
"type": "command",
|
|
80
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cjs\" pre-compact",
|
|
81
|
+
"async": false
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
],
|
|
86
|
+
"Stop": [
|
|
87
|
+
{
|
|
88
|
+
"matcher": ".*",
|
|
89
|
+
"hooks": [
|
|
90
|
+
{
|
|
91
|
+
"type": "command",
|
|
92
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cjs\" post-session-reflect",
|
|
93
|
+
"async": false
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Rune Metrics Collector Hook
|
|
2
|
+
// PostToolUse on Skill ā captures skill invocations for H3 mesh analytics
|
|
3
|
+
// Append-only JSONL to tmpdir. Flushed to .rune/metrics/ at session end.
|
|
4
|
+
// Async: true ā never blocks skill execution.
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
|
|
10
|
+
const cwd = process.cwd();
|
|
11
|
+
const hash = Buffer.from(cwd).toString('base64url').slice(0, 16);
|
|
12
|
+
const metricsFile = path.join(os.tmpdir(), `rune-metrics-${hash}.jsonl`);
|
|
13
|
+
|
|
14
|
+
// Extract skill name from tool input
|
|
15
|
+
const toolInput = process.env.CLAUDE_TOOL_INPUT || '';
|
|
16
|
+
let skillName = 'unknown';
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(toolInput);
|
|
19
|
+
// Skill tool input has { skill: "rune:cook" } or { skill: "cook" }
|
|
20
|
+
const raw = parsed.skill || parsed.name || '';
|
|
21
|
+
skillName = raw.replace(/^rune:/, '');
|
|
22
|
+
} catch {
|
|
23
|
+
// If not JSON, try raw string match
|
|
24
|
+
const match = toolInput.match(/(?:rune:)?([a-z][\w-]*)/i);
|
|
25
|
+
if (match) skillName = match[1];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (skillName && skillName !== 'unknown') {
|
|
29
|
+
const entry = JSON.stringify({
|
|
30
|
+
ts: new Date().toISOString(),
|
|
31
|
+
skill: skillName,
|
|
32
|
+
event: 'invoke'
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
fs.appendFileSync(metricsFile, entry + '\n');
|
|
37
|
+
} catch {
|
|
38
|
+
// Non-critical ā metrics are best-effort
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
process.exit(0);
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// Rune Post-Session Reflect Hook
|
|
2
|
+
// 1. Flushes session metrics from tmpdir to .rune/metrics/ (H3 Intelligence)
|
|
3
|
+
// 2. Displays structured self-review checklist at session end (Stop event)
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
|
|
9
|
+
const cwd = process.cwd();
|
|
10
|
+
const hash = Buffer.from(cwd).toString('base64url').slice(0, 16);
|
|
11
|
+
|
|
12
|
+
// === H3: Flush Session Metrics ===
|
|
13
|
+
|
|
14
|
+
const metricsJsonl = path.join(os.tmpdir(), `rune-metrics-${hash}.jsonl`);
|
|
15
|
+
const counterFile = path.join(os.tmpdir(), `rune-context-watch-${hash}.json`);
|
|
16
|
+
const runeMetricsDir = path.join(cwd, '.rune', 'metrics');
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
flushMetrics();
|
|
20
|
+
} catch (e) {
|
|
21
|
+
// Metrics flush is best-effort ā never block session end
|
|
22
|
+
// Silently ignore errors
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function flushMetrics() {
|
|
26
|
+
// Read session skill invocations from tmpdir JSONL
|
|
27
|
+
let skillEvents = [];
|
|
28
|
+
if (fs.existsSync(metricsJsonl)) {
|
|
29
|
+
const lines = fs.readFileSync(metricsJsonl, 'utf-8').trim().split('\n').filter(Boolean);
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
try { skillEvents.push(JSON.parse(line)); } catch { /* skip malformed */ }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Read context-watch state for tool counts and session timing
|
|
36
|
+
let watchState = { count: 0, sessionStart: null, toolCounts: {} };
|
|
37
|
+
if (fs.existsSync(counterFile)) {
|
|
38
|
+
try {
|
|
39
|
+
watchState = JSON.parse(fs.readFileSync(counterFile, 'utf-8'));
|
|
40
|
+
} catch { /* use defaults */ }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Nothing to flush if no data
|
|
44
|
+
if (skillEvents.length === 0 && watchState.count === 0) return;
|
|
45
|
+
|
|
46
|
+
// Ensure .rune/metrics/ exists
|
|
47
|
+
fs.mkdirSync(runeMetricsDir, { recursive: true });
|
|
48
|
+
|
|
49
|
+
const now = new Date().toISOString();
|
|
50
|
+
const sessionStart = watchState.sessionStart || now;
|
|
51
|
+
const durationMin = Math.round((new Date(now) - new Date(sessionStart)) / 60000);
|
|
52
|
+
|
|
53
|
+
// Build skill usage map
|
|
54
|
+
const skillCounts = {};
|
|
55
|
+
const skillChain = [];
|
|
56
|
+
for (const evt of skillEvents) {
|
|
57
|
+
skillCounts[evt.skill] = (skillCounts[evt.skill] || 0) + 1;
|
|
58
|
+
skillChain.push(evt.skill);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Determine primary skill (most invoked)
|
|
62
|
+
const primarySkill = Object.entries(skillCounts)
|
|
63
|
+
.sort((a, b) => b[1] - a[1])[0]?.[0] || 'none';
|
|
64
|
+
|
|
65
|
+
// Generate session ID
|
|
66
|
+
const sessionId = `s-${now.slice(0, 10).replace(/-/g, '')}-${now.slice(11, 19).replace(/:/g, '')}`;
|
|
67
|
+
|
|
68
|
+
// 1. Append to sessions.jsonl
|
|
69
|
+
const sessionEntry = {
|
|
70
|
+
id: sessionId,
|
|
71
|
+
date: now.slice(0, 10),
|
|
72
|
+
duration_min: durationMin,
|
|
73
|
+
tool_calls: watchState.count,
|
|
74
|
+
tool_distribution: watchState.toolCounts,
|
|
75
|
+
skill_invocations: skillEvents.length,
|
|
76
|
+
skills_used: Object.keys(skillCounts),
|
|
77
|
+
primary_skill: primarySkill
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const sessionsFile = path.join(runeMetricsDir, 'sessions.jsonl');
|
|
81
|
+
fs.appendFileSync(sessionsFile, JSON.stringify(sessionEntry) + '\n');
|
|
82
|
+
|
|
83
|
+
// Cap at 100 sessions (rotate oldest)
|
|
84
|
+
try {
|
|
85
|
+
const allLines = fs.readFileSync(sessionsFile, 'utf-8').trim().split('\n').filter(Boolean);
|
|
86
|
+
if (allLines.length > 100) {
|
|
87
|
+
fs.writeFileSync(sessionsFile, allLines.slice(-100).join('\n') + '\n');
|
|
88
|
+
}
|
|
89
|
+
} catch { /* cap is best-effort */ }
|
|
90
|
+
|
|
91
|
+
// 2. Merge into skills.json (running totals)
|
|
92
|
+
const skillsFile = path.join(runeMetricsDir, 'skills.json');
|
|
93
|
+
let skillsData = { version: 1, updated: now, skills: {} };
|
|
94
|
+
if (fs.existsSync(skillsFile)) {
|
|
95
|
+
try {
|
|
96
|
+
skillsData = JSON.parse(fs.readFileSync(skillsFile, 'utf-8'));
|
|
97
|
+
} catch { /* start fresh */ }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (const [skill, count] of Object.entries(skillCounts)) {
|
|
101
|
+
if (!skillsData.skills[skill]) {
|
|
102
|
+
skillsData.skills[skill] = { total_invocations: 0, last_used: now.slice(0, 10) };
|
|
103
|
+
}
|
|
104
|
+
skillsData.skills[skill].total_invocations += count;
|
|
105
|
+
skillsData.skills[skill].last_used = now.slice(0, 10);
|
|
106
|
+
}
|
|
107
|
+
skillsData.updated = now;
|
|
108
|
+
|
|
109
|
+
fs.writeFileSync(skillsFile, JSON.stringify(skillsData, null, 2) + '\n');
|
|
110
|
+
|
|
111
|
+
// 3. Append to chains.jsonl
|
|
112
|
+
if (skillChain.length > 0) {
|
|
113
|
+
const chainsFile = path.join(runeMetricsDir, 'chains.jsonl');
|
|
114
|
+
const chainEntry = {
|
|
115
|
+
session: sessionId,
|
|
116
|
+
chain: skillChain,
|
|
117
|
+
depth: skillChain.length
|
|
118
|
+
};
|
|
119
|
+
fs.appendFileSync(chainsFile, JSON.stringify(chainEntry) + '\n');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 4. Cleanup tmpdir files
|
|
123
|
+
try { fs.unlinkSync(metricsJsonl); } catch { /* already gone */ }
|
|
124
|
+
// Note: counterFile is cleaned by session-start hook on next session
|
|
125
|
+
|
|
126
|
+
// Report metrics flush
|
|
127
|
+
const skillList = Object.entries(skillCounts)
|
|
128
|
+
.sort((a, b) => b[1] - a[1])
|
|
129
|
+
.map(([s, c]) => `${s}(${c})`)
|
|
130
|
+
.join(', ');
|
|
131
|
+
|
|
132
|
+
console.log(`\nš [Rune metrics] Session ${sessionId} ā ${durationMin}min, ${watchState.count} tool calls, ${skillEvents.length} skill invocations`);
|
|
133
|
+
if (skillList) console.log(` Skills: ${skillList}`);
|
|
134
|
+
console.log(` Saved to .rune/metrics/\n`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// === Original: Verification Checklist ===
|
|
138
|
+
|
|
139
|
+
console.log(`
|
|
140
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
141
|
+
ā Rune Session End ā Verification Checklist ā
|
|
142
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
|
|
143
|
+
ā Before closing this session, confirm: ā
|
|
144
|
+
ā ā
|
|
145
|
+
ā ā” All TodoWrite tasks marked complete? ā
|
|
146
|
+
ā ā” Tests ran and passing? ā
|
|
147
|
+
ā ā” No hardcoded secrets introduced? ā
|
|
148
|
+
ā ā” If schema changed: migration + rollback exist? ā
|
|
149
|
+
ā ā” Verification ran (lint + types + build)? ā
|
|
150
|
+
ā ā
|
|
151
|
+
ā If any item is unclear ā address it now. ā
|
|
152
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
153
|
+
`);
|