@ghl-ai/aw 0.1.37-beta.77 → 0.1.37-beta.79

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.
@@ -8,7 +8,7 @@
8
8
  //
9
9
  // MUST never throw unhandled. Callers (dispatch.mjs) catch, but we guard anyway.
10
10
 
11
- import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, unlinkSync, readdirSync, rmSync } from 'node:fs';
11
+ import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, unlinkSync, readdirSync, rmSync, statSync } from 'node:fs';
12
12
  import { join } from 'node:path';
13
13
  import { homedir } from 'node:os';
14
14
  import { createHash } from 'node:crypto';
@@ -155,6 +155,13 @@ function normalizeCursorInput(input) {
155
155
 
156
156
  // ── Helpers ──────────────────────────────────────────────────────────────────
157
157
 
158
+ function resolveSessionId(input) {
159
+ return input?.session_id
160
+ || input?.conversation_id
161
+ || process.env.CODEX_THREAD_ID
162
+ || 'unknown';
163
+ }
164
+
158
165
  function resolveApiUrl() {
159
166
  if (process.env.AW_API_URL) return `${process.env.AW_API_URL}/telemetry/ingest`;
160
167
  const configPath = join(HOME, '.aw', 'config.json');
@@ -193,6 +200,9 @@ function resolveNamespace() {
193
200
 
194
201
  function detectPlatform(input) {
195
202
  if (input?.hook_event_name || input?.cursor_version || input?.conversation_id) return 'cursor';
203
+ if (input?.platform) return input.platform;
204
+ if (input?.transcript_path) return 'claude-code';
205
+ if (input?.session_id && !process.env.CODEX_THREAD_ID) return 'claude-code';
196
206
  if (process.env.CLAUDECODE) return 'claude-code';
197
207
  if (process.env.CURSOR_TRACE_ID || process.env.CURSOR_PROJECT_DIR) return 'cursor';
198
208
  if (Object.keys(process.env).some(k => k.startsWith('WINDSURF_'))) return 'windsurf';
@@ -200,6 +210,61 @@ function detectPlatform(input) {
200
210
  return 'claude-code';
201
211
  }
202
212
 
213
+ function findCodexTranscriptPath(sessionId) {
214
+ if (!sessionId || sessionId === 'unknown') return null;
215
+
216
+ const sessionsRoot = join(HOME, '.codex', 'sessions');
217
+ if (!existsSync(sessionsRoot)) return null;
218
+
219
+ const stack = [sessionsRoot];
220
+ let newestMatch = null;
221
+ let newestStamp = 0;
222
+
223
+ while (stack.length > 0) {
224
+ const dir = stack.pop();
225
+ let entries = [];
226
+ try {
227
+ entries = readdirSync(dir, { withFileTypes: true });
228
+ } catch {
229
+ continue;
230
+ }
231
+
232
+ for (const entry of entries) {
233
+ const fullPath = join(dir, entry.name);
234
+ if (entry.isDirectory()) {
235
+ stack.push(fullPath);
236
+ continue;
237
+ }
238
+ if (!entry.isFile() || !entry.name.endsWith('.jsonl')) continue;
239
+ if (!entry.name.includes(sessionId)) continue;
240
+
241
+ try {
242
+ const stamp = statSync(fullPath).mtimeMs;
243
+ if (Number.isFinite(stamp) && stamp >= newestStamp) {
244
+ newestStamp = stamp;
245
+ newestMatch = fullPath;
246
+ }
247
+ } catch {
248
+ newestMatch = newestMatch || fullPath;
249
+ }
250
+ }
251
+ }
252
+
253
+ return newestMatch;
254
+ }
255
+
256
+ function classifyError(errStr) {
257
+ if (!errStr) return { errorType: null, errorMessage: null };
258
+ const errorMessage = String(errStr).slice(0, 1024);
259
+ let errorType = 'unknown';
260
+ if (/rate.?limit|429|too many requests/i.test(errorMessage)) errorType = 'rate_limit';
261
+ else if (/context.?(overflow|length|window|limit|too long)/i.test(errorMessage)) errorType = 'context_overflow';
262
+ else if (/timeout|timed?\s*out|ETIMEDOUT/i.test(errorMessage)) errorType = 'timeout';
263
+ else if (/cancel|abort|interrupt/i.test(errorMessage)) errorType = 'user_cancel';
264
+ else if (/tool.?(fail|error)|execution.?error|process exited with code [1-9]/i.test(errorMessage)) errorType = 'tool_failure';
265
+ return { errorType, errorMessage };
266
+ }
267
+
203
268
  function computeProjectHash(cwd) {
204
269
  try {
205
270
  const remote = execSync('git remote get-url origin 2>/dev/null', {
@@ -361,14 +426,7 @@ function extractFromTranscript(transcriptPath) {
361
426
  const entryError = entry.error || entry.message?.error;
362
427
  if (entryError) {
363
428
  const errStr = typeof entryError === 'string' ? entryError : (entryError.message || JSON.stringify(entryError));
364
- errorMessage = errStr.slice(0, 1024);
365
- // Classify error type
366
- if (/rate.?limit|429|too many requests/i.test(errStr)) errorType = 'rate_limit';
367
- else if (/context.?(overflow|length|window|limit|too long)/i.test(errStr)) errorType = 'context_overflow';
368
- else if (/timeout|timed?\s*out|ETIMEDOUT/i.test(errStr)) errorType = 'timeout';
369
- else if (/cancel|abort|interrupt/i.test(errStr)) errorType = 'user_cancel';
370
- else if (/tool.?(fail|error)|execution.?error/i.test(errStr)) errorType = 'tool_failure';
371
- else errorType = 'unknown';
429
+ ({ errorType, errorMessage } = classifyError(errStr));
372
430
  }
373
431
  } catch { /* skip malformed lines */ }
374
432
  }
@@ -417,6 +475,129 @@ function extractFromTranscript(transcriptPath) {
417
475
  }
418
476
  }
419
477
 
478
+ function extractFromCodexSession(transcriptPath) {
479
+ if (!transcriptPath || !existsSync(transcriptPath)) return null;
480
+
481
+ try {
482
+ const content = readFileSync(transcriptPath, 'utf8');
483
+ const lines = content.split('\n').filter(Boolean);
484
+
485
+ let lastInputTokens = 0;
486
+ let lastOutputTokens = 0;
487
+ let lastCacheCreation = 0;
488
+ let lastCacheRead = 0;
489
+ let model = null;
490
+ let command = null;
491
+ let cwd = null;
492
+ let platformVersion = null;
493
+ const agentsUsed = new Set();
494
+ const skillsApplied = new Set();
495
+ const mcpToolsUsed = new Set();
496
+ const prUrls = new Set();
497
+ let toolTotal = 0;
498
+ let toolFailed = 0;
499
+ let errorType = null;
500
+ let errorMessage = null;
501
+
502
+ for (const line of lines) {
503
+ let entry;
504
+ try {
505
+ entry = JSON.parse(line);
506
+ } catch {
507
+ continue;
508
+ }
509
+
510
+ if (entry.type === 'session_meta') {
511
+ const meta = entry.payload || {};
512
+ cwd = meta.cwd || cwd;
513
+ platformVersion = meta.cli_version || platformVersion;
514
+ }
515
+
516
+ if (entry.type === 'event_msg' && entry.payload?.type === 'token_count') {
517
+ const totals = entry.payload?.info?.total_token_usage || {};
518
+ lastInputTokens = totals.input_tokens || 0;
519
+ lastOutputTokens = (totals.output_tokens || 0) + (totals.reasoning_output_tokens || 0);
520
+ lastCacheRead = totals.cached_input_tokens || 0;
521
+ }
522
+
523
+ if (entry.type === 'response_item') {
524
+ const payload = entry.payload || {};
525
+ if (payload.type === 'function_call') {
526
+ const name = payload.name || '';
527
+ const argsRaw = typeof payload.arguments === 'string' ? payload.arguments : JSON.stringify(payload.arguments || {});
528
+
529
+ if (name) toolTotal++;
530
+ if (name === 'spawn_agent') {
531
+ try {
532
+ const parsed = JSON.parse(argsRaw || '{}');
533
+ const agentType = parsed.agent_type || parsed.model || 'spawn_agent';
534
+ agentsUsed.add(String(agentType).slice(0, 128));
535
+ } catch {
536
+ agentsUsed.add('spawn_agent');
537
+ }
538
+ }
539
+ if (name === 'Skill') {
540
+ try {
541
+ const parsed = JSON.parse(argsRaw || '{}');
542
+ if (parsed.skill) {
543
+ command = String(parsed.skill).slice(0, 128);
544
+ skillsApplied.add(command);
545
+ }
546
+ } catch { /* ignore */ }
547
+ }
548
+ if (name.startsWith('mcp__')) {
549
+ mcpToolsUsed.add(name.slice(0, 128));
550
+ }
551
+ scanForPrUrls(argsRaw, prUrls);
552
+ }
553
+
554
+ if (payload.type === 'function_call_output') {
555
+ const outputText = String(payload.output || '');
556
+ scanForPrUrls(outputText, prUrls);
557
+ const failureMatch = outputText.match(/Process exited with code\s+(\d+)/i);
558
+ if (failureMatch) {
559
+ const exitCode = Number.parseInt(failureMatch[1], 10);
560
+ if (Number.isFinite(exitCode) && exitCode !== 0) {
561
+ toolFailed++;
562
+ ({ errorType, errorMessage } = classifyError(outputText));
563
+ }
564
+ }
565
+ }
566
+
567
+ if (payload.type === 'message') {
568
+ const textContent = Array.isArray(payload.content)
569
+ ? payload.content.map(part => part?.text || '').join('\n')
570
+ : '';
571
+ scanForPrUrls(textContent, prUrls);
572
+ }
573
+ }
574
+ }
575
+
576
+ return {
577
+ inputTokens: lastInputTokens,
578
+ outputTokens: lastOutputTokens,
579
+ cacheCreation: lastCacheCreation,
580
+ cacheRead: lastCacheRead,
581
+ totalTokens: lastInputTokens + lastOutputTokens + lastCacheCreation + lastCacheRead,
582
+ model,
583
+ command,
584
+ cwd,
585
+ platformVersion,
586
+ agentsUsed: [...agentsUsed],
587
+ skillsApplied: [...skillsApplied],
588
+ mcpToolsUsed: [...mcpToolsUsed],
589
+ toolTotal,
590
+ toolPassed: Math.max(0, toolTotal - toolFailed),
591
+ toolFailed,
592
+ prUrls: [...prUrls],
593
+ errorType,
594
+ errorMessage,
595
+ };
596
+ } catch {
597
+ return null;
598
+ }
599
+ }
600
+
420
601
  /**
421
602
  * Estimate token counts from Cursor's inline response text.
422
603
  * Cursor doesn't provide token usage, so we estimate ~4 chars/token.
@@ -647,8 +828,14 @@ async function pushPerfSummaries(cwd, transcript, deltaTotal, deltaCost, model,
647
828
  */
648
829
  export async function handleSessionStart(rawInput) {
649
830
  const input = normalizeCursorInput(rawInput);
650
- const sessionId = input.session_id || `s-${Date.now()}`;
651
- const cwd = input.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
831
+ const sessionId = resolveSessionId(input);
832
+ const codexTranscriptPath = detectPlatform(input) === 'codex'
833
+ ? findCodexTranscriptPath(sessionId)
834
+ : null;
835
+ const codexTranscript = codexTranscriptPath
836
+ ? extractFromCodexSession(codexTranscriptPath)
837
+ : null;
838
+ const cwd = input.cwd || codexTranscript?.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
652
839
 
653
840
  // Refresh pricing from LiteLLM if stale (>24h). Non-blocking — if it fails,
654
841
  // existing cached pricing or hardcoded fallback is used.
@@ -659,10 +846,10 @@ export async function handleSessionStart(rawInput) {
659
846
  branch: getBranch(cwd),
660
847
  project_hash: computeProjectHash(cwd),
661
848
  platform: detectPlatform(rawInput),
662
- platform_version: input.platform_version || null,
849
+ platform_version: input.platform_version || codexTranscript?.platformVersion || null,
663
850
  namespace: resolveNamespace(),
664
851
  start_ts: new Date().toISOString(),
665
- model: input.model || null,
852
+ model: input.model || codexTranscript?.model || null,
666
853
  cwd,
667
854
  entry_id: 0, // monotonic counter for costs.jsonl entries
668
855
  last_flushed_entry: 0, // checkpoint: last entry_id pushed to API
@@ -682,17 +869,25 @@ export async function handleSessionStart(rawInput) {
682
869
  */
683
870
  export async function handleStop(rawInput) {
684
871
  const input = normalizeCursorInput(rawInput);
685
- const sessionId = input.session_id || 'unknown';
686
- const transcriptPath = input.transcript_path || process.env.CURSOR_TRANSCRIPT_PATH || null;
872
+ const sessionId = resolveSessionId(input);
873
+ const platform = detectPlatform(rawInput);
874
+ const transcriptPath = input.transcript_path
875
+ || process.env.CURSOR_TRANSCRIPT_PATH
876
+ || (platform === 'codex' ? findCodexTranscriptPath(sessionId) : null);
687
877
  const cwd = input.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
688
878
  const status = input.status || 'completed';
689
879
  const stdinModel = input.model || null;
690
880
 
691
881
  // For Cursor: if no transcript file, estimate from inline text
692
- let transcript = extractFromTranscript(transcriptPath);
882
+ let transcript = platform === 'codex'
883
+ ? extractFromCodexSession(transcriptPath)
884
+ : extractFromTranscript(transcriptPath);
693
885
  if (!transcript && input._cursor_text) {
694
886
  transcript = estimateFromCursorText(input._cursor_text, stdinModel);
695
887
  }
888
+ if (!transcript && platform === 'codex' && transcriptPath) {
889
+ transcript = extractFromCodexSession(transcriptPath);
890
+ }
696
891
  const model = stdinModel || transcript?.model || 'unknown';
697
892
 
698
893
  // ── Token delta via snapshot diffing ─────────────────────────────────────
@@ -726,6 +921,16 @@ export async function handleStop(rawInput) {
726
921
 
727
922
  // ── Append to costs.jsonl with incrementing entry_id ───────────────────
728
923
  let session = readSession(sessionId);
924
+ if (!session && platform === 'codex') {
925
+ await handleSessionStart({
926
+ ...input,
927
+ session_id: sessionId,
928
+ cwd: input.cwd || transcript?.cwd || cwd,
929
+ model: stdinModel || transcript?.model || null,
930
+ platform_version: input.platform_version || transcript?.platformVersion || null,
931
+ });
932
+ session = readSession(sessionId);
933
+ }
729
934
  const entryId = (session?.entry_id || 0) + 1;
730
935
 
731
936
  try {
@@ -742,7 +947,7 @@ export async function handleStop(rawInput) {
742
947
  total_tokens: deltaTotal,
743
948
  cost_usd: Math.round(deltaCost * 1_000_000) / 1_000_000,
744
949
  status,
745
- platform: detectPlatform(rawInput),
950
+ platform,
746
951
  };
747
952
  appendFileSync(costPath, JSON.stringify(record) + '\n');
748
953
  } catch { /* never block */ }
@@ -751,6 +956,8 @@ export async function handleStop(rawInput) {
751
956
  if (session) {
752
957
  session.entry_id = entryId;
753
958
  session.model = model !== 'unknown' ? model : session.model;
959
+ session.platform_version = input.platform_version || transcript?.platformVersion || session.platform_version;
960
+ session.cwd = transcript?.cwd || session.cwd;
754
961
 
755
962
  // Merge PR URLs
756
963
  if (transcript?.prUrls?.length) {
package/integrate.mjs CHANGED
@@ -669,18 +669,43 @@ export function installIdeHooks() {
669
669
  for (const [event, { cmd, timeout }] of Object.entries(codexEvents)) {
670
670
  if (!codexHooksFile.hooks[event]) codexHooksFile.hooks[event] = [];
671
671
 
672
- // Remove old aw entries
673
- codexHooksFile.hooks[event] = codexHooksFile.hooks[event].filter(h =>
674
- !h.command?.includes('.aw/hooks/')
675
- );
672
+ // Codex expects grouped hook entries:
673
+ // { matcher?, hooks: [{ type, command, timeoutSec, statusMessage }] }
674
+ // Preserve non-AW groups and non-AW hooks inside mixed groups.
675
+ const existingGroups = Array.isArray(codexHooksFile.hooks[event]) ? codexHooksFile.hooks[event] : [];
676
+ const preservedGroups = existingGroups
677
+ .map(group => {
678
+ if (!group || !Array.isArray(group.hooks)) return null;
679
+ const hooks = group.hooks.filter(h => !h.command?.includes('.aw/hooks/'));
680
+ return hooks.length > 0 ? { ...group, hooks } : null;
681
+ })
682
+ .filter(Boolean);
676
683
 
677
- // Codex hooks are flat objects: { command, type, timeoutSec, statusMessage }
678
- codexHooksFile.hooks[event].push({
684
+ const awHook = {
679
685
  type: 'command',
680
686
  command: cmd,
681
687
  timeoutSec: timeout,
682
688
  statusMessage: `AW telemetry (${event})`,
683
- });
689
+ };
690
+
691
+ const awGroup = event === 'SessionStart'
692
+ ? { matcher: 'startup', hooks: [awHook] }
693
+ : { hooks: [awHook] };
694
+
695
+ codexHooksFile.hooks[event] = [...preservedGroups, awGroup];
696
+
697
+ // Clean up incorrect top-level event wrappers from prior installs.
698
+ if (Array.isArray(codexHooksFile[event])) {
699
+ const cleaned = codexHooksFile[event]
700
+ .map(group => {
701
+ if (!group || !Array.isArray(group.hooks)) return null;
702
+ const hooks = group.hooks.filter(h => !h.command?.includes('.aw/hooks/'));
703
+ return hooks.length > 0 ? { ...group, hooks } : null;
704
+ })
705
+ .filter(Boolean);
706
+ if (cleaned.length > 0) codexHooksFile[event] = cleaned;
707
+ else delete codexHooksFile[event];
708
+ }
684
709
  }
685
710
 
686
711
  writeFileSync(codexHooksPath, JSON.stringify(codexHooksFile, null, 2) + '\n');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.37-beta.77",
3
+ "version": "0.1.37-beta.79",
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",