noctrace 0.7.3 → 0.7.5

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/bin/noctrace.js CHANGED
@@ -8,25 +8,37 @@ const NOCTRACE_PORT = 4117;
8
8
  const NOCTRACE_BASE_URL = `http://localhost:${NOCTRACE_PORT}`;
9
9
  const HOOKS_ENDPOINT = `${NOCTRACE_BASE_URL}/api/hooks`;
10
10
 
11
- /**
12
- * The curl command used as the hook body.
13
- * Reads the hook event JSON from stdin and POSTs it to noctrace.
14
- * `--data-raw "$(cat)"` captures all of stdin and sends it as the request body.
15
- */
16
- const HOOK_COMMAND = `curl -s -X POST ${HOOKS_ENDPOINT} -H 'Content-Type: application/json' --data-raw "$(cat)"`;
17
-
18
11
  /**
19
12
  * The set of Claude Code hook event names noctrace subscribes to.
20
13
  */
21
14
  const HOOK_EVENT_NAMES = [
22
15
  'PostToolUse',
16
+ 'PostToolUseFailure',
23
17
  'SubagentStart',
24
18
  'SubagentStop',
25
19
  'Stop',
26
20
  'PreCompact',
27
21
  'PostCompact',
22
+ 'SessionStart',
23
+ 'SessionEnd',
24
+ 'PermissionRequest',
25
+ 'PermissionDenied',
26
+ 'WorktreeCreate',
27
+ 'WorktreeRemove',
28
28
  ];
29
29
 
30
+ /**
31
+ * Check if a hook entry array contains a noctrace hook (either old command-type or new http-type).
32
+ */
33
+ function hasNoctraceHook(entry) {
34
+ return Array.isArray(entry.hooks) &&
35
+ entry.hooks.some(
36
+ (h) =>
37
+ (h.type === 'command' && typeof h.command === 'string' && h.command.includes(HOOKS_ENDPOINT)) ||
38
+ (h.type === 'http' && h.url === HOOKS_ENDPOINT),
39
+ );
40
+ }
41
+
30
42
  /**
31
43
  * Read and parse ~/.claude/settings.json.
32
44
  * Returns an empty object if the file doesn't exist yet.
@@ -51,9 +63,8 @@ async function writeSettings(settingsPath, settings) {
51
63
 
52
64
  /**
53
65
  * Install noctrace hooks into ~/.claude/settings.json.
54
- * For each hook event name, adds a "command" hook that POSTs the event
55
- * payload to the noctrace HTTP endpoint.
56
- * Skips events that already have a noctrace hook registered.
66
+ * For each hook event name, adds an HTTP hook pointing at the noctrace endpoint.
67
+ * Detects both old command-type and new http-type hooks to avoid duplicates.
57
68
  */
58
69
  async function installHooks() {
59
70
  const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
@@ -69,14 +80,8 @@ async function installHooks() {
69
80
 
70
81
  const existing = settings.hooks[eventName];
71
82
 
72
- // Check whether a noctrace hook is already registered for this event
73
- const alreadyInstalled = existing.some(
74
- (entry) =>
75
- Array.isArray(entry.hooks) &&
76
- entry.hooks.some(
77
- (h) => h.type === 'command' && typeof h.command === 'string' && h.command.includes(HOOKS_ENDPOINT),
78
- ),
79
- );
83
+ // Check whether a noctrace hook is already registered (either old command or new http format)
84
+ const alreadyInstalled = existing.some(hasNoctraceHook);
80
85
 
81
86
  if (alreadyInstalled) {
82
87
  skipped.push(eventName);
@@ -85,8 +90,8 @@ async function installHooks() {
85
90
 
86
91
  existing.push({
87
92
  hooks: [{
88
- type: 'command',
89
- command: HOOK_COMMAND,
93
+ type: 'http',
94
+ url: HOOKS_ENDPOINT,
90
95
  async: true,
91
96
  }],
92
97
  });
@@ -108,8 +113,8 @@ async function installHooks() {
108
113
 
109
114
  /**
110
115
  * Remove noctrace hooks from ~/.claude/settings.json.
111
- * Only removes hooks whose command string contains the noctrace endpoint —
112
- * other hooks for the same events are left untouched.
116
+ * Removes both old command-type and new http-type hooks.
117
+ * Iterates all hook event keys (not just HOOK_EVENT_NAMES) to clean up orphaned entries.
113
118
  */
114
119
  async function uninstallHooks() {
115
120
  const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
@@ -122,20 +127,13 @@ async function uninstallHooks() {
122
127
 
123
128
  const removed = [];
124
129
 
125
- for (const eventName of HOOK_EVENT_NAMES) {
130
+ // Iterate ALL hook event keys (not just current HOOK_EVENT_NAMES) to clean up orphaned old entries
131
+ for (const eventName of Object.keys(settings.hooks)) {
126
132
  const existing = settings.hooks[eventName];
127
133
  if (!Array.isArray(existing)) continue;
128
134
 
129
135
  const before = existing.length;
130
- settings.hooks[eventName] = existing.filter(
131
- (entry) =>
132
- !(
133
- Array.isArray(entry.hooks) &&
134
- entry.hooks.some(
135
- (h) => h.type === 'command' && typeof h.command === 'string' && h.command.includes(HOOKS_ENDPOINT),
136
- )
137
- ),
138
- );
136
+ settings.hooks[eventName] = existing.filter((entry) => !hasNoctraceHook(entry));
139
137
 
140
138
  if (settings.hooks[eventName].length < before) {
141
139
  removed.push(eventName);