From 1287356efd08c40014a6a5178710e530d641f2d9 Mon Sep 17 00:00:00 2001 From: lukitasxue Date: Mon, 15 Jun 2026 15:38:55 +1000 Subject: [PATCH 1/3] feat(extension): add browser activity prototype --- browser-extension/README.md | 59 ++++++ browser-extension/background.js | 131 ++++++++++++ browser-extension/manifest.json | 11 + electron/src/main/browser-activity-bridge.ts | 200 ++++++++++++++++++ electron/src/main/index.ts | 15 ++ electron/src/main/ipc-handlers.ts | 18 ++ electron/src/preload/index.js | 11 + .../components/deepSesh/FocusMonitorPanel.tsx | 37 +++- electron/src/renderer/pages/DeepSeshPage.tsx | 27 ++- electron/src/renderer/styles/deepSesh.css | 28 +++ electron/src/renderer/vite-env.d.ts | 3 + electron/src/shared/browserActivity.ts | 11 + 12 files changed, 539 insertions(+), 12 deletions(-) create mode 100644 browser-extension/README.md create mode 100644 browser-extension/background.js create mode 100644 browser-extension/manifest.json create mode 100644 electron/src/main/browser-activity-bridge.ts create mode 100644 electron/src/shared/browserActivity.ts diff --git a/browser-extension/README.md b/browser-extension/README.md new file mode 100644 index 0000000..14d9171 --- /dev/null +++ b/browser-extension/README.md @@ -0,0 +1,59 @@ +# Taskmaster Browser Activity Prototype + +This is a dev-only Manifest V3 extension for testing active browser tab monitoring with the local Taskmaster Electron app. + +It sends active tab metadata only to `http://127.0.0.1:17382` and only after Taskmaster reports that browser monitoring is active. It does not store URLs, read page content, read cookies, read form data, or send anything to external servers. + +## Start Taskmaster Dev + +From the project root: + +```bash +cd electron +npm run dev +``` + +In another terminal: + +```bash +cd electron +npm run electron +``` + +Start a Deep Sesh or Pomodoro session before testing browser activity. + +## Load In Chrome + +1. Open `chrome://extensions`. +2. Enable Developer mode. +3. Click Load unpacked. +4. Select the project `browser-extension` folder. +5. Start a Taskmaster focus session, then switch tabs. + +## Load In Opera GX + +1. Open `opera://extensions`. +2. Enable Developer mode. +3. Click Load unpacked. +4. Select the project `browser-extension` folder. +5. Start a Taskmaster focus session, then switch tabs. + +## Manual Test + +1. Start the Taskmaster dev app. +2. Start a Deep Sesh or Pomodoro session. +3. Open GitHub, YouTube, or ChatGPT in Chrome or Opera GX. +4. Confirm the Focus Monitor panel shows the current domain and title. +5. Stop the Taskmaster session. +6. Switch browser tabs again and confirm the panel no longer receives new activity. + +## Current Limitations + +- The localhost bridge is for development only. +- It only reports the active tab in the focused browser window. +- It does not classify, block, notify, or persist browsing activity. +- Internal browser pages such as `chrome://`, `edge://`, `opera://`, `about:`, and `devtools://` are ignored. + +## Future Production Plan + +The production version should use Native Messaging instead of an open dev HTTP bridge. diff --git a/browser-extension/background.js b/browser-extension/background.js new file mode 100644 index 0000000..ad34ee5 --- /dev/null +++ b/browser-extension/background.js @@ -0,0 +1,131 @@ +// Dev-only Taskmaster browser activity bridge. +// Privacy behavior: this service worker asks localhost whether monitoring is +// active before reading the active tab URL/title. When Taskmaster says disabled, +// no tab metadata is read or sent. + +const BRIDGE_ORIGIN = 'http://127.0.0.1:17382' +const STATUS_URL = `${BRIDGE_ORIGIN}/taskmaster-browser-monitor/status` +const ACTIVITY_URL = `${BRIDGE_ORIGIN}/taskmaster-browser-monitor/activity` +const INTERNAL_URL_PREFIXES = [ + 'chrome://', + 'edge://', + 'opera://', + 'about:', + 'devtools://', +] + +let pendingReportTimer = null + +// Tab activation means the user changed tabs, but we still check Taskmaster +// status before reading the active tab metadata. +chrome.tabs.onActivated.addListener(() => { + queueActiveTabReport() +}) + +// Tab updates can mean title or URL changes. The handler only queues work, and +// the status check happens before any active tab data is queried. +chrome.tabs.onUpdated.addListener(() => { + queueActiveTabReport() +}) + +// Browser window focus changes can reveal a new active tab, so queue a report +// after confirming the browser has a real focused window. +chrome.windows.onFocusChanged.addListener((windowId) => { + if (windowId !== chrome.windows.WINDOW_ID_NONE) { + queueActiveTabReport() + } +}) + +// Debounces noisy browser events so quick tab changes do not spam localhost. +function queueActiveTabReport() { + if (pendingReportTimer !== null) { + clearTimeout(pendingReportTimer) + } + + pendingReportTimer = setTimeout(() => { + pendingReportTimer = null + reportActiveTabIfMonitoring() + }, 150) +} + +// Main privacy gate. This does not query tabs unless Taskmaster says a focus +// session is running or paused. +async function reportActiveTabIfMonitoring() { + if (!(await isMonitoringEnabled())) { + return + } + + const [tab] = await chrome.tabs.query({ + active: true, + currentWindow: true, + }) + + if (!tab?.url || shouldIgnoreUrl(tab.url)) { + return + } + + const payload = createPayload(tab) + + if (!payload) { + return + } + + await sendActivity(payload) +} + +// Asks the local Electron bridge whether browser monitoring is currently active. +async function isMonitoringEnabled() { + try { + const response = await fetch(STATUS_URL, { method: 'GET' }) + + if (!response.ok) { + return false + } + + const status = await response.json() + + return status.enabled === true + } catch { + return false + } +} + +// Converts the active Chrome/Opera tab into Taskmaster's dev bridge payload. +function createPayload(tab) { + try { + const url = new URL(tab.url) + + return { + source: 'taskmaster-browser-extension', + title: tab.title || 'Untitled tab', + url: tab.url, + domain: url.hostname, + browser: 'chromium', + timestamp: Date.now(), + } + } catch { + return null + } +} + +// Internal browser pages are skipped because they are not useful focus signals. +function shouldIgnoreUrl(url) { + const normalizedUrl = url.toLowerCase() + + return INTERNAL_URL_PREFIXES.some((prefix) => normalizedUrl.startsWith(prefix)) +} + +// Sends tab metadata to localhost only. Failures are expected when Taskmaster is closed. +async function sendActivity(payload) { + try { + await fetch(ACTIVITY_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }) + } catch { + // The bridge only exists while Taskmaster dev is running. + } +} diff --git a/browser-extension/manifest.json b/browser-extension/manifest.json new file mode 100644 index 0000000..a766207 --- /dev/null +++ b/browser-extension/manifest.json @@ -0,0 +1,11 @@ +{ + "manifest_version": 3, + "name": "Taskmaster Browser Activity Prototype", + "description": "Dev-only bridge that sends the active tab title and URL to the local Taskmaster app while a focus session is active.", + "version": "0.1.0", + "permissions": ["tabs"], + "host_permissions": ["http://127.0.0.1:17382/*"], + "background": { + "service_worker": "background.js" + } +} diff --git a/electron/src/main/browser-activity-bridge.ts b/electron/src/main/browser-activity-bridge.ts new file mode 100644 index 0000000..3a875d0 --- /dev/null +++ b/electron/src/main/browser-activity-bridge.ts @@ -0,0 +1,200 @@ +// Dev-only localhost bridge for the Taskmaster browser extension prototype. +// This accepts active tab metadata only while a focus session has enabled monitoring. + +import http from 'node:http' +import type { BrowserActivityPayload } from '../shared/browserActivity.ts' + +const BRIDGE_HOST = '127.0.0.1' +const BRIDGE_PORT = 17382 +const STATUS_PATH = '/taskmaster-browser-monitor/status' +const ACTIVITY_PATH = '/taskmaster-browser-monitor/activity' + +type BrowserActivityListener = (payload: BrowserActivityPayload) => void + +let server: http.Server | null = null +let isBrowserMonitoringActive = false +let latestBrowserActivity: BrowserActivityPayload | null = null +let notifyRenderer: BrowserActivityListener | null = null + +/* Starts the dev HTTP bridge once the Electron app is ready. */ +export function startBrowserActivityBridge(onActivity: BrowserActivityListener) { + notifyRenderer = onActivity + + if (server) { + return + } + + server = http.createServer((request, response) => { + addCorsHeaders(response) + + if (request.method === 'OPTIONS') { + response.writeHead(204) + response.end() + return + } + + const requestUrl = new URL(request.url ?? '/', `http://${BRIDGE_HOST}:${BRIDGE_PORT}`) + + if (requestUrl.pathname === STATUS_PATH) { + handleStatusRequest(request, response) + return + } + + if (requestUrl.pathname === ACTIVITY_PATH) { + handleActivityRequest(request, response) + return + } + + sendJson(response, 404, { error: 'Not found' }) + }) + + server.listen(BRIDGE_PORT, BRIDGE_HOST, () => { + console.log(`[Taskmaster] Browser activity bridge listening on ${BRIDGE_HOST}:${BRIDGE_PORT}`) + }) + + server.on('error', (error) => { + console.error('[Taskmaster] Browser activity bridge failed:', error) + }) +} + +/* Closes the local bridge during app shutdown. */ +export function stopBrowserActivityBridge() { + setBrowserMonitoringActive(false) + + if (!server) { + return + } + + server.close() + server = null +} + +/* Controls whether the extension is allowed to read and send active tab data. */ +export function setBrowserMonitoringActive(isActive: boolean) { + isBrowserMonitoringActive = isActive + + if (!isActive) { + latestBrowserActivity = null + } +} + +/* Returns the latest in-memory browser activity snapshot for newly active renderers. */ +export function getLatestBrowserActivity() { + return latestBrowserActivity +} + +/* Responds to extension status checks before the extension reads tab metadata. */ +function handleStatusRequest( + request: http.IncomingMessage, + response: http.ServerResponse +) { + if (request.method !== 'GET') { + sendJson(response, 405, { error: 'Method not allowed' }) + return + } + + sendJson(response, 200, { enabled: isBrowserMonitoringActive }) +} + +/* Accepts the latest active tab payload while monitoring is enabled. */ +function handleActivityRequest( + request: http.IncomingMessage, + response: http.ServerResponse +) { + if (request.method !== 'POST') { + sendJson(response, 405, { error: 'Method not allowed' }) + return + } + + if (!isBrowserMonitoringActive) { + sendJson(response, 403, { error: 'Browser monitoring is not active' }) + return + } + + readJsonBody(request, response, (body) => { + const payload = parseBrowserActivityPayload(body) + + if (!payload) { + sendJson(response, 400, { error: 'Invalid browser activity payload' }) + return + } + + latestBrowserActivity = payload + notifyRenderer?.(payload) + sendJson(response, 202, { accepted: true }) + }) +} + +/* Reads a small JSON request body without adding an HTTP framework dependency. */ +function readJsonBody( + request: http.IncomingMessage, + response: http.ServerResponse, + onBody: (body: unknown) => void +) { + const chunks: Buffer[] = [] + let bodySize = 0 + + request.on('data', (chunk: Buffer) => { + bodySize += chunk.length + + if (bodySize > 32_768) { + sendJson(response, 413, { error: 'Payload too large' }) + request.destroy() + return + } + + chunks.push(chunk) + }) + + request.on('end', () => { + try { + onBody(JSON.parse(Buffer.concat(chunks).toString('utf8'))) + } catch { + sendJson(response, 400, { error: 'Invalid JSON' }) + } + }) +} + +/* Validates the extension payload before it reaches renderer windows. */ +function parseBrowserActivityPayload(body: unknown): BrowserActivityPayload | null { + if (!isRecord(body)) { + return null + } + + if ( + body.source !== 'taskmaster-browser-extension' || + body.browser !== 'chromium' || + typeof body.title !== 'string' || + typeof body.url !== 'string' || + typeof body.domain !== 'string' || + typeof body.timestamp !== 'number' + ) { + return null + } + + return { + source: 'taskmaster-browser-extension', + title: body.title.slice(0, 500), + url: body.url.slice(0, 2048), + domain: body.domain.slice(0, 255), + browser: 'chromium', + timestamp: body.timestamp, + } +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null +} + +/* Allows the unpacked dev extension to call the local bridge during development. */ +function addCorsHeaders(response: http.ServerResponse) { + response.setHeader('Access-Control-Allow-Origin', '*') + response.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS') + response.setHeader('Access-Control-Allow-Headers', 'Content-Type') +} + +/* Writes consistent JSON responses for status, validation, and activity calls. */ +function sendJson(response: http.ServerResponse, statusCode: number, body: object) { + response.writeHead(statusCode, { 'Content-Type': 'application/json' }) + response.end(JSON.stringify(body)) +} diff --git a/electron/src/main/index.ts b/electron/src/main/index.ts index 79d98ee..15dbdd8 100644 --- a/electron/src/main/index.ts +++ b/electron/src/main/index.ts @@ -5,6 +5,10 @@ import { app, BrowserWindow, ipcMain, Tray, Menu, nativeImage } from 'electron' import path from 'path' import { fileURLToPath } from 'url' +import { + startBrowserActivityBridge, + stopBrowserActivityBridge, +} from './browser-activity-bridge.ts' import { registerIpcHandlers } from './ipc-handlers.ts' @@ -163,8 +167,19 @@ function registerMiniTimerIpcHandlers() { app.whenReady().then(() => { + startBrowserActivityBridge((payload) => { + BrowserWindow.getAllWindows().forEach((window) => { + if (!window.isDestroyed()) { + window.webContents.send('taskmaster:browser-activity', payload) + } + }) + }) registerIpcHandlers() registerMiniTimerIpcHandlers() createWindow() createTray() }) + +app.on('before-quit', () => { + stopBrowserActivityBridge() +}) diff --git a/electron/src/main/ipc-handlers.ts b/electron/src/main/ipc-handlers.ts index e83efee..3cbc800 100644 --- a/electron/src/main/ipc-handlers.ts +++ b/electron/src/main/ipc-handlers.ts @@ -2,9 +2,14 @@ // This is the entry point for every message the renderer sends. import { ipcMain } from 'electron' import { detectCommonWindowsApps } from './appDetection/detectCommonWindowsApps.ts' +import { + getLatestBrowserActivity, + setBrowserMonitoringActive, +} from './browser-activity-bridge.ts' export function registerIpcHandlers() { ipcMain.removeHandler('taskmaster:detect-common-apps') + ipcMain.removeAllListeners('taskmaster:browser-monitoring-active') ipcMain.handle('taskmaster:detect-common-apps', () => { const detectedApps = detectCommonWindowsApps() @@ -14,4 +19,17 @@ export function registerIpcHandlers() { return detectedApps }) + + /* Renderer enables tab reporting only while a focus session is active. */ + ipcMain.on('taskmaster:browser-monitoring-active', (event, isActive: boolean) => { + setBrowserMonitoringActive(isActive) + + if (isActive) { + const latestActivity = getLatestBrowserActivity() + + if (latestActivity) { + event.sender.send('taskmaster:browser-activity', latestActivity) + } + } + }) } diff --git a/electron/src/preload/index.js b/electron/src/preload/index.js index 08acf0c..3c28f8b 100644 --- a/electron/src/preload/index.js +++ b/electron/src/preload/index.js @@ -13,6 +13,17 @@ contextBridge.exposeInMainWorld('taskmaster', { ipcRenderer.send('taskmaster:mini-timer-state', state), sendMiniTimerCommand: (command) => ipcRenderer.send('taskmaster:mini-timer-command', command), + setBrowserMonitoringActive: (isActive) => + ipcRenderer.send('taskmaster:browser-monitoring-active', isActive), + onBrowserActivity: (callback) => { + const listener = (_event, activity) => callback(activity) + + ipcRenderer.on('taskmaster:browser-activity', listener) + + return () => { + ipcRenderer.removeListener('taskmaster:browser-activity', listener) + } + }, onMiniTimerState: (callback) => { const listener = (_event, state) => callback(state) diff --git a/electron/src/renderer/components/deepSesh/FocusMonitorPanel.tsx b/electron/src/renderer/components/deepSesh/FocusMonitorPanel.tsx index 2fb9472..967dfb4 100644 --- a/electron/src/renderer/components/deepSesh/FocusMonitorPanel.tsx +++ b/electron/src/renderer/components/deepSesh/FocusMonitorPanel.tsx @@ -1,29 +1,46 @@ -/* Placeholder for the future live focus monitor shown during active sessions. */ -export default function FocusMonitorPanel() { +// Shows browser extension activity during an active focus session. +// This prototype reports active tab metadata only and does not classify or block. + +import type { BrowserActivityPayload } from '../../../shared/browserActivity' + +type FocusMonitorPanelProps = { + browserActivity: BrowserActivityPayload | null +} + +export default function FocusMonitorPanel({ + browserActivity, +}: FocusMonitorPanelProps) { return ( diff --git a/electron/src/renderer/pages/DeepSeshPage.tsx b/electron/src/renderer/pages/DeepSeshPage.tsx index 740d8c3..21443e3 100644 --- a/electron/src/renderer/pages/DeepSeshPage.tsx +++ b/electron/src/renderer/pages/DeepSeshPage.tsx @@ -1,13 +1,14 @@ // Main Deep Sesh screen shown after onboarding. // This page composes the Deep Sesh UI and keeps timer display text together. -import { useEffect } from 'react' +import { useEffect, useState } from 'react' import DeepSeshModeSelector from '../components/deepSesh/DeepSeshModeSelector' import DeepSeshSetupPanel from '../components/deepSesh/DeepSeshSetupPanel' import DeepSeshTimerCard from '../components/deepSesh/DeepSeshTimerCard' import FocusEnvironmentSummary from '../components/deepSesh/FocusEnvironmentSummary' import FocusMonitorPanel from '../components/deepSesh/FocusMonitorPanel' import { useDeepSeshTimer } from '../hooks/useDeepSeshTimer' +import type { BrowserActivityPayload } from '../../shared/browserActivity' import '../styles/deepSesh.css' export default function DeepSeshPage() { @@ -15,6 +16,8 @@ export default function DeepSeshPage() { const pauseTimer = timer.pause const resumeTimer = timer.resume const stopTimer = timer.stop + const [browserActivity, setBrowserActivity] = + useState(null) const layoutClass = timer.isSessionActive ? 'deep-sesh-screen--active' : 'deep-sesh-screen--setup' @@ -93,6 +96,22 @@ export default function DeepSeshPage() { }) }, [pauseTimer, resumeTimer, stopTimer]) + /* Receives browser extension activity while the focus monitor is visible. */ + useEffect(() => { + return window.taskmaster?.onBrowserActivity((activity) => { + setBrowserActivity(activity) + }) + }, []) + + /** + * Tells the local browser extension bridge when it may receive tab metadata. + * + * The extension checks this status before reading the active tab URL/title. + */ + useEffect(() => { + window.taskmaster?.setBrowserMonitoringActive(timer.isSessionActive) + }, [timer.isSessionActive]) + /* Opens the mini timer window and reports IPC setup issues during development. */ async function openMiniTimer() { try { @@ -171,7 +190,11 @@ export default function DeepSeshPage() { {!timer.isSessionActive && } - {timer.isSessionActive && } + {timer.isSessionActive && ( + + )} diff --git a/electron/src/renderer/styles/deepSesh.css b/electron/src/renderer/styles/deepSesh.css index d36afa9..144a8fa 100644 --- a/electron/src/renderer/styles/deepSesh.css +++ b/electron/src/renderer/styles/deepSesh.css @@ -448,6 +448,31 @@ gap: var(--space-sm); } +.deep-sesh-monitor-current { + display: grid; + gap: 0.18rem; + min-width: 0; + padding: 0.8rem 0.85rem; + border: 1px solid color-mix(in srgb, var(--color-border-accent) 68%, transparent); + border-radius: var(--radius-sm); + background: color-mix(in srgb, var(--color-accent) 10%, var(--color-bg-card)); +} + +.deep-sesh-monitor-current span { + color: var(--color-accent-bright); + font-size: clamp(0.7rem, 0.66vw, 0.78rem); + font-weight: 850; + text-transform: uppercase; +} + +.deep-sesh-monitor-current strong { + overflow: hidden; + color: var(--color-text-main); + font-size: clamp(1rem, 1vw, 1.15rem); + text-overflow: ellipsis; + white-space: nowrap; +} + .deep-sesh-monitor-list div { display: grid; gap: 0.12rem; @@ -464,8 +489,11 @@ } .deep-sesh-monitor-list strong { + overflow: hidden; color: var(--color-text-main); font-size: clamp(0.8rem, 0.76vw, 0.9rem); + text-overflow: ellipsis; + white-space: nowrap; } .deep-sesh-setting-grid { diff --git a/electron/src/renderer/vite-env.d.ts b/electron/src/renderer/vite-env.d.ts index 9bfbf5f..bec83fd 100644 --- a/electron/src/renderer/vite-env.d.ts +++ b/electron/src/renderer/vite-env.d.ts @@ -1,5 +1,6 @@ /// +import type { BrowserActivityPayload } from '../shared/browserActivity' import type { MiniTimerCommand, MiniTimerState } from './types/miniTimer' type DetectedCommonApp = { @@ -17,6 +18,8 @@ declare global { openMiniTimer: () => Promise sendMiniTimerState: (state: MiniTimerState) => void sendMiniTimerCommand: (command: MiniTimerCommand) => void + setBrowserMonitoringActive: (isActive: boolean) => void + onBrowserActivity: (callback: (activity: BrowserActivityPayload) => void) => () => void onMiniTimerState: (callback: (state: MiniTimerState | null) => void) => () => void onMiniTimerCommand: (callback: (command: MiniTimerCommand) => void) => () => void } diff --git a/electron/src/shared/browserActivity.ts b/electron/src/shared/browserActivity.ts new file mode 100644 index 0000000..e580a3e --- /dev/null +++ b/electron/src/shared/browserActivity.ts @@ -0,0 +1,11 @@ +// Shared browser activity payloads for the dev extension HTTP bridge. +// Raw URLs are kept in memory only and are not persisted by Taskmaster. + +export type BrowserActivityPayload = { + source: 'taskmaster-browser-extension' + title: string + url: string + domain: string + browser: 'chromium' + timestamp: number +} From 78db492338689135f3613f4ed550a8433ce95e3e Mon Sep 17 00:00:00 2001 From: lukitasxue Date: Mon, 15 Jun 2026 18:12:45 +1000 Subject: [PATCH 2/3] google extension privacy policy, and settings --- browser-extension/README.md | 34 +++++++++++--- browser-extension/background.js | 65 ++++++++++++++++++++++++--- browser-extension/manifest.json | 7 ++- docs/browser-extension-privacy.md | 42 +++++++++++++++++ docs/chrome-web-store-listing.md | 46 +++++++++++++++++++ scripts/package-browser-extension.ps1 | 29 ++++++++++++ 6 files changed, 207 insertions(+), 16 deletions(-) create mode 100644 docs/browser-extension-privacy.md create mode 100644 docs/chrome-web-store-listing.md create mode 100644 scripts/package-browser-extension.ps1 diff --git a/browser-extension/README.md b/browser-extension/README.md index 14d9171..9df13d5 100644 --- a/browser-extension/README.md +++ b/browser-extension/README.md @@ -1,8 +1,10 @@ -# Taskmaster Browser Activity Prototype +# Taskmaster Browser Monitor -This is a dev-only Manifest V3 extension for testing active browser tab monitoring with the local Taskmaster Electron app. +This is the browser extension package for connecting active browser tab metadata to the local Taskmaster desktop app. -It sends active tab metadata only to `http://127.0.0.1:17382` and only after Taskmaster reports that browser monitoring is active. It does not store URLs, read page content, read cookies, read form data, or send anything to external servers. +It reads the active tab URL/title only after Taskmaster reports that browser monitoring is active. It does not store raw URLs permanently, read page content, read cookies, read form data, use the browsing history API, or send anything to external servers. + +The official MVP transport is Native Messaging. The previous localhost bridge is retained in `background.js` as a clearly separated development transport path, but the production manifest does not include localhost host permissions. ## Start Taskmaster Dev @@ -22,13 +24,14 @@ npm run electron Start a Deep Sesh or Pomodoro session before testing browser activity. -## Load In Chrome +## Load In Chrome For Local Review 1. Open `chrome://extensions`. 2. Enable Developer mode. 3. Click Load unpacked. 4. Select the project `browser-extension` folder. 5. Start a Taskmaster focus session, then switch tabs. +6. Native Messaging requires a host install step that will be added in the next phase. ## Load In Opera GX @@ -49,11 +52,30 @@ Start a Deep Sesh or Pomodoro session before testing browser activity. ## Current Limitations -- The localhost bridge is for development only. +- Native Messaging host setup is not implemented yet. - It only reports the active tab in the focused browser window. - It does not classify, block, notify, or persist browsing activity. - Internal browser pages such as `chrome://`, `edge://`, `opera://`, `about:`, and `devtools://` are ignored. +## Package For Chrome Web Store Review + +From the project root: + +```powershell +.\scripts\package-browser-extension.ps1 +``` + +The zip is created at: + +```txt +dist/taskmaster-browser-monitor-extension.zip +``` + +The package includes only: + +- `manifest.json` +- `background.js` + ## Future Production Plan -The production version should use Native Messaging instead of an open dev HTTP bridge. +The next phase will add the Native Messaging host and install scripts. diff --git a/browser-extension/background.js b/browser-extension/background.js index ad34ee5..5975ddc 100644 --- a/browser-extension/background.js +++ b/browser-extension/background.js @@ -1,11 +1,20 @@ -// Dev-only Taskmaster browser activity bridge. -// Privacy behavior: this service worker asks localhost whether monitoring is -// active before reading the active tab URL/title. When Taskmaster says disabled, -// no tab metadata is read or sent. +// Taskmaster Browser Monitor service worker. +// This file owns active-tab metadata collection for the browser extension. +// +// Privacy behavior: +// - No external servers are used. +// - No content scripts are used. +// - Page content, cookies, form inputs, and the browsing history API are never read. +// - The active tab URL/title is queried only after Taskmaster says monitoring is active. +// +// Transport is intentionally separated from tab collection so the same privacy +// gate can work with the current dev localhost bridge and future Native Messaging. const BRIDGE_ORIGIN = 'http://127.0.0.1:17382' const STATUS_URL = `${BRIDGE_ORIGIN}/taskmaster-browser-monitor/status` const ACTIVITY_URL = `${BRIDGE_ORIGIN}/taskmaster-browser-monitor/activity` +const NATIVE_HOST_NAME = 'com.taskmaster.browser_monitor' +const TRANSPORT_MODE = 'native-messaging' const INTERNAL_URL_PREFIXES = [ 'chrome://', 'edge://', @@ -73,8 +82,30 @@ async function reportActiveTabIfMonitoring() { await sendActivity(payload) } -// Asks the local Electron bridge whether browser monitoring is currently active. +// Asks the active transport whether browser monitoring is currently enabled. async function isMonitoringEnabled() { + if (TRANSPORT_MODE === 'native-messaging') { + return isNativeMonitoringEnabled() + } + + return isDevLocalhostMonitoringEnabled() +} + +// Placeholder production status check. Phase 2 will wire this to the native host. +async function isNativeMonitoringEnabled() { + try { + const response = await chrome.runtime.sendNativeMessage(NATIVE_HOST_NAME, { + type: 'taskmaster-browser-monitor-status', + }) + + return response?.enabled === true + } catch { + return false + } +} + +// Dev-only localhost status check retained for local prototype testing. +async function isDevLocalhostMonitoringEnabled() { try { const response = await fetch(STATUS_URL, { method: 'GET' }) @@ -115,8 +146,30 @@ function shouldIgnoreUrl(url) { return INTERNAL_URL_PREFIXES.some((prefix) => normalizedUrl.startsWith(prefix)) } -// Sends tab metadata to localhost only. Failures are expected when Taskmaster is closed. +// Sends tab metadata through the active transport. async function sendActivity(payload) { + if (TRANSPORT_MODE === 'native-messaging') { + await sendNativeActivity(payload) + return + } + + await sendDevLocalhostActivity(payload) +} + +// Placeholder production sender. Phase 2 will add the native host implementation. +async function sendNativeActivity(payload) { + try { + await chrome.runtime.sendNativeMessage(NATIVE_HOST_NAME, { + type: 'taskmaster-browser-activity', + payload, + }) + } catch { + // Native Messaging is expected to be unavailable until the host is installed. + } +} + +// Dev-only localhost sender. This is not the official production transport. +async function sendDevLocalhostActivity(payload) { try { await fetch(ACTIVITY_URL, { method: 'POST', diff --git a/browser-extension/manifest.json b/browser-extension/manifest.json index a766207..72b3cd2 100644 --- a/browser-extension/manifest.json +++ b/browser-extension/manifest.json @@ -1,10 +1,9 @@ { "manifest_version": 3, - "name": "Taskmaster Browser Activity Prototype", - "description": "Dev-only bridge that sends the active tab title and URL to the local Taskmaster app while a focus session is active.", + "name": "Taskmaster Browser Monitor", + "description": "Active tab metadata bridge for local Taskmaster focus sessions.", "version": "0.1.0", - "permissions": ["tabs"], - "host_permissions": ["http://127.0.0.1:17382/*"], + "permissions": ["tabs", "nativeMessaging"], "background": { "service_worker": "background.js" } diff --git a/docs/browser-extension-privacy.md b/docs/browser-extension-privacy.md new file mode 100644 index 0000000..742bf55 --- /dev/null +++ b/docs/browser-extension-privacy.md @@ -0,0 +1,42 @@ +# Taskmaster Browser Extension Privacy Note + +This draft explains the data behavior for the Taskmaster Browser Monitor MVP. + +## Data Accessed + +The extension accesses only active tab metadata: + +- Active tab URL +- Active tab title +- Derived domain +- Timestamp + +## When Data Is Accessed + +The extension checks whether Taskmaster browser monitoring is active before reading active tab metadata. Monitoring should be active only while a Taskmaster Deep Sesh or Pomodoro session is running or paused. + +## Where Data Goes + +Active tab metadata is sent only to the local Taskmaster desktop app on the same device. + +Taskmaster does not send this data to external servers. + +## Data Not Accessed + +The extension does not access: + +- Page content +- Cookies +- Passwords +- Form inputs +- Full browsing history +- Background tabs +- Analytics identifiers + +## Storage + +Raw URLs are not permanently stored by default. The MVP keeps current browser activity in memory so the Taskmaster desktop UI can show the current active domain and title. + +## Sale And Advertising + +Taskmaster does not sell browser activity data and does not use it for advertising. diff --git a/docs/chrome-web-store-listing.md b/docs/chrome-web-store-listing.md new file mode 100644 index 0000000..391ab6e --- /dev/null +++ b/docs/chrome-web-store-listing.md @@ -0,0 +1,46 @@ +# Chrome Web Store Listing Draft + +## Extension Name + +Taskmaster Browser Monitor + +## Short Description + +Connects active browser tab metadata to the local Taskmaster desktop focus app. + +## Detailed Description + +Taskmaster Browser Monitor helps the Taskmaster desktop app understand the active browser tab during a running focus session. + +The extension sends the active tab title, URL, domain, and timestamp to the local Taskmaster desktop app only when a Deep Sesh or Pomodoro focus session is running or paused. It does not send data to external servers, does not read page content, and does not collect cookies, passwords, form inputs, or full browsing history. + +The Taskmaster desktop app is required. + +## Permissions Explanation + +### `tabs` + +Taskmaster uses the `tabs` permission to read the active tab title and URL after the local Taskmaster desktop app says browser monitoring is active. The implementation does not use the browsing history API and does not inspect background tabs. + +Chrome may describe this permission broadly. Taskmaster limits use to active-session, active-tab metadata only. + +### `nativeMessaging` + +Taskmaster uses Native Messaging to communicate with the local Taskmaster desktop app on the same device. Browser activity is not sent to any external server. + +## Reviewer Test Instructions + +1. Install and open the Taskmaster desktop app. +2. Install the Taskmaster Browser Monitor extension. +3. Start a Deep Sesh or Pomodoro session in Taskmaster. +4. Open a normal web page such as GitHub, YouTube, or ChatGPT. +5. Confirm the Taskmaster desktop app shows the active tab domain and title. +6. Stop the focus session. +7. Switch tabs and confirm Taskmaster no longer receives new browser activity. + +## Notes + +- The desktop app is required. +- Native Messaging host setup is required for production operation. +- The extension does not include content scripts. +- The extension does not request cookies, history, webRequest, scripting, or ``. diff --git a/scripts/package-browser-extension.ps1 b/scripts/package-browser-extension.ps1 new file mode 100644 index 0000000..02cf6ec --- /dev/null +++ b/scripts/package-browser-extension.ps1 @@ -0,0 +1,29 @@ +# Packages the Taskmaster browser extension for manual Chrome Web Store upload. +# The script is dependency-free and includes only the extension runtime files. + +$ErrorActionPreference = "Stop" + +$repoRoot = Split-Path -Parent $PSScriptRoot +$extensionDir = Join-Path $repoRoot "browser-extension" +$distDir = Join-Path $repoRoot "dist" +$zipPath = Join-Path $distDir "taskmaster-browser-monitor-extension.zip" +$stagingDir = Join-Path $distDir "taskmaster-browser-monitor-extension" + +if (Test-Path $stagingDir) { + Remove-Item -LiteralPath $stagingDir -Recurse -Force +} + +New-Item -ItemType Directory -Path $stagingDir -Force | Out-Null +New-Item -ItemType Directory -Path $distDir -Force | Out-Null + +Copy-Item -LiteralPath (Join-Path $extensionDir "manifest.json") -Destination $stagingDir +Copy-Item -LiteralPath (Join-Path $extensionDir "background.js") -Destination $stagingDir + +if (Test-Path $zipPath) { + Remove-Item -LiteralPath $zipPath -Force +} + +Compress-Archive -Path (Join-Path $stagingDir "*") -DestinationPath $zipPath +Remove-Item -LiteralPath $stagingDir -Recurse -Force + +Write-Host "Created $zipPath" From 2d84162cb43b0dd6f23a1d9d3cc6f49de21be9eb Mon Sep 17 00:00:00 2001 From: lukitasxue Date: Wed, 17 Jun 2026 16:48:40 +1000 Subject: [PATCH 3/3] feat(extension): add native messaging host --- browser-extension/README.md | 29 ++- browser-extension/background.js | 9 +- electron/src/main/browser-activity-bridge.ts | 11 +- native-host/.gitignore | 2 + native-host/README.md | 69 ++++++ ...com.taskmaster.browser_monitor.chrome.json | 9 + native-host/install-chrome-native-host.ps1 | 41 ++++ .../taskmaster-browser-monitor-host.js | 201 ++++++++++++++++++ native-host/uninstall-chrome-native-host.ps1 | 14 ++ 9 files changed, 372 insertions(+), 13 deletions(-) create mode 100644 native-host/.gitignore create mode 100644 native-host/README.md create mode 100644 native-host/com.taskmaster.browser_monitor.chrome.json create mode 100644 native-host/install-chrome-native-host.ps1 create mode 100644 native-host/taskmaster-browser-monitor-host.js create mode 100644 native-host/uninstall-chrome-native-host.ps1 diff --git a/browser-extension/README.md b/browser-extension/README.md index 9df13d5..ca426bc 100644 --- a/browser-extension/README.md +++ b/browser-extension/README.md @@ -31,7 +31,7 @@ Start a Deep Sesh or Pomodoro session before testing browser activity. 3. Click Load unpacked. 4. Select the project `browser-extension` folder. 5. Start a Taskmaster focus session, then switch tabs. -6. Native Messaging requires a host install step that will be added in the next phase. +6. Install the native host using the steps below. ## Load In Opera GX @@ -50,9 +50,24 @@ Start a Deep Sesh or Pomodoro session before testing browser activity. 5. Stop the Taskmaster session. 6. Switch browser tabs again and confirm the panel no longer receives new activity. +## Install Native Messaging Host + +From the project root: + +```powershell +.\native-host\install-chrome-native-host.ps1 +``` + +This registers the host under the current Windows user: + +```txt +HKCU\Software\Google\Chrome\NativeMessagingHosts\com.taskmaster.browser_monitor +``` + +Restart Chrome after installing the host. + ## Current Limitations -- Native Messaging host setup is not implemented yet. - It only reports the active tab in the focused browser window. - It does not classify, block, notify, or persist browsing activity. - Internal browser pages such as `chrome://`, `edge://`, `opera://`, `about:`, and `devtools://` are ignored. @@ -76,6 +91,12 @@ The package includes only: - `manifest.json` - `background.js` -## Future Production Plan +## Native Messaging + +The extension uses: + +```txt +com.taskmaster.browser_monitor +``` -The next phase will add the Native Messaging host and install scripts. +The native host forwards validated messages to the local Taskmaster desktop app on the same device. diff --git a/browser-extension/background.js b/browser-extension/background.js index 5975ddc..9bceaf8 100644 --- a/browser-extension/background.js +++ b/browser-extension/background.js @@ -7,8 +7,8 @@ // - Page content, cookies, form inputs, and the browsing history API are never read. // - The active tab URL/title is queried only after Taskmaster says monitoring is active. // -// Transport is intentionally separated from tab collection so the same privacy -// gate can work with the current dev localhost bridge and future Native Messaging. +// Transport is intentionally separated from tab collection. Production uses +// Native Messaging; the old localhost path is retained only for local debugging. const BRIDGE_ORIGIN = 'http://127.0.0.1:17382' const STATUS_URL = `${BRIDGE_ORIGIN}/taskmaster-browser-monitor/status` @@ -91,7 +91,8 @@ async function isMonitoringEnabled() { return isDevLocalhostMonitoringEnabled() } -// Placeholder production status check. Phase 2 will wire this to the native host. +// Production status check. Chrome launches the registered Native Messaging host, +// then the host asks the local Taskmaster bridge whether monitoring is active. async function isNativeMonitoringEnabled() { try { const response = await chrome.runtime.sendNativeMessage(NATIVE_HOST_NAME, { @@ -156,7 +157,7 @@ async function sendActivity(payload) { await sendDevLocalhostActivity(payload) } -// Placeholder production sender. Phase 2 will add the native host implementation. +// Production sender. The native host validates and forwards this to Taskmaster. async function sendNativeActivity(payload) { try { await chrome.runtime.sendNativeMessage(NATIVE_HOST_NAME, { diff --git a/electron/src/main/browser-activity-bridge.ts b/electron/src/main/browser-activity-bridge.ts index 3a875d0..33315c4 100644 --- a/electron/src/main/browser-activity-bridge.ts +++ b/electron/src/main/browser-activity-bridge.ts @@ -1,5 +1,6 @@ -// Dev-only localhost bridge for the Taskmaster browser extension prototype. -// This accepts active tab metadata only while a focus session has enabled monitoring. +// Local Taskmaster app bridge for browser activity messages. +// Native Messaging host forwards validated active-tab metadata here while a +// focus session has enabled monitoring. import http from 'node:http' import type { BrowserActivityPayload } from '../shared/browserActivity.ts' @@ -16,7 +17,7 @@ let isBrowserMonitoringActive = false let latestBrowserActivity: BrowserActivityPayload | null = null let notifyRenderer: BrowserActivityListener | null = null -/* Starts the dev HTTP bridge once the Electron app is ready. */ +/* Starts the local HTTP bridge once the Electron app is ready. */ export function startBrowserActivityBridge(onActivity: BrowserActivityListener) { notifyRenderer = onActivity @@ -57,7 +58,7 @@ export function startBrowserActivityBridge(onActivity: BrowserActivityListener) }) } -/* Closes the local bridge during app shutdown. */ +/* Closes the local app bridge during app shutdown. */ export function stopBrowserActivityBridge() { setBrowserMonitoringActive(false) @@ -186,7 +187,7 @@ function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null } -/* Allows the unpacked dev extension to call the local bridge during development. */ +/* Allows local development and the native host to call the app bridge. */ function addCorsHeaders(response: http.ServerResponse) { response.setHeader('Access-Control-Allow-Origin', '*') response.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS') diff --git a/native-host/.gitignore b/native-host/.gitignore new file mode 100644 index 0000000..1d7ebf0 --- /dev/null +++ b/native-host/.gitignore @@ -0,0 +1,2 @@ +# Ignore machine-specific launchers/manifests generated by the native host installer. +.generated/ diff --git a/native-host/README.md b/native-host/README.md new file mode 100644 index 0000000..6082760 --- /dev/null +++ b/native-host/README.md @@ -0,0 +1,69 @@ +# Taskmaster Browser Monitor Native Host + +This folder contains the Chrome Native Messaging host for the Taskmaster Browser Monitor extension. + +The extension talks to this host through Chrome Native Messaging. The host then forwards validated messages to the local Taskmaster desktop app bridge on `127.0.0.1:17382`. + +The extension itself does not require localhost host permissions. + +## Host Name + +```txt +com.taskmaster.browser_monitor +``` + +## Allowed Chrome Extension + +```txt +kibldolnfbpajohdnkbjfjefnemllapm +``` + +## Install On Windows + +From the repo root: + +```powershell +.\native-host\install-chrome-native-host.ps1 +``` + +This registers the host under: + +```txt +HKCU\Software\Google\Chrome\NativeMessagingHosts\com.taskmaster.browser_monitor +``` + +HKCU is used so admin rights are not required. + +## Verify Registration + +1. Open `regedit`. +2. Go to: + ```txt + HKEY_CURRENT_USER\Software\Google\Chrome\NativeMessagingHosts\com.taskmaster.browser_monitor + ``` +3. Confirm the default value points to the generated manifest in: + ```txt + native-host\.generated\com.taskmaster.browser_monitor.chrome.json + ``` + +## Test Flow + +1. Install the native host. +2. Start Taskmaster Electron dev. +3. Load or install the Chrome extension with ID `kibldolnfbpajohdnkbjfjefnemllapm`. +4. Start a Deep Sesh or Pomodoro session. +5. Open GitHub, YouTube, or ChatGPT. +6. Confirm FocusMonitorPanel shows the current domain and title. +7. Stop the focus session and confirm tab updates stop. + +## Debugging + +If Chrome says the native host was not found: + +- Confirm the registry key exists under HKCU, not HKLM. +- Confirm the manifest path in the registry exists. +- Confirm the generated manifest `path` points to the generated `.cmd` launcher. +- Confirm `node` is installed and available when running the install script. +- Restart Chrome after installing the native host. + +Raw URLs are not logged by default. diff --git a/native-host/com.taskmaster.browser_monitor.chrome.json b/native-host/com.taskmaster.browser_monitor.chrome.json new file mode 100644 index 0000000..17c226e --- /dev/null +++ b/native-host/com.taskmaster.browser_monitor.chrome.json @@ -0,0 +1,9 @@ +{ + "name": "com.taskmaster.browser_monitor", + "description": "Taskmaster Browser Monitor Native Host", + "path": "ABSOLUTE_PATH_CREATED_BY_install-chrome-native-host.ps1", + "type": "stdio", + "allowed_origins": [ + "chrome-extension://kibldolnfbpajohdnkbjfjefnemllapm/" + ] +} diff --git a/native-host/install-chrome-native-host.ps1 b/native-host/install-chrome-native-host.ps1 new file mode 100644 index 0000000..178ca15 --- /dev/null +++ b/native-host/install-chrome-native-host.ps1 @@ -0,0 +1,41 @@ +# Registers the Taskmaster Browser Monitor native host for Chrome under HKCU. +# This does not require admin rights and writes a generated local manifest. + +$ErrorActionPreference = "Stop" + +$hostName = "com.taskmaster.browser_monitor" +$nativeHostDir = $PSScriptRoot +$generatedDir = Join-Path $nativeHostDir ".generated" +$generatedManifestPath = Join-Path $generatedDir "$hostName.chrome.json" +$generatedLauncherPath = Join-Path $generatedDir "taskmaster-browser-monitor-host.cmd" +$hostScriptPath = Join-Path $nativeHostDir "taskmaster-browser-monitor-host.js" +$nodePath = (Get-Command node -ErrorAction Stop).Source +$registryPath = "HKCU:\Software\Google\Chrome\NativeMessagingHosts\$hostName" + +New-Item -ItemType Directory -Path $generatedDir -Force | Out-Null + +$launcherContent = @" +@echo off +"$nodePath" "$hostScriptPath" +"@ + +Set-Content -LiteralPath $generatedLauncherPath -Value $launcherContent -Encoding ASCII + +$manifest = @{ + name = $hostName + description = "Taskmaster Browser Monitor Native Host" + path = $generatedLauncherPath + type = "stdio" + allowed_origins = @( + "chrome-extension://kibldolnfbpajohdnkbjfjefnemllapm/" + ) +} + +$manifest | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $generatedManifestPath -Encoding UTF8 + +New-Item -Path $registryPath -Force | Out-Null +Set-Item -Path $registryPath -Value $generatedManifestPath + +Write-Host "Registered $hostName" +Write-Host "Manifest: $generatedManifestPath" +Write-Host "Registry: $registryPath" diff --git a/native-host/taskmaster-browser-monitor-host.js b/native-host/taskmaster-browser-monitor-host.js new file mode 100644 index 0000000..f5a4e78 --- /dev/null +++ b/native-host/taskmaster-browser-monitor-host.js @@ -0,0 +1,201 @@ +#!/usr/bin/env node +// Native Messaging host for Taskmaster Browser Monitor. +// Reads Chrome length-prefixed JSON from stdin, validates messages, and forwards +// active-tab metadata to the local Taskmaster desktop app bridge. + +const http = require('node:http') + +const BRIDGE_HOST = '127.0.0.1' +const BRIDGE_PORT = 17382 +const STATUS_PATH = '/taskmaster-browser-monitor/status' +const ACTIVITY_PATH = '/taskmaster-browser-monitor/activity' +const MAX_MESSAGE_BYTES = 1024 * 1024 + +let inputBuffer = Buffer.alloc(0) + +process.stdin.on('data', (chunk) => { + inputBuffer = Buffer.concat([inputBuffer, chunk]) + readAvailableMessages() +}) + +process.stdin.on('end', () => { + process.exit(0) +}) + +process.stdin.resume() + +function readAvailableMessages() { + while (inputBuffer.length >= 4) { + const messageLength = inputBuffer.readUInt32LE(0) + + if (messageLength > MAX_MESSAGE_BYTES) { + writeNativeResponse({ ok: false, error: 'Message too large' }) + process.exit(1) + } + + if (inputBuffer.length < messageLength + 4) { + return + } + + const messageBuffer = inputBuffer.subarray(4, messageLength + 4) + inputBuffer = inputBuffer.subarray(messageLength + 4) + + void handleNativeMessage(messageBuffer) + } +} + +async function handleNativeMessage(messageBuffer) { + let message + + try { + message = JSON.parse(messageBuffer.toString('utf8')) + } catch { + writeNativeResponse({ ok: false, error: 'Invalid JSON' }) + return + } + + if (!isRecord(message) || typeof message.type !== 'string') { + writeNativeResponse({ ok: false, error: 'Invalid message shape' }) + return + } + + if (message.type === 'taskmaster-browser-monitor-status') { + await handleStatusMessage() + return + } + + if (message.type === 'taskmaster-browser-activity') { + await handleActivityMessage(message.payload) + return + } + + writeNativeResponse({ ok: false, error: 'Unknown message type' }) +} + +async function handleStatusMessage() { + try { + const status = await requestTaskmaster({ + method: 'GET', + path: STATUS_PATH, + }) + + writeNativeResponse({ + ok: true, + enabled: status.enabled === true, + }) + } catch { + writeNativeResponse({ + ok: false, + enabled: false, + error: 'Taskmaster bridge unavailable', + }) + } +} + +async function handleActivityMessage(payload) { + const validatedPayload = parseBrowserActivityPayload(payload) + + if (!validatedPayload) { + writeNativeResponse({ ok: false, error: 'Invalid activity payload' }) + return + } + + try { + const response = await requestTaskmaster({ + method: 'POST', + path: ACTIVITY_PATH, + body: validatedPayload, + }) + + writeNativeResponse({ + ok: true, + accepted: response.accepted === true, + }) + } catch { + writeNativeResponse({ + ok: false, + accepted: false, + error: 'Taskmaster bridge unavailable', + }) + } +} + +function requestTaskmaster({ method, path, body }) { + const requestBody = body ? JSON.stringify(body) : '' + + return new Promise((resolve, reject) => { + const request = http.request( + { + host: BRIDGE_HOST, + port: BRIDGE_PORT, + path, + method, + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(requestBody), + }, + }, + (response) => { + const chunks = [] + + response.on('data', (chunk) => chunks.push(chunk)) + response.on('end', () => { + const rawBody = Buffer.concat(chunks).toString('utf8') + + if (!response.statusCode || response.statusCode >= 400) { + reject(new Error(`Taskmaster returned ${response.statusCode}`)) + return + } + + try { + resolve(rawBody ? JSON.parse(rawBody) : {}) + } catch { + reject(new Error('Taskmaster returned invalid JSON')) + } + }) + } + ) + + request.on('error', reject) + request.write(requestBody) + request.end() + }) +} + +function parseBrowserActivityPayload(value) { + if (!isRecord(value)) { + return null + } + + if ( + value.source !== 'taskmaster-browser-extension' || + value.browser !== 'chromium' || + typeof value.title !== 'string' || + typeof value.url !== 'string' || + typeof value.domain !== 'string' || + typeof value.timestamp !== 'number' + ) { + return null + } + + return { + source: 'taskmaster-browser-extension', + title: value.title.slice(0, 500), + url: value.url.slice(0, 2048), + domain: value.domain.slice(0, 255), + browser: 'chromium', + timestamp: value.timestamp, + } +} + +function writeNativeResponse(message) { + const response = Buffer.from(JSON.stringify(message), 'utf8') + const header = Buffer.alloc(4) + + header.writeUInt32LE(response.length, 0) + process.stdout.write(Buffer.concat([header, response])) +} + +function isRecord(value) { + return typeof value === 'object' && value !== null +} diff --git a/native-host/uninstall-chrome-native-host.ps1 b/native-host/uninstall-chrome-native-host.ps1 new file mode 100644 index 0000000..81b9b98 --- /dev/null +++ b/native-host/uninstall-chrome-native-host.ps1 @@ -0,0 +1,14 @@ +# Unregisters the Taskmaster Browser Monitor native host from Chrome under HKCU. +# Generated files are left on disk so local debugging artifacts are not removed unexpectedly. + +$ErrorActionPreference = "Stop" + +$hostName = "com.taskmaster.browser_monitor" +$registryPath = "HKCU:\Software\Google\Chrome\NativeMessagingHosts\$hostName" + +if (Test-Path $registryPath) { + Remove-Item -LiteralPath $registryPath -Recurse -Force + Write-Host "Unregistered $hostName" +} else { + Write-Host "$hostName was not registered" +}