carpool-mcp 0.1.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/LICENSE +21 -0
- package/README.md +94 -0
- package/dist/cli.js +1214 -0
- package/dist/consent.js +49 -0
- package/hooks/pre-publish.mjs +74 -0
- package/package.json +42 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1214 @@
|
|
|
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
|
+
// apps/registry/src/scripts/associate-account-lib.ts
|
|
18
|
+
function parseAccountId(value) {
|
|
19
|
+
const v = (value ?? "").trim();
|
|
20
|
+
if (!v) throw new InputError("HEDERA_ACCOUNT_ID is not set. Pass it as an environment variable, e.g. HEDERA_ACCOUNT_ID=0.0.12345");
|
|
21
|
+
if (!/^0\.0\.\d+$/.test(v)) {
|
|
22
|
+
throw new InputError(`HEDERA_ACCOUNT_ID must look like 0.0.12345 (got a value of length ${v.length})`);
|
|
23
|
+
}
|
|
24
|
+
return v;
|
|
25
|
+
}
|
|
26
|
+
function parsePrivateKey(value) {
|
|
27
|
+
let v = (value ?? "").trim();
|
|
28
|
+
if (!v) throw new InputError("HEDERA_PRIVATE_KEY is not set. Pass it as an environment variable, never as an argument.");
|
|
29
|
+
if (v.startsWith("0x") || v.startsWith("0X")) v = v.slice(2);
|
|
30
|
+
v = v.toLowerCase();
|
|
31
|
+
if (!/^[0-9a-f]+$/.test(v)) {
|
|
32
|
+
throw new InputError("HEDERA_PRIVATE_KEY is not hex. Copy the HEX encoded private key from portal.hedera.com.");
|
|
33
|
+
}
|
|
34
|
+
if (v.startsWith(ED25519_DER_PREFIX) && v.length === ED25519_DER_PREFIX.length + 64) {
|
|
35
|
+
throw ed25519Error();
|
|
36
|
+
}
|
|
37
|
+
if (v.startsWith(ECDSA_DER_PREFIX) && v.length === ECDSA_DER_PREFIX.length + 64) {
|
|
38
|
+
return { kind: "ecdsa", rawHex: v.slice(ECDSA_DER_PREFIX.length) };
|
|
39
|
+
}
|
|
40
|
+
if (v.length === 64) return { kind: "raw", rawHex: v };
|
|
41
|
+
throw new InputError(
|
|
42
|
+
`HEDERA_PRIVATE_KEY has ${v.length} hex characters; expected 64 (raw) or a DER-encoded ECDSA key.`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
function checkAccountKeyType(mirrorKeyType) {
|
|
46
|
+
if (mirrorKeyType === "ECDSA_SECP256K1") return;
|
|
47
|
+
if (mirrorKeyType === "ED25519") throw ed25519Error();
|
|
48
|
+
throw new InputError(
|
|
49
|
+
`This account's key is ${mirrorKeyType ?? "unknown"}, not a single ECDSA key. Create an ECDSA testnet account at portal.hedera.com.`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
function ed25519Error() {
|
|
53
|
+
return new InputError(
|
|
54
|
+
"This is an ED25519 key. Carpool needs ECDSA: the x402 Hedera signer fails on ED25519. Create an ECDSA testnet account at portal.hedera.com and use that one."
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
var USDC_TESTNET, ECDSA_DER_PREFIX, ED25519_DER_PREFIX, InputError;
|
|
58
|
+
var init_associate_account_lib = __esm({
|
|
59
|
+
"apps/registry/src/scripts/associate-account-lib.ts"() {
|
|
60
|
+
"use strict";
|
|
61
|
+
USDC_TESTNET = "0.0.429274";
|
|
62
|
+
ECDSA_DER_PREFIX = "3030020100300706052b8104000a04220420";
|
|
63
|
+
ED25519_DER_PREFIX = "302e020100300506032b657004220420";
|
|
64
|
+
InputError = class extends Error {
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// apps/registry/src/scripts/associate-account-run.ts
|
|
70
|
+
var associate_account_run_exports = {};
|
|
71
|
+
__export(associate_account_run_exports, {
|
|
72
|
+
InputError: () => InputError,
|
|
73
|
+
associateAccount: () => associateAccount
|
|
74
|
+
});
|
|
75
|
+
import { AccountId, Client, PrivateKey, TokenAssociateTransaction, TokenId } from "@hiero-ledger/sdk";
|
|
76
|
+
async function mirrorAccount(id) {
|
|
77
|
+
const res = await fetch(`${MIRROR}/api/v1/accounts/${id}`);
|
|
78
|
+
if (res.status === 404) return null;
|
|
79
|
+
if (!res.ok) throw new Error(`mirror node answered ${res.status} for account ${id}`);
|
|
80
|
+
return await res.json();
|
|
81
|
+
}
|
|
82
|
+
async function isAssociated(id, token) {
|
|
83
|
+
const res = await fetch(`${MIRROR}/api/v1/accounts/${id}/tokens?token.id=${token}`);
|
|
84
|
+
if (!res.ok) throw new Error(`mirror node answered ${res.status} for ${id}'s tokens`);
|
|
85
|
+
const json = await res.json();
|
|
86
|
+
return (json.tokens ?? []).length > 0;
|
|
87
|
+
}
|
|
88
|
+
async function associateAccount() {
|
|
89
|
+
const id = parseAccountId(process.env.HEDERA_ACCOUNT_ID);
|
|
90
|
+
const parsed = parsePrivateKey(process.env.HEDERA_PRIVATE_KEY);
|
|
91
|
+
const token = USDC_TESTNET;
|
|
92
|
+
const account = await mirrorAccount(id);
|
|
93
|
+
if (!account) {
|
|
94
|
+
throw new InputError(`Account ${id} was not found on Hedera testnet. Check the id, and that it is a testnet account.`);
|
|
95
|
+
}
|
|
96
|
+
checkAccountKeyType(account.key?._type);
|
|
97
|
+
const key = PrivateKey.fromStringECDSA(parsed.rawHex);
|
|
98
|
+
const mirrorPub = (account.key?.key ?? "").toLowerCase();
|
|
99
|
+
if (mirrorPub && key.publicKey.toStringRaw().toLowerCase() !== mirrorPub) {
|
|
100
|
+
throw new InputError(`HEDERA_PRIVATE_KEY does not belong to ${id}. Use the private key shown for that account.`);
|
|
101
|
+
}
|
|
102
|
+
if (await isAssociated(id, token)) {
|
|
103
|
+
console.log(`${id}: already associated with USDC ${token}. Nothing to do.`);
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
if ((account.balance?.balance ?? 0) <= 0) {
|
|
107
|
+
throw new InputError(`${id} has no HBAR to pay the association fee (about $0.05). Fund it from portal.hedera.com.`);
|
|
108
|
+
}
|
|
109
|
+
const client = Client.forTestnet().setOperator(AccountId.fromString(id), key);
|
|
110
|
+
try {
|
|
111
|
+
const resp = await new TokenAssociateTransaction().setAccountId(AccountId.fromString(id)).setTokenIds([TokenId.fromString(token)]).freezeWith(client).sign(key).then((tx) => tx.execute(client));
|
|
112
|
+
console.log(`transaction: ${resp.transactionId.toString()}`);
|
|
113
|
+
const rcpt = await resp.getReceipt(client);
|
|
114
|
+
console.log(`status: ${rcpt.status.toString()}`);
|
|
115
|
+
} catch (e) {
|
|
116
|
+
const msg = e.message;
|
|
117
|
+
if (msg.includes("TOKEN_ALREADY_ASSOCIATED_TO_ACCOUNT")) {
|
|
118
|
+
console.log(`${id}: already associated with USDC ${token}. Nothing to do.`);
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
if (msg.includes("INSUFFICIENT_PAYER_BALANCE") || msg.includes("INSUFFICIENT_ACCOUNT_BALANCE")) {
|
|
122
|
+
throw new InputError(`${id} does not have enough HBAR for the association fee. Fund it from portal.hedera.com.`);
|
|
123
|
+
}
|
|
124
|
+
if (msg.includes("INVALID_SIGNATURE")) {
|
|
125
|
+
throw new InputError(`The network rejected the signature: HEDERA_PRIVATE_KEY is not ${id}'s key.`);
|
|
126
|
+
}
|
|
127
|
+
throw e;
|
|
128
|
+
} finally {
|
|
129
|
+
client.close();
|
|
130
|
+
}
|
|
131
|
+
for (let i = 0; i < 10; i++) {
|
|
132
|
+
if (await isAssociated(id, token)) {
|
|
133
|
+
console.log(`mirror node confirms ${id} is associated with USDC ${token}.`);
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
137
|
+
}
|
|
138
|
+
console.log(`receipt succeeded; the mirror node has not caught up yet. Re-run in a minute to confirm.`);
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
var MIRROR;
|
|
142
|
+
var init_associate_account_run = __esm({
|
|
143
|
+
"apps/registry/src/scripts/associate-account-run.ts"() {
|
|
144
|
+
"use strict";
|
|
145
|
+
init_associate_account_lib();
|
|
146
|
+
MIRROR = "https://testnet.mirrornode.hedera.com";
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// apps/mcp/src/consent.ts
|
|
151
|
+
var consent_exports = {};
|
|
152
|
+
__export(consent_exports, {
|
|
153
|
+
ASK_REASON: () => ASK_REASON,
|
|
154
|
+
CONSENT_MODES: () => CONSENT_MODES,
|
|
155
|
+
CONSENT_MODE_ENV: () => CONSENT_MODE_ENV,
|
|
156
|
+
decideConsent: () => decideConsent,
|
|
157
|
+
hookOutput: () => hookOutput
|
|
158
|
+
});
|
|
159
|
+
function decideConsent(input, cfg = {}) {
|
|
160
|
+
if (input.agent_id) {
|
|
161
|
+
return {
|
|
162
|
+
decision: "deny",
|
|
163
|
+
reason: "carpool_publish is not available to subagents. Publishing is irreversible and needs a human in an interactive session; a subagent cannot be one."
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (input.permission_mode && NON_PROMPTING_MODES.has(input.permission_mode)) {
|
|
167
|
+
return {
|
|
168
|
+
decision: "deny",
|
|
169
|
+
reason: `carpool_publish is not available in permission mode "${input.permission_mode}", which does not prompt. Publishing cannot be auto-approved.`
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const raw = cfg.mode?.trim();
|
|
173
|
+
if (raw === "off") {
|
|
174
|
+
return {
|
|
175
|
+
decision: "deny",
|
|
176
|
+
reason: `Publishing is turned off here (${CONSENT_MODE_ENV}=off).`
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
if (raw !== void 0 && raw !== "" && raw !== "ask") {
|
|
180
|
+
return {
|
|
181
|
+
decision: "deny",
|
|
182
|
+
reason: `${CONSENT_MODE_ENV} is set to "${raw}", which is not one of ${CONSENT_MODES.join(" | ")}. Refusing rather than guessing at an irreversible action.`
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return { decision: "ask", reason: ASK_REASON };
|
|
186
|
+
}
|
|
187
|
+
function hookOutput(d) {
|
|
188
|
+
return {
|
|
189
|
+
hookSpecificOutput: {
|
|
190
|
+
hookEventName: "PreToolUse",
|
|
191
|
+
permissionDecision: d.decision,
|
|
192
|
+
permissionDecisionReason: d.reason
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
var CONSENT_MODES, CONSENT_MODE_ENV, NON_PROMPTING_MODES, ASK_REASON;
|
|
197
|
+
var init_consent = __esm({
|
|
198
|
+
"apps/mcp/src/consent.ts"() {
|
|
199
|
+
"use strict";
|
|
200
|
+
CONSENT_MODES = ["ask", "off"];
|
|
201
|
+
CONSENT_MODE_ENV = "CARPOOL_PUBLISH_MODE";
|
|
202
|
+
NON_PROMPTING_MODES = /* @__PURE__ */ new Set(["bypassPermissions", "acceptEdits", "dontAsk", "auto"]);
|
|
203
|
+
ASK_REASON = "Publishing puts this research on sale. It cannot be recalled once bought \u2014 delisting stops new sales but copies already paid for stay with their buyers. Review the question, sources and stripped items before approving.";
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// packages/hedera-x402/src/units.ts
|
|
208
|
+
import { createHash } from "node:crypto";
|
|
209
|
+
function sha256(input) {
|
|
210
|
+
return createHash("sha256").update(input).digest("hex");
|
|
211
|
+
}
|
|
212
|
+
var init_units = __esm({
|
|
213
|
+
"packages/hedera-x402/src/units.ts"() {
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// packages/carpool-core/src/manifest.ts
|
|
218
|
+
import { z } from "zod";
|
|
219
|
+
function normalizeQuestion(q) {
|
|
220
|
+
const collapsed = q.toLowerCase().trim().replace(/\s+/g, " ");
|
|
221
|
+
return collapsed.replace(/[\s.,!?;:'"()]+$/g, "");
|
|
222
|
+
}
|
|
223
|
+
var ManifestSchema;
|
|
224
|
+
var init_manifest = __esm({
|
|
225
|
+
"packages/carpool-core/src/manifest.ts"() {
|
|
226
|
+
"use strict";
|
|
227
|
+
ManifestSchema = z.object({
|
|
228
|
+
/** swarm:<sha256 hex> — see magnet.ts. Content-addressed, not question-addressed. */
|
|
229
|
+
magnet: z.string().regex(/^swarm:[0-9a-f]{64}$/, "magnet must be swarm:<64 hex>"),
|
|
230
|
+
question: z.string().min(1),
|
|
231
|
+
/** Canonicalised question: lowercase, collapsed whitespace, no trailing punctuation. */
|
|
232
|
+
questionNorm: z.string().min(1),
|
|
233
|
+
/** The event this artifact answers a question about, e.g. "ethonline-2026". */
|
|
234
|
+
scope: z.string().min(1).optional(),
|
|
235
|
+
abstract: z.string().min(1),
|
|
236
|
+
sources: z.array(
|
|
237
|
+
z.object({
|
|
238
|
+
url: z.string().url(),
|
|
239
|
+
fetchedAt: z.string().datetime({ offset: true })
|
|
240
|
+
})
|
|
241
|
+
),
|
|
242
|
+
provenance: z.object({
|
|
243
|
+
model: z.string().min(1),
|
|
244
|
+
durationSeconds: z.number().nonnegative(),
|
|
245
|
+
inputTokens: z.number().int().nonnegative(),
|
|
246
|
+
outputTokens: z.number().int().nonnegative(),
|
|
247
|
+
estimatedCostUsd: z.number().nonnegative(),
|
|
248
|
+
toolCalls: z.number().int().nonnegative()
|
|
249
|
+
}),
|
|
250
|
+
decay: z.object({
|
|
251
|
+
/** Zero and negative half-lives are rejected here — freshness would divide by <=0. */
|
|
252
|
+
halfLifeDays: z.number().positive(),
|
|
253
|
+
producedAt: z.string().datetime({ offset: true })
|
|
254
|
+
}),
|
|
255
|
+
/** OPAQUE. Never parse as a Hedera id — see AuthorIdentity in identity.ts. */
|
|
256
|
+
author: z.string().min(1),
|
|
257
|
+
/** sha256 hex of the paid body. */
|
|
258
|
+
bodyHash: z.string().regex(/^[0-9a-f]{64}$/, "bodyHash must be sha256 hex"),
|
|
259
|
+
bodyBytes: z.number().int().nonnegative(),
|
|
260
|
+
redacted: z.boolean()
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// packages/carpool-core/src/magnet.ts
|
|
266
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
267
|
+
function canonicalize(value) {
|
|
268
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
269
|
+
if (value !== null && typeof value === "object") {
|
|
270
|
+
const out = {};
|
|
271
|
+
for (const key of Object.keys(value).sort()) {
|
|
272
|
+
out[key] = canonicalize(value[key]);
|
|
273
|
+
}
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
276
|
+
return value;
|
|
277
|
+
}
|
|
278
|
+
function canonicalHash(value) {
|
|
279
|
+
return createHash2("sha256").update(JSON.stringify(canonicalize(value))).digest("hex");
|
|
280
|
+
}
|
|
281
|
+
function magnetOf(m) {
|
|
282
|
+
const { magnet: _magnet, ...rest } = m;
|
|
283
|
+
return `swarm:${canonicalHash(rest)}`;
|
|
284
|
+
}
|
|
285
|
+
var init_magnet = __esm({
|
|
286
|
+
"packages/carpool-core/src/magnet.ts"() {
|
|
287
|
+
"use strict";
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// packages/carpool-core/src/decay.ts
|
|
292
|
+
var init_decay = __esm({
|
|
293
|
+
"packages/carpool-core/src/decay.ts"() {
|
|
294
|
+
"use strict";
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// packages/carpool-core/src/pricing.ts
|
|
299
|
+
function priceForRedoCost(estimatedCostUsd) {
|
|
300
|
+
return Math.max(
|
|
301
|
+
MIN_PRICE_MICRO_USDC,
|
|
302
|
+
Math.round(estimatedCostUsd * 1e6 * PRICE_SHARE_OF_REDO_COST)
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
function floorForPrice(priceMicroUsdc) {
|
|
306
|
+
return Math.max(MIN_PRICE_MICRO_USDC, Math.round(priceMicroUsdc * PRICE_FLOOR_SHARE));
|
|
307
|
+
}
|
|
308
|
+
function maxBuyableRedoCostUsd(capMicroUsdc2) {
|
|
309
|
+
return capMicroUsdc2 / 1e6 / PRICE_SHARE_OF_REDO_COST;
|
|
310
|
+
}
|
|
311
|
+
function capMicroUsdc(env = process.env) {
|
|
312
|
+
const raw = env.CARPOOL_MAX_MICRO_USDC;
|
|
313
|
+
if (raw == null || raw.trim() === "") return DEFAULT_CAP_MICRO_USDC;
|
|
314
|
+
const n = Number(raw);
|
|
315
|
+
if (!Number.isFinite(n) || n < 0) return DEFAULT_CAP_MICRO_USDC;
|
|
316
|
+
return Math.trunc(n);
|
|
317
|
+
}
|
|
318
|
+
var PRICE_SHARE_OF_REDO_COST, MIN_PRICE_MICRO_USDC, PRICE_FLOOR_SHARE, DEFAULT_MAX_REDO_COST_USD, DEFAULT_CAP_MICRO_USDC;
|
|
319
|
+
var init_pricing = __esm({
|
|
320
|
+
"packages/carpool-core/src/pricing.ts"() {
|
|
321
|
+
"use strict";
|
|
322
|
+
PRICE_SHARE_OF_REDO_COST = 0.1;
|
|
323
|
+
MIN_PRICE_MICRO_USDC = 1e3;
|
|
324
|
+
PRICE_FLOOR_SHARE = 0.1;
|
|
325
|
+
DEFAULT_MAX_REDO_COST_USD = 5;
|
|
326
|
+
DEFAULT_CAP_MICRO_USDC = Math.round(
|
|
327
|
+
DEFAULT_MAX_REDO_COST_USD * 1e6 * PRICE_SHARE_OF_REDO_COST
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// packages/carpool-core/src/health.ts
|
|
333
|
+
var init_health = __esm({
|
|
334
|
+
"packages/carpool-core/src/health.ts"() {
|
|
335
|
+
"use strict";
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// packages/carpool-core/src/ratings.ts
|
|
340
|
+
var VERDICT_SHARE;
|
|
341
|
+
var init_ratings = __esm({
|
|
342
|
+
"packages/carpool-core/src/ratings.ts"() {
|
|
343
|
+
"use strict";
|
|
344
|
+
VERDICT_SHARE = 2 / 3;
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// packages/carpool-core/src/identity.ts
|
|
349
|
+
import { PrivateKey as PrivateKey2, PublicKey } from "@hiero-ledger/sdk";
|
|
350
|
+
function signManifest(privKeyHex, manifestHash) {
|
|
351
|
+
const key = PrivateKey2.fromStringECDSA(privKeyHex);
|
|
352
|
+
return Buffer.from(key.sign(Buffer.from(manifestHash, "hex"))).toString("hex");
|
|
353
|
+
}
|
|
354
|
+
function publicKeyHexOf(privKeyHex) {
|
|
355
|
+
return PrivateKey2.fromStringECDSA(privKeyHex).publicKey.toStringRaw();
|
|
356
|
+
}
|
|
357
|
+
var init_identity = __esm({
|
|
358
|
+
"packages/carpool-core/src/identity.ts"() {
|
|
359
|
+
"use strict";
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// packages/carpool-core/src/ens.ts
|
|
364
|
+
import { PrivateKey as PrivateKey3, PublicKey as PublicKey2 } from "@hiero-ledger/sdk";
|
|
365
|
+
function assertNameShape(name) {
|
|
366
|
+
if (name === "" || /[\s:]/.test(name) || !name.includes(".") || name.split(".").some((l) => l === "")) {
|
|
367
|
+
throw new Error(`not an ENS name: ${JSON.stringify(name)}`);
|
|
368
|
+
}
|
|
369
|
+
if (/[A-Z]/.test(name)) {
|
|
370
|
+
throw new Error(`ENS name must be normalised (ENSIP-15, lower case): ${JSON.stringify(name)}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function ensAuthorString(name, fallbackAccount, publicKeyHex) {
|
|
374
|
+
assertNameShape(name);
|
|
375
|
+
if (!ACCOUNT_RE.test(fallbackAccount)) throw new Error(`fallback account must be shard.realm.num, got ${fallbackAccount}`);
|
|
376
|
+
return `ens:${name}:${fallbackAccount}:${publicKeyHex}`;
|
|
377
|
+
}
|
|
378
|
+
var ACCOUNT_RE;
|
|
379
|
+
var init_ens = __esm({
|
|
380
|
+
"packages/carpool-core/src/ens.ts"() {
|
|
381
|
+
"use strict";
|
|
382
|
+
ACCOUNT_RE = /^(\d+)\.(\d+)\.(\d+)$/;
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// packages/carpool-core/src/index.ts
|
|
387
|
+
var init_src = __esm({
|
|
388
|
+
"packages/carpool-core/src/index.ts"() {
|
|
389
|
+
init_units();
|
|
390
|
+
init_manifest();
|
|
391
|
+
init_magnet();
|
|
392
|
+
init_decay();
|
|
393
|
+
init_pricing();
|
|
394
|
+
init_health();
|
|
395
|
+
init_ratings();
|
|
396
|
+
init_identity();
|
|
397
|
+
init_ens();
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// apps/mcp/src/embed.ts
|
|
402
|
+
async function tryLocalEmbedder(tuple) {
|
|
403
|
+
try {
|
|
404
|
+
const spec = "@carpool/tracker";
|
|
405
|
+
const tracker = await import(
|
|
406
|
+
/* @vite-ignore */
|
|
407
|
+
spec
|
|
408
|
+
);
|
|
409
|
+
const loader = tracker.localEmbedder;
|
|
410
|
+
if (typeof loader !== "function") {
|
|
411
|
+
return { embedder: null, reason: "@carpool/tracker exports no localEmbedder" };
|
|
412
|
+
}
|
|
413
|
+
const e = await loader({
|
|
414
|
+
model: tuple.model
|
|
415
|
+
});
|
|
416
|
+
if (e.dim !== tuple.dim || e.model !== tuple.model) {
|
|
417
|
+
return {
|
|
418
|
+
embedder: null,
|
|
419
|
+
reason: `local embedder is (model="${e.model}", dim=${e.dim}) but this registry declares (model="${tuple.model}", dim=${tuple.dim}) \u2014 vectors from different models are not comparable`
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
return { embedder: { mode: "local", model: e.model, dim: e.dim, embed: (t) => e.embed(t) } };
|
|
423
|
+
} catch (err) {
|
|
424
|
+
return {
|
|
425
|
+
embedder: null,
|
|
426
|
+
reason: `local embedding unavailable (${err.message.split("\n")[0]})`
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
function encodeVector(v) {
|
|
431
|
+
return Buffer.from(v.buffer, v.byteOffset, v.byteLength).toString("base64");
|
|
432
|
+
}
|
|
433
|
+
var REMOTE_EMBED_NOTICE;
|
|
434
|
+
var init_embed = __esm({
|
|
435
|
+
"apps/mcp/src/embed.ts"() {
|
|
436
|
+
"use strict";
|
|
437
|
+
REMOTE_EMBED_NOTICE = "Note: this search sent your question text to the registry, which can read and log it. Install the optional local embedding dependency to send only a vector instead (~240 MB; reduces exposure, does not make queries private \u2014 embedding inversion is real).";
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// apps/mcp/src/client.ts
|
|
442
|
+
function normaliseSummary(raw, nowMs = Date.now()) {
|
|
443
|
+
if (typeof raw.ageDays === "number") return raw;
|
|
444
|
+
const producedMs = Date.parse(raw.decay?.producedAt ?? "");
|
|
445
|
+
const ageDays = Number.isFinite(producedMs) ? Math.max(0, (nowMs - producedMs) / MS_PER_DAY) : 0;
|
|
446
|
+
return { ...raw, ageDays };
|
|
447
|
+
}
|
|
448
|
+
var MS_PER_DAY, RegistryClient;
|
|
449
|
+
var init_client = __esm({
|
|
450
|
+
"apps/mcp/src/client.ts"() {
|
|
451
|
+
"use strict";
|
|
452
|
+
init_src();
|
|
453
|
+
init_embed();
|
|
454
|
+
MS_PER_DAY = 864e5;
|
|
455
|
+
RegistryClient = class {
|
|
456
|
+
constructor(base) {
|
|
457
|
+
this.base = base;
|
|
458
|
+
}
|
|
459
|
+
base;
|
|
460
|
+
url(path) {
|
|
461
|
+
return `${this.base.replace(/\/$/, "")}${path}`;
|
|
462
|
+
}
|
|
463
|
+
async wellKnown() {
|
|
464
|
+
const res = await fetch(this.url("/.well-known/carpool"));
|
|
465
|
+
if (!res.ok) throw new Error(`registry /.well-known/carpool \u2192 ${res.status}`);
|
|
466
|
+
return await res.json();
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Search. Sends a vector when one can be produced locally, otherwise the
|
|
470
|
+
* question text — the caller decides which, and tells the user which happened.
|
|
471
|
+
*
|
|
472
|
+
* Embeds `normalizeQuestion(question)`, not the raw question: the registry
|
|
473
|
+
* indexes each artifact's `questionNorm`, so embedding the raw string would
|
|
474
|
+
* compare a differently-cased, differently-punctuated rendering of the text
|
|
475
|
+
* against the corpus and cost similarity for no reason. Both sides of the
|
|
476
|
+
* comparison now run the same normaliser from @carpool/core.
|
|
477
|
+
*/
|
|
478
|
+
async search(question, limit, embedder2) {
|
|
479
|
+
const params = new URLSearchParams({ limit: String(limit) });
|
|
480
|
+
const norm = normalizeQuestion(question);
|
|
481
|
+
let sentText = true;
|
|
482
|
+
if (embedder2) {
|
|
483
|
+
params.set("vector", encodeVector(await embedder2.embed(norm)));
|
|
484
|
+
sentText = false;
|
|
485
|
+
} else {
|
|
486
|
+
params.set("q", question);
|
|
487
|
+
}
|
|
488
|
+
const res = await fetch(this.url(`/search?${params}`));
|
|
489
|
+
if (!res.ok) throw new Error(`registry /search \u2192 ${res.status}: ${await res.text()}`);
|
|
490
|
+
const body = await res.json();
|
|
491
|
+
const raw = Array.isArray(body) ? body : body.results ?? [];
|
|
492
|
+
return { results: raw.map((r) => normaliseSummary(r)), sentText };
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* One artifact's manifest by magnet. Free, like every other manifest read.
|
|
496
|
+
*
|
|
497
|
+
* The buyer's integrity check needs `bodyHash` fixed *before* payment; this
|
|
498
|
+
* is where it comes from when the caller has a magnet and no search result
|
|
499
|
+
* in hand. Returns null on 404/410 so a caller can distinguish "nothing to
|
|
500
|
+
* buy" from "could not check".
|
|
501
|
+
*/
|
|
502
|
+
async manifest(magnet) {
|
|
503
|
+
const res = await fetch(this.url(`/manifest/${encodeURIComponent(magnet)}`));
|
|
504
|
+
if (res.status === 404 || res.status === 410) return null;
|
|
505
|
+
if (!res.ok) throw new Error(`registry /manifest \u2192 ${res.status}`);
|
|
506
|
+
return normaliseSummary(await res.json());
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// apps/mcp/src/scan.ts
|
|
513
|
+
function sweep(text, where, findings, extraTerms) {
|
|
514
|
+
let out = text;
|
|
515
|
+
for (const [re, label] of SECRET_PATTERNS) {
|
|
516
|
+
out = out.replace(re, () => {
|
|
517
|
+
findings.push({ kind: "secret", what: label, where });
|
|
518
|
+
return REDACTED;
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
out = out.replace(INTERNAL_HOST, (m) => {
|
|
522
|
+
findings.push({ kind: "internal-host", what: m, where });
|
|
523
|
+
return REDACTED;
|
|
524
|
+
});
|
|
525
|
+
out = out.replace(HOME_PATH, () => {
|
|
526
|
+
findings.push({ kind: "private-path", what: "local filesystem path", where });
|
|
527
|
+
return REDACTED;
|
|
528
|
+
});
|
|
529
|
+
for (const term of extraTerms) {
|
|
530
|
+
if (!term.trim()) continue;
|
|
531
|
+
const re = new RegExp(`\\b${term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "gi");
|
|
532
|
+
out = out.replace(re, () => {
|
|
533
|
+
findings.push({ kind: "private-repo", what: term, where });
|
|
534
|
+
return REDACTED;
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
return out;
|
|
538
|
+
}
|
|
539
|
+
function scanForPublish(input) {
|
|
540
|
+
const findings = [];
|
|
541
|
+
const terms = input.neverPublish ?? [];
|
|
542
|
+
const question = sweep(input.question, "question", findings, terms);
|
|
543
|
+
const abstract = sweep(input.abstract, "abstract", findings, terms);
|
|
544
|
+
const body = sweep(input.body, "body", findings, terms);
|
|
545
|
+
const sources = input.sources.filter((s) => {
|
|
546
|
+
INTERNAL_HOST.lastIndex = 0;
|
|
547
|
+
if (INTERNAL_HOST.test(s.url)) {
|
|
548
|
+
findings.push({ kind: "internal-host", what: s.url, where: "sources" });
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
return true;
|
|
552
|
+
});
|
|
553
|
+
return { clean: findings.length === 0, findings, redacted: { question, abstract, body, sources } };
|
|
554
|
+
}
|
|
555
|
+
var SECRET_PATTERNS, INTERNAL_HOST, HOME_PATH, REDACTED;
|
|
556
|
+
var init_scan = __esm({
|
|
557
|
+
"apps/mcp/src/scan.ts"() {
|
|
558
|
+
"use strict";
|
|
559
|
+
SECRET_PATTERNS = [
|
|
560
|
+
[/\b(sk|pk)-[A-Za-z0-9_-]{16,}\b/g, "API key"],
|
|
561
|
+
[/\bgh[pousr]_[A-Za-z0-9]{16,}\b/g, "GitHub token"],
|
|
562
|
+
[/\bAKIA[0-9A-Z]{16}\b/g, "AWS access key id"],
|
|
563
|
+
[/\bxox[abprs]-[A-Za-z0-9-]{10,}\b/g, "Slack token"],
|
|
564
|
+
[/-----BEGIN [A-Z ]*PRIVATE KEY-----/g, "private key block"],
|
|
565
|
+
[/\b[0-9a-f]{64}\b/g, "64-hex string (possible private key)"],
|
|
566
|
+
[/\b(postgres|postgresql|mysql|mongodb(\+srv)?|redis|amqp):\/\/[^\s"']+/g, "connection string"],
|
|
567
|
+
[/\b[A-Za-z0-9._%+-]+:[^\s@"']{6,}@[A-Za-z0-9.-]+\b/g, "inline credentials"]
|
|
568
|
+
];
|
|
569
|
+
INTERNAL_HOST = /\b(?:[a-z0-9-]+\.)*(?:internal|intranet|corp|lan|local|localdomain|test|invalid)\b|\b(?:10|127)\.\d{1,3}\.\d{1,3}\.\d{1,3}\b|\b192\.168\.\d{1,3}\.\d{1,3}\b/gi;
|
|
570
|
+
HOME_PATH = /(?:\/(?:Users|home)\/[A-Za-z0-9._-]+|[A-Z]:\\Users\\[A-Za-z0-9._-]+)[^\s"'`,)]*/g;
|
|
571
|
+
REDACTED = "[redacted]";
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// apps/mcp/src/diff.ts
|
|
576
|
+
function renderPublishDiff(input) {
|
|
577
|
+
const { redacted, findings } = input.scan;
|
|
578
|
+
const lines = [];
|
|
579
|
+
lines.push("PUBLISHING THIS RESEARCH \u2014 review before approving");
|
|
580
|
+
lines.push("");
|
|
581
|
+
lines.push("QUESTION (published verbatim, and usually the leakiest line):");
|
|
582
|
+
lines.push(` ${redacted.question}`);
|
|
583
|
+
lines.push("");
|
|
584
|
+
lines.push("ABSTRACT:");
|
|
585
|
+
for (const l of wrap(redacted.abstract, 76)) lines.push(` ${l}`);
|
|
586
|
+
lines.push("");
|
|
587
|
+
lines.push(`SOURCES (${redacted.sources.length}, published in full):`);
|
|
588
|
+
for (const s of redacted.sources.slice(0, 12)) lines.push(` ${s.url}`);
|
|
589
|
+
if (redacted.sources.length > 12) lines.push(` \u2026 and ${redacted.sources.length - 12} more`);
|
|
590
|
+
lines.push("");
|
|
591
|
+
if (findings.length > 0) {
|
|
592
|
+
lines.push(`REMOVED BY THE PRE-PUBLISH SCAN (${findings.length}):`);
|
|
593
|
+
for (const f of findings) lines.push(` ${f.where}: ${f.what} (${f.kind})`);
|
|
594
|
+
lines.push("");
|
|
595
|
+
lines.push(" These were stripped, not flagged \u2014 they are already gone from what would be published.");
|
|
596
|
+
} else {
|
|
597
|
+
lines.push("PRE-PUBLISH SCAN: nothing removed.");
|
|
598
|
+
}
|
|
599
|
+
lines.push("");
|
|
600
|
+
lines.push(
|
|
601
|
+
`BODY: ${fmtBytes(input.bodyBytes)} \u2014 not shown here. Ask to see it before approving if you want to read it.`
|
|
602
|
+
);
|
|
603
|
+
lines.push("");
|
|
604
|
+
lines.push(
|
|
605
|
+
`TERMS: listed at ${(input.priceMicroUsdc / 1e6).toFixed(4)} USDC, halving every ${input.halfLifeDays} day(s).`
|
|
606
|
+
);
|
|
607
|
+
lines.push(
|
|
608
|
+
"IRREVERSIBLE: delisting stops new sales. It cannot recall a copy someone has already paid for."
|
|
609
|
+
);
|
|
610
|
+
return lines.join("\n");
|
|
611
|
+
}
|
|
612
|
+
function wrap(text, width) {
|
|
613
|
+
const words = text.split(/\s+/);
|
|
614
|
+
const out = [];
|
|
615
|
+
let line = "";
|
|
616
|
+
for (const w of words) {
|
|
617
|
+
if ((line + " " + w).trim().length > width) {
|
|
618
|
+
out.push(line.trim());
|
|
619
|
+
line = w;
|
|
620
|
+
} else line += " " + w;
|
|
621
|
+
}
|
|
622
|
+
if (line.trim()) out.push(line.trim());
|
|
623
|
+
return out;
|
|
624
|
+
}
|
|
625
|
+
function fmtBytes(n) {
|
|
626
|
+
if (n < 1024) return `${n} B`;
|
|
627
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
628
|
+
return `${(n / (1024 * 1024)).toFixed(2)} MB`;
|
|
629
|
+
}
|
|
630
|
+
var init_diff = __esm({
|
|
631
|
+
"apps/mcp/src/diff.ts"() {
|
|
632
|
+
"use strict";
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// apps/mcp/src/render.ts
|
|
637
|
+
function usd(micro) {
|
|
638
|
+
return `$${(micro / 1e6).toFixed(4)}`;
|
|
639
|
+
}
|
|
640
|
+
function renderCandidate(m, i, capMicroUsdc2 = CAP_MICRO_USDC) {
|
|
641
|
+
const p = m.provenance;
|
|
642
|
+
const overCap = m.priceNow > capMicroUsdc2;
|
|
643
|
+
const match = m.similarity != null && m.score != null ? ` match: similarity ${m.similarity.toFixed(2)} \xB7 depth ${(m.depth ?? 0).toFixed(2)} \xB7 score ${m.score.toFixed(3)}` : null;
|
|
644
|
+
return [
|
|
645
|
+
`${i + 1}. ${m.question}`,
|
|
646
|
+
` ${m.magnet}`,
|
|
647
|
+
` price ${usd(m.priceNow)}${overCap ? ` \u2014 OVER your ${usd(capMicroUsdc2)} spend cap; carpool_fetch will refuse it` : ""} \xB7 ${m.ageDays.toFixed(1)}d old \xB7 freshness ${m.freshness.toFixed(2)} \xB7 health ${m.health.toFixed(2)}`,
|
|
648
|
+
...match ? [match] : [],
|
|
649
|
+
` cost to produce: ${p.model}, ${p.durationSeconds}s, ${p.inputTokens + p.outputTokens} tokens, ${p.toolCalls} tool calls, $${p.estimatedCostUsd.toFixed(2)}`,
|
|
650
|
+
` ${m.sources.length} sources: ${m.sources.slice(0, 3).map((s) => s.url).join(", ")}${m.sources.length > 3 ? " \u2026" : ""}`,
|
|
651
|
+
` ${m.abstract.slice(0, 220)}${m.abstract.length > 220 ? "\u2026" : ""}`
|
|
652
|
+
].join("\n");
|
|
653
|
+
}
|
|
654
|
+
function renderIntegrity(result, digest) {
|
|
655
|
+
const short = `sha256 ${digest.slice(0, 16)}\u2026`;
|
|
656
|
+
if (result.verification.state === "verified") {
|
|
657
|
+
return `${short} \u2014 verified: matches the bodyHash in the manifest read before paying`;
|
|
658
|
+
}
|
|
659
|
+
return `${short} \u2014 NOT VERIFIED (${result.verification.reason}). Treat the content as unchecked: it was not compared against a hash fixed before payment.`;
|
|
660
|
+
}
|
|
661
|
+
var CAP_MICRO_USDC;
|
|
662
|
+
var init_render = __esm({
|
|
663
|
+
"apps/mcp/src/render.ts"() {
|
|
664
|
+
"use strict";
|
|
665
|
+
init_src();
|
|
666
|
+
CAP_MICRO_USDC = capMicroUsdc();
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
// apps/mcp/src/pay.ts
|
|
671
|
+
var pay_exports = {};
|
|
672
|
+
__export(pay_exports, {
|
|
673
|
+
payFetch: () => payFetch
|
|
674
|
+
});
|
|
675
|
+
import { createClientHederaSigner, PrivateKey as PrivateKey4 } from "@x402/hedera";
|
|
676
|
+
import { ExactHederaScheme } from "@x402/hedera/exact/client";
|
|
677
|
+
import { wrapFetchWithPayment, x402Client, decodePaymentResponseHeader } from "@x402/fetch";
|
|
678
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
679
|
+
function decodeTx(res) {
|
|
680
|
+
const raw = res.headers.get("PAYMENT-RESPONSE") ?? res.headers.get("payment-response");
|
|
681
|
+
if (!raw) return "";
|
|
682
|
+
try {
|
|
683
|
+
return decodePaymentResponseHeader(raw).transaction ?? "";
|
|
684
|
+
} catch {
|
|
685
|
+
return "";
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
async function payFetch(base, magnet, expectedBodyHash) {
|
|
689
|
+
const accountId = process.env.CARPOOL_BUYER_ACCOUNT_ID;
|
|
690
|
+
const key = process.env.CARPOOL_BUYER_PRIVATE_KEY;
|
|
691
|
+
const network = process.env.HEDERA_NETWORK ?? "hedera:testnet";
|
|
692
|
+
const asset = process.env.USDC_TOKEN_ID ?? "0.0.429274";
|
|
693
|
+
const unpaid = (error) => ({
|
|
694
|
+
ok: false,
|
|
695
|
+
body: "",
|
|
696
|
+
paid: 0,
|
|
697
|
+
txId: "",
|
|
698
|
+
verification: { state: "unverified", reason: "nothing was bought" },
|
|
699
|
+
error
|
|
700
|
+
});
|
|
701
|
+
if (!accountId || !key) {
|
|
702
|
+
return unpaid(
|
|
703
|
+
"no buyer credentials: set CARPOOL_BUYER_ACCOUNT_ID and CARPOOL_BUYER_PRIVATE_KEY. carpool_search works without them; only buying needs a funded account."
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
let expected = expectedBodyHash ?? null;
|
|
707
|
+
let expectedError = null;
|
|
708
|
+
if (!expected) {
|
|
709
|
+
try {
|
|
710
|
+
const manifest = await new RegistryClient(base).manifest(magnet);
|
|
711
|
+
if (!manifest) return unpaid("no such artifact (or it is delisted or expired)");
|
|
712
|
+
expected = manifest.bodyHash;
|
|
713
|
+
} catch (e) {
|
|
714
|
+
expectedError = `could not fetch the manifest to verify against (${e.message})`;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
const url = `${base.replace(/\/$/, "")}/artifact/${encodeURIComponent(magnet)}`;
|
|
718
|
+
const probe = await fetch(url);
|
|
719
|
+
if (probe.status === 404) return unpaid("no such artifact");
|
|
720
|
+
if (probe.status === 410) return unpaid("artifact is delisted or expired");
|
|
721
|
+
if (probe.status !== 402) {
|
|
722
|
+
let detail = "";
|
|
723
|
+
try {
|
|
724
|
+
detail = (await probe.text()).trim().replace(/\s+/g, " ").slice(0, 300);
|
|
725
|
+
} catch {
|
|
726
|
+
}
|
|
727
|
+
return unpaid(
|
|
728
|
+
`the registry answered ${probe.status} instead of quoting a price` + (detail ? `: ${detail}` : "") + ". Nothing was charged, and this is the registry's end rather than your credentials."
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
const quoted = await probe.json();
|
|
732
|
+
const paid = Number(quoted.accepts?.[0]?.amount ?? 0);
|
|
733
|
+
const signer = createClientHederaSigner(accountId, PrivateKey4.fromStringECDSA(key), { network });
|
|
734
|
+
const client = x402Client.fromConfig({
|
|
735
|
+
schemes: [{ network, client: new ExactHederaScheme(signer) }],
|
|
736
|
+
spendControls: {
|
|
737
|
+
allowedAssets: [
|
|
738
|
+
{ network, asset, maxAmountPerPayment: MAX_PER_PAYMENT }
|
|
739
|
+
]
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
const paidFetch = wrapFetchWithPayment(fetch, client);
|
|
743
|
+
let res;
|
|
744
|
+
try {
|
|
745
|
+
res = await paidFetch(url);
|
|
746
|
+
} catch (e) {
|
|
747
|
+
return {
|
|
748
|
+
...unpaid(
|
|
749
|
+
`payment was not attempted (${e.message}). The quote was ${paid} \xB5USDC and this client's per-payment cap is ${MAX_PER_PAYMENT} \xB5USDC \u2014 raise CARPOOL_MAX_MICRO_USDC to buy it. At the default price rule that cap buys research costing up to $${maxBuyableRedoCostUsd(Number(MAX_PER_PAYMENT)).toFixed(2)} to produce.`
|
|
750
|
+
),
|
|
751
|
+
paid
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
if (!res.ok) {
|
|
755
|
+
return { ...unpaid(`paid fetch \u2192 ${res.status}`), paid, txId: decodeTx(res) };
|
|
756
|
+
}
|
|
757
|
+
const body = await res.text();
|
|
758
|
+
const txId = decodeTx(res);
|
|
759
|
+
if (!expected) {
|
|
760
|
+
return {
|
|
761
|
+
ok: true,
|
|
762
|
+
body,
|
|
763
|
+
paid,
|
|
764
|
+
txId,
|
|
765
|
+
verification: {
|
|
766
|
+
state: "unverified",
|
|
767
|
+
reason: expectedError ?? "no manifest bodyHash was available to compare against"
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
const actual = createHash3("sha256").update(body).digest("hex");
|
|
772
|
+
if (actual !== expected) {
|
|
773
|
+
return {
|
|
774
|
+
ok: false,
|
|
775
|
+
body: "",
|
|
776
|
+
paid,
|
|
777
|
+
txId,
|
|
778
|
+
verification: { state: "unverified", reason: "the delivered body did not match the manifest" },
|
|
779
|
+
error: `body hash mismatch: the manifest says ${expected.slice(0, 16)}\u2026 but the registry served ${actual.slice(0, 16)}\u2026. Paid, but the content is not what was sold \u2014 keep the transaction id and request a refund.`
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
return { ok: true, body, paid, txId, verification: { state: "verified", source: "manifest", bodyHash: expected } };
|
|
783
|
+
}
|
|
784
|
+
var MAX_PER_PAYMENT;
|
|
785
|
+
var init_pay = __esm({
|
|
786
|
+
"apps/mcp/src/pay.ts"() {
|
|
787
|
+
"use strict";
|
|
788
|
+
init_client();
|
|
789
|
+
init_src();
|
|
790
|
+
MAX_PER_PAYMENT = String(capMicroUsdc());
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
// apps/mcp/src/publish.ts
|
|
795
|
+
var publish_exports = {};
|
|
796
|
+
__export(publish_exports, {
|
|
797
|
+
publishArtifact: () => publishArtifact
|
|
798
|
+
});
|
|
799
|
+
async function publishArtifact(base, input) {
|
|
800
|
+
const accountId = process.env.CARPOOL_AUTHOR_ACCOUNT_ID;
|
|
801
|
+
const key = process.env.CARPOOL_AUTHOR_PRIVATE_KEY;
|
|
802
|
+
if (!accountId || !key) {
|
|
803
|
+
return {
|
|
804
|
+
ok: false,
|
|
805
|
+
error: "no author credentials: set CARPOOL_AUTHOR_ACCOUNT_ID and CARPOOL_AUTHOR_PRIVATE_KEY. Royalties are paid to this account."
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
let author;
|
|
809
|
+
try {
|
|
810
|
+
const publicKeyHex = publicKeyHexOf(key);
|
|
811
|
+
const ensName = process.env.CARPOOL_AUTHOR_ENS_NAME?.trim();
|
|
812
|
+
if (ensName) {
|
|
813
|
+
try {
|
|
814
|
+
author = ensAuthorString(ensName, accountId, publicKeyHex);
|
|
815
|
+
} catch (e) {
|
|
816
|
+
return {
|
|
817
|
+
ok: false,
|
|
818
|
+
error: `CARPOOL_AUTHOR_ENS_NAME is not usable: ${e.message}. Unset it to publish under the account id alone.`
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
} else {
|
|
822
|
+
author = `${accountId}:${publicKeyHex}`;
|
|
823
|
+
}
|
|
824
|
+
} catch (e) {
|
|
825
|
+
return {
|
|
826
|
+
ok: false,
|
|
827
|
+
error: `CARPOOL_AUTHOR_PRIVATE_KEY is not a usable ECDSA private key (${e.message.split("\n")[0]}). The registry verifies publishes against the public key derived from it, so publishing cannot proceed without one.`
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
const { createHash: createHash5 } = await import("node:crypto");
|
|
831
|
+
const bodyHash = createHash5("sha256").update(input.body).digest("hex");
|
|
832
|
+
const withoutMagnet = {
|
|
833
|
+
question: input.question,
|
|
834
|
+
// @carpool/core's normaliser, the same function the registry recomputes
|
|
835
|
+
// and rejects a mismatch against. This file used to carry a private copy
|
|
836
|
+
// that stripped a different punctuation set — and since questionNorm is
|
|
837
|
+
// inside the content-addressed manifest, the two copies produced two
|
|
838
|
+
// different magnets for one question.
|
|
839
|
+
questionNorm: normalizeQuestion(input.question),
|
|
840
|
+
scope: input.scope,
|
|
841
|
+
abstract: input.abstract,
|
|
842
|
+
sources: input.sources,
|
|
843
|
+
provenance: input.provenance,
|
|
844
|
+
decay: { halfLifeDays: input.halfLifeDays, producedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
845
|
+
author,
|
|
846
|
+
bodyHash,
|
|
847
|
+
bodyBytes: Buffer.byteLength(input.body, "utf8"),
|
|
848
|
+
redacted: input.redacted
|
|
849
|
+
};
|
|
850
|
+
const magnet = magnetOf(withoutMagnet);
|
|
851
|
+
const manifest = { ...withoutMagnet, magnet };
|
|
852
|
+
const authorSig = signManifest(key, magnet.replace(/^swarm:/, ""));
|
|
853
|
+
const res = await fetch(`${base.replace(/\/$/, "")}/publish`, {
|
|
854
|
+
method: "POST",
|
|
855
|
+
headers: { "content-type": "application/json" },
|
|
856
|
+
body: JSON.stringify({
|
|
857
|
+
manifest,
|
|
858
|
+
body: input.body,
|
|
859
|
+
// `authorSig`, not `signature`: the registry's PublishBody schema
|
|
860
|
+
// requires this name and zod rejected the request before any of the
|
|
861
|
+
// cryptography ran.
|
|
862
|
+
authorSig,
|
|
863
|
+
priceBase: input.priceMicroUsdc,
|
|
864
|
+
priceFloor: floorForPrice(input.priceMicroUsdc)
|
|
865
|
+
})
|
|
866
|
+
});
|
|
867
|
+
if (!res.ok) return { ok: false, error: `registry /publish \u2192 ${res.status}: ${await res.text()}` };
|
|
868
|
+
const json = await res.json();
|
|
869
|
+
return { ok: true, magnet, duplicateOf: json.duplicateOf ?? null };
|
|
870
|
+
}
|
|
871
|
+
var init_publish = __esm({
|
|
872
|
+
"apps/mcp/src/publish.ts"() {
|
|
873
|
+
"use strict";
|
|
874
|
+
init_src();
|
|
875
|
+
}
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
// apps/mcp/src/delist.ts
|
|
879
|
+
var delist_exports = {};
|
|
880
|
+
__export(delist_exports, {
|
|
881
|
+
delistArtifact: () => delistArtifact
|
|
882
|
+
});
|
|
883
|
+
async function delistArtifact(base, magnet) {
|
|
884
|
+
const key = process.env.CARPOOL_AUTHOR_PRIVATE_KEY;
|
|
885
|
+
if (!key) {
|
|
886
|
+
return {
|
|
887
|
+
ok: false,
|
|
888
|
+
error: "no author credentials: set CARPOOL_AUTHOR_PRIVATE_KEY. Only the author who published an artifact can withdraw it, and the registry checks that against the key inside the manifest."
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
let authorSig;
|
|
892
|
+
try {
|
|
893
|
+
authorSig = signManifest(key, sha256(`${magnet}:delist`));
|
|
894
|
+
} catch (e) {
|
|
895
|
+
return {
|
|
896
|
+
ok: false,
|
|
897
|
+
error: `CARPOOL_AUTHOR_PRIVATE_KEY is not a usable ECDSA private key (${e.message.split("\n")[0]}).`
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
const res = await fetch(`${base.replace(/\/$/, "")}/delist`, {
|
|
901
|
+
method: "POST",
|
|
902
|
+
headers: { "content-type": "application/json" },
|
|
903
|
+
body: JSON.stringify({ magnet, authorSig })
|
|
904
|
+
});
|
|
905
|
+
if (!res.ok) {
|
|
906
|
+
const detail = await res.text();
|
|
907
|
+
if (res.status === 401) {
|
|
908
|
+
return {
|
|
909
|
+
ok: false,
|
|
910
|
+
error: `the registry does not accept this key as ${magnet}'s author (401). Only the publishing account can withdraw an artifact. ${detail}`
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
if (res.status === 404) return { ok: false, error: `no such artifact: ${magnet}` };
|
|
914
|
+
return { ok: false, error: `registry /delist \u2192 ${res.status}: ${detail}` };
|
|
915
|
+
}
|
|
916
|
+
const json = await res.json();
|
|
917
|
+
return { ok: true, ...json };
|
|
918
|
+
}
|
|
919
|
+
var init_delist = __esm({
|
|
920
|
+
"apps/mcp/src/delist.ts"() {
|
|
921
|
+
"use strict";
|
|
922
|
+
init_src();
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
// apps/mcp/src/server.ts
|
|
927
|
+
var server_exports = {};
|
|
928
|
+
__export(server_exports, {
|
|
929
|
+
carpoolDelist: () => carpoolDelist,
|
|
930
|
+
carpoolFetch: () => carpoolFetch,
|
|
931
|
+
carpoolPublish: () => carpoolPublish,
|
|
932
|
+
carpoolSearch: () => carpoolSearch,
|
|
933
|
+
server: () => server
|
|
934
|
+
});
|
|
935
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
936
|
+
import "@modelcontextprotocol/sdk/server/stdio.js";
|
|
937
|
+
import { z as z2 } from "zod";
|
|
938
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
939
|
+
import { join } from "node:path";
|
|
940
|
+
import { tmpdir } from "node:os";
|
|
941
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
942
|
+
async function ensureEmbedder() {
|
|
943
|
+
if (embedder) return;
|
|
944
|
+
const wk = await registry.wellKnown();
|
|
945
|
+
const attempt = await tryLocalEmbedder(wk.embedding);
|
|
946
|
+
if (attempt.embedder) embedder = attempt.embedder;
|
|
947
|
+
else embedReason = attempt.reason;
|
|
948
|
+
}
|
|
949
|
+
async function carpoolSearch({
|
|
950
|
+
question,
|
|
951
|
+
limit
|
|
952
|
+
}) {
|
|
953
|
+
{
|
|
954
|
+
await ensureEmbedder();
|
|
955
|
+
const { results, sentText } = await registry.search(question, limit ?? 5, embedder);
|
|
956
|
+
if (results.length === 0) {
|
|
957
|
+
return {
|
|
958
|
+
content: [
|
|
959
|
+
{
|
|
960
|
+
type: "text",
|
|
961
|
+
text: `No prior research found for: ${question}
|
|
962
|
+
|
|
963
|
+
Do the research yourself.` + (sentText ? `
|
|
964
|
+
|
|
965
|
+
${REMOTE_EMBED_NOTICE}${embedReason ? ` Reason: ${embedReason}.` : ""}` : "")
|
|
966
|
+
}
|
|
967
|
+
]
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
const body = results.map((m, i) => renderCandidate(m, i)).join("\n\n");
|
|
971
|
+
const affordable = results.filter((r) => r.priceNow <= CAP_MICRO_USDC);
|
|
972
|
+
const cheapest = affordable.length > 0 ? Math.min(...affordable.map((r) => r.priceNow)) : null;
|
|
973
|
+
const dearestToRedo = Math.max(...results.map((r) => r.provenance.estimatedCostUsd));
|
|
974
|
+
return {
|
|
975
|
+
content: [
|
|
976
|
+
{
|
|
977
|
+
type: "text",
|
|
978
|
+
text: `${results.length} candidate(s) for: ${question}
|
|
979
|
+
|
|
980
|
+
${body}
|
|
981
|
+
|
|
982
|
+
` + (cheapest === null ? `All of these are above your ${usd(CAP_MICRO_USDC)} per-payment cap (which buys research costing up to $${maxBuyableRedoCostUsd(CAP_MICRO_USDC).toFixed(2)} to produce); raise CARPOOL_MAX_MICRO_USDC to buy one. ` : `Buying the cheapest costs ${usd(cheapest)}; `) + `the most expensive of these cost $${dearestToRedo.toFixed(2)} to produce. Use carpool_fetch <magnet> to buy one.` + (sentText ? `
|
|
983
|
+
|
|
984
|
+
${REMOTE_EMBED_NOTICE}${embedReason ? ` Reason: ${embedReason}.` : ""}` : "")
|
|
985
|
+
}
|
|
986
|
+
]
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
async function carpoolFetch({ magnet }) {
|
|
991
|
+
{
|
|
992
|
+
const { payFetch: payFetch2 } = await Promise.resolve().then(() => (init_pay(), pay_exports));
|
|
993
|
+
const result = await payFetch2(REGISTRY, magnet);
|
|
994
|
+
if (!result.ok) {
|
|
995
|
+
return {
|
|
996
|
+
content: [{ type: "text", text: `Could not buy ${magnet}: ${result.error}` }],
|
|
997
|
+
isError: true
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
mkdirSync(OUT_DIR, { recursive: true });
|
|
1001
|
+
const name = `${magnet.replace(/^swarm:/, "").slice(0, 16)}.md`;
|
|
1002
|
+
const path = join(OUT_DIR, name);
|
|
1003
|
+
writeFileSync(path, result.body, "utf8");
|
|
1004
|
+
const bytes = Buffer.byteLength(result.body, "utf8");
|
|
1005
|
+
const digest = createHash4("sha256").update(result.body).digest("hex");
|
|
1006
|
+
return {
|
|
1007
|
+
content: [
|
|
1008
|
+
{
|
|
1009
|
+
type: "text",
|
|
1010
|
+
text: `Bought ${magnet} for ${usd(result.paid)}.
|
|
1011
|
+
Written to: ${path}
|
|
1012
|
+
${bytes} bytes \xB7 ${renderIntegrity(result, digest)}
|
|
1013
|
+
Transaction: ${result.txId || "(none reported)"}
|
|
1014
|
+
|
|
1015
|
+
Read the file for the research. You did not have to run it.`
|
|
1016
|
+
}
|
|
1017
|
+
]
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
async function carpoolPublish(args) {
|
|
1022
|
+
{
|
|
1023
|
+
const scan = scanForPublish({
|
|
1024
|
+
question: args.question,
|
|
1025
|
+
abstract: args.abstract,
|
|
1026
|
+
body: args.body,
|
|
1027
|
+
sources: args.sources,
|
|
1028
|
+
neverPublish: args.neverPublish
|
|
1029
|
+
});
|
|
1030
|
+
const priceMicro = priceForRedoCost(args.provenance.estimatedCostUsd);
|
|
1031
|
+
const halfLife = args.halfLifeDays ?? 1;
|
|
1032
|
+
if (!args.confirm) {
|
|
1033
|
+
return {
|
|
1034
|
+
content: [
|
|
1035
|
+
{
|
|
1036
|
+
type: "text",
|
|
1037
|
+
text: renderPublishDiff({
|
|
1038
|
+
scan,
|
|
1039
|
+
bodyBytes: Buffer.byteLength(scan.redacted.body, "utf8"),
|
|
1040
|
+
priceMicroUsdc: priceMicro,
|
|
1041
|
+
halfLifeDays: halfLife
|
|
1042
|
+
}) + "\n\nNothing has been published. Show this to the user and call again with confirm: true only if they approve."
|
|
1043
|
+
}
|
|
1044
|
+
]
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
const { publishArtifact: publishArtifact2 } = await Promise.resolve().then(() => (init_publish(), publish_exports));
|
|
1048
|
+
const out = await publishArtifact2(REGISTRY, {
|
|
1049
|
+
...args,
|
|
1050
|
+
...scan.redacted,
|
|
1051
|
+
halfLifeDays: halfLife,
|
|
1052
|
+
priceMicroUsdc: priceMicro,
|
|
1053
|
+
redacted: !scan.clean
|
|
1054
|
+
});
|
|
1055
|
+
return {
|
|
1056
|
+
content: [
|
|
1057
|
+
{
|
|
1058
|
+
type: "text",
|
|
1059
|
+
text: out.ok ? `Published ${out.magnet} at ${usd(priceMicro)}, halving every ${halfLife} day(s).` + (scan.clean ? "" : ` ${scan.findings.length} item(s) were stripped before publishing.`) + (out.duplicateOf ? `
|
|
1060
|
+
|
|
1061
|
+
Note: ${out.duplicateOf} already answers the same normalised question. Both are live and the tracker ranks between them \u2014 yours is not a replacement.` : "") : `Publish failed: ${out.error}`
|
|
1062
|
+
}
|
|
1063
|
+
],
|
|
1064
|
+
isError: !out.ok
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
async function carpoolDelist({ magnet }) {
|
|
1069
|
+
{
|
|
1070
|
+
const { delistArtifact: delistArtifact2 } = await Promise.resolve().then(() => (init_delist(), delist_exports));
|
|
1071
|
+
const out = await delistArtifact2(REGISTRY, magnet);
|
|
1072
|
+
if (!out.ok) {
|
|
1073
|
+
return { content: [{ type: "text", text: `Could not delist ${magnet}: ${out.error}` }], isError: true };
|
|
1074
|
+
}
|
|
1075
|
+
return {
|
|
1076
|
+
content: [
|
|
1077
|
+
{
|
|
1078
|
+
type: "text",
|
|
1079
|
+
text: (out.alreadyDelisted ? `${magnet} was already withdrawn (at ${new Date(out.delistedAt * 1e3).toISOString()}).` : `Withdrawn ${magnet} from sale.`) + `
|
|
1080
|
+
|
|
1081
|
+
${out.note}`
|
|
1082
|
+
}
|
|
1083
|
+
]
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
var REGISTRY, OUT_DIR, registry, embedder, embedReason, server;
|
|
1088
|
+
var init_server = __esm({
|
|
1089
|
+
async "apps/mcp/src/server.ts"() {
|
|
1090
|
+
"use strict";
|
|
1091
|
+
init_client();
|
|
1092
|
+
init_embed();
|
|
1093
|
+
init_scan();
|
|
1094
|
+
init_diff();
|
|
1095
|
+
init_render();
|
|
1096
|
+
init_src();
|
|
1097
|
+
REGISTRY = process.env.CARPOOL_REGISTRY_URL ?? "http://localhost:8403";
|
|
1098
|
+
OUT_DIR = process.env.CARPOOL_ARTIFACT_DIR ?? join(tmpdir(), "carpool-artifacts");
|
|
1099
|
+
registry = new RegistryClient(REGISTRY);
|
|
1100
|
+
embedder = null;
|
|
1101
|
+
embedReason = "";
|
|
1102
|
+
server = new McpServer({ name: "carpool", version: "0.1.0" });
|
|
1103
|
+
server.registerTool(
|
|
1104
|
+
"carpool_search",
|
|
1105
|
+
{
|
|
1106
|
+
title: "Search prior research before doing your own",
|
|
1107
|
+
description: "Call this BEFORE starting deep research. Returns prior research artifacts whose question matches yours, with what each cost to produce, how stale it is, and what it costs to buy. Free \u2014 no payment is made by this tool. Read the evidence and decide whether buying beats redoing the work.",
|
|
1108
|
+
inputSchema: { question: z2.string().min(3), limit: z2.number().int().min(1).max(20).optional() }
|
|
1109
|
+
},
|
|
1110
|
+
carpoolSearch
|
|
1111
|
+
);
|
|
1112
|
+
server.registerTool(
|
|
1113
|
+
"carpool_fetch",
|
|
1114
|
+
{
|
|
1115
|
+
title: "Buy an artifact and write it to a file",
|
|
1116
|
+
description: "Pays for an artifact over x402 and writes it to a file, returning the path and a summary. It does NOT return the body inline: an MCP tool result is capped at ~25k tokens and a real artifact does not fit. Writing to a file also keeps the context cost under your control, which is the saving this whole system exists to produce \u2014 read only the parts you need.",
|
|
1117
|
+
inputSchema: { magnet: z2.string().startsWith("swarm:") }
|
|
1118
|
+
},
|
|
1119
|
+
carpoolFetch
|
|
1120
|
+
);
|
|
1121
|
+
server.registerTool(
|
|
1122
|
+
"carpool_publish",
|
|
1123
|
+
{
|
|
1124
|
+
title: "List research you already did, for sale",
|
|
1125
|
+
description: "Publishes a completed research artifact so the next person with the same question can buy it instead of redoing the work. IRREVERSIBLE: carpool_delist stops new sales but cannot recall a copy someone has already paid for. Requires explicit approval every time; never available to subagents, and turned off entirely by CARPOOL_PUBLISH_MODE=off.",
|
|
1126
|
+
inputSchema: {
|
|
1127
|
+
question: z2.string().min(3),
|
|
1128
|
+
abstract: z2.string().min(10),
|
|
1129
|
+
body: z2.string().min(1),
|
|
1130
|
+
sources: z2.array(z2.object({ url: z2.string().url(), fetchedAt: z2.string() })),
|
|
1131
|
+
provenance: z2.object({
|
|
1132
|
+
model: z2.string(),
|
|
1133
|
+
durationSeconds: z2.number(),
|
|
1134
|
+
inputTokens: z2.number(),
|
|
1135
|
+
outputTokens: z2.number(),
|
|
1136
|
+
estimatedCostUsd: z2.number(),
|
|
1137
|
+
toolCalls: z2.number()
|
|
1138
|
+
}),
|
|
1139
|
+
scope: z2.string().optional(),
|
|
1140
|
+
halfLifeDays: z2.number().positive().optional(),
|
|
1141
|
+
neverPublish: z2.array(z2.string()).optional(),
|
|
1142
|
+
confirm: z2.boolean().optional()
|
|
1143
|
+
}
|
|
1144
|
+
},
|
|
1145
|
+
carpoolPublish
|
|
1146
|
+
);
|
|
1147
|
+
server.registerTool(
|
|
1148
|
+
"carpool_delist",
|
|
1149
|
+
{
|
|
1150
|
+
title: "Withdraw research you published, from sale",
|
|
1151
|
+
description: "Stops new sales of an artifact you published. What it CANNOT do: recall a copy someone has already paid for, cancel a buyer's refund window, or cancel a royalty you have already earned (you still get paid for sales that happened). Final for that magnet: republishing the same research does not relist it. Requires the author credentials the artifact was published with.",
|
|
1152
|
+
inputSchema: { magnet: z2.string().startsWith("swarm:") }
|
|
1153
|
+
},
|
|
1154
|
+
carpoolDelist
|
|
1155
|
+
);
|
|
1156
|
+
if (false) {
|
|
1157
|
+
await server.connect(new StdioServerTransport());
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
// apps/mcp/scripts/npm-cli.ts
|
|
1163
|
+
import { readFileSync } from "node:fs";
|
|
1164
|
+
var sub = process.argv[2];
|
|
1165
|
+
function emitDeny(reason) {
|
|
1166
|
+
process.stdout.write(
|
|
1167
|
+
JSON.stringify({
|
|
1168
|
+
hookSpecificOutput: {
|
|
1169
|
+
hookEventName: "PreToolUse",
|
|
1170
|
+
permissionDecision: "deny",
|
|
1171
|
+
permissionDecisionReason: reason
|
|
1172
|
+
}
|
|
1173
|
+
})
|
|
1174
|
+
);
|
|
1175
|
+
process.exit(0);
|
|
1176
|
+
}
|
|
1177
|
+
if (sub === "associate") {
|
|
1178
|
+
const { associateAccount: associateAccount2, InputError: InputError2 } = await Promise.resolve().then(() => (init_associate_account_run(), associate_account_run_exports));
|
|
1179
|
+
associateAccount2().then(
|
|
1180
|
+
(code) => process.exit(code),
|
|
1181
|
+
(e) => {
|
|
1182
|
+
console.error(e instanceof InputError2 ? e.message : `failed: ${e.message}`);
|
|
1183
|
+
process.exit(1);
|
|
1184
|
+
}
|
|
1185
|
+
);
|
|
1186
|
+
} else if (sub === "consent-hook") {
|
|
1187
|
+
try {
|
|
1188
|
+
let input;
|
|
1189
|
+
try {
|
|
1190
|
+
input = JSON.parse(readFileSync(0, "utf8"));
|
|
1191
|
+
} catch {
|
|
1192
|
+
emitDeny("carpool publish hook could not read its input; refusing rather than guessing.");
|
|
1193
|
+
}
|
|
1194
|
+
const { decideConsent: decideConsent2, hookOutput: hookOutput2 } = await Promise.resolve().then(() => (init_consent(), consent_exports));
|
|
1195
|
+
const out = hookOutput2(decideConsent2(input, { mode: process.env.CARPOOL_PUBLISH_MODE }));
|
|
1196
|
+
process.stdout.write(JSON.stringify(out));
|
|
1197
|
+
} catch (e) {
|
|
1198
|
+
emitDeny(`carpool publish hook failed (${e.message}); refusing rather than guessing.`);
|
|
1199
|
+
}
|
|
1200
|
+
} else if (sub === void 0 || sub === "serve") {
|
|
1201
|
+
const { StdioServerTransport: StdioServerTransport2 } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
1202
|
+
const { server: server2 } = await init_server().then(() => server_exports);
|
|
1203
|
+
await server2.connect(new StdioServerTransport2());
|
|
1204
|
+
} else if (sub === "--version" || sub === "-v") {
|
|
1205
|
+
console.log("carpool-mcp 0.1.0");
|
|
1206
|
+
} else {
|
|
1207
|
+
console.error(
|
|
1208
|
+
`carpool-mcp: unknown command "${sub}".
|
|
1209
|
+
Usage: carpool-mcp start the MCP server on stdio
|
|
1210
|
+
carpool-mcp associate associate HEDERA_ACCOUNT_ID with test USDC (key from HEDERA_PRIVATE_KEY env)
|
|
1211
|
+
carpool-mcp consent-hook PreToolUse hook for carpool_publish`
|
|
1212
|
+
);
|
|
1213
|
+
process.exit(2);
|
|
1214
|
+
}
|