Vibecode PromptDC
track this build4 phases, 12 steps, beginner friendly0%A useful personal version is highly one-shot-able: an MV3 content script can capture selected text, send it to an LLM with one of two system prompts, then replace a normal editable field or copy the result. The commercial product earns its keep through Writing profiles (grammar fix, email, social post, shorten, tone), a Coding mode that auto-detects which AI tool you are prompting and tailors the rewrite, polished interaction design, and defensive handling of complex editors where naive DOM replacement can fail or corrupt text.
You are building a lean indie version of PromptDC. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # PromptDC · indie build A browser extension whose hard problems are putting rewritten text back into rich editors without corrupting the document, surviving a background worker that is shut down mid-request, and keeping the API key away from every page you visit. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A third generation manifest extension, plain HTML, CSS and JavaScript, no build step | the whole thing is four files and a bundler only adds ways for it to break | | Background | A service worker that assumes it will be killed at any moment | an idle worker is shut down in seconds and a slow model call outlives it | | Storage | The local extension storage area for settings and in-flight requests, never the synced area | an API key does not belong in anything that leaves the machine by itself | | Insertion | Range-based replacement with events dispatched, clipboard as the honest fallback | framework-managed editors ignore text you put in without telling them | | Permissions | Optional host permissions requested per site, never every site up front | an extension that can read every page you open should have to ask | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A browser supporting third generation extension manifests, in developer mode** · free - Why: You will load the folder unpacked dozens of times, and the worker lifecycle behaviour you are designing around is only observable in the real browser. - Get it: Enable developer mode on the extensions page and use Load unpacked. - Verify: The extension loads and its service worker is visible and can be stopped from the extensions page - [ ] **An API key for a text model provider** · pay per call; a few rewrites a day is small change, and the ceiling here is what keeps it that way - Why: Every rewrite is one call, and this is the only cost in the project. - Get it: Create a key and set a spend limit in the provider's console. - Verify: The options page test button returns a short completion - [ ] **A test page with all four insertion cases** · free - Why: Insertion is where this category of extension fails, and you cannot test it against a live site you might corrupt. - Get it: Write one local page with an input, a textarea, an editable element inside a framework, and a read-only paragraph. - Verify: A rewrite in each case behaves as specified, including the clipboard fallback - [ ] **Which sites this is allowed to run on** · free - Why: This decides the install prompt, which is the single thing that makes an extension feel either useful or invasive. - Get it: List the handful of sites you actually rewrite text on, and design the permission flow around asking per site. - Verify: The manifest declares no broad host access and the flow works on a site granted at runtime ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Accounts, sync and hosted credits. - Guaranteed compatibility with every web editor. - A history of everything you rewrote. - A backend server. - Writing profiles tuned for grammar fix, email replies, social posts, shortening, and tone - Coding mode platform detection that tailors the rewrite to the AI tool you are prompting (ChatGPT, Claude, Cursor, Lovable and more) - Reliable behavior across Gmail, LinkedIn, Google Docs, Shadow DOM, and framework-managed rich text editors - Cloud accounts, usage credits, profile sync, and a managed billing flow - Polished mode controls, error recovery, ongoing compatibility updates, and support If one of those is essential to you, that is the reason to keep paying for PromptDC, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · PromptDC The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a browser extension that rewrites the text you selected and puts it back without mangling the page to replace PromptDC. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: A third generation manifest extension, plain HTML, CSS and JavaScript, no build step · the whole thing is four files and a bundler only adds ways for it to break - Background: A service worker that assumes it will be killed at any moment · an idle worker is shut down in seconds and a slow model call outlives it - Storage: The local extension storage area for settings and in-flight requests, never the synced area · an API key does not belong in anything that leaves the machine by itself - Insertion: Range-based replacement with events dispatched, clipboard as the honest fallback · framework-managed editors ignore text you put in without telling them - Permissions: Optional host permissions requested per site, never every site up front · an extension that can read every page you open should have to ask ### Data model (create this before Phase 1) - settings(provider, endpoint, model_id, updated_at) · in the local storage area only, and the key is stored separately from anything ever sent to a page - profiles(id, name, system_prompt, order, built_in, edited_at) · plain editable text the user can read before anything is sent - requests(id, tab_id, profile_id, started_at, finished_at, state, error) · state is pending, running, done or failed, and it lives in storage rather than a variable - captures(request_id, kind, container_signature, start_offset, end_offset, text) · kind is input, textarea, contenteditable or readonly - usage(month, requests, input_chars, output_chars, estimated_cost_minor) · against a ceiling you set - The key appears in exactly one place and is never written into a request record, a log line, or anything a content script can read. ### Phase 1 · Capture the selection, and know what kind it is Build: Detect three cases and record which one you have: a value selection in an input or textarea with its start and end offsets, a range inside an editable element, and a selection you cannot write back to. Capture immediately on the user's action, store it against a request identifier, and never rely on the live selection surviving anything. Trap: Reading the selection again after the model responds finds nothing, because clicking your own button, switching tabs or the page re-rendering has already collapsed it, and the result then lands at the caret or nowhere at all. Capture offsets and a way to find the container at the moment of the click, store them, and treat the live selection as gone from that instant. Done when: A selection is captured and stored before any network call, verified by inspecting storage mid-request. All three cases are distinguished and recorded, tested on an input, a rich editor and a read-only paragraph. Clicking the extension's own button never collapses the captured selection, tested by hand on a rich editor. A selection inside a shadow root is either captured correctly or reported as unsupported with a message, never silently mishandled. Do not build yet: No model call, insertion, profiles or permissions work yet. ### Phase 2 · A request that survives the worker being killed Build: Keep request state in extension storage, never in a variable in the worker. The content script holds a long-lived connection while a request runs, and reconnects if it drops. On worker startup, reconcile: any request left running is either resumed or marked failed with a reason. Enforce your own timeout well inside the platform's per-request ceiling. Trap: Holding the in-flight request in a variable in the background worker loses it the moment the worker is shut down, and the documented behaviour is that an extension service worker is terminated after thirty seconds of inactivity with a five minute ceiling on processing a single request, so a slow rewrite simply vanishes with no error anywhere. Put the request state in storage, keep a connection open from the content script while it runs, and reconcile orphaned requests on startup. Done when: Request state lives in extension storage and no in-flight data is held in a worker variable, verified by review. Forcing the worker to stop mid-request leaves a recoverable state that reconciles on the next event rather than hanging, tested from the extensions page. A request exceeding the configured timeout fails with a message naming the timeout, tested against a slow endpoint. Reloading the extension mid-request removes the injected interface cleanly rather than leaving a dead button. Do not build yet: No insertion, profiles, permissions or cost ceiling yet. ### Phase 3 · Putting the text back without destroying the document Build: For an input or textarea, splice the value between the stored offsets, restore the caret after the inserted text, and dispatch input and change events that bubble. For an editable element, restore the stored range and replace its contents with a text node, then dispatch a bubbling input event. Never assign markup. If the container is gone or the range no longer resolves, copy and say so. Trap: Writing the result with an assignment to the element's markup destroys everything else in that editor, and in a framework-managed field the change is invisible to the framework, so the old text reappears on the next render and the work is gone. Replace only the captured range, use a text node, dispatch events that bubble, and fall back to the clipboard whenever the range cannot be resolved. Done when: Replacing a selection in the middle of a paragraph leaves the surrounding text and formatting untouched, tested in a rich editor. Input and change events bubble from every insertion path, verified with a listener in a test page. No code path assigns markup, verified by review. A stale or unresolvable range results in the text on the clipboard and a visible notice, never a partial write. Do not build yet: No profile editing, permission flow or cost ceiling yet. ### Phase 4 · Profiles you can read before you send Build: Ship a small set of profiles as editable text: improve, fix grammar, shorten, reply, and one that turns a vague request into a specification with goal, constraints and acceptance criteria. Show the full system prompt on the options page. Allow editing, duplication and reset to the shipped version. Record which profile produced each result. Trap: Burying the prompts in the extension's code means the user cannot tell why a rewrite keeps changing their meaning, and the only remedy available to them is to stop using it. Put the prompt text in front of them, let them edit it, and keep a reset so an experiment is never permanent. Done when: Every profile's full system prompt is visible and editable on the options page, checked by opening it. Editing a profile changes the next request and reset restores the shipped text exactly, tested. Each profile instructs the model to return only the revised text and to keep the input language, verified by reading them. The result view names which profile produced it. Do not build yet: No permission flow, usage ceiling or export yet. ### Phase 5 · Permissions, the key and the bill Build: Request host access for the current site when first used there, rather than declaring every site in the manifest. Keep the key in the local storage area and never send it to a content script. Count characters in and out per month against a ceiling that stops requests with a clear message. Export and import profiles as a file. Behave predictably on pages where extensions cannot run. Trap: Declaring access to every site up front means the extension can read every page you open, including your bank and your email, for a feature you use on four sites, and the browser tells the user exactly that at install time. Ask per site on first use, and let someone run it on one domain without granting the rest. Done when: The manifest declares no broad host access and permission is requested per site on first use, verified by reading the install prompt. The key is never present in any message sent to a content script, verified by logging every message shape in a test build. Reaching the monthly character ceiling stops requests with a message naming the ceiling and the reset date, tested. On a restricted browser page the extension shows a short explanation instead of failing silently. Do not build yet: Nothing after this. ### Out of scope (and why) - Accounts, sync and hosted credits, which turn a local tool into a service with a billing relationship and somebody else's uptime. - Reliable behaviour inside every rich editor on the web, which is the paid product's actual work and is a standing maintenance commitment rather than a feature. - Rewrite history, because keeping a copy of everything you selected anywhere on the web is a bigger liability than the feature is worth. - A backend server of your own, since every request this extension makes can go straight from the worker to the model provider. ### README must contain - That the background worker is shut down on a short idle timer, which is why request state lives in storage and why a long rewrite can be reconciled rather than lost - That the key is stored in the local extension storage area only, is never sent to a content script, and is never logged - That host access is requested per site on first use rather than declared for every site in the manifest - That replacement never assigns markup and falls back to the clipboard when a range cannot be resolved - Where the profiles live, how to edit and reset them, and what the monthly usage ceiling does when it is reached ===== AGENTS.md ===== # Agent instructions · PromptDC indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A third generation manifest extension, plain HTML, CSS and JavaScript, no build step, A service worker that assumes it will be killed at any moment, The local extension storage area for settings and in-flight requests, never the synced area, Range-based replacement with events dispatched, clipboard as the honest fallback, Optional host permissions requested per site, never every site up front. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Reading the selection again after the model responds finds nothing, because clicking your own button, switching tabs or the page re-rendering has already collapsed it, and the result then lands at the caret or nowhere at all. Capture offsets and a way to find the container at the moment of the click, store them, and treat the live selection as gone from that instant. - Holding the in-flight request in a variable in the background worker loses it the moment the worker is shut down, and the documented behaviour is that an extension service worker is terminated after thirty seconds of inactivity with a five minute ceiling on processing a single request, so a slow rewrite simply vanishes with no error anywhere. Put the request state in storage, keep a connection open from the content script while it runs, and reconcile orphaned requests on startup. - Writing the result with an assignment to the element's markup destroys everything else in that editor, and in a framework-managed field the change is invisible to the framework, so the old text reappears on the next render and the work is gone. Replace only the captured range, use a text node, dispatch events that bubble, and fall back to the clipboard whenever the range cannot be resolved. - Burying the prompts in the extension's code means the user cannot tell why a rewrite keeps changing their meaning, and the only remedy available to them is to stop using it. Put the prompt text in front of them, let them edit it, and keep a reset so an experiment is never permanent. ===== BUILD_PLAN.md ===== # Build plan · PromptDC A browser extension whose hard problems are putting rewritten text back into rich editors without corrupting the document, surviving a background worker that is shut down mid-request, and keeping the API key away from every page you visit. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Capture the selection, and know what kind it is The selection is captured with everything needed to restore it before any asynchronous work starts. ### Steps 1. Capture at the click, not at the response By the time the response arrives the selection is gone. Files: `content.js` 2. Record which of the three cases you are in Each one has a different way back, and guessing later is how text gets destroyed. Files: `content.js` 3. Store the capture against a request identifier The worker that answers may not be the one that asked. Files: `content.js`, `background.js` ### Done when - [ ] The selection is stored before any network call - [ ] All three selection cases are distinguished - [ ] The extension's own button does not collapse the selection - [ ] Shadow roots are handled or honestly refused ### Watch out - Reading the selection again after the model responds finds nothing, because clicking your own button, switching tabs or the page re-rendering has already collapsed it, and the result then lands at the caret or nowhere at all. Capture offsets and a way to find the container at the moment of the click, store them, and treat the live selection as gone from that instant. ## Phase 2 · A request that survives the worker being killed The model call is resumable, because the background worker is shut down on a short idle timer. ### Steps 1. Put every piece of request state in storage The worker is disposable and behaves like it. Files: `background.js` 2. Keep a connection open while a request runs It is what stops the worker being shut down underneath you. Files: `content.js`, `background.js` 3. Reconcile orphaned requests on startup Something must decide what happened to the one that vanished. Files: `background.js` ### Done when - [ ] No in-flight state lives in a worker variable - [ ] Stopping the worker mid-request is recoverable - [ ] A timeout fails with a clear message - [ ] Reloading the extension cleans up injected interface ### Watch out - Holding the in-flight request in a variable in the background worker loses it the moment the worker is shut down, and the documented behaviour is that an extension service worker is terminated after thirty seconds of inactivity with a five minute ceiling on processing a single request, so a slow rewrite simply vanishes with no error anywhere. Put the request state in storage, keep a connection open from the content script while it runs, and reconcile orphaned requests on startup. ## Phase 3 · Putting the text back without destroying the document Replacement affects only the captured range and the page's own editor notices the change. ### Steps 1. Splice by offsets for inputs, restore the caret The surrounding text is somebody's work in progress. Files: `content.js` 2. Replace range contents with a text node, never markup Assigning markup destroys the rest of the editor. Files: `content.js` 3. Dispatch bubbling input and change events A framework that did not hear about it will undo it. Files: `content.js` ### Done when - [ ] Surrounding text and formatting survive a replacement - [ ] Input and change events bubble on every path - [ ] No path assigns markup - [ ] An unresolvable range copies and says so ### Watch out - Writing the result with an assignment to the element's markup destroys everything else in that editor, and in a framework-managed field the change is invisible to the framework, so the old text reappears on the next render and the work is gone. Replace only the captured range, use a text node, dispatch events that bubble, and fall back to the clipboard whenever the range cannot be resolved. ## Phase 4 · Profiles you can read before you send Every rewrite mode is an editable prompt the user can see, change and reset. ### Steps 1. Ship prompts as editable text, not as code The user should be able to see what is being said about their writing. Files: `options.html`, `options.js` 2. Keep a reset to the shipped version An experiment nobody can undo is not an experiment. Files: `options.js` 3. Record the profile on every result Otherwise you cannot tell which prompt produced the odd output. Files: `background.js` ### Done when - [ ] Prompts are visible and editable - [ ] Reset restores the shipped text exactly - [ ] Every profile returns only revised text - [ ] Results name the profile that produced them ### Watch out - Burying the prompts in the extension's code means the user cannot tell why a rewrite keeps changing their meaning, and the only remedy available to them is to stop using it. Put the prompt text in front of them, let them edit it, and keep a reset so an experiment is never permanent. ## Not in this build - Accounts, sync and hosted credits. - Guaranteed compatibility with every web editor. - A history of everything you rewrote. - A backend server. ## After v1, if you want it - A keyboard shortcut that runs the last used profile without opening the menu - A per-site default profile, stored locally, since the writing you do on each site is usually the same kind - A diff view showing what changed, which is the fastest way to notice a rewrite altering your meaning ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Entered on the options page and stored in the local extension storage area. Not a file in the repository. MODEL_API_KEY=sk-...redacted # Required. The provider endpoint. Also the only host permission the worker needs. MODEL_ENDPOINT=https://api.example.com/v1/messages # Required. Stored with settings and shown on the options page. MODEL_ID=a text model identifier # Required. Your own timeout, set well inside the platform ceiling for a single request. REQUEST_TIMEOUT_MS=45000 # Required. Characters in plus out per month. Requests stop here until the next month. MONTHLY_CHAR_CEILING=500000
You are building a lean indie version of PromptDC. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # PromptDC · indie build A browser extension whose hard problems are putting rewritten text back into rich editors without corrupting the document, surviving a background worker that is shut down mid-request, and keeping the API key away from every page you visit. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A third generation manifest extension, plain HTML, CSS and JavaScript, no build step | the whole thing is four files and a bundler only adds ways for it to break | | Background | A service worker that assumes it will be killed at any moment | an idle worker is shut down in seconds and a slow model call outlives it | | Storage | The local extension storage area for settings and in-flight requests, never the synced area | an API key does not belong in anything that leaves the machine by itself | | Insertion | Range-based replacement with events dispatched, clipboard as the honest fallback | framework-managed editors ignore text you put in without telling them | | Permissions | Optional host permissions requested per site, never every site up front | an extension that can read every page you open should have to ask | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A browser supporting third generation extension manifests, in developer mode** · free - Why: You will load the folder unpacked dozens of times, and the worker lifecycle behaviour you are designing around is only observable in the real browser. - Get it: Enable developer mode on the extensions page and use Load unpacked. - Verify: The extension loads and its service worker is visible and can be stopped from the extensions page - [ ] **An API key for a text model provider** · pay per call; a few rewrites a day is small change, and the ceiling here is what keeps it that way - Why: Every rewrite is one call, and this is the only cost in the project. - Get it: Create a key and set a spend limit in the provider's console. - Verify: The options page test button returns a short completion - [ ] **A test page with all four insertion cases** · free - Why: Insertion is where this category of extension fails, and you cannot test it against a live site you might corrupt. - Get it: Write one local page with an input, a textarea, an editable element inside a framework, and a read-only paragraph. - Verify: A rewrite in each case behaves as specified, including the clipboard fallback - [ ] **Which sites this is allowed to run on** · free - Why: This decides the install prompt, which is the single thing that makes an extension feel either useful or invasive. - Get it: List the handful of sites you actually rewrite text on, and design the permission flow around asking per site. - Verify: The manifest declares no broad host access and the flow works on a site granted at runtime ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Accounts, sync and hosted credits. - Guaranteed compatibility with every web editor. - A history of everything you rewrote. - A backend server. - Writing profiles tuned for grammar fix, email replies, social posts, shortening, and tone - Coding mode platform detection that tailors the rewrite to the AI tool you are prompting (ChatGPT, Claude, Cursor, Lovable and more) - Reliable behavior across Gmail, LinkedIn, Google Docs, Shadow DOM, and framework-managed rich text editors - Cloud accounts, usage credits, profile sync, and a managed billing flow - Polished mode controls, error recovery, ongoing compatibility updates, and support If one of those is essential to you, that is the reason to keep paying for PromptDC, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · PromptDC The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a browser extension that rewrites the text you selected and puts it back without mangling the page to replace PromptDC. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: A third generation manifest extension, plain HTML, CSS and JavaScript, no build step · the whole thing is four files and a bundler only adds ways for it to break - Background: A service worker that assumes it will be killed at any moment · an idle worker is shut down in seconds and a slow model call outlives it - Storage: The local extension storage area for settings and in-flight requests, never the synced area · an API key does not belong in anything that leaves the machine by itself - Insertion: Range-based replacement with events dispatched, clipboard as the honest fallback · framework-managed editors ignore text you put in without telling them - Permissions: Optional host permissions requested per site, never every site up front · an extension that can read every page you open should have to ask ### Data model (create this before Phase 1) - settings(provider, endpoint, model_id, updated_at) · in the local storage area only, and the key is stored separately from anything ever sent to a page - profiles(id, name, system_prompt, order, built_in, edited_at) · plain editable text the user can read before anything is sent - requests(id, tab_id, profile_id, started_at, finished_at, state, error) · state is pending, running, done or failed, and it lives in storage rather than a variable - captures(request_id, kind, container_signature, start_offset, end_offset, text) · kind is input, textarea, contenteditable or readonly - usage(month, requests, input_chars, output_chars, estimated_cost_minor) · against a ceiling you set - The key appears in exactly one place and is never written into a request record, a log line, or anything a content script can read. ### Phase 1 · Capture the selection, and know what kind it is Build: Detect three cases and record which one you have: a value selection in an input or textarea with its start and end offsets, a range inside an editable element, and a selection you cannot write back to. Capture immediately on the user's action, store it against a request identifier, and never rely on the live selection surviving anything. Trap: Reading the selection again after the model responds finds nothing, because clicking your own button, switching tabs or the page re-rendering has already collapsed it, and the result then lands at the caret or nowhere at all. Capture offsets and a way to find the container at the moment of the click, store them, and treat the live selection as gone from that instant. Done when: A selection is captured and stored before any network call, verified by inspecting storage mid-request. All three cases are distinguished and recorded, tested on an input, a rich editor and a read-only paragraph. Clicking the extension's own button never collapses the captured selection, tested by hand on a rich editor. A selection inside a shadow root is either captured correctly or reported as unsupported with a message, never silently mishandled. Do not build yet: No model call, insertion, profiles or permissions work yet. ### Phase 2 · A request that survives the worker being killed Build: Keep request state in extension storage, never in a variable in the worker. The content script holds a long-lived connection while a request runs, and reconnects if it drops. On worker startup, reconcile: any request left running is either resumed or marked failed with a reason. Enforce your own timeout well inside the platform's per-request ceiling. Trap: Holding the in-flight request in a variable in the background worker loses it the moment the worker is shut down, and the documented behaviour is that an extension service worker is terminated after thirty seconds of inactivity with a five minute ceiling on processing a single request, so a slow rewrite simply vanishes with no error anywhere. Put the request state in storage, keep a connection open from the content script while it runs, and reconcile orphaned requests on startup. Done when: Request state lives in extension storage and no in-flight data is held in a worker variable, verified by review. Forcing the worker to stop mid-request leaves a recoverable state that reconciles on the next event rather than hanging, tested from the extensions page. A request exceeding the configured timeout fails with a message naming the timeout, tested against a slow endpoint. Reloading the extension mid-request removes the injected interface cleanly rather than leaving a dead button. Do not build yet: No insertion, profiles, permissions or cost ceiling yet. ### Phase 3 · Putting the text back without destroying the document Build: For an input or textarea, splice the value between the stored offsets, restore the caret after the inserted text, and dispatch input and change events that bubble. For an editable element, restore the stored range and replace its contents with a text node, then dispatch a bubbling input event. Never assign markup. If the container is gone or the range no longer resolves, copy and say so. Trap: Writing the result with an assignment to the element's markup destroys everything else in that editor, and in a framework-managed field the change is invisible to the framework, so the old text reappears on the next render and the work is gone. Replace only the captured range, use a text node, dispatch events that bubble, and fall back to the clipboard whenever the range cannot be resolved. Done when: Replacing a selection in the middle of a paragraph leaves the surrounding text and formatting untouched, tested in a rich editor. Input and change events bubble from every insertion path, verified with a listener in a test page. No code path assigns markup, verified by review. A stale or unresolvable range results in the text on the clipboard and a visible notice, never a partial write. Do not build yet: No profile editing, permission flow or cost ceiling yet. ### Phase 4 · Profiles you can read before you send Build: Ship a small set of profiles as editable text: improve, fix grammar, shorten, reply, and one that turns a vague request into a specification with goal, constraints and acceptance criteria. Show the full system prompt on the options page. Allow editing, duplication and reset to the shipped version. Record which profile produced each result. Trap: Burying the prompts in the extension's code means the user cannot tell why a rewrite keeps changing their meaning, and the only remedy available to them is to stop using it. Put the prompt text in front of them, let them edit it, and keep a reset so an experiment is never permanent. Done when: Every profile's full system prompt is visible and editable on the options page, checked by opening it. Editing a profile changes the next request and reset restores the shipped text exactly, tested. Each profile instructs the model to return only the revised text and to keep the input language, verified by reading them. The result view names which profile produced it. Do not build yet: No permission flow, usage ceiling or export yet. ### Phase 5 · Permissions, the key and the bill Build: Request host access for the current site when first used there, rather than declaring every site in the manifest. Keep the key in the local storage area and never send it to a content script. Count characters in and out per month against a ceiling that stops requests with a clear message. Export and import profiles as a file. Behave predictably on pages where extensions cannot run. Trap: Declaring access to every site up front means the extension can read every page you open, including your bank and your email, for a feature you use on four sites, and the browser tells the user exactly that at install time. Ask per site on first use, and let someone run it on one domain without granting the rest. Done when: The manifest declares no broad host access and permission is requested per site on first use, verified by reading the install prompt. The key is never present in any message sent to a content script, verified by logging every message shape in a test build. Reaching the monthly character ceiling stops requests with a message naming the ceiling and the reset date, tested. On a restricted browser page the extension shows a short explanation instead of failing silently. Do not build yet: Nothing after this. ### Out of scope (and why) - Accounts, sync and hosted credits, which turn a local tool into a service with a billing relationship and somebody else's uptime. - Reliable behaviour inside every rich editor on the web, which is the paid product's actual work and is a standing maintenance commitment rather than a feature. - Rewrite history, because keeping a copy of everything you selected anywhere on the web is a bigger liability than the feature is worth. - A backend server of your own, since every request this extension makes can go straight from the worker to the model provider. ### README must contain - That the background worker is shut down on a short idle timer, which is why request state lives in storage and why a long rewrite can be reconciled rather than lost - That the key is stored in the local extension storage area only, is never sent to a content script, and is never logged - That host access is requested per site on first use rather than declared for every site in the manifest - That replacement never assigns markup and falls back to the clipboard when a range cannot be resolved - Where the profiles live, how to edit and reset them, and what the monthly usage ceiling does when it is reached ===== AGENTS.md ===== # Agent instructions · PromptDC indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A third generation manifest extension, plain HTML, CSS and JavaScript, no build step, A service worker that assumes it will be killed at any moment, The local extension storage area for settings and in-flight requests, never the synced area, Range-based replacement with events dispatched, clipboard as the honest fallback, Optional host permissions requested per site, never every site up front. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Reading the selection again after the model responds finds nothing, because clicking your own button, switching tabs or the page re-rendering has already collapsed it, and the result then lands at the caret or nowhere at all. Capture offsets and a way to find the container at the moment of the click, store them, and treat the live selection as gone from that instant. - Holding the in-flight request in a variable in the background worker loses it the moment the worker is shut down, and the documented behaviour is that an extension service worker is terminated after thirty seconds of inactivity with a five minute ceiling on processing a single request, so a slow rewrite simply vanishes with no error anywhere. Put the request state in storage, keep a connection open from the content script while it runs, and reconcile orphaned requests on startup. - Writing the result with an assignment to the element's markup destroys everything else in that editor, and in a framework-managed field the change is invisible to the framework, so the old text reappears on the next render and the work is gone. Replace only the captured range, use a text node, dispatch events that bubble, and fall back to the clipboard whenever the range cannot be resolved. - Burying the prompts in the extension's code means the user cannot tell why a rewrite keeps changing their meaning, and the only remedy available to them is to stop using it. Put the prompt text in front of them, let them edit it, and keep a reset so an experiment is never permanent. ===== BUILD_PLAN.md ===== # Build plan · PromptDC A browser extension whose hard problems are putting rewritten text back into rich editors without corrupting the document, surviving a background worker that is shut down mid-request, and keeping the API key away from every page you visit. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Capture the selection, and know what kind it is The selection is captured with everything needed to restore it before any asynchronous work starts. ### Steps 1. Capture at the click, not at the response By the time the response arrives the selection is gone. Files: `content.js` 2. Record which of the three cases you are in Each one has a different way back, and guessing later is how text gets destroyed. Files: `content.js` 3. Store the capture against a request identifier The worker that answers may not be the one that asked. Files: `content.js`, `background.js` ### Done when - [ ] The selection is stored before any network call - [ ] All three selection cases are distinguished - [ ] The extension's own button does not collapse the selection - [ ] Shadow roots are handled or honestly refused ### Watch out - Reading the selection again after the model responds finds nothing, because clicking your own button, switching tabs or the page re-rendering has already collapsed it, and the result then lands at the caret or nowhere at all. Capture offsets and a way to find the container at the moment of the click, store them, and treat the live selection as gone from that instant. ## Phase 2 · A request that survives the worker being killed The model call is resumable, because the background worker is shut down on a short idle timer. ### Steps 1. Put every piece of request state in storage The worker is disposable and behaves like it. Files: `background.js` 2. Keep a connection open while a request runs It is what stops the worker being shut down underneath you. Files: `content.js`, `background.js` 3. Reconcile orphaned requests on startup Something must decide what happened to the one that vanished. Files: `background.js` ### Done when - [ ] No in-flight state lives in a worker variable - [ ] Stopping the worker mid-request is recoverable - [ ] A timeout fails with a clear message - [ ] Reloading the extension cleans up injected interface ### Watch out - Holding the in-flight request in a variable in the background worker loses it the moment the worker is shut down, and the documented behaviour is that an extension service worker is terminated after thirty seconds of inactivity with a five minute ceiling on processing a single request, so a slow rewrite simply vanishes with no error anywhere. Put the request state in storage, keep a connection open from the content script while it runs, and reconcile orphaned requests on startup. ## Phase 3 · Putting the text back without destroying the document Replacement affects only the captured range and the page's own editor notices the change. ### Steps 1. Splice by offsets for inputs, restore the caret The surrounding text is somebody's work in progress. Files: `content.js` 2. Replace range contents with a text node, never markup Assigning markup destroys the rest of the editor. Files: `content.js` 3. Dispatch bubbling input and change events A framework that did not hear about it will undo it. Files: `content.js` ### Done when - [ ] Surrounding text and formatting survive a replacement - [ ] Input and change events bubble on every path - [ ] No path assigns markup - [ ] An unresolvable range copies and says so ### Watch out - Writing the result with an assignment to the element's markup destroys everything else in that editor, and in a framework-managed field the change is invisible to the framework, so the old text reappears on the next render and the work is gone. Replace only the captured range, use a text node, dispatch events that bubble, and fall back to the clipboard whenever the range cannot be resolved. ## Phase 4 · Profiles you can read before you send Every rewrite mode is an editable prompt the user can see, change and reset. ### Steps 1. Ship prompts as editable text, not as code The user should be able to see what is being said about their writing. Files: `options.html`, `options.js` 2. Keep a reset to the shipped version An experiment nobody can undo is not an experiment. Files: `options.js` 3. Record the profile on every result Otherwise you cannot tell which prompt produced the odd output. Files: `background.js` ### Done when - [ ] Prompts are visible and editable - [ ] Reset restores the shipped text exactly - [ ] Every profile returns only revised text - [ ] Results name the profile that produced them ### Watch out - Burying the prompts in the extension's code means the user cannot tell why a rewrite keeps changing their meaning, and the only remedy available to them is to stop using it. Put the prompt text in front of them, let them edit it, and keep a reset so an experiment is never permanent. ## Not in this build - Accounts, sync and hosted credits. - Guaranteed compatibility with every web editor. - A history of everything you rewrote. - A backend server. ## After v1, if you want it - A keyboard shortcut that runs the last used profile without opening the menu - A per-site default profile, stored locally, since the writing you do on each site is usually the same kind - A diff view showing what changed, which is the fastest way to notice a rewrite altering your meaning ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Entered on the options page and stored in the local extension storage area. Not a file in the repository. MODEL_API_KEY=sk-...redacted # Required. The provider endpoint. Also the only host permission the worker needs. MODEL_ENDPOINT=https://api.example.com/v1/messages # Required. Stored with settings and shown on the options page. MODEL_ID=a text model identifier # Required. Your own timeout, set well inside the platform ceiling for a single request. REQUEST_TIMEOUT_MS=45000 # Required. Characters in plus out per month. Requests stop here until the next month. MONTHLY_CHAR_CEILING=500000
You are building a production product version of PromptDC. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # PromptDC · product brief ## Problem A useful personal version is highly one-shot-able: an MV3 content script can capture selected text, send it to an LLM with one of two system prompts, then replace a normal editable field or copy the result. The commercial product earns its keep through Writing profiles (grammar fix, email, social post, shorten, tone), a Coding mode that auto-detects which AI tool you are prompting and tailors the rewrite, polished interaction design, and defensive handling of complex editors where naive DOM replacement can fail or corrupt text. ## Product outcome An extension that captures a selection before it can vanish, survives a background worker being shut down mid-request, puts text back without destroying the editor around it, and asks for access one site at a time. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - OpenAI or Anthropic API key - Chrome developer mode with Load unpacked ## Explicit non-goals for v1 - Accounts, sync and hosted credits. - Guaranteed compatibility with every web editor. - A history of everything you rewrote. - A backend server. - Writing profiles tuned for grammar fix, email replies, social posts, shortening, and tone - Coding mode platform detection that tailors the rewrite to the AI tool you are prompting (ChatGPT, Claude, Cursor, Lovable and more) - Reliable behavior across Gmail, LinkedIn, Google Docs, Shadow DOM, and framework-managed rich text editors - Cloud accounts, usage credits, profile sync, and a managed billing flow - Polished mode controls, error recovery, ongoing compatibility updates, and support ## Success criteria - The selection is confirmed captured before any network call - Stopping the worker mid-request is confirmed recoverable - No insertion path is confirmed to assign markup - The key is confirmed absent from every message sent to a page - The manifest is confirmed to declare no broad host access ===== BRIEF.md ===== # Build brief · PromptDC The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a browser extension that rewrites the text you selected and puts it back without mangling the page to replace PromptDC. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: A third generation manifest extension, plain HTML, CSS and JavaScript, no build step · the whole thing is four files and a bundler only adds ways for it to break - Background: A service worker that assumes it will be killed at any moment · an idle worker is shut down in seconds and a slow model call outlives it - Storage: The local extension storage area for settings and in-flight requests, never the synced area · an API key does not belong in anything that leaves the machine by itself - Insertion: Range-based replacement with events dispatched, clipboard as the honest fallback · framework-managed editors ignore text you put in without telling them - Permissions: Optional host permissions requested per site, never every site up front · an extension that can read every page you open should have to ask ### Data model (create this before Phase 1) - settings(provider, endpoint, model_id, updated_at) · in the local storage area only, and the key is stored separately from anything ever sent to a page - profiles(id, name, system_prompt, order, built_in, edited_at) · plain editable text the user can read before anything is sent - requests(id, tab_id, profile_id, started_at, finished_at, state, error) · state is pending, running, done or failed, and it lives in storage rather than a variable - captures(request_id, kind, container_signature, start_offset, end_offset, text) · kind is input, textarea, contenteditable or readonly - usage(month, requests, input_chars, output_chars, estimated_cost_minor) · against a ceiling you set - The key appears in exactly one place and is never written into a request record, a log line, or anything a content script can read. ### Phase 1 · Capture the selection, and know what kind it is Build: Detect three cases and record which one you have: a value selection in an input or textarea with its start and end offsets, a range inside an editable element, and a selection you cannot write back to. Capture immediately on the user's action, store it against a request identifier, and never rely on the live selection surviving anything. Trap: Reading the selection again after the model responds finds nothing, because clicking your own button, switching tabs or the page re-rendering has already collapsed it, and the result then lands at the caret or nowhere at all. Capture offsets and a way to find the container at the moment of the click, store them, and treat the live selection as gone from that instant. Done when: A selection is captured and stored before any network call, verified by inspecting storage mid-request. All three cases are distinguished and recorded, tested on an input, a rich editor and a read-only paragraph. Clicking the extension's own button never collapses the captured selection, tested by hand on a rich editor. A selection inside a shadow root is either captured correctly or reported as unsupported with a message, never silently mishandled. Do not build yet: No model call, insertion, profiles or permissions work yet. ### Phase 2 · A request that survives the worker being killed Build: Keep request state in extension storage, never in a variable in the worker. The content script holds a long-lived connection while a request runs, and reconnects if it drops. On worker startup, reconcile: any request left running is either resumed or marked failed with a reason. Enforce your own timeout well inside the platform's per-request ceiling. Trap: Holding the in-flight request in a variable in the background worker loses it the moment the worker is shut down, and the documented behaviour is that an extension service worker is terminated after thirty seconds of inactivity with a five minute ceiling on processing a single request, so a slow rewrite simply vanishes with no error anywhere. Put the request state in storage, keep a connection open from the content script while it runs, and reconcile orphaned requests on startup. Done when: Request state lives in extension storage and no in-flight data is held in a worker variable, verified by review. Forcing the worker to stop mid-request leaves a recoverable state that reconciles on the next event rather than hanging, tested from the extensions page. A request exceeding the configured timeout fails with a message naming the timeout, tested against a slow endpoint. Reloading the extension mid-request removes the injected interface cleanly rather than leaving a dead button. Do not build yet: No insertion, profiles, permissions or cost ceiling yet. ### Phase 3 · Putting the text back without destroying the document Build: For an input or textarea, splice the value between the stored offsets, restore the caret after the inserted text, and dispatch input and change events that bubble. For an editable element, restore the stored range and replace its contents with a text node, then dispatch a bubbling input event. Never assign markup. If the container is gone or the range no longer resolves, copy and say so. Trap: Writing the result with an assignment to the element's markup destroys everything else in that editor, and in a framework-managed field the change is invisible to the framework, so the old text reappears on the next render and the work is gone. Replace only the captured range, use a text node, dispatch events that bubble, and fall back to the clipboard whenever the range cannot be resolved. Done when: Replacing a selection in the middle of a paragraph leaves the surrounding text and formatting untouched, tested in a rich editor. Input and change events bubble from every insertion path, verified with a listener in a test page. No code path assigns markup, verified by review. A stale or unresolvable range results in the text on the clipboard and a visible notice, never a partial write. Do not build yet: No profile editing, permission flow or cost ceiling yet. ### Phase 4 · Profiles you can read before you send Build: Ship a small set of profiles as editable text: improve, fix grammar, shorten, reply, and one that turns a vague request into a specification with goal, constraints and acceptance criteria. Show the full system prompt on the options page. Allow editing, duplication and reset to the shipped version. Record which profile produced each result. Trap: Burying the prompts in the extension's code means the user cannot tell why a rewrite keeps changing their meaning, and the only remedy available to them is to stop using it. Put the prompt text in front of them, let them edit it, and keep a reset so an experiment is never permanent. Done when: Every profile's full system prompt is visible and editable on the options page, checked by opening it. Editing a profile changes the next request and reset restores the shipped text exactly, tested. Each profile instructs the model to return only the revised text and to keep the input language, verified by reading them. The result view names which profile produced it. Do not build yet: No permission flow, usage ceiling or export yet. ### Phase 5 · Permissions, the key and the bill Build: Request host access for the current site when first used there, rather than declaring every site in the manifest. Keep the key in the local storage area and never send it to a content script. Count characters in and out per month against a ceiling that stops requests with a clear message. Export and import profiles as a file. Behave predictably on pages where extensions cannot run. Trap: Declaring access to every site up front means the extension can read every page you open, including your bank and your email, for a feature you use on four sites, and the browser tells the user exactly that at install time. Ask per site on first use, and let someone run it on one domain without granting the rest. Done when: The manifest declares no broad host access and permission is requested per site on first use, verified by reading the install prompt. The key is never present in any message sent to a content script, verified by logging every message shape in a test build. Reaching the monthly character ceiling stops requests with a message naming the ceiling and the reset date, tested. On a restricted browser page the extension shows a short explanation instead of failing silently. Do not build yet: Nothing after this. ### Out of scope (and why) - Accounts, sync and hosted credits, which turn a local tool into a service with a billing relationship and somebody else's uptime. - Reliable behaviour inside every rich editor on the web, which is the paid product's actual work and is a standing maintenance commitment rather than a feature. - Rewrite history, because keeping a copy of everything you selected anywhere on the web is a bigger liability than the feature is worth. - A backend server of your own, since every request this extension makes can go straight from the worker to the model provider. ### README must contain - That the background worker is shut down on a short idle timer, which is why request state lives in storage and why a long rewrite can be reconciled rather than lost - That the key is stored in the local extension storage area only, is never sent to a content script, and is never logged - That host access is requested per site on first use rather than declared for every site in the manifest - That replacement never assigns markup and falls back to the clipboard when a range cannot be resolved - Where the profiles live, how to edit and reset them, and what the monthly usage ceiling does when it is reached ===== ARCHITECTURE.md ===== # Architecture · PromptDC ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A third generation manifest extension, plain HTML, CSS and JavaScript, no build step | the whole thing is four files and a bundler only adds ways for it to break | | Background | A service worker that assumes it will be killed at any moment | an idle worker is shut down in seconds and a slow model call outlives it | | Storage | The local extension storage area for settings and in-flight requests, never the synced area | an API key does not belong in anything that leaves the machine by itself | | Insertion | Range-based replacement with events dispatched, clipboard as the honest fallback | framework-managed editors ignore text you put in without telling them | | Permissions | Optional host permissions requested per site, never every site up front | an extension that can read every page you open should have to ask | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Capture | Selection offsets, ranges and case detection. | Any implementation, provided capture happens at the click. | | Worker | Request state in storage and reconciliation. | Any transport, provided nothing in flight lives in a variable. | | Insertion | Range replacement and event dispatch. | Not swappable for assigning markup. | | Profiles | Editable prompts with reset. | Any set, provided the text stays visible to the user. | | Permissions | Per-site host access and key handling. | Not swappable for declaring every site. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `MODEL_API_KEY` · required, secret · Entered on the options page and stored in the local extension storage area. Not a file in the repository. - `MODEL_ENDPOINT` · required · The provider endpoint. Also the only host permission the worker needs. - `MODEL_ID` · required · Stored with settings and shown on the options page. - `REQUEST_TIMEOUT_MS` · required · Your own timeout, set well inside the platform ceiling for a single request. - `MONTHLY_CHAR_CEILING` · required · Characters in plus out per month. Requests stop here until the next month. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · PromptDC product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: A third generation manifest extension, plain HTML, CSS and JavaScript, no build step, A service worker that assumes it will be killed at any moment, The local extension storage area for settings and in-flight requests, never the synced area, Range-based replacement with events dispatched, clipboard as the honest fallback, Optional host permissions requested per site, never every site up front. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Reading the selection again after the model responds finds nothing, because clicking your own button, switching tabs or the page re-rendering has already collapsed it, and the result then lands at the caret or nowhere at all. Capture offsets and a way to find the container at the moment of the click, store them, and treat the live selection as gone from that instant. - Holding the in-flight request in a variable in the background worker loses it the moment the worker is shut down, and the documented behaviour is that an extension service worker is terminated after thirty seconds of inactivity with a five minute ceiling on processing a single request, so a slow rewrite simply vanishes with no error anywhere. Put the request state in storage, keep a connection open from the content script while it runs, and reconcile orphaned requests on startup. - Writing the result with an assignment to the element's markup destroys everything else in that editor, and in a framework-managed field the change is invisible to the framework, so the old text reappears on the next render and the work is gone. Replace only the captured range, use a text node, dispatch events that bubble, and fall back to the clipboard whenever the range cannot be resolved. - Burying the prompts in the extension's code means the user cannot tell why a rewrite keeps changing their meaning, and the only remedy available to them is to stop using it. Put the prompt text in front of them, let them edit it, and keep a reset so an experiment is never permanent. - Declaring access to every site up front means the extension can read every page you open, including your bank and your email, for a feature you use on four sites, and the browser tells the user exactly that at install time. Ask per site on first use, and let someone run it on one domain without granting the rest. ===== MILESTONES.md ===== # Delivery milestones · PromptDC Estimated effort: **one sitting** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Capture the selection, and know what kind it is The selection is captured with everything needed to restore it before any asynchronous work starts. ### Steps 1. Capture at the click, not at the response By the time the response arrives the selection is gone. Files: `content.js` 2. Record which of the three cases you are in Each one has a different way back, and guessing later is how text gets destroyed. Files: `content.js` 3. Store the capture against a request identifier The worker that answers may not be the one that asked. Files: `content.js`, `background.js` ### Done when - [ ] The selection is stored before any network call - [ ] All three selection cases are distinguished - [ ] The extension's own button does not collapse the selection - [ ] Shadow roots are handled or honestly refused ### Watch out - Reading the selection again after the model responds finds nothing, because clicking your own button, switching tabs or the page re-rendering has already collapsed it, and the result then lands at the caret or nowhere at all. Capture offsets and a way to find the container at the moment of the click, store them, and treat the live selection as gone from that instant. ## M2 · A request that survives the worker being killed The model call is resumable, because the background worker is shut down on a short idle timer. ### Steps 1. Put every piece of request state in storage The worker is disposable and behaves like it. Files: `background.js` 2. Keep a connection open while a request runs It is what stops the worker being shut down underneath you. Files: `content.js`, `background.js` 3. Reconcile orphaned requests on startup Something must decide what happened to the one that vanished. Files: `background.js` ### Done when - [ ] No in-flight state lives in a worker variable - [ ] Stopping the worker mid-request is recoverable - [ ] A timeout fails with a clear message - [ ] Reloading the extension cleans up injected interface ### Watch out - Holding the in-flight request in a variable in the background worker loses it the moment the worker is shut down, and the documented behaviour is that an extension service worker is terminated after thirty seconds of inactivity with a five minute ceiling on processing a single request, so a slow rewrite simply vanishes with no error anywhere. Put the request state in storage, keep a connection open from the content script while it runs, and reconcile orphaned requests on startup. ## M3 · Putting the text back without destroying the document Replacement affects only the captured range and the page's own editor notices the change. ### Steps 1. Splice by offsets for inputs, restore the caret The surrounding text is somebody's work in progress. Files: `content.js` 2. Replace range contents with a text node, never markup Assigning markup destroys the rest of the editor. Files: `content.js` 3. Dispatch bubbling input and change events A framework that did not hear about it will undo it. Files: `content.js` ### Done when - [ ] Surrounding text and formatting survive a replacement - [ ] Input and change events bubble on every path - [ ] No path assigns markup - [ ] An unresolvable range copies and says so ### Watch out - Writing the result with an assignment to the element's markup destroys everything else in that editor, and in a framework-managed field the change is invisible to the framework, so the old text reappears on the next render and the work is gone. Replace only the captured range, use a text node, dispatch events that bubble, and fall back to the clipboard whenever the range cannot be resolved. ## M4 · Profiles you can read before you send Every rewrite mode is an editable prompt the user can see, change and reset. ### Steps 1. Ship prompts as editable text, not as code The user should be able to see what is being said about their writing. Files: `options.html`, `options.js` 2. Keep a reset to the shipped version An experiment nobody can undo is not an experiment. Files: `options.js` 3. Record the profile on every result Otherwise you cannot tell which prompt produced the odd output. Files: `background.js` ### Done when - [ ] Prompts are visible and editable - [ ] Reset restores the shipped text exactly - [ ] Every profile returns only revised text - [ ] Results name the profile that produced them ### Watch out - Burying the prompts in the extension's code means the user cannot tell why a rewrite keeps changing their meaning, and the only remedy available to them is to stop using it. Put the prompt text in front of them, let them edit it, and keep a reset so an experiment is never permanent. ## M5 · Permissions, the key and the bill (production only) The extension asks for access per site, keeps the key away from pages, and stops before the spend surprises you. ### Steps 1. Ask for host access per site, on first use The install prompt is what most people judge an extension on. Files: `manifest.json`, `background.js` 2. Keep the key out of every message to a page A content script shares a process with the page it runs in. Files: `background.js`, `content.js` 3. Count usage and stop at a ceiling Bring your own key means bring your own bill. Files: `background.js`, `options.js` ### Done when - [ ] No broad host access in the manifest - [ ] The key never appears in a message to a page - [ ] The monthly ceiling stops requests with a clear message - [ ] Restricted pages get an explanation, not silence ### Watch out - Declaring access to every site up front means the extension can read every page you open, including your bank and your email, for a feature you use on four sites, and the browser tells the user exactly that at install time. Ask per site on first use, and let someone run it on one domain without granting the rest. ===== OPERATIONS.md ===== # Operations · PromptDC ## Backup Export the profiles to a file, which is the only thing here worth keeping. Settings are three fields and the key should be recoverable from your provider rather than from a backup. ## Restore Load the extension unpacked, import the profiles file, re-enter the key, and confirm the test button returns a completion. Host permissions are granted again per site, which is by design. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Monthly character count against the ceiling, the failure rate by error kind, and how often insertion falls back to the clipboard. A rising clipboard rate means a site you use changed its editor. ## Incident checklist The failure that matters is silent text loss: a rewrite that replaced more than it should have, or vanished. The request records and the stored captures are what let you see what was selected and what was written back. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] The selection is confirmed captured before any network call - [ ] Stopping the worker mid-request is confirmed recoverable - [ ] No insertion path is confirmed to assign markup - [ ] The key is confirmed absent from every message sent to a page - [ ] The manifest is confirmed to declare no broad host access ## Launch constraint Do not market omitted PromptDC capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Entered on the options page and stored in the local extension storage area. Not a file in the repository. MODEL_API_KEY=sk-...redacted # Required. The provider endpoint. Also the only host permission the worker needs. MODEL_ENDPOINT=https://api.example.com/v1/messages # Required. Stored with settings and shown on the options page. MODEL_ID=a text model identifier # Required. Your own timeout, set well inside the platform ceiling for a single request. REQUEST_TIMEOUT_MS=45000 # Required. Characters in plus out per month. Requests stop here until the next month. MONTHLY_CHAR_CEILING=500000
# PromptDC · indie build A browser extension whose hard problems are putting rewritten text back into rich editors without corrupting the document, surviving a background worker that is shut down mid-request, and keeping the API key away from every page you visit. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A third generation manifest extension, plain HTML, CSS and JavaScript, no build step | the whole thing is four files and a bundler only adds ways for it to break | | Background | A service worker that assumes it will be killed at any moment | an idle worker is shut down in seconds and a slow model call outlives it | | Storage | The local extension storage area for settings and in-flight requests, never the synced area | an API key does not belong in anything that leaves the machine by itself | | Insertion | Range-based replacement with events dispatched, clipboard as the honest fallback | framework-managed editors ignore text you put in without telling them | | Permissions | Optional host permissions requested per site, never every site up front | an extension that can read every page you open should have to ask | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A browser supporting third generation extension manifests, in developer mode** · free - Why: You will load the folder unpacked dozens of times, and the worker lifecycle behaviour you are designing around is only observable in the real browser. - Get it: Enable developer mode on the extensions page and use Load unpacked. - Verify: The extension loads and its service worker is visible and can be stopped from the extensions page - [ ] **An API key for a text model provider** · pay per call; a few rewrites a day is small change, and the ceiling here is what keeps it that way - Why: Every rewrite is one call, and this is the only cost in the project. - Get it: Create a key and set a spend limit in the provider's console. - Verify: The options page test button returns a short completion - [ ] **A test page with all four insertion cases** · free - Why: Insertion is where this category of extension fails, and you cannot test it against a live site you might corrupt. - Get it: Write one local page with an input, a textarea, an editable element inside a framework, and a read-only paragraph. - Verify: A rewrite in each case behaves as specified, including the clipboard fallback - [ ] **Which sites this is allowed to run on** · free - Why: This decides the install prompt, which is the single thing that makes an extension feel either useful or invasive. - Get it: List the handful of sites you actually rewrite text on, and design the permission flow around asking per site. - Verify: The manifest declares no broad host access and the flow works on a site granted at runtime ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Accounts, sync and hosted credits. - Guaranteed compatibility with every web editor. - A history of everything you rewrote. - A backend server. - Writing profiles tuned for grammar fix, email replies, social posts, shortening, and tone - Coding mode platform detection that tailors the rewrite to the AI tool you are prompting (ChatGPT, Claude, Cursor, Lovable and more) - Reliable behavior across Gmail, LinkedIn, Google Docs, Shadow DOM, and framework-managed rich text editors - Cloud accounts, usage credits, profile sync, and a managed billing flow - Polished mode controls, error recovery, ongoing compatibility updates, and support If one of those is essential to you, that is the reason to keep paying for PromptDC, and the README should say so rather than pretend.
# Build brief · PromptDC The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a browser extension that rewrites the text you selected and puts it back without mangling the page to replace PromptDC. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: A third generation manifest extension, plain HTML, CSS and JavaScript, no build step · the whole thing is four files and a bundler only adds ways for it to break - Background: A service worker that assumes it will be killed at any moment · an idle worker is shut down in seconds and a slow model call outlives it - Storage: The local extension storage area for settings and in-flight requests, never the synced area · an API key does not belong in anything that leaves the machine by itself - Insertion: Range-based replacement with events dispatched, clipboard as the honest fallback · framework-managed editors ignore text you put in without telling them - Permissions: Optional host permissions requested per site, never every site up front · an extension that can read every page you open should have to ask ### Data model (create this before Phase 1) - settings(provider, endpoint, model_id, updated_at) · in the local storage area only, and the key is stored separately from anything ever sent to a page - profiles(id, name, system_prompt, order, built_in, edited_at) · plain editable text the user can read before anything is sent - requests(id, tab_id, profile_id, started_at, finished_at, state, error) · state is pending, running, done or failed, and it lives in storage rather than a variable - captures(request_id, kind, container_signature, start_offset, end_offset, text) · kind is input, textarea, contenteditable or readonly - usage(month, requests, input_chars, output_chars, estimated_cost_minor) · against a ceiling you set - The key appears in exactly one place and is never written into a request record, a log line, or anything a content script can read. ### Phase 1 · Capture the selection, and know what kind it is Build: Detect three cases and record which one you have: a value selection in an input or textarea with its start and end offsets, a range inside an editable element, and a selection you cannot write back to. Capture immediately on the user's action, store it against a request identifier, and never rely on the live selection surviving anything. Trap: Reading the selection again after the model responds finds nothing, because clicking your own button, switching tabs or the page re-rendering has already collapsed it, and the result then lands at the caret or nowhere at all. Capture offsets and a way to find the container at the moment of the click, store them, and treat the live selection as gone from that instant. Done when: A selection is captured and stored before any network call, verified by inspecting storage mid-request. All three cases are distinguished and recorded, tested on an input, a rich editor and a read-only paragraph. Clicking the extension's own button never collapses the captured selection, tested by hand on a rich editor. A selection inside a shadow root is either captured correctly or reported as unsupported with a message, never silently mishandled. Do not build yet: No model call, insertion, profiles or permissions work yet. ### Phase 2 · A request that survives the worker being killed Build: Keep request state in extension storage, never in a variable in the worker. The content script holds a long-lived connection while a request runs, and reconnects if it drops. On worker startup, reconcile: any request left running is either resumed or marked failed with a reason. Enforce your own timeout well inside the platform's per-request ceiling. Trap: Holding the in-flight request in a variable in the background worker loses it the moment the worker is shut down, and the documented behaviour is that an extension service worker is terminated after thirty seconds of inactivity with a five minute ceiling on processing a single request, so a slow rewrite simply vanishes with no error anywhere. Put the request state in storage, keep a connection open from the content script while it runs, and reconcile orphaned requests on startup. Done when: Request state lives in extension storage and no in-flight data is held in a worker variable, verified by review. Forcing the worker to stop mid-request leaves a recoverable state that reconciles on the next event rather than hanging, tested from the extensions page. A request exceeding the configured timeout fails with a message naming the timeout, tested against a slow endpoint. Reloading the extension mid-request removes the injected interface cleanly rather than leaving a dead button. Do not build yet: No insertion, profiles, permissions or cost ceiling yet. ### Phase 3 · Putting the text back without destroying the document Build: For an input or textarea, splice the value between the stored offsets, restore the caret after the inserted text, and dispatch input and change events that bubble. For an editable element, restore the stored range and replace its contents with a text node, then dispatch a bubbling input event. Never assign markup. If the container is gone or the range no longer resolves, copy and say so. Trap: Writing the result with an assignment to the element's markup destroys everything else in that editor, and in a framework-managed field the change is invisible to the framework, so the old text reappears on the next render and the work is gone. Replace only the captured range, use a text node, dispatch events that bubble, and fall back to the clipboard whenever the range cannot be resolved. Done when: Replacing a selection in the middle of a paragraph leaves the surrounding text and formatting untouched, tested in a rich editor. Input and change events bubble from every insertion path, verified with a listener in a test page. No code path assigns markup, verified by review. A stale or unresolvable range results in the text on the clipboard and a visible notice, never a partial write. Do not build yet: No profile editing, permission flow or cost ceiling yet. ### Phase 4 · Profiles you can read before you send Build: Ship a small set of profiles as editable text: improve, fix grammar, shorten, reply, and one that turns a vague request into a specification with goal, constraints and acceptance criteria. Show the full system prompt on the options page. Allow editing, duplication and reset to the shipped version. Record which profile produced each result. Trap: Burying the prompts in the extension's code means the user cannot tell why a rewrite keeps changing their meaning, and the only remedy available to them is to stop using it. Put the prompt text in front of them, let them edit it, and keep a reset so an experiment is never permanent. Done when: Every profile's full system prompt is visible and editable on the options page, checked by opening it. Editing a profile changes the next request and reset restores the shipped text exactly, tested. Each profile instructs the model to return only the revised text and to keep the input language, verified by reading them. The result view names which profile produced it. Do not build yet: No permission flow, usage ceiling or export yet. ### Phase 5 · Permissions, the key and the bill Build: Request host access for the current site when first used there, rather than declaring every site in the manifest. Keep the key in the local storage area and never send it to a content script. Count characters in and out per month against a ceiling that stops requests with a clear message. Export and import profiles as a file. Behave predictably on pages where extensions cannot run. Trap: Declaring access to every site up front means the extension can read every page you open, including your bank and your email, for a feature you use on four sites, and the browser tells the user exactly that at install time. Ask per site on first use, and let someone run it on one domain without granting the rest. Done when: The manifest declares no broad host access and permission is requested per site on first use, verified by reading the install prompt. The key is never present in any message sent to a content script, verified by logging every message shape in a test build. Reaching the monthly character ceiling stops requests with a message naming the ceiling and the reset date, tested. On a restricted browser page the extension shows a short explanation instead of failing silently. Do not build yet: Nothing after this. ### Out of scope (and why) - Accounts, sync and hosted credits, which turn a local tool into a service with a billing relationship and somebody else's uptime. - Reliable behaviour inside every rich editor on the web, which is the paid product's actual work and is a standing maintenance commitment rather than a feature. - Rewrite history, because keeping a copy of everything you selected anywhere on the web is a bigger liability than the feature is worth. - A backend server of your own, since every request this extension makes can go straight from the worker to the model provider. ### README must contain - That the background worker is shut down on a short idle timer, which is why request state lives in storage and why a long rewrite can be reconciled rather than lost - That the key is stored in the local extension storage area only, is never sent to a content script, and is never logged - That host access is requested per site on first use rather than declared for every site in the manifest - That replacement never assigns markup and falls back to the clipboard when a range cannot be resolved - Where the profiles live, how to edit and reset them, and what the monthly usage ceiling does when it is reached
# Agent instructions · PromptDC indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A third generation manifest extension, plain HTML, CSS and JavaScript, no build step, A service worker that assumes it will be killed at any moment, The local extension storage area for settings and in-flight requests, never the synced area, Range-based replacement with events dispatched, clipboard as the honest fallback, Optional host permissions requested per site, never every site up front. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Reading the selection again after the model responds finds nothing, because clicking your own button, switching tabs or the page re-rendering has already collapsed it, and the result then lands at the caret or nowhere at all. Capture offsets and a way to find the container at the moment of the click, store them, and treat the live selection as gone from that instant. - Holding the in-flight request in a variable in the background worker loses it the moment the worker is shut down, and the documented behaviour is that an extension service worker is terminated after thirty seconds of inactivity with a five minute ceiling on processing a single request, so a slow rewrite simply vanishes with no error anywhere. Put the request state in storage, keep a connection open from the content script while it runs, and reconcile orphaned requests on startup. - Writing the result with an assignment to the element's markup destroys everything else in that editor, and in a framework-managed field the change is invisible to the framework, so the old text reappears on the next render and the work is gone. Replace only the captured range, use a text node, dispatch events that bubble, and fall back to the clipboard whenever the range cannot be resolved. - Burying the prompts in the extension's code means the user cannot tell why a rewrite keeps changing their meaning, and the only remedy available to them is to stop using it. Put the prompt text in front of them, let them edit it, and keep a reset so an experiment is never permanent.
# Build plan · PromptDC A browser extension whose hard problems are putting rewritten text back into rich editors without corrupting the document, surviving a background worker that is shut down mid-request, and keeping the API key away from every page you visit. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Capture the selection, and know what kind it is The selection is captured with everything needed to restore it before any asynchronous work starts. ### Steps 1. Capture at the click, not at the response By the time the response arrives the selection is gone. Files: `content.js` 2. Record which of the three cases you are in Each one has a different way back, and guessing later is how text gets destroyed. Files: `content.js` 3. Store the capture against a request identifier The worker that answers may not be the one that asked. Files: `content.js`, `background.js` ### Done when - [ ] The selection is stored before any network call - [ ] All three selection cases are distinguished - [ ] The extension's own button does not collapse the selection - [ ] Shadow roots are handled or honestly refused ### Watch out - Reading the selection again after the model responds finds nothing, because clicking your own button, switching tabs or the page re-rendering has already collapsed it, and the result then lands at the caret or nowhere at all. Capture offsets and a way to find the container at the moment of the click, store them, and treat the live selection as gone from that instant. ## Phase 2 · A request that survives the worker being killed The model call is resumable, because the background worker is shut down on a short idle timer. ### Steps 1. Put every piece of request state in storage The worker is disposable and behaves like it. Files: `background.js` 2. Keep a connection open while a request runs It is what stops the worker being shut down underneath you. Files: `content.js`, `background.js` 3. Reconcile orphaned requests on startup Something must decide what happened to the one that vanished. Files: `background.js` ### Done when - [ ] No in-flight state lives in a worker variable - [ ] Stopping the worker mid-request is recoverable - [ ] A timeout fails with a clear message - [ ] Reloading the extension cleans up injected interface ### Watch out - Holding the in-flight request in a variable in the background worker loses it the moment the worker is shut down, and the documented behaviour is that an extension service worker is terminated after thirty seconds of inactivity with a five minute ceiling on processing a single request, so a slow rewrite simply vanishes with no error anywhere. Put the request state in storage, keep a connection open from the content script while it runs, and reconcile orphaned requests on startup. ## Phase 3 · Putting the text back without destroying the document Replacement affects only the captured range and the page's own editor notices the change. ### Steps 1. Splice by offsets for inputs, restore the caret The surrounding text is somebody's work in progress. Files: `content.js` 2. Replace range contents with a text node, never markup Assigning markup destroys the rest of the editor. Files: `content.js` 3. Dispatch bubbling input and change events A framework that did not hear about it will undo it. Files: `content.js` ### Done when - [ ] Surrounding text and formatting survive a replacement - [ ] Input and change events bubble on every path - [ ] No path assigns markup - [ ] An unresolvable range copies and says so ### Watch out - Writing the result with an assignment to the element's markup destroys everything else in that editor, and in a framework-managed field the change is invisible to the framework, so the old text reappears on the next render and the work is gone. Replace only the captured range, use a text node, dispatch events that bubble, and fall back to the clipboard whenever the range cannot be resolved. ## Phase 4 · Profiles you can read before you send Every rewrite mode is an editable prompt the user can see, change and reset. ### Steps 1. Ship prompts as editable text, not as code The user should be able to see what is being said about their writing. Files: `options.html`, `options.js` 2. Keep a reset to the shipped version An experiment nobody can undo is not an experiment. Files: `options.js` 3. Record the profile on every result Otherwise you cannot tell which prompt produced the odd output. Files: `background.js` ### Done when - [ ] Prompts are visible and editable - [ ] Reset restores the shipped text exactly - [ ] Every profile returns only revised text - [ ] Results name the profile that produced them ### Watch out - Burying the prompts in the extension's code means the user cannot tell why a rewrite keeps changing their meaning, and the only remedy available to them is to stop using it. Put the prompt text in front of them, let them edit it, and keep a reset so an experiment is never permanent. ## Not in this build - Accounts, sync and hosted credits. - Guaranteed compatibility with every web editor. - A history of everything you rewrote. - A backend server. ## After v1, if you want it - A keyboard shortcut that runs the last used profile without opening the menu - A per-site default profile, stored locally, since the writing you do on each site is usually the same kind - A diff view showing what changed, which is the fastest way to notice a rewrite altering your meaning
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Entered on the options page and stored in the local extension storage area. Not a file in the repository. MODEL_API_KEY=sk-...redacted # Required. The provider endpoint. Also the only host permission the worker needs. MODEL_ENDPOINT=https://api.example.com/v1/messages # Required. Stored with settings and shown on the options page. MODEL_ID=a text model identifier # Required. Your own timeout, set well inside the platform ceiling for a single request. REQUEST_TIMEOUT_MS=45000 # Required. Characters in plus out per month. Requests stop here until the next month. MONTHLY_CHAR_CEILING=500000
# PromptDC · product brief ## Problem A useful personal version is highly one-shot-able: an MV3 content script can capture selected text, send it to an LLM with one of two system prompts, then replace a normal editable field or copy the result. The commercial product earns its keep through Writing profiles (grammar fix, email, social post, shorten, tone), a Coding mode that auto-detects which AI tool you are prompting and tailors the rewrite, polished interaction design, and defensive handling of complex editors where naive DOM replacement can fail or corrupt text. ## Product outcome An extension that captures a selection before it can vanish, survives a background worker being shut down mid-request, puts text back without destroying the editor around it, and asks for access one site at a time. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - OpenAI or Anthropic API key - Chrome developer mode with Load unpacked ## Explicit non-goals for v1 - Accounts, sync and hosted credits. - Guaranteed compatibility with every web editor. - A history of everything you rewrote. - A backend server. - Writing profiles tuned for grammar fix, email replies, social posts, shortening, and tone - Coding mode platform detection that tailors the rewrite to the AI tool you are prompting (ChatGPT, Claude, Cursor, Lovable and more) - Reliable behavior across Gmail, LinkedIn, Google Docs, Shadow DOM, and framework-managed rich text editors - Cloud accounts, usage credits, profile sync, and a managed billing flow - Polished mode controls, error recovery, ongoing compatibility updates, and support ## Success criteria - The selection is confirmed captured before any network call - Stopping the worker mid-request is confirmed recoverable - No insertion path is confirmed to assign markup - The key is confirmed absent from every message sent to a page - The manifest is confirmed to declare no broad host access
# Build brief · PromptDC The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a browser extension that rewrites the text you selected and puts it back without mangling the page to replace PromptDC. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: A third generation manifest extension, plain HTML, CSS and JavaScript, no build step · the whole thing is four files and a bundler only adds ways for it to break - Background: A service worker that assumes it will be killed at any moment · an idle worker is shut down in seconds and a slow model call outlives it - Storage: The local extension storage area for settings and in-flight requests, never the synced area · an API key does not belong in anything that leaves the machine by itself - Insertion: Range-based replacement with events dispatched, clipboard as the honest fallback · framework-managed editors ignore text you put in without telling them - Permissions: Optional host permissions requested per site, never every site up front · an extension that can read every page you open should have to ask ### Data model (create this before Phase 1) - settings(provider, endpoint, model_id, updated_at) · in the local storage area only, and the key is stored separately from anything ever sent to a page - profiles(id, name, system_prompt, order, built_in, edited_at) · plain editable text the user can read before anything is sent - requests(id, tab_id, profile_id, started_at, finished_at, state, error) · state is pending, running, done or failed, and it lives in storage rather than a variable - captures(request_id, kind, container_signature, start_offset, end_offset, text) · kind is input, textarea, contenteditable or readonly - usage(month, requests, input_chars, output_chars, estimated_cost_minor) · against a ceiling you set - The key appears in exactly one place and is never written into a request record, a log line, or anything a content script can read. ### Phase 1 · Capture the selection, and know what kind it is Build: Detect three cases and record which one you have: a value selection in an input or textarea with its start and end offsets, a range inside an editable element, and a selection you cannot write back to. Capture immediately on the user's action, store it against a request identifier, and never rely on the live selection surviving anything. Trap: Reading the selection again after the model responds finds nothing, because clicking your own button, switching tabs or the page re-rendering has already collapsed it, and the result then lands at the caret or nowhere at all. Capture offsets and a way to find the container at the moment of the click, store them, and treat the live selection as gone from that instant. Done when: A selection is captured and stored before any network call, verified by inspecting storage mid-request. All three cases are distinguished and recorded, tested on an input, a rich editor and a read-only paragraph. Clicking the extension's own button never collapses the captured selection, tested by hand on a rich editor. A selection inside a shadow root is either captured correctly or reported as unsupported with a message, never silently mishandled. Do not build yet: No model call, insertion, profiles or permissions work yet. ### Phase 2 · A request that survives the worker being killed Build: Keep request state in extension storage, never in a variable in the worker. The content script holds a long-lived connection while a request runs, and reconnects if it drops. On worker startup, reconcile: any request left running is either resumed or marked failed with a reason. Enforce your own timeout well inside the platform's per-request ceiling. Trap: Holding the in-flight request in a variable in the background worker loses it the moment the worker is shut down, and the documented behaviour is that an extension service worker is terminated after thirty seconds of inactivity with a five minute ceiling on processing a single request, so a slow rewrite simply vanishes with no error anywhere. Put the request state in storage, keep a connection open from the content script while it runs, and reconcile orphaned requests on startup. Done when: Request state lives in extension storage and no in-flight data is held in a worker variable, verified by review. Forcing the worker to stop mid-request leaves a recoverable state that reconciles on the next event rather than hanging, tested from the extensions page. A request exceeding the configured timeout fails with a message naming the timeout, tested against a slow endpoint. Reloading the extension mid-request removes the injected interface cleanly rather than leaving a dead button. Do not build yet: No insertion, profiles, permissions or cost ceiling yet. ### Phase 3 · Putting the text back without destroying the document Build: For an input or textarea, splice the value between the stored offsets, restore the caret after the inserted text, and dispatch input and change events that bubble. For an editable element, restore the stored range and replace its contents with a text node, then dispatch a bubbling input event. Never assign markup. If the container is gone or the range no longer resolves, copy and say so. Trap: Writing the result with an assignment to the element's markup destroys everything else in that editor, and in a framework-managed field the change is invisible to the framework, so the old text reappears on the next render and the work is gone. Replace only the captured range, use a text node, dispatch events that bubble, and fall back to the clipboard whenever the range cannot be resolved. Done when: Replacing a selection in the middle of a paragraph leaves the surrounding text and formatting untouched, tested in a rich editor. Input and change events bubble from every insertion path, verified with a listener in a test page. No code path assigns markup, verified by review. A stale or unresolvable range results in the text on the clipboard and a visible notice, never a partial write. Do not build yet: No profile editing, permission flow or cost ceiling yet. ### Phase 4 · Profiles you can read before you send Build: Ship a small set of profiles as editable text: improve, fix grammar, shorten, reply, and one that turns a vague request into a specification with goal, constraints and acceptance criteria. Show the full system prompt on the options page. Allow editing, duplication and reset to the shipped version. Record which profile produced each result. Trap: Burying the prompts in the extension's code means the user cannot tell why a rewrite keeps changing their meaning, and the only remedy available to them is to stop using it. Put the prompt text in front of them, let them edit it, and keep a reset so an experiment is never permanent. Done when: Every profile's full system prompt is visible and editable on the options page, checked by opening it. Editing a profile changes the next request and reset restores the shipped text exactly, tested. Each profile instructs the model to return only the revised text and to keep the input language, verified by reading them. The result view names which profile produced it. Do not build yet: No permission flow, usage ceiling or export yet. ### Phase 5 · Permissions, the key and the bill Build: Request host access for the current site when first used there, rather than declaring every site in the manifest. Keep the key in the local storage area and never send it to a content script. Count characters in and out per month against a ceiling that stops requests with a clear message. Export and import profiles as a file. Behave predictably on pages where extensions cannot run. Trap: Declaring access to every site up front means the extension can read every page you open, including your bank and your email, for a feature you use on four sites, and the browser tells the user exactly that at install time. Ask per site on first use, and let someone run it on one domain without granting the rest. Done when: The manifest declares no broad host access and permission is requested per site on first use, verified by reading the install prompt. The key is never present in any message sent to a content script, verified by logging every message shape in a test build. Reaching the monthly character ceiling stops requests with a message naming the ceiling and the reset date, tested. On a restricted browser page the extension shows a short explanation instead of failing silently. Do not build yet: Nothing after this. ### Out of scope (and why) - Accounts, sync and hosted credits, which turn a local tool into a service with a billing relationship and somebody else's uptime. - Reliable behaviour inside every rich editor on the web, which is the paid product's actual work and is a standing maintenance commitment rather than a feature. - Rewrite history, because keeping a copy of everything you selected anywhere on the web is a bigger liability than the feature is worth. - A backend server of your own, since every request this extension makes can go straight from the worker to the model provider. ### README must contain - That the background worker is shut down on a short idle timer, which is why request state lives in storage and why a long rewrite can be reconciled rather than lost - That the key is stored in the local extension storage area only, is never sent to a content script, and is never logged - That host access is requested per site on first use rather than declared for every site in the manifest - That replacement never assigns markup and falls back to the clipboard when a range cannot be resolved - Where the profiles live, how to edit and reset them, and what the monthly usage ceiling does when it is reached
# Architecture · PromptDC ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A third generation manifest extension, plain HTML, CSS and JavaScript, no build step | the whole thing is four files and a bundler only adds ways for it to break | | Background | A service worker that assumes it will be killed at any moment | an idle worker is shut down in seconds and a slow model call outlives it | | Storage | The local extension storage area for settings and in-flight requests, never the synced area | an API key does not belong in anything that leaves the machine by itself | | Insertion | Range-based replacement with events dispatched, clipboard as the honest fallback | framework-managed editors ignore text you put in without telling them | | Permissions | Optional host permissions requested per site, never every site up front | an extension that can read every page you open should have to ask | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Capture | Selection offsets, ranges and case detection. | Any implementation, provided capture happens at the click. | | Worker | Request state in storage and reconciliation. | Any transport, provided nothing in flight lives in a variable. | | Insertion | Range replacement and event dispatch. | Not swappable for assigning markup. | | Profiles | Editable prompts with reset. | Any set, provided the text stays visible to the user. | | Permissions | Per-site host access and key handling. | Not swappable for declaring every site. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `MODEL_API_KEY` · required, secret · Entered on the options page and stored in the local extension storage area. Not a file in the repository. - `MODEL_ENDPOINT` · required · The provider endpoint. Also the only host permission the worker needs. - `MODEL_ID` · required · Stored with settings and shown on the options page. - `REQUEST_TIMEOUT_MS` · required · Your own timeout, set well inside the platform ceiling for a single request. - `MONTHLY_CHAR_CEILING` · required · Characters in plus out per month. Requests stop here until the next month. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · PromptDC product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: A third generation manifest extension, plain HTML, CSS and JavaScript, no build step, A service worker that assumes it will be killed at any moment, The local extension storage area for settings and in-flight requests, never the synced area, Range-based replacement with events dispatched, clipboard as the honest fallback, Optional host permissions requested per site, never every site up front. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Reading the selection again after the model responds finds nothing, because clicking your own button, switching tabs or the page re-rendering has already collapsed it, and the result then lands at the caret or nowhere at all. Capture offsets and a way to find the container at the moment of the click, store them, and treat the live selection as gone from that instant. - Holding the in-flight request in a variable in the background worker loses it the moment the worker is shut down, and the documented behaviour is that an extension service worker is terminated after thirty seconds of inactivity with a five minute ceiling on processing a single request, so a slow rewrite simply vanishes with no error anywhere. Put the request state in storage, keep a connection open from the content script while it runs, and reconcile orphaned requests on startup. - Writing the result with an assignment to the element's markup destroys everything else in that editor, and in a framework-managed field the change is invisible to the framework, so the old text reappears on the next render and the work is gone. Replace only the captured range, use a text node, dispatch events that bubble, and fall back to the clipboard whenever the range cannot be resolved. - Burying the prompts in the extension's code means the user cannot tell why a rewrite keeps changing their meaning, and the only remedy available to them is to stop using it. Put the prompt text in front of them, let them edit it, and keep a reset so an experiment is never permanent. - Declaring access to every site up front means the extension can read every page you open, including your bank and your email, for a feature you use on four sites, and the browser tells the user exactly that at install time. Ask per site on first use, and let someone run it on one domain without granting the rest.
# Delivery milestones · PromptDC Estimated effort: **one sitting** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Capture the selection, and know what kind it is The selection is captured with everything needed to restore it before any asynchronous work starts. ### Steps 1. Capture at the click, not at the response By the time the response arrives the selection is gone. Files: `content.js` 2. Record which of the three cases you are in Each one has a different way back, and guessing later is how text gets destroyed. Files: `content.js` 3. Store the capture against a request identifier The worker that answers may not be the one that asked. Files: `content.js`, `background.js` ### Done when - [ ] The selection is stored before any network call - [ ] All three selection cases are distinguished - [ ] The extension's own button does not collapse the selection - [ ] Shadow roots are handled or honestly refused ### Watch out - Reading the selection again after the model responds finds nothing, because clicking your own button, switching tabs or the page re-rendering has already collapsed it, and the result then lands at the caret or nowhere at all. Capture offsets and a way to find the container at the moment of the click, store them, and treat the live selection as gone from that instant. ## M2 · A request that survives the worker being killed The model call is resumable, because the background worker is shut down on a short idle timer. ### Steps 1. Put every piece of request state in storage The worker is disposable and behaves like it. Files: `background.js` 2. Keep a connection open while a request runs It is what stops the worker being shut down underneath you. Files: `content.js`, `background.js` 3. Reconcile orphaned requests on startup Something must decide what happened to the one that vanished. Files: `background.js` ### Done when - [ ] No in-flight state lives in a worker variable - [ ] Stopping the worker mid-request is recoverable - [ ] A timeout fails with a clear message - [ ] Reloading the extension cleans up injected interface ### Watch out - Holding the in-flight request in a variable in the background worker loses it the moment the worker is shut down, and the documented behaviour is that an extension service worker is terminated after thirty seconds of inactivity with a five minute ceiling on processing a single request, so a slow rewrite simply vanishes with no error anywhere. Put the request state in storage, keep a connection open from the content script while it runs, and reconcile orphaned requests on startup. ## M3 · Putting the text back without destroying the document Replacement affects only the captured range and the page's own editor notices the change. ### Steps 1. Splice by offsets for inputs, restore the caret The surrounding text is somebody's work in progress. Files: `content.js` 2. Replace range contents with a text node, never markup Assigning markup destroys the rest of the editor. Files: `content.js` 3. Dispatch bubbling input and change events A framework that did not hear about it will undo it. Files: `content.js` ### Done when - [ ] Surrounding text and formatting survive a replacement - [ ] Input and change events bubble on every path - [ ] No path assigns markup - [ ] An unresolvable range copies and says so ### Watch out - Writing the result with an assignment to the element's markup destroys everything else in that editor, and in a framework-managed field the change is invisible to the framework, so the old text reappears on the next render and the work is gone. Replace only the captured range, use a text node, dispatch events that bubble, and fall back to the clipboard whenever the range cannot be resolved. ## M4 · Profiles you can read before you send Every rewrite mode is an editable prompt the user can see, change and reset. ### Steps 1. Ship prompts as editable text, not as code The user should be able to see what is being said about their writing. Files: `options.html`, `options.js` 2. Keep a reset to the shipped version An experiment nobody can undo is not an experiment. Files: `options.js` 3. Record the profile on every result Otherwise you cannot tell which prompt produced the odd output. Files: `background.js` ### Done when - [ ] Prompts are visible and editable - [ ] Reset restores the shipped text exactly - [ ] Every profile returns only revised text - [ ] Results name the profile that produced them ### Watch out - Burying the prompts in the extension's code means the user cannot tell why a rewrite keeps changing their meaning, and the only remedy available to them is to stop using it. Put the prompt text in front of them, let them edit it, and keep a reset so an experiment is never permanent. ## M5 · Permissions, the key and the bill (production only) The extension asks for access per site, keeps the key away from pages, and stops before the spend surprises you. ### Steps 1. Ask for host access per site, on first use The install prompt is what most people judge an extension on. Files: `manifest.json`, `background.js` 2. Keep the key out of every message to a page A content script shares a process with the page it runs in. Files: `background.js`, `content.js` 3. Count usage and stop at a ceiling Bring your own key means bring your own bill. Files: `background.js`, `options.js` ### Done when - [ ] No broad host access in the manifest - [ ] The key never appears in a message to a page - [ ] The monthly ceiling stops requests with a clear message - [ ] Restricted pages get an explanation, not silence ### Watch out - Declaring access to every site up front means the extension can read every page you open, including your bank and your email, for a feature you use on four sites, and the browser tells the user exactly that at install time. Ask per site on first use, and let someone run it on one domain without granting the rest.
# Operations · PromptDC ## Backup Export the profiles to a file, which is the only thing here worth keeping. Settings are three fields and the key should be recoverable from your provider rather than from a backup. ## Restore Load the extension unpacked, import the profiles file, re-enter the key, and confirm the test button returns a completion. Host permissions are granted again per site, which is by design. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Monthly character count against the ceiling, the failure rate by error kind, and how often insertion falls back to the clipboard. A rising clipboard rate means a site you use changed its editor. ## Incident checklist The failure that matters is silent text loss: a rewrite that replaced more than it should have, or vanished. The request records and the stored captures are what let you see what was selected and what was written back. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] The selection is confirmed captured before any network call - [ ] Stopping the worker mid-request is confirmed recoverable - [ ] No insertion path is confirmed to assign markup - [ ] The key is confirmed absent from every message sent to a page - [ ] The manifest is confirmed to declare no broad host access ## Launch constraint Do not market omitted PromptDC capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Entered on the options page and stored in the local extension storage area. Not a file in the repository. MODEL_API_KEY=sk-...redacted # Required. The provider endpoint. Also the only host permission the worker needs. MODEL_ENDPOINT=https://api.example.com/v1/messages # Required. Stored with settings and shown on the options page. MODEL_ID=a text model identifier # Required. Your own timeout, set well inside the platform ceiling for a single request. REQUEST_TIMEOUT_MS=45000 # Required. Characters in plus out per month. Requests stop here until the next month. MONTHLY_CHAR_CEILING=500000
$ choose a build depth, inspect the files, then open the complete pack in your agent
People pay for confidence that rewriting works across the sites and editors they use every day, plus maintained profiles, a polished interface, account features, and fixes when web apps change. A personal BYOK clone covers the basic loop well, but keeping rich editors reliable is continuing product work.
xWriting profiles tuned for grammar fix, email replies, social posts, shortening, and tone
xCoding mode platform detection that tailors the rewrite to the AI tool you are prompting (ChatGPT, Claude, Cursor, Lovable and more)
xReliable behavior across Gmail, LinkedIn, Google Docs, Shadow DOM, and framework-managed rich text editors
xCloud accounts, usage credits, profile sync, and a managed billing flow
xPolished mode controls, error recovery, ongoing compatibility updates, and support
Don't feel like building it? These folks already made it free.
all 3 free alternatives to PromptDC →· no votes, no pay-to-list · just what's real
PromptDC pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| pro | $9 | — | 200 prompt enhancements/month; 50 prompt/Markdown files; unlimited profiles. |
| enterprise | $15 | — | Unlimited enhancements, prompt/Markdown files, and profiles. |
| lifetime | custom | — | Unlimited local/BYOK use. |
free tierno free tier
billingmonthly only for Pro and Enterprise; no annual plan; separate one-time Lifetime license
hidden costsThe Lifetime license is BYOK, so model-provider API charges are separate.
verified 2026-08-12 · source ↗
Vibecode PromptDC
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal PromptDC replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does PromptDC cost?
PromptDC costs about $9/month (Pro, checked 2026-07-30), which is $108 per year. That's what you save by replacing it with one prompt.
What do I lose by replacing PromptDC?
Honestly: Writing profiles tuned for grammar fix, email replies, social posts, shortening, and tone; Coding mode platform detection that tailors the rewrite to the AI tool you are prompting (ChatGPT, Claude, Cursor, Lovable and more); Reliable behavior across Gmail, LinkedIn, Google Docs, Shadow DOM, and framework-managed rich text editors; Cloud accounts, usage credits, profile sync, and a managed billing flow; Polished mode controls, error recovery, ongoing compatibility updates, and support. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to PromptDC?
Yes: Page Assist (A browser sidebar that reads the page, rewrites the selection, and saves custom actions; your local model does the thinking.) Writing Tools (Select text anywhere, hit a hotkey, and fix, rewrite, summarize, or obey a custom instruction without opening another tab.) Witsy (System-wide selected-text commands, reusable experts, a scratchpad, and document context; bring the model and skip the subscription.) All 3 curated free alternatives are at vibecodeit.com/promptdc/alternatives. The prompt is for when you want it exactly your way.