From 2aa13f8ffce6916dc39800555ef55d11677df7ba Mon Sep 17 00:00:00 2001 From: sarahxsanders Date: Thu, 12 Feb 2026 10:44:36 -0500 Subject: [PATCH 1/4] js skill --- src/javascript/javascript-wizard-agent.ts | 194 ++++++++++++++++++++++ src/javascript/utils.ts | 72 ++++++++ src/lib/constants.ts | 1 + src/lib/registry.ts | 2 + 4 files changed, 269 insertions(+) create mode 100644 src/javascript/javascript-wizard-agent.ts create mode 100644 src/javascript/utils.ts diff --git a/src/javascript/javascript-wizard-agent.ts b/src/javascript/javascript-wizard-agent.ts new file mode 100644 index 0000000..90f8a94 --- /dev/null +++ b/src/javascript/javascript-wizard-agent.ts @@ -0,0 +1,194 @@ +/* Generic JavaScript language wizard using posthog-agent with PostHog MCP */ +import type { WizardOptions } from '../utils/types'; +import type { FrameworkConfig } from '../lib/framework-config'; +import { Integration } from '../lib/constants'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { hasPackageInstalled } from '../utils/package-json'; +import { tryGetPackageJson } from '../utils/clack-utils'; +import { + FRAMEWORK_PACKAGES, + detectJsPackageManager, + detectBundler, + type JavaScriptContext, +} from './utils'; + +export const JAVASCRIPT_AGENT_CONFIG: FrameworkConfig = { + metadata: { + name: 'JavaScript', + integration: Integration.javascript, + beta: true, + docsUrl: 'https://posthog.com/docs/libraries/js', + gatherContext: async (options: WizardOptions) => { + const packageManagerName = detectJsPackageManager(options); + const hasTypeScript = fs.existsSync( + path.join(options.installDir, 'tsconfig.json'), + ); + const hasBundler = detectBundler(options); + return { packageManagerName, hasTypeScript, hasBundler }; + }, + }, + + detection: { + packageName: 'posthog-js', + packageDisplayName: 'JavaScript', + usesPackageJson: false, + getVersion: () => undefined, + detect: async (options) => { + const packageJson = await tryGetPackageJson(options); + if (!packageJson) { + return false; + } + + // Exclude projects with known framework packages + for (const frameworkPkg of FRAMEWORK_PACKAGES) { + if (hasPackageInstalled(frameworkPkg, packageJson)) { + return false; + } + } + + // Ensure this is actually a JS project, not just a package.json for tooling + const { installDir } = options; + + // Check for a lockfile + const hasLockfile = [ + 'package-lock.json', + 'yarn.lock', + 'pnpm-lock.yaml', + 'bun.lockb', + 'bun.lock', + ].some((lockfile) => fs.existsSync(path.join(installDir, lockfile))); + + if (hasLockfile) { + return true; + } + + // Fallback: check if package.json has actual dependencies + const hasDeps = + (packageJson.dependencies && + Object.keys(packageJson.dependencies).length > 0) || + (packageJson.devDependencies && + Object.keys(packageJson.devDependencies).length > 0); + + return !!hasDeps; + }, + }, + + environment: { + uploadToHosting: false, + getEnvVars: (apiKey: string, host: string) => ({ + POSTHOG_API_KEY: apiKey, + POSTHOG_HOST: host, + }), + }, + + analytics: { + getTags: (context) => { + const tags: Record = { + packageManager: context.packageManagerName ?? 'unknown', + }; + if (context.hasBundler) { + tags.bundler = context.hasBundler; + } + return tags; + }, + }, + + prompts: { + projectTypeDetection: + 'This is a JavaScript/TypeScript project. Look for package.json and lockfiles (package-lock.json, yarn.lock, pnpm-lock.yaml, bun.lockb) to confirm.', + packageInstallation: + 'Look for lockfiles to determine the package manager (npm, yarn, pnpm, bun). Do not manually edit package.json.', + getAdditionalContextLines: (context) => { + const lines = [ + `Package manager: ${context.packageManagerName ?? 'unknown'}`, + `Has TypeScript: ${context.hasTypeScript ? 'yes' : 'no'}`, + `Framework docs ID: js (use posthog://docs/frameworks/js for documentation if available)`, + `Project type: Generic JavaScript/TypeScript application (no specific framework detected)`, + ``, + `## CRITICAL: posthog-js Best Practices`, + ``, + `### 1. Use posthog-js (Browser SDK)`, + `This is a browser-side JavaScript project. Use the posthog-js package, NOT posthog-node.`, + `posthog-js is designed for client-side use and includes autocapture, session recording, and feature flags.`, + ``, + `### 2. Initialization (REQUIRED)`, + `posthog.init() MUST be called before any other PostHog methods:`, + ``, + `import posthog from 'posthog-js'`, + ``, + `posthog.init('', {`, + ` api_host: '',`, + `})`, + ``, + `### 3. Autocapture`, + `Autocapture is ON by default with posthog-js. It tracks clicks, form submissions, and pageviews automatically.`, + `Do NOT disable autocapture unless the user explicitly requests it.`, + ``, + `### 4. Error Tracking`, + `Use posthog.captureException(error) for error tracking.`, + ``, + `### 5. NEVER Send PII (Personally Identifiable Information)`, + `DO NOT include in event properties:`, + `- Email addresses`, + `- Full names`, + `- Phone numbers`, + `- Physical addresses`, + `- IP addresses`, + `- Any user-generated content (messages, comments, form submissions)`, + ``, + `SAFE event properties:`, + `posthog.capture('form_submitted', {`, + ` form_type: 'contact',`, + ` field_count: 5,`, + ` has_attachment: true`, + `})`, + ``, + `UNSAFE (DO NOT DO THIS):`, + `posthog.capture('form_submitted', {`, + ` email: userEmail, // NEVER send actual email`, + ` message: messageContent, // NEVER send user content`, + `})`, + ``, + `### 6. SPA Pageview Tracking`, + `For single-page applications without a framework router, you may need to manually capture pageviews:`, + `posthog.capture('$pageview')`, + `Or use capture_pageview: 'history_change' in the init options for History API based routing.`, + ``, + `### 7. User Identification`, + `Use posthog.identify('distinct_id') to link events to a user after login.`, + `Call posthog.reset() on logout to unlink future events from the current user.`, + ``, + `IMPORTANT: These best practices are MANDATORY. The implementation will fail review if they are not followed.`, + ]; + + if (context.hasBundler) { + lines.unshift(`Bundler: ${context.hasBundler}`); + } + + return lines; + }, + }, + + ui: { + successMessage: 'PostHog integration complete', + estimatedDurationMinutes: 5, + getOutroChanges: (context) => { + const packageManagerName = + context.packageManagerName ?? 'package manager'; + return [ + `Analyzed your JavaScript project structure`, + `Installed the posthog-js package using ${packageManagerName}`, + `Created PostHog initialization code`, + `Configured autocapture, error tracking, and event capture`, + ]; + }, + getOutroNextSteps: () => [ + 'Ensure posthog.init() is called before any capture calls', + 'Autocapture tracks clicks, form submissions, and pageviews automatically', + 'Use posthog.capture() for custom events and posthog.identify() for users', + 'NEVER send PII in event properties (no emails, names, or user content)', + 'Visit your PostHog dashboard to see incoming events', + ], + }, +}; diff --git a/src/javascript/utils.ts b/src/javascript/utils.ts new file mode 100644 index 0000000..013959d --- /dev/null +++ b/src/javascript/utils.ts @@ -0,0 +1,72 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { detectAllPackageManagers } from '../utils/package-manager'; +import type { WizardOptions } from '../utils/types'; + +export type JavaScriptContext = { + packageManagerName?: string; + hasTypeScript?: boolean; + hasBundler?: string; +}; + +/** + * Packages that indicate a specific framework integration exists. + * If any of these are in package.json, we should NOT match as generic JavaScript. + * + * When adding a new JS framework integration to the wizard, + * add its detection package here too. + */ +export const FRAMEWORK_PACKAGES = [ + 'next', + 'nuxt', + 'vue', + 'react-router', + '@tanstack/react-start', + '@tanstack/react-router', + 'react-native', + '@angular/core', + 'astro', + '@sveltejs/kit', +] as const; + +/** + * Detect the JS package manager for the project by checking lockfiles. + * Reuses the existing package manager detection infrastructure. + */ +export function detectJsPackageManager( + options: Pick, +): string { + const detected = detectAllPackageManagers(options); + if (detected.length > 0) { + return detected[0].label; + } + return 'unknown'; +} + +/** + * Detect the bundler used in the project by checking package.json dependencies. + */ +export function detectBundler( + options: Pick, +): string | undefined { + try { + const content = fs.readFileSync( + path.join(options.installDir, 'package.json'), + 'utf-8', + ); + const pkg = JSON.parse(content); + const allDeps: Record = { + ...pkg.dependencies, + ...pkg.devDependencies, + }; + + if (allDeps['vite']) return 'vite'; + if (allDeps['webpack']) return 'webpack'; + if (allDeps['esbuild']) return 'esbuild'; + if (allDeps['parcel']) return 'parcel'; + if (allDeps['rollup']) return 'rollup'; + return undefined; + } catch { + return undefined; + } +} diff --git a/src/lib/constants.ts b/src/lib/constants.ts index fa53e5b..f2efa32 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -22,6 +22,7 @@ export enum Integration { // Language fallbacks python = 'python', + javascript = 'javascript', } export interface Args { debug: boolean; diff --git a/src/lib/registry.ts b/src/lib/registry.ts index 3787f29..e28a4ac 100644 --- a/src/lib/registry.ts +++ b/src/lib/registry.ts @@ -17,6 +17,7 @@ import { SVELTEKIT_AGENT_CONFIG } from '../svelte/svelte-wizard-agent'; import { SWIFT_AGENT_CONFIG } from '../swift/swift-wizard-agent'; import { ANDROID_AGENT_CONFIG } from '../android/android-wizard-agent'; import { PYTHON_AGENT_CONFIG } from '../python/python-wizard-agent'; +import { JAVASCRIPT_AGENT_CONFIG } from '../javascript/javascript-wizard-agent'; export const FRAMEWORK_REGISTRY: Record = { [Integration.nextjs]: NEXTJS_AGENT_CONFIG, @@ -36,4 +37,5 @@ export const FRAMEWORK_REGISTRY: Record = { [Integration.swift]: SWIFT_AGENT_CONFIG, [Integration.android]: ANDROID_AGENT_CONFIG, [Integration.python]: PYTHON_AGENT_CONFIG, + [Integration.javascript]: JAVASCRIPT_AGENT_CONFIG, }; From 52e6e0cb58846d7c608305b81f17adeb0fff46ad Mon Sep 17 00:00:00 2001 From: sarahxsanders Date: Thu, 12 Feb 2026 10:55:06 -0500 Subject: [PATCH 2/4] lint --- src/javascript/javascript-wizard-agent.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/javascript/javascript-wizard-agent.ts b/src/javascript/javascript-wizard-agent.ts index 90f8a94..31e76e2 100644 --- a/src/javascript/javascript-wizard-agent.ts +++ b/src/javascript/javascript-wizard-agent.ts @@ -19,13 +19,13 @@ export const JAVASCRIPT_AGENT_CONFIG: FrameworkConfig = { integration: Integration.javascript, beta: true, docsUrl: 'https://posthog.com/docs/libraries/js', - gatherContext: async (options: WizardOptions) => { + gatherContext: (options: WizardOptions) => { const packageManagerName = detectJsPackageManager(options); const hasTypeScript = fs.existsSync( path.join(options.installDir, 'tsconfig.json'), ); const hasBundler = detectBundler(options); - return { packageManagerName, hasTypeScript, hasBundler }; + return Promise.resolve({ packageManagerName, hasTypeScript, hasBundler }); }, }, From 8094d225feb7a98556fc27a8f6f9ed3e36fcd0d1 Mon Sep 17 00:00:00 2001 From: sarahxsanders Date: Thu, 12 Feb 2026 11:40:24 -0500 Subject: [PATCH 3/4] fix identity/capture --- src/javascript/javascript-wizard-agent.ts | 39 +++++++++++------------ 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/javascript/javascript-wizard-agent.ts b/src/javascript/javascript-wizard-agent.ts index 31e76e2..3a35808 100644 --- a/src/javascript/javascript-wizard-agent.ts +++ b/src/javascript/javascript-wizard-agent.ts @@ -128,37 +128,36 @@ export const JAVASCRIPT_AGENT_CONFIG: FrameworkConfig = { `### 4. Error Tracking`, `Use posthog.captureException(error) for error tracking.`, ``, - `### 5. NEVER Send PII (Personally Identifiable Information)`, - `DO NOT include in event properties:`, - `- Email addresses`, - `- Full names`, - `- Phone numbers`, - `- Physical addresses`, - `- IP addresses`, + `### 5. NEVER Send PII in Event Properties`, + `DO NOT include in posthog.capture() event properties:`, + `- Email addresses, full names, phone numbers, physical addresses, IP addresses`, `- Any user-generated content (messages, comments, form submissions)`, ``, `SAFE event properties:`, - `posthog.capture('form_submitted', {`, - ` form_type: 'contact',`, - ` field_count: 5,`, - ` has_attachment: true`, - `})`, + `posthog.capture('form_submitted', { form_type: 'contact', field_count: 5 })`, ``, `UNSAFE (DO NOT DO THIS):`, - `posthog.capture('form_submitted', {`, - ` email: userEmail, // NEVER send actual email`, - ` message: messageContent, // NEVER send user content`, + `posthog.capture('form_submitted', { email: userEmail, message: content })`, + ``, + `### 6. User Identification (identify with person properties)`, + `Call posthog.identify() on login AND on page refresh if the user is already logged in.`, + `ALWAYS pass person properties as the second argument — this is where email/name/role BELONG:`, + ``, + `posthog.identify(user.id, {`, + ` email: user.email,`, + ` name: user.name,`, + ` role: user.role`, `})`, ``, - `### 6. SPA Pageview Tracking`, + `Person properties via identify() are NOT the same as event properties via capture().`, + `PII in identify() person properties is expected and recommended by PostHog docs.`, + `Call posthog.reset() on logout to unlink future events from the current user.`, + ``, + `### 7. SPA Pageview Tracking`, `For single-page applications without a framework router, you may need to manually capture pageviews:`, `posthog.capture('$pageview')`, `Or use capture_pageview: 'history_change' in the init options for History API based routing.`, ``, - `### 7. User Identification`, - `Use posthog.identify('distinct_id') to link events to a user after login.`, - `Call posthog.reset() on logout to unlink future events from the current user.`, - ``, `IMPORTANT: These best practices are MANDATORY. The implementation will fail review if they are not followed.`, ]; From 1b73653565580168a4394e5e6d52bd71cc2f3ced Mon Sep 17 00:00:00 2001 From: sarahxsanders Date: Thu, 12 Feb 2026 16:06:06 -0500 Subject: [PATCH 4/4] rename to js web --- .../javascript-web-wizard-agent.ts} | 14 +++++++------- src/{javascript => javascript-web}/utils.ts | 0 src/lib/constants.ts | 2 +- src/lib/registry.ts | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) rename src/{javascript/javascript-wizard-agent.ts => javascript-web/javascript-web-wizard-agent.ts} (93%) rename src/{javascript => javascript-web}/utils.ts (100%) diff --git a/src/javascript/javascript-wizard-agent.ts b/src/javascript-web/javascript-web-wizard-agent.ts similarity index 93% rename from src/javascript/javascript-wizard-agent.ts rename to src/javascript-web/javascript-web-wizard-agent.ts index 3a35808..9153a51 100644 --- a/src/javascript/javascript-wizard-agent.ts +++ b/src/javascript-web/javascript-web-wizard-agent.ts @@ -1,4 +1,4 @@ -/* Generic JavaScript language wizard using posthog-agent with PostHog MCP */ +/* Generic JavaScript Web (client-side) wizard using posthog-agent with PostHog MCP */ import type { WizardOptions } from '../utils/types'; import type { FrameworkConfig } from '../lib/framework-config'; import { Integration } from '../lib/constants'; @@ -13,10 +13,10 @@ import { type JavaScriptContext, } from './utils'; -export const JAVASCRIPT_AGENT_CONFIG: FrameworkConfig = { +export const JAVASCRIPT_WEB_AGENT_CONFIG: FrameworkConfig = { metadata: { - name: 'JavaScript', - integration: Integration.javascript, + name: 'JavaScript (Web)', + integration: Integration.javascript_web, beta: true, docsUrl: 'https://posthog.com/docs/libraries/js', gatherContext: (options: WizardOptions) => { @@ -31,7 +31,7 @@ export const JAVASCRIPT_AGENT_CONFIG: FrameworkConfig = { detection: { packageName: 'posthog-js', - packageDisplayName: 'JavaScript', + packageDisplayName: 'JavaScript (Web)', usesPackageJson: false, getVersion: () => undefined, detect: async (options) => { @@ -109,8 +109,8 @@ export const JAVASCRIPT_AGENT_CONFIG: FrameworkConfig = { `## CRITICAL: posthog-js Best Practices`, ``, `### 1. Use posthog-js (Browser SDK)`, - `This is a browser-side JavaScript project. Use the posthog-js package, NOT posthog-node.`, - `posthog-js is designed for client-side use and includes autocapture, session recording, and feature flags.`, + `This is a client-side web JavaScript project. Use the posthog-js package, NOT posthog-node.`, + `posthog-js is designed for browser use and includes autocapture, session recording, and feature flags.`, ``, `### 2. Initialization (REQUIRED)`, `posthog.init() MUST be called before any other PostHog methods:`, diff --git a/src/javascript/utils.ts b/src/javascript-web/utils.ts similarity index 100% rename from src/javascript/utils.ts rename to src/javascript-web/utils.ts diff --git a/src/lib/constants.ts b/src/lib/constants.ts index f2efa32..96ec9f9 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -22,7 +22,7 @@ export enum Integration { // Language fallbacks python = 'python', - javascript = 'javascript', + javascript_web = 'javascript_web', } export interface Args { debug: boolean; diff --git a/src/lib/registry.ts b/src/lib/registry.ts index e28a4ac..da910ba 100644 --- a/src/lib/registry.ts +++ b/src/lib/registry.ts @@ -17,7 +17,7 @@ import { SVELTEKIT_AGENT_CONFIG } from '../svelte/svelte-wizard-agent'; import { SWIFT_AGENT_CONFIG } from '../swift/swift-wizard-agent'; import { ANDROID_AGENT_CONFIG } from '../android/android-wizard-agent'; import { PYTHON_AGENT_CONFIG } from '../python/python-wizard-agent'; -import { JAVASCRIPT_AGENT_CONFIG } from '../javascript/javascript-wizard-agent'; +import { JAVASCRIPT_WEB_AGENT_CONFIG } from '../javascript-web/javascript-web-wizard-agent'; export const FRAMEWORK_REGISTRY: Record = { [Integration.nextjs]: NEXTJS_AGENT_CONFIG, @@ -37,5 +37,5 @@ export const FRAMEWORK_REGISTRY: Record = { [Integration.swift]: SWIFT_AGENT_CONFIG, [Integration.android]: ANDROID_AGENT_CONFIG, [Integration.python]: PYTHON_AGENT_CONFIG, - [Integration.javascript]: JAVASCRIPT_AGENT_CONFIG, + [Integration.javascript_web]: JAVASCRIPT_WEB_AGENT_CONFIG, };