toolfunnel 0.6.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 -231
- package/SECURITY.md +84 -0
- package/bin/toolfunnel.js +451 -372
- package/docs/KNOWN_BUGS.md +80 -99
- package/docs/MANUAL.pdf +0 -0
- package/docs/design.md +84 -70
- 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 -31
- package/mcp/expose.example.json +30 -30
- package/mcp/servers/mock-modern/server.js +147 -147
- package/mcp/servers/mock-upstream/server.js +380 -350
- package/package.json +3 -1
- package/src/auth/config.js +174 -174
- package/src/auth/install.js +181 -181
- package/src/auth/resource-server.js +353 -344
- package/src/core/child-reaper.js +61 -0
- package/src/core/config-home.js +134 -134
- package/src/core/events.js +105 -105
- package/src/core/hook-engine.js +329 -275
- package/src/core/hook-loader.js +824 -791
- package/src/core/hook-runner.js +566 -346
- package/src/core/logger.js +297 -171
- package/src/core/matcher.js +100 -51
- package/src/core/metrics.js +75 -75
- package/src/core/rename-migration.js +163 -0
- 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 -122
- package/src/extend/configure.md +88 -87
- package/src/extend/create-tool.md +168 -168
- package/src/extend/howto.js +102 -102
- package/src/extend/package.md +190 -187
- package/src/extend/wrap.md +99 -99
- package/src/mcp/aggregator.js +1719 -1246
- package/src/mcp/expose-store.js +577 -460
- package/src/mcp/gated-run.js +210 -201
- package/src/mcp/http-transport.js +1563 -1311
- package/src/mcp/mcp-client.js +1070 -907
- package/src/mcp/modern.js +406 -372
- package/src/mcp/protocol.js +468 -468
- package/src/mcp/server.js +3112 -2688
- package/src/tools/registry.js +661 -513
- package/src/tools/tool-state.js +170 -170
- package/src/ui/public/app.js +1187 -1174
- package/src/ui/public/index.html +286 -286
- package/src/ui/public/styles.css +535 -535
- package/src/ui/server.js +1616 -1348
- 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 -112
- 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/tools/tools.register.json +227 -227
- package/src/core/tool-registry.js +0 -180
- package/src/packages/packager.js +0 -715
- package/src/tools/drift.js +0 -138
package/src/tools/tool-state.js
CHANGED
|
@@ -1,170 +1,170 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* tool-state.js - per-tool ACTIVE/DISABLED overlay for the register.
|
|
5
|
-
*
|
|
6
|
-
* Mirrors a sibling state overlay (see a config file): a small
|
|
7
|
-
* overlay keyed by tool id, written atomically, loaded on startup -> survives
|
|
8
|
-
* close/detach (persistent session-to-session). The register (tools.register.json)
|
|
9
|
-
* holds ALL tools; this overlay decides which are surfaced to toolfunnel_list_tools.
|
|
10
|
-
*
|
|
11
|
-
* DEFAULT ON: a tool absent from the overlay is ACTIVE. Only an explicit
|
|
12
|
-
* { enabled: false } disables it. So newly-added tools are active until you
|
|
13
|
-
* untick them - and the file only ever lists the tools you've toggled.
|
|
14
|
-
*
|
|
15
|
-
* THREE independent axes (the visibility MATRIX), all keyed by the surfaced name
|
|
16
|
-
* (a local tool id, OR an upstream's surfaced name `<upstream>_<tool>` / its `as`,
|
|
17
|
-
* OR a meta-tool name like `toolfunnel_list_tools`):
|
|
18
|
-
* - enabled : LEAN-VISIBLE - surfaced by toolfunnel_list_tools AND runnable. Default ON.
|
|
19
|
-
* - hidden : manager-list declutter only (UI/tf_list). Default OFF. INDEPENDENT of enabled.
|
|
20
|
-
* - hot : promoted to the TOP-LEVEL tools/list (injected EVERY turn). Default depends on
|
|
21
|
-
* the tool KIND, supplied by the caller via isToolHot(state,id,defaultHot):
|
|
22
|
-
* meta-tools default hot:true (the management surface), local/upstream default
|
|
23
|
-
* hot:false (opt-in promotion). A disabled tool is never hot.
|
|
24
|
-
*
|
|
25
|
-
* Shape of tools.state.json: { "<id>": { "enabled": false, "hidden": true, "hot": true }, ... }
|
|
26
|
-
*
|
|
27
|
-
* PASSTHROUGH (0.6.0): one RESERVED top-level key, `"passthrough": "<upstreamId>"` (a STRING,
|
|
28
|
-
* unambiguous against the per-tool object entries). When set, the gateway becomes a transparent
|
|
29
|
-
* wrapper for that one upstream: its tools ARE the advertised surface (real schemas, implicitly
|
|
30
|
-
* hot), the meta-tools and everything else are hidden and uncallable, and every call still fires
|
|
31
|
-
* the PreToolUse gate. An explicit `enabled:false` on an upstream tool is still honoured (the
|
|
32
|
-
* safety off-switch survives the wrap). Set/cleared by `toolfunnel wrap <id>` / `wrap --off`.
|
|
33
|
-
*
|
|
34
|
-
* CommonJS, Node built-ins only. Reuses registry.js's atomicWriteJson (temp+rename).
|
|
35
|
-
*/
|
|
36
|
-
|
|
37
|
-
const fs = require('node:fs');
|
|
38
|
-
const { atomicWriteJson } = require('./registry');
|
|
39
|
-
|
|
40
|
-
/** Load the overlay ({} if missing/malformed - never throws). */
|
|
41
|
-
function loadToolState(statePath) {
|
|
42
|
-
try {
|
|
43
|
-
const d = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
44
|
-
return (d && typeof d === 'object' && !Array.isArray(d)) ? d : {};
|
|
45
|
-
} catch (_e) {
|
|
46
|
-
return {};
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Like loadToolState, but distinguishes "file MISSING" (defaults are correct) from "file PRESENT
|
|
52
|
-
* but unparseable / not an object" (a corrupt overlay - the caller may want to fail closed or warn
|
|
53
|
-
* rather than silently serve defaults). Returns { state, parseError }. `parseError` is true ONLY
|
|
54
|
-
* when the file exists but could not be read as a JSON object. NEVER throws.
|
|
55
|
-
* @param {string} statePath
|
|
56
|
-
* @returns {{ state: object, parseError: boolean }}
|
|
57
|
-
*/
|
|
58
|
-
function loadToolStateResult(statePath) {
|
|
59
|
-
let raw;
|
|
60
|
-
try {
|
|
61
|
-
raw = fs.readFileSync(statePath, 'utf8');
|
|
62
|
-
} catch (_e) {
|
|
63
|
-
return { state: {}, parseError: false }; // missing (or unreadable) -> defaults, not a corruption
|
|
64
|
-
}
|
|
65
|
-
try {
|
|
66
|
-
const d = JSON.parse(raw);
|
|
67
|
-
if (d && typeof d === 'object' && !Array.isArray(d)) return { state: d, parseError: false };
|
|
68
|
-
return { state: {}, parseError: true }; // present but not a JSON object
|
|
69
|
-
} catch (_e) {
|
|
70
|
-
return { state: {}, parseError: true }; // present but not valid JSON
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Default ON: enabled unless the overlay explicitly says enabled:false. */
|
|
75
|
-
function isToolEnabled(state, id) {
|
|
76
|
-
const e = state && state[id];
|
|
77
|
-
return !(e && e.enabled === false);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Default NOT hidden: hidden only when the overlay explicitly says hidden:true.
|
|
81
|
-
* `hidden` is a manager-list-only flag (declutter); it is INDEPENDENT of `enabled`
|
|
82
|
-
* (MCP visibility). A tool can be active-but-hidden, or disabled-but-visible. */
|
|
83
|
-
function isToolHidden(state, id) {
|
|
84
|
-
const e = state && state[id];
|
|
85
|
-
return !!(e && e.hidden === true);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Default OFF for local/upstream, ON for meta - the caller supplies the kind-appropriate
|
|
89
|
-
* `defaultHot`. An explicit boolean in the overlay always wins; absence falls back to the
|
|
90
|
-
* default. `hot` is the TOP-LEVEL (every-turn) promotion axis - INDEPENDENT of enabled/hidden,
|
|
91
|
-
* though a disabled tool is filtered out of the top-level surface by the assembler, not here. */
|
|
92
|
-
function isToolHot(state, id, defaultHot) {
|
|
93
|
-
const e = state && state[id];
|
|
94
|
-
if (e && typeof e.hot === 'boolean') return e.hot;
|
|
95
|
-
return !!defaultHot;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** The one top-level key reserved for the passthrough wrap (a STRING value). A per-tool overlay
|
|
99
|
-
* entry may NEVER use this key, else a per-tool toggle would clobber (or be clobbered by) the wrap
|
|
100
|
-
* - they share the same flat object. The per-tool setters reject
|
|
101
|
-
* it; a tool whose surfaced name is literally "passthrough" simply cannot carry an overlay entry
|
|
102
|
-
* (it stays at defaults - enabled, not hot, not hidden), which is a fine, safe degradation. */
|
|
103
|
-
const RESERVED_PASSTHROUGH_KEY = 'passthrough';
|
|
104
|
-
function assertNotReserved(id, fn) {
|
|
105
|
-
if (id === RESERVED_PASSTHROUGH_KEY) {
|
|
106
|
-
throw new Error(`${fn}: "${RESERVED_PASSTHROUGH_KEY}" is a reserved state key (the passthrough wrap) and cannot be a per-tool id`);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** Set a tool's enabled flag, MERGING (so `hidden`/`hot` are preserved). Persists atomically. */
|
|
111
|
-
function setToolEnabled(statePath, id, enabled) {
|
|
112
|
-
if (typeof id !== 'string' || !id) throw new Error('setToolEnabled: id required');
|
|
113
|
-
assertNotReserved(id, 'setToolEnabled');
|
|
114
|
-
const s = loadToolState(statePath);
|
|
115
|
-
s[id] = { ...(s[id] || {}), enabled: !!enabled };
|
|
116
|
-
atomicWriteJson(statePath, s);
|
|
117
|
-
return s;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** Set a tool's hot flag, MERGING (so `enabled`/`hidden` are preserved). Persists atomically. */
|
|
121
|
-
function setToolHot(statePath, id, hot) {
|
|
122
|
-
if (typeof id !== 'string' || !id) throw new Error('setToolHot: id required');
|
|
123
|
-
assertNotReserved(id, 'setToolHot');
|
|
124
|
-
const s = loadToolState(statePath);
|
|
125
|
-
s[id] = { ...(s[id] || {}), hot: !!hot };
|
|
126
|
-
atomicWriteJson(statePath, s);
|
|
127
|
-
return s;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** Set a tool's hidden flag, MERGING (so `enabled` is preserved). Persists atomically. */
|
|
131
|
-
function setToolHidden(statePath, id, hidden) {
|
|
132
|
-
if (typeof id !== 'string' || !id) throw new Error('setToolHidden: id required');
|
|
133
|
-
assertNotReserved(id, 'setToolHidden');
|
|
134
|
-
const s = loadToolState(statePath);
|
|
135
|
-
s[id] = { ...(s[id] || {}), hidden: !!hidden };
|
|
136
|
-
atomicWriteJson(statePath, s);
|
|
137
|
-
return s;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** Drop a tool's overlay entry entirely (revert to defaults: active + not hidden).
|
|
141
|
-
* No-op if the id was never toggled. Persists atomically. */
|
|
142
|
-
function clearToolState(statePath, id) {
|
|
143
|
-
if (typeof id !== 'string' || !id) throw new Error('clearToolState: id required');
|
|
144
|
-
assertNotReserved(id, 'clearToolState'); // a register tool id'd "passthrough" must not clear a live wrap
|
|
145
|
-
const s = loadToolState(statePath);
|
|
146
|
-
delete s[id];
|
|
147
|
-
atomicWriteJson(statePath, s);
|
|
148
|
-
return s;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/** The active passthrough upstream id, or null. Only a non-empty STRING value counts - a
|
|
152
|
-
* per-tool object entry that happens to be keyed "passthrough" is not a wrap. */
|
|
153
|
-
function getPassthrough(state) {
|
|
154
|
-
const v = state && state.passthrough;
|
|
155
|
-
return typeof v === 'string' && v.length > 0 ? v : null;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/** Set (a non-empty string) or clear (null/'') the passthrough wrap. Persists atomically. */
|
|
159
|
-
function setPassthrough(statePath, upstreamId) {
|
|
160
|
-
const s = loadToolState(statePath);
|
|
161
|
-
if (typeof upstreamId === 'string' && upstreamId.length > 0) {
|
|
162
|
-
s.passthrough = upstreamId;
|
|
163
|
-
} else {
|
|
164
|
-
delete s.passthrough;
|
|
165
|
-
}
|
|
166
|
-
atomicWriteJson(statePath, s);
|
|
167
|
-
return s;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
module.exports = { loadToolState, loadToolStateResult, isToolEnabled, isToolHidden, isToolHot, setToolEnabled, setToolHidden, setToolHot, clearToolState, getPassthrough, setPassthrough };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* tool-state.js - per-tool ACTIVE/DISABLED overlay for the register.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors a sibling state overlay (see a config file): a small
|
|
7
|
+
* overlay keyed by tool id, written atomically, loaded on startup -> survives
|
|
8
|
+
* close/detach (persistent session-to-session). The register (tools.register.json)
|
|
9
|
+
* holds ALL tools; this overlay decides which are surfaced to toolfunnel_list_tools.
|
|
10
|
+
*
|
|
11
|
+
* DEFAULT ON: a tool absent from the overlay is ACTIVE. Only an explicit
|
|
12
|
+
* { enabled: false } disables it. So newly-added tools are active until you
|
|
13
|
+
* untick them - and the file only ever lists the tools you've toggled.
|
|
14
|
+
*
|
|
15
|
+
* THREE independent axes (the visibility MATRIX), all keyed by the surfaced name
|
|
16
|
+
* (a local tool id, OR an upstream's surfaced name `<upstream>_<tool>` / its `as`,
|
|
17
|
+
* OR a meta-tool name like `toolfunnel_list_tools`):
|
|
18
|
+
* - enabled : LEAN-VISIBLE - surfaced by toolfunnel_list_tools AND runnable. Default ON.
|
|
19
|
+
* - hidden : manager-list declutter only (UI/tf_list). Default OFF. INDEPENDENT of enabled.
|
|
20
|
+
* - hot : promoted to the TOP-LEVEL tools/list (injected EVERY turn). Default depends on
|
|
21
|
+
* the tool KIND, supplied by the caller via isToolHot(state,id,defaultHot):
|
|
22
|
+
* meta-tools default hot:true (the management surface), local/upstream default
|
|
23
|
+
* hot:false (opt-in promotion). A disabled tool is never hot.
|
|
24
|
+
*
|
|
25
|
+
* Shape of tools.state.json: { "<id>": { "enabled": false, "hidden": true, "hot": true }, ... }
|
|
26
|
+
*
|
|
27
|
+
* PASSTHROUGH (0.6.0): one RESERVED top-level key, `"passthrough": "<upstreamId>"` (a STRING,
|
|
28
|
+
* unambiguous against the per-tool object entries). When set, the gateway becomes a transparent
|
|
29
|
+
* wrapper for that one upstream: its tools ARE the advertised surface (real schemas, implicitly
|
|
30
|
+
* hot), the meta-tools and everything else are hidden and uncallable, and every call still fires
|
|
31
|
+
* the PreToolUse gate. An explicit `enabled:false` on an upstream tool is still honoured (the
|
|
32
|
+
* safety off-switch survives the wrap). Set/cleared by `toolfunnel wrap <id>` / `wrap --off`.
|
|
33
|
+
*
|
|
34
|
+
* CommonJS, Node built-ins only. Reuses registry.js's atomicWriteJson (temp+rename).
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const fs = require('node:fs');
|
|
38
|
+
const { atomicWriteJson } = require('./registry');
|
|
39
|
+
|
|
40
|
+
/** Load the overlay ({} if missing/malformed - never throws). */
|
|
41
|
+
function loadToolState(statePath) {
|
|
42
|
+
try {
|
|
43
|
+
const d = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
44
|
+
return (d && typeof d === 'object' && !Array.isArray(d)) ? d : {};
|
|
45
|
+
} catch (_e) {
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Like loadToolState, but distinguishes "file MISSING" (defaults are correct) from "file PRESENT
|
|
52
|
+
* but unparseable / not an object" (a corrupt overlay - the caller may want to fail closed or warn
|
|
53
|
+
* rather than silently serve defaults). Returns { state, parseError }. `parseError` is true ONLY
|
|
54
|
+
* when the file exists but could not be read as a JSON object. NEVER throws.
|
|
55
|
+
* @param {string} statePath
|
|
56
|
+
* @returns {{ state: object, parseError: boolean }}
|
|
57
|
+
*/
|
|
58
|
+
function loadToolStateResult(statePath) {
|
|
59
|
+
let raw;
|
|
60
|
+
try {
|
|
61
|
+
raw = fs.readFileSync(statePath, 'utf8');
|
|
62
|
+
} catch (_e) {
|
|
63
|
+
return { state: {}, parseError: false }; // missing (or unreadable) -> defaults, not a corruption
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const d = JSON.parse(raw);
|
|
67
|
+
if (d && typeof d === 'object' && !Array.isArray(d)) return { state: d, parseError: false };
|
|
68
|
+
return { state: {}, parseError: true }; // present but not a JSON object
|
|
69
|
+
} catch (_e) {
|
|
70
|
+
return { state: {}, parseError: true }; // present but not valid JSON
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Default ON: enabled unless the overlay explicitly says enabled:false. */
|
|
75
|
+
function isToolEnabled(state, id) {
|
|
76
|
+
const e = state && state[id];
|
|
77
|
+
return !(e && e.enabled === false);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Default NOT hidden: hidden only when the overlay explicitly says hidden:true.
|
|
81
|
+
* `hidden` is a manager-list-only flag (declutter); it is INDEPENDENT of `enabled`
|
|
82
|
+
* (MCP visibility). A tool can be active-but-hidden, or disabled-but-visible. */
|
|
83
|
+
function isToolHidden(state, id) {
|
|
84
|
+
const e = state && state[id];
|
|
85
|
+
return !!(e && e.hidden === true);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Default OFF for local/upstream, ON for meta - the caller supplies the kind-appropriate
|
|
89
|
+
* `defaultHot`. An explicit boolean in the overlay always wins; absence falls back to the
|
|
90
|
+
* default. `hot` is the TOP-LEVEL (every-turn) promotion axis - INDEPENDENT of enabled/hidden,
|
|
91
|
+
* though a disabled tool is filtered out of the top-level surface by the assembler, not here. */
|
|
92
|
+
function isToolHot(state, id, defaultHot) {
|
|
93
|
+
const e = state && state[id];
|
|
94
|
+
if (e && typeof e.hot === 'boolean') return e.hot;
|
|
95
|
+
return !!defaultHot;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The one top-level key reserved for the passthrough wrap (a STRING value). A per-tool overlay
|
|
99
|
+
* entry may NEVER use this key, else a per-tool toggle would clobber (or be clobbered by) the wrap
|
|
100
|
+
* - they share the same flat object. The per-tool setters reject
|
|
101
|
+
* it; a tool whose surfaced name is literally "passthrough" simply cannot carry an overlay entry
|
|
102
|
+
* (it stays at defaults - enabled, not hot, not hidden), which is a fine, safe degradation. */
|
|
103
|
+
const RESERVED_PASSTHROUGH_KEY = 'passthrough';
|
|
104
|
+
function assertNotReserved(id, fn) {
|
|
105
|
+
if (id === RESERVED_PASSTHROUGH_KEY) {
|
|
106
|
+
throw new Error(`${fn}: "${RESERVED_PASSTHROUGH_KEY}" is a reserved state key (the passthrough wrap) and cannot be a per-tool id`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Set a tool's enabled flag, MERGING (so `hidden`/`hot` are preserved). Persists atomically. */
|
|
111
|
+
function setToolEnabled(statePath, id, enabled) {
|
|
112
|
+
if (typeof id !== 'string' || !id) throw new Error('setToolEnabled: id required');
|
|
113
|
+
assertNotReserved(id, 'setToolEnabled');
|
|
114
|
+
const s = loadToolState(statePath);
|
|
115
|
+
s[id] = { ...(s[id] || {}), enabled: !!enabled };
|
|
116
|
+
atomicWriteJson(statePath, s);
|
|
117
|
+
return s;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Set a tool's hot flag, MERGING (so `enabled`/`hidden` are preserved). Persists atomically. */
|
|
121
|
+
function setToolHot(statePath, id, hot) {
|
|
122
|
+
if (typeof id !== 'string' || !id) throw new Error('setToolHot: id required');
|
|
123
|
+
assertNotReserved(id, 'setToolHot');
|
|
124
|
+
const s = loadToolState(statePath);
|
|
125
|
+
s[id] = { ...(s[id] || {}), hot: !!hot };
|
|
126
|
+
atomicWriteJson(statePath, s);
|
|
127
|
+
return s;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Set a tool's hidden flag, MERGING (so `enabled` is preserved). Persists atomically. */
|
|
131
|
+
function setToolHidden(statePath, id, hidden) {
|
|
132
|
+
if (typeof id !== 'string' || !id) throw new Error('setToolHidden: id required');
|
|
133
|
+
assertNotReserved(id, 'setToolHidden');
|
|
134
|
+
const s = loadToolState(statePath);
|
|
135
|
+
s[id] = { ...(s[id] || {}), hidden: !!hidden };
|
|
136
|
+
atomicWriteJson(statePath, s);
|
|
137
|
+
return s;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Drop a tool's overlay entry entirely (revert to defaults: active + not hidden).
|
|
141
|
+
* No-op if the id was never toggled. Persists atomically. */
|
|
142
|
+
function clearToolState(statePath, id) {
|
|
143
|
+
if (typeof id !== 'string' || !id) throw new Error('clearToolState: id required');
|
|
144
|
+
assertNotReserved(id, 'clearToolState'); // a register tool id'd "passthrough" must not clear a live wrap
|
|
145
|
+
const s = loadToolState(statePath);
|
|
146
|
+
delete s[id];
|
|
147
|
+
atomicWriteJson(statePath, s);
|
|
148
|
+
return s;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The active passthrough upstream id, or null. Only a non-empty STRING value counts - a
|
|
152
|
+
* per-tool object entry that happens to be keyed "passthrough" is not a wrap. */
|
|
153
|
+
function getPassthrough(state) {
|
|
154
|
+
const v = state && state.passthrough;
|
|
155
|
+
return typeof v === 'string' && v.length > 0 ? v : null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Set (a non-empty string) or clear (null/'') the passthrough wrap. Persists atomically. */
|
|
159
|
+
function setPassthrough(statePath, upstreamId) {
|
|
160
|
+
const s = loadToolState(statePath);
|
|
161
|
+
if (typeof upstreamId === 'string' && upstreamId.length > 0) {
|
|
162
|
+
s.passthrough = upstreamId;
|
|
163
|
+
} else {
|
|
164
|
+
delete s.passthrough;
|
|
165
|
+
}
|
|
166
|
+
atomicWriteJson(statePath, s);
|
|
167
|
+
return s;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = { loadToolState, loadToolStateResult, isToolEnabled, isToolHidden, isToolHot, setToolEnabled, setToolHidden, setToolHot, clearToolState, getPassthrough, setPassthrough };
|