@rune-kit/rune 2.10.0 → 2.12.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/LICENSE +21 -21
- package/README.md +65 -6
- package/commands/rune.md +168 -168
- package/compiler/__tests__/detect-invariants.test.js +136 -0
- package/compiler/__tests__/doctor-mesh.test.js +229 -0
- package/compiler/__tests__/hook-dispatch.test.js +91 -0
- package/compiler/__tests__/hooks-antigravity.test.js +118 -0
- package/compiler/__tests__/hooks-cursor.test.js +139 -0
- package/compiler/__tests__/hooks-install.test.js +305 -0
- package/compiler/__tests__/hooks-merge.test.js +204 -0
- package/compiler/__tests__/hooks-tiers.test.js +519 -0
- package/compiler/__tests__/hooks-windsurf.test.js +115 -0
- package/compiler/__tests__/inject-claude-md.test.js +152 -0
- package/compiler/__tests__/load-invariants.test.js +408 -0
- package/compiler/__tests__/onboard-invariants.test.js +240 -0
- package/compiler/adapters/hooks/antigravity.js +140 -0
- package/compiler/adapters/hooks/claude.js +166 -0
- package/compiler/adapters/hooks/cursor.js +191 -0
- package/compiler/adapters/hooks/index.js +82 -0
- package/compiler/adapters/hooks/tier-emitter.js +182 -0
- package/compiler/adapters/hooks/windsurf.js +202 -0
- package/compiler/bin/rune.js +196 -6
- package/compiler/commands/hook-dispatch.js +87 -0
- package/compiler/commands/hooks/install.js +120 -0
- package/compiler/commands/hooks/merge.js +211 -0
- package/compiler/commands/hooks/presets.js +116 -0
- package/compiler/commands/hooks/status.js +112 -0
- package/compiler/commands/hooks/tiers.js +221 -0
- package/compiler/commands/hooks/uninstall.js +94 -0
- package/compiler/doctor.js +236 -0
- package/contexts/dev.md +34 -34
- package/contexts/research.md +43 -43
- package/contexts/review.md +55 -55
- package/extensions/ai-ml/PACK.md +88 -88
- package/extensions/ai-ml/skills/ai-agents.md +172 -172
- package/extensions/ai-ml/skills/code-sandbox.md +187 -187
- package/extensions/ai-ml/skills/deep-research.md +146 -146
- package/extensions/ai-ml/skills/embedding-search.md +66 -66
- package/extensions/ai-ml/skills/fine-tuning-guide.md +74 -74
- package/extensions/ai-ml/skills/llm-architect.md +125 -125
- package/extensions/ai-ml/skills/llm-integration.md +64 -64
- package/extensions/ai-ml/skills/prompt-patterns.md +72 -72
- package/extensions/ai-ml/skills/rag-patterns.md +66 -66
- package/extensions/ai-ml/skills/web-extraction.md +114 -114
- package/extensions/analytics/PACK.md +92 -92
- package/extensions/analytics/skills/ab-testing.md +72 -72
- package/extensions/analytics/skills/dashboard-patterns.md +83 -83
- package/extensions/analytics/skills/data-validation.md +68 -68
- package/extensions/analytics/skills/funnel-analysis.md +81 -81
- package/extensions/analytics/skills/sql-patterns.md +57 -57
- package/extensions/analytics/skills/statistical-analysis.md +79 -79
- package/extensions/analytics/skills/tracking-setup.md +71 -71
- package/extensions/backend/PACK.md +104 -104
- package/extensions/backend/skills/api-patterns.md +84 -84
- package/extensions/backend/skills/async-pipeline.md +193 -193
- package/extensions/backend/skills/auth-patterns.md +97 -97
- package/extensions/backend/skills/background-jobs.md +133 -133
- package/extensions/backend/skills/caching-patterns.md +108 -108
- package/extensions/backend/skills/cli-generation.md +133 -133
- package/extensions/backend/skills/database-patterns.md +87 -87
- package/extensions/backend/skills/middleware-patterns.md +104 -104
- package/extensions/chrome-ext/PACK.md +93 -93
- package/extensions/chrome-ext/skills/cws-preflight.md +143 -143
- package/extensions/chrome-ext/skills/cws-publish.md +104 -104
- package/extensions/chrome-ext/skills/ext-ai-integration.md +251 -251
- package/extensions/chrome-ext/skills/ext-messaging.md +139 -139
- package/extensions/chrome-ext/skills/ext-storage.md +133 -133
- package/extensions/chrome-ext/skills/mv3-scaffold.md +164 -164
- package/extensions/content/PACK.md +96 -96
- package/extensions/content/skills/blog-patterns.md +88 -88
- package/extensions/content/skills/cms-integration.md +131 -131
- package/extensions/content/skills/content-scoring.md +107 -107
- package/extensions/content/skills/i18n.md +83 -83
- package/extensions/content/skills/mdx-authoring.md +137 -137
- package/extensions/content/skills/reference.md +1014 -1014
- package/extensions/content/skills/seo-patterns.md +67 -67
- package/extensions/content/skills/video-repurpose.md +153 -153
- package/extensions/devops/PACK.md +101 -101
- package/extensions/devops/skills/chaos-testing.md +67 -67
- package/extensions/devops/skills/ci-cd.md +75 -75
- package/extensions/devops/skills/docker.md +58 -58
- package/extensions/devops/skills/edge-serverless.md +163 -163
- package/extensions/devops/skills/infra-as-code.md +158 -158
- package/extensions/devops/skills/kubernetes.md +110 -110
- package/extensions/devops/skills/monitoring.md +57 -57
- package/extensions/devops/skills/server-setup.md +64 -64
- package/extensions/devops/skills/ssl-domain.md +42 -42
- package/extensions/ecommerce/PACK.md +116 -116
- package/extensions/ecommerce/skills/cart-system.md +79 -79
- package/extensions/ecommerce/skills/inventory-mgmt.md +102 -102
- package/extensions/ecommerce/skills/order-management.md +126 -126
- package/extensions/ecommerce/skills/payment-integration.md +472 -472
- package/extensions/ecommerce/skills/shopify-dev.md +69 -69
- package/extensions/ecommerce/skills/subscription-billing.md +93 -93
- package/extensions/ecommerce/skills/tax-compliance.md +117 -117
- package/extensions/gamedev/PACK.md +142 -142
- package/extensions/gamedev/skills/asset-pipeline.md +74 -74
- package/extensions/gamedev/skills/audio-system.md +129 -129
- package/extensions/gamedev/skills/camera-system.md +87 -87
- package/extensions/gamedev/skills/ecs.md +98 -98
- package/extensions/gamedev/skills/game-loops.md +72 -72
- package/extensions/gamedev/skills/input-system.md +199 -199
- package/extensions/gamedev/skills/multiplayer.md +180 -180
- package/extensions/gamedev/skills/particles.md +105 -105
- package/extensions/gamedev/skills/physics-engine.md +89 -89
- package/extensions/gamedev/skills/scene-management.md +146 -146
- package/extensions/gamedev/skills/threejs-patterns.md +90 -90
- package/extensions/gamedev/skills/webgl.md +71 -71
- package/extensions/mobile/PACK.md +106 -106
- package/extensions/mobile/skills/app-store-connect.md +152 -152
- package/extensions/mobile/skills/app-store-prep.md +66 -66
- package/extensions/mobile/skills/deep-linking.md +109 -109
- package/extensions/mobile/skills/flutter.md +60 -60
- package/extensions/mobile/skills/ios-build-pipeline.md +142 -142
- package/extensions/mobile/skills/native-bridge.md +66 -66
- package/extensions/mobile/skills/ota-updates.md +97 -97
- package/extensions/mobile/skills/push-notifications.md +111 -111
- package/extensions/mobile/skills/react-native.md +82 -82
- package/extensions/saas/PACK.md +116 -116
- package/extensions/saas/skills/billing-integration.md +200 -200
- package/extensions/saas/skills/feature-flags.md +130 -130
- package/extensions/saas/skills/multi-tenant.md +103 -103
- package/extensions/saas/skills/onboarding-flow.md +139 -139
- package/extensions/saas/skills/subscription-flow.md +95 -95
- package/extensions/saas/skills/team-management.md +144 -144
- package/extensions/security/PACK.md +99 -99
- package/extensions/security/skills/api-security.md +140 -140
- package/extensions/security/skills/compliance.md +68 -68
- package/extensions/security/skills/owasp-audit.md +64 -64
- package/extensions/security/skills/pentest-patterns.md +77 -77
- package/extensions/security/skills/secret-mgmt.md +65 -65
- package/extensions/security/skills/supply-chain.md +65 -65
- package/extensions/trading/PACK.md +80 -80
- package/extensions/trading/skills/chart-components.md +55 -55
- package/extensions/trading/skills/experiment-loop.md +125 -125
- package/extensions/trading/skills/fintech-patterns.md +47 -47
- package/extensions/trading/skills/indicator-library.md +58 -58
- package/extensions/trading/skills/quant-analysis.md +111 -111
- package/extensions/trading/skills/realtime-data.md +58 -58
- package/extensions/trading/skills/trade-logic.md +104 -104
- package/extensions/ui/PACK.md +130 -130
- package/extensions/ui/skills/a11y-audit.md +91 -91
- package/extensions/ui/skills/animation-patterns.md +127 -127
- package/extensions/ui/skills/component-patterns.md +100 -100
- package/extensions/ui/skills/design-decision.md +108 -108
- package/extensions/ui/skills/design-system.md +68 -68
- package/extensions/ui/skills/landing-patterns.md +155 -155
- package/extensions/ui/skills/palette-picker.md +173 -173
- package/extensions/ui/skills/react-health.md +90 -90
- package/extensions/ui/skills/type-system.md +125 -125
- package/extensions/ui/skills/web-vitals.md +153 -153
- package/extensions/zalo/PACK.md +145 -145
- package/extensions/zalo/skills/zalo-oa-mcp.md +317 -317
- package/extensions/zalo/skills/zalo-oa-messaging.md +429 -429
- package/extensions/zalo/skills/zalo-oa-setup.md +236 -236
- package/extensions/zalo/skills/zalo-oa-webhook.md +189 -189
- package/extensions/zalo/skills/zalo-personal-messaging.md +194 -194
- package/extensions/zalo/skills/zalo-personal-setup.md +153 -153
- package/extensions/zalo/skills/zalo-rate-guard.md +219 -219
- package/hooks/auto-format/index.cjs +48 -48
- package/hooks/hooks.json +111 -111
- package/hooks/post-session-reflect/index.cjs +189 -189
- package/hooks/pre-compact/index.cjs +95 -95
- package/hooks/run-hook.cmd +1 -1
- package/hooks/secrets-scan/index.cjs +100 -100
- package/hooks/session-start/index.cjs +71 -71
- package/hooks/typecheck/index.cjs +65 -65
- package/package.json +63 -63
- package/references/ui-pro-max-data/LICENSE-UI-PRO-MAX +21 -21
- package/references/ui-pro-max-data/charts.csv +26 -26
- package/references/ui-pro-max-data/colors.csv +161 -161
- package/references/ui-pro-max-data/styles.csv +68 -68
- package/references/ui-pro-max-data/typography.csv +74 -74
- package/references/ui-pro-max-data/ui-reasoning.csv +162 -162
- package/references/ui-pro-max-data/ux-guidelines.csv +99 -99
- package/skills/adversary/SKILL.md +283 -283
- package/skills/asset-creator/SKILL.md +157 -157
- package/skills/audit/SKILL.md +147 -2
- package/skills/autopsy/SKILL.md +335 -335
- package/skills/ba/SKILL.md +85 -1
- package/skills/brainstorm/SKILL.md +380 -342
- package/skills/browser-pilot/SKILL.md +169 -168
- package/skills/constraint-check/SKILL.md +165 -165
- package/skills/context-engine/SKILL.md +408 -404
- package/skills/cook/SKILL.md +917 -863
- package/skills/db/SKILL.md +273 -273
- package/skills/debug/SKILL.md +465 -465
- package/skills/dependency-doctor/SKILL.md +265 -235
- package/skills/deploy/SKILL.md +274 -231
- package/skills/design/DESIGN-REFERENCE.md +365 -365
- package/skills/design/SKILL.md +590 -589
- package/skills/doc-processor/SKILL.md +254 -254
- package/skills/docs/SKILL.md +374 -374
- package/skills/docs-seeker/SKILL.md +178 -177
- package/skills/fix/SKILL.md +332 -330
- package/skills/git/SKILL.md +339 -339
- package/skills/hallucination-guard/SKILL.md +220 -219
- package/skills/incident/SKILL.md +254 -253
- package/skills/integrity-check/SKILL.md +169 -169
- package/skills/journal/SKILL.md +241 -240
- package/skills/launch/SKILL.md +344 -344
- package/skills/logic-guardian/SKILL.md +269 -251
- package/skills/marketing/SKILL.md +351 -289
- package/skills/mcp-builder/SKILL.md +425 -425
- package/skills/neural-memory/SKILL.md +359 -362
- package/skills/onboard/SKILL.md +432 -403
- package/skills/onboard/references/invariants-template.md +76 -0
- package/skills/onboard/scripts/detect-invariants.js +439 -0
- package/skills/onboard/scripts/inject-claude-md.js +150 -0
- package/skills/onboard/scripts/onboard-invariants.js +194 -0
- package/skills/perf/SKILL.md +347 -346
- package/skills/plan/SKILL.md +435 -428
- package/skills/preflight/SKILL.md +415 -415
- package/skills/problem-solver/SKILL.md +380 -284
- package/skills/rescue/SKILL.md +474 -474
- package/skills/research/SKILL.md +4 -0
- package/skills/retro/SKILL.md +3 -1
- package/skills/review/SKILL.md +614 -588
- package/skills/review-intake/SKILL.md +249 -249
- package/skills/safeguard/SKILL.md +200 -200
- package/skills/sast/SKILL.md +190 -190
- package/skills/scaffold/SKILL.md +328 -287
- package/skills/scope-guard/SKILL.md +183 -180
- package/skills/scout/SKILL.md +269 -263
- package/skills/sentinel/SKILL.md +384 -381
- package/skills/sentinel-env/SKILL.md +254 -254
- package/skills/sequential-thinking/SKILL.md +234 -234
- package/skills/session-bridge/SKILL.md +595 -543
- package/skills/session-bridge/scripts/load-invariants.js +397 -0
- package/skills/skill-forge/SKILL.md +581 -581
- package/skills/skill-router/SKILL.md +3 -0
- package/skills/slides/SKILL.md +19 -0
- package/skills/surgeon/SKILL.md +215 -215
- package/skills/team/SKILL.md +557 -537
- package/skills/test/SKILL.md +620 -614
- package/skills/trend-scout/SKILL.md +145 -145
- package/skills/verification/SKILL.md +334 -326
- package/skills/video-creator/SKILL.md +201 -201
- package/skills/watchdog/SKILL.md +168 -168
- package/skills/worktree/SKILL.md +140 -140
|
@@ -1,139 +1,139 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: "ext-messaging"
|
|
3
|
-
pack: "@rune/chrome-ext"
|
|
4
|
-
description: "Typed message passing between popup, service worker, and content script — discriminated union message types, one-shot sendMessage, long-lived port connections for streaming, and Chrome 146+ error handling."
|
|
5
|
-
model: sonnet
|
|
6
|
-
tools: [Read, Edit, Write, Grep, Glob, Bash]
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# ext-messaging
|
|
10
|
-
|
|
11
|
-
Typed message passing between popup, service worker, and content script — discriminated union message types, one-shot `sendMessage`, long-lived port connections for streaming, and Chrome 146+ error handling. Prevents the #2 MV3 failure: untyped `any` messages, missing `return true` for async handlers, and ports used for single messages.
|
|
12
|
-
|
|
13
|
-
#### Workflow
|
|
14
|
-
|
|
15
|
-
**Step 1 — Identify message flows**
|
|
16
|
-
Use `Grep` to find existing `chrome.runtime.sendMessage`, `chrome.tabs.sendMessage`, and `chrome.runtime.connect` calls. Map the full message topology:
|
|
17
|
-
- popup → service worker (sendMessage — one-shot)
|
|
18
|
-
- service worker → content script (chrome.tabs.sendMessage — requires tab ID)
|
|
19
|
-
- content script → service worker (sendMessage — one-shot)
|
|
20
|
-
- service worker → popup (port — only if popup is open)
|
|
21
|
-
- streaming AI responses → use Port (not sendMessage — ports survive multiple sends)
|
|
22
|
-
|
|
23
|
-
**Step 2 — Define TypeScript message types**
|
|
24
|
-
Create `src/types/messages.ts` with a discriminated union covering all message directions. Each message type has a `type` literal and a strongly-typed `payload`. Response types are paired per message type.
|
|
25
|
-
|
|
26
|
-
**Step 3 — Implement chrome.runtime.sendMessage patterns**
|
|
27
|
-
For one-shot request/response between extension contexts. Key rules:
|
|
28
|
-
- Listener must `return true` if the response is sent asynchronously (inside a Promise or async function)
|
|
29
|
-
- `chrome.runtime.lastError` MUST be checked in the callback — unhandled errors throw in MV3
|
|
30
|
-
- Content scripts cannot receive messages via `chrome.runtime.sendMessage` — use `chrome.tabs.sendMessage` from the service worker with the target tab's ID
|
|
31
|
-
|
|
32
|
-
**Step 4 — Implement chrome.tabs.sendMessage (service worker → content)**
|
|
33
|
-
Service worker must resolve the target tab ID before sending. Use `chrome.tabs.query({ active: true, currentWindow: true })` or receive the tab ID from the content script's original message (sender.tab.id).
|
|
34
|
-
|
|
35
|
-
**Step 5 — Implement port-based long-lived connections**
|
|
36
|
-
Use `chrome.runtime.connect` for streaming scenarios (AI token streaming, progress updates, live data feeds). Ports stay open until explicitly disconnected. Each side must handle `port.onDisconnect` to clean up.
|
|
37
|
-
|
|
38
|
-
**Step 6 — Add Chrome 146+ error handling**
|
|
39
|
-
Chrome 146 changed message listener error behavior: uncaught errors in listeners now reject the Promise returned by `sendMessage` on the sender side. Wrap all listener handlers in try/catch and send structured error responses.
|
|
40
|
-
|
|
41
|
-
#### Example
|
|
42
|
-
|
|
43
|
-
```typescript
|
|
44
|
-
// src/types/messages.ts — discriminated union message types
|
|
45
|
-
export type ExtensionMessage =
|
|
46
|
-
| { type: 'SUMMARIZE_PAGE'; payload: { text: string; tabId: number } }
|
|
47
|
-
| { type: 'GET_SETTINGS'; payload: Record<string, never> }
|
|
48
|
-
| { type: 'UPDATE_SETTINGS'; payload: Partial<Settings> }
|
|
49
|
-
| { type: 'OPEN_SIDEBAR'; payload: { tabId: number } };
|
|
50
|
-
|
|
51
|
-
export type ExtensionResponse<T extends ExtensionMessage> =
|
|
52
|
-
T extends { type: 'SUMMARIZE_PAGE' } ? { summary: string; error?: string } :
|
|
53
|
-
T extends { type: 'GET_SETTINGS' } ? { settings: Settings } :
|
|
54
|
-
T extends { type: 'UPDATE_SETTINGS' } ? { ok: boolean } :
|
|
55
|
-
T extends { type: 'OPEN_SIDEBAR' } ? { ok: boolean } :
|
|
56
|
-
never;
|
|
57
|
-
|
|
58
|
-
export interface Settings {
|
|
59
|
-
useBuiltinAI: boolean;
|
|
60
|
-
externalApiKey: string;
|
|
61
|
-
maxLength: number;
|
|
62
|
-
}
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
```typescript
|
|
66
|
-
// background.ts — typed message handler
|
|
67
|
-
import type { ExtensionMessage } from './types/messages';
|
|
68
|
-
|
|
69
|
-
chrome.runtime.onMessage.addListener(
|
|
70
|
-
(message: ExtensionMessage, sender, sendResponse) => {
|
|
71
|
-
// CRITICAL: return true to keep channel open for async response
|
|
72
|
-
(async () => {
|
|
73
|
-
try {
|
|
74
|
-
switch (message.type) {
|
|
75
|
-
case 'SUMMARIZE_PAGE': {
|
|
76
|
-
const summary = await summarize(message.payload.text);
|
|
77
|
-
sendResponse({ summary });
|
|
78
|
-
break;
|
|
79
|
-
}
|
|
80
|
-
case 'GET_SETTINGS': {
|
|
81
|
-
const result = await chrome.storage.sync.get('settings');
|
|
82
|
-
sendResponse({ settings: result['settings'] as Settings });
|
|
83
|
-
break;
|
|
84
|
-
}
|
|
85
|
-
default:
|
|
86
|
-
sendResponse({ error: 'Unknown message type' });
|
|
87
|
-
}
|
|
88
|
-
} catch (err) {
|
|
89
|
-
// Chrome 146+: send error response instead of letting it throw
|
|
90
|
-
sendResponse({ error: String(err) });
|
|
91
|
-
}
|
|
92
|
-
})();
|
|
93
|
-
return true; // MUST return true — async response
|
|
94
|
-
}
|
|
95
|
-
);
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
```typescript
|
|
99
|
-
// Port-based streaming (service worker → sidebar/popup)
|
|
100
|
-
// background.ts
|
|
101
|
-
chrome.runtime.onConnect.addListener((port) => {
|
|
102
|
-
if (port.name !== 'ai-stream') return;
|
|
103
|
-
|
|
104
|
-
port.onMessage.addListener(async (message: { text: string }) => {
|
|
105
|
-
try {
|
|
106
|
-
const session = await chrome.aiLanguageModel.create();
|
|
107
|
-
const stream = session.promptStreaming(message.text);
|
|
108
|
-
|
|
109
|
-
for await (const chunk of stream) {
|
|
110
|
-
port.postMessage({ type: 'CHUNK', content: chunk });
|
|
111
|
-
}
|
|
112
|
-
port.postMessage({ type: 'DONE' });
|
|
113
|
-
session.destroy();
|
|
114
|
-
} catch (err) {
|
|
115
|
-
port.postMessage({ type: 'ERROR', error: String(err) });
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
port.onDisconnect.addListener(() => {
|
|
120
|
-
// cleanup — sidebar/popup was closed
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
// sidebar.ts — connect and stream
|
|
125
|
-
const port = chrome.runtime.connect({ name: 'ai-stream' });
|
|
126
|
-
port.postMessage({ text: selectedText });
|
|
127
|
-
|
|
128
|
-
port.onMessage.addListener((msg: { type: string; content?: string; error?: string }) => {
|
|
129
|
-
if (msg.type === 'CHUNK') appendToOutput(msg.content ?? '');
|
|
130
|
-
if (msg.type === 'DONE') finalizeOutput();
|
|
131
|
-
if (msg.type === 'ERROR') showError(msg.error ?? 'Unknown error');
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
port.onDisconnect.addListener(() => {
|
|
135
|
-
if (chrome.runtime.lastError) {
|
|
136
|
-
console.error('[Sidebar] Port disconnected with error:', chrome.runtime.lastError.message);
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
```
|
|
1
|
+
---
|
|
2
|
+
name: "ext-messaging"
|
|
3
|
+
pack: "@rune/chrome-ext"
|
|
4
|
+
description: "Typed message passing between popup, service worker, and content script — discriminated union message types, one-shot sendMessage, long-lived port connections for streaming, and Chrome 146+ error handling."
|
|
5
|
+
model: sonnet
|
|
6
|
+
tools: [Read, Edit, Write, Grep, Glob, Bash]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# ext-messaging
|
|
10
|
+
|
|
11
|
+
Typed message passing between popup, service worker, and content script — discriminated union message types, one-shot `sendMessage`, long-lived port connections for streaming, and Chrome 146+ error handling. Prevents the #2 MV3 failure: untyped `any` messages, missing `return true` for async handlers, and ports used for single messages.
|
|
12
|
+
|
|
13
|
+
#### Workflow
|
|
14
|
+
|
|
15
|
+
**Step 1 — Identify message flows**
|
|
16
|
+
Use `Grep` to find existing `chrome.runtime.sendMessage`, `chrome.tabs.sendMessage`, and `chrome.runtime.connect` calls. Map the full message topology:
|
|
17
|
+
- popup → service worker (sendMessage — one-shot)
|
|
18
|
+
- service worker → content script (chrome.tabs.sendMessage — requires tab ID)
|
|
19
|
+
- content script → service worker (sendMessage — one-shot)
|
|
20
|
+
- service worker → popup (port — only if popup is open)
|
|
21
|
+
- streaming AI responses → use Port (not sendMessage — ports survive multiple sends)
|
|
22
|
+
|
|
23
|
+
**Step 2 — Define TypeScript message types**
|
|
24
|
+
Create `src/types/messages.ts` with a discriminated union covering all message directions. Each message type has a `type` literal and a strongly-typed `payload`. Response types are paired per message type.
|
|
25
|
+
|
|
26
|
+
**Step 3 — Implement chrome.runtime.sendMessage patterns**
|
|
27
|
+
For one-shot request/response between extension contexts. Key rules:
|
|
28
|
+
- Listener must `return true` if the response is sent asynchronously (inside a Promise or async function)
|
|
29
|
+
- `chrome.runtime.lastError` MUST be checked in the callback — unhandled errors throw in MV3
|
|
30
|
+
- Content scripts cannot receive messages via `chrome.runtime.sendMessage` — use `chrome.tabs.sendMessage` from the service worker with the target tab's ID
|
|
31
|
+
|
|
32
|
+
**Step 4 — Implement chrome.tabs.sendMessage (service worker → content)**
|
|
33
|
+
Service worker must resolve the target tab ID before sending. Use `chrome.tabs.query({ active: true, currentWindow: true })` or receive the tab ID from the content script's original message (sender.tab.id).
|
|
34
|
+
|
|
35
|
+
**Step 5 — Implement port-based long-lived connections**
|
|
36
|
+
Use `chrome.runtime.connect` for streaming scenarios (AI token streaming, progress updates, live data feeds). Ports stay open until explicitly disconnected. Each side must handle `port.onDisconnect` to clean up.
|
|
37
|
+
|
|
38
|
+
**Step 6 — Add Chrome 146+ error handling**
|
|
39
|
+
Chrome 146 changed message listener error behavior: uncaught errors in listeners now reject the Promise returned by `sendMessage` on the sender side. Wrap all listener handlers in try/catch and send structured error responses.
|
|
40
|
+
|
|
41
|
+
#### Example
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
// src/types/messages.ts — discriminated union message types
|
|
45
|
+
export type ExtensionMessage =
|
|
46
|
+
| { type: 'SUMMARIZE_PAGE'; payload: { text: string; tabId: number } }
|
|
47
|
+
| { type: 'GET_SETTINGS'; payload: Record<string, never> }
|
|
48
|
+
| { type: 'UPDATE_SETTINGS'; payload: Partial<Settings> }
|
|
49
|
+
| { type: 'OPEN_SIDEBAR'; payload: { tabId: number } };
|
|
50
|
+
|
|
51
|
+
export type ExtensionResponse<T extends ExtensionMessage> =
|
|
52
|
+
T extends { type: 'SUMMARIZE_PAGE' } ? { summary: string; error?: string } :
|
|
53
|
+
T extends { type: 'GET_SETTINGS' } ? { settings: Settings } :
|
|
54
|
+
T extends { type: 'UPDATE_SETTINGS' } ? { ok: boolean } :
|
|
55
|
+
T extends { type: 'OPEN_SIDEBAR' } ? { ok: boolean } :
|
|
56
|
+
never;
|
|
57
|
+
|
|
58
|
+
export interface Settings {
|
|
59
|
+
useBuiltinAI: boolean;
|
|
60
|
+
externalApiKey: string;
|
|
61
|
+
maxLength: number;
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
// background.ts — typed message handler
|
|
67
|
+
import type { ExtensionMessage } from './types/messages';
|
|
68
|
+
|
|
69
|
+
chrome.runtime.onMessage.addListener(
|
|
70
|
+
(message: ExtensionMessage, sender, sendResponse) => {
|
|
71
|
+
// CRITICAL: return true to keep channel open for async response
|
|
72
|
+
(async () => {
|
|
73
|
+
try {
|
|
74
|
+
switch (message.type) {
|
|
75
|
+
case 'SUMMARIZE_PAGE': {
|
|
76
|
+
const summary = await summarize(message.payload.text);
|
|
77
|
+
sendResponse({ summary });
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
case 'GET_SETTINGS': {
|
|
81
|
+
const result = await chrome.storage.sync.get('settings');
|
|
82
|
+
sendResponse({ settings: result['settings'] as Settings });
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
default:
|
|
86
|
+
sendResponse({ error: 'Unknown message type' });
|
|
87
|
+
}
|
|
88
|
+
} catch (err) {
|
|
89
|
+
// Chrome 146+: send error response instead of letting it throw
|
|
90
|
+
sendResponse({ error: String(err) });
|
|
91
|
+
}
|
|
92
|
+
})();
|
|
93
|
+
return true; // MUST return true — async response
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
// Port-based streaming (service worker → sidebar/popup)
|
|
100
|
+
// background.ts
|
|
101
|
+
chrome.runtime.onConnect.addListener((port) => {
|
|
102
|
+
if (port.name !== 'ai-stream') return;
|
|
103
|
+
|
|
104
|
+
port.onMessage.addListener(async (message: { text: string }) => {
|
|
105
|
+
try {
|
|
106
|
+
const session = await chrome.aiLanguageModel.create();
|
|
107
|
+
const stream = session.promptStreaming(message.text);
|
|
108
|
+
|
|
109
|
+
for await (const chunk of stream) {
|
|
110
|
+
port.postMessage({ type: 'CHUNK', content: chunk });
|
|
111
|
+
}
|
|
112
|
+
port.postMessage({ type: 'DONE' });
|
|
113
|
+
session.destroy();
|
|
114
|
+
} catch (err) {
|
|
115
|
+
port.postMessage({ type: 'ERROR', error: String(err) });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
port.onDisconnect.addListener(() => {
|
|
120
|
+
// cleanup — sidebar/popup was closed
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// sidebar.ts — connect and stream
|
|
125
|
+
const port = chrome.runtime.connect({ name: 'ai-stream' });
|
|
126
|
+
port.postMessage({ text: selectedText });
|
|
127
|
+
|
|
128
|
+
port.onMessage.addListener((msg: { type: string; content?: string; error?: string }) => {
|
|
129
|
+
if (msg.type === 'CHUNK') appendToOutput(msg.content ?? '');
|
|
130
|
+
if (msg.type === 'DONE') finalizeOutput();
|
|
131
|
+
if (msg.type === 'ERROR') showError(msg.error ?? 'Unknown error');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
port.onDisconnect.addListener(() => {
|
|
135
|
+
if (chrome.runtime.lastError) {
|
|
136
|
+
console.error('[Sidebar] Port disconnected with error:', chrome.runtime.lastError.message);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
```
|
|
@@ -1,133 +1,133 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: "ext-storage"
|
|
3
|
-
pack: "@rune/chrome-ext"
|
|
4
|
-
description: "Typed Chrome storage patterns — choose the right storage tier, define schema, implement typed helpers, handle schema migrations, and monitor quota."
|
|
5
|
-
model: sonnet
|
|
6
|
-
tools: [Read, Edit, Write, Grep, Glob, Bash]
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# ext-storage
|
|
10
|
-
|
|
11
|
-
Typed Chrome storage patterns — choose the right storage tier, define schema, implement typed helpers, handle schema migrations, and monitor quota. Prevents the #3 MV3 failure: storing state in service worker JS variables that reset on termination.
|
|
12
|
-
|
|
13
|
-
#### Workflow
|
|
14
|
-
|
|
15
|
-
**Step 1 — Choose storage type**
|
|
16
|
-
| Type | Capacity | Persistence | Sync | Use For |
|
|
17
|
-
|------|----------|-------------|------|---------|
|
|
18
|
-
| `chrome.storage.local` | 10 MB | Until uninstall | No | User data, large payloads, cached content |
|
|
19
|
-
| `chrome.storage.sync` | 100 KB / 8 KB per item | Cross-device | Yes | Settings, small preferences |
|
|
20
|
-
| `chrome.storage.session` | 10 MB | Until browser closes | No | Ephemeral state that service worker needs across terminations |
|
|
21
|
-
| `chrome.storage.managed` | Read-only | Admin-controlled | No | Enterprise policy |
|
|
22
|
-
|
|
23
|
-
CRITICAL: `chrome.storage.session` is the correct replacement for service worker JS variables. If you need state to survive a 30-second termination but clear on browser close, use session storage.
|
|
24
|
-
|
|
25
|
-
**Step 2 — Define TypeScript storage schema**
|
|
26
|
-
Create `src/types/storage.ts` with versioned schema interface. Include a `version` field for migration tracking.
|
|
27
|
-
|
|
28
|
-
**Step 3 — Implement typed get/set helpers**
|
|
29
|
-
Create `src/lib/storage.ts` with typed wrappers that preserve the schema type. Avoid `chrome.storage.*.get(null)` which returns `any` — always specify keys.
|
|
30
|
-
|
|
31
|
-
**Step 4 — Add migration logic**
|
|
32
|
-
On `chrome.runtime.onInstalled` with `reason === 'update'`, check stored schema version and run incremental migrations. Each migration transforms data from version N to N+1.
|
|
33
|
-
|
|
34
|
-
**Step 5 — Implement quota monitoring**
|
|
35
|
-
Chrome storage has hard limits that throw `QUOTA_BYTES_PER_ITEM` and `QUOTA_BYTES` errors on write. Wrap all writes with error handling and warn the user or prune old data when approaching 80% capacity.
|
|
36
|
-
|
|
37
|
-
#### Example
|
|
38
|
-
|
|
39
|
-
```typescript
|
|
40
|
-
// src/types/storage.ts — versioned storage schema
|
|
41
|
-
export const STORAGE_VERSION = 2;
|
|
42
|
-
|
|
43
|
-
export interface StorageSchema {
|
|
44
|
-
version: number;
|
|
45
|
-
settings: {
|
|
46
|
-
useBuiltinAI: boolean;
|
|
47
|
-
externalApiKey: string;
|
|
48
|
-
maxLength: number;
|
|
49
|
-
theme: 'light' | 'dark' | 'system';
|
|
50
|
-
};
|
|
51
|
-
cache: {
|
|
52
|
-
lastSummary: string;
|
|
53
|
-
lastUrl: string;
|
|
54
|
-
timestamp: number;
|
|
55
|
-
} | null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export const STORAGE_DEFAULTS: StorageSchema = {
|
|
59
|
-
version: STORAGE_VERSION,
|
|
60
|
-
settings: {
|
|
61
|
-
useBuiltinAI: true,
|
|
62
|
-
externalApiKey: '',
|
|
63
|
-
maxLength: 500,
|
|
64
|
-
theme: 'system',
|
|
65
|
-
},
|
|
66
|
-
cache: null,
|
|
67
|
-
};
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
```typescript
|
|
71
|
-
// src/lib/storage.ts — typed get/set helpers with quota monitoring
|
|
72
|
-
|
|
73
|
-
import type { StorageSchema } from '../types/storage';
|
|
74
|
-
import { STORAGE_DEFAULTS, STORAGE_VERSION } from '../types/storage';
|
|
75
|
-
|
|
76
|
-
type StorageKey = keyof StorageSchema;
|
|
77
|
-
|
|
78
|
-
export async function storageGet<K extends StorageKey>(
|
|
79
|
-
key: K
|
|
80
|
-
): Promise<StorageSchema[K]> {
|
|
81
|
-
const result = await chrome.storage.local.get(key);
|
|
82
|
-
return (result[key] as StorageSchema[K]) ?? STORAGE_DEFAULTS[key];
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export async function storageSet<K extends StorageKey>(
|
|
86
|
-
key: K,
|
|
87
|
-
value: StorageSchema[K]
|
|
88
|
-
): Promise<void> {
|
|
89
|
-
try {
|
|
90
|
-
await chrome.storage.local.set({ [key]: value });
|
|
91
|
-
} catch (err) {
|
|
92
|
-
const error = err as Error;
|
|
93
|
-
if (error.message.includes('QUOTA_BYTES')) {
|
|
94
|
-
console.warn('[Storage] Quota exceeded — clearing cache');
|
|
95
|
-
await chrome.storage.local.remove('cache');
|
|
96
|
-
// retry once after clearing cache
|
|
97
|
-
await chrome.storage.local.set({ [key]: value });
|
|
98
|
-
} else {
|
|
99
|
-
throw err;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Quota monitoring — warn at 80% capacity
|
|
105
|
-
export async function checkStorageQuota(): Promise<void> {
|
|
106
|
-
const bytesUsed = await chrome.storage.local.getBytesInUse(null);
|
|
107
|
-
const quota = chrome.storage.local.QUOTA_BYTES; // 10 MB = 10,485,760 bytes
|
|
108
|
-
const pct = (bytesUsed / quota) * 100;
|
|
109
|
-
if (pct > 80) {
|
|
110
|
-
console.warn(`[Storage] ${pct.toFixed(1)}% of local storage used (${bytesUsed} / ${quota} bytes)`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Migration runner — call on onInstalled with reason='update'
|
|
115
|
-
export async function runMigrations(): Promise<void> {
|
|
116
|
-
const stored = await chrome.storage.local.get('version');
|
|
117
|
-
const currentVersion = (stored['version'] as number | undefined) ?? 1;
|
|
118
|
-
|
|
119
|
-
if (currentVersion < 2) {
|
|
120
|
-
// v1 → v2: renamed 'apiKey' to 'externalApiKey'
|
|
121
|
-
const legacy = await chrome.storage.local.get('settings');
|
|
122
|
-
const legacySettings = legacy['settings'] as Record<string, unknown> | undefined;
|
|
123
|
-
if (legacySettings?.['apiKey']) {
|
|
124
|
-
await chrome.storage.local.set({
|
|
125
|
-
settings: { ...legacySettings, externalApiKey: legacySettings['apiKey'], apiKey: undefined },
|
|
126
|
-
version: 2,
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
await chrome.storage.local.set({ version: STORAGE_VERSION });
|
|
132
|
-
}
|
|
133
|
-
```
|
|
1
|
+
---
|
|
2
|
+
name: "ext-storage"
|
|
3
|
+
pack: "@rune/chrome-ext"
|
|
4
|
+
description: "Typed Chrome storage patterns — choose the right storage tier, define schema, implement typed helpers, handle schema migrations, and monitor quota."
|
|
5
|
+
model: sonnet
|
|
6
|
+
tools: [Read, Edit, Write, Grep, Glob, Bash]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# ext-storage
|
|
10
|
+
|
|
11
|
+
Typed Chrome storage patterns — choose the right storage tier, define schema, implement typed helpers, handle schema migrations, and monitor quota. Prevents the #3 MV3 failure: storing state in service worker JS variables that reset on termination.
|
|
12
|
+
|
|
13
|
+
#### Workflow
|
|
14
|
+
|
|
15
|
+
**Step 1 — Choose storage type**
|
|
16
|
+
| Type | Capacity | Persistence | Sync | Use For |
|
|
17
|
+
|------|----------|-------------|------|---------|
|
|
18
|
+
| `chrome.storage.local` | 10 MB | Until uninstall | No | User data, large payloads, cached content |
|
|
19
|
+
| `chrome.storage.sync` | 100 KB / 8 KB per item | Cross-device | Yes | Settings, small preferences |
|
|
20
|
+
| `chrome.storage.session` | 10 MB | Until browser closes | No | Ephemeral state that service worker needs across terminations |
|
|
21
|
+
| `chrome.storage.managed` | Read-only | Admin-controlled | No | Enterprise policy |
|
|
22
|
+
|
|
23
|
+
CRITICAL: `chrome.storage.session` is the correct replacement for service worker JS variables. If you need state to survive a 30-second termination but clear on browser close, use session storage.
|
|
24
|
+
|
|
25
|
+
**Step 2 — Define TypeScript storage schema**
|
|
26
|
+
Create `src/types/storage.ts` with versioned schema interface. Include a `version` field for migration tracking.
|
|
27
|
+
|
|
28
|
+
**Step 3 — Implement typed get/set helpers**
|
|
29
|
+
Create `src/lib/storage.ts` with typed wrappers that preserve the schema type. Avoid `chrome.storage.*.get(null)` which returns `any` — always specify keys.
|
|
30
|
+
|
|
31
|
+
**Step 4 — Add migration logic**
|
|
32
|
+
On `chrome.runtime.onInstalled` with `reason === 'update'`, check stored schema version and run incremental migrations. Each migration transforms data from version N to N+1.
|
|
33
|
+
|
|
34
|
+
**Step 5 — Implement quota monitoring**
|
|
35
|
+
Chrome storage has hard limits that throw `QUOTA_BYTES_PER_ITEM` and `QUOTA_BYTES` errors on write. Wrap all writes with error handling and warn the user or prune old data when approaching 80% capacity.
|
|
36
|
+
|
|
37
|
+
#### Example
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
// src/types/storage.ts — versioned storage schema
|
|
41
|
+
export const STORAGE_VERSION = 2;
|
|
42
|
+
|
|
43
|
+
export interface StorageSchema {
|
|
44
|
+
version: number;
|
|
45
|
+
settings: {
|
|
46
|
+
useBuiltinAI: boolean;
|
|
47
|
+
externalApiKey: string;
|
|
48
|
+
maxLength: number;
|
|
49
|
+
theme: 'light' | 'dark' | 'system';
|
|
50
|
+
};
|
|
51
|
+
cache: {
|
|
52
|
+
lastSummary: string;
|
|
53
|
+
lastUrl: string;
|
|
54
|
+
timestamp: number;
|
|
55
|
+
} | null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const STORAGE_DEFAULTS: StorageSchema = {
|
|
59
|
+
version: STORAGE_VERSION,
|
|
60
|
+
settings: {
|
|
61
|
+
useBuiltinAI: true,
|
|
62
|
+
externalApiKey: '',
|
|
63
|
+
maxLength: 500,
|
|
64
|
+
theme: 'system',
|
|
65
|
+
},
|
|
66
|
+
cache: null,
|
|
67
|
+
};
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
// src/lib/storage.ts — typed get/set helpers with quota monitoring
|
|
72
|
+
|
|
73
|
+
import type { StorageSchema } from '../types/storage';
|
|
74
|
+
import { STORAGE_DEFAULTS, STORAGE_VERSION } from '../types/storage';
|
|
75
|
+
|
|
76
|
+
type StorageKey = keyof StorageSchema;
|
|
77
|
+
|
|
78
|
+
export async function storageGet<K extends StorageKey>(
|
|
79
|
+
key: K
|
|
80
|
+
): Promise<StorageSchema[K]> {
|
|
81
|
+
const result = await chrome.storage.local.get(key);
|
|
82
|
+
return (result[key] as StorageSchema[K]) ?? STORAGE_DEFAULTS[key];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function storageSet<K extends StorageKey>(
|
|
86
|
+
key: K,
|
|
87
|
+
value: StorageSchema[K]
|
|
88
|
+
): Promise<void> {
|
|
89
|
+
try {
|
|
90
|
+
await chrome.storage.local.set({ [key]: value });
|
|
91
|
+
} catch (err) {
|
|
92
|
+
const error = err as Error;
|
|
93
|
+
if (error.message.includes('QUOTA_BYTES')) {
|
|
94
|
+
console.warn('[Storage] Quota exceeded — clearing cache');
|
|
95
|
+
await chrome.storage.local.remove('cache');
|
|
96
|
+
// retry once after clearing cache
|
|
97
|
+
await chrome.storage.local.set({ [key]: value });
|
|
98
|
+
} else {
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Quota monitoring — warn at 80% capacity
|
|
105
|
+
export async function checkStorageQuota(): Promise<void> {
|
|
106
|
+
const bytesUsed = await chrome.storage.local.getBytesInUse(null);
|
|
107
|
+
const quota = chrome.storage.local.QUOTA_BYTES; // 10 MB = 10,485,760 bytes
|
|
108
|
+
const pct = (bytesUsed / quota) * 100;
|
|
109
|
+
if (pct > 80) {
|
|
110
|
+
console.warn(`[Storage] ${pct.toFixed(1)}% of local storage used (${bytesUsed} / ${quota} bytes)`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Migration runner — call on onInstalled with reason='update'
|
|
115
|
+
export async function runMigrations(): Promise<void> {
|
|
116
|
+
const stored = await chrome.storage.local.get('version');
|
|
117
|
+
const currentVersion = (stored['version'] as number | undefined) ?? 1;
|
|
118
|
+
|
|
119
|
+
if (currentVersion < 2) {
|
|
120
|
+
// v1 → v2: renamed 'apiKey' to 'externalApiKey'
|
|
121
|
+
const legacy = await chrome.storage.local.get('settings');
|
|
122
|
+
const legacySettings = legacy['settings'] as Record<string, unknown> | undefined;
|
|
123
|
+
if (legacySettings?.['apiKey']) {
|
|
124
|
+
await chrome.storage.local.set({
|
|
125
|
+
settings: { ...legacySettings, externalApiKey: legacySettings['apiKey'], apiKey: undefined },
|
|
126
|
+
version: 2,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
await chrome.storage.local.set({ version: STORAGE_VERSION });
|
|
132
|
+
}
|
|
133
|
+
```
|