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 +1 -1
- package/src/commands/init.ts +16 -10
- package/src/commands/pr.ts +1 -1
- package/src/hooks/post-bash.ts +1 -1
- package/src/hooks/pre-tool-use.ts +0 -1
- package/src/metrics/transcript.ts +1 -1
- package/src/setup/install.ts +14 -3
- package/src/setup/shared.ts +81 -4
- package/src/setup/templates/pr-metrics-workflow.yml +1 -1
package/package.json
CHANGED
package/src/commands/init.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
102
|
-
|
|
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
|
-
|
|
109
|
+
aiHashes.add(hashLine(line));
|
|
106
110
|
}
|
|
107
111
|
}
|
|
108
112
|
|
|
109
|
-
return
|
|
110
|
-
relPath,
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
}
|
package/src/commands/pr.ts
CHANGED
package/src/hooks/post-bash.ts
CHANGED
|
@@ -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() {
|
|
@@ -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);
|
package/src/setup/install.ts
CHANGED
|
@@ -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
|
|
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(
|
|
157
|
+
const { runsOn, detected } = await installCiWorkflow(
|
|
158
|
+
targetRepo,
|
|
159
|
+
runnerOverride,
|
|
160
|
+
);
|
|
161
|
+
const detectedNote = detected ? " (detected from existing workflows)" : "";
|
|
151
162
|
console.log(
|
|
152
|
-
|
|
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
|
package/src/setup/shared.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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(
|
|
261
|
+
await writeFile(
|
|
262
|
+
join(workflowsDir, "claude-attribution-pr.yml"),
|
|
263
|
+
template.replace("{{RUNS_ON}}", runsOn),
|
|
264
|
+
);
|
|
265
|
+
return { runsOn, detected };
|
|
189
266
|
}
|