ai-otel-setup 1.0.3 → 1.0.4
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.
package/cli.js
CHANGED
|
@@ -25,6 +25,28 @@ const fs = require("fs");
|
|
|
25
25
|
const path = require("path");
|
|
26
26
|
const os = require("os");
|
|
27
27
|
|
|
28
|
+
const PKG_VERSION = require("./package.json").version;
|
|
29
|
+
|
|
30
|
+
// 安装时这台机器的 node 绝对路径,给 hook 命令做兜底(见 buildHookCommand)。
|
|
31
|
+
const NODE_BIN = process.execPath;
|
|
32
|
+
|
|
33
|
+
// 跨平台 hook 命令:固定形式 `<NODE_BIN> <launcher> <hook>`,三段都是绝对路径,
|
|
34
|
+
// 对 shell 完全透明(POSIX sh / cmd.exe / PowerShell 5.1+ / PowerShell 7+ 全 cover)。
|
|
35
|
+
// "PATH 上 node 优先 → 否则用 baked 绝对路径" 的兜底逻辑放在 launch-hook.js 里做,
|
|
36
|
+
// 不再依赖 shell `||` 操作符——PS 5.1 不支持 `||`,cc/gemini 在 Windows 上默认就
|
|
37
|
+
// 是 PS,会被坑。
|
|
38
|
+
function buildHookCommand(launcherPath, scriptPath) {
|
|
39
|
+
return `"${NODE_BIN}" "${launcherPath}" "${scriptPath}"`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 把 launcher 模板拷到 hook 同目录,返回 launcher 的绝对路径
|
|
43
|
+
function installLauncher(installDir) {
|
|
44
|
+
const launcherDest = path.join(installDir, "launch-hook.js");
|
|
45
|
+
fs.copyFileSync(path.join(__dirname, "templates", "launch-hook.js"), launcherDest);
|
|
46
|
+
fs.chmodSync(launcherDest, 0o755);
|
|
47
|
+
return launcherDest;
|
|
48
|
+
}
|
|
49
|
+
|
|
28
50
|
const REQUIRED_KEYS = ["url"];
|
|
29
51
|
const HOOK_ID = "team:session-start";
|
|
30
52
|
// UserPromptSubmit 兜底 hook:复用同一脚本,靠 stdin.hook_event_name 分流;
|
|
@@ -256,7 +278,13 @@ function stripLegacyCodexHook(text) {
|
|
|
256
278
|
);
|
|
257
279
|
}
|
|
258
280
|
|
|
259
|
-
function
|
|
281
|
+
function stripLegacyCodexHooksFlag(text) {
|
|
282
|
+
// Codex 把 [features].codex_hooks 重命名为 [features].hooks,旧 key 启动时触发 deprecation 警告
|
|
283
|
+
// 删 = true 这行,由 managed 块统一写 hooks = true;= false 是显式 opt-out,保留
|
|
284
|
+
return text.replace(/^[ \t]*codex_hooks[ \t]*=[ \t]*true[ \t]*\r?\n/gm, "");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function buildCodexManagedBlock(endpoint, hookDest, launcherDest) {
|
|
260
288
|
// exporter / trace_exporter / metrics_exporter 是 externally-tagged enum:
|
|
261
289
|
// - 写 scalar `exporter = "otlp-grpc"`:codex 解析为 unit variant,因为
|
|
262
290
|
// OtlpGrpc 是 struct variant(带 endpoint 等字段),报
|
|
@@ -268,7 +296,7 @@ function buildCodexManagedBlock(endpoint, hookDest, logsEndpoint) {
|
|
|
268
296
|
return [
|
|
269
297
|
CODEX_MANAGED_BEGIN,
|
|
270
298
|
"[features]",
|
|
271
|
-
"
|
|
299
|
+
"hooks = true",
|
|
272
300
|
"",
|
|
273
301
|
"[otel]",
|
|
274
302
|
'environment = "prod"',
|
|
@@ -288,7 +316,7 @@ function buildCodexManagedBlock(endpoint, hookDest, logsEndpoint) {
|
|
|
288
316
|
"",
|
|
289
317
|
"[[hooks.SessionStart.hooks]]",
|
|
290
318
|
'type = "command"',
|
|
291
|
-
`command = ${JSON.stringify(
|
|
319
|
+
`command = ${JSON.stringify(buildHookCommand(launcherDest, hookDest))}`,
|
|
292
320
|
CODEX_MANAGED_END,
|
|
293
321
|
].join("\n");
|
|
294
322
|
}
|
|
@@ -304,16 +332,23 @@ function installCodex(home, endpoint) {
|
|
|
304
332
|
fs.mkdirSync(installDir, { recursive: true });
|
|
305
333
|
fs.copyFileSync(path.join(__dirname, "templates", "codex", "on-session-start.js"), hookDest);
|
|
306
334
|
fs.chmodSync(hookDest, 0o755);
|
|
335
|
+
const launcherDest = installLauncher(installDir);
|
|
307
336
|
const bak = backup(configPath);
|
|
308
337
|
let existing = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
|
|
309
338
|
|
|
310
|
-
//
|
|
339
|
+
// 四步去重:先剥离上一次的 managed 块,再清掉旧 schema 残留
|
|
311
340
|
existing = stripCodexManagedBlock(existing);
|
|
312
341
|
existing = stripLegacyCodexOtel(existing);
|
|
313
342
|
existing = stripLegacyCodexHook(existing);
|
|
343
|
+
existing = stripLegacyCodexHooksFlag(existing);
|
|
314
344
|
|
|
315
|
-
|
|
316
|
-
|
|
345
|
+
// hook 同目录的 endpoint.json:hook 脚本运行时读它拿 logs endpoint,避免依赖
|
|
346
|
+
// shell 前缀注入 env(cmd.exe 不认那种语法,跨平台必须改成走文件)。
|
|
347
|
+
writeJSONAtomic(path.join(installDir, "endpoint.json"), {
|
|
348
|
+
endpoint,
|
|
349
|
+
logsEndpoint: logsEndpointFromGrpc(endpoint),
|
|
350
|
+
});
|
|
351
|
+
const managed = buildCodexManagedBlock(endpoint, hookDest, launcherDest);
|
|
317
352
|
const merged = (existing.trimEnd() + "\n\n" + managed + "\n").replace(/\n{3,}/g, "\n\n");
|
|
318
353
|
fs.writeFileSync(configPath, merged, "utf8");
|
|
319
354
|
return { tool: "codex", status: "installed", path: configPath, backup: bak };
|
|
@@ -330,6 +365,12 @@ function installGemini(home, endpoint) {
|
|
|
330
365
|
fs.mkdirSync(installDir, { recursive: true });
|
|
331
366
|
fs.copyFileSync(path.join(__dirname, "templates", "gemini", "on-session-start.js"), hookDest);
|
|
332
367
|
fs.chmodSync(hookDest, 0o755);
|
|
368
|
+
const launcherDest = installLauncher(installDir);
|
|
369
|
+
// 同 Codex:endpoint.json 给 hook 脚本读,跨平台不依赖 env 前缀。
|
|
370
|
+
writeJSONAtomic(path.join(installDir, "endpoint.json"), {
|
|
371
|
+
endpoint,
|
|
372
|
+
logsEndpoint: logsEndpointFromGrpc(endpoint),
|
|
373
|
+
});
|
|
333
374
|
const existing = readJSONSafe(settingsPath);
|
|
334
375
|
const bak = backup(settingsPath);
|
|
335
376
|
const merged = { ...existing };
|
|
@@ -350,7 +391,7 @@ function installGemini(home, endpoint) {
|
|
|
350
391
|
: [];
|
|
351
392
|
const hookEntry = {
|
|
352
393
|
id: HOOK_ID,
|
|
353
|
-
command:
|
|
394
|
+
command: buildHookCommand(launcherDest, hookDest),
|
|
354
395
|
};
|
|
355
396
|
const idx = sessionStart.findIndex((h) => h && h.id === HOOK_ID);
|
|
356
397
|
if (idx >= 0) sessionStart[idx] = hookEntry;
|
|
@@ -384,6 +425,7 @@ function main() {
|
|
|
384
425
|
const installDir = path.join(claudeDir, "cc-otel");
|
|
385
426
|
const settingsPath = path.join(claudeDir, "settings.json");
|
|
386
427
|
const hookScriptDest = path.join(installDir, "on-session-start.js");
|
|
428
|
+
const launcherDest = path.join(installDir, "launch-hook.js");
|
|
387
429
|
|
|
388
430
|
const templateDir = path.join(__dirname, "templates");
|
|
389
431
|
const settingsTemplate = readJSONSafe(path.join(templateDir, "settings.template.json"));
|
|
@@ -402,7 +444,7 @@ function main() {
|
|
|
402
444
|
hooks: [
|
|
403
445
|
{
|
|
404
446
|
type: "command",
|
|
405
|
-
command:
|
|
447
|
+
command: buildHookCommand(launcherDest, hookScriptDest),
|
|
406
448
|
timeout: 3,
|
|
407
449
|
},
|
|
408
450
|
],
|
|
@@ -419,7 +461,7 @@ function main() {
|
|
|
419
461
|
hooks: [
|
|
420
462
|
{
|
|
421
463
|
type: "command",
|
|
422
|
-
command:
|
|
464
|
+
command: buildHookCommand(launcherDest, hookScriptDest),
|
|
423
465
|
timeout: 3,
|
|
424
466
|
},
|
|
425
467
|
],
|
|
@@ -431,6 +473,7 @@ function main() {
|
|
|
431
473
|
fs.mkdirSync(installDir, { recursive: true });
|
|
432
474
|
fs.copyFileSync(hookScriptSrc, hookScriptDest);
|
|
433
475
|
fs.chmodSync(hookScriptDest, 0o755);
|
|
476
|
+
installLauncher(installDir);
|
|
434
477
|
|
|
435
478
|
// v1.0.3:把 endpoint 写盘,给 hook 脚本的 resolveLogsEndpoint 当兜底。
|
|
436
479
|
// 修的是 v1.0.2 的真实事故:settings.json 的 env 不一定能继承到 hook 子进程
|
|
@@ -469,6 +512,7 @@ function main() {
|
|
|
469
512
|
|
|
470
513
|
console.log("[ai-otel-setup] 安装完成。");
|
|
471
514
|
console.log("");
|
|
515
|
+
console.log(` ${"version".padEnd(12)}: ${PKG_VERSION}`);
|
|
472
516
|
console.log(` ${"endpoint".padEnd(12)}: ${endpoint}`);
|
|
473
517
|
for (const r of allResults) {
|
|
474
518
|
console.log(` ${r.tool.padEnd(12)}: ${r.status}${r.reason ? " (" + r.reason + ")" : ""}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-otel-setup",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "One-shot installer for AI CLI OpenTelemetry forwarding. Writes Claude Code, Codex CLI, and Gemini CLI telemetry config in a single npx command.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ai-otel-setup": "cli.js",
|
|
@@ -27,8 +27,17 @@ function safeGit(args) {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// 解析 OTLP/HTTP logs endpoint。优先级:env 覆盖 → installer 写在 hook 同目录的
|
|
31
|
+
// endpoint.json → localhost 兜底。原本走 shell 前缀 `AI_OTEL_LOGS_ENDPOINT=...` 注入
|
|
32
|
+
// env,但那是 POSIX 独有语法、cmd.exe 把它当程序名就 G 了,所以 v1.0.4 起命令行
|
|
33
|
+
// 不再带前缀,改让脚本自己读 endpoint.json,跨平台统一。env 留作 debug 覆盖口。
|
|
30
34
|
function endpoint() {
|
|
31
|
-
|
|
35
|
+
if (process.env.AI_OTEL_LOGS_ENDPOINT) return process.env.AI_OTEL_LOGS_ENDPOINT;
|
|
36
|
+
try {
|
|
37
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(__dirname, "endpoint.json"), "utf8"));
|
|
38
|
+
if (cfg && cfg.logsEndpoint) return cfg.logsEndpoint;
|
|
39
|
+
} catch (_) { /* 文件不存在/解析失败:继续走 localhost */ }
|
|
40
|
+
return "http://localhost:4318/v1/logs";
|
|
32
41
|
}
|
|
33
42
|
|
|
34
43
|
(async () => {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
const { execFileSync } = require("child_process");
|
|
5
5
|
const os = require("os");
|
|
6
6
|
const path = require("path");
|
|
7
|
+
const fs = require("fs");
|
|
7
8
|
const http = require("http");
|
|
8
9
|
const https = require("https");
|
|
9
10
|
const { URL } = require("url");
|
|
@@ -16,8 +17,19 @@ function safeGit(args) {
|
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
// 解析 OTLP/HTTP logs endpoint。优先级:env 覆盖 → installer 写在 hook 同目录的
|
|
21
|
+
// endpoint.json → localhost 兜底。v1.0.4 起 hook 自己读 endpoint.json,避免依赖
|
|
22
|
+
// shell 前缀注入 env 那种 POSIX-only 写法(cmd.exe 不认)。
|
|
19
23
|
function endpoint() {
|
|
20
|
-
|
|
24
|
+
let base = process.env.GEMINI_TELEMETRY_OTLP_ENDPOINT;
|
|
25
|
+
if (!base) {
|
|
26
|
+
try {
|
|
27
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(__dirname, "endpoint.json"), "utf8"));
|
|
28
|
+
if (cfg && cfg.logsEndpoint) return cfg.logsEndpoint;
|
|
29
|
+
if (cfg && cfg.endpoint) base = cfg.endpoint;
|
|
30
|
+
} catch (_) { /* 文件不存在/解析失败:继续走 localhost */ }
|
|
31
|
+
}
|
|
32
|
+
if (!base) base = "http://localhost:4317";
|
|
21
33
|
const url = new URL(base);
|
|
22
34
|
if (url.port === "4317") url.port = "4318";
|
|
23
35
|
if (!url.pathname || url.pathname === "/") url.pathname = "/v1/logs";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// 跨平台 hook 启动器:优先 PATH 上的 node(用户升级 Node 时自动跟新版本),
|
|
5
|
+
// 找不到再用安装时这台机器上 node 的绝对路径(即当前进程的 execPath,一定可用)。
|
|
6
|
+
// 改用 JS 内部兜底替代 shell `||` 操作符链——PowerShell 5.1(Win10/11 默认 shell)
|
|
7
|
+
// 不支持 `||`,cc/gemini 在 Windows 上默认走 PowerShell,会被坑。统一在 JS 里兜底
|
|
8
|
+
// 后,命令字符串变成纯 `<node> <launcher> <hook>` 三段绝对路径调用,对 shell 透明,
|
|
9
|
+
// POSIX sh / cmd.exe / PowerShell 5.1 / PowerShell 7 全 cover。
|
|
10
|
+
//
|
|
11
|
+
// stdio: "inherit":stdin(Codex/CC 传 hook payload JSON)、stderr 都直通给 hook
|
|
12
|
+
// 子进程,hook 那边的 readStdin / process.stderr 行为不受影响。退出码原样转发。
|
|
13
|
+
|
|
14
|
+
const { spawnSync, execFileSync } = require("child_process");
|
|
15
|
+
|
|
16
|
+
const scriptPath = process.argv[2];
|
|
17
|
+
if (!scriptPath) process.exit(0);
|
|
18
|
+
|
|
19
|
+
let nodeBin = process.execPath;
|
|
20
|
+
try {
|
|
21
|
+
// -v 只打版本号立即退出,用来探 PATH 上是否有可执行 node。
|
|
22
|
+
// timeout 防 PATH 上的 "node" 是个会卡住的 wrapper(极少见但存在)。
|
|
23
|
+
execFileSync("node", ["-v"], { stdio: "ignore", timeout: 1500 });
|
|
24
|
+
nodeBin = "node";
|
|
25
|
+
} catch (_) {
|
|
26
|
+
// PATH 上没 node 或探测失败 → 沿用当前进程 execPath(即 installer 焊死的那条
|
|
27
|
+
// 绝对路径)。注意此时如果用户连 baked 那个版本也卸了,那 launcher 自己根本就
|
|
28
|
+
// 启动不起来,根本走不到这里——也就是说 hook 真挂的时候用户会感知到,符合
|
|
29
|
+
// 不静默吞错的预期。
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const r = spawnSync(nodeBin, [scriptPath], { stdio: "inherit" });
|
|
33
|
+
// status 为 null 表示进程被信号杀掉(SIGTERM 等),按 1 处理
|
|
34
|
+
process.exit(r.status === null ? 1 : r.status);
|