claude-attribution 1.2.1 → 1.2.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-attribution",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "AI code attribution tracking for Claude Code sessions — checkpoint-based line diff approach",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,7 +16,7 @@ import { execFile } from "child_process";
16
16
  import { promisify } from "util";
17
17
  import { hashLine } from "../attribution/differ.ts";
18
18
  import {
19
- computeMinimapFile,
19
+ hashSetToString,
20
20
  writeMinimap,
21
21
  type MinimapFileState,
22
22
  type MinimapResult,
@@ -97,21 +97,27 @@ async function main() {
97
97
  if (content.includes("\0")) return null;
98
98
 
99
99
  const lines = content.split("\n");
100
+ const total = lines.length;
100
101
 
101
- // Build currentAiHashes from every non-blank line in the file
102
- const currentAiHashes = new Set<string>();
102
+ // init --ai is an explicit declaration: every line (including blank)
103
+ // is AI-written. Build ai_hashes from all non-blank lines so the
104
+ // carry-forward algorithm in future commits works correctly; count
105
+ // blank lines as AI in the totals.
106
+ const aiHashes = new Set<string>();
103
107
  for (const line of lines) {
104
108
  if (line.trim() !== "") {
105
- currentAiHashes.add(hashLine(line));
109
+ aiHashes.add(hashLine(line));
106
110
  }
107
111
  }
108
112
 
109
- return computeMinimapFile(
110
- relPath,
111
- lines,
112
- currentAiHashes,
113
- new Set<string>(),
114
- );
113
+ return {
114
+ path: relPath,
115
+ ai_hashes: hashSetToString(aiHashes),
116
+ ai: total,
117
+ human: 0,
118
+ total,
119
+ pctAi: total > 0 ? 100 : 0,
120
+ } satisfies MinimapFileState;
115
121
  } catch {
116
122
  return null;
117
123
  }
@@ -88,7 +88,7 @@ async function main() {
88
88
  draft = true;
89
89
  } else if (args[i] === "--base" && i + 1 < args.length) {
90
90
  base = args[++i];
91
- } else if (!args[i].startsWith("--")) {
91
+ } else if (!(args[i] ?? "").startsWith("--")) {
92
92
  title = args[i];
93
93
  }
94
94
  }
@@ -44,7 +44,7 @@ function extractPrUrl(response: unknown): string | null {
44
44
 
45
45
  function prNumberFromUrl(url: string): number | null {
46
46
  const match = url.match(/\/pull\/(\d+)$/);
47
- return match ? parseInt(match[1], 10) : null;
47
+ return match?.[1] ? parseInt(match[1], 10) : null;
48
48
  }
49
49
 
50
50
  async function main() {
@@ -20,7 +20,6 @@ import { readStdin, WRITE_TOOLS, getFilePath } from "../lib/hooks.ts";
20
20
  import { maybeAutoUpgrade } from "../lib/auto-upgrade.ts";
21
21
  import {
22
22
  otelEndpoint,
23
- otelHeaders,
24
23
  readOtelContext,
25
24
  writeOtelContext,
26
25
  makeTraceId,
@@ -120,7 +120,7 @@ function computeActiveMinutes(allTimestamps: number[]): number {
120
120
  let totalMs = 0;
121
121
  const IDLE_THRESHOLD_MS = 900_000; // 15 minutes
122
122
  for (let i = 1; i < sorted.length; i++) {
123
- const gap = sorted[i] - sorted[i - 1];
123
+ const gap = (sorted[i] ?? 0) - (sorted[i - 1] ?? 0);
124
124
  if (gap < IDLE_THRESHOLD_MS) totalMs += gap;
125
125
  }
126
126
  return Math.round(totalMs / 60_000);
@@ -25,7 +25,14 @@ const execFileAsync = promisify(execFile);
25
25
  const CLI_BIN = resolve(ATTRIBUTION_ROOT, "bin", "claude-attribution");
26
26
 
27
27
  async function main() {
28
- const targetRepo = resolve(process.argv[2] ?? process.cwd());
28
+ const args = process.argv.slice(2);
29
+ const runnerFlagIdx = args.findIndex((a: string) => a === "--runner");
30
+ const runnerOverride =
31
+ runnerFlagIdx !== -1 ? args[runnerFlagIdx + 1] : undefined;
32
+ const positional = args.filter(
33
+ (a: string, i: number) => a !== "--runner" && i !== runnerFlagIdx + 1,
34
+ );
35
+ const targetRepo = resolve(positional[0] ?? process.cwd());
29
36
 
30
37
  if (!existsSync(join(targetRepo, ".git"))) {
31
38
  console.error(`Error: ${targetRepo} is not a git repository`);
@@ -147,9 +154,13 @@ async function main() {
147
154
  console.log("✓ Installed .claude/commands/pr.md (/pr command)");
148
155
 
149
156
  // 4. Install GitHub Actions workflow for automatic PR metrics injection
150
- await installCiWorkflow(targetRepo);
157
+ const { runsOn, detected } = await installCiWorkflow(
158
+ targetRepo,
159
+ runnerOverride,
160
+ );
161
+ const detectedNote = detected ? " (detected from existing workflows)" : "";
151
162
  console.log(
152
- "✓ Installed .github/workflows/claude-attribution-pr.yml (auto-injects metrics on PR open)",
163
+ `✓ Installed .github/workflows/claude-attribution-pr.yml runner: ${runsOn}${detectedNote}`,
153
164
  );
154
165
 
155
166
  // 5. Record installed version for auto-upgrade tracking
@@ -4,7 +4,7 @@
4
4
  * No top-level side effects. No console.log — logging is the caller's
5
5
  * responsibility.
6
6
  */
7
- import { readFile, writeFile, chmod, mkdir } from "fs/promises";
7
+ import { readFile, writeFile, chmod, mkdir, readdir } from "fs/promises";
8
8
  import { existsSync } from "fs";
9
9
  import { resolve, join, dirname } from "path";
10
10
  import { fileURLToPath } from "url";
@@ -169,13 +169,86 @@ export async function installSlashCommands(repoRoot: string): Promise<void> {
169
169
  }
170
170
  }
171
171
 
172
+ /** GitHub-hosted runner labels — anything else is self-hosted or custom. */
173
+ const GITHUB_HOSTED_RUNNERS = new Set([
174
+ "ubuntu-latest",
175
+ "ubuntu-24.04",
176
+ "ubuntu-22.04",
177
+ "ubuntu-20.04",
178
+ "windows-latest",
179
+ "windows-2025",
180
+ "windows-2022",
181
+ "windows-2019",
182
+ "macos-latest",
183
+ "macos-15",
184
+ "macos-14",
185
+ "macos-13",
186
+ "macos-12",
187
+ ]);
188
+
189
+ /**
190
+ * Scan existing workflows in .github/workflows/ (excluding our own) and return
191
+ * the most common non-GitHub-hosted `runs-on` value, or null if none found.
192
+ */
193
+ async function detectRunsOn(repoRoot: string): Promise<string | null> {
194
+ const workflowsDir = join(repoRoot, ".github", "workflows");
195
+ if (!existsSync(workflowsDir)) return null;
196
+
197
+ let files: string[];
198
+ try {
199
+ files = await readdir(workflowsDir);
200
+ } catch {
201
+ return null;
202
+ }
203
+
204
+ const freq = new Map<string, number>();
205
+
206
+ for (const file of files) {
207
+ if (file === "claude-attribution-pr.yml") continue;
208
+ if (!file.endsWith(".yml") && !file.endsWith(".yaml")) continue;
209
+ try {
210
+ const content = await readFile(join(workflowsDir, file), "utf8");
211
+ for (const match of content.matchAll(/^\s+runs-on:\s+(.+)$/gm)) {
212
+ const raw: string = match[1] ?? "";
213
+ const value = (raw.split("#")[0] ?? raw).trim();
214
+ if (!GITHUB_HOSTED_RUNNERS.has(value)) {
215
+ freq.set(value, (freq.get(value) ?? 0) + 1);
216
+ }
217
+ }
218
+ } catch {
219
+ continue;
220
+ }
221
+ }
222
+
223
+ if (freq.size === 0) return null;
224
+ const sorted = [...freq.entries()].sort((a, b) => b[1] - a[1]);
225
+ return sorted[0]?.[0] ?? null;
226
+ }
227
+
172
228
  /**
173
229
  * Install (or overwrite) the GitHub Actions PR metrics workflow.
230
+ * Auto-detects the runner from existing workflows; override with runsOn param.
231
+ * Returns the runner value used and whether it was auto-detected.
174
232
  */
175
- export async function installCiWorkflow(repoRoot: string): Promise<void> {
233
+ export async function installCiWorkflow(
234
+ repoRoot: string,
235
+ runsOn?: string,
236
+ ): Promise<{ runsOn: string; detected: boolean }> {
176
237
  const workflowsDir = join(repoRoot, ".github", "workflows");
177
238
  await mkdir(workflowsDir, { recursive: true });
178
- const content = await readFile(
239
+
240
+ let detected = false;
241
+ if (!runsOn) {
242
+ const autoDetected = await detectRunsOn(repoRoot);
243
+ if (autoDetected) {
244
+ runsOn = autoDetected;
245
+ detected = true;
246
+ } else {
247
+ runsOn = "ubuntu-latest";
248
+ }
249
+ }
250
+
251
+ const template = await readFile(
179
252
  join(
180
253
  ATTRIBUTION_ROOT,
181
254
  "src",
@@ -185,5 +258,9 @@ export async function installCiWorkflow(repoRoot: string): Promise<void> {
185
258
  ),
186
259
  "utf8",
187
260
  );
188
- await writeFile(join(workflowsDir, "claude-attribution-pr.yml"), content);
261
+ await writeFile(
262
+ join(workflowsDir, "claude-attribution-pr.yml"),
263
+ template.replace("{{RUNS_ON}}", runsOn),
264
+ );
265
+ return { runsOn, detected };
189
266
  }
@@ -11,7 +11,7 @@ permissions:
11
11
  jobs:
12
12
  metrics:
13
13
  name: Claude Code Attribution Metrics
14
- runs-on: ubuntu-latest
14
+ runs-on: {{RUNS_ON}}
15
15
  steps:
16
16
  - uses: actions/checkout@v4
17
17
  with: