toolfunnel 0.7.0 → 0.7.1
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/README.md +228 -235
- package/SECURITY.md +84 -84
- package/bin/toolfunnel.js +451 -451
- package/docs/KNOWN_BUGS.md +80 -80
- package/docs/design.md +84 -84
- package/docs/packaging.md +115 -115
- package/hooks/hooks.manifest.json +27 -27
- package/hooks/scripts/example-audit.js +21 -21
- package/hooks/scripts/example-deny-dangerous.js +39 -39
- package/mcp/expose.example.json +30 -30
- package/mcp/servers/mock-modern/server.js +147 -147
- package/mcp/servers/mock-upstream/server.js +380 -380
- package/package.json +67 -67
- package/src/auth/config.js +174 -174
- package/src/auth/install.js +181 -181
- package/src/auth/resource-server.js +353 -353
- package/src/core/child-reaper.js +61 -61
- package/src/core/config-home.js +134 -134
- package/src/core/events.js +105 -105
- package/src/core/hook-engine.js +329 -329
- package/src/core/hook-loader.js +824 -824
- package/src/core/hook-runner.js +566 -566
- package/src/core/logger.js +297 -297
- package/src/core/matcher.js +100 -100
- package/src/core/metrics.js +75 -75
- package/src/core/rename-migration.js +163 -163
- package/src/core/requires.js +138 -138
- package/src/core/server-config.js +98 -98
- package/src/extend/add-hook.md +152 -152
- package/src/extend/add-mcp.md +119 -119
- package/src/extend/configure.md +88 -88
- package/src/extend/create-tool.md +168 -168
- package/src/extend/howto.js +102 -102
- package/src/extend/package.md +190 -190
- package/src/extend/wrap.md +99 -99
- package/src/mcp/aggregator.js +1719 -1674
- package/src/mcp/expose-store.js +577 -577
- package/src/mcp/gated-run.js +210 -210
- package/src/mcp/http-transport.js +1563 -1563
- package/src/mcp/mcp-client.js +1070 -1070
- package/src/mcp/modern.js +406 -406
- package/src/mcp/protocol.js +468 -468
- package/src/mcp/server.js +3112 -3112
- package/src/tools/registry.js +661 -661
- package/src/tools/tool-state.js +170 -170
- package/src/ui/public/app.js +1187 -1187
- package/src/ui/public/index.html +286 -286
- package/src/ui/public/styles.css +535 -535
- package/src/ui/server.js +1616 -1616
- package/tools/scripts/base64.js +139 -139
- package/tools/scripts/danger.js +61 -61
- package/tools/scripts/echo.js +39 -39
- package/tools/scripts/hash.js +101 -101
- package/tools/scripts/json.js +109 -109
- package/tools/scripts/text-stats.js +131 -131
- package/tools/scripts/tf-env.js +43 -43
- package/tools/scripts/tf-hook-add.js +151 -151
- package/tools/scripts/tf-hook-set.js +120 -120
- package/tools/scripts/tf-list.js +192 -192
- package/tools/scripts/tf-log.js +86 -86
- package/tools/scripts/tf-mcp-add.js +117 -117
- package/tools/scripts/tf-mcp-set.js +121 -121
- package/tools/scripts/tf-pack.js +237 -237
- package/tools/scripts/tf-tool-add.js +134 -134
- package/tools/scripts/tf-tool-set.js +142 -142
- package/tools/scripts/tf-wrap.js +103 -103
- package/tools/scripts/uuid.js +102 -102
package/src/core/matcher.js
CHANGED
|
@@ -1,100 +1,100 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* matcher.js - does a hook's matcher string fire for a given tool?
|
|
5
|
-
*
|
|
6
|
-
* Mirrors Claude Code's matcher semantics:
|
|
7
|
-
* - "" | undefined | "*" -> always fire (wildcard).
|
|
8
|
-
* - otherwise the matcher is a regex, anchored as a FULL match against toolName
|
|
9
|
-
* (e.g. "Bash|Write|Edit").
|
|
10
|
-
* - tool-less events (SessionStart, UserPromptSubmit, Stop, PreCompact) pass
|
|
11
|
-
* toolName == null/undefined -> always fire (the matcher is ignored).
|
|
12
|
-
*
|
|
13
|
-
* CommonJS only. Zero host imports (core/ must run headless under node --test).
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
// One compile per distinct matcher string. matches() sits on the gate's hot path (every
|
|
17
|
-
// hook x every tool call), and rebuilding `^(?:Bash|Write|Edit)$` per call was pure waste:
|
|
18
|
-
// RegExp construction is a deterministic function of the string and ours carries no flags
|
|
19
|
-
// (no g/y -> no lastIndex state), so memoising by the string can never change an answer.
|
|
20
|
-
// Invalidation is by construction, not by event: a reloaded manifest's new matcher is a
|
|
21
|
-
// NEW string -> a new key; the old entry just goes unused. Failed compiles memoise too
|
|
22
|
-
// (a malformed matcher answers false without a fresh throw per call). The cap is a
|
|
23
|
-
// backstop against degenerate dynamically-generated matchers: past it the memo resets -
|
|
24
|
-
// worst case is exactly the old per-call compile, never unbounded growth or a wrong fire.
|
|
25
|
-
const COMPILE_CACHE_MAX = 256;
|
|
26
|
-
const compileCache = new Map(); // String(matcher) -> { re: RegExp|null, err: string|null }
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* The memoised compile shared by matches() / matcherError(). Keyed on String(matcher) so a
|
|
30
|
-
* non-string matcher coerces exactly as the template literal always did.
|
|
31
|
-
* @param {*} matcher
|
|
32
|
-
* @returns {{ re: RegExp|null, err: string|null }}
|
|
33
|
-
*/
|
|
34
|
-
function compiled(matcher) {
|
|
35
|
-
const key = String(matcher);
|
|
36
|
-
let hit = compileCache.get(key);
|
|
37
|
-
if (hit) return hit;
|
|
38
|
-
try {
|
|
39
|
-
hit = { re: new RegExp(`^(?:${key})$`), err: null };
|
|
40
|
-
} catch (err) {
|
|
41
|
-
hit = { re: null, err: (err && err.message) || String(err) };
|
|
42
|
-
}
|
|
43
|
-
if (compileCache.size >= COMPILE_CACHE_MAX) compileCache.clear();
|
|
44
|
-
compileCache.set(key, hit);
|
|
45
|
-
return hit;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* @param {string|undefined|null} matcher the hook's matcher string.
|
|
50
|
-
* @param {string|undefined|null} toolName the tool the model requested, or
|
|
51
|
-
* null/undefined for events without a tool.
|
|
52
|
-
* @returns {boolean} true if the hook should fire for this tool.
|
|
53
|
-
*/
|
|
54
|
-
function matches(matcher, toolName) {
|
|
55
|
-
// Wildcard / unset matcher -> always fire, regardless of tool.
|
|
56
|
-
if (matcher === undefined || matcher === null || matcher === '' || matcher === '*') {
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Tool-less events: there is nothing to match against, so the matcher is
|
|
61
|
-
// ignored and the hook always fires. This is checked AFTER the wildcard
|
|
62
|
-
// case so a "*" matcher is handled uniformly, and BEFORE we attempt a regex
|
|
63
|
-
// (which would otherwise need a string to test).
|
|
64
|
-
if (toolName === undefined || toolName === null) {
|
|
65
|
-
return true;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Anything else is a regex, anchored as a full match against toolName.
|
|
69
|
-
// We wrap in ^(?:...)$ so alternations like "Bash|Write" mean "the whole tool
|
|
70
|
-
// name is one of these", not "contains one of these".
|
|
71
|
-
const c = compiled(matcher);
|
|
72
|
-
if (!c.re) {
|
|
73
|
-
// A malformed matcher must never throw out of the engine, and matches() cannot
|
|
74
|
-
// claim a fire it could not evaluate - so this answers false. That is NOT
|
|
75
|
-
// permission to proceed: callers that gate on the answer must distinguish
|
|
76
|
-
// "did not match" from "could not be read" via matcherError() (hook-engine
|
|
77
|
-
// denies the call; hook-loader.addEntry refuses to store the matcher at all).
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return c.re.test(toolName);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Why does this matcher NOT compile? Returns null for a wildcard/unset matcher or one that
|
|
86
|
-
* compiles under the same ^(?:...)$ anchoring matches() applies; otherwise the compile error's
|
|
87
|
-
* message. This is the discriminator between "did not match" and "could not be read":
|
|
88
|
-
* authoring surfaces (hook-loader.addEntry, and through it the UI and tf_hook_add) use it to
|
|
89
|
-
* REFUSE a pattern that can never be evaluated, and hook-engine.fire treats a non-null answer
|
|
90
|
-
* on a tool-bearing event as an unreadable gate and denies the call (matcher-compile.test.js).
|
|
91
|
-
*
|
|
92
|
-
* @param {string|undefined|null} m the hook's matcher string.
|
|
93
|
-
* @returns {string|null} the compile problem, or null when the matcher is usable.
|
|
94
|
-
*/
|
|
95
|
-
function matcherError(m) {
|
|
96
|
-
if (m === undefined || m === null || m === '' || m === '*') return null;
|
|
97
|
-
return compiled(m).err;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
module.exports = { matches, matcherError };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* matcher.js - does a hook's matcher string fire for a given tool?
|
|
5
|
+
*
|
|
6
|
+
* Mirrors Claude Code's matcher semantics:
|
|
7
|
+
* - "" | undefined | "*" -> always fire (wildcard).
|
|
8
|
+
* - otherwise the matcher is a regex, anchored as a FULL match against toolName
|
|
9
|
+
* (e.g. "Bash|Write|Edit").
|
|
10
|
+
* - tool-less events (SessionStart, UserPromptSubmit, Stop, PreCompact) pass
|
|
11
|
+
* toolName == null/undefined -> always fire (the matcher is ignored).
|
|
12
|
+
*
|
|
13
|
+
* CommonJS only. Zero host imports (core/ must run headless under node --test).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// One compile per distinct matcher string. matches() sits on the gate's hot path (every
|
|
17
|
+
// hook x every tool call), and rebuilding `^(?:Bash|Write|Edit)$` per call was pure waste:
|
|
18
|
+
// RegExp construction is a deterministic function of the string and ours carries no flags
|
|
19
|
+
// (no g/y -> no lastIndex state), so memoising by the string can never change an answer.
|
|
20
|
+
// Invalidation is by construction, not by event: a reloaded manifest's new matcher is a
|
|
21
|
+
// NEW string -> a new key; the old entry just goes unused. Failed compiles memoise too
|
|
22
|
+
// (a malformed matcher answers false without a fresh throw per call). The cap is a
|
|
23
|
+
// backstop against degenerate dynamically-generated matchers: past it the memo resets -
|
|
24
|
+
// worst case is exactly the old per-call compile, never unbounded growth or a wrong fire.
|
|
25
|
+
const COMPILE_CACHE_MAX = 256;
|
|
26
|
+
const compileCache = new Map(); // String(matcher) -> { re: RegExp|null, err: string|null }
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The memoised compile shared by matches() / matcherError(). Keyed on String(matcher) so a
|
|
30
|
+
* non-string matcher coerces exactly as the template literal always did.
|
|
31
|
+
* @param {*} matcher
|
|
32
|
+
* @returns {{ re: RegExp|null, err: string|null }}
|
|
33
|
+
*/
|
|
34
|
+
function compiled(matcher) {
|
|
35
|
+
const key = String(matcher);
|
|
36
|
+
let hit = compileCache.get(key);
|
|
37
|
+
if (hit) return hit;
|
|
38
|
+
try {
|
|
39
|
+
hit = { re: new RegExp(`^(?:${key})$`), err: null };
|
|
40
|
+
} catch (err) {
|
|
41
|
+
hit = { re: null, err: (err && err.message) || String(err) };
|
|
42
|
+
}
|
|
43
|
+
if (compileCache.size >= COMPILE_CACHE_MAX) compileCache.clear();
|
|
44
|
+
compileCache.set(key, hit);
|
|
45
|
+
return hit;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {string|undefined|null} matcher the hook's matcher string.
|
|
50
|
+
* @param {string|undefined|null} toolName the tool the model requested, or
|
|
51
|
+
* null/undefined for events without a tool.
|
|
52
|
+
* @returns {boolean} true if the hook should fire for this tool.
|
|
53
|
+
*/
|
|
54
|
+
function matches(matcher, toolName) {
|
|
55
|
+
// Wildcard / unset matcher -> always fire, regardless of tool.
|
|
56
|
+
if (matcher === undefined || matcher === null || matcher === '' || matcher === '*') {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Tool-less events: there is nothing to match against, so the matcher is
|
|
61
|
+
// ignored and the hook always fires. This is checked AFTER the wildcard
|
|
62
|
+
// case so a "*" matcher is handled uniformly, and BEFORE we attempt a regex
|
|
63
|
+
// (which would otherwise need a string to test).
|
|
64
|
+
if (toolName === undefined || toolName === null) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Anything else is a regex, anchored as a full match against toolName.
|
|
69
|
+
// We wrap in ^(?:...)$ so alternations like "Bash|Write" mean "the whole tool
|
|
70
|
+
// name is one of these", not "contains one of these".
|
|
71
|
+
const c = compiled(matcher);
|
|
72
|
+
if (!c.re) {
|
|
73
|
+
// A malformed matcher must never throw out of the engine, and matches() cannot
|
|
74
|
+
// claim a fire it could not evaluate - so this answers false. That is NOT
|
|
75
|
+
// permission to proceed: callers that gate on the answer must distinguish
|
|
76
|
+
// "did not match" from "could not be read" via matcherError() (hook-engine
|
|
77
|
+
// denies the call; hook-loader.addEntry refuses to store the matcher at all).
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return c.re.test(toolName);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Why does this matcher NOT compile? Returns null for a wildcard/unset matcher or one that
|
|
86
|
+
* compiles under the same ^(?:...)$ anchoring matches() applies; otherwise the compile error's
|
|
87
|
+
* message. This is the discriminator between "did not match" and "could not be read":
|
|
88
|
+
* authoring surfaces (hook-loader.addEntry, and through it the UI and tf_hook_add) use it to
|
|
89
|
+
* REFUSE a pattern that can never be evaluated, and hook-engine.fire treats a non-null answer
|
|
90
|
+
* on a tool-bearing event as an unreadable gate and denies the call (matcher-compile.test.js).
|
|
91
|
+
*
|
|
92
|
+
* @param {string|undefined|null} m the hook's matcher string.
|
|
93
|
+
* @returns {string|null} the compile problem, or null when the matcher is usable.
|
|
94
|
+
*/
|
|
95
|
+
function matcherError(m) {
|
|
96
|
+
if (m === undefined || m === null || m === '' || m === '*') return null;
|
|
97
|
+
return compiled(m).err;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = { matches, matcherError };
|
package/src/core/metrics.js
CHANGED
|
@@ -1,75 +1,75 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* metrics.js - in-memory, per-process tool-call counters (lightweight observability).
|
|
5
|
-
*
|
|
6
|
-
* A privileged gateway should be able to answer "what has run through me, and how often did it
|
|
7
|
-
* fail?" without standing up a metrics stack. This is the zero-dependency MVP of that: process-
|
|
8
|
-
* lifetime counters incremented at the single tools/call chokepoint and surfaced via the HTTP
|
|
9
|
-
* `/health` endpoint and the admin UI. It is NOT persistent (counts reset when the host restarts)
|
|
10
|
-
* and NOT a substitute for the JSONL audit log (src/core/logger.js, which records individual events
|
|
11
|
-
* to disk when enabled) - the two are complementary: the log is the durable per-event record, this
|
|
12
|
-
* is the cheap live aggregate.
|
|
13
|
-
*
|
|
14
|
-
* Design rules (same discipline as logger.js):
|
|
15
|
-
* - Zero dependencies. Plain objects + integers.
|
|
16
|
-
* - record() / snapshot() NEVER throw - instrumentation must never break a tool call.
|
|
17
|
-
* - In-memory only; no files, no timers, no network.
|
|
18
|
-
*
|
|
19
|
-
* CommonJS only.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
const state = {
|
|
23
|
-
startedAt: new Date().toISOString(),
|
|
24
|
-
calls: 0,
|
|
25
|
-
errors: 0,
|
|
26
|
-
byTool: Object.create(null), // name -> { calls, errors }
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* record - count one tools/call outcome. `ok:false` (an isError result - a tool failure OR a
|
|
31
|
-
* PreToolUse denial, which both surface as an error envelope) increments the error counters.
|
|
32
|
-
* @param {{ tool?:string, ok?:boolean }} ev
|
|
33
|
-
*/
|
|
34
|
-
function record(ev) {
|
|
35
|
-
try {
|
|
36
|
-
const tool = ev && typeof ev.tool === 'string' && ev.tool.length > 0 ? ev.tool : 'unknown';
|
|
37
|
-
const ok = !(ev && ev.ok === false);
|
|
38
|
-
state.calls += 1;
|
|
39
|
-
if (!ok) state.errors += 1;
|
|
40
|
-
let t = state.byTool[tool];
|
|
41
|
-
if (!t) {
|
|
42
|
-
t = { calls: 0, errors: 0 };
|
|
43
|
-
state.byTool[tool] = t;
|
|
44
|
-
}
|
|
45
|
-
t.calls += 1;
|
|
46
|
-
if (!ok) t.errors += 1;
|
|
47
|
-
} catch (_e) {
|
|
48
|
-
/* instrumentation must never break the caller */
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* snapshot - a fresh, mutation-safe copy of the counters for /health + the UI. NEVER throws.
|
|
54
|
-
* @returns {{ startedAt:string, calls:number, errors:number, byTool:Object<string,{calls:number,errors:number}> }}
|
|
55
|
-
*/
|
|
56
|
-
function snapshot() {
|
|
57
|
-
try {
|
|
58
|
-
const byTool = {};
|
|
59
|
-
for (const k of Object.keys(state.byTool)) {
|
|
60
|
-
byTool[k] = { calls: state.byTool[k].calls, errors: state.byTool[k].errors };
|
|
61
|
-
}
|
|
62
|
-
return { startedAt: state.startedAt, calls: state.calls, errors: state.errors, byTool };
|
|
63
|
-
} catch (_e) {
|
|
64
|
-
return { startedAt: state.startedAt, calls: 0, errors: 0, byTool: {} };
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** reset - zero the counters (tests). The startedAt stamp is left as the process start. */
|
|
69
|
-
function reset() {
|
|
70
|
-
state.calls = 0;
|
|
71
|
-
state.errors = 0;
|
|
72
|
-
state.byTool = Object.create(null);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
module.exports = { record, snapshot, reset };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* metrics.js - in-memory, per-process tool-call counters (lightweight observability).
|
|
5
|
+
*
|
|
6
|
+
* A privileged gateway should be able to answer "what has run through me, and how often did it
|
|
7
|
+
* fail?" without standing up a metrics stack. This is the zero-dependency MVP of that: process-
|
|
8
|
+
* lifetime counters incremented at the single tools/call chokepoint and surfaced via the HTTP
|
|
9
|
+
* `/health` endpoint and the admin UI. It is NOT persistent (counts reset when the host restarts)
|
|
10
|
+
* and NOT a substitute for the JSONL audit log (src/core/logger.js, which records individual events
|
|
11
|
+
* to disk when enabled) - the two are complementary: the log is the durable per-event record, this
|
|
12
|
+
* is the cheap live aggregate.
|
|
13
|
+
*
|
|
14
|
+
* Design rules (same discipline as logger.js):
|
|
15
|
+
* - Zero dependencies. Plain objects + integers.
|
|
16
|
+
* - record() / snapshot() NEVER throw - instrumentation must never break a tool call.
|
|
17
|
+
* - In-memory only; no files, no timers, no network.
|
|
18
|
+
*
|
|
19
|
+
* CommonJS only.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const state = {
|
|
23
|
+
startedAt: new Date().toISOString(),
|
|
24
|
+
calls: 0,
|
|
25
|
+
errors: 0,
|
|
26
|
+
byTool: Object.create(null), // name -> { calls, errors }
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* record - count one tools/call outcome. `ok:false` (an isError result - a tool failure OR a
|
|
31
|
+
* PreToolUse denial, which both surface as an error envelope) increments the error counters.
|
|
32
|
+
* @param {{ tool?:string, ok?:boolean }} ev
|
|
33
|
+
*/
|
|
34
|
+
function record(ev) {
|
|
35
|
+
try {
|
|
36
|
+
const tool = ev && typeof ev.tool === 'string' && ev.tool.length > 0 ? ev.tool : 'unknown';
|
|
37
|
+
const ok = !(ev && ev.ok === false);
|
|
38
|
+
state.calls += 1;
|
|
39
|
+
if (!ok) state.errors += 1;
|
|
40
|
+
let t = state.byTool[tool];
|
|
41
|
+
if (!t) {
|
|
42
|
+
t = { calls: 0, errors: 0 };
|
|
43
|
+
state.byTool[tool] = t;
|
|
44
|
+
}
|
|
45
|
+
t.calls += 1;
|
|
46
|
+
if (!ok) t.errors += 1;
|
|
47
|
+
} catch (_e) {
|
|
48
|
+
/* instrumentation must never break the caller */
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* snapshot - a fresh, mutation-safe copy of the counters for /health + the UI. NEVER throws.
|
|
54
|
+
* @returns {{ startedAt:string, calls:number, errors:number, byTool:Object<string,{calls:number,errors:number}> }}
|
|
55
|
+
*/
|
|
56
|
+
function snapshot() {
|
|
57
|
+
try {
|
|
58
|
+
const byTool = {};
|
|
59
|
+
for (const k of Object.keys(state.byTool)) {
|
|
60
|
+
byTool[k] = { calls: state.byTool[k].calls, errors: state.byTool[k].errors };
|
|
61
|
+
}
|
|
62
|
+
return { startedAt: state.startedAt, calls: state.calls, errors: state.errors, byTool };
|
|
63
|
+
} catch (_e) {
|
|
64
|
+
return { startedAt: state.startedAt, calls: 0, errors: 0, byTool: {} };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** reset - zero the counters (tests). The startedAt stamp is left as the process start. */
|
|
69
|
+
function reset() {
|
|
70
|
+
state.calls = 0;
|
|
71
|
+
state.errors = 0;
|
|
72
|
+
state.byTool = Object.create(null);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { record, snapshot, reset };
|