@ghl-ai/aw 0.1.37-beta.71 → 0.1.37-beta.72

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/integrate.mjs CHANGED
@@ -522,27 +522,10 @@ export function installIdeHooks() {
522
522
  'capabilities/telemetry.mjs',
523
523
  ];
524
524
 
525
- // Legacy hooks (kept for --legacy-hooks rollback)
526
- const legacyFiles = ['telemetry-stop.js', 'activity-tracker.js'];
527
-
528
- // Verify new hook files exist in the package
525
+ // Verify hook files exist in the package
529
526
  const hasNewHooks = newHookFiles.every(f => existsSync(join(pkgHooksDir, f)));
530
527
  if (!hasNewHooks) {
531
- // Fallback: try legacy hooks if new ones aren't built yet
532
- const hasLegacy = legacyFiles.every(f => existsSync(join(pkgHooksDir, f)));
533
- if (!hasLegacy) {
534
- fmt.logWarn('Hook scripts not found in aw package — skipping IDE hooks');
535
- return;
536
- }
537
- // Install legacy hooks only
538
- mkdirSync(hooksDir, { recursive: true });
539
- for (const file of legacyFiles) {
540
- const dest = join(hooksDir, file);
541
- writeFileSync(dest, readFileSync(join(pkgHooksDir, file), 'utf8'));
542
- try { chmodSync(dest, 0o755); } catch { /* best effort */ }
543
- }
544
- _wireLegacyHooks(home, hooksDir);
545
- fmt.logSuccess('Legacy IDE hooks installed (Claude + Cursor → ~/.aw/hooks/)');
528
+ fmt.logWarn('Hook scripts not found in aw package skipping IDE hooks');
546
529
  return;
547
530
  }
548
531
 
@@ -560,16 +543,6 @@ export function installIdeHooks() {
560
543
  try { chmodSync(dest, 0o755); } catch { /* best effort */ }
561
544
  }
562
545
 
563
- // Also copy legacy files for rollback
564
- for (const file of legacyFiles) {
565
- const src = join(pkgHooksDir, file);
566
- if (existsSync(src)) {
567
- const dest = join(hooksDir, file);
568
- writeFileSync(dest, readFileSync(src, 'utf8'));
569
- try { chmodSync(dest, 0o755); } catch { /* best effort */ }
570
- }
571
- }
572
-
573
546
  // Dispatcher commands — one per hook event
574
547
  const dispatchers = {
575
548
  SessionStart: { cmd: `node "${join(hooksDir, 'shared', 'session-start.mjs')}"`, timeout: 10 },
@@ -727,36 +700,6 @@ export function installIdeHooks() {
727
700
  fmt.logSuccess('IDE hooks installed — 4 events (Claude + Cursor) + 2 events (Codex) → ~/.aw/hooks/');
728
701
  }
729
702
 
730
- /** Legacy hook wiring — used as fallback when new dispatchers aren't available */
731
- function _wireLegacyHooks(home, hooksDir) {
732
- const stopCmd = `node "${join(hooksDir, 'telemetry-stop.js')}"`;
733
- const activityCmd = `node "${join(hooksDir, 'activity-tracker.js')}"`;
734
-
735
- // Claude Code
736
- const claudeSettingsPath = join(home, '.claude', 'settings.json');
737
- let claudeSettings = {};
738
- if (existsSync(claudeSettingsPath)) {
739
- try { claudeSettings = JSON.parse(readFileSync(claudeSettingsPath, 'utf8')); } catch { claudeSettings = {}; }
740
- }
741
- if (!claudeSettings.hooks) claudeSettings.hooks = {};
742
- claudeSettings.hooks.Stop = [{ hooks: [{ type: 'command', command: stopCmd, timeout: 15, statusMessage: 'Recording telemetry...' }] }];
743
- claudeSettings.hooks.PostToolUse = [{ hooks: [{ type: 'command', command: activityCmd, timeout: 5 }] }];
744
- writeFileSync(claudeSettingsPath, JSON.stringify(claudeSettings, null, 2) + '\n');
745
-
746
- // Cursor
747
- const cursorHooksPath = join(home, '.cursor', 'hooks.json');
748
- let cursorHooks = { version: 1, hooks: {} };
749
- if (existsSync(cursorHooksPath)) {
750
- try { cursorHooks = JSON.parse(readFileSync(cursorHooksPath, 'utf8')); } catch { /* overwrite */ }
751
- }
752
- if (!cursorHooks.hooks) cursorHooks.hooks = {};
753
- if (!cursorHooks.hooks.stop) cursorHooks.hooks.stop = [];
754
- if (!cursorHooks.hooks.stop.some(h => h.command?.includes('telemetry-stop.js'))) {
755
- cursorHooks.hooks.stop.push({ command: stopCmd, timeout: 15 });
756
- }
757
- writeFileSync(cursorHooksPath, JSON.stringify(cursorHooks, null, 2) + '\n');
758
- }
759
-
760
703
  /**
761
704
  * Return top-level team namespace names from config (excludes 'platform').
762
705
  * cfg.include may contain full paths like 'mobile/core/backend/agents/dev.md'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.37-beta.71",
3
+ "version": "0.1.37-beta.72",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": "bin.js",
@@ -1,61 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
- // activity-tracker.js — Lightweight PostToolUse hook for Claude Code + Cursor.
4
- //
5
- // Fires after every tool call. Appends one line to .claude/telemetry/activity.jsonl.
6
- // MUST complete in <100ms — no child processes, no network, pure fs append.
7
- // MUST never exit non-zero or block the agent loop.
8
-
9
- const { appendFileSync, mkdirSync } = require('node:fs');
10
- const { join } = require('node:path');
11
-
12
- async function main() {
13
- // Read stdin (JSON from hook system)
14
- const chunks = [];
15
- for await (const chunk of process.stdin) chunks.push(chunk);
16
- const raw = Buffer.concat(chunks).toString('utf8').trim();
17
- if (!raw) return;
18
-
19
- let input;
20
- try { input = JSON.parse(raw); } catch { return; }
21
-
22
- // Normalize platform differences
23
- // Claude Code: { tool_name, tool_input, tool_response, cwd, session_id }
24
- // Cursor: { tool_name, tool_input, tool_output, cwd, duration, conversation_id }
25
- const toolName = input.tool_name || 'unknown';
26
- const sessionId = input.session_id || input.conversation_id || 'unknown';
27
- const cwd = input.cwd
28
- || process.env.CURSOR_PROJECT_DIR
29
- || process.env.CLAUDE_PROJECT_DIR
30
- || process.cwd();
31
- const duration = input.duration || 0;
32
-
33
- // Extract exit code (Claude Code nests it in tool_response)
34
- let exitCode = 0;
35
- const resp = input.tool_response || input.tool_output;
36
- if (resp && typeof resp === 'object') {
37
- exitCode = resp.exitCode ?? 0;
38
- } else if (typeof resp === 'string') {
39
- try {
40
- const parsed = JSON.parse(resp);
41
- exitCode = parsed.exitCode ?? 0;
42
- } catch { /* not JSON */ }
43
- }
44
-
45
- // Append to activity.jsonl (atomic for writes < PIPE_BUF = 4096 bytes)
46
- const telemetryDir = join(cwd, '.claude', 'telemetry');
47
- try {
48
- mkdirSync(telemetryDir, { recursive: true });
49
- const record = JSON.stringify({
50
- ts: new Date().toISOString(),
51
- sid: sessionId,
52
- tool: toolName,
53
- exit: exitCode,
54
- dur: duration,
55
- });
56
- // Single appendFileSync — atomic on most OS for small writes
57
- appendFileSync(join(telemetryDir, 'activity.jsonl'), record + '\n');
58
- } catch { /* never block on write failure */ }
59
- }
60
-
61
- main().catch(() => process.exit(0));
@@ -1,411 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
- // telemetry-stop.js — Unified Stop hook for Claude Code + Cursor.
4
- //
5
- // Fires when the AI stops responding. Reads the session transcript,
6
- // extracts token counts + model, estimates cost, and:
7
- // 1. Appends to .claude/telemetry/costs.jsonl (local, always works)
8
- // 2. Pushes unpushed perf-summary.json files via `aw telemetry push`
9
- //
10
- // Works on BOTH platforms — normalizes the minor stdin schema differences.
11
- // MUST never exit non-zero or block the agent loop.
12
- //
13
- // Installed to ~/.aw/hooks/ by `aw init`. Referenced by:
14
- // ~/.claude/settings.json (Stop hook)
15
- // ~/.cursor/hooks.json (stop hook)
16
-
17
- const { readFileSync, appendFileSync, mkdirSync, existsSync } = require('node:fs');
18
- const { join } = require('node:path');
19
-
20
- // ── API URL resolution ─────────────────────────────────────────────────
21
- // Read from: 1) env var 2) ~/.aw/config.json 3) hardcoded staging
22
- function resolveApiUrl() {
23
- if (process.env.AW_API_URL) return `${process.env.AW_API_URL}/telemetry/ingest`;
24
- // Read from config written by `aw init`
25
- const configPath = join(require('node:os').homedir(), '.aw', 'config.json');
26
- if (existsSync(configPath)) {
27
- try {
28
- const cfg = JSON.parse(readFileSync(configPath, 'utf8'));
29
- if (cfg.api_url) return `${cfg.api_url}/telemetry/ingest`;
30
- } catch { /* corrupted */ }
31
- }
32
- return 'https://staging.services.leadconnectorhq.com/agentic-workspace/telemetry/ingest';
33
- }
34
-
35
- // ── Direct HTTP push (no shelling out to aw CLI) ───────────────────────
36
- async function pushToApi(payload) {
37
- const url = resolveApiUrl();
38
- const controller = new AbortController();
39
- const timer = setTimeout(() => controller.abort(), 8_000);
40
- try {
41
- const res = await fetch(url, {
42
- method: 'POST',
43
- headers: { 'Content-Type': 'application/json' },
44
- body: JSON.stringify(payload),
45
- signal: controller.signal,
46
- });
47
- return res.ok;
48
- } catch {
49
- return false;
50
- } finally {
51
- clearTimeout(timer);
52
- }
53
- }
54
-
55
- // ── Resolve github login for attribution ───────────────────────────────
56
- function resolveGithubLogin() {
57
- // Try .aw_registry/.token first
58
- const tokenPath = join(require('node:os').homedir(), '.aw_registry', '.token');
59
- if (existsSync(tokenPath)) {
60
- try {
61
- const t = JSON.parse(readFileSync(tokenPath, 'utf8'));
62
- if (t.github_login) return t.github_login;
63
- } catch { /* corrupted */ }
64
- }
65
- // Fallback to git email
66
- try {
67
- return require('node:child_process')
68
- .execSync('git config --global user.email', { encoding: 'utf8', stdio: ['pipe','pipe','pipe'] })
69
- .trim() || null;
70
- } catch { return null; }
71
- }
72
-
73
- // ── Token pricing (per 1M tokens) ──────────────────────────────────────
74
- const PRICING = {
75
- // Anthropic Claude
76
- 'claude-haiku': { input: 0.80, output: 4.00 },
77
- 'claude-sonnet': { input: 3.00, output: 15.00 },
78
- 'claude-opus': { input: 15.00, output: 75.00 },
79
- 'haiku': { input: 0.80, output: 4.00 },
80
- 'sonnet': { input: 3.00, output: 15.00 },
81
- 'opus': { input: 15.00, output: 75.00 },
82
- // OpenAI GPT
83
- 'gpt-4o': { input: 2.50, output: 10.00 },
84
- 'gpt-4o-mini': { input: 0.15, output: 0.60 },
85
- 'gpt-4-turbo': { input: 10.00, output: 30.00 },
86
- 'gpt-4': { input: 30.00, output: 60.00 },
87
- 'gpt-3.5': { input: 0.50, output: 1.50 },
88
- 'o1': { input: 15.00, output: 60.00 },
89
- 'o1-mini': { input: 3.00, output: 12.00 },
90
- 'o1-pro': { input: 150.00, output: 600.00 },
91
- 'o3': { input: 10.00, output: 40.00 },
92
- 'o3-mini': { input: 1.10, output: 4.40 },
93
- 'o4-mini': { input: 1.10, output: 4.40 },
94
- // Google Gemini
95
- 'gemini-2.5-pro': { input: 1.25, output: 10.00 },
96
- 'gemini-2.5-flash': { input: 0.15, output: 0.60 },
97
- 'gemini-2.0-flash': { input: 0.10, output: 0.40 },
98
- 'gemini-1.5-pro': { input: 1.25, output: 5.00 },
99
- 'gemini-1.5-flash': { input: 0.075, output: 0.30 },
100
- // Cursor Composer (uses Claude/GPT under the hood — estimate)
101
- 'composer': { input: 3.00, output: 15.00 },
102
- 'composer-2': { input: 3.00, output: 15.00 },
103
- };
104
-
105
- function estimateCost(model, inputTokens, outputTokens) {
106
- const key = Object.keys(PRICING).find(k => (model || '').toLowerCase().includes(k));
107
- const rates = key ? PRICING[key] : PRICING['sonnet'];
108
- return (inputTokens / 1_000_000) * rates.input + (outputTokens / 1_000_000) * rates.output;
109
- }
110
-
111
- // ── Parse stdin ────────────────────────────────────────────────────────
112
- async function readStdin() {
113
- const chunks = [];
114
- for await (const chunk of process.stdin) chunks.push(chunk);
115
- const raw = Buffer.concat(chunks).toString('utf8').trim();
116
- if (!raw) return {};
117
- try { return JSON.parse(raw); } catch { return {}; }
118
- }
119
-
120
- // ── Extract tokens from transcript JSONL ────────────────────────────────
121
- // Claude Code usage entries are cumulative per conversation.
122
- // We take the LAST usage entry (final token count for the session).
123
- function extractFromTranscript(transcriptPath) {
124
- if (!transcriptPath || !existsSync(transcriptPath)) return null;
125
-
126
- try {
127
- const content = readFileSync(transcriptPath, 'utf8');
128
- const lines = content.split('\n').filter(Boolean);
129
-
130
- let lastInputTokens = 0;
131
- let lastOutputTokens = 0;
132
- let lastCacheCreation = 0;
133
- let lastCacheRead = 0;
134
- let model = null;
135
- let command = null;
136
- const agentsUsed = new Set();
137
- const skillsApplied = new Set();
138
- let toolTotal = 0;
139
- let toolFailed = 0;
140
-
141
- for (const line of lines) {
142
- try {
143
- const entry = JSON.parse(line);
144
- if (entry.model) model = entry.model;
145
-
146
- // Token usage — Claude Code format (cumulative)
147
- const usage = entry.usage || entry.message?.usage;
148
- if (usage) {
149
- lastInputTokens = usage.input_tokens || 0;
150
- lastOutputTokens = usage.output_tokens || 0;
151
- lastCacheCreation = usage.cache_creation_input_tokens || 0;
152
- lastCacheRead = usage.cache_read_input_tokens || 0;
153
- }
154
-
155
- // Claude Code nests tool calls inside assistant message.content[]
156
- const contentBlocks = entry.message?.content;
157
- if (Array.isArray(contentBlocks)) {
158
- for (const block of contentBlocks) {
159
- if (!block || block.type !== 'tool_use') continue;
160
- const name = block.name || '';
161
- const inp = block.input || {};
162
-
163
- if (name === 'Skill' && inp.skill) {
164
- command = String(inp.skill).slice(0, 128);
165
- skillsApplied.add(command);
166
- }
167
- if (name === 'Agent' && inp.subagent_type) {
168
- agentsUsed.add(String(inp.subagent_type).slice(0, 128));
169
- }
170
- if (name && name !== 'Skill') {
171
- toolTotal++;
172
- }
173
- }
174
- }
175
-
176
- // Also handle flat tool_name format (Cursor, older Claude)
177
- const toolName = entry.tool_name;
178
- const toolInput = entry.tool_input || {};
179
- if (toolName === 'Skill' && toolInput.skill) {
180
- command = String(toolInput.skill).slice(0, 128);
181
- skillsApplied.add(command);
182
- }
183
- if (toolName === 'Agent' && toolInput.subagent_type) {
184
- agentsUsed.add(String(toolInput.subagent_type).slice(0, 128));
185
- }
186
-
187
- // Count tool failures from tool results
188
- const result = entry.toolUseResult;
189
- if (result && typeof result === 'object') {
190
- const ec = result.exitCode ?? result.exit_code;
191
- if (ec && ec !== 0) toolFailed++;
192
- }
193
- } catch { /* skip malformed lines */ }
194
- }
195
-
196
- // Cursor fallback: estimate tokens from content length (~4 chars/token)
197
- // when no usage data is available (Cursor doesn't write usage entries)
198
- if (lastInputTokens === 0 && lastOutputTokens === 0) {
199
- let inputChars = 0;
200
- let outputChars = 0;
201
- for (const line of lines) {
202
- try {
203
- const entry = JSON.parse(line);
204
- const msgContent = entry.message?.content;
205
- const len = Array.isArray(msgContent)
206
- ? msgContent.reduce((s, c) => s + (typeof c === 'string' ? c.length : JSON.stringify(c).length), 0)
207
- : typeof msgContent === 'string' ? msgContent.length : 0;
208
- if (entry.role === 'user') inputChars += len;
209
- else if (entry.role === 'assistant') outputChars += len;
210
- } catch {}
211
- }
212
- if (inputChars + outputChars > 0) {
213
- lastInputTokens = Math.round(inputChars / 4);
214
- lastOutputTokens = Math.round(outputChars / 4);
215
- }
216
- }
217
-
218
- return {
219
- inputTokens: lastInputTokens,
220
- outputTokens: lastOutputTokens,
221
- cacheCreation: lastCacheCreation,
222
- cacheRead: lastCacheRead,
223
- totalTokens: lastInputTokens + lastOutputTokens + lastCacheCreation + lastCacheRead,
224
- model,
225
- command,
226
- agentsUsed: [...agentsUsed],
227
- skillsApplied: [...skillsApplied],
228
- toolTotal,
229
- toolPassed: toolTotal - toolFailed,
230
- toolFailed,
231
- };
232
- } catch {
233
- return null;
234
- }
235
- }
236
-
237
- // ── Main ────────────────────────────────────────────────────────────────
238
- async function main() {
239
- const input = await readStdin();
240
-
241
- const sessionId = input.session_id || input.conversation_id || 'unknown';
242
- const transcriptPath = input.transcript_path
243
- || process.env.CURSOR_TRANSCRIPT_PATH
244
- || null;
245
- const cwd = input.cwd
246
- || process.env.CURSOR_PROJECT_DIR
247
- || process.env.CLAUDE_PROJECT_DIR
248
- || process.cwd();
249
- const status = input.status || 'completed';
250
- const stdinModel = input.model || null;
251
-
252
- const transcript = extractFromTranscript(transcriptPath);
253
- const model = stdinModel || transcript?.model || 'unknown';
254
-
255
- // ── Token delta: diff current cumulative vs previous snapshot ────────
256
- // Claude Code reports cumulative session totals. By snapshotting after
257
- // each Stop event, we get the per-response delta.
258
- const { writeFileSync } = require('node:fs');
259
- const telemetryDir = join(cwd, '.claude', 'telemetry');
260
- mkdirSync(telemetryDir, { recursive: true });
261
-
262
- const snapshotPath = join(telemetryDir, `.token-snapshot-${sessionId}`);
263
- let prevSnapshot = { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 };
264
- if (existsSync(snapshotPath)) {
265
- try { prevSnapshot = JSON.parse(readFileSync(snapshotPath, 'utf8')); } catch { /* corrupted */ }
266
- }
267
-
268
- const currInput = transcript?.inputTokens || 0;
269
- const currOutput = transcript?.outputTokens || 0;
270
- const currCacheCreation = transcript?.cacheCreation || 0;
271
- const currCacheRead = transcript?.cacheRead || 0;
272
-
273
- // Delta = current cumulative - previous snapshot
274
- const deltaInput = Math.max(0, currInput - (prevSnapshot.input || 0));
275
- const deltaOutput = Math.max(0, currOutput - (prevSnapshot.output || 0));
276
- const deltaCacheCreation = Math.max(0, currCacheCreation - (prevSnapshot.cacheCreation || 0));
277
- const deltaCacheRead = Math.max(0, currCacheRead - (prevSnapshot.cacheRead || 0));
278
- const deltaTotal = deltaInput + deltaOutput + deltaCacheCreation + deltaCacheRead;
279
- const deltaCost = estimateCost(model, deltaInput, deltaOutput);
280
-
281
- // Save current cumulative as snapshot for next time
282
- try {
283
- writeFileSync(snapshotPath, JSON.stringify({
284
- input: currInput, output: currOutput,
285
- cacheCreation: currCacheCreation, cacheRead: currCacheRead,
286
- ts: new Date().toISOString(),
287
- }));
288
- } catch { /* best effort */ }
289
-
290
- // ── 1. Local JSONL (always works, even offline) ─────────────────────
291
- try {
292
- const record = {
293
- ts: new Date().toISOString(),
294
- session_id: sessionId,
295
- model,
296
- input_tokens: deltaInput,
297
- output_tokens: deltaOutput,
298
- cache_creation: deltaCacheCreation,
299
- cache_read: deltaCacheRead,
300
- total_tokens: deltaTotal,
301
- cost_usd: Math.round(deltaCost * 1_000_000) / 1_000_000,
302
- status,
303
- platform: input.cursor_version ? 'cursor' : 'claude',
304
- };
305
- appendFileSync(join(telemetryDir, 'costs.jsonl'), JSON.stringify(record) + '\n');
306
- } catch { /* never block on local write failure */ }
307
-
308
- // ── 2. Push unpushed perf-summary.json from .aw_docs/runs/ ──────────
309
- // Direct HTTP POST — no shelling out to aw CLI (avoids env/path issues).
310
- try {
311
- const { readdirSync } = require('node:fs');
312
- const { homedir } = require('node:os');
313
- const searchPaths = [...new Set([
314
- join(cwd, '.aw_docs', 'runs'),
315
- join(homedir(), '.aw_docs', 'runs'),
316
- ])];
317
-
318
- for (const runsDir of searchPaths) {
319
- if (!existsSync(runsDir)) continue;
320
- const runDirs = readdirSync(runsDir, { withFileTypes: true })
321
- .filter(d => d.isDirectory())
322
- .map(d => join(runsDir, d.name));
323
-
324
- for (const runDir of runDirs) {
325
- const perfPath = join(runDir, 'perf-summary.json');
326
- const pushedPath = join(runDir, '.pushed');
327
- if (!existsSync(perfPath) || existsSync(pushedPath)) continue;
328
-
329
- try {
330
- const perf = JSON.parse(readFileSync(perfPath, 'utf8'));
331
-
332
- // Enrich: hook-derived data fills gaps left by the command
333
- if ((!perf.tokens_used || perf.tokens_used === null) && deltaTotal > 0) {
334
- perf.tokens_used = deltaTotal;
335
- }
336
- if ((!perf.cost_usd || perf.cost_usd === null) && deltaCost > 0) {
337
- perf.cost_usd = Math.round(deltaCost * 1_000_000) / 1_000_000;
338
- }
339
- if (!perf.model && model !== 'unknown') {
340
- perf.model = model;
341
- }
342
- if (!perf.environment) {
343
- perf.environment = input.cursor_version ? 'cursor' : 'claude-code';
344
- }
345
- // Enrich agents/skills from transcript if command left them empty
346
- if ((!perf.agents_used || perf.agents_used.length === 0) && transcript?.agentsUsed?.length) {
347
- perf.agents_used = transcript.agentsUsed;
348
- }
349
- if ((!perf.skills_applied || perf.skills_applied.length === 0) && transcript?.skillsApplied?.length) {
350
- perf.skills_applied = transcript.skillsApplied;
351
- }
352
- // Enrich steps from tool call counts if command left them null/zero
353
- if (!perf.steps_total && transcript?.toolTotal) {
354
- perf.steps_total = transcript.toolTotal;
355
- perf.steps_passed = transcript.toolPassed;
356
- perf.steps_failed = transcript.toolFailed;
357
- }
358
- // Enrich branch if missing
359
- if (!perf.branch || perf.branch === 'no-branch') {
360
- try {
361
- perf.branch = require('node:child_process')
362
- .execSync('git branch --show-current 2>/dev/null', { encoding: 'utf8', cwd })
363
- .trim() || null;
364
- } catch { /* not a git repo */ }
365
- }
366
- writeFileSync(perfPath, JSON.stringify(perf, null, 2) + '\n');
367
-
368
- // Build DTO matching IngestRunDto and POST directly
369
- const dto = {
370
- shell_run_id: perf.run_id,
371
- command: perf.command,
372
- status: perf.status || 'complete',
373
- branch: perf.branch || null,
374
- environment: perf.environment || null,
375
- model: perf.model || null,
376
- duration_ms: perf.duration_ms || null,
377
- tokens_used: perf.tokens_used || null,
378
- cost_usd: perf.cost_usd || null,
379
- first_pass_rate: perf.first_pass_rate || null,
380
- steps_total: perf.steps_total || null,
381
- steps_passed: perf.steps_passed || null,
382
- steps_failed: perf.steps_failed || null,
383
- steps_skipped: perf.steps_skipped || null,
384
- retries_total: perf.retries_total || null,
385
- knowledge_used_count: perf.knowledge_used_count || null,
386
- knowledge_saved_count: perf.knowledge_saved_count || null,
387
- agents_used: perf.agents_used || [],
388
- skills_applied: perf.skills_applied || [],
389
- github_login: resolveGithubLogin(),
390
- };
391
-
392
- if (dto.shell_run_id && dto.command) {
393
- const pushed = await pushToApi(dto);
394
- if (pushed) {
395
- // Clean up: remove perf-summary + run dir after successful push
396
- const { rmSync } = require('node:fs');
397
- try { rmSync(runDir, { recursive: true, force: true }); } catch {}
398
- }
399
- }
400
- } catch { /* best effort */ }
401
- }
402
- }
403
- } catch { /* never block */ }
404
-
405
- process.stdout.write('{}\n');
406
- }
407
-
408
- main().catch(() => {
409
- process.stdout.write('{}\n');
410
- process.exit(0);
411
- });