@vymalo/opencode-otel 0.12.0 → 0.14.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/dist/vcs.js ADDED
@@ -0,0 +1,188 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { isAbsolute, join, resolve } from "node:path";
3
+ const defaultReader = (path) => readFile(path, "utf8");
4
+ const PROVIDERS = [
5
+ [/(^|\.)github\.com$/i, "github"],
6
+ [/(^|\.)gitlab\.com$/i, "gitlab"],
7
+ [/(^|\.)bitbucket\.org$/i, "bitbucket"],
8
+ [/(^|\.)gitea\./i, "gitea"]
9
+ ];
10
+ /**
11
+ * Strip credentials and normalise a git remote to something safe to export.
12
+ *
13
+ * The userinfo component is dropped **unconditionally**. A remote of the form
14
+ * `https://user:ghp_xxx@github.com/org/repo.git` is entirely ordinary in a CI
15
+ * checkout, and that token would otherwise be published as a resource attribute
16
+ * on every span, metric and log the process emits. Nothing in the userinfo
17
+ * identifies the repository, so there is no value being traded away.
18
+ *
19
+ * scp-like SSH remotes (`git@github.com:org/repo.git`) are rewritten to an
20
+ * `ssh://` URL rather than to `https://` — the transport is a fact about the
21
+ * checkout, and inventing a different scheme would misreport it.
22
+ */
23
+ export function sanitizeRemoteUrl(raw) {
24
+ const trimmed = raw.trim();
25
+ if (trimmed === "") {
26
+ return undefined;
27
+ }
28
+ // scp-like syntax has no scheme and a `:` separating host from path.
29
+ const scpLike = /^(?:([^@/]+)@)?([^:/]+):(?!\/)(.+)$/.exec(trimmed);
30
+ const normalized = scpLike && !/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? `ssh://${scpLike[2]}/${scpLike[3]}` : trimmed;
31
+ let parsed;
32
+ try {
33
+ parsed = new URL(normalized);
34
+ } catch {
35
+ // A local path (`/srv/git/repo.git`) or something unparseable: report
36
+ // nothing rather than guessing at its shape.
37
+ return undefined;
38
+ }
39
+ parsed.username = "";
40
+ parsed.password = "";
41
+ parsed.hash = "";
42
+ parsed.search = "";
43
+ parsed.pathname = parsed.pathname.replace(/\.git\/?$/, "");
44
+ return parsed.toString().replace(/\/$/, "");
45
+ }
46
+ /** Split `https://github.com/org/repo` into its owner, name and provider. */
47
+ export function describeRemote(url) {
48
+ let parsed;
49
+ try {
50
+ parsed = new URL(url);
51
+ } catch {
52
+ return {};
53
+ }
54
+ const segments = parsed.pathname.split("/").filter((s) => s !== "");
55
+ const provider = PROVIDERS.find(([pattern]) => pattern.test(parsed.hostname))?.[1];
56
+ return {
57
+ ...segments.length > 0 ? { name: segments[segments.length - 1] } : {},
58
+ ...segments.length > 1 ? { owner: segments[segments.length - 2] } : {},
59
+ ...provider ? { provider } : {}
60
+ };
61
+ }
62
+ /** Pull the `origin` remote out of a git config, falling back to the first one. */
63
+ export function parseRemoteFromConfig(config) {
64
+ const remotes = new Map();
65
+ let current;
66
+ for (const line of config.split("\n")) {
67
+ const section = /^\s*\[remote\s+"([^"]+)"\]/.exec(line);
68
+ if (section) {
69
+ current = section[1];
70
+ continue;
71
+ }
72
+ if (/^\s*\[/.test(line)) {
73
+ current = undefined;
74
+ continue;
75
+ }
76
+ const url = current ? /^\s*url\s*=\s*(.+?)\s*$/.exec(line) : null;
77
+ if (url && current && !remotes.has(current)) {
78
+ remotes.set(current, url[1]);
79
+ }
80
+ }
81
+ return remotes.get("origin") ?? remotes.values().next().value;
82
+ }
83
+ /**
84
+ * Resolve the git directory for a checkout.
85
+ *
86
+ * `.git` is a **file** rather than a directory in a linked worktree or a
87
+ * submodule — it holds `gitdir: <path>`. This repository's own development
88
+ * happens in worktrees, so treating `.git` as always-a-directory would report
89
+ * nothing precisely where it is most used.
90
+ *
91
+ * Returns both the worktree's own git dir (which owns `HEAD`) and the common
92
+ * dir (which owns `config` and `packed-refs`); for a plain clone they are the
93
+ * same path.
94
+ */
95
+ export async function resolveGitDirs(start, read = defaultReader) {
96
+ let gitDir;
97
+ try {
98
+ const pointer = await read(join(start, ".git"));
99
+ const match = /^gitdir:\s*(.+?)\s*$/m.exec(pointer);
100
+ if (match) {
101
+ gitDir = isAbsolute(match[1]) ? match[1] : resolve(start, match[1]);
102
+ }
103
+ } catch {}
104
+ if (!gitDir) {
105
+ try {
106
+ await read(join(start, ".git", "HEAD"));
107
+ gitDir = join(start, ".git");
108
+ } catch {
109
+ return undefined;
110
+ }
111
+ }
112
+ let commonDir = gitDir;
113
+ try {
114
+ const common = (await read(join(gitDir, "commondir"))).trim();
115
+ if (common !== "") {
116
+ commonDir = isAbsolute(common) ? common : resolve(gitDir, common);
117
+ }
118
+ } catch {}
119
+ return {
120
+ gitDir,
121
+ commonDir
122
+ };
123
+ }
124
+ async function readRevision(ref, gitDir, commonDir, read) {
125
+ for (const base of new Set([gitDir, commonDir])) {
126
+ try {
127
+ const sha = (await read(join(base, ref))).trim();
128
+ if (/^[0-9a-f]{40,64}$/i.test(sha)) {
129
+ return sha;
130
+ }
131
+ } catch {}
132
+ }
133
+ try {
134
+ const packed = await read(join(commonDir, "packed-refs"));
135
+ for (const line of packed.split("\n")) {
136
+ const [sha, name] = line.trim().split(/\s+/);
137
+ if (name === ref && /^[0-9a-f]{40,64}$/i.test(sha)) {
138
+ return sha;
139
+ }
140
+ }
141
+ } catch {}
142
+ return undefined;
143
+ }
144
+ /**
145
+ * Read repository metadata straight off disk. Never spawns `git`: the plugin
146
+ * runs inside the host's process on every session start, and a subprocess there
147
+ * costs more than the data is worth — and would fail wherever `git` is not on
148
+ * `PATH` while the files are right there.
149
+ *
150
+ * Every field is independent and best-effort. A repository with no remote still
151
+ * reports its branch; an unreadable config loses only the remote.
152
+ */
153
+ export async function readVcsInfo(start, read = defaultReader) {
154
+ if (!start) {
155
+ return {};
156
+ }
157
+ const dirs = await resolveGitDirs(start, read);
158
+ if (!dirs) {
159
+ return {};
160
+ }
161
+ const { gitDir, commonDir } = dirs;
162
+ const info = {};
163
+ try {
164
+ const url = parseRemoteFromConfig(await read(join(commonDir, "config")));
165
+ const sanitized = url ? sanitizeRemoteUrl(url) : undefined;
166
+ if (sanitized) {
167
+ info.url = sanitized;
168
+ Object.assign(info, describeRemote(sanitized));
169
+ }
170
+ } catch {}
171
+ try {
172
+ const head = (await read(join(gitDir, "HEAD"))).trim();
173
+ const symbolic = /^ref:\s*(.+)$/.exec(head);
174
+ if (symbolic) {
175
+ const ref = symbolic[1].trim();
176
+ const tag = ref.startsWith("refs/tags/");
177
+ info.ref = ref.replace(/^refs\/(heads|tags)\//, "");
178
+ info.refType = tag ? "tag" : "branch";
179
+ info.revision = await readRevision(ref, gitDir, commonDir, read);
180
+ } else if (/^[0-9a-f]{40,64}$/i.test(head)) {
181
+ // Detached HEAD: there is a revision but no ref name to report.
182
+ info.revision = head;
183
+ }
184
+ } catch {}
185
+ return info;
186
+ }
187
+
188
+ //# sourceMappingURL=vcs.js.map
@@ -0,0 +1 @@
1
+ {"mappings":"AAAA,SAAS,gBAAgB;AACzB,SAAS,YAAY,MAAM,eAAe;AAuB1C,MAAM,iBAA6B,SAAS,SAAS,MAAM,MAAM;AAEjE,MAAM,YAAqC;CACzC,CAAC,uBAAuB,QAAQ;CAChC,CAAC,uBAAuB,QAAQ;CAChC,CAAC,0BAA0B,WAAW;CACtC,CAAC,kBAAkB,OAAO;AAC5B;;;;;;;;;;;;;;AAeA,OAAO,SAAS,kBAAkB,KAAiC;CACjE,MAAM,UAAU,IAAI,KAAK;CACzB,IAAI,YAAY,IAAI;EAClB,OAAO;CACT;;CAGA,MAAM,UAAU,sCAAsC,KAAK,OAAO;CAClE,MAAM,aACJ,WAAW,CAAC,2BAA2B,KAAK,OAAO,IAC/C,SAAS,QAAQ,GAAG,GAAG,QAAQ,OAC/B;CAEN,IAAI;CACJ,IAAI;EACF,SAAS,IAAI,IAAI,UAAU;CAC7B,QAAQ;;;EAGN,OAAO;CACT;CAEA,OAAO,WAAW;CAClB,OAAO,WAAW;CAClB,OAAO,OAAO;CACd,OAAO,SAAS;CAChB,OAAO,WAAW,OAAO,SAAS,QAAQ,aAAa,EAAE;CACzD,OAAO,OAAO,SAAS,CAAC,CAAC,QAAQ,OAAO,EAAE;AAC5C;;AAGA,OAAO,SAAS,eAAe,KAA2D;CACxF,IAAI;CACJ,IAAI;EACF,SAAS,IAAI,IAAI,GAAG;CACtB,QAAQ;EACN,OAAO,CAAC;CACV;CACA,MAAM,WAAW,OAAO,SAAS,MAAM,GAAG,CAAC,CAAC,QAAQ,MAAM,MAAM,EAAE;CAClE,MAAM,WAAW,UAAU,MAAM,CAAC,aAAa,QAAQ,KAAK,OAAO,QAAQ,CAAC,CAAC,GAAG;CAChF,OAAO;EACL,GAAI,SAAS,SAAS,IAAI,EAAE,MAAM,SAAS,SAAS,SAAS,GAAG,IAAI,CAAC;EACrE,GAAI,SAAS,SAAS,IAAI,EAAE,OAAO,SAAS,SAAS,SAAS,GAAG,IAAI,CAAC;EACtE,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;CACjC;AACF;;AAGA,OAAO,SAAS,sBAAsB,QAAoC;CACxE,MAAM,UAAU,IAAI,IAAoB;CACxC,IAAI;CAEJ,KAAK,MAAM,QAAQ,OAAO,MAAM,IAAI,GAAG;EACrC,MAAM,UAAU,6BAA6B,KAAK,IAAI;EACtD,IAAI,SAAS;GACX,UAAU,QAAQ;GAClB;EACF;EACA,IAAI,SAAS,KAAK,IAAI,GAAG;GACvB,UAAU;GACV;EACF;EACA,MAAM,MAAM,UAAU,0BAA0B,KAAK,IAAI,IAAI;EAC7D,IAAI,OAAO,WAAW,CAAC,QAAQ,IAAI,OAAO,GAAG;GAC3C,QAAQ,IAAI,SAAS,IAAI,EAAE;EAC7B;CACF;CAEA,OAAO,QAAQ,IAAI,QAAQ,KAAK,QAAQ,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC;AAC1D;;;;;;;;;;;;;AAcA,OAAO,eAAe,eACpB,OACA,OAAmB,eACyC;CAC5D,IAAI;CAEJ,IAAI;EACF,MAAM,UAAU,MAAM,KAAK,KAAK,OAAO,MAAM,CAAC;EAC9C,MAAM,QAAQ,wBAAwB,KAAK,OAAO;EAClD,IAAI,OAAO;GACT,SAAS,WAAW,MAAM,EAAE,IAAI,MAAM,KAAK,QAAQ,OAAO,MAAM,EAAE;EACpE;CACF,QAAQ,CAGR;CAEA,IAAI,CAAC,QAAQ;EACX,IAAI;GACF,MAAM,KAAK,KAAK,OAAO,QAAQ,MAAM,CAAC;GACtC,SAAS,KAAK,OAAO,MAAM;EAC7B,QAAQ;GACN,OAAO;EACT;CACF;CAEA,IAAI,YAAY;CAChB,IAAI;EACF,MAAM,UAAU,MAAM,KAAK,KAAK,QAAQ,WAAW,CAAC,EAAC,AAAC,CAAC,KAAK;EAC5D,IAAI,WAAW,IAAI;GACjB,YAAY,WAAW,MAAM,IAAI,SAAS,QAAQ,QAAQ,MAAM;EAClE;CACF,QAAQ,CAER;CAEA,OAAO;EAAE;EAAQ;CAAU;AAC7B;AAEA,eAAe,aACb,KACA,QACA,WACA,MAC6B;CAC7B,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,QAAQ,SAAS,CAAC,GAAG;EAC/C,IAAI;GACF,MAAM,OAAO,MAAM,KAAK,KAAK,MAAM,GAAG,CAAC,EAAC,AAAC,CAAC,KAAK;GAC/C,IAAI,qBAAqB,KAAK,GAAG,GAAG;IAClC,OAAO;GACT;EACF,QAAQ,CAER;CACF;CAEA,IAAI;EACF,MAAM,SAAS,MAAM,KAAK,KAAK,WAAW,aAAa,CAAC;EACxD,KAAK,MAAM,QAAQ,OAAO,MAAM,IAAI,GAAG;GACrC,MAAM,CAAC,KAAK,QAAQ,KAAK,KAAK,CAAC,CAAC,MAAM,KAAK;GAC3C,IAAI,SAAS,OAAO,qBAAqB,KAAK,GAAG,GAAG;IAClD,OAAO;GACT;EACF;CACF,QAAQ,CAER;CACA,OAAO;AACT;;;;;;;;;;AAWA,OAAO,eAAe,YACpB,OACA,OAAmB,eACD;CAClB,IAAI,CAAC,OAAO;EACV,OAAO,CAAC;CACV;CAEA,MAAM,OAAO,MAAM,eAAe,OAAO,IAAI;CAC7C,IAAI,CAAC,MAAM;EACT,OAAO,CAAC;CACV;CACA,MAAM,EAAE,QAAQ,cAAc;CAC9B,MAAM,OAAgB,CAAC;CAEvB,IAAI;EACF,MAAM,MAAM,sBAAsB,MAAM,KAAK,KAAK,WAAW,QAAQ,CAAC,CAAC;EACvE,MAAM,YAAY,MAAM,kBAAkB,GAAG,IAAI;EACjD,IAAI,WAAW;GACb,KAAK,MAAM;GACX,OAAO,OAAO,MAAM,eAAe,SAAS,CAAC;EAC/C;CACF,QAAQ,CAER;CAEA,IAAI;EACF,MAAM,QAAQ,MAAM,KAAK,KAAK,QAAQ,MAAM,CAAC,EAAC,AAAC,CAAC,KAAK;EACrD,MAAM,WAAW,gBAAgB,KAAK,IAAI;EAC1C,IAAI,UAAU;GACZ,MAAM,MAAM,SAAS,EAAE,CAAC,KAAK;GAC7B,MAAM,MAAM,IAAI,WAAW,YAAY;GACvC,KAAK,MAAM,IAAI,QAAQ,yBAAyB,EAAE;GAClD,KAAK,UAAU,MAAM,QAAQ;GAC7B,KAAK,WAAW,MAAM,aAAa,KAAK,QAAQ,WAAW,IAAI;EACjE,OAAO,IAAI,qBAAqB,KAAK,IAAI,GAAG;;GAE1C,KAAK,WAAW;EAClB;CACF,QAAQ,CAER;CAEA,OAAO;AACT","names":[],"sources":["../src/vcs.ts"],"version":3,"file":"vcs.js","sourceRoot":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vymalo/opencode-otel",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "OpenTelemetry plugin for OpenCode: exports developer interactions as OTLP traces, metrics and logs — real USD cost, all five token types (including cache read/write and reasoning), tool results, permission decisions, API errors and lines of code. Configurable from opencode.json or standard OTEL_* environment variables.",
5
5
  "license": "MIT",
6
6
  "author": "vymalo contributors",
@@ -66,11 +66,11 @@
66
66
  "@opentelemetry/semantic-conventions": "^1.43.0"
67
67
  },
68
68
  "devDependencies": {
69
- "vite": "^8.0.14",
69
+ "vite": "^8.2.1",
70
70
  "vitest": "^4.1.7"
71
71
  },
72
72
  "scripts": {
73
- "build": "tsc -p tsconfig.json",
73
+ "build": "node ../../scripts/build-package.mjs",
74
74
  "lint": "biome lint .",
75
75
  "typecheck": "tsc -p tsconfig.json --noEmit",
76
76
  "test": "vitest run",