diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index b5876935..717202b8 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,32 @@ ## [Unreleased] +## Telemetry + +Interactive mode now sends a lightweight anonymous install/update telemetry ping to `https://pi.dev/install?version=x.y.z` after it writes `lastChangelogVersion` in `settings.json`. + +Why this exists: +- Pi needs a reliable per-version usage signal to understand whether releases are being adopted and to help justify funding continued development. +- npm download counts are not a reliable proxy for actual Pi usage. + +How it works: +- It only runs in interactive mode. +- It does not run in RPC mode, print mode, JSON mode, or SDK mode. +- On a fresh interactive install, Pi writes `lastChangelogVersion`, then sends the ping. +- On later interactive startups, if the local changelog contains entries newer than the previously stored `lastChangelogVersion`, Pi writes the new `lastChangelogVersion`, then sends the ping. +- The request is fire-and-forget. Startup does not wait for it, and any errors are ignored. + +What data is collected: +- Only the Pi version in the request path, for example `https://pi.dev/install?version=0.66.1`. +- The server stores only aggregate per-version counters such as `{ "0.66.1": 3 }`. +- It does not store IP addresses, client identifiers, prompts, paths, models, auth state, or any other per-user data. It literally only increments a counter for that version. + +How to disable it: +- `/settings` → disable `Install telemetry` +- `settings.json` → set `enableInstallTelemetry` to `false` +- `PI_OFFLINE=1` +- `PI_TELEMETRY=0` + ### New Features - Full `openRouterRouting` support in `models.json`, including fallbacks, parameter requirements, data collection, ZDR, ignore lists, quantizations, provider sorting, max price, and preferred throughput and latency constraints. See [docs/models.md](docs/models.md). diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 24957c9b..aec80cf5 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -271,6 +271,8 @@ Use `/settings` to modify common options, or edit JSON files directly: See [docs/settings.md](docs/settings.md) for all options. +To opt out of anonymous install/update telemetry tied to changelog detection, set `enableInstallTelemetry` to `false` in `settings.json`, or set `PI_TELEMETRY=0`. + --- ## Context Files @@ -594,6 +596,7 @@ pi --thinking high "Solve this complex problem" | `PI_CODING_AGENT_DIR` | Override config directory (default: `~/.pi/agent`) | | `PI_PACKAGE_DIR` | Override package directory (useful for Nix/Guix where store paths tokenize poorly) | | `PI_SKIP_VERSION_CHECK` | Skip version check at startup | +| `PI_TELEMETRY` | Override install telemetry. Use `1`/`true`/`yes` to enable or `0`/`false`/`no` to disable | | `PI_CACHE_RETENTION` | Set to `long` for extended prompt cache (Anthropic: 1h, OpenAI: 24h) | | `VISUAL`, `EDITOR` | External editor for Ctrl+G | diff --git a/packages/coding-agent/docs/settings.md b/packages/coding-agent/docs/settings.md index 1405d61c..63d4aa88 100644 --- a/packages/coding-agent/docs/settings.md +++ b/packages/coding-agent/docs/settings.md @@ -41,6 +41,7 @@ Edit directly or use `/settings` for common options. | `theme` | string | `"dark"` | Theme name (`"dark"`, `"light"`, or custom) | | `quietStartup` | boolean | `false` | Hide startup header | | `collapseChangelog` | boolean | `false` | Show condensed changelog after updates | +| `enableInstallTelemetry` | boolean | `true` | Send an anonymous version/update ping after changelog-detected updates | | `doubleEscapeAction` | string | `"tree"` | Action for double-escape: `"tree"`, `"fork"`, or `"none"` | | `treeFilterMode` | string | `"default"` | Default filter for `/tree`: `"default"`, `"no-tools"`, `"user-only"`, `"labeled-only"`, `"all"` | | `editorPaddingX` | number | `0` | Horizontal padding for input editor (0-3) | diff --git a/packages/coding-agent/src/cli/args.ts b/packages/coding-agent/src/cli/args.ts index 186a4371..b0c72b84 100644 --- a/packages/coding-agent/src/cli/args.ts +++ b/packages/coding-agent/src/cli/args.ts @@ -323,6 +323,7 @@ ${chalk.bold("Environment Variables:")} ${ENV_AGENT_DIR.padEnd(32)} - Session storage directory (default: ~/${CONFIG_DIR_NAME}/agent) PI_PACKAGE_DIR - Override package directory (for Nix/Guix store paths) PI_OFFLINE - Disable startup network operations when set to 1/true/yes + PI_TELEMETRY - Override install telemetry when set to 1/true/yes or 0/false/no PI_SHARE_VIEWER_URL - Base URL for /share command (default: https://pi.dev/session/) PI_AI_ANTIGRAVITY_VERSION - Override Antigravity User-Agent version (e.g., 1.23.0) diff --git a/packages/coding-agent/src/core/settings-manager.ts b/packages/coding-agent/src/core/settings-manager.ts index fdd9a282..d1f3493f 100644 --- a/packages/coding-agent/src/core/settings-manager.ts +++ b/packages/coding-agent/src/core/settings-manager.ts @@ -78,6 +78,7 @@ export interface Settings { shellCommandPrefix?: string; // Prefix prepended to every bash command (e.g., "shopt -s expand_aliases" for alias support) npmCommand?: string[]; // Command used for npm package lookup/install operations, argv-style (e.g., ["mise", "exec", "node@20", "--", "npm"]) collapseChangelog?: boolean; // Show condensed changelog after update (use /changelog for full) + enableInstallTelemetry?: boolean; // default: true - anonymous version/update ping after changelog-detected updates packages?: PackageSource[]; // Array of npm/git package sources (string or object with filtering) extensions?: string[]; // Array of local extension file paths or directories skills?: string[]; // Array of local skill file paths or directories @@ -736,6 +737,16 @@ export class SettingsManager { this.save(); } + getEnableInstallTelemetry(): boolean { + return this.settings.enableInstallTelemetry ?? true; + } + + setEnableInstallTelemetry(enabled: boolean): void { + this.globalSettings.enableInstallTelemetry = enabled; + this.markModified("enableInstallTelemetry"); + this.save(); + } + getPackages(): PackageSource[] { return [...(this.settings.packages ?? [])]; } diff --git a/packages/coding-agent/src/modes/interactive/components/settings-selector.ts b/packages/coding-agent/src/modes/interactive/components/settings-selector.ts index bbe3e7e5..cf3473e2 100644 --- a/packages/coding-agent/src/modes/interactive/components/settings-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/settings-selector.ts @@ -43,6 +43,7 @@ export interface SettingsConfig { availableThemes: string[]; hideThinkingBlock: boolean; collapseChangelog: boolean; + enableInstallTelemetry: boolean; doubleEscapeAction: "fork" | "tree" | "none"; treeFilterMode: "default" | "no-tools" | "user-only" | "labeled-only" | "all"; showHardwareCursor: boolean; @@ -66,6 +67,7 @@ export interface SettingsCallbacks { onThemePreview?: (theme: string) => void; onHideThinkingBlockChange: (hidden: boolean) => void; onCollapseChangelogChange: (collapsed: boolean) => void; + onEnableInstallTelemetryChange: (enabled: boolean) => void; onDoubleEscapeActionChange: (action: "fork" | "tree" | "none") => void; onTreeFilterModeChange: (mode: "default" | "no-tools" | "user-only" | "labeled-only" | "all") => void; onShowHardwareCursorChange: (enabled: boolean) => void; @@ -206,6 +208,13 @@ export class SettingsSelectorComponent extends Container { currentValue: config.quietStartup ? "true" : "false", values: ["true", "false"], }, + { + id: "install-telemetry", + label: "Install telemetry", + description: "Send an anonymous version/update ping after changelog-detected updates", + currentValue: config.enableInstallTelemetry ? "true" : "false", + values: ["true", "false"], + }, { id: "double-escape-action", label: "Double-escape action", @@ -396,6 +405,9 @@ export class SettingsSelectorComponent extends Container { case "quiet-startup": callbacks.onQuietStartupChange(newValue === "true"); break; + case "install-telemetry": + callbacks.onEnableInstallTelemetryChange(newValue === "true"); + break; case "double-escape-action": callbacks.onDoubleEscapeActionChange(newValue as "fork" | "tree"); break; diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index ef90fe80..a1aaadaa 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -136,6 +136,11 @@ function isAnthropicSubscriptionAuthKey(apiKey: string | undefined): boolean { return typeof apiKey === "string" && apiKey.startsWith("sk-ant-oat"); } +function isTruthyEnvFlag(value: string | undefined): boolean { + if (!value) return false; + return value === "1" || value.toLowerCase() === "true" || value.toLowerCase() === "yes"; +} + /** * Options for InteractiveMode initialization. */ @@ -767,20 +772,41 @@ export class InteractiveMode { const entries = parseChangelog(changelogPath); if (!lastVersion) { - // Fresh install - just record the version, don't show changelog + // Fresh install - record the version, send telemetry, don't show changelog this.settingsManager.setLastChangelogVersion(VERSION); + this.reportInstallTelemetry(VERSION); return undefined; - } else { - const newEntries = getNewEntries(entries, lastVersion); - if (newEntries.length > 0) { - this.settingsManager.setLastChangelogVersion(VERSION); - return newEntries.map((e) => e.content).join("\n\n"); - } + } + + const newEntries = getNewEntries(entries, lastVersion); + if (newEntries.length > 0) { + this.settingsManager.setLastChangelogVersion(VERSION); + this.reportInstallTelemetry(VERSION); + return newEntries.map((e) => e.content).join("\n\n"); } return undefined; } + private reportInstallTelemetry(version: string): void { + if (process.env.PI_OFFLINE) { + return; + } + + const telemetryEnv = process.env.PI_TELEMETRY; + const telemetryEnabled = + telemetryEnv !== undefined ? isTruthyEnvFlag(telemetryEnv) : this.settingsManager.getEnableInstallTelemetry(); + if (!telemetryEnabled) { + return; + } + + void fetch(`https://pi.dev/install?version=${encodeURIComponent(version)}`, { + signal: AbortSignal.timeout(5000), + }) + .then(() => undefined) + .catch(() => undefined); + } + private getMarkdownThemeWithSettings(): MarkdownTheme { return { ...getMarkdownTheme(), @@ -3352,6 +3378,7 @@ export class InteractiveMode { availableThemes: getAvailableThemes(), hideThinkingBlock: this.hideThinkingBlock, collapseChangelog: this.settingsManager.getCollapseChangelog(), + enableInstallTelemetry: this.settingsManager.getEnableInstallTelemetry(), doubleEscapeAction: this.settingsManager.getDoubleEscapeAction(), treeFilterMode: this.settingsManager.getTreeFilterMode(), showHardwareCursor: this.settingsManager.getShowHardwareCursor(), @@ -3427,6 +3454,9 @@ export class InteractiveMode { onCollapseChangelogChange: (collapsed) => { this.settingsManager.setCollapseChangelog(collapsed); }, + onEnableInstallTelemetryChange: (enabled) => { + this.settingsManager.setEnableInstallTelemetry(enabled); + }, onQuietStartupChange: (enabled) => { this.settingsManager.setQuietStartup(enabled); },