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/tools/scripts/tf-wrap.js
CHANGED
|
@@ -1,103 +1,103 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* tf-wrap.js - gateway-run MANAGEMENT tool: set, clear, or report the PASSTHROUGH wrap.
|
|
6
|
-
*
|
|
7
|
-
* Wrapping turns the gateway into a transparent wrapper for ONE attached upstream MCP: its
|
|
8
|
-
* tools become the ENTIRE advertised surface (under their ORIGINAL names), the meta-tools and
|
|
9
|
-
* every other tool are hidden and uncallable, and every call still fires the PreToolUse gate.
|
|
10
|
-
* Because the gateway answers both MCP eras, this is how a legacy-only (or unmaintained) MCP
|
|
11
|
-
* keeps working with 2026-07-28-era clients.
|
|
12
|
-
*
|
|
13
|
-
* ⚠ SELF-LOCKOUT: the wrap hides ALL ToolFunnel tools - INCLUDING tf_wrap itself. The AI that
|
|
14
|
-
* sets a wrap loses in-band access to undo it. That is why `confirm: true` is REQUIRED to set
|
|
15
|
-
* one. Undo paths that survive the wrap: the config web UI (--ui), or `toolfunnel wrap --off`
|
|
16
|
-
* on a terminal. A running host picks either up live.
|
|
17
|
-
*
|
|
18
|
-
* Contract with the register (`defaultRunScript`)
|
|
19
|
-
* ----------------------------------------------------------
|
|
20
|
-
* - Structured args arrive as a JSON string in env TOOLFUNNEL_TOOL_ARGS ({} if absent).
|
|
21
|
-
* Shape: {} (status) | { off:true } (clear) | { upstream:'<id>', confirm:true } (set).
|
|
22
|
-
* - Prints EXACTLY ONE JSON line to stdout and exits 0 - ALWAYS. A logical failure is
|
|
23
|
-
* reported as { ok:false, error }.
|
|
24
|
-
*
|
|
25
|
-
* Output (stdout), exactly one JSON object:
|
|
26
|
-
* status: { ok:true, wrapping:(id|null), upstreams:[id...] }
|
|
27
|
-
* clear: { ok:true, cleared:true, was:(id|null) }
|
|
28
|
-
* set: { ok:true, wrapping:id, warning:'...' }
|
|
29
|
-
* failure:{ ok:false, error }
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
const path = require('node:path');
|
|
33
|
-
const { HOME, srcRequire } = require('./tf-env');
|
|
34
|
-
const { loadExposeStore } = srcRequire('mcp/expose-store');
|
|
35
|
-
const { loadToolState, getPassthrough, setPassthrough } = srcRequire('tools/tool-state');
|
|
36
|
-
|
|
37
|
-
const STATE_PATH = path.join(HOME, 'tools', 'tools.state.json');
|
|
38
|
-
const EXPOSE_PATH = path.join(HOME, 'mcp', 'expose.json');
|
|
39
|
-
|
|
40
|
-
const LOCKOUT_WARNING =
|
|
41
|
-
'The wrap hides ALL ToolFunnel tools - including tf_wrap itself. In-band undo is now ' +
|
|
42
|
-
'impossible for you: recover via the config web UI (--ui) or `toolfunnel wrap --off` on a terminal.';
|
|
43
|
-
|
|
44
|
-
function parseArgs() {
|
|
45
|
-
const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
|
|
46
|
-
if (raw === undefined || raw === null || raw === '') return {};
|
|
47
|
-
const parsed = JSON.parse(raw);
|
|
48
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
49
|
-
throw new Error('TOOLFUNNEL_TOOL_ARGS must be a JSON object');
|
|
50
|
-
}
|
|
51
|
-
return parsed;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function run() {
|
|
55
|
-
const args = parseArgs();
|
|
56
|
-
|
|
57
|
-
if (args.off === true) {
|
|
58
|
-
const was = getPassthrough(loadToolState(STATE_PATH));
|
|
59
|
-
setPassthrough(STATE_PATH, null);
|
|
60
|
-
return { ok: true, cleared: true, was };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const store = loadExposeStore(EXPOSE_PATH);
|
|
64
|
-
const ids = store.listUpstreams().map((u) => u.id);
|
|
65
|
-
|
|
66
|
-
if (args.upstream === undefined) {
|
|
67
|
-
return { ok: true, wrapping: getPassthrough(loadToolState(STATE_PATH)), upstreams: ids };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const id = args.upstream;
|
|
71
|
-
if (typeof id !== 'string' || id.length === 0) {
|
|
72
|
-
throw new Error('args.upstream must be a non-empty string (an attached upstream id)');
|
|
73
|
-
}
|
|
74
|
-
const upstream = store.getUpstream(id);
|
|
75
|
-
if (!upstream) {
|
|
76
|
-
throw new Error(`no upstream with id "${id}" - attached: ${ids.join(', ') || '(none)'}`);
|
|
77
|
-
}
|
|
78
|
-
if (upstream.enabled === false) {
|
|
79
|
-
throw new Error(`upstream "${id}" is disabled in mcp/expose.json - enable it first (tf_mcp_set)`);
|
|
80
|
-
}
|
|
81
|
-
if (args.confirm !== true) {
|
|
82
|
-
throw new Error(
|
|
83
|
-
'confirm:true is required. ' + LOCKOUT_WARNING +
|
|
84
|
-
' Call again with { "upstream": "' + id + '", "confirm": true } to proceed.'
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
setPassthrough(STATE_PATH, id);
|
|
89
|
-
return { ok: true, wrapping: id, warning: LOCKOUT_WARNING };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function main() {
|
|
93
|
-
let payload;
|
|
94
|
-
try {
|
|
95
|
-
payload = run();
|
|
96
|
-
} catch (err) {
|
|
97
|
-
payload = { ok: false, error: (err && err.message) || String(err) };
|
|
98
|
-
}
|
|
99
|
-
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
100
|
-
process.exit(0);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
main();
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* tf-wrap.js - gateway-run MANAGEMENT tool: set, clear, or report the PASSTHROUGH wrap.
|
|
6
|
+
*
|
|
7
|
+
* Wrapping turns the gateway into a transparent wrapper for ONE attached upstream MCP: its
|
|
8
|
+
* tools become the ENTIRE advertised surface (under their ORIGINAL names), the meta-tools and
|
|
9
|
+
* every other tool are hidden and uncallable, and every call still fires the PreToolUse gate.
|
|
10
|
+
* Because the gateway answers both MCP eras, this is how a legacy-only (or unmaintained) MCP
|
|
11
|
+
* keeps working with 2026-07-28-era clients.
|
|
12
|
+
*
|
|
13
|
+
* ⚠ SELF-LOCKOUT: the wrap hides ALL ToolFunnel tools - INCLUDING tf_wrap itself. The AI that
|
|
14
|
+
* sets a wrap loses in-band access to undo it. That is why `confirm: true` is REQUIRED to set
|
|
15
|
+
* one. Undo paths that survive the wrap: the config web UI (--ui), or `toolfunnel wrap --off`
|
|
16
|
+
* on a terminal. A running host picks either up live.
|
|
17
|
+
*
|
|
18
|
+
* Contract with the register (`defaultRunScript`)
|
|
19
|
+
* ----------------------------------------------------------
|
|
20
|
+
* - Structured args arrive as a JSON string in env TOOLFUNNEL_TOOL_ARGS ({} if absent).
|
|
21
|
+
* Shape: {} (status) | { off:true } (clear) | { upstream:'<id>', confirm:true } (set).
|
|
22
|
+
* - Prints EXACTLY ONE JSON line to stdout and exits 0 - ALWAYS. A logical failure is
|
|
23
|
+
* reported as { ok:false, error }.
|
|
24
|
+
*
|
|
25
|
+
* Output (stdout), exactly one JSON object:
|
|
26
|
+
* status: { ok:true, wrapping:(id|null), upstreams:[id...] }
|
|
27
|
+
* clear: { ok:true, cleared:true, was:(id|null) }
|
|
28
|
+
* set: { ok:true, wrapping:id, warning:'...' }
|
|
29
|
+
* failure:{ ok:false, error }
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const path = require('node:path');
|
|
33
|
+
const { HOME, srcRequire } = require('./tf-env');
|
|
34
|
+
const { loadExposeStore } = srcRequire('mcp/expose-store');
|
|
35
|
+
const { loadToolState, getPassthrough, setPassthrough } = srcRequire('tools/tool-state');
|
|
36
|
+
|
|
37
|
+
const STATE_PATH = path.join(HOME, 'tools', 'tools.state.json');
|
|
38
|
+
const EXPOSE_PATH = path.join(HOME, 'mcp', 'expose.json');
|
|
39
|
+
|
|
40
|
+
const LOCKOUT_WARNING =
|
|
41
|
+
'The wrap hides ALL ToolFunnel tools - including tf_wrap itself. In-band undo is now ' +
|
|
42
|
+
'impossible for you: recover via the config web UI (--ui) or `toolfunnel wrap --off` on a terminal.';
|
|
43
|
+
|
|
44
|
+
function parseArgs() {
|
|
45
|
+
const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
|
|
46
|
+
if (raw === undefined || raw === null || raw === '') return {};
|
|
47
|
+
const parsed = JSON.parse(raw);
|
|
48
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
49
|
+
throw new Error('TOOLFUNNEL_TOOL_ARGS must be a JSON object');
|
|
50
|
+
}
|
|
51
|
+
return parsed;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function run() {
|
|
55
|
+
const args = parseArgs();
|
|
56
|
+
|
|
57
|
+
if (args.off === true) {
|
|
58
|
+
const was = getPassthrough(loadToolState(STATE_PATH));
|
|
59
|
+
setPassthrough(STATE_PATH, null);
|
|
60
|
+
return { ok: true, cleared: true, was };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const store = loadExposeStore(EXPOSE_PATH);
|
|
64
|
+
const ids = store.listUpstreams().map((u) => u.id);
|
|
65
|
+
|
|
66
|
+
if (args.upstream === undefined) {
|
|
67
|
+
return { ok: true, wrapping: getPassthrough(loadToolState(STATE_PATH)), upstreams: ids };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const id = args.upstream;
|
|
71
|
+
if (typeof id !== 'string' || id.length === 0) {
|
|
72
|
+
throw new Error('args.upstream must be a non-empty string (an attached upstream id)');
|
|
73
|
+
}
|
|
74
|
+
const upstream = store.getUpstream(id);
|
|
75
|
+
if (!upstream) {
|
|
76
|
+
throw new Error(`no upstream with id "${id}" - attached: ${ids.join(', ') || '(none)'}`);
|
|
77
|
+
}
|
|
78
|
+
if (upstream.enabled === false) {
|
|
79
|
+
throw new Error(`upstream "${id}" is disabled in mcp/expose.json - enable it first (tf_mcp_set)`);
|
|
80
|
+
}
|
|
81
|
+
if (args.confirm !== true) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
'confirm:true is required. ' + LOCKOUT_WARNING +
|
|
84
|
+
' Call again with { "upstream": "' + id + '", "confirm": true } to proceed.'
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
setPassthrough(STATE_PATH, id);
|
|
89
|
+
return { ok: true, wrapping: id, warning: LOCKOUT_WARNING };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function main() {
|
|
93
|
+
let payload;
|
|
94
|
+
try {
|
|
95
|
+
payload = run();
|
|
96
|
+
} catch (err) {
|
|
97
|
+
payload = { ok: false, error: (err && err.message) || String(err) };
|
|
98
|
+
}
|
|
99
|
+
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
100
|
+
process.exit(0);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
main();
|
package/tools/scripts/uuid.js
CHANGED
|
@@ -1,102 +1,102 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* uuid-tool.js - a safe, dependency-free local UUID generator.
|
|
6
|
-
*
|
|
7
|
-
* Purpose
|
|
8
|
-
* -------
|
|
9
|
-
* Generate one or more RFC 4122 v4 UUIDs via the Node built-in
|
|
10
|
-
* `node:crypto.randomUUID()`. Useful from the Tool Manager whenever a fresh
|
|
11
|
-
* identifier is needed. No network, no filesystem, no new deps.
|
|
12
|
-
*
|
|
13
|
-
* Contract with the register (the register's `defaultRunScript`)
|
|
14
|
-
* ----------------------------------------------------------
|
|
15
|
-
* - Structured args arrive as JSON in env TOOLFUNNEL_TOOL_ARGS.
|
|
16
|
-
* - Args: { count?: <integer> } - how many UUIDs to mint. Defaults to 1.
|
|
17
|
-
* count is clamped to the range [1, 100] so a runaway request cannot ask
|
|
18
|
-
* for an unbounded number.
|
|
19
|
-
* - Prints a SINGLE JSON object to stdout and exits 0 - ALWAYS exit 0, even
|
|
20
|
-
* on bad input (reported as { ok:false, error }), never a thrown exception.
|
|
21
|
-
*
|
|
22
|
-
* Output (stdout), exactly one JSON object:
|
|
23
|
-
* success: { ok:true, uuids: [ "...", ... ] }
|
|
24
|
-
* failure: { ok:false, error }
|
|
25
|
-
*
|
|
26
|
-
* Safety invariants (mirror the isolation rule):
|
|
27
|
-
* - NO filesystem, NO network, NO process mutation. Pure CPU on the args.
|
|
28
|
-
* - count is hard-capped at 100; a non-integer / out-of-range count is an
|
|
29
|
-
* error rather than a silent surprise.
|
|
30
|
-
* - NEVER throws for ordinary bad input - only well-shaped JSON on stdout.
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
const crypto = require('node:crypto');
|
|
34
|
-
|
|
35
|
-
const MAX_COUNT = 100;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Parse the structured args handed in via env TOOLFUNNEL_TOOL_ARGS.
|
|
39
|
-
* @returns {{ value: any } | { parseError: string }}
|
|
40
|
-
*/
|
|
41
|
-
function parseStructuredArgs() {
|
|
42
|
-
const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
|
|
43
|
-
if (raw === undefined || raw === null || raw === '') {
|
|
44
|
-
return { value: null };
|
|
45
|
-
}
|
|
46
|
-
try {
|
|
47
|
-
return { value: JSON.parse(raw) };
|
|
48
|
-
} catch (_err) {
|
|
49
|
-
return { parseError: `TOOLFUNNEL_TOOL_ARGS was not valid JSON: ${String(raw)}` };
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Pure core: given the parsed args, produce the result object. Never throws.
|
|
55
|
-
*
|
|
56
|
-
* @param {any} args may be null (treated as { count: 1 })
|
|
57
|
-
* @returns {object} the JSON-serialisable result
|
|
58
|
-
*/
|
|
59
|
-
function run(args) {
|
|
60
|
-
// No args (or null) is valid - default to a single UUID.
|
|
61
|
-
let count = 1;
|
|
62
|
-
if (args !== null && args !== undefined) {
|
|
63
|
-
if (typeof args !== 'object' || Array.isArray(args)) {
|
|
64
|
-
return { ok: false, error: 'args must be an object { count? }' };
|
|
65
|
-
}
|
|
66
|
-
if (args.count !== undefined && args.count !== null) {
|
|
67
|
-
if (typeof args.count !== 'number' || !Number.isInteger(args.count)) {
|
|
68
|
-
return { ok: false, error: 'count must be an integer' };
|
|
69
|
-
}
|
|
70
|
-
if (args.count < 1) {
|
|
71
|
-
return { ok: false, error: 'count must be >= 1' };
|
|
72
|
-
}
|
|
73
|
-
if (args.count > MAX_COUNT) {
|
|
74
|
-
return { ok: false, error: `count must be <= ${MAX_COUNT}` };
|
|
75
|
-
}
|
|
76
|
-
count = args.count;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
const uuids = [];
|
|
80
|
-
for (let i = 0; i < count; i += 1) {
|
|
81
|
-
uuids.push(crypto.randomUUID());
|
|
82
|
-
}
|
|
83
|
-
return { ok: true, uuids };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function main() {
|
|
87
|
-
const parsed = parseStructuredArgs();
|
|
88
|
-
let payload;
|
|
89
|
-
if (parsed.parseError) {
|
|
90
|
-
payload = { ok: false, error: parsed.parseError };
|
|
91
|
-
} else {
|
|
92
|
-
try {
|
|
93
|
-
payload = run(parsed.value);
|
|
94
|
-
} catch (err) {
|
|
95
|
-
payload = { ok: false, error: `unexpected error: ${(err && err.message) || String(err)}` };
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
99
|
-
process.exit(0);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
main();
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* uuid-tool.js - a safe, dependency-free local UUID generator.
|
|
6
|
+
*
|
|
7
|
+
* Purpose
|
|
8
|
+
* -------
|
|
9
|
+
* Generate one or more RFC 4122 v4 UUIDs via the Node built-in
|
|
10
|
+
* `node:crypto.randomUUID()`. Useful from the Tool Manager whenever a fresh
|
|
11
|
+
* identifier is needed. No network, no filesystem, no new deps.
|
|
12
|
+
*
|
|
13
|
+
* Contract with the register (the register's `defaultRunScript`)
|
|
14
|
+
* ----------------------------------------------------------
|
|
15
|
+
* - Structured args arrive as JSON in env TOOLFUNNEL_TOOL_ARGS.
|
|
16
|
+
* - Args: { count?: <integer> } - how many UUIDs to mint. Defaults to 1.
|
|
17
|
+
* count is clamped to the range [1, 100] so a runaway request cannot ask
|
|
18
|
+
* for an unbounded number.
|
|
19
|
+
* - Prints a SINGLE JSON object to stdout and exits 0 - ALWAYS exit 0, even
|
|
20
|
+
* on bad input (reported as { ok:false, error }), never a thrown exception.
|
|
21
|
+
*
|
|
22
|
+
* Output (stdout), exactly one JSON object:
|
|
23
|
+
* success: { ok:true, uuids: [ "...", ... ] }
|
|
24
|
+
* failure: { ok:false, error }
|
|
25
|
+
*
|
|
26
|
+
* Safety invariants (mirror the isolation rule):
|
|
27
|
+
* - NO filesystem, NO network, NO process mutation. Pure CPU on the args.
|
|
28
|
+
* - count is hard-capped at 100; a non-integer / out-of-range count is an
|
|
29
|
+
* error rather than a silent surprise.
|
|
30
|
+
* - NEVER throws for ordinary bad input - only well-shaped JSON on stdout.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const crypto = require('node:crypto');
|
|
34
|
+
|
|
35
|
+
const MAX_COUNT = 100;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Parse the structured args handed in via env TOOLFUNNEL_TOOL_ARGS.
|
|
39
|
+
* @returns {{ value: any } | { parseError: string }}
|
|
40
|
+
*/
|
|
41
|
+
function parseStructuredArgs() {
|
|
42
|
+
const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
|
|
43
|
+
if (raw === undefined || raw === null || raw === '') {
|
|
44
|
+
return { value: null };
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
return { value: JSON.parse(raw) };
|
|
48
|
+
} catch (_err) {
|
|
49
|
+
return { parseError: `TOOLFUNNEL_TOOL_ARGS was not valid JSON: ${String(raw)}` };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Pure core: given the parsed args, produce the result object. Never throws.
|
|
55
|
+
*
|
|
56
|
+
* @param {any} args may be null (treated as { count: 1 })
|
|
57
|
+
* @returns {object} the JSON-serialisable result
|
|
58
|
+
*/
|
|
59
|
+
function run(args) {
|
|
60
|
+
// No args (or null) is valid - default to a single UUID.
|
|
61
|
+
let count = 1;
|
|
62
|
+
if (args !== null && args !== undefined) {
|
|
63
|
+
if (typeof args !== 'object' || Array.isArray(args)) {
|
|
64
|
+
return { ok: false, error: 'args must be an object { count? }' };
|
|
65
|
+
}
|
|
66
|
+
if (args.count !== undefined && args.count !== null) {
|
|
67
|
+
if (typeof args.count !== 'number' || !Number.isInteger(args.count)) {
|
|
68
|
+
return { ok: false, error: 'count must be an integer' };
|
|
69
|
+
}
|
|
70
|
+
if (args.count < 1) {
|
|
71
|
+
return { ok: false, error: 'count must be >= 1' };
|
|
72
|
+
}
|
|
73
|
+
if (args.count > MAX_COUNT) {
|
|
74
|
+
return { ok: false, error: `count must be <= ${MAX_COUNT}` };
|
|
75
|
+
}
|
|
76
|
+
count = args.count;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const uuids = [];
|
|
80
|
+
for (let i = 0; i < count; i += 1) {
|
|
81
|
+
uuids.push(crypto.randomUUID());
|
|
82
|
+
}
|
|
83
|
+
return { ok: true, uuids };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function main() {
|
|
87
|
+
const parsed = parseStructuredArgs();
|
|
88
|
+
let payload;
|
|
89
|
+
if (parsed.parseError) {
|
|
90
|
+
payload = { ok: false, error: parsed.parseError };
|
|
91
|
+
} else {
|
|
92
|
+
try {
|
|
93
|
+
payload = run(parsed.value);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
payload = { ok: false, error: `unexpected error: ${(err && err.message) || String(err)}` };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
main();
|