toolfunnel 0.4.0 → 0.6.0

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.
Files changed (78) hide show
  1. package/README.md +102 -213
  2. package/bin/toolfunnel.js +372 -147
  3. package/docs/KNOWN_BUGS.md +99 -225
  4. package/docs/MANUAL.pdf +0 -0
  5. package/docs/design.md +70 -0
  6. package/docs/packaging.md +115 -115
  7. package/hooks/hooks.manifest.json +27 -27
  8. package/hooks/scripts/example-audit.js +21 -21
  9. package/hooks/scripts/example-deny-dangerous.js +31 -31
  10. package/mcp/expose.example.json +30 -30
  11. package/mcp/servers/mock-modern/server.js +147 -0
  12. package/mcp/servers/mock-upstream/server.js +192 -12
  13. package/package.json +1 -1
  14. package/src/auth/config.js +174 -174
  15. package/src/auth/install.js +181 -181
  16. package/src/auth/resource-server.js +344 -344
  17. package/src/core/config-home.js +134 -134
  18. package/src/core/events.js +105 -105
  19. package/src/core/hook-engine.js +275 -275
  20. package/src/core/hook-loader.js +791 -791
  21. package/src/core/hook-runner.js +346 -346
  22. package/src/core/logger.js +171 -171
  23. package/src/core/matcher.js +51 -51
  24. package/src/core/metrics.js +75 -75
  25. package/src/core/requires.js +138 -138
  26. package/src/core/server-config.js +98 -77
  27. package/src/core/tool-registry.js +180 -180
  28. package/src/extend/add-hook.md +152 -142
  29. package/src/extend/add-mcp.md +122 -121
  30. package/src/extend/configure.md +87 -0
  31. package/src/extend/create-tool.md +168 -152
  32. package/src/extend/howto.js +102 -98
  33. package/src/extend/package.md +187 -187
  34. package/src/extend/wrap.md +99 -0
  35. package/src/mcp/aggregator.js +1246 -807
  36. package/src/mcp/expose-store.js +460 -430
  37. package/src/mcp/gated-run.js +201 -194
  38. package/src/mcp/http-transport.js +1311 -981
  39. package/src/mcp/mcp-client.js +907 -530
  40. package/src/mcp/modern.js +372 -0
  41. package/src/mcp/protocol.js +468 -466
  42. package/src/mcp/server.js +2688 -1322
  43. package/src/packages/packager.js +715 -715
  44. package/src/tools/drift.js +138 -138
  45. package/src/tools/registry.js +513 -511
  46. package/src/tools/tool-state.js +170 -104
  47. package/src/ui/public/app.js +1174 -1055
  48. package/src/ui/public/index.html +286 -249
  49. package/src/ui/public/styles.css +535 -535
  50. package/src/ui/server.js +1348 -1190
  51. package/tools/scripts/base64.js +139 -139
  52. package/tools/scripts/danger.js +61 -61
  53. package/tools/scripts/echo.js +39 -39
  54. package/tools/scripts/hash.js +101 -101
  55. package/tools/scripts/json.js +109 -109
  56. package/tools/scripts/text-stats.js +131 -131
  57. package/tools/scripts/tf-env.js +43 -43
  58. package/tools/scripts/tf-hook-add.js +151 -151
  59. package/tools/scripts/tf-hook-set.js +112 -112
  60. package/tools/scripts/tf-list.js +192 -192
  61. package/tools/scripts/tf-log.js +86 -86
  62. package/tools/scripts/tf-mcp-add.js +117 -117
  63. package/tools/scripts/tf-mcp-set.js +121 -102
  64. package/tools/scripts/tf-pack.js +237 -237
  65. package/tools/scripts/tf-tool-add.js +134 -129
  66. package/tools/scripts/tf-tool-set.js +142 -142
  67. package/tools/scripts/tf-wrap.js +103 -0
  68. package/tools/scripts/uuid.js +102 -102
  69. package/tools/tools.register.json +227 -216
  70. package/docs/admin-ui.md +0 -404
  71. package/docs/configuration.md +0 -402
  72. package/docs/hooks-and-gating.md +0 -428
  73. package/docs/logging.md +0 -223
  74. package/docs/management.md +0 -429
  75. package/docs/oauth.md +0 -103
  76. package/docs/releasing.md +0 -31
  77. package/docs/setup.md +0 -352
  78. package/docs/tools-and-upstreams.md +0 -469
@@ -1,139 +1,139 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * base64-tool.js — a safe, dependency-free local Base64 codec tool.
6
- *
7
- * Purpose
8
- * -------
9
- * Encode UTF-8 text to standard Base64, or decode Base64 back to UTF-8 text.
10
- * A genuinely useful utility for the Tool Manager: inspecting / producing
11
- * Base64 blobs is a frequent chore and this gives a gated, no-side-effect way
12
- * to do it. Uses ONLY Node built-ins (Buffer) — no network, no filesystem.
13
- *
14
- * Contract with the register (the register's `defaultRunScript`)
15
- * ----------------------------------------------------------
16
- * - Structured args arrive as JSON in env TOOLFUNNEL_TOOL_ARGS.
17
- * - Args: { op: "encode" | "decode", text: <string> }.
18
- * - Prints a SINGLE JSON object to stdout and exits 0 — ALWAYS exit 0, even
19
- * on bad input. Bad input is reported as { ok:false, error } rather than a
20
- * thrown exception, because the gated run-path treats exit-0-with-parseable
21
- * stdout as a completed run.
22
- *
23
- * Output (stdout), exactly one JSON object:
24
- * success: { ok:true, op, result }
25
- * failure: { ok:false, error }
26
- *
27
- * Notes on tolerance
28
- * ------------------
29
- * - encode: any string is encodable. A non-string `text` is rejected.
30
- * - decode: Node's Buffer.from(x,'base64') is itself lenient (it skips
31
- * non-base64 chars), so to give callers a MEANINGFUL "bad base64" signal we
32
- * validate the input shape first and round-trip-check the result. If the
33
- * re-encoded form does not match a normalised version of the input, we
34
- * report ok:false rather than silently returning garbage.
35
- *
36
- * Safety invariants (mirror the isolation rule):
37
- * - NO filesystem, NO network, NO process mutation. Pure CPU on the args.
38
- * - NEVER throws for ordinary bad input — only well-shaped JSON on stdout.
39
- */
40
-
41
- /**
42
- * Parse the structured args handed in via env TOOLFUNNEL_TOOL_ARGS.
43
- * Returns the parsed value, or a sentinel describing a parse failure so the
44
- * caller can surface a clean { ok:false, error } instead of crashing.
45
- *
46
- * @returns {{ value: any } | { parseError: string }}
47
- */
48
- function parseStructuredArgs() {
49
- const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
50
- if (raw === undefined || raw === null || raw === '') {
51
- return { value: null };
52
- }
53
- try {
54
- return { value: JSON.parse(raw) };
55
- } catch (_err) {
56
- return { parseError: `TOOLFUNNEL_TOOL_ARGS was not valid JSON: ${String(raw)}` };
57
- }
58
- }
59
-
60
- /**
61
- * Decode tolerantly: validate that the input looks like Base64, decode it, and
62
- * round-trip-check so genuinely malformed input is reported rather than mangled.
63
- *
64
- * @param {string} text the candidate Base64 string
65
- * @returns {{ ok:true, result:string } | { ok:false, error:string }}
66
- */
67
- function decodeTolerant(text) {
68
- // Strip surrounding whitespace; Base64 may legitimately contain internal
69
- // newlines (MIME wrapping), so remove all ASCII whitespace before validating.
70
- const stripped = String(text).replace(/\s+/g, '');
71
- if (stripped.length === 0) {
72
- // Empty input decodes to empty string — a valid, unambiguous result.
73
- return { ok: true, result: '' };
74
- }
75
- // Standard Base64 alphabet (with optional '=' padding). Reject anything else
76
- // up front so the caller gets a clear "bad base64" rather than silent garbage.
77
- if (!/^[A-Za-z0-9+/]+={0,2}$/.test(stripped)) {
78
- return { ok: false, error: 'input is not valid Base64' };
79
- }
80
- // Length of a Base64 string (sans padding-trimming nuance) must be a multiple
81
- // of 4 once padding is included; reject obviously truncated input.
82
- if (stripped.length % 4 !== 0) {
83
- return { ok: false, error: 'input is not valid Base64 (bad length)' };
84
- }
85
- const buf = Buffer.from(stripped, 'base64');
86
- // Round-trip: re-encode and compare to the normalised input. Buffer's decoder
87
- // is lenient, so this catches inputs that "decode" but were never valid.
88
- const reencoded = buf.toString('base64');
89
- if (reencoded !== stripped) {
90
- return { ok: false, error: 'input is not valid Base64' };
91
- }
92
- return { ok: true, result: buf.toString('utf8') };
93
- }
94
-
95
- /**
96
- * Pure core: given the parsed args, produce the result object. Never throws.
97
- *
98
- * @param {any} args
99
- * @returns {object} the JSON-serialisable result
100
- */
101
- function run(args) {
102
- if (!args || typeof args !== 'object' || Array.isArray(args)) {
103
- return { ok: false, error: 'args must be an object { op, text }' };
104
- }
105
- const { op, text } = args;
106
- if (op !== 'encode' && op !== 'decode') {
107
- return { ok: false, error: 'op must be "encode" or "decode"' };
108
- }
109
- if (typeof text !== 'string') {
110
- return { ok: false, error: 'text must be a string' };
111
- }
112
- if (op === 'encode') {
113
- return { ok: true, op: 'encode', result: Buffer.from(text, 'utf8').toString('base64') };
114
- }
115
- // op === 'decode'
116
- const decoded = decodeTolerant(text);
117
- if (!decoded.ok) return { ok: false, error: decoded.error };
118
- return { ok: true, op: 'decode', result: decoded.result };
119
- }
120
-
121
- function main() {
122
- const parsed = parseStructuredArgs();
123
- let payload;
124
- if (parsed.parseError) {
125
- payload = { ok: false, error: parsed.parseError };
126
- } else {
127
- try {
128
- payload = run(parsed.value);
129
- } catch (err) {
130
- // Defensive: run() is written not to throw, but never let an unexpected
131
- // error escape as a non-zero exit / unparseable stdout.
132
- payload = { ok: false, error: `unexpected error: ${(err && err.message) || String(err)}` };
133
- }
134
- }
135
- process.stdout.write(JSON.stringify(payload) + '\n');
136
- process.exit(0);
137
- }
138
-
139
- main();
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * base64-tool.js - a safe, dependency-free local Base64 codec tool.
6
+ *
7
+ * Purpose
8
+ * -------
9
+ * Encode UTF-8 text to standard Base64, or decode Base64 back to UTF-8 text.
10
+ * A genuinely useful utility for the Tool Manager: inspecting / producing
11
+ * Base64 blobs is a frequent chore and this gives a gated, no-side-effect way
12
+ * to do it. Uses ONLY Node built-ins (Buffer) - no network, no filesystem.
13
+ *
14
+ * Contract with the register (the register's `defaultRunScript`)
15
+ * ----------------------------------------------------------
16
+ * - Structured args arrive as JSON in env TOOLFUNNEL_TOOL_ARGS.
17
+ * - Args: { op: "encode" | "decode", text: <string> }.
18
+ * - Prints a SINGLE JSON object to stdout and exits 0 - ALWAYS exit 0, even
19
+ * on bad input. Bad input is reported as { ok:false, error } rather than a
20
+ * thrown exception, because the gated run-path treats exit-0-with-parseable
21
+ * stdout as a completed run.
22
+ *
23
+ * Output (stdout), exactly one JSON object:
24
+ * success: { ok:true, op, result }
25
+ * failure: { ok:false, error }
26
+ *
27
+ * Notes on tolerance
28
+ * ------------------
29
+ * - encode: any string is encodable. A non-string `text` is rejected.
30
+ * - decode: Node's Buffer.from(x,'base64') is itself lenient (it skips
31
+ * non-base64 chars), so to give callers a MEANINGFUL "bad base64" signal we
32
+ * validate the input shape first and round-trip-check the result. If the
33
+ * re-encoded form does not match a normalised version of the input, we
34
+ * report ok:false rather than silently returning garbage.
35
+ *
36
+ * Safety invariants (mirror the isolation rule):
37
+ * - NO filesystem, NO network, NO process mutation. Pure CPU on the args.
38
+ * - NEVER throws for ordinary bad input - only well-shaped JSON on stdout.
39
+ */
40
+
41
+ /**
42
+ * Parse the structured args handed in via env TOOLFUNNEL_TOOL_ARGS.
43
+ * Returns the parsed value, or a sentinel describing a parse failure so the
44
+ * caller can surface a clean { ok:false, error } instead of crashing.
45
+ *
46
+ * @returns {{ value: any } | { parseError: string }}
47
+ */
48
+ function parseStructuredArgs() {
49
+ const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
50
+ if (raw === undefined || raw === null || raw === '') {
51
+ return { value: null };
52
+ }
53
+ try {
54
+ return { value: JSON.parse(raw) };
55
+ } catch (_err) {
56
+ return { parseError: `TOOLFUNNEL_TOOL_ARGS was not valid JSON: ${String(raw)}` };
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Decode tolerantly: validate that the input looks like Base64, decode it, and
62
+ * round-trip-check so genuinely malformed input is reported rather than mangled.
63
+ *
64
+ * @param {string} text the candidate Base64 string
65
+ * @returns {{ ok:true, result:string } | { ok:false, error:string }}
66
+ */
67
+ function decodeTolerant(text) {
68
+ // Strip surrounding whitespace; Base64 may legitimately contain internal
69
+ // newlines (MIME wrapping), so remove all ASCII whitespace before validating.
70
+ const stripped = String(text).replace(/\s+/g, '');
71
+ if (stripped.length === 0) {
72
+ // Empty input decodes to empty string - a valid, unambiguous result.
73
+ return { ok: true, result: '' };
74
+ }
75
+ // Standard Base64 alphabet (with optional '=' padding). Reject anything else
76
+ // up front so the caller gets a clear "bad base64" rather than silent garbage.
77
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(stripped)) {
78
+ return { ok: false, error: 'input is not valid Base64' };
79
+ }
80
+ // Length of a Base64 string (sans padding-trimming nuance) must be a multiple
81
+ // of 4 once padding is included; reject obviously truncated input.
82
+ if (stripped.length % 4 !== 0) {
83
+ return { ok: false, error: 'input is not valid Base64 (bad length)' };
84
+ }
85
+ const buf = Buffer.from(stripped, 'base64');
86
+ // Round-trip: re-encode and compare to the normalised input. Buffer's decoder
87
+ // is lenient, so this catches inputs that "decode" but were never valid.
88
+ const reencoded = buf.toString('base64');
89
+ if (reencoded !== stripped) {
90
+ return { ok: false, error: 'input is not valid Base64' };
91
+ }
92
+ return { ok: true, result: buf.toString('utf8') };
93
+ }
94
+
95
+ /**
96
+ * Pure core: given the parsed args, produce the result object. Never throws.
97
+ *
98
+ * @param {any} args
99
+ * @returns {object} the JSON-serialisable result
100
+ */
101
+ function run(args) {
102
+ if (!args || typeof args !== 'object' || Array.isArray(args)) {
103
+ return { ok: false, error: 'args must be an object { op, text }' };
104
+ }
105
+ const { op, text } = args;
106
+ if (op !== 'encode' && op !== 'decode') {
107
+ return { ok: false, error: 'op must be "encode" or "decode"' };
108
+ }
109
+ if (typeof text !== 'string') {
110
+ return { ok: false, error: 'text must be a string' };
111
+ }
112
+ if (op === 'encode') {
113
+ return { ok: true, op: 'encode', result: Buffer.from(text, 'utf8').toString('base64') };
114
+ }
115
+ // op === 'decode'
116
+ const decoded = decodeTolerant(text);
117
+ if (!decoded.ok) return { ok: false, error: decoded.error };
118
+ return { ok: true, op: 'decode', result: decoded.result };
119
+ }
120
+
121
+ function main() {
122
+ const parsed = parseStructuredArgs();
123
+ let payload;
124
+ if (parsed.parseError) {
125
+ payload = { ok: false, error: parsed.parseError };
126
+ } else {
127
+ try {
128
+ payload = run(parsed.value);
129
+ } catch (err) {
130
+ // Defensive: run() is written not to throw, but never let an unexpected
131
+ // error escape as a non-zero exit / unparseable stdout.
132
+ payload = { ok: false, error: `unexpected error: ${(err && err.message) || String(err)}` };
133
+ }
134
+ }
135
+ process.stdout.write(JSON.stringify(payload) + '\n');
136
+ process.exit(0);
137
+ }
138
+
139
+ main();
@@ -1,61 +1,61 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * danger-tool.js — a demo "dangerous" tool used to PROVE the gate works.
6
- *
7
- * The safety case is: `toolfunnel_run_tool` must route through the hook
8
- * engine's PreToolUse gate, and a DENY there must mean the tool's side effect
9
- * never happens.
10
- *
11
- * This tool's observable side effect is: it APPENDS one line to the file named
12
- * by the env var TOOLFUNNEL_DANGER_LOG. A test arranges a PreToolUse hook that
13
- * DENIES this tool, runs the gated path, and then asserts the log file was NOT
14
- * written (or did not gain a line). If the gate had failed open, the line would
15
- * appear.
16
- *
17
- * When it DOES run (gate allows, or run-direct), it:
18
- * 1. appends `<ISO timestamp> danger-tool fired <args>\n` to TOOLFUNNEL_DANGER_LOG
19
- * 2. prints a JSON confirmation to stdout
20
- * 3. exits 0
21
- *
22
- * If TOOLFUNNEL_DANGER_LOG is unset, it still "fires" (prints, exits 0) but
23
- * writes nothing — so it is never destructive on its own; the log is the only
24
- * effect and the caller chooses the path.
25
- */
26
-
27
- const fs = require('node:fs');
28
-
29
- function structuredArgs() {
30
- const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
31
- if (!raw) return null;
32
- try {
33
- return JSON.parse(raw);
34
- } catch (_err) {
35
- return { _parseError: raw };
36
- }
37
- }
38
-
39
- function main() {
40
- const logPath = process.env.TOOLFUNNEL_DANGER_LOG;
41
- const args = structuredArgs();
42
- const argv = process.argv.slice(2);
43
- let wrote = false;
44
-
45
- if (logPath) {
46
- const line =
47
- new Date().toISOString() +
48
- ' danger-tool fired ' +
49
- JSON.stringify({ args, argv }) +
50
- '\n';
51
- fs.appendFileSync(logPath, line, 'utf8');
52
- wrote = true;
53
- }
54
-
55
- process.stdout.write(
56
- JSON.stringify({ ok: true, fired: true, wrote, logPath: logPath || null }) + '\n'
57
- );
58
- process.exit(0);
59
- }
60
-
61
- main();
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * danger-tool.js - a demo "dangerous" tool used to PROVE the gate works.
6
+ *
7
+ * The safety case is: `toolfunnel_run_tool` must route through the hook
8
+ * engine's PreToolUse gate, and a DENY there must mean the tool's side effect
9
+ * never happens.
10
+ *
11
+ * This tool's observable side effect is: it APPENDS one line to the file named
12
+ * by the env var TOOLFUNNEL_DANGER_LOG. A test arranges a PreToolUse hook that
13
+ * DENIES this tool, runs the gated path, and then asserts the log file was NOT
14
+ * written (or did not gain a line). If the gate had failed open, the line would
15
+ * appear.
16
+ *
17
+ * When it DOES run (gate allows, or run-direct), it:
18
+ * 1. appends `<ISO timestamp> danger-tool fired <args>\n` to TOOLFUNNEL_DANGER_LOG
19
+ * 2. prints a JSON confirmation to stdout
20
+ * 3. exits 0
21
+ *
22
+ * If TOOLFUNNEL_DANGER_LOG is unset, it still "fires" (prints, exits 0) but
23
+ * writes nothing - so it is never destructive on its own; the log is the only
24
+ * effect and the caller chooses the path.
25
+ */
26
+
27
+ const fs = require('node:fs');
28
+
29
+ function structuredArgs() {
30
+ const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
31
+ if (!raw) return null;
32
+ try {
33
+ return JSON.parse(raw);
34
+ } catch (_err) {
35
+ return { _parseError: raw };
36
+ }
37
+ }
38
+
39
+ function main() {
40
+ const logPath = process.env.TOOLFUNNEL_DANGER_LOG;
41
+ const args = structuredArgs();
42
+ const argv = process.argv.slice(2);
43
+ let wrote = false;
44
+
45
+ if (logPath) {
46
+ const line =
47
+ new Date().toISOString() +
48
+ ' danger-tool fired ' +
49
+ JSON.stringify({ args, argv }) +
50
+ '\n';
51
+ fs.appendFileSync(logPath, line, 'utf8');
52
+ wrote = true;
53
+ }
54
+
55
+ process.stdout.write(
56
+ JSON.stringify({ ok: true, fired: true, wrote, logPath: logPath || null }) + '\n'
57
+ );
58
+ process.exit(0);
59
+ }
60
+
61
+ main();
@@ -1,39 +1,39 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * echo-tool.js — a safe local demo tool.
6
- *
7
- * Prints a JSON object of the args it was invoked with, then exits 0.
8
- * It has NO side effects: it touches no filesystem, no network, no env-driven
9
- * writes. It exists so the register / run-path / gate tests have a benign tool
10
- * to exercise (the "allowed" counterpart to danger-tool.js).
11
- *
12
- * Args are read two ways so callers can use whichever is convenient:
13
- * 1. process.argv — positional CLI args, e.g. `node echo-tool.js a b c`
14
- * 2. TOOLFUNNEL_TOOL_ARGS — a JSON-encoded object/array of structured args
15
- * (this is how the register's "script" invoke passes `args` through).
16
- *
17
- * Output (stdout): a single JSON object { ok, args, argv }.
18
- */
19
-
20
- function parseStructuredArgs() {
21
- const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
22
- if (!raw) return null;
23
- try {
24
- return JSON.parse(raw);
25
- } catch (_err) {
26
- // Malformed env — surface it rather than silently dropping.
27
- return { _parseError: `TOOLFUNNEL_TOOL_ARGS was not valid JSON: ${raw}` };
28
- }
29
- }
30
-
31
- function main() {
32
- const argv = process.argv.slice(2);
33
- const args = parseStructuredArgs();
34
- const payload = { ok: true, args, argv };
35
- process.stdout.write(JSON.stringify(payload) + '\n');
36
- process.exit(0);
37
- }
38
-
39
- main();
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * echo-tool.js - a safe local demo tool.
6
+ *
7
+ * Prints a JSON object of the args it was invoked with, then exits 0.
8
+ * It has NO side effects: it touches no filesystem, no network, no env-driven
9
+ * writes. It exists so the register / run-path / gate tests have a benign tool
10
+ * to exercise (the "allowed" counterpart to danger-tool.js).
11
+ *
12
+ * Args are read two ways so callers can use whichever is convenient:
13
+ * 1. process.argv - positional CLI args, e.g. `node echo-tool.js a b c`
14
+ * 2. TOOLFUNNEL_TOOL_ARGS - a JSON-encoded object/array of structured args
15
+ * (this is how the register's "script" invoke passes `args` through).
16
+ *
17
+ * Output (stdout): a single JSON object { ok, args, argv }.
18
+ */
19
+
20
+ function parseStructuredArgs() {
21
+ const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
22
+ if (!raw) return null;
23
+ try {
24
+ return JSON.parse(raw);
25
+ } catch (_err) {
26
+ // Malformed env - surface it rather than silently dropping.
27
+ return { _parseError: `TOOLFUNNEL_TOOL_ARGS was not valid JSON: ${raw}` };
28
+ }
29
+ }
30
+
31
+ function main() {
32
+ const argv = process.argv.slice(2);
33
+ const args = parseStructuredArgs();
34
+ const payload = { ok: true, args, argv };
35
+ process.stdout.write(JSON.stringify(payload) + '\n');
36
+ process.exit(0);
37
+ }
38
+
39
+ main();