noctrace 0.7.5 → 0.8.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,161 @@
1
+ function isObj(val) {
2
+ return typeof val === 'object' && val !== null && !Array.isArray(val);
3
+ }
4
+ /**
5
+ * Extract compaction boundaries with optional metadata from JSONL content.
6
+ * Returns CompactionBoundary[] with trigger type and pre-compaction token count.
7
+ */
8
+ export function parseCompactionBoundaries(content) {
9
+ const lines = content.split('\n');
10
+ const out = [];
11
+ for (const line of lines) {
12
+ const t = line.trim();
13
+ if (!t)
14
+ continue;
15
+ let parsed;
16
+ try {
17
+ parsed = JSON.parse(t);
18
+ }
19
+ catch {
20
+ continue;
21
+ }
22
+ if (!isObj(parsed))
23
+ continue;
24
+ if (parsed['type'] !== 'system')
25
+ continue;
26
+ if (parsed['subtype'] !== 'compact_boundary')
27
+ continue;
28
+ const timestamp = typeof parsed['timestamp'] === 'string'
29
+ ? new Date(parsed['timestamp']).getTime() : Date.now();
30
+ // Extract compact_metadata if present
31
+ const meta = isObj(parsed['compact_metadata']) ? parsed['compact_metadata'] : null;
32
+ const trigger = meta && (meta['trigger'] === 'manual' || meta['trigger'] === 'auto')
33
+ ? meta['trigger'] : null;
34
+ const preTokens = meta && typeof meta['pre_tokens'] === 'number' ? meta['pre_tokens'] : null;
35
+ out.push({ timestamp, trigger, preTokens });
36
+ }
37
+ return out;
38
+ }
39
+ /**
40
+ * Parse the terminal result record from JSONL content.
41
+ * Extracts duration_api_ms, modelUsage, stop_reason, permission_denials.
42
+ */
43
+ export function parseSessionResultMetrics(content) {
44
+ const defaults = {
45
+ durationApiMs: null,
46
+ modelUsage: [],
47
+ stopReason: null,
48
+ permissionDenialCount: 0,
49
+ };
50
+ const lines = content.split('\n');
51
+ for (const line of lines) {
52
+ const t = line.trim();
53
+ if (!t)
54
+ continue;
55
+ let parsed;
56
+ try {
57
+ parsed = JSON.parse(t);
58
+ }
59
+ catch {
60
+ continue;
61
+ }
62
+ if (!isObj(parsed))
63
+ continue;
64
+ if (parsed['type'] !== 'result')
65
+ continue;
66
+ // duration_api_ms
67
+ if (typeof parsed['duration_api_ms'] === 'number') {
68
+ defaults.durationApiMs = parsed['duration_api_ms'];
69
+ }
70
+ // stop_reason
71
+ const sr = parsed['stop_reason'];
72
+ if (sr === 'end_turn' || sr === 'max_tokens' || sr === 'refusal') {
73
+ defaults.stopReason = sr;
74
+ }
75
+ // permission_denials
76
+ if (Array.isArray(parsed['permission_denials'])) {
77
+ defaults.permissionDenialCount = parsed['permission_denials'].length;
78
+ }
79
+ // modelUsage — { [modelName]: { input_tokens, output_tokens, cache_read_input_tokens?, cache_creation_input_tokens? } }
80
+ const mu = parsed['modelUsage'];
81
+ if (isObj(mu)) {
82
+ const entries = [];
83
+ for (const [model, usage] of Object.entries(mu)) {
84
+ if (!isObj(usage))
85
+ continue;
86
+ entries.push({
87
+ model,
88
+ inputTokens: typeof usage['input_tokens'] === 'number' ? usage['input_tokens'] : 0,
89
+ outputTokens: typeof usage['output_tokens'] === 'number' ? usage['output_tokens'] : 0,
90
+ cacheReadTokens: typeof usage['cache_read_input_tokens'] === 'number' ? usage['cache_read_input_tokens'] : 0,
91
+ cacheCreateTokens: typeof usage['cache_creation_input_tokens'] === 'number' ? usage['cache_creation_input_tokens'] : 0,
92
+ });
93
+ }
94
+ defaults.modelUsage = entries;
95
+ }
96
+ // Only one result record per session — stop scanning
97
+ break;
98
+ }
99
+ return defaults;
100
+ }
101
+ /**
102
+ * Parse session init context (agents, skills, plugins, effort) from JSONL content.
103
+ * Reads the first system init record.
104
+ */
105
+ export function parseInitContext(content) {
106
+ const defaults = { agents: [], skills: [], plugins: [], effort: null };
107
+ const lines = content.split('\n');
108
+ for (const line of lines) {
109
+ const t = line.trim();
110
+ if (!t)
111
+ continue;
112
+ let parsed;
113
+ try {
114
+ parsed = JSON.parse(t);
115
+ }
116
+ catch {
117
+ continue;
118
+ }
119
+ if (!isObj(parsed))
120
+ continue;
121
+ if (parsed['type'] !== 'system')
122
+ continue;
123
+ // Match init records — subtype may be 'init' or absent on the first system record
124
+ const subtype = parsed['subtype'];
125
+ if (subtype !== undefined && subtype !== 'init')
126
+ continue;
127
+ // agents
128
+ if (Array.isArray(parsed['agents'])) {
129
+ defaults.agents = parsed['agents'].filter((a) => typeof a === 'string');
130
+ }
131
+ // skills
132
+ if (Array.isArray(parsed['skills'])) {
133
+ defaults.skills = parsed['skills'].filter((s) => typeof s === 'string');
134
+ }
135
+ // plugins
136
+ if (Array.isArray(parsed['plugins'])) {
137
+ for (const p of parsed['plugins']) {
138
+ if (isObj(p) && typeof p['name'] === 'string') {
139
+ defaults.plugins.push({
140
+ name: p['name'],
141
+ path: typeof p['path'] === 'string' ? p['path'] : '',
142
+ });
143
+ }
144
+ }
145
+ }
146
+ // effort level
147
+ if (typeof parsed['effort'] === 'string') {
148
+ defaults.effort = parsed['effort'];
149
+ }
150
+ // Also check nested config field where effort might appear
151
+ if (defaults.effort === null && isObj(parsed['config'])) {
152
+ const config = parsed['config'];
153
+ if (typeof config['effort'] === 'string') {
154
+ defaults.effort = config['effort'];
155
+ }
156
+ }
157
+ // Only read the first init record
158
+ break;
159
+ }
160
+ return defaults;
161
+ }
package/hooks/hooks.json CHANGED
@@ -64,5 +64,38 @@
64
64
  }
65
65
  ]
66
66
  }
67
+ ],
68
+ "SessionStart": [
69
+ {
70
+ "hooks": [
71
+ {
72
+ "type": "command",
73
+ "command": "curl -s -X POST http://localhost:4117/api/hooks -H 'Content-Type: application/json' --data-raw \"$(cat)\"",
74
+ "async": true
75
+ }
76
+ ]
77
+ }
78
+ ],
79
+ "SessionEnd": [
80
+ {
81
+ "hooks": [
82
+ {
83
+ "type": "command",
84
+ "command": "curl -s -X POST http://localhost:4117/api/hooks -H 'Content-Type: application/json' --data-raw \"$(cat)\"",
85
+ "async": true
86
+ }
87
+ ]
88
+ }
89
+ ],
90
+ "PostToolUseFailure": [
91
+ {
92
+ "hooks": [
93
+ {
94
+ "type": "command",
95
+ "command": "curl -s -X POST http://localhost:4117/api/hooks -H 'Content-Type: application/json' --data-raw \"$(cat)\"",
96
+ "async": true
97
+ }
98
+ ]
99
+ }
67
100
  ]
68
101
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "noctrace",
3
- "version": "0.7.5",
3
+ "version": "0.8.0",
4
4
  "description": "Chrome DevTools Network-tab-style waterfall visualizer for Claude Code agent workflows",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -17,17 +17,26 @@
17
17
  "node": ">=20"
18
18
  },
19
19
  "keywords": [
20
- "claude",
20
+ "noctrace",
21
21
  "claude-code",
22
+ "claude",
22
23
  "anthropic",
24
+ "observability",
23
25
  "devtools",
24
26
  "waterfall",
25
- "observability",
26
- "agent",
27
- "ai",
27
+ "ai-agent",
28
28
  "llm",
29
29
  "timeline",
30
- "visualizer"
30
+ "visualizer",
31
+ "claude-code-hooks",
32
+ "token-usage",
33
+ "context-window",
34
+ "agent-monitoring",
35
+ "ai-observability",
36
+ "claude-code-devtools",
37
+ "session-viewer",
38
+ "tool-calls",
39
+ "sub-agents"
31
40
  ],
32
41
  "bin": {
33
42
  "noctrace": "bin/noctrace.js"