@rachel_rotenberg/ai-contribution-tracker 1.0.15 → 1.0.17

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.
Files changed (2) hide show
  1. package/index.ts +26 -30
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -60,7 +60,7 @@ function loadState(g: string): TrackerState {
60
60
  }
61
61
  function saveState(g: string, s: TrackerState) { s.lastUpdated = new Date().toISOString(); fs.writeFileSync(getStatePath(g), JSON.stringify(s, null, 2)); }
62
62
  function formatK(n: number) { return n >= 1000 ? `${Math.round(n / 1000)}k` : String(n); }
63
- function buildMarkerParts(s: TrackerState): string[] {
63
+ function formatMarker(s: TrackerState): string {
64
64
  const p: string[] = [];
65
65
  const ma = [...new Set(s.mainAgentTypes)]; if (ma.length) p.push(`Agent mode: ${ma.join(", ")}`);
66
66
  if (s.models.length) p.push(`Model: ${s.models.join(", ")}`);
@@ -74,26 +74,24 @@ function buildMarkerParts(s: TrackerState): string[] {
74
74
  if (t.reasoningTokens > 0) r += ` +${formatK(t.reasoningTokens)} reasoning`;
75
75
  return r;
76
76
  }).join(" | ")}`); }
77
- return p;
78
- }
79
- function buildCoAuthoredBy(s: TrackerState): string {
80
- const models = s.models.join(", ");
81
- const coAuthorName = models || s.promptCount > 0
82
- ? `OpenCode (${[models, s.promptCount > 0 ? `${s.promptCount}p` : ""].filter(Boolean).join(", ")})`
83
- : "OpenCode";
84
- return `Co-authored-by: ${coAuthorName} <noreply@opencode.dev>`;
85
- }
86
- function formatMarker(s: TrackerState, prefix?: string): string {
87
- const parts = buildMarkerParts(s);
88
- if (prefix) parts.unshift(prefix);
89
- const marker = parts.length ? `Impacted by AI (${parts.join(" | ")})` : "Impacted by AI";
90
- return `${marker}\n${buildCoAuthoredBy(s)}`;
77
+ return p.length ? `Impacted by AI (${p.join(" | ")})` : "Impacted by AI";
91
78
  }
92
79
  function writeFlag(g: string, s: TrackerState) {
93
80
  if (s.promptCount === 0 && s.mainAgentTypes.length === 0 && s.subagentTypes.length === 0 && s.subagentCount === 0 && Object.keys(s.tokensByModel).length === 0) return;
94
- const fp = getFlagPath(g);
95
- const isInline = fs.existsSync(fp) && fs.readFileSync(fp, "utf8").includes("Inline");
96
- fs.writeFileSync(fp, formatMarker(s, isInline ? "Inline" : undefined));
81
+ const fp = getFlagPath(g), marker = formatMarker(s);
82
+ if (fs.existsSync(fp)) {
83
+ const ex = fs.readFileSync(fp, "utf8").trim();
84
+ if (ex.includes("Inline")) {
85
+ // Merge: preserve Inline marker, add agent data
86
+ const inner = marker.split('\n')[0].match(/\((.+)\)$/)?.[1];
87
+ const coAuthorLine = marker.split('\n').slice(1).join('\n');
88
+ const merged = inner ? `Impacted by AI (Inline + ${inner})` : "Impacted by AI (Inline)";
89
+ fs.writeFileSync(fp, coAuthorLine ? `${merged}\n${coAuthorLine}` : merged);
90
+ return;
91
+ }
92
+ // Always overwrite with latest state — state only grows, never shrinks
93
+ }
94
+ fs.writeFileSync(fp, marker);
97
95
  }
98
96
 
99
97
  // ─── Plugin ─────────────────────────────────────────────────
@@ -109,7 +107,7 @@ function appendOrCreateHook(hooksDir: string) {
109
107
  'if [ -f "$IMPACT_FLAG" ]; then',
110
108
  ' MARKER=$(cat "$IMPACT_FLAG")',
111
109
  ' if [ -z "$MARKER" ]; then MARKER="Impacted by AI"; fi',
112
- ' if ! grep -qF "Impacted by AI" "$1"; then',
110
+ ' if ! grep -qF "$MARKER" "$1"; then',
113
111
  ' echo "" >> "$1"',
114
112
  ' echo "$MARKER" >> "$1"',
115
113
  ' fi',
@@ -155,15 +153,11 @@ function extractSessionId(event: any): string | null {
155
153
  return i.sessionID || i.session_id || i.id || null;
156
154
  }
157
155
 
158
- function sanitizeAgentName(name: string): string {
159
- return name.trim();
160
- }
161
-
162
156
  function getOrCreateSession(sid: string, agent?: string): SessionState {
163
157
  let sess = sessions.get(sid);
164
158
  if (sess) return sess;
165
159
  // gitDir starts null — resolved ONLY from file paths in tool.execute.after
166
- sess = { gitDir: null, isSubagent: false, agentName: sanitizeAgentName(agent ?? "session"), pendingPrompts: 0, pendingModels: [], lastTokens: new Map() };
160
+ sess = { gitDir: null, isSubagent: false, agentName: agent ?? "session", pendingPrompts: 0, pendingModels: [], lastTokens: new Map() };
167
161
  sessions.set(sid, sess);
168
162
  return sess;
169
163
  }
@@ -199,7 +193,7 @@ const AIContributionTracker: Plugin = async ({ directory, worktree }) => {
199
193
  sessions.set(sid, {
200
194
  gitDir: null, // NEVER resolve from cwd — wait for file paths
201
195
  isSubagent: Boolean(info?.parentID),
202
- agentName: sanitizeAgentName(info?.agent ?? "session"),
196
+ agentName: info?.agent ?? "session",
203
197
  pendingPrompts: 0, pendingModels: [],
204
198
  lastTokens: new Map(),
205
199
  });
@@ -224,8 +218,11 @@ const AIContributionTracker: Plugin = async ({ directory, worktree }) => {
224
218
  if (info.role !== "assistant" || !info.finish || !info.tokens) return;
225
219
  const modelId = info.modelID ?? "unknown";
226
220
  const msgId = info.id || info.messageID;
227
- const cur: TokenTotals = { inputTokens: Number(info.tokens.input ?? 0), outputTokens: Number(info.tokens.output ?? 0),
228
- cachedTokens: Number(info.tokens.cache?.read ?? 0), reasoningTokens: Number(info.tokens.reasoning ?? 0) };
221
+ const rawInput = Number(info.tokens.input ?? 0);
222
+ const cacheRead = Number(info.tokens.cache?.read ?? 0);
223
+ const totalInput = Number(info.tokens.total ?? 0) || (rawInput + cacheRead);
224
+ const cur: TokenTotals = { inputTokens: totalInput, outputTokens: Number(info.tokens.output ?? 0),
225
+ cachedTokens: cacheRead, reasoningTokens: Number(info.tokens.reasoning ?? 0) };
229
226
  const prev = msgId ? sess.lastTokens.get(msgId) : undefined;
230
227
  // Clamp deltas to >= 0 (snapshots can decrease; we never subtract)
231
228
  const delta: TokenTotals = {
@@ -253,7 +250,7 @@ const AIContributionTracker: Plugin = async ({ directory, worktree }) => {
253
250
  if (sess.gitDir) {
254
251
  // gitDir known — write directly
255
252
  const state = loadState(sess.gitDir);
256
- const src = `opencode/${sanitizeAgentName(input.agent ?? "session")}`;
253
+ const src = `opencode/${input.agent ?? "session"}`;
257
254
  if (!state.mainAgentTypes.includes(src)) state.mainAgentTypes.push(src);
258
255
  state.promptCount += 1;
259
256
  if (input.model?.modelID && !state.models.includes(input.model.modelID)) state.models.push(input.model.modelID);
@@ -262,7 +259,7 @@ const AIContributionTracker: Plugin = async ({ directory, worktree }) => {
262
259
  } else {
263
260
  // gitDir NOT known yet — buffer in memory
264
261
  sess.pendingPrompts += 1;
265
- if (input.agent) sess.agentName = sanitizeAgentName(input.agent);
262
+ if (input.agent) sess.agentName = input.agent;
266
263
  if (input.model?.modelID && !sess.pendingModels.includes(input.model.modelID)) sess.pendingModels.push(input.model.modelID);
267
264
  }
268
265
  } catch { /* never crash OpenCode */ }
@@ -293,7 +290,6 @@ const AIContributionTracker: Plugin = async ({ directory, worktree }) => {
293
290
  state.subagentCount += 1;
294
291
  if (!state.subagentTypes.includes(agentType)) state.subagentTypes.push(agentType);
295
292
  saveState(sess.gitDir, state);
296
- writeFlag(sess.gitDir, state);
297
293
  writeFlag(sess.gitDir, state); // keep flag current for commit-before-idle
298
294
  }
299
295
  } catch { /* never crash OpenCode */ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rachel_rotenberg/ai-contribution-tracker",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "description": "OpenCode plugin \u2014 tracks AI coding sessions and tags git commits with Impacted by AI markers",
5
5
  "main": "index.ts",
6
6
  "types": "index.ts",