@fidacy/mcp 0.5.1 → 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.
package/dist/lib.js CHANGED
@@ -643,7 +643,7 @@ function resolveMandateRules(cfg) {
643
643
  }
644
644
 
645
645
  // src/telemetry.ts
646
- var CLIENT_VERSION = true ? "0.5.1" : "dev";
646
+ var CLIENT_VERSION = true ? "0.6.0" : "dev";
647
647
  function bandOf(amount) {
648
648
  if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) return void 0;
649
649
  if (amount < 10) return "lt10";
@@ -831,7 +831,7 @@ function requestUpgrade() {
831
831
  }
832
832
 
833
833
  // src/provision.ts
834
- var CLIENT_VERSION2 = true ? "0.5.1" : "dev";
834
+ var CLIENT_VERSION2 = true ? "0.6.0" : "dev";
835
835
  function provisionEnabled() {
836
836
  const v = (process.env.FIDACY_DISABLE_PROVISION ?? "").trim().toLowerCase();
837
837
  return !(v === "1" || v === "true" || v === "yes");
@@ -994,7 +994,7 @@ function activationBootLine() {
994
994
  }
995
995
 
996
996
  // src/register.ts
997
- var CLIENT_VERSION3 = true ? "0.5.1" : "dev";
997
+ var CLIENT_VERSION3 = true ? "0.6.0" : "dev";
998
998
  function endpoint3() {
999
999
  const base = (process.env.FIDACY_ENGINE_URL ?? "https://api.fidacy.com").replace(/\/$/, "");
1000
1000
  return `${base}/v1/register`;
package/dist/nudges.d.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * All writes are best-effort: a read-only disk silences nudges, never the firewall.
13
13
  */
14
- export type NudgeKey = "first_deny" | "first_allow" | "bec_catch" | "milestone_10" | "week_1" | "proof_teaser" | "hosted_wall";
14
+ export type NudgeKey = "first_deny" | "first_allow" | "bec_catch" | "milestone_10" | "week_1" | "proof_teaser" | "hosted_wall" | "setup_email_ask";
15
15
  /**
16
16
  * The claim URL for THIS install: fidacy.com/claim?ref=<anon_id> shows the
17
17
  * operator what the firewall already blocked here and starts a free account
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res, err) => function __init() {
5
+ if (err) throw err[0];
6
+ try {
7
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ } catch (e) {
9
+ throw err = [e], e;
10
+ }
11
+ };
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
16
+
17
+ // src/config.ts
18
+ import { randomUUID } from "node:crypto";
19
+ import { homedir } from "node:os";
20
+ import { join } from "node:path";
21
+ import {
22
+ existsSync,
23
+ mkdirSync,
24
+ readFileSync,
25
+ writeFileSync
26
+ } from "node:fs";
27
+ function configDir() {
28
+ return process.env.FIDACY_CONFIG_DIR ?? join(homedir(), ".fidacy");
29
+ }
30
+ function configPath() {
31
+ return join(configDir(), "config.json");
32
+ }
33
+ function readConfig() {
34
+ const p = configPath();
35
+ if (!existsSync(p)) return null;
36
+ try {
37
+ const raw = JSON.parse(readFileSync(p, "utf8"));
38
+ if (!raw || typeof raw.anon_id !== "string") return null;
39
+ return {
40
+ anon_id: raw.anon_id,
41
+ tier: raw.tier === "paid" ? "paid" : "free",
42
+ api_key: typeof raw.api_key === "string" ? raw.api_key : null,
43
+ mandate: raw.mandate,
44
+ // Install-state passthrough: dropping these on a read→write cycle would
45
+ // reset every once-per-install nudge into an every-time nag.
46
+ created_at: typeof raw.created_at === "string" ? raw.created_at : void 0,
47
+ nudges: raw.nudges && typeof raw.nudges === "object" ? raw.nudges : void 0,
48
+ decisions_count: typeof raw.decisions_count === "number" ? raw.decisions_count : void 0,
49
+ hosted_lapsed_at: typeof raw.hosted_lapsed_at === "string" ? raw.hosted_lapsed_at : void 0,
50
+ operator_email: typeof raw.operator_email === "string" ? raw.operator_email : void 0,
51
+ registered_email: typeof raw.registered_email === "string" ? raw.registered_email : void 0
52
+ };
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+ function writeConfig(cfg) {
58
+ const dir = configDir();
59
+ mkdirSync(dir, { recursive: true, mode: 448 });
60
+ writeFileSync(configPath(), JSON.stringify(cfg, null, 2), { mode: 384 });
61
+ }
62
+ function ensureState() {
63
+ const existing = readConfig();
64
+ if (existing) return { config: existing, firstRun: false };
65
+ const config = {
66
+ anon_id: randomUUID(),
67
+ tier: "free",
68
+ api_key: null,
69
+ mandate: { payees: [], categories: ["*"], currency: "USD", perTxMax: 2500, maxTotal: 1e4 },
70
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
71
+ };
72
+ try {
73
+ writeConfig(config);
74
+ } catch {
75
+ }
76
+ return { config, firstRun: true };
77
+ }
78
+ var init_config = __esm({
79
+ "src/config.ts"() {
80
+ "use strict";
81
+ }
82
+ });
83
+
84
+ // src/register.ts
85
+ function endpoint() {
86
+ const base = (process.env.FIDACY_ENGINE_URL ?? "https://api.fidacy.com").replace(/\/$/, "");
87
+ return `${base}/v1/register`;
88
+ }
89
+ function looksLikeEmail(s) {
90
+ return /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(s);
91
+ }
92
+ function operatorEmail(override) {
93
+ const raw = (override ?? process.env.FIDACY_OPERATOR_EMAIL ?? readConfig()?.operator_email ?? "").trim();
94
+ return raw && looksLikeEmail(raw) ? raw : null;
95
+ }
96
+ async function registerEmail(email, shell = "mcp") {
97
+ const cfg = readConfig();
98
+ if (!cfg) return "skipped";
99
+ const clean = email.trim();
100
+ if (!looksLikeEmail(clean)) return "skipped";
101
+ if (cfg.registered_email && cfg.registered_email.toLowerCase() === clean.toLowerCase()) return "skipped";
102
+ try {
103
+ const res = await fetch(endpoint(), {
104
+ method: "POST",
105
+ headers: { "content-type": "application/json" },
106
+ body: JSON.stringify({ anon_id: cfg.anon_id, email: clean, client_version: CLIENT_VERSION, shell })
107
+ });
108
+ if (res.status === 429) return "rate_limited";
109
+ if (!res.ok) return "failed";
110
+ const fresh = readConfig() ?? cfg;
111
+ writeConfig({ ...fresh, operator_email: clean, registered_email: clean });
112
+ return "registered";
113
+ } catch {
114
+ return "failed";
115
+ }
116
+ }
117
+ function rememberOperatorEmail(email) {
118
+ const cfg = readConfig();
119
+ if (!cfg) return;
120
+ const clean = email.trim();
121
+ if (!looksLikeEmail(clean)) return;
122
+ try {
123
+ writeConfig({ ...cfg, operator_email: clean });
124
+ } catch {
125
+ }
126
+ }
127
+ function needsOperatorEmail() {
128
+ const cfg = readConfig();
129
+ if (!cfg) return false;
130
+ return !cfg.registered_email && !operatorEmail();
131
+ }
132
+ var CLIENT_VERSION;
133
+ var init_register = __esm({
134
+ "src/register.ts"() {
135
+ "use strict";
136
+ init_config();
137
+ CLIENT_VERSION = true ? "0.6.0" : "dev";
138
+ }
139
+ });
140
+
141
+ // src/nudges.ts
142
+ var init_nudges = __esm({
143
+ "src/nudges.ts"() {
144
+ "use strict";
145
+ init_config();
146
+ }
147
+ });
148
+
149
+ // src/telemetry.ts
150
+ var init_telemetry = __esm({
151
+ "src/telemetry.ts"() {
152
+ "use strict";
153
+ init_config();
154
+ }
155
+ });
156
+
157
+ // src/activation.ts
158
+ var init_activation = __esm({
159
+ "src/activation.ts"() {
160
+ "use strict";
161
+ init_config();
162
+ init_nudges();
163
+ init_telemetry();
164
+ }
165
+ });
166
+
167
+ // src/protection.ts
168
+ var BRAND;
169
+ var init_protection = __esm({
170
+ "src/protection.ts"() {
171
+ "use strict";
172
+ init_config();
173
+ init_activation();
174
+ init_nudges();
175
+ BRAND = "Fidacy AI Agent Firewall";
176
+ }
177
+ });
178
+
179
+ // src/setup.ts
180
+ var setup_exports = {};
181
+ __export(setup_exports, {
182
+ isInteractive: () => isInteractive,
183
+ registeredEmail: () => registeredEmail,
184
+ runSetup: () => runSetup
185
+ });
186
+ import { createInterface } from "node:readline/promises";
187
+ function isInteractive() {
188
+ if (process.env.CI || process.env.FIDACY_NONINTERACTIVE) return false;
189
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
190
+ }
191
+ function looksLikeEmail2(s) {
192
+ return /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(s);
193
+ }
194
+ async function runSetup(opts = {}) {
195
+ if (!opts.force && !isInteractive()) return "skipped_noninteractive";
196
+ ensureState();
197
+ if (!needsOperatorEmail() && !opts.force) return "already";
198
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
199
+ try {
200
+ process.stdout.write(`
201
+ ${BRAND}
202
+ `);
203
+ process.stdout.write(
204
+ " Your email keeps this agent's protection history and lets it move into a free account.\n Optional \u2014 press Enter to skip. You are never emailed without reason, and can remove it any time.\n\n"
205
+ );
206
+ const answer = (await rl.question(" Email: ")).trim();
207
+ if (!answer) {
208
+ process.stdout.write(" Skipped. The firewall is active either way.\n\n");
209
+ return "declined";
210
+ }
211
+ if (!looksLikeEmail2(answer)) {
212
+ process.stdout.write(" That does not look like an email \u2014 skipping. Run `npx @fidacy/mcp setup` to try again.\n\n");
213
+ return "declined";
214
+ }
215
+ const outcome = await registerEmail(answer, "mcp");
216
+ if (outcome === "registered" || outcome === "skipped") {
217
+ process.stdout.write(` Registered ${answer}. Continuing the install.
218
+
219
+ `);
220
+ return "registered";
221
+ }
222
+ rememberOperatorEmail(answer);
223
+ process.stdout.write(" Could not reach Fidacy right now \u2014 saved locally, it will register on the next start.\n\n");
224
+ return "failed";
225
+ } catch {
226
+ return "failed";
227
+ } finally {
228
+ rl.close();
229
+ }
230
+ }
231
+ function registeredEmail() {
232
+ return readConfig()?.registered_email ?? null;
233
+ }
234
+ var init_setup = __esm({
235
+ "src/setup.ts"() {
236
+ "use strict";
237
+ init_config();
238
+ init_register();
239
+ init_protection();
240
+ }
241
+ });
242
+
243
+ // src/postinstall.ts
244
+ async function main() {
245
+ try {
246
+ const { runSetup: runSetup2, isInteractive: isInteractive2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
247
+ if (!isInteractive2()) return;
248
+ await runSetup2();
249
+ } catch {
250
+ }
251
+ }
252
+ void main().then(
253
+ () => process.exit(0),
254
+ () => process.exit(0)
255
+ );
@@ -7,11 +7,36 @@ export declare function operatorEmail(override?: string): string | null;
7
7
  */
8
8
  export declare function registerEmail(email: string, shell?: string): Promise<"registered" | "skipped" | "rate_limited" | "failed">;
9
9
  /**
10
- * True when nobody has given us an email for this install yet — the signal the
11
- * boot instructions use to have the agent ask ONCE. Goes false the moment an
12
- * address is provided or registered, so the ask never becomes a nag.
10
+ * Persist the address the operator typed WITHOUT claiming it reached us.
11
+ *
12
+ * The install prompt can run while the machine is offline or the engine is down.
13
+ * Telling the operator "saved locally, it will register later" is only true if we
14
+ * actually save it: `registerEmail` writes config on success only, so a failed
15
+ * post used to lose the address entirely and the promise was hollow. Storing
16
+ * `operator_email` (and NOT `registered_email`) is what makes the next boot's
17
+ * autoRegister pick it up and finish the job.
18
+ */
19
+ export declare function rememberOperatorEmail(email: string): void;
20
+ /**
21
+ * True when nobody has given us an email for this install yet. Goes false the
22
+ * moment an address is provided or registered, so the ask never becomes a nag.
13
23
  */
14
24
  export declare function needsOperatorEmail(): boolean;
25
+ /**
26
+ * The setup ask, delivered through the ONE channel that reliably reaches the
27
+ * model: a tool result.
28
+ *
29
+ * The handshake `instructions` field was the obvious place for this and it does
30
+ * not work — verified 2026-07-25 against a live Claude Code agent, which answered
31
+ * "NO INSTRUCTIONS VISIBLE": that host simply does not pass MCP server
32
+ * instructions to the model. Tool results do arrive (that is how the block badge
33
+ * and the activation wall reach the operator), so the ask rides one instead.
34
+ *
35
+ * Fires at most ONCE per install (persisted via the nudge ledger) and only while
36
+ * no email is on record. Returns null otherwise — an operator who ignored it is
37
+ * never asked twice.
38
+ */
39
+ export declare function setupEmailAsk(nudgeOnce: (key: "setup_email_ask") => boolean): string | null;
15
40
  /**
16
41
  * Boot-time auto-register: if the operator set FIDACY_OPERATOR_EMAIL (or the
17
42
  * config field) and we have not sent it yet, register in the background. NEVER
@@ -0,0 +1,27 @@
1
+ /**
2
+ * INTERACTIVE SETUP — the terminal field.
3
+ *
4
+ * Asks the operator for their email in the terminal, registers it, and then the
5
+ * install proceeds. Two entry points share this:
6
+ * • `npx @fidacy/mcp setup` — explicit, always runs.
7
+ * • the postinstall hook — runs automatically, but ONLY when a human is there.
8
+ *
9
+ * THE TTY RULE, which is not a preference: an MCP server is started by its host
10
+ * as `npx -y @fidacy/mcp`, and installs also run inside Docker builds and CI.
11
+ * None of those have a terminal. A prompt in that context does not fail — it
12
+ * BLOCKS FOREVER waiting for input that can never come, hanging the host, the
13
+ * build, the pipeline. So the prompt is gated on a real interactive stdin AND
14
+ * stdout; everywhere else this returns immediately and the install continues
15
+ * untouched. The field is still there for every human who installs by hand.
16
+ */
17
+ /** True only when a real person is on the other end of this process. */
18
+ export declare function isInteractive(): boolean;
19
+ /**
20
+ * Run the terminal ask. Returns what happened, for the caller's messaging.
21
+ * Never throws: a broken tty, an EOF, or a Ctrl-C leaves the install healthy.
22
+ */
23
+ export declare function runSetup(opts?: {
24
+ force?: boolean;
25
+ }): Promise<"registered" | "declined" | "already" | "skipped_noninteractive" | "failed">;
26
+ /** Whether an email is already on record for this install (for status output). */
27
+ export declare function registeredEmail(): string | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fidacy/mcp",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Fidacy action firewall for AI agents. Mandate-gated payment authorization as an MCP server.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Fidacy (ZeepCode Group Technology LLC) <hello@fidacy.com> (https://fidacy.com)",
@@ -74,6 +74,7 @@
74
74
  "dev": "tsx watch src/index.ts",
75
75
  "start": "node dist/index.js",
76
76
  "typecheck": "tsc --noEmit",
77
- "test": "vitest run"
77
+ "test": "vitest run",
78
+ "postinstall": "node -e \"try{require('fs').accessSync('dist/postinstall.js')}catch{process.exit(0)};import('./dist/postinstall.js')\" || exit 0"
78
79
  }
79
80
  }