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
|
@@ -1,134 +1,134 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* tf-tool-add.js - a first-party MANAGEMENT tool that ADDS a register entry.
|
|
6
|
-
*
|
|
7
|
-
* Purpose
|
|
8
|
-
* -------
|
|
9
|
-
* The write-side counterpart to the read-only status / inventory tools: it lets
|
|
10
|
-
* the manager surface a NEW tool in the register at runtime (no reconnect - the
|
|
11
|
-
* running gateway watches tools.register.json and hot-reloads its register on
|
|
12
|
-
* change). Optionally it also writes the host-local script the new entry invokes,
|
|
13
|
-
* so a single call can create both the register entry AND its backing script
|
|
14
|
-
* atomically.
|
|
15
|
-
*
|
|
16
|
-
* This is a "gateway-run" tool: it mutates ToolFunnel's OWN config (the register
|
|
17
|
-
* and, optionally, a script under the scripts root). It runs through the same
|
|
18
|
-
* register -> resolveExecution -> gated run path as any other script tool, so
|
|
19
|
-
* the PreToolUse gate still applies to invoking it.
|
|
20
|
-
*
|
|
21
|
-
* Contract with the register (registry.js `defaultRunScript`)
|
|
22
|
-
* ----------------------------------------------------------
|
|
23
|
-
* - Structured args arrive as JSON in env TOOLFUNNEL_TOOL_ARGS ({} / absent => null).
|
|
24
|
-
* - Args: { id, name, summary?, category?, instructions?, inputSchema?,
|
|
25
|
-
* invoke: { type:'script'|'shell', path?|command? },
|
|
26
|
-
* scriptText? }
|
|
27
|
-
* inputSchema (a JSON Schema object) is what a HOT-promoted tool advertises verbatim in
|
|
28
|
-
* the top-level tools/list - dropping it here was the gap between "your own tools" and
|
|
29
|
-
* "your own tools AND schemas".
|
|
30
|
-
* When scriptText is a string AND invoke.type === 'script', the script is
|
|
31
|
-
* written first (registry.writeScript), THEN the entry is added.
|
|
32
|
-
* - Prints a SINGLE JSON object to stdout and exits 0 - ALWAYS exit 0, even on
|
|
33
|
-
* a logical error (reported as { ok:false, error }), never a thrown exception.
|
|
34
|
-
*
|
|
35
|
-
* Output (stdout), exactly one JSON object:
|
|
36
|
-
* success: { ok:true, ...<the added register entry>, scriptPath?: "<abs path>" }
|
|
37
|
-
* failure: { ok:false, error }
|
|
38
|
-
*
|
|
39
|
-
* Safety invariants:
|
|
40
|
-
* - Touches ONLY files inside the toolfunnel root, resolved from THIS script's
|
|
41
|
-
* own location (__dirname) - never a caller-supplied absolute path. The
|
|
42
|
-
* register's writeScript guards path-escape (basename + inside-root check).
|
|
43
|
-
* - NO network. Writes are atomic (temp + fsync + rename) via the register module.
|
|
44
|
-
* - NEVER throws for ordinary bad input - only well-shaped JSON on stdout.
|
|
45
|
-
*/
|
|
46
|
-
|
|
47
|
-
const path = require('node:path');
|
|
48
|
-
// Shared HOME/engine resolution (see tf-env.js): config beside us, engine from the package.
|
|
49
|
-
const { HOME, srcRequire } = require('./tf-env');
|
|
50
|
-
const { loadRegistry } = srcRequire('tools/registry');
|
|
51
|
-
|
|
52
|
-
const ROOT = HOME;
|
|
53
|
-
const REGISTER_PATH = path.join(ROOT, 'tools', 'tools.register.json');
|
|
54
|
-
const SCRIPTS_ROOT = path.join(ROOT, 'tools', 'scripts');
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Parse the structured args handed in via env TOOLFUNNEL_TOOL_ARGS.
|
|
58
|
-
* @returns {{ value: any } | { parseError: string }}
|
|
59
|
-
*/
|
|
60
|
-
function parseStructuredArgs() {
|
|
61
|
-
const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
|
|
62
|
-
if (raw === undefined || raw === null || raw === '') {
|
|
63
|
-
return { value: null };
|
|
64
|
-
}
|
|
65
|
-
try {
|
|
66
|
-
return { value: JSON.parse(raw) };
|
|
67
|
-
} catch (_err) {
|
|
68
|
-
return { parseError: `TOOLFUNNEL_TOOL_ARGS was not valid JSON: ${String(raw)}` };
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Pure core: given the parsed args, add the register entry (and optionally write
|
|
74
|
-
* its backing script first). Throws are caught by main() and surfaced as
|
|
75
|
-
* { ok:false, error }; loadRegistry / writeScript / add all throw on bad shape.
|
|
76
|
-
*
|
|
77
|
-
* @param {any} args
|
|
78
|
-
* @returns {object} the JSON-serialisable result
|
|
79
|
-
*/
|
|
80
|
-
function run(args) {
|
|
81
|
-
if (!args || typeof args !== 'object' || Array.isArray(args)) {
|
|
82
|
-
return {
|
|
83
|
-
ok: false,
|
|
84
|
-
error: 'args must be an object { id, name, summary?, category?, instructions?, inputSchema?, mode?, invoke?, scriptText? }',
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
const { id, name, summary, category, instructions, inputSchema, mode, invoke, scriptText } = args;
|
|
88
|
-
|
|
89
|
-
// Build a clean register entry (only carry through the optional fields when
|
|
90
|
-
// present so the persisted entry stays tidy). registry.add validates shape.
|
|
91
|
-
// `mode` MUST ride along: a reference-mode tool has NO invoke at all, and dropping the field
|
|
92
|
-
// here made the agent path reject every reference tool with "entry.invoke is required" while
|
|
93
|
-
// the UI path accepted them.
|
|
94
|
-
const entry = { id, name };
|
|
95
|
-
if (invoke !== undefined) entry.invoke = invoke;
|
|
96
|
-
if (mode !== undefined) entry.mode = mode;
|
|
97
|
-
if (summary !== undefined) entry.summary = summary;
|
|
98
|
-
if (category !== undefined) entry.category = category;
|
|
99
|
-
if (instructions !== undefined) entry.instructions = instructions;
|
|
100
|
-
if (inputSchema !== undefined) entry.inputSchema = inputSchema; // validated by registry.add
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const registry = loadRegistry(REGISTER_PATH, { scriptsRoot: SCRIPTS_ROOT });
|
|
104
|
-
|
|
105
|
-
// When a script body is supplied for a script invoke, write it FIRST so the
|
|
106
|
-
// entry never references a missing file once add() succeeds.
|
|
107
|
-
let scriptPath;
|
|
108
|
-
if (typeof scriptText === 'string' && invoke && invoke.type === 'script') {
|
|
109
|
-
scriptPath = registry.writeScript(invoke.path, scriptText);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const added = registry.add(entry);
|
|
113
|
-
const result = { ok: true, ...added };
|
|
114
|
-
if (scriptPath) result.scriptPath = scriptPath;
|
|
115
|
-
return result;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function main() {
|
|
119
|
-
const parsed = parseStructuredArgs();
|
|
120
|
-
let payload;
|
|
121
|
-
if (parsed.parseError) {
|
|
122
|
-
payload = { ok: false, error: parsed.parseError };
|
|
123
|
-
} else {
|
|
124
|
-
try {
|
|
125
|
-
payload = run(parsed.value);
|
|
126
|
-
} catch (err) {
|
|
127
|
-
payload = { ok: false, error: (err && err.message) || String(err) };
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
131
|
-
process.exit(0);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
main();
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* tf-tool-add.js - a first-party MANAGEMENT tool that ADDS a register entry.
|
|
6
|
+
*
|
|
7
|
+
* Purpose
|
|
8
|
+
* -------
|
|
9
|
+
* The write-side counterpart to the read-only status / inventory tools: it lets
|
|
10
|
+
* the manager surface a NEW tool in the register at runtime (no reconnect - the
|
|
11
|
+
* running gateway watches tools.register.json and hot-reloads its register on
|
|
12
|
+
* change). Optionally it also writes the host-local script the new entry invokes,
|
|
13
|
+
* so a single call can create both the register entry AND its backing script
|
|
14
|
+
* atomically.
|
|
15
|
+
*
|
|
16
|
+
* This is a "gateway-run" tool: it mutates ToolFunnel's OWN config (the register
|
|
17
|
+
* and, optionally, a script under the scripts root). It runs through the same
|
|
18
|
+
* register -> resolveExecution -> gated run path as any other script tool, so
|
|
19
|
+
* the PreToolUse gate still applies to invoking it.
|
|
20
|
+
*
|
|
21
|
+
* Contract with the register (registry.js `defaultRunScript`)
|
|
22
|
+
* ----------------------------------------------------------
|
|
23
|
+
* - Structured args arrive as JSON in env TOOLFUNNEL_TOOL_ARGS ({} / absent => null).
|
|
24
|
+
* - Args: { id, name, summary?, category?, instructions?, inputSchema?,
|
|
25
|
+
* invoke: { type:'script'|'shell', path?|command? },
|
|
26
|
+
* scriptText? }
|
|
27
|
+
* inputSchema (a JSON Schema object) is what a HOT-promoted tool advertises verbatim in
|
|
28
|
+
* the top-level tools/list - dropping it here was the gap between "your own tools" and
|
|
29
|
+
* "your own tools AND schemas".
|
|
30
|
+
* When scriptText is a string AND invoke.type === 'script', the script is
|
|
31
|
+
* written first (registry.writeScript), THEN the entry is added.
|
|
32
|
+
* - Prints a SINGLE JSON object to stdout and exits 0 - ALWAYS exit 0, even on
|
|
33
|
+
* a logical error (reported as { ok:false, error }), never a thrown exception.
|
|
34
|
+
*
|
|
35
|
+
* Output (stdout), exactly one JSON object:
|
|
36
|
+
* success: { ok:true, ...<the added register entry>, scriptPath?: "<abs path>" }
|
|
37
|
+
* failure: { ok:false, error }
|
|
38
|
+
*
|
|
39
|
+
* Safety invariants:
|
|
40
|
+
* - Touches ONLY files inside the toolfunnel root, resolved from THIS script's
|
|
41
|
+
* own location (__dirname) - never a caller-supplied absolute path. The
|
|
42
|
+
* register's writeScript guards path-escape (basename + inside-root check).
|
|
43
|
+
* - NO network. Writes are atomic (temp + fsync + rename) via the register module.
|
|
44
|
+
* - NEVER throws for ordinary bad input - only well-shaped JSON on stdout.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
const path = require('node:path');
|
|
48
|
+
// Shared HOME/engine resolution (see tf-env.js): config beside us, engine from the package.
|
|
49
|
+
const { HOME, srcRequire } = require('./tf-env');
|
|
50
|
+
const { loadRegistry } = srcRequire('tools/registry');
|
|
51
|
+
|
|
52
|
+
const ROOT = HOME;
|
|
53
|
+
const REGISTER_PATH = path.join(ROOT, 'tools', 'tools.register.json');
|
|
54
|
+
const SCRIPTS_ROOT = path.join(ROOT, 'tools', 'scripts');
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Parse the structured args handed in via env TOOLFUNNEL_TOOL_ARGS.
|
|
58
|
+
* @returns {{ value: any } | { parseError: string }}
|
|
59
|
+
*/
|
|
60
|
+
function parseStructuredArgs() {
|
|
61
|
+
const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
|
|
62
|
+
if (raw === undefined || raw === null || raw === '') {
|
|
63
|
+
return { value: null };
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
return { value: JSON.parse(raw) };
|
|
67
|
+
} catch (_err) {
|
|
68
|
+
return { parseError: `TOOLFUNNEL_TOOL_ARGS was not valid JSON: ${String(raw)}` };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Pure core: given the parsed args, add the register entry (and optionally write
|
|
74
|
+
* its backing script first). Throws are caught by main() and surfaced as
|
|
75
|
+
* { ok:false, error }; loadRegistry / writeScript / add all throw on bad shape.
|
|
76
|
+
*
|
|
77
|
+
* @param {any} args
|
|
78
|
+
* @returns {object} the JSON-serialisable result
|
|
79
|
+
*/
|
|
80
|
+
function run(args) {
|
|
81
|
+
if (!args || typeof args !== 'object' || Array.isArray(args)) {
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
error: 'args must be an object { id, name, summary?, category?, instructions?, inputSchema?, mode?, invoke?, scriptText? }',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const { id, name, summary, category, instructions, inputSchema, mode, invoke, scriptText } = args;
|
|
88
|
+
|
|
89
|
+
// Build a clean register entry (only carry through the optional fields when
|
|
90
|
+
// present so the persisted entry stays tidy). registry.add validates shape.
|
|
91
|
+
// `mode` MUST ride along: a reference-mode tool has NO invoke at all, and dropping the field
|
|
92
|
+
// here made the agent path reject every reference tool with "entry.invoke is required" while
|
|
93
|
+
// the UI path accepted them.
|
|
94
|
+
const entry = { id, name };
|
|
95
|
+
if (invoke !== undefined) entry.invoke = invoke;
|
|
96
|
+
if (mode !== undefined) entry.mode = mode;
|
|
97
|
+
if (summary !== undefined) entry.summary = summary;
|
|
98
|
+
if (category !== undefined) entry.category = category;
|
|
99
|
+
if (instructions !== undefined) entry.instructions = instructions;
|
|
100
|
+
if (inputSchema !== undefined) entry.inputSchema = inputSchema; // validated by registry.add
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
const registry = loadRegistry(REGISTER_PATH, { scriptsRoot: SCRIPTS_ROOT });
|
|
104
|
+
|
|
105
|
+
// When a script body is supplied for a script invoke, write it FIRST so the
|
|
106
|
+
// entry never references a missing file once add() succeeds.
|
|
107
|
+
let scriptPath;
|
|
108
|
+
if (typeof scriptText === 'string' && invoke && invoke.type === 'script') {
|
|
109
|
+
scriptPath = registry.writeScript(invoke.path, scriptText);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const added = registry.add(entry);
|
|
113
|
+
const result = { ok: true, ...added };
|
|
114
|
+
if (scriptPath) result.scriptPath = scriptPath;
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function main() {
|
|
119
|
+
const parsed = parseStructuredArgs();
|
|
120
|
+
let payload;
|
|
121
|
+
if (parsed.parseError) {
|
|
122
|
+
payload = { ok: false, error: parsed.parseError };
|
|
123
|
+
} else {
|
|
124
|
+
try {
|
|
125
|
+
payload = run(parsed.value);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
payload = { ok: false, error: (err && err.message) || String(err) };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
131
|
+
process.exit(0);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
main();
|
|
@@ -1,142 +1,142 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* tf-tool-set.js - a first-party MANAGEMENT tool that toggles or removes a tool.
|
|
6
|
-
*
|
|
7
|
-
* Purpose
|
|
8
|
-
* -------
|
|
9
|
-
* The state-side counterpart to tf-tool-add: it flips a tool's ACTIVE/DISABLED
|
|
10
|
-
* overlay flag (enable / disable) or fully removes a tool (drops the register
|
|
11
|
-
* entry AND clears its state overlay). Register/state edits are visible to the
|
|
12
|
-
* meta-tools on the next call - no reconnect.
|
|
13
|
-
*
|
|
14
|
-
* This is a "gateway-run" tool: it mutates ToolFunnel's OWN config (the per-tool
|
|
15
|
-
* state overlay, and for `remove`, the register too). It runs through the same
|
|
16
|
-
* gated run path as any other script tool.
|
|
17
|
-
*
|
|
18
|
-
* Contract with the register (registry.js `defaultRunScript`)
|
|
19
|
-
* ----------------------------------------------------------
|
|
20
|
-
* - Structured args arrive as JSON in env TOOLFUNNEL_TOOL_ARGS ({} / absent => null).
|
|
21
|
-
* - Args: { id, action: 'enable' | 'disable' | 'hot' | 'unhot' | 'hide' | 'unhide' | 'remove' }.
|
|
22
|
-
* enable/disable -> setToolEnabled(state, id, bool) (overlay merge; preserves hidden/hot)
|
|
23
|
-
* hot/unhot -> setToolHot(state, id, bool) (overlay merge; preserves enabled/hidden)
|
|
24
|
-
* - promote/demote the tool on the TOP-LEVEL every-turn surface (the matrix).
|
|
25
|
-
* hide/unhide -> setToolHidden(state, id, bool) (overlay merge; preserves enabled/hot)
|
|
26
|
-
* - declutter the MANAGER views only (tf_list + the UI); does NOT change
|
|
27
|
-
* what the connected AI sees (the lean list / top-level surface).
|
|
28
|
-
* remove -> registry.remove(id) AND clearToolState(state, id)
|
|
29
|
-
* - Prints a SINGLE JSON object to stdout and exits 0 - ALWAYS exit 0, even on
|
|
30
|
-
* a logical error (reported as { ok:false, error }), never a thrown exception.
|
|
31
|
-
*
|
|
32
|
-
* Output (stdout), exactly one JSON object:
|
|
33
|
-
* enable/disable: { ok:true, id, action, enabled, state }
|
|
34
|
-
* hot/unhot: { ok:true, id, action, hot, state }
|
|
35
|
-
* hide/unhide: { ok:true, id, action, hidden, state }
|
|
36
|
-
* remove: { ok:true, id, action, removed:true, state }
|
|
37
|
-
* failure: { ok:false, error }
|
|
38
|
-
* (`state` is the resulting overlay map after the operation.)
|
|
39
|
-
*
|
|
40
|
-
* Safety invariants:
|
|
41
|
-
* - Touches ONLY config files inside the toolfunnel root, resolved from THIS
|
|
42
|
-
* script's own location (__dirname) - never a caller-supplied path.
|
|
43
|
-
* - NO network. Writes are atomic (temp + fsync + rename) via the store modules.
|
|
44
|
-
* - NEVER throws for ordinary bad input - only well-shaped JSON on stdout.
|
|
45
|
-
*/
|
|
46
|
-
|
|
47
|
-
const path = require('node:path');
|
|
48
|
-
// Shared HOME/engine resolution (see tf-env.js): config beside us, engine from the package.
|
|
49
|
-
const { HOME, srcRequire } = require('./tf-env');
|
|
50
|
-
const { loadRegistry } = srcRequire('tools/registry');
|
|
51
|
-
const { setToolEnabled, setToolHot, setToolHidden, clearToolState } = srcRequire('tools/tool-state');
|
|
52
|
-
|
|
53
|
-
const ROOT = HOME;
|
|
54
|
-
const REGISTER_PATH = path.join(ROOT, 'tools', 'tools.register.json');
|
|
55
|
-
const STATE_PATH = path.join(ROOT, 'tools', 'tools.state.json');
|
|
56
|
-
const SCRIPTS_ROOT = path.join(ROOT, 'tools', 'scripts');
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Parse the structured args handed in via env TOOLFUNNEL_TOOL_ARGS.
|
|
60
|
-
* @returns {{ value: any } | { parseError: string }}
|
|
61
|
-
*/
|
|
62
|
-
function parseStructuredArgs() {
|
|
63
|
-
const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
|
|
64
|
-
if (raw === undefined || raw === null || raw === '') {
|
|
65
|
-
return { value: null };
|
|
66
|
-
}
|
|
67
|
-
try {
|
|
68
|
-
return { value: JSON.parse(raw) };
|
|
69
|
-
} catch (_err) {
|
|
70
|
-
return { parseError: `TOOLFUNNEL_TOOL_ARGS was not valid JSON: ${String(raw)}` };
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Pure core: given the parsed args, perform the requested state/register change.
|
|
76
|
-
* Throws are caught by main() and surfaced as { ok:false, error }; registry.remove
|
|
77
|
-
* throws on an unknown id, setToolEnabled/clearToolState throw on a bad id.
|
|
78
|
-
*
|
|
79
|
-
* @param {any} args
|
|
80
|
-
* @returns {object} the JSON-serialisable result
|
|
81
|
-
*/
|
|
82
|
-
function run(args) {
|
|
83
|
-
if (!args || typeof args !== 'object' || Array.isArray(args)) {
|
|
84
|
-
return { ok: false, error: "args must be an object { id, action: 'enable'|'disable'|'hot'|'unhot'|'hide'|'unhide'|'remove' }" };
|
|
85
|
-
}
|
|
86
|
-
const { id, action } = args;
|
|
87
|
-
if (typeof id !== 'string' || id.length === 0) {
|
|
88
|
-
return { ok: false, error: 'id must be a non-empty string' };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (action === 'enable' || action === 'disable') {
|
|
92
|
-
const enabled = action === 'enable';
|
|
93
|
-
const state = setToolEnabled(STATE_PATH, id, enabled);
|
|
94
|
-
return { ok: true, id, action, enabled, state };
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (action === 'hot' || action === 'unhot') {
|
|
98
|
-
// Promote (hot) / demote (unhot) the tool on the TOP-LEVEL every-turn surface. Keyed by the
|
|
99
|
-
// surfaced name (a local id, an upstream surfaced name, or a meta-tool name) - the same key the
|
|
100
|
-
// server's matrix assembler reads. The id is NOT verified against the register here: an upstream
|
|
101
|
-
// surfaced name or a meta-tool name is a legitimate key with no register entry.
|
|
102
|
-
const hot = action === 'hot';
|
|
103
|
-
const state = setToolHot(STATE_PATH, id, hot);
|
|
104
|
-
return { ok: true, id, action, hot, state };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (action === 'hide' || action === 'unhide') {
|
|
108
|
-
// Declutter the MANAGER views (tf_list + the UI) only - hide/unhide does NOT change the lean list
|
|
109
|
-
// or the top-level surface the connected AI sees. Same id-not-verified policy as hot/unhot.
|
|
110
|
-
const hidden = action === 'hide';
|
|
111
|
-
const state = setToolHidden(STATE_PATH, id, hidden);
|
|
112
|
-
return { ok: true, id, action, hidden, state };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (action === 'remove') {
|
|
116
|
-
// Drop the register entry first (throws on unknown id), then clear any overlay.
|
|
117
|
-
const registry = loadRegistry(REGISTER_PATH, { scriptsRoot: SCRIPTS_ROOT });
|
|
118
|
-
registry.remove(id);
|
|
119
|
-
const state = clearToolState(STATE_PATH, id);
|
|
120
|
-
return { ok: true, id, action, removed: true, state };
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return { ok: false, error: "action must be one of 'enable'|'disable'|'hot'|'unhot'|'hide'|'unhide'|'remove'" };
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function main() {
|
|
127
|
-
const parsed = parseStructuredArgs();
|
|
128
|
-
let payload;
|
|
129
|
-
if (parsed.parseError) {
|
|
130
|
-
payload = { ok: false, error: parsed.parseError };
|
|
131
|
-
} else {
|
|
132
|
-
try {
|
|
133
|
-
payload = run(parsed.value);
|
|
134
|
-
} catch (err) {
|
|
135
|
-
payload = { ok: false, error: (err && err.message) || String(err) };
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
139
|
-
process.exit(0);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
main();
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* tf-tool-set.js - a first-party MANAGEMENT tool that toggles or removes a tool.
|
|
6
|
+
*
|
|
7
|
+
* Purpose
|
|
8
|
+
* -------
|
|
9
|
+
* The state-side counterpart to tf-tool-add: it flips a tool's ACTIVE/DISABLED
|
|
10
|
+
* overlay flag (enable / disable) or fully removes a tool (drops the register
|
|
11
|
+
* entry AND clears its state overlay). Register/state edits are visible to the
|
|
12
|
+
* meta-tools on the next call - no reconnect.
|
|
13
|
+
*
|
|
14
|
+
* This is a "gateway-run" tool: it mutates ToolFunnel's OWN config (the per-tool
|
|
15
|
+
* state overlay, and for `remove`, the register too). It runs through the same
|
|
16
|
+
* gated run path as any other script tool.
|
|
17
|
+
*
|
|
18
|
+
* Contract with the register (registry.js `defaultRunScript`)
|
|
19
|
+
* ----------------------------------------------------------
|
|
20
|
+
* - Structured args arrive as JSON in env TOOLFUNNEL_TOOL_ARGS ({} / absent => null).
|
|
21
|
+
* - Args: { id, action: 'enable' | 'disable' | 'hot' | 'unhot' | 'hide' | 'unhide' | 'remove' }.
|
|
22
|
+
* enable/disable -> setToolEnabled(state, id, bool) (overlay merge; preserves hidden/hot)
|
|
23
|
+
* hot/unhot -> setToolHot(state, id, bool) (overlay merge; preserves enabled/hidden)
|
|
24
|
+
* - promote/demote the tool on the TOP-LEVEL every-turn surface (the matrix).
|
|
25
|
+
* hide/unhide -> setToolHidden(state, id, bool) (overlay merge; preserves enabled/hot)
|
|
26
|
+
* - declutter the MANAGER views only (tf_list + the UI); does NOT change
|
|
27
|
+
* what the connected AI sees (the lean list / top-level surface).
|
|
28
|
+
* remove -> registry.remove(id) AND clearToolState(state, id)
|
|
29
|
+
* - Prints a SINGLE JSON object to stdout and exits 0 - ALWAYS exit 0, even on
|
|
30
|
+
* a logical error (reported as { ok:false, error }), never a thrown exception.
|
|
31
|
+
*
|
|
32
|
+
* Output (stdout), exactly one JSON object:
|
|
33
|
+
* enable/disable: { ok:true, id, action, enabled, state }
|
|
34
|
+
* hot/unhot: { ok:true, id, action, hot, state }
|
|
35
|
+
* hide/unhide: { ok:true, id, action, hidden, state }
|
|
36
|
+
* remove: { ok:true, id, action, removed:true, state }
|
|
37
|
+
* failure: { ok:false, error }
|
|
38
|
+
* (`state` is the resulting overlay map after the operation.)
|
|
39
|
+
*
|
|
40
|
+
* Safety invariants:
|
|
41
|
+
* - Touches ONLY config files inside the toolfunnel root, resolved from THIS
|
|
42
|
+
* script's own location (__dirname) - never a caller-supplied path.
|
|
43
|
+
* - NO network. Writes are atomic (temp + fsync + rename) via the store modules.
|
|
44
|
+
* - NEVER throws for ordinary bad input - only well-shaped JSON on stdout.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
const path = require('node:path');
|
|
48
|
+
// Shared HOME/engine resolution (see tf-env.js): config beside us, engine from the package.
|
|
49
|
+
const { HOME, srcRequire } = require('./tf-env');
|
|
50
|
+
const { loadRegistry } = srcRequire('tools/registry');
|
|
51
|
+
const { setToolEnabled, setToolHot, setToolHidden, clearToolState } = srcRequire('tools/tool-state');
|
|
52
|
+
|
|
53
|
+
const ROOT = HOME;
|
|
54
|
+
const REGISTER_PATH = path.join(ROOT, 'tools', 'tools.register.json');
|
|
55
|
+
const STATE_PATH = path.join(ROOT, 'tools', 'tools.state.json');
|
|
56
|
+
const SCRIPTS_ROOT = path.join(ROOT, 'tools', 'scripts');
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Parse the structured args handed in via env TOOLFUNNEL_TOOL_ARGS.
|
|
60
|
+
* @returns {{ value: any } | { parseError: string }}
|
|
61
|
+
*/
|
|
62
|
+
function parseStructuredArgs() {
|
|
63
|
+
const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
|
|
64
|
+
if (raw === undefined || raw === null || raw === '') {
|
|
65
|
+
return { value: null };
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
return { value: JSON.parse(raw) };
|
|
69
|
+
} catch (_err) {
|
|
70
|
+
return { parseError: `TOOLFUNNEL_TOOL_ARGS was not valid JSON: ${String(raw)}` };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Pure core: given the parsed args, perform the requested state/register change.
|
|
76
|
+
* Throws are caught by main() and surfaced as { ok:false, error }; registry.remove
|
|
77
|
+
* throws on an unknown id, setToolEnabled/clearToolState throw on a bad id.
|
|
78
|
+
*
|
|
79
|
+
* @param {any} args
|
|
80
|
+
* @returns {object} the JSON-serialisable result
|
|
81
|
+
*/
|
|
82
|
+
function run(args) {
|
|
83
|
+
if (!args || typeof args !== 'object' || Array.isArray(args)) {
|
|
84
|
+
return { ok: false, error: "args must be an object { id, action: 'enable'|'disable'|'hot'|'unhot'|'hide'|'unhide'|'remove' }" };
|
|
85
|
+
}
|
|
86
|
+
const { id, action } = args;
|
|
87
|
+
if (typeof id !== 'string' || id.length === 0) {
|
|
88
|
+
return { ok: false, error: 'id must be a non-empty string' };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (action === 'enable' || action === 'disable') {
|
|
92
|
+
const enabled = action === 'enable';
|
|
93
|
+
const state = setToolEnabled(STATE_PATH, id, enabled);
|
|
94
|
+
return { ok: true, id, action, enabled, state };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (action === 'hot' || action === 'unhot') {
|
|
98
|
+
// Promote (hot) / demote (unhot) the tool on the TOP-LEVEL every-turn surface. Keyed by the
|
|
99
|
+
// surfaced name (a local id, an upstream surfaced name, or a meta-tool name) - the same key the
|
|
100
|
+
// server's matrix assembler reads. The id is NOT verified against the register here: an upstream
|
|
101
|
+
// surfaced name or a meta-tool name is a legitimate key with no register entry.
|
|
102
|
+
const hot = action === 'hot';
|
|
103
|
+
const state = setToolHot(STATE_PATH, id, hot);
|
|
104
|
+
return { ok: true, id, action, hot, state };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (action === 'hide' || action === 'unhide') {
|
|
108
|
+
// Declutter the MANAGER views (tf_list + the UI) only - hide/unhide does NOT change the lean list
|
|
109
|
+
// or the top-level surface the connected AI sees. Same id-not-verified policy as hot/unhot.
|
|
110
|
+
const hidden = action === 'hide';
|
|
111
|
+
const state = setToolHidden(STATE_PATH, id, hidden);
|
|
112
|
+
return { ok: true, id, action, hidden, state };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (action === 'remove') {
|
|
116
|
+
// Drop the register entry first (throws on unknown id), then clear any overlay.
|
|
117
|
+
const registry = loadRegistry(REGISTER_PATH, { scriptsRoot: SCRIPTS_ROOT });
|
|
118
|
+
registry.remove(id);
|
|
119
|
+
const state = clearToolState(STATE_PATH, id);
|
|
120
|
+
return { ok: true, id, action, removed: true, state };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { ok: false, error: "action must be one of 'enable'|'disable'|'hot'|'unhot'|'hide'|'unhide'|'remove'" };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function main() {
|
|
127
|
+
const parsed = parseStructuredArgs();
|
|
128
|
+
let payload;
|
|
129
|
+
if (parsed.parseError) {
|
|
130
|
+
payload = { ok: false, error: parsed.parseError };
|
|
131
|
+
} else {
|
|
132
|
+
try {
|
|
133
|
+
payload = run(parsed.value);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
payload = { ok: false, error: (err && err.message) || String(err) };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
139
|
+
process.exit(0);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
main();
|