@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.
- package/LICENSE +21 -0
- package/README.md +357 -0
- package/agents/.gitkeep +0 -0
- package/agents/architect.md +29 -0
- package/agents/asset-creator.md +11 -0
- package/agents/audit.md +11 -0
- package/agents/autopsy.md +11 -0
- package/agents/brainstorm.md +11 -0
- package/agents/browser-pilot.md +11 -0
- package/agents/coder.md +29 -0
- package/agents/completion-gate.md +11 -0
- package/agents/constraint-check.md +11 -0
- package/agents/context-engine.md +11 -0
- package/agents/cook.md +11 -0
- package/agents/db.md +11 -0
- package/agents/debug.md +11 -0
- package/agents/dependency-doctor.md +11 -0
- package/agents/deploy.md +11 -0
- package/agents/design.md +11 -0
- package/agents/docs-seeker.md +11 -0
- package/agents/fix.md +11 -0
- package/agents/hallucination-guard.md +11 -0
- package/agents/incident.md +11 -0
- package/agents/integrity-check.md +11 -0
- package/agents/journal.md +11 -0
- package/agents/launch.md +11 -0
- package/agents/logic-guardian.md +11 -0
- package/agents/marketing.md +11 -0
- package/agents/onboard.md +11 -0
- package/agents/perf.md +11 -0
- package/agents/plan.md +11 -0
- package/agents/preflight.md +11 -0
- package/agents/problem-solver.md +11 -0
- package/agents/rescue.md +11 -0
- package/agents/research.md +11 -0
- package/agents/researcher.md +29 -0
- package/agents/review-intake.md +11 -0
- package/agents/review.md +11 -0
- package/agents/reviewer.md +28 -0
- package/agents/safeguard.md +11 -0
- package/agents/sast.md +11 -0
- package/agents/scanner.md +28 -0
- package/agents/scope-guard.md +11 -0
- package/agents/scout.md +11 -0
- package/agents/sentinel.md +11 -0
- package/agents/sequential-thinking.md +11 -0
- package/agents/session-bridge.md +11 -0
- package/agents/skill-forge.md +11 -0
- package/agents/skill-router.md +11 -0
- package/agents/surgeon.md +11 -0
- package/agents/team.md +11 -0
- package/agents/test.md +11 -0
- package/agents/trend-scout.md +11 -0
- package/agents/verification.md +11 -0
- package/agents/video-creator.md +11 -0
- package/agents/watchdog.md +11 -0
- package/agents/worktree.md +11 -0
- package/commands/.gitkeep +0 -0
- package/commands/rune.md +168 -0
- package/compiler/__tests__/openclaw-adapter.test.js +140 -0
- package/compiler/__tests__/parser.test.js +55 -0
- package/compiler/adapters/antigravity.js +59 -0
- package/compiler/adapters/claude.js +37 -0
- package/compiler/adapters/cursor.js +67 -0
- package/compiler/adapters/generic.js +60 -0
- package/compiler/adapters/index.js +45 -0
- package/compiler/adapters/openclaw.js +150 -0
- package/compiler/adapters/windsurf.js +60 -0
- package/compiler/bin/rune.js +288 -0
- package/compiler/doctor.js +153 -0
- package/compiler/emitter.js +240 -0
- package/compiler/parser.js +208 -0
- package/compiler/transformer.js +69 -0
- package/compiler/transforms/branding.js +27 -0
- package/compiler/transforms/cross-references.js +29 -0
- package/compiler/transforms/frontmatter.js +38 -0
- package/compiler/transforms/hooks.js +68 -0
- package/compiler/transforms/subagents.js +36 -0
- package/compiler/transforms/tool-names.js +60 -0
- package/contexts/dev.md +34 -0
- package/contexts/research.md +43 -0
- package/contexts/review.md +55 -0
- package/extensions/ai-ml/PACK.md +517 -0
- package/extensions/analytics/PACK.md +557 -0
- package/extensions/backend/PACK.md +678 -0
- package/extensions/chrome-ext/PACK.md +995 -0
- package/extensions/content/PACK.md +381 -0
- package/extensions/devops/PACK.md +520 -0
- package/extensions/ecommerce/PACK.md +280 -0
- package/extensions/gamedev/PACK.md +393 -0
- package/extensions/mobile/PACK.md +273 -0
- package/extensions/saas/PACK.md +805 -0
- package/extensions/security/PACK.md +536 -0
- package/extensions/trading/PACK.md +597 -0
- package/extensions/ui/PACK.md +947 -0
- package/package.json +47 -0
- package/skills/.gitkeep +0 -0
- package/skills/adversary/SKILL.md +271 -0
- package/skills/asset-creator/SKILL.md +157 -0
- package/skills/audit/SKILL.md +466 -0
- package/skills/autopsy/SKILL.md +200 -0
- package/skills/ba/SKILL.md +279 -0
- package/skills/brainstorm/SKILL.md +266 -0
- package/skills/browser-pilot/SKILL.md +168 -0
- package/skills/completion-gate/SKILL.md +151 -0
- package/skills/constraint-check/SKILL.md +165 -0
- package/skills/context-engine/SKILL.md +176 -0
- package/skills/cook/SKILL.md +636 -0
- package/skills/db/SKILL.md +256 -0
- package/skills/debug/SKILL.md +240 -0
- package/skills/dependency-doctor/SKILL.md +235 -0
- package/skills/deploy/SKILL.md +174 -0
- package/skills/design/DESIGN-REFERENCE.md +365 -0
- package/skills/design/SKILL.md +462 -0
- package/skills/doc-processor/SKILL.md +254 -0
- package/skills/docs/SKILL.md +336 -0
- package/skills/docs-seeker/SKILL.md +166 -0
- package/skills/fix/SKILL.md +192 -0
- package/skills/git/SKILL.md +285 -0
- package/skills/hallucination-guard/SKILL.md +204 -0
- package/skills/incident/SKILL.md +241 -0
- package/skills/integrity-check/SKILL.md +169 -0
- package/skills/journal/SKILL.md +190 -0
- package/skills/launch/SKILL.md +330 -0
- package/skills/logic-guardian/SKILL.md +240 -0
- package/skills/marketing/SKILL.md +229 -0
- package/skills/mcp-builder/SKILL.md +311 -0
- package/skills/onboard/SKILL.md +298 -0
- package/skills/perf/SKILL.md +297 -0
- package/skills/plan/SKILL.md +520 -0
- package/skills/preflight/SKILL.md +231 -0
- package/skills/problem-solver/SKILL.md +284 -0
- package/skills/rescue/SKILL.md +434 -0
- package/skills/research/SKILL.md +122 -0
- package/skills/review/SKILL.md +354 -0
- package/skills/review-intake/SKILL.md +222 -0
- package/skills/safeguard/SKILL.md +188 -0
- package/skills/sast/SKILL.md +190 -0
- package/skills/scaffold/SKILL.md +276 -0
- package/skills/scope-guard/SKILL.md +150 -0
- package/skills/scout/SKILL.md +232 -0
- package/skills/sentinel/SKILL.md +320 -0
- package/skills/sentinel-env/SKILL.md +226 -0
- package/skills/sequential-thinking/SKILL.md +234 -0
- package/skills/session-bridge/SKILL.md +287 -0
- package/skills/skill-forge/SKILL.md +317 -0
- package/skills/skill-router/SKILL.md +267 -0
- package/skills/surgeon/SKILL.md +203 -0
- package/skills/team/SKILL.md +397 -0
- package/skills/test/SKILL.md +271 -0
- package/skills/trend-scout/SKILL.md +145 -0
- package/skills/verification/SKILL.md +201 -0
- package/skills/video-creator/SKILL.md +201 -0
- package/skills/watchdog/SKILL.md +166 -0
- 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.
|