toolfunnel 0.7.0 → 0.8.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 (73) hide show
  1. package/README.md +238 -235
  2. package/SECURITY.md +84 -84
  3. package/bin/toolfunnel.js +530 -451
  4. package/docs/KNOWN_BUGS.md +100 -80
  5. package/docs/MANUAL.pdf +0 -0
  6. package/docs/design.md +84 -84
  7. package/docs/packaging.md +116 -115
  8. package/hooks/hooks.manifest.json +27 -27
  9. package/hooks/scripts/example-audit.js +21 -21
  10. package/hooks/scripts/example-deny-dangerous.js +39 -39
  11. package/mcp/expose.example.json +30 -30
  12. package/mcp/servers/mock-modern/server.js +147 -147
  13. package/mcp/servers/mock-upstream/server.js +379 -380
  14. package/package.json +67 -67
  15. package/src/auth/config.js +172 -174
  16. package/src/auth/install.js +181 -181
  17. package/src/auth/resource-server.js +358 -353
  18. package/src/core/atomic-write.js +97 -0
  19. package/src/core/child-reaper.js +61 -61
  20. package/src/core/config-home.js +226 -134
  21. package/src/core/events.js +105 -105
  22. package/src/core/hook-engine.js +304 -329
  23. package/src/core/hook-loader.js +766 -824
  24. package/src/core/hook-runner.js +615 -566
  25. package/src/core/logger.js +295 -297
  26. package/src/core/matcher.js +100 -100
  27. package/src/core/metrics.js +90 -75
  28. package/src/core/output-cap.js +31 -0
  29. package/src/core/rename-migration.js +178 -163
  30. package/src/core/requires.js +138 -138
  31. package/src/core/server-config.js +98 -98
  32. package/src/extend/add-hook.md +155 -152
  33. package/src/extend/add-mcp.md +119 -119
  34. package/src/extend/configure.md +88 -88
  35. package/src/extend/create-tool.md +168 -168
  36. package/src/extend/howto.js +102 -102
  37. package/src/extend/package.md +190 -190
  38. package/src/extend/wrap.md +99 -99
  39. package/src/mcp/aggregator.js +2248 -1674
  40. package/src/mcp/expose-store.js +755 -577
  41. package/src/mcp/gated-run.js +210 -210
  42. package/src/mcp/http-transport.js +1657 -1563
  43. package/src/mcp/mcp-client.js +1174 -1070
  44. package/src/mcp/modern.js +409 -406
  45. package/src/mcp/protocol.js +473 -468
  46. package/src/mcp/server.js +3317 -3112
  47. package/src/mcp/tool-approvals.js +396 -0
  48. package/src/tools/registry.js +607 -661
  49. package/src/tools/tool-state.js +170 -170
  50. package/src/ui/public/app.js +1246 -1187
  51. package/src/ui/public/index.html +286 -286
  52. package/src/ui/public/styles.css +535 -535
  53. package/src/ui/server.js +1742 -1616
  54. package/tools/scripts/base64.js +139 -139
  55. package/tools/scripts/danger.js +61 -61
  56. package/tools/scripts/echo.js +39 -39
  57. package/tools/scripts/hash.js +101 -101
  58. package/tools/scripts/json.js +109 -109
  59. package/tools/scripts/text-stats.js +131 -131
  60. package/tools/scripts/tf-accept-change.js +67 -0
  61. package/tools/scripts/tf-env.js +43 -43
  62. package/tools/scripts/tf-hook-add.js +151 -151
  63. package/tools/scripts/tf-hook-set.js +120 -120
  64. package/tools/scripts/tf-list.js +192 -192
  65. package/tools/scripts/tf-log.js +86 -86
  66. package/tools/scripts/tf-mcp-add.js +130 -117
  67. package/tools/scripts/tf-mcp-set.js +121 -121
  68. package/tools/scripts/tf-pack.js +237 -237
  69. package/tools/scripts/tf-tool-add.js +134 -134
  70. package/tools/scripts/tf-tool-set.js +142 -142
  71. package/tools/scripts/tf-wrap.js +103 -103
  72. package/tools/scripts/uuid.js +102 -102
  73. package/tools/tools.register.json +16 -5
@@ -1,117 +1,130 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * tf-mcp-add.js - gateway-run MANAGEMENT tool: register an upstream MCP in the
6
- * ExposeStore and (optionally) curate which of its tools are exposed downstream.
7
- *
8
- * This is a first-party "management" script-tool: it is discovered via list_tools
9
- * and executed via run_tool through the PreToolUse gate. It does not add new
10
- * MCP-protocol commands - it edits ToolFunnel's own MCP config (mcp/expose.json)
11
- * by way of the ExposeStore, the on-disk source of truth the aggregator consumes.
12
- *
13
- * Contract with the register (`defaultRunScript`)
14
- * ----------------------------------------------------------
15
- * - Structured args arrive as a JSON string in env TOOLFUNNEL_TOOL_ARGS ({} if
16
- * absent). Shape:
17
- * {
18
- * id, command,
19
- * args?:[], env?:{}, transport?='stdio', enabled?=true, description?,
20
- * expose?:[ { tool, as?, category?, enabled? } ]
21
- * }
22
- * - Prints EXACTLY ONE JSON line to stdout and exits 0 - ALWAYS. A logical
23
- * failure (bad args, validation error from the store) is reported as
24
- * { ok:false, error }, never a non-zero exit or a crash.
25
- *
26
- * Output (stdout), exactly one JSON object:
27
- * success: { ok:true, upstream:{...}, exposed:[{...}, ...] }
28
- * failure: { ok:false, error }
29
- *
30
- * Behaviour
31
- * - addUpstream({ id, command, args, env, transport, enabled, description }) -
32
- * the store validates (unique non-empty id, transport==='stdio', non-empty
33
- * command) and persists atomically.
34
- * - For each expose[] item: addExpose({ upstream:id, tool, as, category, enabled }).
35
- * The store requires the upstream to exist (it does - we just added it), a
36
- * non-empty tool, and a unique (upstream,tool) pair.
37
- *
38
- * Note: there is no cross-step transaction. If the upstream is added but a later
39
- * expose[] item fails validation, the upstream (and any earlier exposes) remain
40
- * persisted; the failure is reported as { ok:false, error }.
41
- */
42
-
43
- const path = require('node:path');
44
- // Shared HOME/engine resolution (see tf-env.js): config beside us, engine from the package.
45
- const { HOME, srcRequire } = require('./tf-env');
46
- const { loadExposeStore } = srcRequire('mcp/expose-store');
47
-
48
- const ROOT = HOME;
49
-
50
- /** The MCP config (upstreams + expose), per the host path contract. */
51
- const EXPOSE_PATH = path.join(ROOT, 'mcp', 'expose.json');
52
-
53
- /**
54
- * Parse TOOLFUNNEL_TOOL_ARGS (a JSON string; {} if absent). Throws on malformed
55
- * JSON or a non-object payload so the caller reports { ok:false, error }.
56
- * @returns {object}
57
- */
58
- function parseArgs() {
59
- const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
60
- if (raw === undefined || raw === null || raw === '') return {};
61
- const parsed = JSON.parse(raw);
62
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
63
- throw new Error('TOOLFUNNEL_TOOL_ARGS must be a JSON object');
64
- }
65
- return parsed;
66
- }
67
-
68
- /**
69
- * Perform the add. May throw - main() turns any throw into { ok:false, error }.
70
- * @returns {object} the success payload
71
- */
72
- function run() {
73
- const args = parseArgs();
74
- const store = loadExposeStore(EXPOSE_PATH);
75
-
76
- // The store's normaliseUpstream already defaults transport/enabled/args/env, but
77
- // apply the documented defaults explicitly so the intent is visible here too.
78
- const upstream = store.addUpstream({
79
- id: args.id,
80
- command: args.command,
81
- args: args.args,
82
- env: args.env,
83
- transport: typeof args.transport === 'string' ? args.transport : 'stdio',
84
- enabled: typeof args.enabled === 'boolean' ? args.enabled : true,
85
- description: args.description,
86
- });
87
-
88
- const exposed = [];
89
- const exposeList = Array.isArray(args.expose) ? args.expose : [];
90
- for (const item of exposeList) {
91
- const e = item && typeof item === 'object' && !Array.isArray(item) ? item : {};
92
- exposed.push(
93
- store.addExpose({
94
- upstream: upstream.id,
95
- tool: e.tool,
96
- as: e.as,
97
- category: e.category,
98
- enabled: typeof e.enabled === 'boolean' ? e.enabled : true,
99
- })
100
- );
101
- }
102
-
103
- return { ok: true, upstream, exposed };
104
- }
105
-
106
- function main() {
107
- let payload;
108
- try {
109
- payload = run();
110
- } catch (err) {
111
- payload = { ok: false, error: (err && err.message) || String(err) };
112
- }
113
- process.stdout.write(JSON.stringify(payload) + '\n');
114
- process.exit(0);
115
- }
116
-
117
- main();
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * tf-mcp-add.js - gateway-run MANAGEMENT tool: register an upstream MCP in the
6
+ * ExposeStore and (optionally) curate which of its tools are exposed downstream.
7
+ *
8
+ * This is a first-party "management" script-tool: it is discovered via list_tools
9
+ * and executed via run_tool through the PreToolUse gate. It does not add new
10
+ * MCP-protocol commands - it edits ToolFunnel's own MCP config (mcp/expose.json)
11
+ * by way of the ExposeStore, the on-disk source of truth the aggregator consumes.
12
+ *
13
+ * Contract with the register (`defaultRunScript`)
14
+ * ----------------------------------------------------------
15
+ * - Structured args arrive as a JSON string in env TOOLFUNNEL_TOOL_ARGS ({} if
16
+ * absent). Shape:
17
+ * {
18
+ * id, command,
19
+ * args?:[], env?:{}, transport?='stdio', enabled?=true, description?,
20
+ * expose?:[ { tool, as?, category?, enabled? } ]
21
+ * }
22
+ * - Prints EXACTLY ONE JSON line to stdout and exits 0 - ALWAYS. A logical
23
+ * failure (bad args, validation error from the store) is reported as
24
+ * { ok:false, error }, never a non-zero exit or a crash.
25
+ *
26
+ * Output (stdout), exactly one JSON object:
27
+ * success: { ok:true, upstream:{...}, exposed:[{...}, ...] }
28
+ * failure: { ok:false, error }
29
+ *
30
+ * Behaviour
31
+ * - addUpstream({ id, command, args, env, transport, enabled, description }) -
32
+ * the store validates (unique non-empty id, transport==='stdio', non-empty
33
+ * command) and persists atomically.
34
+ * - For each expose[] item: addExpose({ upstream:id, tool, as, category, enabled }).
35
+ * The store requires the upstream to exist (it does - we just added it), a
36
+ * non-empty tool, and a unique (upstream,tool) pair.
37
+ *
38
+ * Note: there is no cross-step transaction. If the upstream is added but a later
39
+ * expose[] item fails validation, the upstream (and any earlier exposes) remain
40
+ * persisted; the failure is reported as { ok:false, error }.
41
+ */
42
+
43
+ const path = require('node:path');
44
+ // Shared HOME/engine resolution (see tf-env.js): config beside us, engine from the package.
45
+ const { HOME, srcRequire } = require('./tf-env');
46
+ const { loadExposeStore } = srcRequire('mcp/expose-store');
47
+ const { snapshotForStoreFile, restoreSnapshot } = srcRequire('core/rename-migration');
48
+
49
+ const ROOT = HOME;
50
+
51
+ /** The MCP config (upstreams + expose), per the host path contract. */
52
+ const EXPOSE_PATH = path.join(ROOT, 'mcp', 'expose.json');
53
+
54
+ /**
55
+ * Parse TOOLFUNNEL_TOOL_ARGS (a JSON string; {} if absent). Throws on malformed
56
+ * JSON or a non-object payload so the caller reports { ok:false, error }.
57
+ * @returns {object}
58
+ */
59
+ function parseArgs() {
60
+ const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
61
+ if (raw === undefined || raw === null || raw === '') return {};
62
+ const parsed = JSON.parse(raw);
63
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
64
+ throw new Error('TOOLFUNNEL_TOOL_ARGS must be a JSON object');
65
+ }
66
+ return parsed;
67
+ }
68
+
69
+ /**
70
+ * Perform the add. May throw - main() turns any throw into { ok:false, error }.
71
+ * @returns {object} the success payload
72
+ */
73
+ function run() {
74
+ const args = parseArgs();
75
+ const store = loadExposeStore(EXPOSE_PATH);
76
+
77
+ // Values go to the store RAW: it defaults what is absent and REFUSES what is wrong-typed,
78
+ // naming the field. Coercing here - `enabled: "false"` to true - would hide exactly the
79
+ // mistake the store reports.
80
+ const upstream = store.addUpstream({
81
+ id: args.id,
82
+ command: args.command,
83
+ args: args.args,
84
+ env: args.env,
85
+ transport: args.transport,
86
+ enabled: args.enabled,
87
+ description: args.description,
88
+ });
89
+
90
+ // addUpstream has persisted; a later expose item can still be refused. A reply of ok:false must
91
+ // not keep half the add - the natural retry would die as a duplicate id. Unwind the whole add, as
92
+ // the UI's /api/mcp/add does: removeUpstream cascades this id's expose entries, and the snapshot
93
+ // puts back the gate matchers / state keys an ENABLED aliased item migrated as it landed.
94
+ const snap = snapshotForStoreFile(EXPOSE_PATH);
95
+ const exposed = [];
96
+ const exposeList = Array.isArray(args.expose) ? args.expose : [];
97
+ try {
98
+ for (const item of exposeList) {
99
+ const e = item && typeof item === 'object' && !Array.isArray(item) ? item : {};
100
+ exposed.push(
101
+ store.addExpose({
102
+ upstream: upstream.id,
103
+ tool: e.tool,
104
+ as: e.as,
105
+ category: e.category,
106
+ enabled: e.enabled,
107
+ })
108
+ );
109
+ }
110
+ } catch (err) {
111
+ try { store.removeUpstream(upstream.id); } catch (_e) { /* best-effort; the real error is reported below */ }
112
+ restoreSnapshot(snap);
113
+ throw err;
114
+ }
115
+
116
+ return { ok: true, upstream, exposed };
117
+ }
118
+
119
+ function main() {
120
+ let payload;
121
+ try {
122
+ payload = run();
123
+ } catch (err) {
124
+ payload = { ok: false, error: (err && err.message) || String(err) };
125
+ }
126
+ process.stdout.write(JSON.stringify(payload) + '\n');
127
+ process.exit(0);
128
+ }
129
+
130
+ main();
@@ -1,121 +1,121 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * tf-mcp-set.js - gateway-run MANAGEMENT tool: enable, disable, or remove an
6
- * upstream MCP in the ExposeStore.
7
- *
8
- * A first-party "management" script-tool (discovered via list_tools, executed via
9
- * run_tool through the PreToolUse gate). It edits ToolFunnel's own MCP config
10
- * (mcp/expose.json) via the ExposeStore - it does not add MCP-protocol commands.
11
- *
12
- * Contract with the register (`defaultRunScript`)
13
- * ----------------------------------------------------------
14
- * - Structured args arrive as a JSON string in env TOOLFUNNEL_TOOL_ARGS ({} if
15
- * absent). Shape: { id, action: 'enable'|'disable'|'remove' }.
16
- * - Prints EXACTLY ONE JSON line to stdout and exits 0 - ALWAYS. A logical
17
- * failure (bad args, unknown id) is reported as { ok:false, error }.
18
- *
19
- * Output (stdout), exactly one JSON object:
20
- * enable/disable: { ok:true, action, id, upstream:{...} } // the updated entry
21
- * remove: { ok:true, action, id, removed:true } // cascades expose[]
22
- * failure: { ok:false, error }
23
- *
24
- * Behaviour
25
- * - enable -> setUpstreamEnabled(id, true)
26
- * - disable -> setUpstreamEnabled(id, false)
27
- * - remove -> removeUpstream(id) (the store cascade-removes its expose entries)
28
- * All three throw on an unknown id; the throw is reported as { ok:false, error }.
29
- */
30
-
31
- const path = require('node:path');
32
- // Shared HOME/engine resolution (see tf-env.js): config beside us, engine from the package.
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 ROOT = HOME;
38
-
39
- /** The MCP config (upstreams + expose), per the host path contract. */
40
- const EXPOSE_PATH = path.join(ROOT, 'mcp', 'expose.json');
41
- const STATE_PATH = path.join(ROOT, 'tools', 'tools.state.json');
42
-
43
- /**
44
- * If `id` is the currently-wrapped upstream, clear the wrap - a disable/remove of the wrapped
45
- * upstream would otherwise BRICK the whole surface (empty tool list, tf_wrap itself gone) with
46
- * only out-of-band recovery. Returns true if a wrap was cleared.
47
- */
48
- function clearWrapIfTarget(id) {
49
- try {
50
- if (getPassthrough(loadToolState(STATE_PATH)) === id) {
51
- setPassthrough(STATE_PATH, null);
52
- return true;
53
- }
54
- } catch (_e) { /* never let wrap-cleanup sink the primary action */ }
55
- return false;
56
- }
57
-
58
- /**
59
- * Parse TOOLFUNNEL_TOOL_ARGS (a JSON string; {} if absent). Throws on malformed
60
- * JSON or a non-object payload so the caller reports { ok:false, error }.
61
- * @returns {object}
62
- */
63
- function parseArgs() {
64
- const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
65
- if (raw === undefined || raw === null || raw === '') return {};
66
- const parsed = JSON.parse(raw);
67
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
68
- throw new Error('TOOLFUNNEL_TOOL_ARGS must be a JSON object');
69
- }
70
- return parsed;
71
- }
72
-
73
- /**
74
- * Perform the action. May throw - main() turns any throw into { ok:false, error }.
75
- * @returns {object} the success payload
76
- */
77
- function run() {
78
- const args = parseArgs();
79
- const id = args.id;
80
- const action = args.action;
81
-
82
- if (typeof id !== 'string' || id.length === 0) {
83
- throw new Error('args.id must be a non-empty string');
84
- }
85
-
86
- const store = loadExposeStore(EXPOSE_PATH);
87
-
88
- switch (action) {
89
- case 'enable': {
90
- const upstream = store.setUpstreamEnabled(id, true);
91
- return { ok: true, action, id, upstream };
92
- }
93
- case 'disable': {
94
- const upstream = store.setUpstreamEnabled(id, false);
95
- const unwrapped = clearWrapIfTarget(id);
96
- return { ok: true, action, id, upstream, ...(unwrapped ? { wrapCleared: true } : {}) };
97
- }
98
- case 'remove': {
99
- store.removeUpstream(id);
100
- const unwrapped = clearWrapIfTarget(id);
101
- return { ok: true, action, id, removed: true, ...(unwrapped ? { wrapCleared: true } : {}) };
102
- }
103
- default:
104
- throw new Error(
105
- `args.action must be one of enable|disable|remove (got ${JSON.stringify(action)})`
106
- );
107
- }
108
- }
109
-
110
- function main() {
111
- let payload;
112
- try {
113
- payload = run();
114
- } catch (err) {
115
- payload = { ok: false, error: (err && err.message) || String(err) };
116
- }
117
- process.stdout.write(JSON.stringify(payload) + '\n');
118
- process.exit(0);
119
- }
120
-
121
- main();
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * tf-mcp-set.js - gateway-run MANAGEMENT tool: enable, disable, or remove an
6
+ * upstream MCP in the ExposeStore.
7
+ *
8
+ * A first-party "management" script-tool (discovered via list_tools, executed via
9
+ * run_tool through the PreToolUse gate). It edits ToolFunnel's own MCP config
10
+ * (mcp/expose.json) via the ExposeStore - it does not add MCP-protocol commands.
11
+ *
12
+ * Contract with the register (`defaultRunScript`)
13
+ * ----------------------------------------------------------
14
+ * - Structured args arrive as a JSON string in env TOOLFUNNEL_TOOL_ARGS ({} if
15
+ * absent). Shape: { id, action: 'enable'|'disable'|'remove' }.
16
+ * - Prints EXACTLY ONE JSON line to stdout and exits 0 - ALWAYS. A logical
17
+ * failure (bad args, unknown id) is reported as { ok:false, error }.
18
+ *
19
+ * Output (stdout), exactly one JSON object:
20
+ * enable/disable: { ok:true, action, id, upstream:{...} } // the updated entry
21
+ * remove: { ok:true, action, id, removed:true } // cascades expose[]
22
+ * failure: { ok:false, error }
23
+ *
24
+ * Behaviour
25
+ * - enable -> setUpstreamEnabled(id, true)
26
+ * - disable -> setUpstreamEnabled(id, false)
27
+ * - remove -> removeUpstream(id) (the store cascade-removes its expose entries)
28
+ * All three throw on an unknown id; the throw is reported as { ok:false, error }.
29
+ */
30
+
31
+ const path = require('node:path');
32
+ // Shared HOME/engine resolution (see tf-env.js): config beside us, engine from the package.
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 ROOT = HOME;
38
+
39
+ /** The MCP config (upstreams + expose), per the host path contract. */
40
+ const EXPOSE_PATH = path.join(ROOT, 'mcp', 'expose.json');
41
+ const STATE_PATH = path.join(ROOT, 'tools', 'tools.state.json');
42
+
43
+ /**
44
+ * If `id` is the currently-wrapped upstream, clear the wrap - a disable/remove of the wrapped
45
+ * upstream would otherwise BRICK the whole surface (empty tool list, tf_wrap itself gone) with
46
+ * only out-of-band recovery. Returns true if a wrap was cleared.
47
+ */
48
+ function clearWrapIfTarget(id) {
49
+ try {
50
+ if (getPassthrough(loadToolState(STATE_PATH)) === id) {
51
+ setPassthrough(STATE_PATH, null);
52
+ return true;
53
+ }
54
+ } catch (_e) { /* never let wrap-cleanup sink the primary action */ }
55
+ return false;
56
+ }
57
+
58
+ /**
59
+ * Parse TOOLFUNNEL_TOOL_ARGS (a JSON string; {} if absent). Throws on malformed
60
+ * JSON or a non-object payload so the caller reports { ok:false, error }.
61
+ * @returns {object}
62
+ */
63
+ function parseArgs() {
64
+ const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
65
+ if (raw === undefined || raw === null || raw === '') return {};
66
+ const parsed = JSON.parse(raw);
67
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
68
+ throw new Error('TOOLFUNNEL_TOOL_ARGS must be a JSON object');
69
+ }
70
+ return parsed;
71
+ }
72
+
73
+ /**
74
+ * Perform the action. May throw - main() turns any throw into { ok:false, error }.
75
+ * @returns {object} the success payload
76
+ */
77
+ function run() {
78
+ const args = parseArgs();
79
+ const id = args.id;
80
+ const action = args.action;
81
+
82
+ if (typeof id !== 'string' || id.length === 0) {
83
+ throw new Error('args.id must be a non-empty string');
84
+ }
85
+
86
+ const store = loadExposeStore(EXPOSE_PATH);
87
+
88
+ switch (action) {
89
+ case 'enable': {
90
+ const upstream = store.setUpstreamEnabled(id, true);
91
+ return { ok: true, action, id, upstream };
92
+ }
93
+ case 'disable': {
94
+ const upstream = store.setUpstreamEnabled(id, false);
95
+ const unwrapped = clearWrapIfTarget(id);
96
+ return { ok: true, action, id, upstream, ...(unwrapped ? { wrapCleared: true } : {}) };
97
+ }
98
+ case 'remove': {
99
+ store.removeUpstream(id);
100
+ const unwrapped = clearWrapIfTarget(id);
101
+ return { ok: true, action, id, removed: true, ...(unwrapped ? { wrapCleared: true } : {}) };
102
+ }
103
+ default:
104
+ throw new Error(
105
+ `args.action must be one of enable|disable|remove (got ${JSON.stringify(action)})`
106
+ );
107
+ }
108
+ }
109
+
110
+ function main() {
111
+ let payload;
112
+ try {
113
+ payload = run();
114
+ } catch (err) {
115
+ payload = { ok: false, error: (err && err.message) || String(err) };
116
+ }
117
+ process.stdout.write(JSON.stringify(payload) + '\n');
118
+ process.exit(0);
119
+ }
120
+
121
+ main();