From 67449fc5ef1eacbbbd5826471596b36e6fd447d0 Mon Sep 17 00:00:00 2001 From: ykb Date: Thu, 24 Sep 2026 16:16:06 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E8=A1=A5=E9=BD=90=E5=AE=89=E8=A3=85?= =?UTF-8?q?=E5=85=A5=E5=8F=A3=E5=AE=BF=E4=B8=BB=E6=9D=A5=E6=BA=90=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E8=AF=86=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 2 +- README.md | 4 +-- scripts/install-cli.test.js | 27 +++++++++++++++-- scripts/install-wizard.js | 6 ++-- scripts/install-wizard.test.js | 53 ++++++++++++++++++++++++++++------ scripts/install.js | 2 +- scripts/telemetry.js | 16 ++++++++++ 7 files changed, 92 insertions(+), 18 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1fee781..034ece3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,4 +22,4 @@ - `--source` 由宿主 Agent 根据可信运行环境填写真实、稳定的平台标识;不得固定为 `codex` 或默认代填,来源未知时省略。示例中的平台名仅为示例,生成提交由 CLI 透传为服务端 `platform`;安装/更新的 `report_telemetry` 用新增 `host_platform` 承载宿主标识,保留已有 `source` 的安装入口语义和 `platform` 的操作系统语义。已有上报字段被消费时,不得改写其含义,应新增独立字段。 -- 来源采集应尽力而为:显式 `--source` 优先,其次宿主提供的 `PIPPIT_CLI_SOURCE`,最后使用已核实的运行标记;显式空值禁用兜底。仅上报稳定宿主名,不上传会话 ID,不用安装痕迹、API Key 或 prompt 猜来源,冲突时省略。新增运行标记须有实际环境或官方实现依据。 +- 来源采集应尽力而为:显式 `--source` 优先,其次宿主提供的 `PIPPIT_CLI_SOURCE`,最后使用已核实的运行标记;显式空值禁用兜底。仅上报稳定宿主名,不上传会话 ID,不用安装痕迹、API Key 或 prompt 猜来源,冲突时省略。新增运行标记须有实际环境或官方实现依据。npm postinstall、install 向导和原生 update 都必须覆盖同一来源优先级;验证须检查各入口实际发出的请求体,不能只验证通用解析函数或原生命令。 diff --git a/README.md b/README.md index 6d7414c..8f4ed98 100644 --- a/README.md +++ b/README.md @@ -369,9 +369,9 @@ npx @pippit-dev/cli install --source HOST pippit-tool-cli update --source HOST ``` -直接使用 `npm install -g @pippit-dev/cli` 时,可由宿主在该次命令的环境中设置 `PIPPIT_CLI_SOURCE`。install/update 的显式 `--source` 优先于此环境变量;首尾空白去除,显式空值清空来源。该值不持久化;生成命令运行环境中若也存在此变量,同样会读取它作为宿主来源,具体兜底规则见下文。 +直接使用 `npm install -g @pippit-dev/cli` 时,可由宿主在该次命令的环境中设置 `PIPPIT_CLI_SOURCE`。安装和更新均按显式 `--source`(命令支持时)、`PIPPIT_CLI_SOURCE`、已核实宿主运行标记的顺序识别来源;首尾空白去除,显式空值清空来源。该值不持久化;生成命令运行环境中若也存在此变量,同样会读取它作为宿主来源,具体兜底规则见下文。 -`report_telemetry` 新增可选请求字段 `host_platform` 上报宿主标识;原有 `source` 保留 `npm_install`、`npx_install`、`cli_update`,`event` 区分 install/update,`platform` 仍表示操作系统。当前仅完成 CLI 字段上报,服务端协议和指标接收仍需适配。帮助不安装也不上报,`PIPPIT_CLI_DISABLE_TELEMETRY=1` 仍可关闭上报。内部 `install-cli.js` 仅安装二进制,沿用不安装 Skill、不上报的原有行为。 +`report_telemetry` 新增可选请求字段 `host_platform` 上报宿主标识;原有 `source` 保留 `npm_install`、`npx_install`、`cli_update`,`event` 区分 install/update,`platform` 仍表示操作系统。服务端通过独立的 `host_platform` 指标标签统计宿主来源。帮助不安装也不上报,`PIPPIT_CLI_DISABLE_TELEMETRY=1` 仍可关闭上报。内部 `install-cli.js` 仅安装二进制,沿用不安装 Skill、不上报的原有行为。 ### 宿主来源统计 diff --git a/scripts/install-cli.test.js b/scripts/install-cli.test.js index be66d21..d58ff04 100644 --- a/scripts/install-cli.test.js +++ b/scripts/install-cli.test.js @@ -21,7 +21,7 @@ function load(file, { modules = {}, process: overrides = {}, dirname, main = fal const req = (name) => Object.prototype.hasOwnProperty.call(modules, name) ? modules[name] : require(name); req.main = main ? module : null; vm.runInNewContext(fs.readFileSync(file, "utf8"), { - require: req, module, process: proc, Buffer, + require: req, module, process: proc, Buffer, URL, __dirname: dirname || path.dirname(file), console: { log: (s) => output.push(s), warn: (s) => errors.push(s), error: (s) => errors.push(s) }, }, { filename: file }); @@ -39,6 +39,16 @@ function checkInstaller() { const packageDir = path.join(root, "package"); fs.mkdirSync(packageDir); const effects = []; + const payloads = []; + const environment = { CODEX_THREAD_ID: 'private-id' }; + const telemetry = load(path.join(repository, "scripts/telemetry.js"), { + process: { env: environment }, + modules: { + "../package.json": { version: "9.9.9" }, + http: { request: () => ({ on() {}, end: body => payloads.push(JSON.parse(body)) }) }, + https: { request: () => ({ on() {}, end: body => payloads.push(JSON.parse(body)) }) }, + }, + }); const archive = Buffer.from("verified archive fixture"); const mockPlatform = { isWindows: process.platform === "win32", @@ -58,6 +68,7 @@ function checkInstaller() { }; const installer = load(path.join(repository, "scripts/install.js"), { dirname: path.join(packageDir, "scripts"), + process: { env: environment }, modules: { "../package.json": { version: "9.9.9" }, "./platform": mockPlatform, @@ -65,7 +76,10 @@ function checkInstaller() { installSkillsFromRoot: () => effects.push("install-skills"), cleanupLegacyGlobalSkills: () => effects.push("cleanup-skills"), }, - "./telemetry": { reportBundledSkillTelemetry: () => effects.push("telemetry") }, + "./telemetry": { reportBundledSkillTelemetry: (...args) => { + effects.push("telemetry"); + telemetry.api.reportBundledSkillTelemetry(...args); + } }, }, }); const hash = crypto.createHash("sha256").update(archive).digest("hex"); @@ -85,9 +99,18 @@ function checkInstaller() { delete installer.proc.env.PIPPIT_CLI_SKIP_SKILLS; installer.api.install(); assert.deepStrictEqual(effects.splice(0), ["install-skills", "telemetry"]); + assert.strictEqual(payloads.length, 2); + for (const payload of payloads.splice(0)) { + assert.strictEqual(payload.host_platform, "codex"); + assert.strictEqual(payload.source, "npm_install"); + assert.strictEqual(payload.event, "install"); + assert.strictEqual(payload.platform, process.platform); + assert(!JSON.stringify(payload).includes("private-id")); + } installer.proc.env.PIPPIT_CLI_SKIP_SKILLS = "1"; installer.api.install(); assert.deepStrictEqual(effects.splice(0), ["cleanup-skills"]); + assert.strictEqual(payloads.length, 0); fs.writeFileSync(checksumFile, `${"0".repeat(64)} ${installer.api.archiveName}\n`); const failed = load(path.join(repository, "scripts/install-cli.js"), { diff --git a/scripts/install-wizard.js b/scripts/install-wizard.js index bd5f5ad..0b4f817 100755 --- a/scripts/install-wizard.js +++ b/scripts/install-wizard.js @@ -21,7 +21,7 @@ PIPPIT_CLI_INSTALL_PACKAGE can override the package/version to install. Options: --source HOST Real host identifier reported as host_platform; unknown hosts omit it - Defaults to PIPPIT_CLI_SOURCE when unset; an explicit empty value omits host_platform + Defaults to PIPPIT_CLI_SOURCE, then known host runtime markers; an explicit empty value omits host_platform -h, --help Show this help without installing, updating, or sending telemetry `; @@ -67,7 +67,7 @@ function main(args = process.argv.slice(2)) { console.log(INSTALL_HELP); return; } - let source = process.env.PIPPIT_CLI_SOURCE || ''; + let source; for (let i = 0; i < args.length; i++) { if (args[i] === '--source' && i + 1 < args.length && !args[i + 1].startsWith('--')) { source = args[++i]; @@ -79,7 +79,7 @@ function main(args = process.argv.slice(2)) { return; } } - source = source.trim(); + if (source !== undefined) source = source.trim(); const pkg = installPackage(); const installed = getGloballyInstalledVersion(); if (installed) { diff --git a/scripts/install-wizard.test.js b/scripts/install-wizard.test.js index ec01598..5281edd 100644 --- a/scripts/install-wizard.test.js +++ b/scripts/install-wizard.test.js @@ -15,9 +15,9 @@ assert.strictEqual(installPackage(), `${DEFAULT_PKG}@0.0.26`); // Exercise install attribution without npm, Skill writes, or network requests. const fs = require('fs'); const vm = require('vm'); -function installFixture(args, source, retry = false) { - const reports = [], installs = []; - const proc = { argv: [], env: source === undefined ? {} : { PIPPIT_CLI_SOURCE: source } }; +function installFixture(args, source, retry = false, environment = {}) { + const reports = [], installs = [], payloads = []; + const proc = { argv: [], env: { ...environment, ...(source === undefined ? {} : { PIPPIT_CLI_SOURCE: source }) } }; const module = { exports: {} }; let skillCalls = 0; const modules = { @@ -30,14 +30,17 @@ function installFixture(args, source, retry = false) { './skills': { DEFAULT_PKG, installGlobalPackageSkills: () => { if (retry && skillCalls++ === 0) throw new Error('missing skills'); } }, - './telemetry': { reportBundledSkillTelemetry: (...args) => reports.push(args) }, + './telemetry': { reportBundledSkillTelemetry: (event, source, host) => { + loadTelemetry(proc.env, payloads).reportBundledSkillTelemetry(event, source, host); + reports.push([event, source, payloads[0].host_platform || '']); + } }, }; vm.runInNewContext(fs.readFileSync(require.resolve('./install-wizard'), 'utf8'), { module, process: proc, console: { log() {}, error() {} }, require: name => modules[name] || require(name), }); module.exports.main(args); - return { reports, installs, proc }; + return { reports, installs, proc, payloads }; } for (const [args, env, want] of [ [[], undefined, ''], [[], ' workbuddy ', 'workbuddy'], @@ -57,16 +60,20 @@ for (const args of [['--help'], ['--source'], ['--unknown']]) { } // Verify the HTTP payload, including omission and telemetry opt-out. -function telemetryFixture(source, disabled) { - const payloads = []; +function loadTelemetry(env, payloads) { const request = () => ({ on() {}, end: body => payloads.push(JSON.parse(body)) }); const module = { exports: {} }; vm.runInNewContext(fs.readFileSync(require.resolve('./telemetry'), 'utf8'), { module, URL, Buffer, console, - process: { env: { PIPPIT_CLI_DISABLE_TELEMETRY: disabled } }, + process: { env }, require: name => ['http', 'https'].includes(name) ? { request } : require(name), }); - module.exports.reportBundledSkillTelemetry('install', 'npm_install', source); + return module.exports; +} +function telemetryFixture(source, disabled) { + const payloads = []; + loadTelemetry({ PIPPIT_CLI_DISABLE_TELEMETRY: disabled }, payloads) + .reportBundledSkillTelemetry('install', 'npm_install', source); return payloads; } for (const source of [undefined, '', ' ', ' workbuddy ', 'another-host']) { @@ -81,3 +88,31 @@ for (const source of [undefined, '', ' ', ' workbuddy ', 'another-host']) { } assert.strictEqual(telemetryFixture('workbuddy', '1').length, 0); console.log('Install source forwarding, precedence, omission, retry and telemetry payload checks passed'); + +// Exercise the actual install entry through JSON serialization; no real installation/network. +for (const [args, env, want] of [ + [[], { CODEX_THREAD_ID: 'private-id' }, 'codex'], + [[], { CODEX_SESSION_ID: 'private-id', CODEX_THREAD_ID: 'another-id' }, 'codex'], + [[], { CLAUDECODE: '1' }, 'claude_code'], + [[], { CURSOR_AGENT: '1' }, 'cursor'], + [[], { GEMINI_CLI: 'true' }, 'gemini_cli'], + [[], { PIPPIT_CLI_SOURCE: ' workbuddy ', CODEX_THREAD_ID: 'private-id' }, 'workbuddy'], + [['--source= doubao_office '], { PIPPIT_CLI_SOURCE: 'workbuddy', CODEX_THREAD_ID: 'private-id' }, 'doubao_office'], + [['--source', ''], { PIPPIT_CLI_SOURCE: 'workbuddy', CODEX_THREAD_ID: 'private-id' }, undefined], + [['--source= '], { CODEX_THREAD_ID: 'private-id' }, undefined], + [[], { PIPPIT_CLI_SOURCE: ' ', CODEX_THREAD_ID: 'private-id' }, 'codex'], + [[], { CODEX_THREAD_ID: 'private-id', CLAUDECODE: '1' }, undefined], + [[], { CLAUDECODE: '0', CURSOR_AGENT: ' FALSE ', GEMINI_CLI: '' }, undefined], + [[], {}, undefined], +]) { + const { payloads } = installFixture(args, undefined, false, env); + assert.strictEqual(payloads.length, 2); + for (const payload of payloads) { + assert.strictEqual(payload.host_platform, want); + assert.strictEqual(payload.source, 'npx_install'); + assert.strictEqual(payload.event, 'install'); + assert.strictEqual(payload.platform, process.platform); + assert(!JSON.stringify(payload).includes('private-id')); + } +} +console.log('Install runtime attribution and serialized payload regression checks passed'); diff --git a/scripts/install.js b/scripts/install.js index 33519cd..b77bbbe 100644 --- a/scripts/install.js +++ b/scripts/install.js @@ -137,7 +137,7 @@ function install({ cliOnly = false } = {}) { if (!cliOnly) { if (process.env.PIPPIT_CLI_SKIP_SKILLS !== "1") { installSkillsFromRoot(ROOT); - reportBundledSkillTelemetry("install", "npm_install", process.env.PIPPIT_CLI_SOURCE); + reportBundledSkillTelemetry("install", "npm_install"); } else { cleanupLegacyGlobalSkills(); } diff --git a/scripts/telemetry.js b/scripts/telemetry.js index ee4950b..e5bc809 100644 --- a/scripts/telemetry.js +++ b/scripts/telemetry.js @@ -19,10 +19,26 @@ function telemetryBaseURL() { return DEFAULT_BASE_URL; } +// Keep the same attribution rules as native CLI common.DetectHostSource. +function resolveHostSource(explicit) { + if (explicit !== undefined) return explicit.trim(); + if ((process.env.PIPPIT_CLI_SOURCE || "").trim()) return process.env.PIPPIT_CLI_SOURCE.trim(); + const hosts = new Set(); + for (const [key, host] of [ + ["CODEX_THREAD_ID", "codex"], ["CODEX_SESSION_ID", "codex"], + ["CLAUDECODE", "claude_code"], ["CURSOR_AGENT", "cursor"], ["GEMINI_CLI", "gemini_cli"], + ]) { + const value = (process.env[key] || "").trim().toLowerCase(); + if (value && value !== "0" && value !== "false") hosts.add(host); + } + return hosts.size === 1 ? [...hosts][0] : ""; +} + function reportBundledSkillTelemetry(event, source, hostPlatform) { if (process.env.PIPPIT_CLI_DISABLE_TELEMETRY === "1") { return; } + hostPlatform = resolveHostSource(hostPlatform); for (const skillName of SKILL_NAMES) { reportSkillTelemetry({ event, From 073a35ee6b6c60689e5dc5c0a12f97fa4705be00 Mon Sep 17 00:00:00 2001 From: ykb Date: Thu, 24 Sep 2026 16:16:53 +0800 Subject: [PATCH 2/2] =?UTF-8?q?chore:=20=E5=8D=87=E7=BA=A7=20CLI=20?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E8=87=B3=201.0.38?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 247c0b7..4be156e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@pippit-dev/cli", - "version": "1.0.37", + "version": "1.0.38", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pippit-dev/cli", - "version": "1.0.37", + "version": "1.0.38", "hasInstallScript": true, "license": "MIT", "bin": { diff --git a/package.json b/package.json index 957f60e..2a5b232 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pippit-dev/cli", - "version": "1.0.37", + "version": "1.0.38", "description": "Pippit CLI", "bin": { "pippit-tool-cli": "scripts/run.js"