pruff-skills 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +9 -1
  2. package/dist/cli.mjs +458 -142
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -181,7 +181,15 @@ Scaffold one with `npx pruff-skills init my-skill`, then publish it by pushing t
181
181
 
182
182
  ## Telemetry
183
183
 
184
- **None.** pruff-skills ships with all usage telemetry removed — no events leave your machine.
184
+ **The CLI sends nothing.** pruff-skills ships with the upstream usage telemetry removed — running
185
+ `add`, `update`, `find` or any other command emits no events.
186
+
187
+ Separately, `pruff-skills analytics` can turn on **Claude Code's own** OpenTelemetry exporter and
188
+ point it at Pruff's analytics backend, so an organisation can see which skills and tools its token
189
+ spend goes to. It is opt-in and Pruff-internal: `analytics enable` is the only command that writes
190
+ anything (never `add`, `update` or `onboarding`), the endpoint credentials live in the private
191
+ catalog repo rather than in this package, and it exports no prompts, responses or file contents.
192
+ `analytics status` shows what is on; `analytics disable` removes it.
185
193
 
186
194
  ## Attribution
187
195
 
package/dist/cli.mjs CHANGED
@@ -434,7 +434,7 @@ const CLONE_TIMEOUT_MS = (() => {
434
434
  const parsed = Number.parseInt(raw, 10);
435
435
  return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_CLONE_TIMEOUT_MS;
436
436
  })();
437
- const execFileAsync$4 = promisify(execFile);
437
+ const execFileAsync$5 = promisify(execFile);
438
438
  var GitCloneError = class extends Error {
439
439
  url;
440
440
  isTimeout;
@@ -513,7 +513,7 @@ async function resetTempDir(dir) {
513
513
  async function tryGhClone(repo, tempDir, ref) {
514
514
  let cloneTarget = repo.slug;
515
515
  try {
516
- const { stdout, stderr } = await execFileAsync$4("gh", [
516
+ const { stdout, stderr } = await execFileAsync$5("gh", [
517
517
  "auth",
518
518
  "status",
519
519
  "-h",
@@ -530,7 +530,7 @@ async function tryGhClone(repo, tempDir, ref) {
530
530
  } catch {
531
531
  return false;
532
532
  }
533
- await execFileAsync$4("gh", [
533
+ await execFileAsync$5("gh", [
534
534
  "repo",
535
535
  "clone",
536
536
  cloneTarget,
@@ -926,16 +926,16 @@ function filterSkills(skills, inputNames) {
926
926
  return normalizedInputs.some((input) => input === name || input === displayName);
927
927
  });
928
928
  }
929
- const home = homedir();
930
- const configHome = xdgConfig ?? join(home, ".config");
931
- const codexHome = process.env.CODEX_HOME?.trim() || join(home, ".codex");
932
- const claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() || join(home, ".claude");
933
- const vibeHome = process.env.VIBE_HOME?.trim() || join(home, ".vibe");
934
- const hermesHome = process.env.HERMES_HOME?.trim() || join(home, ".hermes");
935
- const autohandHome = process.env.AUTOHAND_HOME?.trim() || join(home, ".autohand");
929
+ const home$1 = homedir();
930
+ const configHome = xdgConfig ?? join(home$1, ".config");
931
+ const codexHome = process.env.CODEX_HOME?.trim() || join(home$1, ".codex");
932
+ const claudeHome$1 = process.env.CLAUDE_CONFIG_DIR?.trim() || join(home$1, ".claude");
933
+ const vibeHome = process.env.VIBE_HOME?.trim() || join(home$1, ".vibe");
934
+ const hermesHome = process.env.HERMES_HOME?.trim() || join(home$1, ".hermes");
935
+ const autohandHome = process.env.AUTOHAND_HOME?.trim() || join(home$1, ".autohand");
936
936
  const zedAppDataHome = process.env.APPDATA?.trim();
937
937
  const zedFlatpakConfigHome = process.env.FLATPAK_XDG_CONFIG_HOME?.trim();
938
- function getOpenClawGlobalSkillsDir(homeDir = home, pathExists = existsSync) {
938
+ function getOpenClawGlobalSkillsDir(homeDir = home$1, pathExists = existsSync) {
939
939
  if (pathExists(join(homeDir, ".openclaw"))) return join(homeDir, ".openclaw/skills");
940
940
  if (pathExists(join(homeDir, ".clawdbot"))) return join(homeDir, ".clawdbot/skills");
941
941
  if (pathExists(join(homeDir, ".moltbot"))) return join(homeDir, ".moltbot/skills");
@@ -946,9 +946,9 @@ const agents = {
946
946
  name: "aider-desk",
947
947
  displayName: "AiderDesk",
948
948
  skillsDir: ".aider-desk/skills",
949
- globalSkillsDir: join(home, ".aider-desk/skills"),
949
+ globalSkillsDir: join(home$1, ".aider-desk/skills"),
950
950
  detectInstalled: async () => {
951
- return existsSync(join(home, ".aider-desk"));
951
+ return existsSync(join(home$1, ".aider-desk"));
952
952
  }
953
953
  },
954
954
  amp: {
@@ -964,27 +964,27 @@ const agents = {
964
964
  name: "antigravity",
965
965
  displayName: "Antigravity",
966
966
  skillsDir: ".agents/skills",
967
- globalSkillsDir: join(home, ".gemini/antigravity/skills"),
967
+ globalSkillsDir: join(home$1, ".gemini/antigravity/skills"),
968
968
  detectInstalled: async () => {
969
- return existsSync(join(home, ".gemini/antigravity"));
969
+ return existsSync(join(home$1, ".gemini/antigravity"));
970
970
  }
971
971
  },
972
972
  "antigravity-cli": {
973
973
  name: "antigravity-cli",
974
974
  displayName: "Antigravity CLI",
975
975
  skillsDir: ".agents/skills",
976
- globalSkillsDir: join(home, ".gemini/antigravity-cli/skills"),
976
+ globalSkillsDir: join(home$1, ".gemini/antigravity-cli/skills"),
977
977
  detectInstalled: async () => {
978
- return existsSync(join(home, ".gemini/antigravity-cli"));
978
+ return existsSync(join(home$1, ".gemini/antigravity-cli"));
979
979
  }
980
980
  },
981
981
  astrbot: {
982
982
  name: "astrbot",
983
983
  displayName: "AstrBot",
984
984
  skillsDir: "data/skills",
985
- globalSkillsDir: join(home, ".astrbot/data/skills"),
985
+ globalSkillsDir: join(home$1, ".astrbot/data/skills"),
986
986
  detectInstalled: async () => {
987
- return existsSync(join(process.cwd(), "data/skills")) || existsSync(join(home, ".astrbot"));
987
+ return existsSync(join(process.cwd(), "data/skills")) || existsSync(join(home$1, ".astrbot"));
988
988
  }
989
989
  },
990
990
  "autohand-code": {
@@ -1000,27 +1000,27 @@ const agents = {
1000
1000
  name: "augment",
1001
1001
  displayName: "Augment",
1002
1002
  skillsDir: ".augment/skills",
1003
- globalSkillsDir: join(home, ".augment/skills"),
1003
+ globalSkillsDir: join(home$1, ".augment/skills"),
1004
1004
  detectInstalled: async () => {
1005
- return existsSync(join(home, ".augment"));
1005
+ return existsSync(join(home$1, ".augment"));
1006
1006
  }
1007
1007
  },
1008
1008
  bob: {
1009
1009
  name: "bob",
1010
1010
  displayName: "IBM Bob",
1011
1011
  skillsDir: ".bob/skills",
1012
- globalSkillsDir: join(home, ".bob/skills"),
1012
+ globalSkillsDir: join(home$1, ".bob/skills"),
1013
1013
  detectInstalled: async () => {
1014
- return existsSync(join(home, ".bob"));
1014
+ return existsSync(join(home$1, ".bob"));
1015
1015
  }
1016
1016
  },
1017
1017
  "claude-code": {
1018
1018
  name: "claude-code",
1019
1019
  displayName: "Claude Code",
1020
1020
  skillsDir: ".claude/skills",
1021
- globalSkillsDir: join(claudeHome, "skills"),
1021
+ globalSkillsDir: join(claudeHome$1, "skills"),
1022
1022
  detectInstalled: async () => {
1023
- return existsSync(claudeHome);
1023
+ return existsSync(claudeHome$1);
1024
1024
  }
1025
1025
  },
1026
1026
  openclaw: {
@@ -1029,52 +1029,52 @@ const agents = {
1029
1029
  skillsDir: "skills",
1030
1030
  globalSkillsDir: getOpenClawGlobalSkillsDir(),
1031
1031
  detectInstalled: async () => {
1032
- return existsSync(join(home, ".openclaw")) || existsSync(join(home, ".clawdbot")) || existsSync(join(home, ".moltbot"));
1032
+ return existsSync(join(home$1, ".openclaw")) || existsSync(join(home$1, ".clawdbot")) || existsSync(join(home$1, ".moltbot"));
1033
1033
  }
1034
1034
  },
1035
1035
  cline: {
1036
1036
  name: "cline",
1037
1037
  displayName: "Cline",
1038
1038
  skillsDir: ".agents/skills",
1039
- globalSkillsDir: join(home, ".agents", "skills"),
1039
+ globalSkillsDir: join(home$1, ".agents", "skills"),
1040
1040
  detectInstalled: async () => {
1041
- return existsSync(join(home, ".cline"));
1041
+ return existsSync(join(home$1, ".cline"));
1042
1042
  }
1043
1043
  },
1044
1044
  "codearts-agent": {
1045
1045
  name: "codearts-agent",
1046
1046
  displayName: "CodeArts Agent",
1047
1047
  skillsDir: ".codeartsdoer/skills",
1048
- globalSkillsDir: join(home, ".codeartsdoer/skills"),
1048
+ globalSkillsDir: join(home$1, ".codeartsdoer/skills"),
1049
1049
  detectInstalled: async () => {
1050
- return existsSync(join(home, ".codeartsdoer"));
1050
+ return existsSync(join(home$1, ".codeartsdoer"));
1051
1051
  }
1052
1052
  },
1053
1053
  codebuddy: {
1054
1054
  name: "codebuddy",
1055
1055
  displayName: "CodeBuddy",
1056
1056
  skillsDir: ".codebuddy/skills",
1057
- globalSkillsDir: join(home, ".codebuddy/skills"),
1057
+ globalSkillsDir: join(home$1, ".codebuddy/skills"),
1058
1058
  detectInstalled: async () => {
1059
- return existsSync(join(process.cwd(), ".codebuddy")) || existsSync(join(home, ".codebuddy"));
1059
+ return existsSync(join(process.cwd(), ".codebuddy")) || existsSync(join(home$1, ".codebuddy"));
1060
1060
  }
1061
1061
  },
1062
1062
  codemaker: {
1063
1063
  name: "codemaker",
1064
1064
  displayName: "Codemaker",
1065
1065
  skillsDir: ".codemaker/skills",
1066
- globalSkillsDir: join(home, ".codemaker/skills"),
1066
+ globalSkillsDir: join(home$1, ".codemaker/skills"),
1067
1067
  detectInstalled: async () => {
1068
- return existsSync(join(home, ".codemaker"));
1068
+ return existsSync(join(home$1, ".codemaker"));
1069
1069
  }
1070
1070
  },
1071
1071
  codestudio: {
1072
1072
  name: "codestudio",
1073
1073
  displayName: "Code Studio",
1074
1074
  skillsDir: ".codestudio/skills",
1075
- globalSkillsDir: join(home, ".codestudio/skills"),
1075
+ globalSkillsDir: join(home$1, ".codestudio/skills"),
1076
1076
  detectInstalled: async () => {
1077
- return existsSync(join(home, ".codestudio"));
1077
+ return existsSync(join(home$1, ".codestudio"));
1078
1078
  }
1079
1079
  },
1080
1080
  codex: {
@@ -1090,54 +1090,54 @@ const agents = {
1090
1090
  name: "command-code",
1091
1091
  displayName: "Command Code",
1092
1092
  skillsDir: ".commandcode/skills",
1093
- globalSkillsDir: join(home, ".commandcode/skills"),
1093
+ globalSkillsDir: join(home$1, ".commandcode/skills"),
1094
1094
  detectInstalled: async () => {
1095
- return existsSync(join(home, ".commandcode"));
1095
+ return existsSync(join(home$1, ".commandcode"));
1096
1096
  }
1097
1097
  },
1098
1098
  continue: {
1099
1099
  name: "continue",
1100
1100
  displayName: "Continue",
1101
1101
  skillsDir: ".continue/skills",
1102
- globalSkillsDir: join(home, ".continue/skills"),
1102
+ globalSkillsDir: join(home$1, ".continue/skills"),
1103
1103
  detectInstalled: async () => {
1104
- return existsSync(join(process.cwd(), ".continue")) || existsSync(join(home, ".continue"));
1104
+ return existsSync(join(process.cwd(), ".continue")) || existsSync(join(home$1, ".continue"));
1105
1105
  }
1106
1106
  },
1107
1107
  cortex: {
1108
1108
  name: "cortex",
1109
1109
  displayName: "Cortex Code",
1110
1110
  skillsDir: ".cortex/skills",
1111
- globalSkillsDir: join(home, ".snowflake/cortex/skills"),
1111
+ globalSkillsDir: join(home$1, ".snowflake/cortex/skills"),
1112
1112
  detectInstalled: async () => {
1113
- return existsSync(join(home, ".snowflake/cortex"));
1113
+ return existsSync(join(home$1, ".snowflake/cortex"));
1114
1114
  }
1115
1115
  },
1116
1116
  crush: {
1117
1117
  name: "crush",
1118
1118
  displayName: "Crush",
1119
1119
  skillsDir: ".crush/skills",
1120
- globalSkillsDir: join(home, ".config/crush/skills"),
1120
+ globalSkillsDir: join(home$1, ".config/crush/skills"),
1121
1121
  detectInstalled: async () => {
1122
- return existsSync(join(home, ".config/crush"));
1122
+ return existsSync(join(home$1, ".config/crush"));
1123
1123
  }
1124
1124
  },
1125
1125
  cursor: {
1126
1126
  name: "cursor",
1127
1127
  displayName: "Cursor",
1128
1128
  skillsDir: ".agents/skills",
1129
- globalSkillsDir: join(home, ".cursor/skills"),
1129
+ globalSkillsDir: join(home$1, ".cursor/skills"),
1130
1130
  detectInstalled: async () => {
1131
- return existsSync(join(home, ".cursor"));
1131
+ return existsSync(join(home$1, ".cursor"));
1132
1132
  }
1133
1133
  },
1134
1134
  deepagents: {
1135
1135
  name: "deepagents",
1136
1136
  displayName: "Deep Agents",
1137
1137
  skillsDir: ".agents/skills",
1138
- globalSkillsDir: join(home, ".deepagents/agent/skills"),
1138
+ globalSkillsDir: join(home$1, ".deepagents/agent/skills"),
1139
1139
  detectInstalled: async () => {
1140
- return existsSync(join(home, ".deepagents"));
1140
+ return existsSync(join(home$1, ".deepagents"));
1141
1141
  }
1142
1142
  },
1143
1143
  devin: {
@@ -1153,56 +1153,56 @@ const agents = {
1153
1153
  name: "dexto",
1154
1154
  displayName: "Dexto",
1155
1155
  skillsDir: ".agents/skills",
1156
- globalSkillsDir: join(home, ".agents/skills"),
1156
+ globalSkillsDir: join(home$1, ".agents/skills"),
1157
1157
  showInUniversalPrompt: false,
1158
1158
  detectInstalled: async () => {
1159
- return existsSync(join(home, ".dexto"));
1159
+ return existsSync(join(home$1, ".dexto"));
1160
1160
  }
1161
1161
  },
1162
1162
  droid: {
1163
1163
  name: "droid",
1164
1164
  displayName: "Droid",
1165
1165
  skillsDir: ".factory/skills",
1166
- globalSkillsDir: join(home, ".factory/skills"),
1166
+ globalSkillsDir: join(home$1, ".factory/skills"),
1167
1167
  detectInstalled: async () => {
1168
- return existsSync(join(home, ".factory"));
1168
+ return existsSync(join(home$1, ".factory"));
1169
1169
  }
1170
1170
  },
1171
1171
  firebender: {
1172
1172
  name: "firebender",
1173
1173
  displayName: "Firebender",
1174
1174
  skillsDir: ".agents/skills",
1175
- globalSkillsDir: join(home, ".firebender/skills"),
1175
+ globalSkillsDir: join(home$1, ".firebender/skills"),
1176
1176
  showInUniversalPrompt: false,
1177
1177
  detectInstalled: async () => {
1178
- return existsSync(join(home, ".firebender"));
1178
+ return existsSync(join(home$1, ".firebender"));
1179
1179
  }
1180
1180
  },
1181
1181
  forgecode: {
1182
1182
  name: "forgecode",
1183
1183
  displayName: "ForgeCode",
1184
1184
  skillsDir: ".forge/skills",
1185
- globalSkillsDir: join(home, ".forge/skills"),
1185
+ globalSkillsDir: join(home$1, ".forge/skills"),
1186
1186
  detectInstalled: async () => {
1187
- return existsSync(join(home, ".forge"));
1187
+ return existsSync(join(home$1, ".forge"));
1188
1188
  }
1189
1189
  },
1190
1190
  "gemini-cli": {
1191
1191
  name: "gemini-cli",
1192
1192
  displayName: "Gemini CLI",
1193
1193
  skillsDir: ".agents/skills",
1194
- globalSkillsDir: join(home, ".gemini/skills"),
1194
+ globalSkillsDir: join(home$1, ".gemini/skills"),
1195
1195
  detectInstalled: async () => {
1196
- return existsSync(join(home, ".gemini"));
1196
+ return existsSync(join(home$1, ".gemini"));
1197
1197
  }
1198
1198
  },
1199
1199
  "github-copilot": {
1200
1200
  name: "github-copilot",
1201
1201
  displayName: "GitHub Copilot",
1202
1202
  skillsDir: ".agents/skills",
1203
- globalSkillsDir: join(home, ".copilot/skills"),
1203
+ globalSkillsDir: join(home$1, ".copilot/skills"),
1204
1204
  detectInstalled: async () => {
1205
- return existsSync(join(home, ".copilot"));
1205
+ return existsSync(join(home$1, ".copilot"));
1206
1206
  }
1207
1207
  },
1208
1208
  goose: {
@@ -1227,100 +1227,100 @@ const agents = {
1227
1227
  name: "inference-sh",
1228
1228
  displayName: "inference.sh",
1229
1229
  skillsDir: ".inferencesh/skills",
1230
- globalSkillsDir: join(home, ".inferencesh/skills"),
1230
+ globalSkillsDir: join(home$1, ".inferencesh/skills"),
1231
1231
  detectInstalled: async () => {
1232
- return existsSync(join(home, ".inferencesh"));
1232
+ return existsSync(join(home$1, ".inferencesh"));
1233
1233
  }
1234
1234
  },
1235
1235
  jazz: {
1236
1236
  name: "jazz",
1237
1237
  displayName: "Jazz",
1238
1238
  skillsDir: ".jazz/skills",
1239
- globalSkillsDir: join(home, ".jazz/skills"),
1239
+ globalSkillsDir: join(home$1, ".jazz/skills"),
1240
1240
  detectInstalled: async () => {
1241
- return existsSync(join(home, ".jazz")) || existsSync(join(process.cwd(), ".jazz"));
1241
+ return existsSync(join(home$1, ".jazz")) || existsSync(join(process.cwd(), ".jazz"));
1242
1242
  }
1243
1243
  },
1244
1244
  junie: {
1245
1245
  name: "junie",
1246
1246
  displayName: "Junie",
1247
1247
  skillsDir: ".junie/skills",
1248
- globalSkillsDir: join(home, ".junie/skills"),
1248
+ globalSkillsDir: join(home$1, ".junie/skills"),
1249
1249
  detectInstalled: async () => {
1250
- return existsSync(join(home, ".junie"));
1250
+ return existsSync(join(home$1, ".junie"));
1251
1251
  }
1252
1252
  },
1253
1253
  "iflow-cli": {
1254
1254
  name: "iflow-cli",
1255
1255
  displayName: "iFlow CLI",
1256
1256
  skillsDir: ".iflow/skills",
1257
- globalSkillsDir: join(home, ".iflow/skills"),
1257
+ globalSkillsDir: join(home$1, ".iflow/skills"),
1258
1258
  detectInstalled: async () => {
1259
- return existsSync(join(home, ".iflow"));
1259
+ return existsSync(join(home$1, ".iflow"));
1260
1260
  }
1261
1261
  },
1262
1262
  kilo: {
1263
1263
  name: "kilo",
1264
1264
  displayName: "Kilo Code",
1265
1265
  skillsDir: ".kilocode/skills",
1266
- globalSkillsDir: join(home, ".kilocode/skills"),
1266
+ globalSkillsDir: join(home$1, ".kilocode/skills"),
1267
1267
  detectInstalled: async () => {
1268
- return existsSync(join(home, ".kilocode"));
1268
+ return existsSync(join(home$1, ".kilocode"));
1269
1269
  }
1270
1270
  },
1271
1271
  "kimi-code-cli": {
1272
1272
  name: "kimi-code-cli",
1273
1273
  displayName: "Kimi Code CLI",
1274
1274
  skillsDir: ".agents/skills",
1275
- globalSkillsDir: join(home, ".agents/skills"),
1275
+ globalSkillsDir: join(home$1, ".agents/skills"),
1276
1276
  detectInstalled: async () => {
1277
- return existsSync(join(home, ".kimi-code")) || existsSync(join(home, ".kimi"));
1277
+ return existsSync(join(home$1, ".kimi-code")) || existsSync(join(home$1, ".kimi"));
1278
1278
  }
1279
1279
  },
1280
1280
  "kiro-cli": {
1281
1281
  name: "kiro-cli",
1282
1282
  displayName: "Kiro CLI",
1283
1283
  skillsDir: ".kiro/skills",
1284
- globalSkillsDir: join(home, ".kiro/skills"),
1284
+ globalSkillsDir: join(home$1, ".kiro/skills"),
1285
1285
  detectInstalled: async () => {
1286
- return existsSync(join(home, ".kiro"));
1286
+ return existsSync(join(home$1, ".kiro"));
1287
1287
  }
1288
1288
  },
1289
1289
  kode: {
1290
1290
  name: "kode",
1291
1291
  displayName: "Kode",
1292
1292
  skillsDir: ".kode/skills",
1293
- globalSkillsDir: join(home, ".kode/skills"),
1293
+ globalSkillsDir: join(home$1, ".kode/skills"),
1294
1294
  detectInstalled: async () => {
1295
- return existsSync(join(home, ".kode"));
1295
+ return existsSync(join(home$1, ".kode"));
1296
1296
  }
1297
1297
  },
1298
1298
  lingma: {
1299
1299
  name: "lingma",
1300
1300
  displayName: "Lingma",
1301
1301
  skillsDir: ".lingma/skills",
1302
- globalSkillsDir: join(home, ".lingma/skills"),
1302
+ globalSkillsDir: join(home$1, ".lingma/skills"),
1303
1303
  detectInstalled: async () => {
1304
- return existsSync(join(home, ".lingma"));
1304
+ return existsSync(join(home$1, ".lingma"));
1305
1305
  }
1306
1306
  },
1307
1307
  loaf: {
1308
1308
  name: "loaf",
1309
1309
  displayName: "Loaf",
1310
1310
  skillsDir: ".agents/skills",
1311
- globalSkillsDir: join(home, ".agents/skills"),
1311
+ globalSkillsDir: join(home$1, ".agents/skills"),
1312
1312
  showInUniversalPrompt: false,
1313
1313
  detectInstalled: async () => {
1314
- return existsSync(join(home, ".loaf"));
1314
+ return existsSync(join(home$1, ".loaf"));
1315
1315
  }
1316
1316
  },
1317
1317
  mcpjam: {
1318
1318
  name: "mcpjam",
1319
1319
  displayName: "MCPJam",
1320
1320
  skillsDir: ".mcpjam/skills",
1321
- globalSkillsDir: join(home, ".mcpjam/skills"),
1321
+ globalSkillsDir: join(home$1, ".mcpjam/skills"),
1322
1322
  detectInstalled: async () => {
1323
- return existsSync(join(home, ".mcpjam"));
1323
+ return existsSync(join(home$1, ".mcpjam"));
1324
1324
  }
1325
1325
  },
1326
1326
  "mistral-vibe": {
@@ -1336,18 +1336,18 @@ const agents = {
1336
1336
  name: "moxby",
1337
1337
  displayName: "Moxby",
1338
1338
  skillsDir: ".moxby/skills",
1339
- globalSkillsDir: join(home, ".moxby/skills"),
1339
+ globalSkillsDir: join(home$1, ".moxby/skills"),
1340
1340
  detectInstalled: async () => {
1341
- return existsSync(join(home, ".moxby"));
1341
+ return existsSync(join(home$1, ".moxby"));
1342
1342
  }
1343
1343
  },
1344
1344
  mux: {
1345
1345
  name: "mux",
1346
1346
  displayName: "Mux",
1347
1347
  skillsDir: ".mux/skills",
1348
- globalSkillsDir: join(home, ".mux/skills"),
1348
+ globalSkillsDir: join(home$1, ".mux/skills"),
1349
1349
  detectInstalled: async () => {
1350
- return existsSync(join(home, ".mux"));
1350
+ return existsSync(join(home$1, ".mux"));
1351
1351
  }
1352
1352
  },
1353
1353
  opencode: {
@@ -1363,54 +1363,54 @@ const agents = {
1363
1363
  name: "openhands",
1364
1364
  displayName: "OpenHands",
1365
1365
  skillsDir: ".openhands/skills",
1366
- globalSkillsDir: join(home, ".openhands/skills"),
1366
+ globalSkillsDir: join(home$1, ".openhands/skills"),
1367
1367
  detectInstalled: async () => {
1368
- return existsSync(join(home, ".openhands"));
1368
+ return existsSync(join(home$1, ".openhands"));
1369
1369
  }
1370
1370
  },
1371
1371
  ona: {
1372
1372
  name: "ona",
1373
1373
  displayName: "Ona",
1374
1374
  skillsDir: ".ona/skills",
1375
- globalSkillsDir: join(home, ".ona/skills"),
1375
+ globalSkillsDir: join(home$1, ".ona/skills"),
1376
1376
  detectInstalled: async () => {
1377
- return existsSync(join(home, ".ona"));
1377
+ return existsSync(join(home$1, ".ona"));
1378
1378
  }
1379
1379
  },
1380
1380
  pi: {
1381
1381
  name: "pi",
1382
1382
  displayName: "Pi",
1383
1383
  skillsDir: ".pi/skills",
1384
- globalSkillsDir: join(home, ".pi/agent/skills"),
1384
+ globalSkillsDir: join(home$1, ".pi/agent/skills"),
1385
1385
  detectInstalled: async () => {
1386
- return existsSync(join(home, ".pi/agent"));
1386
+ return existsSync(join(home$1, ".pi/agent"));
1387
1387
  }
1388
1388
  },
1389
1389
  qoder: {
1390
1390
  name: "qoder",
1391
1391
  displayName: "Qoder",
1392
1392
  skillsDir: ".qoder/skills",
1393
- globalSkillsDir: join(home, ".qoder/skills"),
1393
+ globalSkillsDir: join(home$1, ".qoder/skills"),
1394
1394
  detectInstalled: async () => {
1395
- return existsSync(join(home, ".qoder"));
1395
+ return existsSync(join(home$1, ".qoder"));
1396
1396
  }
1397
1397
  },
1398
1398
  "qoder-cn": {
1399
1399
  name: "qoder-cn",
1400
1400
  displayName: "Qoder CN",
1401
1401
  skillsDir: ".qoder/skills",
1402
- globalSkillsDir: join(home, ".qoder-cn/skills"),
1402
+ globalSkillsDir: join(home$1, ".qoder-cn/skills"),
1403
1403
  detectInstalled: async () => {
1404
- return existsSync(join(home, ".qoder-cn"));
1404
+ return existsSync(join(home$1, ".qoder-cn"));
1405
1405
  }
1406
1406
  },
1407
1407
  "qwen-code": {
1408
1408
  name: "qwen-code",
1409
1409
  displayName: "Qwen Code",
1410
1410
  skillsDir: ".qwen/skills",
1411
- globalSkillsDir: join(home, ".qwen/skills"),
1411
+ globalSkillsDir: join(home$1, ".qwen/skills"),
1412
1412
  detectInstalled: async () => {
1413
- return existsSync(join(home, ".qwen"));
1413
+ return existsSync(join(home$1, ".qwen"));
1414
1414
  }
1415
1415
  },
1416
1416
  replit: {
@@ -1427,97 +1427,97 @@ const agents = {
1427
1427
  name: "reasonix",
1428
1428
  displayName: "Reasonix",
1429
1429
  skillsDir: ".reasonix/skills",
1430
- globalSkillsDir: join(home, ".reasonix/skills"),
1430
+ globalSkillsDir: join(home$1, ".reasonix/skills"),
1431
1431
  detectInstalled: async () => {
1432
- return existsSync(join(home, ".reasonix"));
1432
+ return existsSync(join(home$1, ".reasonix"));
1433
1433
  }
1434
1434
  },
1435
1435
  rovodev: {
1436
1436
  name: "rovodev",
1437
1437
  displayName: "Rovo Dev",
1438
1438
  skillsDir: ".rovodev/skills",
1439
- globalSkillsDir: join(home, ".rovodev/skills"),
1439
+ globalSkillsDir: join(home$1, ".rovodev/skills"),
1440
1440
  detectInstalled: async () => {
1441
- return existsSync(join(home, ".rovodev"));
1441
+ return existsSync(join(home$1, ".rovodev"));
1442
1442
  }
1443
1443
  },
1444
1444
  roo: {
1445
1445
  name: "roo",
1446
1446
  displayName: "Roo Code",
1447
1447
  skillsDir: ".roo/skills",
1448
- globalSkillsDir: join(home, ".roo/skills"),
1448
+ globalSkillsDir: join(home$1, ".roo/skills"),
1449
1449
  detectInstalled: async () => {
1450
- return existsSync(join(home, ".roo"));
1450
+ return existsSync(join(home$1, ".roo"));
1451
1451
  }
1452
1452
  },
1453
1453
  "tabnine-cli": {
1454
1454
  name: "tabnine-cli",
1455
1455
  displayName: "Tabnine CLI",
1456
1456
  skillsDir: ".tabnine/agent/skills",
1457
- globalSkillsDir: join(home, ".tabnine/agent/skills"),
1457
+ globalSkillsDir: join(home$1, ".tabnine/agent/skills"),
1458
1458
  detectInstalled: async () => {
1459
- return existsSync(join(home, ".tabnine"));
1459
+ return existsSync(join(home$1, ".tabnine"));
1460
1460
  }
1461
1461
  },
1462
1462
  terramind: {
1463
1463
  name: "terramind",
1464
1464
  displayName: "Terramind",
1465
1465
  skillsDir: ".terramind/skills",
1466
- globalSkillsDir: join(home, ".terramind/skills"),
1466
+ globalSkillsDir: join(home$1, ".terramind/skills"),
1467
1467
  detectInstalled: async () => {
1468
- return existsSync(join(home, ".terramind"));
1468
+ return existsSync(join(home$1, ".terramind"));
1469
1469
  }
1470
1470
  },
1471
1471
  tinycloud: {
1472
1472
  name: "tinycloud",
1473
1473
  displayName: "Tinycloud",
1474
1474
  skillsDir: ".tinycloud/skills",
1475
- globalSkillsDir: join(home, ".tinycloud/skills"),
1475
+ globalSkillsDir: join(home$1, ".tinycloud/skills"),
1476
1476
  detectInstalled: async () => {
1477
- return existsSync(join(home, ".tinycloud"));
1477
+ return existsSync(join(home$1, ".tinycloud"));
1478
1478
  }
1479
1479
  },
1480
1480
  trae: {
1481
1481
  name: "trae",
1482
1482
  displayName: "Trae",
1483
1483
  skillsDir: ".trae/skills",
1484
- globalSkillsDir: join(home, ".trae/skills"),
1484
+ globalSkillsDir: join(home$1, ".trae/skills"),
1485
1485
  detectInstalled: async () => {
1486
- return existsSync(join(home, ".trae"));
1486
+ return existsSync(join(home$1, ".trae"));
1487
1487
  }
1488
1488
  },
1489
1489
  "trae-cn": {
1490
1490
  name: "trae-cn",
1491
1491
  displayName: "Trae CN",
1492
1492
  skillsDir: ".trae/skills",
1493
- globalSkillsDir: join(home, ".trae-cn/skills"),
1493
+ globalSkillsDir: join(home$1, ".trae-cn/skills"),
1494
1494
  detectInstalled: async () => {
1495
- return existsSync(join(home, ".trae-cn"));
1495
+ return existsSync(join(home$1, ".trae-cn"));
1496
1496
  }
1497
1497
  },
1498
1498
  warp: {
1499
1499
  name: "warp",
1500
1500
  displayName: "Warp",
1501
1501
  skillsDir: ".agents/skills",
1502
- globalSkillsDir: join(home, ".agents/skills"),
1502
+ globalSkillsDir: join(home$1, ".agents/skills"),
1503
1503
  detectInstalled: async () => {
1504
- return existsSync(join(home, ".warp"));
1504
+ return existsSync(join(home$1, ".warp"));
1505
1505
  }
1506
1506
  },
1507
1507
  windsurf: {
1508
1508
  name: "windsurf",
1509
1509
  displayName: "Windsurf",
1510
1510
  skillsDir: ".windsurf/skills",
1511
- globalSkillsDir: join(home, ".codeium/windsurf/skills"),
1511
+ globalSkillsDir: join(home$1, ".codeium/windsurf/skills"),
1512
1512
  detectInstalled: async () => {
1513
- return existsSync(join(home, ".codeium/windsurf"));
1513
+ return existsSync(join(home$1, ".codeium/windsurf"));
1514
1514
  }
1515
1515
  },
1516
1516
  zed: {
1517
1517
  name: "zed",
1518
1518
  displayName: "Zed",
1519
1519
  skillsDir: ".agents/skills",
1520
- globalSkillsDir: join(home, ".agents/skills"),
1520
+ globalSkillsDir: join(home$1, ".agents/skills"),
1521
1521
  detectInstalled: async () => {
1522
1522
  return existsSync(join(configHome, "zed")) || !!zedAppDataHome && existsSync(join(zedAppDataHome, "Zed")) || !!zedFlatpakConfigHome && existsSync(join(zedFlatpakConfigHome, "zed"));
1523
1523
  }
@@ -1526,36 +1526,36 @@ const agents = {
1526
1526
  name: "zencoder",
1527
1527
  displayName: "Zencoder",
1528
1528
  skillsDir: ".zencoder/skills",
1529
- globalSkillsDir: join(home, ".zencoder/skills"),
1529
+ globalSkillsDir: join(home$1, ".zencoder/skills"),
1530
1530
  detectInstalled: async () => {
1531
- return existsSync(join(home, ".zencoder"));
1531
+ return existsSync(join(home$1, ".zencoder"));
1532
1532
  }
1533
1533
  },
1534
1534
  zenflow: {
1535
1535
  name: "zenflow",
1536
1536
  displayName: "Zenflow",
1537
1537
  skillsDir: ".zencoder/skills",
1538
- globalSkillsDir: join(home, ".zencoder/skills"),
1538
+ globalSkillsDir: join(home$1, ".zencoder/skills"),
1539
1539
  detectInstalled: async () => {
1540
- return existsSync(join(home, ".zencoder"));
1540
+ return existsSync(join(home$1, ".zencoder"));
1541
1541
  }
1542
1542
  },
1543
1543
  neovate: {
1544
1544
  name: "neovate",
1545
1545
  displayName: "Neovate",
1546
1546
  skillsDir: ".neovate/skills",
1547
- globalSkillsDir: join(home, ".neovate/skills"),
1547
+ globalSkillsDir: join(home$1, ".neovate/skills"),
1548
1548
  detectInstalled: async () => {
1549
- return existsSync(join(home, ".neovate"));
1549
+ return existsSync(join(home$1, ".neovate"));
1550
1550
  }
1551
1551
  },
1552
1552
  pochi: {
1553
1553
  name: "pochi",
1554
1554
  displayName: "Pochi",
1555
1555
  skillsDir: ".pochi/skills",
1556
- globalSkillsDir: join(home, ".pochi/skills"),
1556
+ globalSkillsDir: join(home$1, ".pochi/skills"),
1557
1557
  detectInstalled: async () => {
1558
- return existsSync(join(home, ".pochi"));
1558
+ return existsSync(join(home$1, ".pochi"));
1559
1559
  }
1560
1560
  },
1561
1561
  promptscript: {
@@ -1572,9 +1572,9 @@ const agents = {
1572
1572
  name: "adal",
1573
1573
  displayName: "AdaL",
1574
1574
  skillsDir: ".adal/skills",
1575
- globalSkillsDir: join(home, ".adal/skills"),
1575
+ globalSkillsDir: join(home$1, ".adal/skills"),
1576
1576
  detectInstalled: async () => {
1577
- return existsSync(join(home, ".adal"));
1577
+ return existsSync(join(home$1, ".adal"));
1578
1578
  }
1579
1579
  },
1580
1580
  universal: {
@@ -3997,7 +3997,7 @@ function parseAddOptions(args) {
3997
3997
  options
3998
3998
  };
3999
3999
  }
4000
- const execFileAsync$3 = promisify(execFile);
4000
+ const execFileAsync$4 = promisify(execFile);
4001
4001
  const claudeAgentsDir = (global) => global ? join(process.env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), ".claude"), "agents") : join(process.cwd(), ".claude", "agents");
4002
4002
  async function fetchAgentFile(source, name) {
4003
4003
  const rel = `agents/${name}.md`;
@@ -4006,7 +4006,7 @@ async function fetchAgentFile(source, name) {
4006
4006
  return existsSync(file) ? readFile(file, "utf8") : null;
4007
4007
  }
4008
4008
  if (/^[^/]+\/[^/]+$/.test(source)) try {
4009
- const { stdout } = await execFileAsync$3("gh", [
4009
+ const { stdout } = await execFileAsync$4("gh", [
4010
4010
  "api",
4011
4011
  `repos/${source}/contents/${rel}`,
4012
4012
  "--jq",
@@ -4103,7 +4103,7 @@ async function runAgents(args) {
4103
4103
  console.log(`Unknown agents command: ${sub}`);
4104
4104
  console.log(AGENTS_HELP);
4105
4105
  }
4106
- const execFileAsync$2 = promisify(execFile);
4106
+ const execFileAsync$3 = promisify(execFile);
4107
4107
  const RULE_SCOPES = [
4108
4108
  "client",
4109
4109
  "server",
@@ -4120,7 +4120,7 @@ async function fetchFromCatalog(source, rel) {
4120
4120
  return existsSync(file) ? readFile(file, "utf8") : null;
4121
4121
  }
4122
4122
  if (/^[^/]+\/[^/]+$/.test(source)) try {
4123
- const { stdout } = await execFileAsync$2("gh", [
4123
+ const { stdout } = await execFileAsync$3("gh", [
4124
4124
  "api",
4125
4125
  `repos/${source}/contents/${rel}`,
4126
4126
  "--jq",
@@ -4249,7 +4249,7 @@ async function runRules(args) {
4249
4249
  const { names, options } = parseRulesOptions(sub === "add" || sub === "a" || sub === "install" || sub === "i" ? args.slice(1) : args);
4250
4250
  await addRules(names, options);
4251
4251
  }
4252
- const execFileAsync$1 = promisify(execFile);
4252
+ const execFileAsync$2 = promisify(execFile);
4253
4253
  const RESET$3 = "\x1B[0m";
4254
4254
  const BOLD$3 = "\x1B[1m";
4255
4255
  const DIM$3 = "\x1B[38;5;102m";
@@ -4268,7 +4268,7 @@ async function fetchCatalogIndex(source) {
4268
4268
  return JSON.parse(raw);
4269
4269
  }
4270
4270
  if (source.match(/^[^/]+\/[^/]+$/)) {
4271
- const { stdout } = await execFileAsync$1("gh", [
4271
+ const { stdout } = await execFileAsync$2("gh", [
4272
4272
  "api",
4273
4273
  `repos/${source}/contents/${rel}`,
4274
4274
  "--jq",
@@ -7830,7 +7830,7 @@ function registerVault(vaultPath) {
7830
7830
  writeFileSync(configPath, JSON.stringify(config), "utf-8");
7831
7831
  M.success("Registered vault in Obsidian");
7832
7832
  }
7833
- const execFileAsync = promisify(execFile);
7833
+ const execFileAsync$1 = promisify(execFile);
7834
7834
  const BUZZ_LOGO_LINES = [
7835
7835
  "██████╗ ██╗ ██╗███████╗███████╗",
7836
7836
  "██╔══██╗██║ ██║╚══███╔╝╚══███╔╝",
@@ -7946,7 +7946,7 @@ const TEAM_AGENTS = {
7946
7946
  const TEAMS = Object.keys(TEAM_SKILLS);
7947
7947
  async function isBuzzRunning() {
7948
7948
  try {
7949
- const { stdout } = await execFileAsync("pgrep", ["-f", "Buzz.app|buzz-desktop|buzz-acp"]);
7949
+ const { stdout } = await execFileAsync$1("pgrep", ["-f", "Buzz.app|buzz-desktop|buzz-acp"]);
7950
7950
  return stdout.trim().length > 0;
7951
7951
  } catch {
7952
7952
  return false;
@@ -7954,7 +7954,7 @@ async function isBuzzRunning() {
7954
7954
  }
7955
7955
  async function claudeAuthStatus() {
7956
7956
  try {
7957
- const { stdout } = await execFileAsync("claude", ["auth", "status"]);
7957
+ const { stdout } = await execFileAsync$1("claude", ["auth", "status"]);
7958
7958
  const parsed = JSON.parse(stdout);
7959
7959
  const ok = parsed.apiProvider === "firstParty";
7960
7960
  return {
@@ -7997,14 +7997,14 @@ async function syncVault() {
7997
7997
  M.step("Pruff knowledge vault");
7998
7998
  const dest = join(nestDir(), "REPOS", "pruff-knowledge");
7999
7999
  if (existsSync(join(dest, ".git"))) try {
8000
- await execFileAsync("git", [
8000
+ await execFileAsync$1("git", [
8001
8001
  "-C",
8002
8002
  dest,
8003
8003
  "pull",
8004
8004
  "--ff-only",
8005
8005
  "--quiet"
8006
8006
  ]);
8007
- const { stdout } = await execFileAsync("git", [
8007
+ const { stdout } = await execFileAsync$1("git", [
8008
8008
  "-C",
8009
8009
  dest,
8010
8010
  "log",
@@ -8020,13 +8020,13 @@ async function syncVault() {
8020
8020
  else {
8021
8021
  mkdirSync(dirname(dest), { recursive: true });
8022
8022
  try {
8023
- await execFileAsync("git", [
8023
+ await execFileAsync$1("git", [
8024
8024
  "clone",
8025
8025
  "--quiet",
8026
8026
  VAULT_REPO,
8027
8027
  dest
8028
8028
  ]);
8029
- const { stdout } = await execFileAsync("git", [
8029
+ const { stdout } = await execFileAsync$1("git", [
8030
8030
  "-C",
8031
8031
  dest,
8032
8032
  "log",
@@ -8319,6 +8319,310 @@ async function runBuzz(args) {
8319
8319
  console.log(BUZZ_HELP);
8320
8320
  }
8321
8321
  }
8322
+ const execFileAsync = promisify(execFile);
8323
+ const __analyticsDirname = dirname(fileURLToPath(import.meta.url));
8324
+ const POSTHOG_HOST = "https://us.i.posthog.com";
8325
+ const ANALYTICS_TOKEN_CATALOG_PATH = "analytics/posthog.json";
8326
+ let cachedToken = null;
8327
+ async function getAnalyticsToken() {
8328
+ const fromEnv = process.env.PRUFF_ANALYTICS_POSTHOG_TOKEN?.trim();
8329
+ if (fromEnv) return fromEnv;
8330
+ if (cachedToken !== null) return cachedToken;
8331
+ const raw = readLocalCheckoutToken() ?? await fetchTokenFile(PRUFF_CATALOG, ANALYTICS_TOKEN_CATALOG_PATH);
8332
+ cachedToken = raw ? JSON.parse(raw).token ?? "" : "";
8333
+ return cachedToken;
8334
+ }
8335
+ function readLocalCheckoutToken() {
8336
+ let dir = __analyticsDirname;
8337
+ for (let depth = 0; depth < 4; depth++) {
8338
+ const candidate = join(dir, ANALYTICS_TOKEN_CATALOG_PATH);
8339
+ if (existsSync(candidate)) return readFileSync(candidate, "utf-8");
8340
+ const parent = dirname(dir);
8341
+ if (parent === dir) break;
8342
+ dir = parent;
8343
+ }
8344
+ return null;
8345
+ }
8346
+ async function fetchTokenFile(source, rel) {
8347
+ if (source.startsWith(".") || isAbsolute(source)) {
8348
+ const file = join(source, rel);
8349
+ return existsSync(file) ? readFileSync(file, "utf-8") : null;
8350
+ }
8351
+ if (/^[^/]+\/[^/]+$/.test(source)) try {
8352
+ const { stdout } = await execFileAsync("gh", [
8353
+ "api",
8354
+ `repos/${source}/contents/${rel}`,
8355
+ "--jq",
8356
+ ".content"
8357
+ ], { env: {
8358
+ ...process.env,
8359
+ GIT_TERMINAL_PROMPT: "0"
8360
+ } });
8361
+ return Buffer.from(stdout.trim(), "base64").toString("utf8");
8362
+ } catch {
8363
+ return null;
8364
+ }
8365
+ return null;
8366
+ }
8367
+ const home = homedir();
8368
+ const claudeHome = () => process.env.CLAUDE_CONFIG_DIR?.trim() || join(home, ".claude");
8369
+ const claudeSettingsPath = () => join(claudeHome(), "settings.json");
8370
+ function buildAnalyticsEnv(token, options = {}) {
8371
+ return {
8372
+ ...options.withToolDetails ? { OTEL_LOG_TOOL_DETAILS: "1" } : {},
8373
+ CLAUDE_CODE_ENABLE_TELEMETRY: "1",
8374
+ OTEL_LOGS_EXPORTER: "otlp",
8375
+ OTEL_METRICS_EXPORTER: "otlp",
8376
+ OTEL_EXPORTER_OTLP_PROTOCOL: "http/protobuf",
8377
+ OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: `${POSTHOG_HOST}/i/v1/logs`,
8378
+ OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: `${POSTHOG_HOST}/i/v1/metrics`,
8379
+ OTEL_EXPORTER_OTLP_LOGS_HEADERS: `Authorization=Bearer ${token}`,
8380
+ OTEL_EXPORTER_OTLP_METRICS_HEADERS: `Authorization=Bearer ${token}`,
8381
+ OTEL_SERVICE_NAME: "claude-code",
8382
+ OTEL_RESOURCE_ATTRIBUTES: "pruff.rollout=team",
8383
+ OTEL_METRICS_INCLUDE_SESSION_ID: "false",
8384
+ OTEL_LOG_USER_PROMPTS: "0",
8385
+ OTEL_LOG_ASSISTANT_RESPONSES: "0"
8386
+ };
8387
+ }
8388
+ const ANALYTICS_ENV_KEYS = Object.keys(buildAnalyticsEnv("x", { withToolDetails: true }));
8389
+ function readClaudeSettings() {
8390
+ const path = claudeSettingsPath();
8391
+ if (!existsSync(path)) return {};
8392
+ try {
8393
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
8394
+ return parsed && typeof parsed === "object" ? parsed : {};
8395
+ } catch {
8396
+ throw new Error(`${path} is not valid JSON. Fix it (or move it aside) before running this command.`);
8397
+ }
8398
+ }
8399
+ function writeClaudeSettings(settings) {
8400
+ mkdirSync(claudeHome(), { recursive: true });
8401
+ writeFileSync(claudeSettingsPath(), `${JSON.stringify(settings, null, 2)}\n`, "utf-8");
8402
+ }
8403
+ function applyAnalyticsEnv(settings, token, options = {}) {
8404
+ const base = stripAnalyticsEnv(settings);
8405
+ return {
8406
+ ...base,
8407
+ env: {
8408
+ ...base.env ?? {},
8409
+ ...buildAnalyticsEnv(token, options)
8410
+ }
8411
+ };
8412
+ }
8413
+ function stripAnalyticsEnv(settings) {
8414
+ if (!settings.env) return settings;
8415
+ const env = { ...settings.env };
8416
+ for (const key of ANALYTICS_ENV_KEYS) delete env[key];
8417
+ const next = { ...settings };
8418
+ if (Object.keys(env).length === 0) delete next.env;
8419
+ else next.env = env;
8420
+ return next;
8421
+ }
8422
+ function isAnalyticsEnabled(settings = readClaudeSettings()) {
8423
+ return settings.env?.CLAUDE_CODE_ENABLE_TELEMETRY === "1";
8424
+ }
8425
+ async function requireToken() {
8426
+ const token = await getAnalyticsToken();
8427
+ if (token.startsWith("phc_")) return token;
8428
+ console.log();
8429
+ console.log(import_picocolors.default.red(" Could not resolve the PostHog project token."));
8430
+ console.log(import_picocolors.default.dim(` Looked in three places, in order:
8431
+ 1. \$PRUFF_ANALYTICS_POSTHOG_TOKEN (unset)
8432
+ 2. ${ANALYTICS_TOKEN_CATALOG_PATH} in this checkout (not found)\n 3. ${ANALYTICS_TOKEN_CATALOG_PATH} in ${PRUFF_CATALOG} via gh (not found)\n Expected a file containing {"token":"phc_..."}.\n Check \`gh auth status\` — the catalog is private and read with your own
8433
+ GitHub auth. To test without the catalog, export PRUFF_ANALYTICS_POSTHOG_TOKEN.`));
8434
+ console.log();
8435
+ return null;
8436
+ }
8437
+ async function runStatus() {
8438
+ const settings = readClaudeSettings();
8439
+ const enabled = isAnalyticsEnabled(settings);
8440
+ const token = await getAnalyticsToken();
8441
+ console.log();
8442
+ console.log(` ${import_picocolors.default.bold("Usage analytics")} ${enabled ? import_picocolors.default.green("on") : import_picocolors.default.dim("off")}`);
8443
+ console.log(` ${import_picocolors.default.dim("settings")} ${claudeSettingsPath()}`);
8444
+ if (enabled) {
8445
+ console.log(` ${import_picocolors.default.dim("logs")} ${settings.env?.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT}`);
8446
+ console.log(` ${import_picocolors.default.dim("metrics")} ${settings.env?.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT}`);
8447
+ console.log(` ${import_picocolors.default.dim("token")} ${token ? `${token.slice(0, 12)}…` : import_picocolors.default.red("missing")}`);
8448
+ console.log(` ${import_picocolors.default.dim("prompts")} not exported (OTEL_LOG_USER_PROMPTS=0)`);
8449
+ const toolDetails = settings.env?.OTEL_LOG_TOOL_DETAILS === "1";
8450
+ console.log(` ${import_picocolors.default.dim("cmds")} ${toolDetails ? import_picocolors.default.yellow("EXPORTED verbatim (--with-tool-details on)") : "not exported — skills report as \"custom_skill\""}`);
8451
+ }
8452
+ console.log();
8453
+ console.log(import_picocolors.default.dim(enabled ? " Turn it off with: npx pruff-skills analytics disable" : " Turn it on with: npx pruff-skills analytics enable"));
8454
+ console.log();
8455
+ }
8456
+ async function runEnable(args) {
8457
+ const withToolDetails = args.includes("--with-tool-details");
8458
+ const token = await requireToken();
8459
+ if (!token) return;
8460
+ const settings = readClaudeSettings();
8461
+ const already = isAnalyticsEnabled(settings);
8462
+ writeClaudeSettings(applyAnalyticsEnv(settings, token, { withToolDetails }));
8463
+ console.log();
8464
+ console.log(` ${import_picocolors.default.green("✓")} Usage analytics ${already ? "refreshed" : "enabled"} in ${import_picocolors.default.dim(claudeSettingsPath())}`);
8465
+ console.log();
8466
+ console.log(` ${import_picocolors.default.dim("Exports")} token + cost usage, tool names, results and timings,`);
8467
+ console.log(` ${import_picocolors.default.dim(" ")} MCP tool calls, model, subagent vs main, your email.`);
8468
+ console.log(` ${import_picocolors.default.dim("Never")} prompts, responses, file contents, raw API bodies.`);
8469
+ console.log();
8470
+ if (withToolDetails) {
8471
+ console.log(` ${import_picocolors.default.yellow("!")} ${import_picocolors.default.bold("--with-tool-details is ON.")}`);
8472
+ console.log(import_picocolors.default.dim(" Real skill and slash-command names will be exported — and so will"));
8473
+ console.log(import_picocolors.default.dim(" every shell command and tool input, verbatim. Anything you type into"));
8474
+ console.log(import_picocolors.default.dim(" a command (RUTs, connection strings, tokens) reaches PostHog."));
8475
+ console.log(import_picocolors.default.dim(" Turn it back off by re-running `analytics enable` without the flag."));
8476
+ } else {
8477
+ console.log(import_picocolors.default.dim(" Pruff-authored skills report as \"custom_skill\", not their real names."));
8478
+ console.log(import_picocolors.default.dim(" `analytics enable --with-tool-details` unlocks the names — read what that"));
8479
+ console.log(import_picocolors.default.dim(" also exports first: npx pruff-skills analytics --help"));
8480
+ }
8481
+ console.log();
8482
+ console.log(` ${import_picocolors.default.bold("Restart Claude Code")} — the OTEL exporter is read once at session start.`);
8483
+ console.log(import_picocolors.default.dim(" Then confirm the pipeline works: npx pruff-skills analytics verify"));
8484
+ console.log();
8485
+ }
8486
+ function runDisable() {
8487
+ writeClaudeSettings(stripAnalyticsEnv(readClaudeSettings()));
8488
+ console.log();
8489
+ console.log(` ${import_picocolors.default.green("✓")} Usage analytics disabled — the env block is gone from`);
8490
+ console.log(import_picocolors.default.dim(` ${claudeSettingsPath()}, your own keys untouched.`));
8491
+ console.log(import_picocolors.default.dim(" Nothing turns it back on: only `analytics enable` ever writes it."));
8492
+ console.log(import_picocolors.default.dim(" Restart Claude Code to stop the current session exporting."));
8493
+ console.log();
8494
+ }
8495
+ async function verifyToken(token) {
8496
+ try {
8497
+ const res = await fetch(`${POSTHOG_HOST}/flags?v=2`, {
8498
+ method: "POST",
8499
+ headers: { "Content-Type": "application/json" },
8500
+ body: JSON.stringify({
8501
+ api_key: token,
8502
+ distinct_id: "pruff-analytics-verify"
8503
+ })
8504
+ });
8505
+ if (res.ok) {
8506
+ console.log(` ${import_picocolors.default.green("✓")} Token accepted by PostHog (HTTP ${res.status}).`);
8507
+ return true;
8508
+ }
8509
+ console.log(` ${import_picocolors.default.red("✗")} Token rejected (HTTP ${res.status}).`);
8510
+ console.log(import_picocolors.default.dim(` ${ANALYTICS_TOKEN_CATALOG_PATH} in ${PRUFF_CATALOG} does not hold a valid`));
8511
+ console.log(import_picocolors.default.dim(" project token for the Pruff PostHog project."));
8512
+ return false;
8513
+ } catch (error) {
8514
+ console.log(` ${import_picocolors.default.red("✗")} Could not reach PostHog: ${error instanceof Error ? error.message : error}`);
8515
+ return false;
8516
+ }
8517
+ }
8518
+ async function runVerify() {
8519
+ const token = await requireToken();
8520
+ if (!token) return;
8521
+ console.log();
8522
+ console.log(import_picocolors.default.dim(` POST ${POSTHOG_HOST}/flags?v=2 ${import_picocolors.default.dim("(does this token exist?)")}`));
8523
+ if (!await verifyToken(token)) {
8524
+ console.log();
8525
+ return;
8526
+ }
8527
+ const endpoint = `${POSTHOG_HOST}/i/v1/logs`;
8528
+ const body = { resourceLogs: [{
8529
+ resource: { attributes: [{
8530
+ key: "service.name",
8531
+ value: { stringValue: "claude-code" }
8532
+ }, {
8533
+ key: "pruff.rollout",
8534
+ value: { stringValue: "team" }
8535
+ }] },
8536
+ scopeLogs: [{ logRecords: [{
8537
+ timeUnixNano: `${Date.now() * 1e6}`,
8538
+ severityText: "INFO",
8539
+ body: { stringValue: "pruff_analytics_verify" },
8540
+ attributes: [{
8541
+ key: "pruff.verify",
8542
+ value: { boolValue: true }
8543
+ }]
8544
+ }] }]
8545
+ }] };
8546
+ console.log(import_picocolors.default.dim(` POST ${endpoint} ${import_picocolors.default.dim("(does the OTLP path accept our payload?)")}`));
8547
+ try {
8548
+ const res = await fetch(endpoint, {
8549
+ method: "POST",
8550
+ headers: {
8551
+ "Content-Type": "application/json",
8552
+ Authorization: `Bearer ${token}`
8553
+ },
8554
+ body: JSON.stringify(body)
8555
+ });
8556
+ if (res.ok) console.log(` ${import_picocolors.default.green("✓")} Payload accepted (HTTP ${res.status}).`);
8557
+ else {
8558
+ console.log(` ${import_picocolors.default.red("✗")} Rejected (HTTP ${res.status}).`);
8559
+ console.log(import_picocolors.default.dim(` ${(await res.text()).slice(0, 300)}`));
8560
+ console.log();
8561
+ return;
8562
+ }
8563
+ } catch (error) {
8564
+ console.log(` ${import_picocolors.default.red("✗")} Request failed: ${error instanceof Error ? error.message : error}`);
8565
+ console.log();
8566
+ return;
8567
+ }
8568
+ console.log();
8569
+ console.log(` ${import_picocolors.default.bold("Last step — confirm it landed:")}`);
8570
+ console.log(import_picocolors.default.dim(` PostHog → Logs, filter service = "claude-code", search "pruff_analytics_verify".`));
8571
+ console.log(import_picocolors.default.dim(" If the token check passed but nothing appears within a minute or two,"));
8572
+ console.log(import_picocolors.default.dim(" the project has log ingestion disabled or is over quota."));
8573
+ console.log();
8574
+ }
8575
+ function showAnalyticsHelp() {
8576
+ console.log(`
8577
+ ${import_picocolors.default.bold("Usage:")} pruff-skills analytics <command>
8578
+
8579
+ status Show whether Claude Code is exporting to PostHog (default)
8580
+ enable [--with-tool-details] Turn the exporter on in ~/.claude/settings.json
8581
+ disable Turn the exporter off again
8582
+ verify Prove the PostHog ingest path works
8583
+
8584
+ ${import_picocolors.default.bold("What is exported by default")}
8585
+ Token and cost usage, model, subagent vs main agent, tool names with their
8586
+ success and duration, MCP tool calls, session and prompt counts, your email.
8587
+
8588
+ ${import_picocolors.default.bold("What is never exported")}
8589
+ Your prompts, Claude's responses, file contents and raw API bodies.
8590
+
8591
+ ${import_picocolors.default.bold("--with-tool-details")}
8592
+ Unlocks real skill and slash-command names, which are otherwise anonymized to
8593
+ "custom_skill" / "custom". It ALSO exports every shell command and tool input
8594
+ verbatim — measured, not theoretical. Off by default: Pruff's own rule is that
8595
+ RUTs, SII credentials and API keys are never logged. MCP attribution does not
8596
+ need this flag; mcp_server.name and mcp_tool.name are exported either way.
8597
+ `);
8598
+ }
8599
+ async function runAnalytics(args = []) {
8600
+ const sub = args[0];
8601
+ switch (sub) {
8602
+ case void 0:
8603
+ case "status":
8604
+ await runStatus();
8605
+ break;
8606
+ case "enable":
8607
+ case "on":
8608
+ await runEnable(args.slice(1));
8609
+ break;
8610
+ case "disable":
8611
+ case "off":
8612
+ runDisable();
8613
+ break;
8614
+ case "verify":
8615
+ await runVerify();
8616
+ break;
8617
+ case "--help":
8618
+ case "-h":
8619
+ showAnalyticsHelp();
8620
+ break;
8621
+ default:
8622
+ console.log(`Unknown analytics command: ${sub}`);
8623
+ showAnalyticsHelp();
8624
+ }
8625
+ }
8322
8626
  const __dirname = dirname(fileURLToPath(import.meta.url));
8323
8627
  function getVersion() {
8324
8628
  try {
@@ -8461,6 +8765,14 @@ ${BOLD}Rules:${RESET}
8461
8765
  rules add --scope <client|server|common>
8462
8766
  Install a whole repo's rule set (client/server also pull in common)
8463
8767
 
8768
+ ${BOLD}Usage analytics:${RESET}
8769
+ analytics status Is Claude Code reporting usage to Pruff's PostHog? (alias: telemetry)
8770
+ analytics enable Turn it on in ~/.claude/settings.json
8771
+ analytics disable Turn it off again, leaving your own env keys untouched
8772
+ analytics verify Prove the PostHog ingest path works before trusting it
8773
+ ${DIM}Reports token/cost by skill, MCP tool, subagent and model.${RESET}
8774
+ ${DIM}Never exports prompts, responses or file contents.${RESET}
8775
+
8464
8776
  ${BOLD}Onboarding:${RESET}
8465
8777
  onboarding [dir] Set up a new machine (macOS), end to end: prerequisites +
8466
8778
  GitHub auth, Claude Code, Obsidian, Bun, clone the
@@ -8684,6 +8996,10 @@ async function main() {
8684
8996
  if (!inAgent) await showBuzzLogo();
8685
8997
  await runBuzz(restArgs);
8686
8998
  break;
8999
+ case "analytics":
9000
+ case "telemetry":
9001
+ await runAnalytics(restArgs);
9002
+ break;
8687
9003
  case "onboarding":
8688
9004
  case "onboard":
8689
9005
  if (!inAgent) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pruff-skills",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "The official Pruff agent skills CLI",
5
5
  "type": "module",
6
6
  "bin": {