diff --git a/packages/omo-codex/plugin/components/telemetry/AGENTS.md b/packages/omo-codex/plugin/components/telemetry/AGENTS.md new file mode 100644 index 000000000..a3887f655 --- /dev/null +++ b/packages/omo-codex/plugin/components/telemetry/AGENTS.md @@ -0,0 +1,37 @@ +# Repository Conventions + +Conventions for human contributors and AI agents working on this component. + +## Style + +- Terse technical prose. No emojis in commits, issues, PR comments, or code. +- TypeScript strict mode. No `any`, no `@ts-ignore`, no `@ts-expect-error`, no enums. +- ESM modules with `.js` suffix in runtime import paths. +- Tabs for indentation. Double quotes for strings. +- Tests use vitest with `#given .. #when .. #then` descriptions or plain `// given / // when / // then` body comments. + +## Commands + +- `npm install` - install dependencies. +- `npm test` - run vitest once. +- `npm run typecheck` - strict TypeScript check. +- `npm run check` - type check, biome, and build. +- `npm run build` - emit `dist/`. +- `node dist/cli.js hook session-start < fixture.json` - smoke-test the SessionStart hook. + +## Constraints + +- No Bun APIs. Runtime is Node only because Codex launches plugin hooks with Node. +- The single hook handler is `runSessionStartHook`. Do not add new hook handlers without also wiring them in `hooks/hooks.json` and `plugin/hooks/hooks.json`. +- Telemetry MUST be silent on every failure path. The CLI MUST exit 0 with empty stdout even when PostHog construction, capture, or shutdown throws. +- Telemetry MUST be daily-deduplicated. Adding a new event type requires a new state file slot, not removal of the existing dedup. +- Hook output MUST stay empty (no `additionalContext`, no `systemMessage`). This component is observability-only and MUST NOT inject context into the Codex conversation. +- Constants in `src/product-identity.ts` MUST stay byte-equivalent with `packages/omo-codex/src/telemetry/product-identity.ts`. The cross-package equivalence test will fail otherwise. +- Do not couple this component back to omo internal source paths beyond what `cross-package-equivalence.test.ts` already asserts at the constants layer. + +## Don'ts + +- No `git add -A` or `git add .`. Stage only the files you changed. +- No `git commit --no-verify`. No force pushes. No history rewriting on shared branches. +- No new network calls. PostHog is the only allowed sink. +- No new env vars without README + privacy-policy update. diff --git a/packages/omo-codex/plugin/components/telemetry/README.md b/packages/omo-codex/plugin/components/telemetry/README.md new file mode 100644 index 000000000..c0f8655ef --- /dev/null +++ b/packages/omo-codex/plugin/components/telemetry/README.md @@ -0,0 +1,102 @@ +# codex-telemetry + +Codex plugin component that emits a single anonymous daily-active event (`omo_codex_daily_active`) to PostHog whenever a Codex session starts. + +The event is sent **at most once per UTC day per machine**. It uses a SHA256-hashed installation identifier derived from `omo-codex:${hostname}` and never sends the raw hostname. PostHog person profiles are explicitly disabled. + +## Hook Wiring + +The component registers a single `SessionStart` hook: + +```json +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook session-start", + "timeout": 5 + } + ] + } + ] + } +} +``` + +The aggregate `plugin/hooks/hooks.json` mounts this hook alongside `rules` and `ultrawork` so all three fire in parallel at the start of every Codex session. + +## What Is Captured + +A single PostHog `capture` call with: + +- `event: "omo_codex_daily_active"` +- `distinctId: sha256("omo-codex:" + hostname)` +- `properties`: + - `platform`, `product_name`, `package_name`, `package_version` + - `runtime` (`"node"`), `runtime_version` + - `source: "plugin"`, `reason: "session_start"` + - `$os`, `$os_version`, `os_arch`, `os_type` + - `cpu_count`, `cpu_model`, `total_memory_gb` + - `locale`, `timezone`, `shell`, `ci`, `terminal` + - `day_utc` (today's UTC date) + - `$process_person_profile: false` + +The component never sends prompt contents, file contents, API keys, raw hostnames, or any user-identifying data. + +## Opt-Out + +Set any of the following environment variables before launching Codex: + +```bash +# Codex-only opt-out +export OMO_CODEX_DISABLE_POSTHOG=1 +export OMO_CODEX_SEND_ANONYMOUS_TELEMETRY=0 + +# Global opt-out (covers both omo and omo-codex) +export OMO_DISABLE_POSTHOG=1 +export OMO_SEND_ANONYMOUS_TELEMETRY=0 +``` + +When any of these is set the component creates a no-op PostHog client and exits without any network call. + +## Daily Deduplication + +The component writes a small JSON state file at: + +``` +$XDG_DATA_HOME/omo-codex/posthog-activity.json +# or, when XDG_DATA_HOME is unset: +~/.local/share/omo-codex/posthog-activity.json +``` + +containing `{ "lastActiveDayUTC": "YYYY-MM-DD" }`. If the stored day matches today (UTC), the hook returns without sending anything. The file is written atomically via `rename(2)`. + +## Failure Behavior + +Every telemetry path is wrapped in `try`/`catch`. The hook always exits 0 with no stdout output, even when PostHog construction, capture, or shutdown fails. Codex session startup is never blocked or slowed by telemetry failures. + +## Endpoint Overrides + +| Variable | Default | +|----------|---------| +| `POSTHOG_HOST` | `https://us.i.posthog.com` | +| `POSTHOG_API_KEY` | shared `omo-codex` project key | + +## Development + +```bash +npm install +npm test # vitest (in-process + subprocess CLI smoke) +npm run typecheck +npm run build # tsc -> dist/ +npm run check # typecheck + biome + build +``` + +The component shares its product identity constants with the `@oh-my-opencode/omo-codex` CLI installer. Drift between the two implementations is guarded by `packages/omo-codex/src/telemetry/cross-package-equivalence.test.ts`. + +## Privacy + +See [the omo Privacy Policy](https://github.com/code-yeongyu/oh-my-openagent/blob/dev/docs/legal/privacy-policy.md) for the full disclosure. diff --git a/packages/omo-codex/plugin/components/telemetry/biome.json b/packages/omo-codex/plugin/components/telemetry/biome.json new file mode 100644 index 000000000..5aa1a0dcc --- /dev/null +++ b/packages/omo-codex/plugin/components/telemetry/biome.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.15/schema.json", + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "style": { + "noDefaultExport": "error", + "noEnum": "error", + "noNonNullAssertion": "error", + "useImportType": "error", + "useConst": "error", + "useNodejsImportProtocol": "off" + }, + "complexity": { + "useLiteralKeys": "off" + }, + "suspicious": { + "noExplicitAny": "error", + "noTsIgnore": "error", + "noControlCharactersInRegex": "off", + "noEmptyInterface": "off" + } + } + }, + "formatter": { + "enabled": true, + "formatWithErrors": false, + "indentStyle": "tab", + "indentWidth": 3, + "lineWidth": 120 + }, + "files": { + "includes": ["src/**/*.ts", "test/**/*.ts", "vitest.config.ts", "!**/node_modules/**/*", "!**/dist/**/*"] + }, + "overrides": [ + { + "includes": ["vitest.config.ts"], + "linter": { + "rules": { + "style": { + "noDefaultExport": "off" + } + } + } + } + ] +} diff --git a/packages/omo-codex/plugin/components/telemetry/package.json b/packages/omo-codex/plugin/components/telemetry/package.json new file mode 100644 index 000000000..27f9e7249 --- /dev/null +++ b/packages/omo-codex/plugin/components/telemetry/package.json @@ -0,0 +1,56 @@ +{ + "name": "@code-yeongyu/codex-telemetry", + "version": "0.1.0", + "description": "Codex plugin component that emits omo-codex anonymous daily-active telemetry on SessionStart.", + "type": "module", + "packageManager": "npm@11.12.1", + "license": "MIT", + "homepage": "https://github.com/code-yeongyu/oh-my-openagent", + "repository": { + "type": "git", + "url": "git+https://github.com/code-yeongyu/oh-my-openagent.git" + }, + "bugs": { + "url": "https://github.com/code-yeongyu/oh-my-openagent/issues" + }, + "keywords": [ + "codex", + "codex-plugin", + "omo", + "telemetry", + "posthog", + "hooks", + "daily-active" + ], + "bin": { + "omo-telemetry": "./dist/cli.js" + }, + "files": [ + "dist", + "hooks", + "LICENSE", + "README.md", + "CHANGELOG.md" + ], + "scripts": { + "build": "tsc -p tsconfig.build.json", + "test": "vitest --run", + "test:watch": "vitest", + "typecheck": "tsc --noEmit", + "lint": "biome check .", + "lint:fix": "biome check --write .", + "check": "tsc --noEmit && biome check . && npm run build" + }, + "dependencies": { + "posthog-node": "^5.34.3" + }, + "devDependencies": { + "@biomejs/biome": "2.4.15", + "@types/node": "^25.7.0", + "typescript": "^6.0.3", + "vitest": "^4.1.5" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/packages/omo-codex/plugin/components/telemetry/tsconfig.build.json b/packages/omo-codex/plugin/components/telemetry/tsconfig.build.json new file mode 100644 index 000000000..5b5bbcafd --- /dev/null +++ b/packages/omo-codex/plugin/components/telemetry/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": false, + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "noEmit": false + }, + "include": ["src/**/*"], + "exclude": ["test/**/*"] +} diff --git a/packages/omo-codex/plugin/components/telemetry/tsconfig.json b/packages/omo-codex/plugin/components/telemetry/tsconfig.json new file mode 100644 index 000000000..342229c02 --- /dev/null +++ b/packages/omo-codex/plugin/components/telemetry/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "lib": ["ES2022"], + "strict": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true, + "noPropertyAccessFromIndexSignature": true, + "verbatimModuleSyntax": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "esModuleInterop": true, + "allowImportingTsExtensions": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "useDefineForClassFields": false, + "types": ["node"], + "noEmit": true + }, + "include": ["src/**/*", "test/**/*"] +} diff --git a/packages/omo-codex/plugin/components/telemetry/vitest.config.ts b/packages/omo-codex/plugin/components/telemetry/vitest.config.ts new file mode 100644 index 000000000..c4fddb41c --- /dev/null +++ b/packages/omo-codex/plugin/components/telemetry/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + pool: "threads", + }, +});