@rune-kit/rune 2.1.1

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 (155) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +357 -0
  3. package/agents/.gitkeep +0 -0
  4. package/agents/architect.md +29 -0
  5. package/agents/asset-creator.md +11 -0
  6. package/agents/audit.md +11 -0
  7. package/agents/autopsy.md +11 -0
  8. package/agents/brainstorm.md +11 -0
  9. package/agents/browser-pilot.md +11 -0
  10. package/agents/coder.md +29 -0
  11. package/agents/completion-gate.md +11 -0
  12. package/agents/constraint-check.md +11 -0
  13. package/agents/context-engine.md +11 -0
  14. package/agents/cook.md +11 -0
  15. package/agents/db.md +11 -0
  16. package/agents/debug.md +11 -0
  17. package/agents/dependency-doctor.md +11 -0
  18. package/agents/deploy.md +11 -0
  19. package/agents/design.md +11 -0
  20. package/agents/docs-seeker.md +11 -0
  21. package/agents/fix.md +11 -0
  22. package/agents/hallucination-guard.md +11 -0
  23. package/agents/incident.md +11 -0
  24. package/agents/integrity-check.md +11 -0
  25. package/agents/journal.md +11 -0
  26. package/agents/launch.md +11 -0
  27. package/agents/logic-guardian.md +11 -0
  28. package/agents/marketing.md +11 -0
  29. package/agents/onboard.md +11 -0
  30. package/agents/perf.md +11 -0
  31. package/agents/plan.md +11 -0
  32. package/agents/preflight.md +11 -0
  33. package/agents/problem-solver.md +11 -0
  34. package/agents/rescue.md +11 -0
  35. package/agents/research.md +11 -0
  36. package/agents/researcher.md +29 -0
  37. package/agents/review-intake.md +11 -0
  38. package/agents/review.md +11 -0
  39. package/agents/reviewer.md +28 -0
  40. package/agents/safeguard.md +11 -0
  41. package/agents/sast.md +11 -0
  42. package/agents/scanner.md +28 -0
  43. package/agents/scope-guard.md +11 -0
  44. package/agents/scout.md +11 -0
  45. package/agents/sentinel.md +11 -0
  46. package/agents/sequential-thinking.md +11 -0
  47. package/agents/session-bridge.md +11 -0
  48. package/agents/skill-forge.md +11 -0
  49. package/agents/skill-router.md +11 -0
  50. package/agents/surgeon.md +11 -0
  51. package/agents/team.md +11 -0
  52. package/agents/test.md +11 -0
  53. package/agents/trend-scout.md +11 -0
  54. package/agents/verification.md +11 -0
  55. package/agents/video-creator.md +11 -0
  56. package/agents/watchdog.md +11 -0
  57. package/agents/worktree.md +11 -0
  58. package/commands/.gitkeep +0 -0
  59. package/commands/rune.md +168 -0
  60. package/compiler/__tests__/openclaw-adapter.test.js +140 -0
  61. package/compiler/__tests__/parser.test.js +55 -0
  62. package/compiler/adapters/antigravity.js +59 -0
  63. package/compiler/adapters/claude.js +37 -0
  64. package/compiler/adapters/cursor.js +67 -0
  65. package/compiler/adapters/generic.js +60 -0
  66. package/compiler/adapters/index.js +45 -0
  67. package/compiler/adapters/openclaw.js +150 -0
  68. package/compiler/adapters/windsurf.js +60 -0
  69. package/compiler/bin/rune.js +288 -0
  70. package/compiler/doctor.js +153 -0
  71. package/compiler/emitter.js +240 -0
  72. package/compiler/parser.js +208 -0
  73. package/compiler/transformer.js +69 -0
  74. package/compiler/transforms/branding.js +27 -0
  75. package/compiler/transforms/cross-references.js +29 -0
  76. package/compiler/transforms/frontmatter.js +38 -0
  77. package/compiler/transforms/hooks.js +68 -0
  78. package/compiler/transforms/subagents.js +36 -0
  79. package/compiler/transforms/tool-names.js +60 -0
  80. package/contexts/dev.md +34 -0
  81. package/contexts/research.md +43 -0
  82. package/contexts/review.md +55 -0
  83. package/extensions/ai-ml/PACK.md +517 -0
  84. package/extensions/analytics/PACK.md +557 -0
  85. package/extensions/backend/PACK.md +678 -0
  86. package/extensions/chrome-ext/PACK.md +995 -0
  87. package/extensions/content/PACK.md +381 -0
  88. package/extensions/devops/PACK.md +520 -0
  89. package/extensions/ecommerce/PACK.md +280 -0
  90. package/extensions/gamedev/PACK.md +393 -0
  91. package/extensions/mobile/PACK.md +273 -0
  92. package/extensions/saas/PACK.md +805 -0
  93. package/extensions/security/PACK.md +536 -0
  94. package/extensions/trading/PACK.md +597 -0
  95. package/extensions/ui/PACK.md +947 -0
  96. package/package.json +47 -0
  97. package/skills/.gitkeep +0 -0
  98. package/skills/adversary/SKILL.md +271 -0
  99. package/skills/asset-creator/SKILL.md +157 -0
  100. package/skills/audit/SKILL.md +466 -0
  101. package/skills/autopsy/SKILL.md +200 -0
  102. package/skills/ba/SKILL.md +279 -0
  103. package/skills/brainstorm/SKILL.md +266 -0
  104. package/skills/browser-pilot/SKILL.md +168 -0
  105. package/skills/completion-gate/SKILL.md +151 -0
  106. package/skills/constraint-check/SKILL.md +165 -0
  107. package/skills/context-engine/SKILL.md +176 -0
  108. package/skills/cook/SKILL.md +636 -0
  109. package/skills/db/SKILL.md +256 -0
  110. package/skills/debug/SKILL.md +240 -0
  111. package/skills/dependency-doctor/SKILL.md +235 -0
  112. package/skills/deploy/SKILL.md +174 -0
  113. package/skills/design/DESIGN-REFERENCE.md +365 -0
  114. package/skills/design/SKILL.md +462 -0
  115. package/skills/doc-processor/SKILL.md +254 -0
  116. package/skills/docs/SKILL.md +336 -0
  117. package/skills/docs-seeker/SKILL.md +166 -0
  118. package/skills/fix/SKILL.md +192 -0
  119. package/skills/git/SKILL.md +285 -0
  120. package/skills/hallucination-guard/SKILL.md +204 -0
  121. package/skills/incident/SKILL.md +241 -0
  122. package/skills/integrity-check/SKILL.md +169 -0
  123. package/skills/journal/SKILL.md +190 -0
  124. package/skills/launch/SKILL.md +330 -0
  125. package/skills/logic-guardian/SKILL.md +240 -0
  126. package/skills/marketing/SKILL.md +229 -0
  127. package/skills/mcp-builder/SKILL.md +311 -0
  128. package/skills/onboard/SKILL.md +298 -0
  129. package/skills/perf/SKILL.md +297 -0
  130. package/skills/plan/SKILL.md +520 -0
  131. package/skills/preflight/SKILL.md +231 -0
  132. package/skills/problem-solver/SKILL.md +284 -0
  133. package/skills/rescue/SKILL.md +434 -0
  134. package/skills/research/SKILL.md +122 -0
  135. package/skills/review/SKILL.md +354 -0
  136. package/skills/review-intake/SKILL.md +222 -0
  137. package/skills/safeguard/SKILL.md +188 -0
  138. package/skills/sast/SKILL.md +190 -0
  139. package/skills/scaffold/SKILL.md +276 -0
  140. package/skills/scope-guard/SKILL.md +150 -0
  141. package/skills/scout/SKILL.md +232 -0
  142. package/skills/sentinel/SKILL.md +320 -0
  143. package/skills/sentinel-env/SKILL.md +226 -0
  144. package/skills/sequential-thinking/SKILL.md +234 -0
  145. package/skills/session-bridge/SKILL.md +287 -0
  146. package/skills/skill-forge/SKILL.md +317 -0
  147. package/skills/skill-router/SKILL.md +267 -0
  148. package/skills/surgeon/SKILL.md +203 -0
  149. package/skills/team/SKILL.md +397 -0
  150. package/skills/test/SKILL.md +271 -0
  151. package/skills/trend-scout/SKILL.md +145 -0
  152. package/skills/verification/SKILL.md +201 -0
  153. package/skills/video-creator/SKILL.md +201 -0
  154. package/skills/watchdog/SKILL.md +166 -0
  155. package/skills/worktree/SKILL.md +140 -0
@@ -0,0 +1,995 @@
1
+ ---
2
+ name: "@rune/chrome-ext"
3
+ description: Chrome extension development patterns — Manifest V3 scaffolding, service worker lifecycle, message passing, storage patterns, Chrome Web Store compliance, and built-in AI integration.
4
+ metadata:
5
+ author: runedev
6
+ version: "0.1.0"
7
+ layer: L4
8
+ price: "free"
9
+ target: Chrome extension developers
10
+ ---
11
+
12
+ # @rune/chrome-ext
13
+
14
+ ## Purpose
15
+
16
+ Chrome extension development has a steep cliff of Manifest V3 gotchas that no other AI coding pack addresses. Service workers terminate silently after 30 seconds of idle, taking all JS-variable state with them. Fifty-eight percent of Chrome Web Store rejections are preventable compliance errors. The new Chrome AI APIs (Gemini Nano, Chrome 138+) require hardware checks, graceful fallbacks, and port-based streaming — none of which are obvious from the docs. This pack groups six tightly-coupled concerns — MV3 scaffolding, message passing, storage, CWS preflight, store listing, and built-in AI — because a gap in any single layer produces a broken, rejected, or battery-draining extension. Activates automatically when `manifest.json` with `manifest_version: 3` or `chrome.*` API usage is detected.
17
+
18
+ ## Triggers
19
+
20
+ - Auto-trigger: when `manifest.json` containing `"manifest_version": 3` is found in project root or `src/`
21
+ - Auto-trigger: when files matching `**/background.ts`, `**/service-worker.ts`, `**/content.ts`, `**/popup.ts` exist alongside a `manifest.json`
22
+ - Auto-trigger: when `chrome.*` API calls are found in project source files
23
+ - `/rune chrome-ext` — manual invocation
24
+ - Called by `cook` (L1) when Chrome extension project context is detected
25
+ - Called by `scaffold` (L1) when user requests a new browser extension project
26
+
27
+ ## Skills Included
28
+
29
+ ### mv3-scaffold
30
+
31
+ Manifest V3 project scaffolding — detect extension type, generate minimal-permission manifest, scaffold service worker with correct lifecycle patterns, scaffold content script, and generate build config. Prevents the #1 MV3 mistake: carrying MV2 mental models (background pages, remote scripts, setTimeout for keepalive) into an MV3 project.
32
+
33
+ #### Workflow
34
+
35
+ **Step 1 — Detect or clarify extension type**
36
+ Use `Read` on any existing `manifest.json` or project description to classify the extension type:
37
+ - **popup**: user-triggered UI (toolbar button → popup.html)
38
+ - **sidebar**: persistent panel (chrome.sidePanel API, Chrome 114+)
39
+ - **content-injector**: modifies host pages (content scripts + optional popup)
40
+ - **background-only**: no visible UI, reacts to events (alarms, network, tabs)
41
+ - **devtools**: extends Chrome DevTools panel
42
+
43
+ If undetectable from files, ask the user. Extension type determines which APIs, permissions, and scaffold components are generated.
44
+
45
+ **Step 2 — Generate minimal-permission manifest.json**
46
+ Emit `manifest.json` with only the permissions required for the detected type. Flag over-permissioning immediately — requesting `<all_urls>` when only `activeTab` is needed is the #1 CWS rejection cause.
47
+
48
+ Key MV3 manifest rules:
49
+ - `"manifest_version": 3` — mandatory, MV2 deprecated Jan 2023
50
+ - `"background"` uses `{ "service_worker": "background.js" }` — NOT `"scripts"` array
51
+ - `"action"` replaces `"browser_action"` and `"page_action"`
52
+ - No `"content_security_policy"` that relaxes `script-src` (blocks CWS review)
53
+ - No `"web_accessible_resources"` with `matches: ["<all_urls>"]` unless justified
54
+ - External URLs in `"host_permissions"` require justification in CWS dashboard
55
+
56
+ **Step 3 — Scaffold service worker (CRITICAL lifecycle patterns)**
57
+ Generate `background.ts` / `background.js` with the following non-negotiable patterns:
58
+
59
+ CRITICAL: service workers terminate after 30 seconds of idle. Every assumption that breaks because of this:
60
+ - JS variables reset on termination — use `chrome.storage.session` for ephemeral state
61
+ - `setTimeout` / `setInterval` — NOT reliable across terminations, use `chrome.alarms`
62
+ - Pending async operations mid-flight get killed — use alarm + storage to resume
63
+ - `fetch()` initiated in a response to a non-event call may not complete
64
+
65
+ All event listeners MUST be registered at the top level synchronously — NOT inside `async` functions, Promises, or conditionals. Chrome only registers listeners present during the initial synchronous execution of the service worker.
66
+
67
+ **Step 4 — Scaffold content script**
68
+ Generate `content.ts` with correct isolation model:
69
+ - Runs in an **isolated world** — own JS context, cannot access page's JS variables
70
+ - Has access to the DOM but NOT to `chrome.storage`, `chrome.tabs`, most `chrome.*` APIs (exceptions: `chrome.runtime`, `chrome.storage`, `chrome.i18n`)
71
+ - Must message the service worker for privileged operations
72
+ - Inject only when needed — prefer `"run_at": "document_idle"` over `"document_start"`
73
+
74
+ **Step 5 — Scaffold popup/sidebar UI**
75
+ For popup and sidebar types, generate `popup.html` + `popup.ts`:
76
+ - Popup HTML MUST NOT load remote scripts (`<script src="https://...">`) — blocked by CSP
77
+ - All scripts must be local and listed in `web_accessible_resources` if loaded from content scripts
78
+ - Popup closes when user clicks away — don't depend on popup state for background operations
79
+ - For sidebar: register `chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true })`
80
+
81
+ **Step 6 — Generate build config**
82
+ Emit a build configuration based on detected tooling:
83
+ - If `vite` in `package.json` → emit `vite.config.ts` using `@crxjs/vite-plugin` (hot-reload for extension dev)
84
+ - Otherwise → emit vanilla TypeScript config with `tsc` + file copy script
85
+ - Include `web-ext` config for local loading and reload
86
+
87
+ #### Example
88
+
89
+ ```json
90
+ // manifest.json — content-injector type, minimal permissions
91
+ {
92
+ "manifest_version": 3,
93
+ "name": "Page Summarizer",
94
+ "version": "1.0.0",
95
+ "description": "Summarize any page using built-in AI or an external API.",
96
+ "permissions": ["activeTab", "storage", "sidePanel"],
97
+ "host_permissions": [],
98
+ "background": {
99
+ "service_worker": "background.js",
100
+ "type": "module"
101
+ },
102
+ "content_scripts": [
103
+ {
104
+ "matches": ["<all_urls>"],
105
+ "js": ["content.js"],
106
+ "run_at": "document_idle"
107
+ }
108
+ ],
109
+ "action": {
110
+ "default_title": "Summarize this page",
111
+ "default_icon": { "128": "icons/icon128.png" }
112
+ },
113
+ "side_panel": {
114
+ "default_path": "sidebar.html"
115
+ },
116
+ "icons": { "128": "icons/icon128.png" },
117
+ "content_security_policy": {
118
+ "extension_pages": "script-src 'self'; object-src 'self'"
119
+ }
120
+ }
121
+ ```
122
+
123
+ ```typescript
124
+ // background.ts — correct MV3 service worker patterns
125
+ // CRITICAL: all listeners registered synchronously at top level
126
+
127
+ chrome.runtime.onInstalled.addListener(({ reason }) => {
128
+ if (reason === 'install') {
129
+ console.log('[SW] Extension installed');
130
+ }
131
+ });
132
+
133
+ // Use chrome.alarms — NOT setTimeout (alarms survive service worker termination)
134
+ chrome.runtime.onInstalled.addListener(() => {
135
+ chrome.alarms.create('heartbeat', { periodInMinutes: 1 });
136
+ });
137
+
138
+ chrome.alarms.onAlarm.addListener((alarm) => {
139
+ if (alarm.name === 'heartbeat') {
140
+ // periodic work here — service worker woke up for this
141
+ }
142
+ });
143
+
144
+ // Message handler — registered synchronously, NOT inside async function
145
+ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
146
+ if (message.type === 'SUMMARIZE_PAGE') {
147
+ // Return true to keep the message channel open for async response
148
+ handleSummarize(message.payload).then(sendResponse);
149
+ return true;
150
+ }
151
+ });
152
+
153
+ async function handleSummarize(payload: { text: string }): Promise<{ summary: string }> {
154
+ // Service worker is alive for the duration of this message handler
155
+ const summary = await callExternalApi(payload.text);
156
+ return { summary };
157
+ }
158
+ ```
159
+
160
+ ```typescript
161
+ // content.ts — isolated world, limited chrome.* access
162
+ const selectedText = window.getSelection()?.toString() ?? '';
163
+
164
+ if (selectedText.length > 0) {
165
+ // Content scripts can message service worker
166
+ chrome.runtime.sendMessage(
167
+ { type: 'SUMMARIZE_PAGE', payload: { text: selectedText } },
168
+ (response: { summary: string }) => {
169
+ if (chrome.runtime.lastError) {
170
+ console.error('[Content] Message failed:', chrome.runtime.lastError.message);
171
+ return;
172
+ }
173
+ displaySummary(response.summary);
174
+ }
175
+ );
176
+ }
177
+
178
+ function displaySummary(summary: string): void {
179
+ const panel = document.createElement('div');
180
+ panel.id = 'rune-summarizer-panel';
181
+ panel.textContent = summary;
182
+ document.body.appendChild(panel);
183
+ }
184
+ ```
185
+
186
+ ---
187
+
188
+ ### ext-messaging
189
+
190
+ 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.
191
+
192
+ #### Workflow
193
+
194
+ **Step 1 — Identify message flows**
195
+ Use `Grep` to find existing `chrome.runtime.sendMessage`, `chrome.tabs.sendMessage`, and `chrome.runtime.connect` calls. Map the full message topology:
196
+ - popup → service worker (sendMessage — one-shot)
197
+ - service worker → content script (chrome.tabs.sendMessage — requires tab ID)
198
+ - content script → service worker (sendMessage — one-shot)
199
+ - service worker → popup (port — only if popup is open)
200
+ - streaming AI responses → use Port (not sendMessage — ports survive multiple sends)
201
+
202
+ **Step 2 — Define TypeScript message types**
203
+ 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.
204
+
205
+ **Step 3 — Implement chrome.runtime.sendMessage patterns**
206
+ For one-shot request/response between extension contexts. Key rules:
207
+ - Listener must `return true` if the response is sent asynchronously (inside a Promise or async function)
208
+ - `chrome.runtime.lastError` MUST be checked in the callback — unhandled errors throw in MV3
209
+ - Content scripts cannot receive messages via `chrome.runtime.sendMessage` — use `chrome.tabs.sendMessage` from the service worker with the target tab's ID
210
+
211
+ **Step 4 — Implement chrome.tabs.sendMessage (service worker → content)**
212
+ 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).
213
+
214
+ **Step 5 — Implement port-based long-lived connections**
215
+ 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.
216
+
217
+ **Step 6 — Add Chrome 146+ error handling**
218
+ 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.
219
+
220
+ #### Example
221
+
222
+ ```typescript
223
+ // src/types/messages.ts — discriminated union message types
224
+ export type ExtensionMessage =
225
+ | { type: 'SUMMARIZE_PAGE'; payload: { text: string; tabId: number } }
226
+ | { type: 'GET_SETTINGS'; payload: Record<string, never> }
227
+ | { type: 'UPDATE_SETTINGS'; payload: Partial<Settings> }
228
+ | { type: 'OPEN_SIDEBAR'; payload: { tabId: number } };
229
+
230
+ export type ExtensionResponse<T extends ExtensionMessage> =
231
+ T extends { type: 'SUMMARIZE_PAGE' } ? { summary: string; error?: string } :
232
+ T extends { type: 'GET_SETTINGS' } ? { settings: Settings } :
233
+ T extends { type: 'UPDATE_SETTINGS' } ? { ok: boolean } :
234
+ T extends { type: 'OPEN_SIDEBAR' } ? { ok: boolean } :
235
+ never;
236
+
237
+ export interface Settings {
238
+ useBuiltinAI: boolean;
239
+ externalApiKey: string;
240
+ maxLength: number;
241
+ }
242
+ ```
243
+
244
+ ```typescript
245
+ // background.ts — typed message handler
246
+ import type { ExtensionMessage } from './types/messages';
247
+
248
+ chrome.runtime.onMessage.addListener(
249
+ (message: ExtensionMessage, sender, sendResponse) => {
250
+ // CRITICAL: return true to keep channel open for async response
251
+ (async () => {
252
+ try {
253
+ switch (message.type) {
254
+ case 'SUMMARIZE_PAGE': {
255
+ const summary = await summarize(message.payload.text);
256
+ sendResponse({ summary });
257
+ break;
258
+ }
259
+ case 'GET_SETTINGS': {
260
+ const result = await chrome.storage.sync.get('settings');
261
+ sendResponse({ settings: result['settings'] as Settings });
262
+ break;
263
+ }
264
+ default:
265
+ sendResponse({ error: 'Unknown message type' });
266
+ }
267
+ } catch (err) {
268
+ // Chrome 146+: send error response instead of letting it throw
269
+ sendResponse({ error: String(err) });
270
+ }
271
+ })();
272
+ return true; // MUST return true — async response
273
+ }
274
+ );
275
+ ```
276
+
277
+ ```typescript
278
+ // Port-based streaming (service worker → sidebar/popup)
279
+ // background.ts
280
+ chrome.runtime.onConnect.addListener((port) => {
281
+ if (port.name !== 'ai-stream') return;
282
+
283
+ port.onMessage.addListener(async (message: { text: string }) => {
284
+ try {
285
+ const session = await chrome.aiLanguageModel.create();
286
+ const stream = session.promptStreaming(message.text);
287
+
288
+ for await (const chunk of stream) {
289
+ port.postMessage({ type: 'CHUNK', content: chunk });
290
+ }
291
+ port.postMessage({ type: 'DONE' });
292
+ session.destroy();
293
+ } catch (err) {
294
+ port.postMessage({ type: 'ERROR', error: String(err) });
295
+ }
296
+ });
297
+
298
+ port.onDisconnect.addListener(() => {
299
+ // cleanup — sidebar/popup was closed
300
+ });
301
+ });
302
+
303
+ // sidebar.ts — connect and stream
304
+ const port = chrome.runtime.connect({ name: 'ai-stream' });
305
+ port.postMessage({ text: selectedText });
306
+
307
+ port.onMessage.addListener((msg: { type: string; content?: string; error?: string }) => {
308
+ if (msg.type === 'CHUNK') appendToOutput(msg.content ?? '');
309
+ if (msg.type === 'DONE') finalizeOutput();
310
+ if (msg.type === 'ERROR') showError(msg.error ?? 'Unknown error');
311
+ });
312
+
313
+ port.onDisconnect.addListener(() => {
314
+ if (chrome.runtime.lastError) {
315
+ console.error('[Sidebar] Port disconnected with error:', chrome.runtime.lastError.message);
316
+ }
317
+ });
318
+ ```
319
+
320
+ ---
321
+
322
+ ### ext-storage
323
+
324
+ 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.
325
+
326
+ #### Workflow
327
+
328
+ **Step 1 — Choose storage type**
329
+ | Type | Capacity | Persistence | Sync | Use For |
330
+ |------|----------|-------------|------|---------|
331
+ | `chrome.storage.local` | 10 MB | Until uninstall | No | User data, large payloads, cached content |
332
+ | `chrome.storage.sync` | 100 KB / 8 KB per item | Cross-device | Yes | Settings, small preferences |
333
+ | `chrome.storage.session` | 10 MB | Until browser closes | No | Ephemeral state that service worker needs across terminations |
334
+ | `chrome.storage.managed` | Read-only | Admin-controlled | No | Enterprise policy |
335
+
336
+ 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.
337
+
338
+ **Step 2 — Define TypeScript storage schema**
339
+ Create `src/types/storage.ts` with versioned schema interface. Include a `version` field for migration tracking.
340
+
341
+ **Step 3 — Implement typed get/set helpers**
342
+ Create `src/lib/storage.ts` with typed wrappers that preserve the schema type. Avoid `chrome.storage.*.get(null)` which returns `any` — always specify keys.
343
+
344
+ **Step 4 — Add migration logic**
345
+ 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.
346
+
347
+ **Step 5 — Implement quota monitoring**
348
+ 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.
349
+
350
+ #### Example
351
+
352
+ ```typescript
353
+ // src/types/storage.ts — versioned storage schema
354
+ export const STORAGE_VERSION = 2;
355
+
356
+ export interface StorageSchema {
357
+ version: number;
358
+ settings: {
359
+ useBuiltinAI: boolean;
360
+ externalApiKey: string;
361
+ maxLength: number;
362
+ theme: 'light' | 'dark' | 'system';
363
+ };
364
+ cache: {
365
+ lastSummary: string;
366
+ lastUrl: string;
367
+ timestamp: number;
368
+ } | null;
369
+ }
370
+
371
+ export const STORAGE_DEFAULTS: StorageSchema = {
372
+ version: STORAGE_VERSION,
373
+ settings: {
374
+ useBuiltinAI: true,
375
+ externalApiKey: '',
376
+ maxLength: 500,
377
+ theme: 'system',
378
+ },
379
+ cache: null,
380
+ };
381
+ ```
382
+
383
+ ```typescript
384
+ // src/lib/storage.ts — typed get/set helpers with quota monitoring
385
+
386
+ import type { StorageSchema } from '../types/storage';
387
+ import { STORAGE_DEFAULTS, STORAGE_VERSION } from '../types/storage';
388
+
389
+ type StorageKey = keyof StorageSchema;
390
+
391
+ export async function storageGet<K extends StorageKey>(
392
+ key: K
393
+ ): Promise<StorageSchema[K]> {
394
+ const result = await chrome.storage.local.get(key);
395
+ return (result[key] as StorageSchema[K]) ?? STORAGE_DEFAULTS[key];
396
+ }
397
+
398
+ export async function storageSet<K extends StorageKey>(
399
+ key: K,
400
+ value: StorageSchema[K]
401
+ ): Promise<void> {
402
+ try {
403
+ await chrome.storage.local.set({ [key]: value });
404
+ } catch (err) {
405
+ const error = err as Error;
406
+ if (error.message.includes('QUOTA_BYTES')) {
407
+ console.warn('[Storage] Quota exceeded — clearing cache');
408
+ await chrome.storage.local.remove('cache');
409
+ // retry once after clearing cache
410
+ await chrome.storage.local.set({ [key]: value });
411
+ } else {
412
+ throw err;
413
+ }
414
+ }
415
+ }
416
+
417
+ // Quota monitoring — warn at 80% capacity
418
+ export async function checkStorageQuota(): Promise<void> {
419
+ const bytesUsed = await chrome.storage.local.getBytesInUse(null);
420
+ const quota = chrome.storage.local.QUOTA_BYTES; // 10 MB = 10,485,760 bytes
421
+ const pct = (bytesUsed / quota) * 100;
422
+ if (pct > 80) {
423
+ console.warn(`[Storage] ${pct.toFixed(1)}% of local storage used (${bytesUsed} / ${quota} bytes)`);
424
+ }
425
+ }
426
+
427
+ // Migration runner — call on onInstalled with reason='update'
428
+ export async function runMigrations(): Promise<void> {
429
+ const stored = await chrome.storage.local.get('version');
430
+ const currentVersion = (stored['version'] as number | undefined) ?? 1;
431
+
432
+ if (currentVersion < 2) {
433
+ // v1 → v2: renamed 'apiKey' to 'externalApiKey'
434
+ const legacy = await chrome.storage.local.get('settings');
435
+ const legacySettings = legacy['settings'] as Record<string, unknown> | undefined;
436
+ if (legacySettings?.['apiKey']) {
437
+ await chrome.storage.local.set({
438
+ settings: { ...legacySettings, externalApiKey: legacySettings['apiKey'], apiKey: undefined },
439
+ version: 2,
440
+ });
441
+ }
442
+ }
443
+
444
+ await chrome.storage.local.set({ version: STORAGE_VERSION });
445
+ }
446
+ ```
447
+
448
+ ---
449
+
450
+ ### cws-preflight
451
+
452
+ Chrome Web Store compliance audit — scan for over-permissioning, remote code execution, CSP violations, missing assets, and generate permission justification text. The highest-value skill in this pack: 58% of CWS rejections are preventable compliance errors caught here before submission.
453
+
454
+ **Top 5 CWS rejection reasons (2024 data):**
455
+ 1. Over-permissioning — requesting permissions not demonstrably used in submitted code
456
+ 2. Remote code execution — `eval()`, `Function()` constructor, CDN `<script>` tags, `import()` from external URLs
457
+ 3. Misleading description — functionality not matching store listing claims
458
+ 4. Missing or inaccessible privacy policy — required for any extension that handles user data
459
+ 5. Branding violations — trademarked names (Google, Chrome, YouTube) in extension name or icon
460
+
461
+ **Triggers for manual review (3+ weeks instead of 24-72h):**
462
+ - Broad `host_permissions` with `<all_urls>` or `https://*/*`
463
+ - Sensitive permission combinations: `tabs` + `history` + `cookies`
464
+ - New developer account submitting extension with sensitive permissions
465
+ - Relaxed `content_security_policy` (`unsafe-eval`, `unsafe-inline`)
466
+ - First submission of a new extension (always manual)
467
+
468
+ #### Workflow
469
+
470
+ **Step 1 — Lint manifest for over-permissioning**
471
+ Use `Read` on `manifest.json`. For each declared permission, verify it is actually used in source code with `Grep`. Flag any permission declared but not found in `*.ts` / `*.js` source files. Severity: HIGH.
472
+
473
+ Common over-permissioning patterns to flag:
474
+ - `"tabs"` declared when only `activeTab` is needed (activeTab is granted on user click, requires no declaration)
475
+ - `"history"` declared without `chrome.history.*` usage
476
+ - `"bookmarks"` declared without `chrome.bookmarks.*` usage
477
+ - `"<all_urls>"` in `host_permissions` when specific domains suffice
478
+ - `"cookies"` declared without `chrome.cookies.*` usage
479
+
480
+ **Step 2 — Scan for remote code execution**
481
+ Use `Grep` to find patterns that trigger automatic CWS rejection:
482
+
483
+ ```
484
+ pattern: "eval\s*\(" → remote code execution
485
+ pattern: "new Function\s*\(" → remote code execution
486
+ pattern: "<script[^>]+src=['\"]https?://" → remote script loading in HTML files
487
+ pattern: "import\s*\(['\"]https?://" → dynamic import from external URL
488
+ ```
489
+
490
+ Flag each result as CRITICAL — these cause automatic rejection with no appeal path.
491
+
492
+ **Step 3 — Validate Content Security Policy**
493
+ Read the `content_security_policy.extension_pages` value from `manifest.json`. Flag any of:
494
+ - `'unsafe-eval'` in `script-src` — allows eval, triggers rejection
495
+ - `'unsafe-inline'` in `script-src` — allows inline scripts, triggers rejection
496
+ - External domains in `script-src` (anything not `'self'`) — remote code execution risk
497
+ - Missing CSP entirely — defaults to `script-src 'self'` which is fine, but document it
498
+
499
+ **Step 4 — Verify privacy policy**
500
+ Check if the extension collects user data (network requests to external servers, `chrome.storage` usage, content script reading page content). If yes:
501
+ - Privacy policy URL must be set in CWS Developer Dashboard
502
+ - Privacy policy must be publicly accessible (verify URL is live)
503
+ - Generate a minimal privacy policy template if none exists
504
+
505
+ **Step 5 — Check required assets**
506
+ Verify the following exist at declared paths in `manifest.json`:
507
+ - Icon at 128×128px (required for store listing)
508
+ - Screenshots: at least 1, dimensions 1280×800 or 640×400 (PNG or JPEG)
509
+ - Promotional tile: 440×280px (optional but strongly recommended)
510
+ - All declared icons (16, 32, 48, 128px) present at referenced paths
511
+
512
+ Use `Glob` to verify file existence. Use `Bash` to check image dimensions with `file` or `identify` if ImageMagick is available.
513
+
514
+ **Step 6 — Generate permission justification text**
515
+ For each declared permission, generate CWS-ready justification text. The CWS dashboard requires one justification per permission. Justifications must be specific — "We need this to work" is rejected.
516
+
517
+ **Step 7 — Produce preflight report**
518
+ Write `.rune/chrome-ext/preflight-report.md` with:
519
+ - PASS / WARN / FAIL per check
520
+ - Specific file + line for each issue
521
+ - Fix instructions
522
+ - Estimated review timeline (fast-track vs manual review triggers)
523
+ - Submission checklist
524
+
525
+ #### Example
526
+
527
+ ```markdown
528
+ <!-- .rune/chrome-ext/preflight-report.md (generated by cws-preflight) -->
529
+
530
+ # CWS Preflight Report — Page Summarizer v1.0.0
531
+ Generated: 2026-03-12
532
+
533
+ ## Summary
534
+ | Check | Status | Issues |
535
+ |-------|--------|--------|
536
+ | Permissions audit | ⚠️ WARN | 1 over-permission |
537
+ | Remote code execution | ✅ PASS | None found |
538
+ | Content Security Policy | ✅ PASS | Correct default |
539
+ | Privacy policy | ⚠️ WARN | URL not set in manifest |
540
+ | Required assets | ✅ PASS | All present |
541
+ | Permission justifications | ✅ READY | Generated below |
542
+
543
+ ## Issues
544
+
545
+ ### WARN: Over-permission — `"tabs"` not required
546
+ **File**: manifest.json line 7
547
+ **Detail**: `"tabs"` permission is declared but no `chrome.tabs.*` API calls found in source.
548
+ The extension uses `activeTab` (implicit on action click) — remove `"tabs"` from permissions array.
549
+ **Fix**: Remove `"tabs"` from `"permissions"` array.
550
+
551
+ ### WARN: Privacy policy URL missing
552
+ **Detail**: Extension reads page content via content script (content.ts:L12 — `document.body.innerText`).
553
+ This constitutes user data handling and requires a privacy policy URL in the CWS Developer Dashboard.
554
+ **Fix**: Add privacy policy URL at publish time. Template: `.rune/chrome-ext/privacy-policy-template.md`
555
+
556
+ ## Permission Justifications (paste into CWS dashboard)
557
+
558
+ ### activeTab
559
+ "The extension reads the content of the current active tab when the user clicks the toolbar button
560
+ to initiate a summarization. No data is collected without explicit user action."
561
+
562
+ ### storage
563
+ "The extension stores user settings (AI preference, API key, summary length) locally to persist
564
+ preferences between browser sessions. No data is synced externally."
565
+
566
+ ### sidePanel
567
+ "The extension uses the Side Panel API to display AI-generated summaries in a persistent panel
568
+ without obscuring the page content."
569
+
570
+ ## Estimated Review Timeline
571
+ - No sensitive permissions detected
572
+ - No broad host_permissions
573
+ - Timeline: **24–72 hours** (standard review)
574
+ - Recommendation: submit Tuesday–Thursday for fastest turnaround
575
+
576
+ ## Submission Checklist
577
+ - [ ] Remove `"tabs"` from permissions array
578
+ - [ ] Add privacy policy URL to CWS Developer Dashboard
579
+ - [ ] Upload 1280×800 screenshot showing extension in use
580
+ - [ ] Write store description (min 132 chars for detailed description)
581
+ - [ ] Set category: Productivity
582
+ - [ ] Set language: English
583
+ - [ ] $5 one-time developer registration fee paid
584
+ ```
585
+
586
+ ---
587
+
588
+ ### cws-publish
589
+
590
+ Chrome Web Store listing preparation and submission guide — store listing copy, screenshot descriptions, permission justifications, visibility settings, and timeline expectations. Produces a ready-to-paste store listing document.
591
+
592
+ #### Workflow
593
+
594
+ **Step 1 — Verify preflight passed**
595
+ Check for `.rune/chrome-ext/preflight-report.md`. If it does not exist or contains FAIL items, halt and direct user to run `cws-preflight` first. WARN items should be reviewed and resolved before submission.
596
+
597
+ **Step 2 — Prepare store listing copy**
598
+ Generate CWS listing text following Google's constraints:
599
+ - **Name**: max 45 characters. Must not include trademarked names (Google, Chrome, YouTube, Gmail). Cannot include "Extension" (Chrome adds it automatically).
600
+ - **Short description**: max 132 characters. First thing users see in search results — front-load the value proposition.
601
+ - **Detailed description**: no hard limit but 400–800 words is optimal. Structure: opening hook (1 sentence) → feature bullets (5-7) → how it works (2-3 sentences) → privacy statement (1-2 sentences).
602
+ - Avoid keyword stuffing — Google's policy considers it spam.
603
+
604
+ **Step 3 — Generate screenshot descriptions**
605
+ CWS screenshots need captions (optional but recommended). Generate 3-5 screenshot scenarios showing distinct use cases. Each screenshot should be 1280×800 or 640×400 pixels, PNG or JPEG, <2MB.
606
+
607
+ **Step 4 — Fill permission justifications**
608
+ Pull from `cws-preflight` output. Each permission needs a one-paragraph justification in plain English. Write from the user's perspective: "This permission allows the extension to..." not "We need this to...".
609
+
610
+ **Step 5 — Choose visibility and distribution**
611
+ | Visibility | Use Case |
612
+ |------------|----------|
613
+ | Public | Visible in CWS search — default for most extensions |
614
+ | Unlisted | Direct URL only — good for beta testing with known users |
615
+ | Private | Team-only — enterprise internal tools |
616
+
617
+ Select distribution regions (default: all). Consider unlisted for v1.0 while gathering initial feedback, then switch to public after first positive reviews.
618
+
619
+ **Step 6 — Generate submission guide with timeline**
620
+ Emit `.rune/chrome-ext/store-listing.md` with all copy ready to paste. Include submission steps and timeline expectations.
621
+
622
+ **Timeline expectations:**
623
+ - Simple extension, experienced developer account, no sensitive permissions: **24–72 hours**
624
+ - Sensitive permissions (`tabs`, `history`, `cookies`, `management`): **3–7 business days**
625
+ - Broad `host_permissions` or first submission: **up to 3 weeks** (manual review queue)
626
+ - Rejection: **10-day resubmission window** after fixing issues; same review time applies
627
+
628
+ **Submission tips:**
629
+ - Never submit on Friday — reviewers are less available Mon-Tue; submit Tue-Thu
630
+ - Use `optional_permissions` for non-critical features — reduces barrier to install and CWS scrutiny
631
+ - `optional_host_permissions` can be requested at runtime, reducing declared permissions
632
+ - Version bump required for each resubmission after rejection
633
+ - Include a test account in submission notes if extension requires authentication
634
+
635
+ #### Example
636
+
637
+ ```markdown
638
+ <!-- .rune/chrome-ext/store-listing.md (generated by cws-publish) -->
639
+
640
+ # CWS Store Listing — Page Summarizer
641
+
642
+ ## Name (max 45 chars)
643
+ Page Summarizer — AI-Powered Summaries
644
+ (38 chars ✅)
645
+
646
+ ## Short Description (max 132 chars)
647
+ Summarize any webpage instantly with built-in Chrome AI. One click, no account required, no data sent externally.
648
+ (113 chars ✅)
649
+
650
+ ## Detailed Description
651
+ Tired of spending 10 minutes reading a page to find out it wasn't worth your time?
652
+
653
+ **Page Summarizer** gives you the core ideas of any webpage in seconds — powered by Chrome's built-in Gemini Nano model, which runs entirely on your device.
654
+
655
+ **Features:**
656
+ - One-click summarization — click the toolbar button or select text to summarize a section
657
+ - Built-in AI — no API key required, no data leaves your device (requires Chrome 138+ with AI hardware support)
658
+ - External API fallback — configure your own OpenAI or Anthropic key for older hardware
659
+ - Summary length control — short (100 words), medium (300 words), or detailed (500 words)
660
+ - Side panel view — summaries appear in a non-intrusive panel alongside the page
661
+ - Dark mode support
662
+
663
+ **How it works:**
664
+ Click the toolbar button on any page. The extension reads the visible text and generates a summary using the on-device Gemini Nano model. If your hardware does not support built-in AI, the extension falls back to an external API of your choice (optional — extension still works without it in built-in AI mode).
665
+
666
+ **Privacy:**
667
+ No user data is collected, stored, or transmitted without your action. Summaries generated via the built-in AI model never leave your device. External API calls (if configured) are made directly to the API provider — not through any intermediary server.
668
+
669
+ ## Screenshots (1280x800px)
670
+
671
+ 1. **Main Use** — Extension sidebar showing a 3-paragraph summary of a news article beside the original page.
672
+ 2. **Settings** — Settings panel showing AI model selector, API key field, and length preference.
673
+ 3. **Text Selection** — Right-click context menu on selected text showing "Summarize selection" option.
674
+
675
+ ## Category
676
+ Productivity
677
+
678
+ ## Language
679
+ English
680
+
681
+ ## Submission Notes (visible to reviewers, not users)
682
+ Test the extension on https://en.wikipedia.org/wiki/Artificial_intelligence — click the toolbar button to summarize. The extension requires Chrome 138+ for built-in AI. On older Chrome versions, configure an external API key in Settings to test the fallback path.
683
+ ```
684
+
685
+ ---
686
+
687
+ ### ext-ai-integration
688
+
689
+ Chrome built-in AI and external API integration — detect AI type, check hardware requirements, implement Gemini Nano with graceful fallback, wire streaming responses via ports, handle rate limits, and test offline behavior. The differentiating skill for next-generation extensions.
690
+
691
+ **Chrome AI APIs (Chrome 138+ stable):**
692
+ | API | Namespace | Purpose |
693
+ |-----|-----------|---------|
694
+ | Prompt API | `chrome.aiLanguageModel` | General text generation, Q&A, classification |
695
+ | Summarizer | `chrome.aiSummarizer` | Condense long text |
696
+ | Writer | `chrome.aiWriter` | Generate new content from prompts |
697
+ | Rewriter | `chrome.aiRewriter` | Transform existing text (tone, length, format) |
698
+ | Translator | `chrome.aiTranslator` | Language translation |
699
+ | Language Detector | `chrome.aiLanguageDetector` | Detect text language |
700
+
701
+ **Hardware requirements for Gemini Nano:**
702
+ - Storage: 22 GB free disk space (model download)
703
+ - RAM: 4 GB VRAM (dedicated GPU) OR 16 GB system RAM (CPU inference)
704
+ - OS: macOS 13+, Windows 10/11 64-bit, ChromeOS (no Linux support)
705
+ - Cannot be checked programmatically — use capability API and handle `NotSupportedError`
706
+
707
+ **Manifest permission:**
708
+ ```json
709
+ { "permissions": ["aiLanguageModelParams"] }
710
+ ```
711
+
712
+ #### Workflow
713
+
714
+ **Step 1 — Detect AI integration type**
715
+ Use `Read` on existing source and `manifest.json` to determine:
716
+ - Does `"aiLanguageModelParams"` appear in permissions? → Built-in Nano intended
717
+ - Does code reference `openai`, `anthropic`, `fetch` to an external AI endpoint? → External API
718
+ - Neither? → Need to design integration from scratch
719
+
720
+ Ask the user: "Do you want to use Chrome's built-in Gemini Nano (no API cost, runs on device, requires Chrome 138+ and compatible hardware), an external API (OpenAI/Anthropic, requires API key and network), or both with automatic fallback?"
721
+
722
+ **Step 2 — Check hardware capability for Nano**
723
+ `chrome.aiLanguageModel.capabilities()` returns `{ available: 'readily' | 'after-download' | 'no' }`. Map these:
724
+ - `'readily'` → model is downloaded, use immediately
725
+ - `'after-download'` → model needs download (~2GB), show progress UI and wait
726
+ - `'no'` → hardware not supported, fall through to fallback
727
+
728
+ This check MUST happen in the service worker (not content script — restricted APIs). Cache the result in `chrome.storage.session` to avoid repeated capability checks.
729
+
730
+ **Step 3 — Implement with graceful fallback chain**
731
+ Fallback chain: Gemini Nano → External API → Static response
732
+
733
+ Each tier is a distinct function with the same signature. The orchestrator tries each in order, catching `NotSupportedError`, network errors, and quota errors.
734
+
735
+ **Step 4 — Wire streaming responses via port messaging**
736
+ AI streaming MUST use ports — not `sendMessage`. `sendMessage` is one-shot: the response is sent once and the channel closes. Streaming requires a port to send multiple `CHUNK` messages followed by a `DONE` message.
737
+
738
+ See `ext-messaging` skill for port setup. Streaming pattern:
739
+ 1. Sidebar/popup opens a port named `'ai-stream'`
740
+ 2. Sends `{ text: inputText }` to start generation
741
+ 3. Service worker receives, calls `session.promptStreaming()`
742
+ 4. For each chunk in the async iterator, posts `{ type: 'CHUNK', content: chunk }` back on the port
743
+ 5. On completion, posts `{ type: 'DONE' }` and calls `session.destroy()`
744
+
745
+ **Step 5 — Handle rate limits and quota**
746
+ Chrome built-in AI has per-session token limits. External APIs have rate limits and cost.
747
+ - Per session: call `session.destroy()` after each summary to free context window
748
+ - External API: implement exponential backoff on 429 responses (1s, 2s, 4s, cap 30s)
749
+ - User-facing: show token usage in settings panel if using external API
750
+
751
+ **Step 6 — Test offline behavior**
752
+ Extensions may run without network. Test:
753
+ - Built-in Nano: works offline (on-device model)
754
+ - External API: fails offline — catch `TypeError: Failed to fetch` and show "No network connection" message
755
+ - Storage: `chrome.storage.local` works offline
756
+ - Service worker: registers and responds to messages offline
757
+
758
+ #### Example
759
+
760
+ ```typescript
761
+ // src/lib/ai.ts — AI integration with graceful fallback
762
+ import { storageGet } from './storage';
763
+
764
+ export interface AiSummaryResult {
765
+ summary: string;
766
+ source: 'builtin' | 'external' | 'error';
767
+ error?: string;
768
+ }
769
+
770
+ // Check and cache Nano capability
771
+ export async function getNanoCapability(): Promise<'readily' | 'after-download' | 'no'> {
772
+ // Check session cache first (avoid repeated API calls)
773
+ const cached = await chrome.storage.session.get('nanoCapability');
774
+ if (cached['nanoCapability']) return cached['nanoCapability'] as 'readily' | 'after-download' | 'no';
775
+
776
+ const caps = await chrome.aiLanguageModel.capabilities();
777
+ await chrome.storage.session.set({ nanoCapability: caps.available });
778
+ return caps.available;
779
+ }
780
+
781
+ // Tier 1: Gemini Nano (built-in, on-device)
782
+ async function summarizeWithNano(text: string): Promise<string> {
783
+ const capability = await getNanoCapability();
784
+
785
+ if (capability === 'no') {
786
+ throw new Error('NotSupportedError: Built-in AI not available on this device');
787
+ }
788
+
789
+ if (capability === 'after-download') {
790
+ // Notify UI that model is downloading — caller can show progress
791
+ // Download starts automatically when create() is called
792
+ chrome.runtime.sendMessage({ type: 'AI_DOWNLOADING' });
793
+ }
794
+
795
+ const session = await chrome.aiLanguageModel.create({
796
+ systemPrompt: 'You are a concise summarizer. Summarize the provided text in 3-5 sentences.',
797
+ });
798
+
799
+ try {
800
+ const summary = await session.prompt(
801
+ `Summarize this text:\n\n${text.slice(0, 4000)}` // context window limit
802
+ );
803
+ return summary;
804
+ } finally {
805
+ session.destroy(); // always destroy to free resources
806
+ }
807
+ }
808
+
809
+ // Tier 2: External API (OpenAI-compatible)
810
+ async function summarizeWithExternalApi(text: string): Promise<string> {
811
+ const settings = await storageGet('settings');
812
+ if (!settings.externalApiKey) {
813
+ throw new Error('No external API key configured');
814
+ }
815
+
816
+ const controller = new AbortController();
817
+ const timeoutId = setTimeout(() => controller.abort(), 30_000);
818
+
819
+ try {
820
+ const response = await fetch('https://api.openai.com/v1/chat/completions', {
821
+ method: 'POST',
822
+ headers: {
823
+ 'Content-Type': 'application/json',
824
+ Authorization: `Bearer ${settings.externalApiKey}`,
825
+ },
826
+ body: JSON.stringify({
827
+ model: 'gpt-4o-mini',
828
+ messages: [
829
+ { role: 'system', content: 'Summarize the provided text in 3-5 sentences.' },
830
+ { role: 'user', content: text.slice(0, 8000) },
831
+ ],
832
+ max_tokens: 300,
833
+ }),
834
+ signal: controller.signal,
835
+ });
836
+
837
+ if (!response.ok) {
838
+ if (response.status === 429) throw new Error('RateLimitError');
839
+ throw new Error(`API error: ${response.status}`);
840
+ }
841
+
842
+ const data = await response.json() as {
843
+ choices: Array<{ message: { content: string } }>;
844
+ };
845
+ return data.choices[0]?.message.content ?? '';
846
+ } finally {
847
+ clearTimeout(timeoutId);
848
+ }
849
+ }
850
+
851
+ // Orchestrator — tries each tier in order
852
+ export async function summarize(text: string): Promise<AiSummaryResult> {
853
+ const settings = await storageGet('settings');
854
+
855
+ if (settings.useBuiltinAI) {
856
+ try {
857
+ const summary = await summarizeWithNano(text);
858
+ return { summary, source: 'builtin' };
859
+ } catch (err) {
860
+ console.warn('[AI] Nano failed, falling back to external API:', err);
861
+ }
862
+ }
863
+
864
+ if (settings.externalApiKey) {
865
+ try {
866
+ const summary = await summarizeWithExternalApi(text);
867
+ return { summary, source: 'external' };
868
+ } catch (err) {
869
+ console.error('[AI] External API failed:', err);
870
+ return {
871
+ summary: '',
872
+ source: 'error',
873
+ error: err instanceof Error ? err.message : 'Unknown error',
874
+ };
875
+ }
876
+ }
877
+
878
+ return {
879
+ summary: '',
880
+ source: 'error',
881
+ error: 'No AI source available. Enable built-in AI or configure an external API key in Settings.',
882
+ };
883
+ }
884
+ ```
885
+
886
+ ```typescript
887
+ // Streaming with port (service worker side)
888
+ // background.ts
889
+ chrome.runtime.onConnect.addListener((port) => {
890
+ if (port.name !== 'ai-stream') return;
891
+
892
+ let session: chrome.aiLanguageModel.LanguageModel | null = null;
893
+
894
+ port.onMessage.addListener(async (message: { text: string }) => {
895
+ try {
896
+ const capability = await getNanoCapability();
897
+ if (capability === 'no') throw new Error('NotSupportedError');
898
+
899
+ session = await chrome.aiLanguageModel.create({
900
+ systemPrompt: 'Summarize concisely.',
901
+ });
902
+
903
+ const stream = session.promptStreaming(
904
+ `Summarize:\n\n${message.text.slice(0, 4000)}`
905
+ );
906
+
907
+ let previous = '';
908
+ for await (const chunk of stream) {
909
+ // Chrome's streaming returns cumulative text — extract the delta
910
+ const delta = chunk.slice(previous.length);
911
+ previous = chunk;
912
+ port.postMessage({ type: 'CHUNK', content: delta });
913
+ }
914
+
915
+ port.postMessage({ type: 'DONE' });
916
+ } catch (err) {
917
+ port.postMessage({ type: 'ERROR', error: String(err) });
918
+ } finally {
919
+ session?.destroy();
920
+ session = null;
921
+ }
922
+ });
923
+
924
+ port.onDisconnect.addListener(() => {
925
+ session?.destroy();
926
+ session = null;
927
+ });
928
+ });
929
+ ```
930
+
931
+ ---
932
+
933
+ ## Connections
934
+
935
+ ```
936
+ Calls → sentinel (L2): security audit on permissions, CSP, and storage patterns
937
+ Calls → verification (L3): validate TypeScript types, run extension build
938
+ Calls → git (L3): semantic commit after scaffold or publish prep
939
+ Called By ← cook (L1): when Chrome extension project context detected
940
+ Called By ← scaffold (L1): when user requests new browser extension project
941
+ Called By ← launch (L1): pre-flight check before CWS submission
942
+ Called By ← preflight (L2): runs cws-preflight as part of broader pre-deploy audit
943
+ ```
944
+
945
+ ## Tech Stack Support
946
+
947
+ | Build Tool | Plugin | Hot Reload | Notes |
948
+ |------------|--------|------------|-------|
949
+ | Vite 5 | @crxjs/vite-plugin | Yes | Best DX — recommended for MV3 |
950
+ | Webpack 5 | chrome-extension-webpack | Partial | Mature, more config overhead |
951
+ | Parcel 2 | @parcel/config-webextension | Yes | Zero-config option |
952
+ | Vanilla tsc | Manual copy scripts | No | Fine for simple extensions |
953
+
954
+ | API | Min Chrome Version | Notes |
955
+ |-----|-------------------|-------|
956
+ | chrome.sidePanel | 114 | Sidebar panel (replaces popup for persistent UI) |
957
+ | chrome.aiLanguageModel | 138 | Gemini Nano — built-in LLM |
958
+ | chrome.aiSummarizer | 138 | Specialized summarization API |
959
+ | chrome.offscreen | 109 | Background DOM/audio access workaround |
960
+ | chrome.storage.session | 102 | Session storage surviving SW termination |
961
+
962
+ ## Constraints
963
+
964
+ 1. MUST register ALL chrome.* event listeners synchronously at the top level of the service worker — listeners registered inside async functions, Promises, or setTimeout are silently ignored after the first service worker termination.
965
+ 2. MUST NOT store any state in service worker JS variables that must survive beyond the current event — use `chrome.storage.session` for ephemeral state and `chrome.storage.local` for persistent state.
966
+ 3. MUST NOT load scripts from external URLs — no CDN `<script>` tags in HTML files, no `import()` from external URLs — these trigger automatic CWS rejection with no appeal path.
967
+ 4. MUST check `chrome.aiLanguageModel.capabilities()` before calling `create()` and implement graceful fallback — hardware requirements (22GB disk, 4GB VRAM or 16GB RAM) are not met on most user machines.
968
+ 5. MUST use ports (not sendMessage) for streaming AI responses — sendMessage is one-shot and cannot carry multiple chunks.
969
+
970
+ ## Sharp Edges
971
+
972
+ | Failure Mode | Severity | Mitigation |
973
+ |---|---|---|
974
+ | Event listener registered inside `addEventListener('load', ...)` or async IIFE — silently ignored after SW termination | CRITICAL | Grep for `onMessage.addListener` not at module top level; scaffold always generates top-level listeners |
975
+ | `setTimeout` keepalive hack breaks on Chrome 119+ — Chrome patched the timeout extension trick | HIGH | Use `chrome.alarms` for periodic work; use `chrome.storage.session` for state; never rely on SW staying alive |
976
+ | `sendMessage` returns `undefined` when no listener responds — mistaken for success | HIGH | Check `chrome.runtime.lastError` in callback; use typed response interface that includes `error?: string` |
977
+ | Streaming AI returns cumulative text (not delta chunks) — UI duplicates content | HIGH | Slice previous from current: `const delta = chunk.slice(prev.length); prev = chunk` |
978
+ | `chrome.tabs.sendMessage` throws when content script not yet injected or tab is restricted | HIGH | Wrap in try/catch; check `sender.tab` exists; use `executeScript` to inject first if needed |
979
+ | Extension passes local testing but fails CWS review for `eval()` in bundled node_modules | CRITICAL | Run `grep -r "eval(" node_modules/` before submission; replace or patch offending dependency |
980
+
981
+ ## Done When
982
+
983
+ - `manifest.json` has no declared permissions absent from source code (verified by Grep)
984
+ - Service worker registers all listeners synchronously at module top level — no listener inside async function
985
+ - `chrome.storage` is used for all state — no JS variables relied upon to survive termination
986
+ - No `eval()`, `Function()`, remote `<script>` tags, or external `import()` in any source or bundled file
987
+ - `cws-preflight` report shows no FAIL items and WARN items are reviewed
988
+ - `chrome.aiLanguageModel.capabilities()` is checked before use and graceful fallback is implemented
989
+ - Streaming AI uses port-based messaging and correctly extracts deltas from cumulative chunks
990
+ - Store listing copy is under character limits, permission justifications are written in plain English
991
+ - Extension loads in Chrome via `chrome://extensions → Load unpacked` without errors
992
+
993
+ ## Cost Profile
994
+
995
+ ~1,500–3,000 tokens per skill activation. `haiku` for file scans (Grep, Glob, manifest reading); `sonnet` for scaffold generation, storage schema, and message type definitions; `sonnet` for cws-preflight audit and store listing copy; `sonnet` for AI integration wiring. Full pack activation (all 6 skills) runs ~12,000–18,000 tokens end-to-end. `cws-preflight` is the heaviest single skill (~3,000 tokens) due to multi-pass scanning.