@codetelemetry/connect 0.1.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.
- package/LICENSE +21 -0
- package/README.md +26 -0
- package/dist/bin/connect.d.ts +2 -0
- package/dist/bin/connect.js +6 -0
- package/dist/src/cli.d.ts +10 -0
- package/dist/src/cli.js +172 -0
- package/dist/src/codexConfig.d.ts +23 -0
- package/dist/src/codexConfig.js +265 -0
- package/dist/src/config.d.ts +26 -0
- package/dist/src/config.js +32 -0
- package/dist/src/connectivity.d.ts +27 -0
- package/dist/src/connectivity.js +54 -0
- package/dist/src/deviceClient.d.ts +32 -0
- package/dist/src/deviceClient.js +112 -0
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.js +8 -0
- package/dist/src/keys.d.ts +5 -0
- package/dist/src/keys.js +24 -0
- package/dist/src/onboard.d.ts +47 -0
- package/dist/src/onboard.js +84 -0
- package/dist/src/otlp.d.ts +18 -0
- package/dist/src/otlp.js +60 -0
- package/dist/src/repositoryHook.d.ts +17 -0
- package/dist/src/repositoryHook.js +631 -0
- package/dist/src/settings.d.ts +28 -0
- package/dist/src/settings.js +97 -0
- package/package.json +25 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Elliot Gardiner
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# @codetelemetry/connect
|
|
2
|
+
|
|
3
|
+
The public-safe CodeTelemetry onboarding CLI for Claude Code and Codex. This
|
|
4
|
+
package contains only local client configuration, device authentication, a test
|
|
5
|
+
event, and connectivity verification. CodeTelemetry application, storage,
|
|
6
|
+
planning, host-capacity, dispatch, and infrastructure code are not included.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npx -y @codetelemetry/connect login
|
|
10
|
+
npx -y @codetelemetry/connect login --codex
|
|
11
|
+
npx -y @codetelemetry/connect doctor
|
|
12
|
+
npx -y @codetelemetry/connect print-config
|
|
13
|
+
npx -y @codetelemetry/connect --key-env CODETELEMETRY_INGEST_KEY
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
If you already have an ingest key, `npx -y @codetelemetry/connect <cot_key>` is
|
|
17
|
+
supported for compatibility, but command-line arguments can be visible to other
|
|
18
|
+
local processes. Device login or `--key-env` is preferred. The key is never echoed and tool
|
|
19
|
+
results contain only a masked preview and hash.
|
|
20
|
+
|
|
21
|
+
Content telemetry (`--enrich`) and repository-path observation
|
|
22
|
+
(`--repository-observation`) are independent explicit opt-ins. The CLI uses only
|
|
23
|
+
CodeTelemetry production endpoints and recognized Claude Code/Codex config paths;
|
|
24
|
+
arbitrary endpoint and filesystem overrides are not part of its public CLI.
|
|
25
|
+
|
|
26
|
+
Requires Node.js 22.6 or newer.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Endpoints } from "./config.ts";
|
|
2
|
+
import { type FetchLike } from "./otlp.ts";
|
|
3
|
+
type RunDeps = {
|
|
4
|
+
endpointsOverride?: Partial<Endpoints>;
|
|
5
|
+
fetchImpl?: FetchLike;
|
|
6
|
+
openBrowser?: (url: string) => boolean;
|
|
7
|
+
env?: NodeJS.ProcessEnv;
|
|
8
|
+
};
|
|
9
|
+
export declare function run(argv: string[], deps?: RunDeps): Promise<number>;
|
|
10
|
+
export {};
|
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { onboard } from "./onboard.js";
|
|
4
|
+
import { resolveEndpoints, resolveSettingsPath } from "./config.js";
|
|
5
|
+
import { buildEnvBlock } from "./settings.js";
|
|
6
|
+
import { buildOtelSection, readCodexKey, resolveCodexConfigPath } from "./codexConfig.js";
|
|
7
|
+
import { looksLikeKey, mask } from "./keys.js";
|
|
8
|
+
import { requestDeviceCode, pollForKey } from "./deviceClient.js";
|
|
9
|
+
import { sendTestEvent } from "./otlp.js";
|
|
10
|
+
import { confirmLanded } from "./connectivity.js";
|
|
11
|
+
const VALUE_FLAGS = new Set(["scope", "key-env"]);
|
|
12
|
+
const BOOL_FLAGS = new Set(["codex", "enrich", "repository-observation", "open", "test", "verify", "json", "help", "h"]);
|
|
13
|
+
function parse(argv) {
|
|
14
|
+
const positionals = [];
|
|
15
|
+
const flags = {};
|
|
16
|
+
for (let i = 0; i < argv.length; i++) {
|
|
17
|
+
const arg = argv[i];
|
|
18
|
+
if (!arg.startsWith("--") && arg !== "-h") {
|
|
19
|
+
positionals.push(arg);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const body = arg === "-h" ? "h" : arg.slice(2);
|
|
23
|
+
const negative = body.startsWith("no-");
|
|
24
|
+
const name = negative ? body.slice(3) : body.split("=", 1)[0];
|
|
25
|
+
if (!BOOL_FLAGS.has(name) && !VALUE_FLAGS.has(name))
|
|
26
|
+
return `Unknown option --${name}.`;
|
|
27
|
+
if (VALUE_FLAGS.has(name)) {
|
|
28
|
+
if (negative)
|
|
29
|
+
return `--no-${name} is not valid.`;
|
|
30
|
+
const value = body.includes("=") ? body.slice(body.indexOf("=") + 1) : argv[++i];
|
|
31
|
+
if (!value || value.startsWith("--"))
|
|
32
|
+
return `--${name} needs a value.`;
|
|
33
|
+
flags[name] = value;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
if (body.includes("="))
|
|
37
|
+
return `--${name} takes no value.`;
|
|
38
|
+
flags[name] = !negative;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { positionals, flags };
|
|
42
|
+
}
|
|
43
|
+
const HELP = `codetelemetry-connect — connect Claude Code or Codex to CodeTelemetry
|
|
44
|
+
|
|
45
|
+
Usage:
|
|
46
|
+
codetelemetry-connect login [options]
|
|
47
|
+
codetelemetry-connect <cot_key> [options]
|
|
48
|
+
codetelemetry-connect doctor [options]
|
|
49
|
+
codetelemetry-connect print-config [options]
|
|
50
|
+
codetelemetry-connect --key-env <NAME> [options]
|
|
51
|
+
|
|
52
|
+
Options:
|
|
53
|
+
--codex Configure Codex instead of Claude Code
|
|
54
|
+
--scope user|project Claude Code config scope (default: user)
|
|
55
|
+
--enrich Opt in to prompt/body/tool-content telemetry
|
|
56
|
+
--repository-observation Opt in to repository-path observation hooks
|
|
57
|
+
--key-env <NAME> Read an ingest key from an environment variable
|
|
58
|
+
--no-open Do not open the device-approval page
|
|
59
|
+
--no-test Do not send a test event
|
|
60
|
+
--no-verify Do not wait for the test event to land
|
|
61
|
+
--json Print a machine-readable result
|
|
62
|
+
-h, --help Show this help`;
|
|
63
|
+
const out = (line) => process.stdout.write(`${line}\n`);
|
|
64
|
+
function defaultOpenBrowser(url) {
|
|
65
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
66
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
67
|
+
try {
|
|
68
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
69
|
+
child.on("error", () => { });
|
|
70
|
+
child.unref();
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function readClaudeKey(settingsPath) {
|
|
78
|
+
if (!fs.existsSync(settingsPath))
|
|
79
|
+
return null;
|
|
80
|
+
try {
|
|
81
|
+
const value = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
|
|
82
|
+
const headers = value?.env?.OTEL_EXPORTER_OTLP_HEADERS;
|
|
83
|
+
return typeof headers === "string" ? headers.match(/Bearer\s+(cot_[A-Za-z0-9_-]+)/)?.[1] ?? null : null;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export async function run(argv, deps = {}) {
|
|
90
|
+
const parsed = parse(argv);
|
|
91
|
+
if (typeof parsed === "string") {
|
|
92
|
+
out(parsed);
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
const { positionals, flags } = parsed;
|
|
96
|
+
const command = positionals[0];
|
|
97
|
+
if (positionals.length > 1) {
|
|
98
|
+
out(`Unexpected argument: ${positionals[1]}`);
|
|
99
|
+
return 1;
|
|
100
|
+
}
|
|
101
|
+
if (flags.help || flags.h || command === "help") {
|
|
102
|
+
out(HELP);
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
if (!command && !flags["key-env"]) {
|
|
106
|
+
out(HELP);
|
|
107
|
+
return 1;
|
|
108
|
+
}
|
|
109
|
+
const scope = flags.scope ?? "user";
|
|
110
|
+
if (scope !== "user" && scope !== "project") {
|
|
111
|
+
out("--scope must be user or project.");
|
|
112
|
+
return 1;
|
|
113
|
+
}
|
|
114
|
+
const client = flags.codex ? "codex" : "claude";
|
|
115
|
+
const endpoints = resolveEndpoints(deps.endpointsOverride);
|
|
116
|
+
const configPath = client === "codex" ? resolveCodexConfigPath() : resolveSettingsPath({ scope: scope });
|
|
117
|
+
const common = {
|
|
118
|
+
client,
|
|
119
|
+
endpoints,
|
|
120
|
+
settings: { scope: scope, enrich: flags.enrich === true, repositoryObservation: flags["repository-observation"] === true },
|
|
121
|
+
sendTest: flags.test !== false,
|
|
122
|
+
verify: flags.verify !== false,
|
|
123
|
+
fetchImpl: deps.fetchImpl,
|
|
124
|
+
logger: flags.json ? undefined : out,
|
|
125
|
+
};
|
|
126
|
+
if (command === "print-config") {
|
|
127
|
+
out(client === "codex"
|
|
128
|
+
? buildOtelSection("cot_<YOUR_INGEST_KEY>", endpoints.ingestEndpoint, flags.enrich === true)
|
|
129
|
+
: JSON.stringify({ env: buildEnvBlock("<YOUR_INGEST_KEY>", endpoints.ingestEndpoint, flags.enrich === true) }, null, 2));
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
if (command === "doctor") {
|
|
133
|
+
const key = client === "codex" ? readCodexKey(configPath) : readClaudeKey(configPath);
|
|
134
|
+
if (!key) {
|
|
135
|
+
out(`No active CodeTelemetry key found in ${configPath}.`);
|
|
136
|
+
return 1;
|
|
137
|
+
}
|
|
138
|
+
if (!flags.json)
|
|
139
|
+
out(`Re-testing ${mask(key)} → ${endpoints.ingestEndpoint}`);
|
|
140
|
+
const test = await sendTestEvent({ key, ingestEndpoint: endpoints.ingestEndpoint, fetchImpl: deps.fetchImpl });
|
|
141
|
+
if (!test.ok)
|
|
142
|
+
return 1;
|
|
143
|
+
const verified = await confirmLanded({ key, connectivityUrl: endpoints.connectivityUrl, probeId: test.probeId, fetchImpl: deps.fetchImpl });
|
|
144
|
+
if (flags.json)
|
|
145
|
+
out(JSON.stringify({ ok: verified.landed, probeId: test.probeId, elapsedMs: verified.elapsedMs }));
|
|
146
|
+
else
|
|
147
|
+
out(verified.landed ? `✓ Telemetry landed in ${verified.elapsedMs}ms` : "Probe not confirmed within timeout.");
|
|
148
|
+
return verified.landed ? 0 : 1;
|
|
149
|
+
}
|
|
150
|
+
if (command === "login") {
|
|
151
|
+
const code = await requestDeviceCode(endpoints, deps.fetchImpl);
|
|
152
|
+
const url = `${code.verificationUri}?code=${encodeURIComponent(code.userCode)}`;
|
|
153
|
+
out(`Approve CodeTelemetry access: ${url}`);
|
|
154
|
+
out(`Code: ${code.userCode}`);
|
|
155
|
+
if (flags.open !== false && !flags.json)
|
|
156
|
+
(deps.openBrowser ?? defaultOpenBrowser)(url);
|
|
157
|
+
const result = await onboard({ ...common, getKey: () => pollForKey(endpoints, code.deviceCode, { intervalMs: code.interval * 1000, timeoutMs: code.expiresIn * 1000, fetchImpl: deps.fetchImpl }) });
|
|
158
|
+
if (flags.json)
|
|
159
|
+
out(JSON.stringify(result, null, 2));
|
|
160
|
+
return result.ok ? 0 : 1;
|
|
161
|
+
}
|
|
162
|
+
const fromEnv = typeof flags["key-env"] === "string" ? (deps.env ?? process.env)[flags["key-env"]]?.trim() : undefined;
|
|
163
|
+
const key = fromEnv ?? command;
|
|
164
|
+
if (!looksLikeKey(key)) {
|
|
165
|
+
out(fromEnv === undefined && flags["key-env"] ? `No valid CodeTelemetry key found in $${String(flags["key-env"])}.` : HELP);
|
|
166
|
+
return 1;
|
|
167
|
+
}
|
|
168
|
+
const result = await onboard({ ...common, key });
|
|
169
|
+
if (flags.json)
|
|
170
|
+
out(JSON.stringify(result, null, 2));
|
|
171
|
+
return result.ok ? 0 : 1;
|
|
172
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { WriteResult } from "./settings.ts";
|
|
2
|
+
export declare function resolveCodexConfigPath(opts?: {
|
|
3
|
+
path?: string;
|
|
4
|
+
home?: string;
|
|
5
|
+
codexHome?: string;
|
|
6
|
+
}): string;
|
|
7
|
+
export declare function buildOtelSection(key: string, ingestEndpoint: string, enrich: boolean): string;
|
|
8
|
+
export declare function mergeOtelBody(body: string[], key: string, ingestEndpoint: string, enrich: boolean): string[];
|
|
9
|
+
export type CodexWriteOptions = {
|
|
10
|
+
key: string;
|
|
11
|
+
ingestEndpoint: string;
|
|
12
|
+
configPath: string;
|
|
13
|
+
enrich?: boolean;
|
|
14
|
+
repositoryObservation?: boolean;
|
|
15
|
+
backup?: boolean;
|
|
16
|
+
};
|
|
17
|
+
export declare function writeCodexConfig(opts: CodexWriteOptions): WriteResult;
|
|
18
|
+
export declare function readCodexKey(configPath: string): string | null;
|
|
19
|
+
export declare function readCodexSummary(configPath: string): {
|
|
20
|
+
exists: boolean;
|
|
21
|
+
otelKeys: string[];
|
|
22
|
+
hasKey: boolean;
|
|
23
|
+
};
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { assertKey } from "./keys.js";
|
|
5
|
+
import { installCodexRepositoryObservationHook, removeCodexRepositoryObservationHook, validateCodexRepositoryObservationHook, } from "./repositoryHook.js";
|
|
6
|
+
export function resolveCodexConfigPath(opts = {}) {
|
|
7
|
+
if (opts.path)
|
|
8
|
+
return path.resolve(opts.path);
|
|
9
|
+
const codexHome = opts.codexHome ?? process.env.CODEX_HOME;
|
|
10
|
+
if (codexHome)
|
|
11
|
+
return path.resolve(codexHome, "config.toml");
|
|
12
|
+
return path.join(opts.home ?? os.homedir(), ".codex", "config.toml");
|
|
13
|
+
}
|
|
14
|
+
function exporterLine(kind, signal, key, ingestEndpoint) {
|
|
15
|
+
return `${kind} = { otlp-http = { endpoint = "${ingestEndpoint}/v1/${signal}", protocol = "binary", headers = { authorization = "Bearer ${key}" } } }`;
|
|
16
|
+
}
|
|
17
|
+
const OWNED_KEYS = ["exporter", "trace_exporter", "metrics_exporter"];
|
|
18
|
+
const COMMENT_RE = /^\s*#/;
|
|
19
|
+
function keyOf(line) {
|
|
20
|
+
if (COMMENT_RE.test(line))
|
|
21
|
+
return null;
|
|
22
|
+
const eq = line.indexOf("=");
|
|
23
|
+
if (eq === -1)
|
|
24
|
+
return null;
|
|
25
|
+
const key = line.slice(0, eq).trim().replace(/\s*\.\s*/g, ".");
|
|
26
|
+
return key && /^[A-Za-z0-9_.\-"']+$/.test(key) ? key : null;
|
|
27
|
+
}
|
|
28
|
+
function rootKey(key) {
|
|
29
|
+
const q = key[0];
|
|
30
|
+
if (q === '"' || q === "'") {
|
|
31
|
+
const end = key.indexOf(q, 1);
|
|
32
|
+
if (end !== -1)
|
|
33
|
+
return key.slice(1, end);
|
|
34
|
+
}
|
|
35
|
+
return key.split(".")[0];
|
|
36
|
+
}
|
|
37
|
+
function ownedLines(key, ingestEndpoint, enrich) {
|
|
38
|
+
const lines = [];
|
|
39
|
+
if (enrich)
|
|
40
|
+
lines.push("log_user_prompt = true");
|
|
41
|
+
lines.push(exporterLine("exporter", "logs", key, ingestEndpoint), exporterLine("trace_exporter", "traces", key, ingestEndpoint), exporterLine("metrics_exporter", "metrics", key, ingestEndpoint));
|
|
42
|
+
return lines;
|
|
43
|
+
}
|
|
44
|
+
export function buildOtelSection(key, ingestEndpoint, enrich) {
|
|
45
|
+
return ["[otel]", ...ownedLines(key, ingestEndpoint, enrich)].join("\n");
|
|
46
|
+
}
|
|
47
|
+
export function mergeOtelBody(body, key, ingestEndpoint, enrich) {
|
|
48
|
+
const replaced = new Set(OWNED_KEYS);
|
|
49
|
+
if (enrich)
|
|
50
|
+
replaced.add("log_user_prompt");
|
|
51
|
+
const kept = body.filter((l) => {
|
|
52
|
+
const k = keyOf(l);
|
|
53
|
+
return k === null || !replaced.has(rootKey(k));
|
|
54
|
+
});
|
|
55
|
+
while (kept.length && kept[kept.length - 1].trim() === "")
|
|
56
|
+
kept.pop();
|
|
57
|
+
return [...kept, ...ownedLines(key, ingestEndpoint, enrich)];
|
|
58
|
+
}
|
|
59
|
+
function sectionKeys(enrich) {
|
|
60
|
+
return enrich ? ["log_user_prompt", ...OWNED_KEYS] : [...OWNED_KEYS];
|
|
61
|
+
}
|
|
62
|
+
function headerName(line) {
|
|
63
|
+
const text = stripComment(line).trim();
|
|
64
|
+
if (!text.startsWith("["))
|
|
65
|
+
return null;
|
|
66
|
+
const arrayTable = text.startsWith("[[");
|
|
67
|
+
const closer = arrayTable ? "]]" : "]";
|
|
68
|
+
if (!text.endsWith(closer) || text.length <= (arrayTable ? 4 : 2))
|
|
69
|
+
return null;
|
|
70
|
+
const inner = text.slice(arrayTable ? 2 : 1, text.length - closer.length);
|
|
71
|
+
const segments = [];
|
|
72
|
+
let current = "";
|
|
73
|
+
let quote = null;
|
|
74
|
+
for (let i = 0; i < inner.length; i++) {
|
|
75
|
+
const ch = inner[i];
|
|
76
|
+
if (quote) {
|
|
77
|
+
if (ch === "\\" && quote === '"')
|
|
78
|
+
current += inner[++i] ?? "";
|
|
79
|
+
else if (ch === quote)
|
|
80
|
+
quote = null;
|
|
81
|
+
else
|
|
82
|
+
current += ch;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (ch === '"' || ch === "'")
|
|
86
|
+
quote = ch;
|
|
87
|
+
else if (ch === ".") {
|
|
88
|
+
segments.push(current.trim());
|
|
89
|
+
current = "";
|
|
90
|
+
}
|
|
91
|
+
else if (ch === "[" || ch === "]" || ch === ",")
|
|
92
|
+
return null;
|
|
93
|
+
else
|
|
94
|
+
current += ch;
|
|
95
|
+
}
|
|
96
|
+
if (quote)
|
|
97
|
+
return null;
|
|
98
|
+
segments.push(current.trim());
|
|
99
|
+
return segments.some((s) => s === "") ? null : segments.join(".");
|
|
100
|
+
}
|
|
101
|
+
const isHeader = (line) => headerName(line) !== null;
|
|
102
|
+
const isOtelHeader = (line) => headerName(line) === "otel";
|
|
103
|
+
const isOtelSubtable = (line) => (headerName(line) ?? "").startsWith("otel.");
|
|
104
|
+
const MULTILINE_STRING_RE = /"""|'''/;
|
|
105
|
+
function structuralOnly(line) {
|
|
106
|
+
let out = "";
|
|
107
|
+
let quote = null;
|
|
108
|
+
for (let i = 0; i < line.length; i++) {
|
|
109
|
+
const ch = line[i];
|
|
110
|
+
if (quote) {
|
|
111
|
+
if (ch === "\\" && quote === '"')
|
|
112
|
+
i++;
|
|
113
|
+
else if (ch === quote)
|
|
114
|
+
quote = null;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (ch === '"' || ch === "'")
|
|
118
|
+
quote = ch;
|
|
119
|
+
else if (ch === "#")
|
|
120
|
+
break;
|
|
121
|
+
else
|
|
122
|
+
out += ch;
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
function depthDelta(line) {
|
|
127
|
+
let d = 0;
|
|
128
|
+
for (const ch of structuralOnly(line)) {
|
|
129
|
+
if (ch === "[" || ch === "{")
|
|
130
|
+
d++;
|
|
131
|
+
else if (ch === "]" || ch === "}")
|
|
132
|
+
d--;
|
|
133
|
+
}
|
|
134
|
+
return d;
|
|
135
|
+
}
|
|
136
|
+
function findOtelSection(lines) {
|
|
137
|
+
let depth = 0;
|
|
138
|
+
let start = -1;
|
|
139
|
+
for (let i = 0; i < lines.length; i++) {
|
|
140
|
+
if (depth === 0) {
|
|
141
|
+
if (start === -1) {
|
|
142
|
+
if (isOtelHeader(lines[i]))
|
|
143
|
+
start = i;
|
|
144
|
+
}
|
|
145
|
+
else if (i > start && isHeader(lines[i])) {
|
|
146
|
+
return { start, end: i };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
depth = Math.max(0, depth + depthDelta(lines[i]));
|
|
150
|
+
}
|
|
151
|
+
return start === -1 ? null : { start, end: lines.length };
|
|
152
|
+
}
|
|
153
|
+
function hasOtelSubtable(lines) {
|
|
154
|
+
let depth = 0;
|
|
155
|
+
for (const line of lines) {
|
|
156
|
+
if (depth === 0 && isOtelSubtable(line))
|
|
157
|
+
return true;
|
|
158
|
+
depth = Math.max(0, depth + depthDelta(line));
|
|
159
|
+
}
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
function splitLines(raw) {
|
|
163
|
+
const crlf = (raw.match(/\r\n/g) ?? []).length;
|
|
164
|
+
const lf = (raw.match(/\n/g) ?? []).length - crlf;
|
|
165
|
+
return { lines: raw.split(/\r?\n/), eol: crlf > lf ? "\r\n" : "\n" };
|
|
166
|
+
}
|
|
167
|
+
export function writeCodexConfig(opts) {
|
|
168
|
+
const key = assertKey(opts.key);
|
|
169
|
+
const filePath = path.resolve(opts.configPath);
|
|
170
|
+
const existed = fs.existsSync(filePath);
|
|
171
|
+
const raw = existed ? fs.readFileSync(filePath, "utf8") : "";
|
|
172
|
+
const { lines, eol } = splitLines(raw);
|
|
173
|
+
if (lines.some((l) => MULTILINE_STRING_RE.test(l))) {
|
|
174
|
+
throw new Error(`refusing to edit ${filePath}: it uses a multi-line TOML string (""" or '''), whose contents ` +
|
|
175
|
+
`this writer can't tell apart from real section headers. Configure the [otel] section by hand ` +
|
|
176
|
+
`(https://codetelemetry.com/docs/data-sources/openai-codex), or remove the multi-line string and retry.`);
|
|
177
|
+
}
|
|
178
|
+
if (hasOtelSubtable(lines)) {
|
|
179
|
+
throw new Error(`refusing to edit ${filePath}: it defines [otel.*] sub-tables this writer can't safely merge. ` +
|
|
180
|
+
`Remove or inline them, then retry.`);
|
|
181
|
+
}
|
|
182
|
+
if (opts.repositoryObservation)
|
|
183
|
+
validateCodexRepositoryObservationHook(filePath);
|
|
184
|
+
else
|
|
185
|
+
removeCodexRepositoryObservationHook(filePath);
|
|
186
|
+
const section = findOtelSection(lines);
|
|
187
|
+
let out;
|
|
188
|
+
if (section === null) {
|
|
189
|
+
const fresh = buildOtelSection(key, opts.ingestEndpoint, !!opts.enrich).split("\n");
|
|
190
|
+
const body = lines.length && lines[lines.length - 1] === "" ? lines.slice(0, -1) : lines;
|
|
191
|
+
out = body.length ? [...body, "", ...fresh] : [...fresh];
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
const { start, end } = section;
|
|
195
|
+
const merged = mergeOtelBody(lines.slice(start + 1, end), key, opts.ingestEndpoint, !!opts.enrich);
|
|
196
|
+
out = [...lines.slice(0, start), lines[start], ...merged, ...lines.slice(end)];
|
|
197
|
+
}
|
|
198
|
+
const next = out.join(eol).replace(/(\r?\n)+$/, "") + eol;
|
|
199
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
200
|
+
let backupPath = null;
|
|
201
|
+
if (existed && opts.backup !== false) {
|
|
202
|
+
backupPath = `${filePath}.bak`;
|
|
203
|
+
fs.copyFileSync(filePath, backupPath);
|
|
204
|
+
}
|
|
205
|
+
fs.writeFileSync(filePath, next, { mode: 0o600 });
|
|
206
|
+
try {
|
|
207
|
+
fs.chmodSync(filePath, 0o600);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
}
|
|
211
|
+
if (opts.repositoryObservation)
|
|
212
|
+
installCodexRepositoryObservationHook(filePath);
|
|
213
|
+
return { path: filePath, wrote: sectionKeys(!!opts.enrich), backupPath, created: !existed };
|
|
214
|
+
}
|
|
215
|
+
function otelSectionLines(raw) {
|
|
216
|
+
const { lines } = splitLines(raw);
|
|
217
|
+
if (lines.some((l) => MULTILINE_STRING_RE.test(l)))
|
|
218
|
+
return [];
|
|
219
|
+
const section = findOtelSection(lines);
|
|
220
|
+
return section === null ? [] : lines.slice(section.start + 1, section.end);
|
|
221
|
+
}
|
|
222
|
+
function activeOtelLines(raw) {
|
|
223
|
+
return otelSectionLines(raw).filter((l) => keyOf(l) !== null);
|
|
224
|
+
}
|
|
225
|
+
function stripComment(line) {
|
|
226
|
+
let quote = null;
|
|
227
|
+
for (let i = 0; i < line.length; i++) {
|
|
228
|
+
const ch = line[i];
|
|
229
|
+
if (quote) {
|
|
230
|
+
if (ch === "\\" && quote === '"')
|
|
231
|
+
i++;
|
|
232
|
+
else if (ch === quote)
|
|
233
|
+
quote = null;
|
|
234
|
+
}
|
|
235
|
+
else if (ch === '"' || ch === "'")
|
|
236
|
+
quote = ch;
|
|
237
|
+
else if (ch === "#")
|
|
238
|
+
return line.slice(0, i);
|
|
239
|
+
}
|
|
240
|
+
return line;
|
|
241
|
+
}
|
|
242
|
+
function exporterKeyText(raw) {
|
|
243
|
+
return activeOtelLines(raw)
|
|
244
|
+
.filter((l) => {
|
|
245
|
+
const k = keyOf(l);
|
|
246
|
+
return k !== null && OWNED_KEYS.includes(rootKey(k));
|
|
247
|
+
})
|
|
248
|
+
.map(stripComment)
|
|
249
|
+
.join("\n");
|
|
250
|
+
}
|
|
251
|
+
export function readCodexKey(configPath) {
|
|
252
|
+
if (!fs.existsSync(configPath))
|
|
253
|
+
return null;
|
|
254
|
+
const m = exporterKeyText(fs.readFileSync(configPath, "utf8")).match(/Bearer\s+(cot_[A-Za-z0-9_-]+)/);
|
|
255
|
+
return m ? m[1] : null;
|
|
256
|
+
}
|
|
257
|
+
export function readCodexSummary(configPath) {
|
|
258
|
+
if (!fs.existsSync(configPath))
|
|
259
|
+
return { exists: false, otelKeys: [], hasKey: false };
|
|
260
|
+
const raw = fs.readFileSync(configPath, "utf8");
|
|
261
|
+
const otelKeys = activeOtelLines(raw)
|
|
262
|
+
.map((l) => keyOf(l))
|
|
263
|
+
.filter((k) => k !== null);
|
|
264
|
+
return { exists: true, otelKeys, hasKey: /Bearer\s+cot_/.test(exporterKeyText(raw)) };
|
|
265
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type Endpoints = {
|
|
2
|
+
apiBase: string;
|
|
3
|
+
ingestEndpoint: string;
|
|
4
|
+
deviceCodeUrl: string;
|
|
5
|
+
deviceApproveUrl: string;
|
|
6
|
+
deviceTokenUrl: string;
|
|
7
|
+
connectivityUrl: string;
|
|
8
|
+
verificationUri: string;
|
|
9
|
+
agentCostUrl: string;
|
|
10
|
+
agentSessionPerformanceUrl: string;
|
|
11
|
+
agentSimilarErrorsUrl: string;
|
|
12
|
+
agentSessionSearchUrl: string;
|
|
13
|
+
agentTaskSizeUrl: string;
|
|
14
|
+
agentLinearIssueSizeUrl: string;
|
|
15
|
+
};
|
|
16
|
+
export declare function resolveEndpoints(overrides?: Partial<{
|
|
17
|
+
apiBase: string;
|
|
18
|
+
ingestEndpoint: string;
|
|
19
|
+
}>): Endpoints;
|
|
20
|
+
export type SettingsScope = "user" | "project";
|
|
21
|
+
export declare function resolveSettingsPath(opts?: {
|
|
22
|
+
scope?: SettingsScope;
|
|
23
|
+
path?: string;
|
|
24
|
+
cwd?: string;
|
|
25
|
+
home?: string;
|
|
26
|
+
}): string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const DEFAULT_API_BASE = "https://codetelemetry.com";
|
|
4
|
+
const DEFAULT_INGEST = "https://ingest.codetelemetry.com";
|
|
5
|
+
export function resolveEndpoints(overrides = {}) {
|
|
6
|
+
const apiBase = (overrides.apiBase || process.env.CODETELEMETRY_API_BASE || DEFAULT_API_BASE).replace(/\/$/, "");
|
|
7
|
+
const ingestEndpoint = (overrides.ingestEndpoint || process.env.CODETELEMETRY_INGEST_ENDPOINT || DEFAULT_INGEST).replace(/\/$/, "");
|
|
8
|
+
return {
|
|
9
|
+
apiBase,
|
|
10
|
+
ingestEndpoint,
|
|
11
|
+
deviceCodeUrl: `${apiBase}/api/device/code`,
|
|
12
|
+
deviceApproveUrl: `${apiBase}/api/device/approve`,
|
|
13
|
+
deviceTokenUrl: `${apiBase}/api/device/token`,
|
|
14
|
+
connectivityUrl: `${apiBase}/api/connectivity/check`,
|
|
15
|
+
verificationUri: `${apiBase}/activate`,
|
|
16
|
+
agentCostUrl: `${apiBase}/api/agent/cost`,
|
|
17
|
+
agentSessionPerformanceUrl: `${apiBase}/api/agent/session-performance`,
|
|
18
|
+
agentSimilarErrorsUrl: `${apiBase}/api/agent/similar-errors`,
|
|
19
|
+
agentSessionSearchUrl: `${apiBase}/api/agent/session-search`,
|
|
20
|
+
agentTaskSizeUrl: `${apiBase}/api/agent/task-size`,
|
|
21
|
+
agentLinearIssueSizeUrl: `${apiBase}/api/agent/linear-issue-size`,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function resolveSettingsPath(opts = {}) {
|
|
25
|
+
if (opts.path)
|
|
26
|
+
return path.resolve(opts.path);
|
|
27
|
+
const scope = opts.scope ?? "user";
|
|
28
|
+
if (scope === "project") {
|
|
29
|
+
return path.join(opts.cwd ?? process.cwd(), ".claude", "settings.json");
|
|
30
|
+
}
|
|
31
|
+
return path.join(opts.home ?? os.homedir(), ".claude", "settings.json");
|
|
32
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { FetchLike } from "./otlp.ts";
|
|
2
|
+
export type CheckResult = {
|
|
3
|
+
seen: boolean;
|
|
4
|
+
httpStatus: number;
|
|
5
|
+
};
|
|
6
|
+
export declare function checkOnce(opts: {
|
|
7
|
+
key: string;
|
|
8
|
+
connectivityUrl: string;
|
|
9
|
+
probeId: string;
|
|
10
|
+
fetchImpl?: FetchLike;
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
}): Promise<CheckResult>;
|
|
13
|
+
export type ConfirmResult = {
|
|
14
|
+
landed: boolean;
|
|
15
|
+
elapsedMs: number;
|
|
16
|
+
polls: number;
|
|
17
|
+
};
|
|
18
|
+
export declare function confirmLanded(opts: {
|
|
19
|
+
key: string;
|
|
20
|
+
connectivityUrl: string;
|
|
21
|
+
probeId: string;
|
|
22
|
+
fetchImpl?: FetchLike;
|
|
23
|
+
intervalMs?: number;
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
onTick?: (poll: number) => void;
|
|
26
|
+
now?: () => number;
|
|
27
|
+
}): Promise<ConfirmResult>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { assertKey } from "./keys.js";
|
|
2
|
+
export async function checkOnce(opts) {
|
|
3
|
+
const key = assertKey(opts.key);
|
|
4
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
5
|
+
const ac = new AbortController();
|
|
6
|
+
const timer = setTimeout(() => ac.abort(), opts.timeoutMs ?? 8_000);
|
|
7
|
+
try {
|
|
8
|
+
const res = await fetchImpl(opts.connectivityUrl, {
|
|
9
|
+
method: "POST",
|
|
10
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${key}` },
|
|
11
|
+
body: JSON.stringify({ probeId: opts.probeId }),
|
|
12
|
+
signal: ac.signal,
|
|
13
|
+
});
|
|
14
|
+
let seen = false;
|
|
15
|
+
try {
|
|
16
|
+
const json = (await res.json());
|
|
17
|
+
seen = !!json?.seen;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
}
|
|
21
|
+
return { seen, httpStatus: res.status };
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
clearTimeout(timer);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
28
|
+
export async function confirmLanded(opts) {
|
|
29
|
+
const interval = opts.intervalMs ?? 2_000;
|
|
30
|
+
const timeout = opts.timeoutMs ?? 30_000;
|
|
31
|
+
const now = opts.now ?? (() => Date.now());
|
|
32
|
+
const start = now();
|
|
33
|
+
let polls = 0;
|
|
34
|
+
while (true) {
|
|
35
|
+
const remaining = timeout - (now() - start);
|
|
36
|
+
if (remaining <= 0)
|
|
37
|
+
return { landed: false, elapsedMs: now() - start, polls };
|
|
38
|
+
polls += 1;
|
|
39
|
+
opts.onTick?.(polls);
|
|
40
|
+
const { seen } = await checkOnce({
|
|
41
|
+
key: opts.key,
|
|
42
|
+
connectivityUrl: opts.connectivityUrl,
|
|
43
|
+
probeId: opts.probeId,
|
|
44
|
+
fetchImpl: opts.fetchImpl,
|
|
45
|
+
timeoutMs: Math.min(8_000, remaining),
|
|
46
|
+
});
|
|
47
|
+
const elapsedMs = now() - start;
|
|
48
|
+
if (seen)
|
|
49
|
+
return { landed: true, elapsedMs, polls };
|
|
50
|
+
if (elapsedMs + interval >= timeout)
|
|
51
|
+
return { landed: false, elapsedMs, polls };
|
|
52
|
+
await sleep(interval);
|
|
53
|
+
}
|
|
54
|
+
}
|