claude-attribution 1.0.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.
@@ -0,0 +1,33 @@
1
+ # Create PR with AI Metrics
2
+
3
+ Run this when you're ready to open a pull request. Collects AI metrics and creates the PR in one step — no manual copy-paste needed.
4
+
5
+ ## Instructions
6
+
7
+ ```bash
8
+ "{{CLI_BIN}}" pr "feat: your PR title"
9
+ ```
10
+
11
+ This will:
12
+ 1. Collect tool usage, model usage, and code attribution for the current session
13
+ 2. Read `.github/PULL_REQUEST_TEMPLATE.md` if it exists (uses a built-in template otherwise)
14
+ 3. Inject the metrics block into the PR body
15
+ 4. Create the PR via `gh pr create` and print the URL
16
+
17
+ ## Options
18
+
19
+ ```bash
20
+ "{{CLI_BIN}}" pr "feat: title" --draft # Open as draft PR
21
+ "{{CLI_BIN}}" pr "feat: title" --base develop # Target a different base branch
22
+ "{{CLI_BIN}}" pr # Title derived from branch name
23
+ ```
24
+
25
+ ## Requirements
26
+
27
+ - `gh` (GitHub CLI) must be installed and authenticated: `gh auth status`
28
+
29
+ ## Example
30
+
31
+ ```
32
+ /pr "feat: COMM-1234 add user authentication"
33
+ ```
@@ -0,0 +1,4 @@
1
+ #!/bin/sh
2
+ # Auto-installed by claude-attribution
3
+ # Pushes attribution git notes to origin so GitHub Actions can read them
4
+ "{{CLI_BIN}}" hook pre-push || true
@@ -0,0 +1,25 @@
1
+ # Start Claude Attribution Session
2
+
3
+ Run this when beginning work on a new Jira ticket or feature branch.
4
+ Records a start timestamp so /metrics only counts activity from this point forward.
5
+
6
+ ## Instructions
7
+
8
+ ```bash
9
+ "{{CLI_BIN}}" start
10
+ ```
11
+
12
+ This writes a marker to `.claude/attribution-state/session-start` with the current
13
+ timestamp and session ID. The /metrics command uses this to scope tool and token
14
+ counts to the current Jira, even if Claude Code has been running across multiple tickets.
15
+
16
+ ## When to run
17
+
18
+ - When you `git checkout -b COMM-1234/my-feature` and start a new ticket
19
+ - Any time you want a clean metrics baseline
20
+
21
+ ## Example Usage
22
+
23
+ ```
24
+ /start
25
+ ```
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Uninstaller — removes claude-attribution hooks from a repo.
3
+ *
4
+ * Usage: bun src/setup/uninstall.ts [/path/to/target-repo]
5
+ *
6
+ * If no path is given, uninstalls from the current working directory.
7
+ */
8
+ import { readFile, writeFile, unlink } from "fs/promises";
9
+ import { existsSync } from "fs";
10
+ import { execFile } from "child_process";
11
+ import { promisify } from "util";
12
+ import { resolve, join } from "path";
13
+
14
+ const execFileAsync = promisify(execFile);
15
+
16
+ interface HookEntry {
17
+ matcher: string;
18
+ hooks: Array<{ type: string; command: string }>;
19
+ }
20
+
21
+ type HooksConfig = Record<string, HookEntry[]>;
22
+
23
+ interface ClaudeSettings {
24
+ hooks?: HooksConfig;
25
+ [key: string]: unknown;
26
+ }
27
+
28
+ async function stripSettingsHooks(settingsPath: string): Promise<boolean> {
29
+ if (!existsSync(settingsPath)) return false;
30
+
31
+ const raw = await readFile(settingsPath, "utf8");
32
+ const settings = JSON.parse(raw) as ClaudeSettings;
33
+ if (!settings.hooks) return false;
34
+
35
+ let changed = false;
36
+ for (const [event, entries] of Object.entries(settings.hooks)) {
37
+ const filtered = entries.filter(
38
+ (e) => !e.hooks.some((h) => h.command.includes("claude-attribution")),
39
+ );
40
+ if (filtered.length !== entries.length) {
41
+ settings.hooks[event] = filtered;
42
+ changed = true;
43
+ }
44
+ }
45
+
46
+ if (changed) {
47
+ await writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n");
48
+ }
49
+ return changed;
50
+ }
51
+
52
+ async function stripGitHook(hookPath: string): Promise<boolean> {
53
+ if (!existsSync(hookPath)) return false;
54
+
55
+ const content = await readFile(hookPath, "utf8");
56
+ if (!content.includes("claude-attribution")) return false;
57
+
58
+ const marker = "\n# claude-attribution\n";
59
+ const markerIndex = content.indexOf(marker);
60
+
61
+ if (markerIndex !== -1) {
62
+ const before = content.slice(0, markerIndex).trimEnd();
63
+
64
+ if (!before || before === "#!/bin/sh") {
65
+ // Nothing left but the shebang — remove the file
66
+ await unlink(hookPath);
67
+ } else {
68
+ await writeFile(hookPath, before + "\n");
69
+ }
70
+ return true;
71
+ }
72
+
73
+ // Fallback: hook was fully auto-generated (no marker) but contains claude-attribution.
74
+ // Detect by the "Auto-installed by claude-attribution" header or the hook command pattern.
75
+ const hasAutoInstallHeader =
76
+ /^\s*#\s*Auto-installed by claude-attribution/m.test(content);
77
+ const hasClaudeHookCommand = /claude-attribution[^\n]*\bhook\b/.test(content);
78
+
79
+ if (hasAutoInstallHeader || hasClaudeHookCommand) {
80
+ await unlink(hookPath);
81
+ return true;
82
+ }
83
+
84
+ // Contains "claude-attribution" in some unrecognized form — be conservative.
85
+ return false;
86
+ }
87
+
88
+ async function removeFile(filePath: string): Promise<boolean> {
89
+ if (!existsSync(filePath)) return false;
90
+ await unlink(filePath);
91
+ return true;
92
+ }
93
+
94
+ async function main() {
95
+ const targetRepo = resolve(process.argv[2] ?? process.cwd());
96
+
97
+ if (!existsSync(join(targetRepo, ".git"))) {
98
+ console.error(`Error: ${targetRepo} is not a git repository`);
99
+ process.exit(1);
100
+ }
101
+
102
+ console.log(`Uninstalling claude-attribution from: ${targetRepo}`);
103
+
104
+ // 1. Strip hooks from .claude/settings.json
105
+ const settingsPath = join(targetRepo, ".claude", "settings.json");
106
+ if (await stripSettingsHooks(settingsPath)) {
107
+ console.log("✓ Removed hooks from .claude/settings.json");
108
+ } else {
109
+ console.log(" (no hooks found in .claude/settings.json)");
110
+ }
111
+
112
+ // 2. Strip/remove post-commit hook (plain git or Husky)
113
+ const huskyPostCommit = join(targetRepo, ".husky", "post-commit");
114
+ const plainPostCommit = join(targetRepo, ".git", "hooks", "post-commit");
115
+ const isHuskyPostCommit = existsSync(huskyPostCommit);
116
+ const postCommitPath = isHuskyPostCommit ? huskyPostCommit : plainPostCommit;
117
+ if (await stripGitHook(postCommitPath)) {
118
+ console.log(
119
+ `✓ Removed post-commit hook (${isHuskyPostCommit ? ".husky" : ".git/hooks"})`,
120
+ );
121
+ } else {
122
+ console.log(" (no post-commit hook found)");
123
+ }
124
+
125
+ // 3. Strip/remove pre-push hook (plain git or Husky)
126
+ const huskyPrePush = join(targetRepo, ".husky", "pre-push");
127
+ const plainPrePush = join(targetRepo, ".git", "hooks", "pre-push");
128
+ const isHuskyPrePush = existsSync(huskyPrePush);
129
+ const prePushPath = isHuskyPrePush ? huskyPrePush : plainPrePush;
130
+ if (await stripGitHook(prePushPath)) {
131
+ console.log(
132
+ `✓ Removed pre-push hook (${isHuskyPrePush ? ".husky" : ".git/hooks"})`,
133
+ );
134
+ } else {
135
+ console.log(" (no pre-push hook found)");
136
+ }
137
+
138
+ // 4. Remove remote.origin.push refspec for attribution notes (best-effort)
139
+ const notesRefspec =
140
+ "refs/notes/claude-attribution:refs/notes/claude-attribution";
141
+ try {
142
+ await execFileAsync(
143
+ "git",
144
+ ["config", "--unset", "remote.origin.push", notesRefspec],
145
+ { cwd: targetRepo },
146
+ );
147
+ console.log("✓ Removed remote.origin.push refspec for attribution notes");
148
+ } catch {
149
+ // Ignore — refspec may not be present or git may be unavailable
150
+ }
151
+
152
+ // 5. Remove slash commands
153
+ const commandsDir = join(targetRepo, ".claude", "commands");
154
+ const metricsRemoved = await removeFile(join(commandsDir, "metrics.md"));
155
+ const startRemoved = await removeFile(join(commandsDir, "start.md"));
156
+ const prRemoved = await removeFile(join(commandsDir, "pr.md"));
157
+ if (metricsRemoved || startRemoved || prRemoved) {
158
+ const removed = ["metrics.md", "start.md", "pr.md"].filter(
159
+ (_, i) => [metricsRemoved, startRemoved, prRemoved][i],
160
+ );
161
+ console.log(`✓ Removed .claude/commands/${removed.join(", ")}`);
162
+ } else {
163
+ console.log(" (no slash commands found)");
164
+ }
165
+
166
+ console.log("\nDone! claude-attribution has been removed from this repo.");
167
+ console.log(
168
+ "Note: .claude/logs/ and .claude/attribution-state/ are preserved.",
169
+ );
170
+ }
171
+
172
+ main().catch((err) => {
173
+ console.error("Uninstall failed:", err);
174
+ process.exit(1);
175
+ });