@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/CHANGELOG.md +14 -0
- package/dist/core.js +1 -1
- package/dist/index.js +612 -452
- package/dist/lib.js +3 -3
- package/dist/nudges.d.ts +1 -1
- package/dist/postinstall.d.ts +2 -0
- package/dist/postinstall.js +255 -0
- package/dist/register.d.ts +28 -3
- package/dist/setup.d.ts +27 -0
- package/package.json +3 -2
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.
|
|
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.
|
|
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.
|
|
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,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
|
+
);
|
package/dist/register.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
package/dist/setup.d.ts
ADDED
|
@@ -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.
|
|
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
|
}
|