@elisym/mcp 0.8.10 → 0.8.11
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/index.js +619 -72
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { LIMITS, DEFAULT_KIND_OFFSET, SolanaPaymentStrategy, makeCensor, DEFAULT_REDACT_PATHS, validateAgentName, RELAYS, toDTag, formatAssetAmount, USDC_SOLANA_DEVNET, estimateSolFeeLamports, formatFeeBreakdown, resolveAssetFromPaymentRequest as resolveAssetFromPaymentRequest$1, parseAssetAmount, ElisymIdentity, ElisymClient, NATIVE_SOL, resolveKnownAsset, getProtocolProgramId, getProtocolConfig, assetKey, assetByKey, KNOWN_ASSETS } from '@elisym/sdk';
|
|
3
|
-
import { listAgents, createAgentDir, writeYaml, writeSecrets, loadAgent, globalConfigPath } from '@elisym/sdk/agent-store';
|
|
2
|
+
import { LIMITS, DEFAULT_KIND_OFFSET, SolanaPaymentStrategy, makeCensor, DEFAULT_REDACT_PATHS, validateAgentName, RELAYS, toDTag, formatAssetAmount, USDC_SOLANA_DEVNET, estimateSolFeeLamports, formatFeeBreakdown, resolveAssetFromPaymentRequest as resolveAssetFromPaymentRequest$1, parseAssetAmount, ElisymIdentity, ElisymClient, NATIVE_SOL, resolveKnownAsset, estimateNetworkBaseline, formatNetworkBaseline, getProtocolProgramId, getProtocolConfig, assetKey, assetByKey, KNOWN_ASSETS } from '@elisym/sdk';
|
|
3
|
+
import { listAgents, createAgentDir, writeYaml, writeSecrets, resolveAgent, loadAgent, globalConfigPath, writeFileAtomic as writeFileAtomic$1 } from '@elisym/sdk/agent-store';
|
|
4
4
|
import { loadGlobalConfig, writeGlobalConfig } from '@elisym/sdk/node';
|
|
5
5
|
import { getBase58Encoder, getBase58Decoder, generateKeyPairSigner, address, createSolanaRpcSubscriptions, sendAndConfirmTransactionFactory, getSignatureFromTransaction, pipe, createTransactionMessage, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstructions, signTransactionMessageWithSigners, createKeyPairSignerFromBytes, createSolanaRpc, isAddress } from '@solana/kit';
|
|
6
6
|
import { Command } from 'commander';
|
|
@@ -352,6 +352,13 @@ function payment() {
|
|
|
352
352
|
_paymentStrategy ??= new SolanaPaymentStrategy();
|
|
353
353
|
return _paymentStrategy;
|
|
354
354
|
}
|
|
355
|
+
function decodeNpub(npub) {
|
|
356
|
+
const decoded = nip19.decode(npub);
|
|
357
|
+
if (decoded.type !== "npub") {
|
|
358
|
+
throw new Error(`Expected npub, got ${decoded.type}`);
|
|
359
|
+
}
|
|
360
|
+
return decoded.data;
|
|
361
|
+
}
|
|
355
362
|
|
|
356
363
|
// src/install.ts
|
|
357
364
|
function elisymPackageArgs() {
|
|
@@ -752,13 +759,15 @@ async function buildAgentInstance(name, config) {
|
|
|
752
759
|
);
|
|
753
760
|
}
|
|
754
761
|
}
|
|
762
|
+
const resolved = resolveAgent(name, process.cwd());
|
|
755
763
|
return {
|
|
756
764
|
client,
|
|
757
765
|
identity,
|
|
758
766
|
name,
|
|
759
767
|
network: config.network,
|
|
760
768
|
solanaKeypair,
|
|
761
|
-
security: config.security ?? {}
|
|
769
|
+
security: config.security ?? {},
|
|
770
|
+
agentDir: resolved?.dir
|
|
762
771
|
};
|
|
763
772
|
}
|
|
764
773
|
var agentTools = [
|
|
@@ -1156,6 +1165,107 @@ function sanitizeField(input, maxLen) {
|
|
|
1156
1165
|
}
|
|
1157
1166
|
return text;
|
|
1158
1167
|
}
|
|
1168
|
+
var CUSTOMER_HISTORY_FILENAME = ".customer-history.json";
|
|
1169
|
+
var MAX_HISTORY_ENTRIES = 500;
|
|
1170
|
+
var StatusSchema = z.enum(["completed", "failed", "timeout"]);
|
|
1171
|
+
var FeedbackSchema = z.enum(["positive", "negative"]);
|
|
1172
|
+
var CustomerJobEntrySchema = z.object({
|
|
1173
|
+
jobEventId: z.string().min(1).max(128),
|
|
1174
|
+
capability: z.string().min(1).max(200),
|
|
1175
|
+
providerPubkey: z.string().regex(/^[a-f0-9]{64}$/),
|
|
1176
|
+
providerName: z.string().max(200).optional(),
|
|
1177
|
+
paidAmountSubunits: z.string().max(40).optional(),
|
|
1178
|
+
assetKey: z.string().max(80).optional(),
|
|
1179
|
+
status: StatusSchema,
|
|
1180
|
+
submittedAt: z.number().int().nonnegative(),
|
|
1181
|
+
completedAt: z.number().int().nonnegative(),
|
|
1182
|
+
resultPreview: z.string().max(500).optional(),
|
|
1183
|
+
paymentSig: z.string().max(128).optional(),
|
|
1184
|
+
customerFeedback: FeedbackSchema.optional()
|
|
1185
|
+
}).strict();
|
|
1186
|
+
var CustomerHistorySchema = z.object({
|
|
1187
|
+
version: z.literal(1),
|
|
1188
|
+
jobs: z.array(CustomerJobEntrySchema)
|
|
1189
|
+
}).strict();
|
|
1190
|
+
var EMPTY = { version: 1, jobs: [] };
|
|
1191
|
+
var writeLocks = /* @__PURE__ */ new Map();
|
|
1192
|
+
function withLock(path, fn) {
|
|
1193
|
+
const previous = writeLocks.get(path) ?? Promise.resolve();
|
|
1194
|
+
const next = previous.then(fn, fn);
|
|
1195
|
+
writeLocks.set(
|
|
1196
|
+
path,
|
|
1197
|
+
next.finally(() => {
|
|
1198
|
+
if (writeLocks.get(path) === next) {
|
|
1199
|
+
writeLocks.delete(path);
|
|
1200
|
+
}
|
|
1201
|
+
})
|
|
1202
|
+
);
|
|
1203
|
+
return next;
|
|
1204
|
+
}
|
|
1205
|
+
function pathFor(agentDir) {
|
|
1206
|
+
return join(agentDir, CUSTOMER_HISTORY_FILENAME);
|
|
1207
|
+
}
|
|
1208
|
+
async function readRaw(path) {
|
|
1209
|
+
let raw;
|
|
1210
|
+
try {
|
|
1211
|
+
raw = await readFile(path, "utf-8");
|
|
1212
|
+
} catch {
|
|
1213
|
+
return { ...EMPTY, jobs: [] };
|
|
1214
|
+
}
|
|
1215
|
+
try {
|
|
1216
|
+
const parsed = JSON.parse(raw);
|
|
1217
|
+
const result = CustomerHistorySchema.safeParse(parsed);
|
|
1218
|
+
return result.success ? result.data : { ...EMPTY, jobs: [] };
|
|
1219
|
+
} catch {
|
|
1220
|
+
return { ...EMPTY, jobs: [] };
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
async function writeRaw(path, history) {
|
|
1224
|
+
const body = JSON.stringify(history, null, 2) + "\n";
|
|
1225
|
+
await writeFileAtomic$1(path, body, 384);
|
|
1226
|
+
}
|
|
1227
|
+
async function readCustomerHistory(agentDir) {
|
|
1228
|
+
return readRaw(pathFor(agentDir));
|
|
1229
|
+
}
|
|
1230
|
+
async function appendCustomerJob(agentDir, entry) {
|
|
1231
|
+
const validated = CustomerJobEntrySchema.parse(entry);
|
|
1232
|
+
const path = pathFor(agentDir);
|
|
1233
|
+
return withLock(path, async () => {
|
|
1234
|
+
const history = await readRaw(path);
|
|
1235
|
+
const existingIndex = history.jobs.findIndex((job) => job.jobEventId === validated.jobEventId);
|
|
1236
|
+
if (existingIndex >= 0) {
|
|
1237
|
+
history.jobs[existingIndex] = validated;
|
|
1238
|
+
} else {
|
|
1239
|
+
history.jobs.push(validated);
|
|
1240
|
+
}
|
|
1241
|
+
if (history.jobs.length > MAX_HISTORY_ENTRIES) {
|
|
1242
|
+
history.jobs.sort((left, right) => left.submittedAt - right.submittedAt);
|
|
1243
|
+
history.jobs.splice(0, history.jobs.length - MAX_HISTORY_ENTRIES);
|
|
1244
|
+
}
|
|
1245
|
+
await writeRaw(path, history);
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
async function updateCustomerJob(agentDir, jobEventId, patch) {
|
|
1249
|
+
const path = pathFor(agentDir);
|
|
1250
|
+
return withLock(path, async () => {
|
|
1251
|
+
const history = await readRaw(path);
|
|
1252
|
+
const index = history.jobs.findIndex((job) => job.jobEventId === jobEventId);
|
|
1253
|
+
if (index < 0) {
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
const merged = CustomerJobEntrySchema.parse({ ...history.jobs[index], ...patch });
|
|
1257
|
+
history.jobs[index] = merged;
|
|
1258
|
+
await writeRaw(path, history);
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
async function findCustomerJob(agentDir, jobEventId) {
|
|
1262
|
+
const history = await readCustomerHistory(agentDir);
|
|
1263
|
+
return history.jobs.find((job) => job.jobEventId === jobEventId);
|
|
1264
|
+
}
|
|
1265
|
+
async function findCustomerJobsByProvider(agentDir, providerPubkey) {
|
|
1266
|
+
const history = await readCustomerHistory(agentDir);
|
|
1267
|
+
return history.jobs.filter((job) => job.providerPubkey === providerPubkey).sort((left, right) => right.completedAt - left.completedAt);
|
|
1268
|
+
}
|
|
1159
1269
|
|
|
1160
1270
|
// src/tools/customer.ts
|
|
1161
1271
|
var PRE_PING_TIMEOUT_MS = 5e3;
|
|
@@ -1174,7 +1284,10 @@ var GetJobResultSchema = z.object({
|
|
|
1174
1284
|
});
|
|
1175
1285
|
var ListMyJobsSchema = z.object({
|
|
1176
1286
|
limit: z.number().int().min(1).max(50).default(20),
|
|
1177
|
-
kind_offset: z.number().int().min(0).max(999).default(DEFAULT_KIND_OFFSET)
|
|
1287
|
+
kind_offset: z.number().int().min(0).max(999).default(DEFAULT_KIND_OFFSET),
|
|
1288
|
+
include_nostr: z.boolean().default(false).describe(
|
|
1289
|
+
"When true, also pull jobs from Nostr relays and merge them with the local cache. Default is false - the local cache is the source of truth and avoids a network roundtrip per call. Use true when looking for jobs submitted from outside this MCP (e.g. the web app) or to recover after a local-cache wipe."
|
|
1290
|
+
)
|
|
1178
1291
|
});
|
|
1179
1292
|
var SubmitAndPayJobSchema = z.object({
|
|
1180
1293
|
input: z.string(),
|
|
@@ -1191,13 +1304,6 @@ var BuyCapabilitySchema = z.object({
|
|
|
1191
1304
|
max_price_lamports: z.number().int().optional(),
|
|
1192
1305
|
timeout_secs: z.number().int().min(1).max(600).default(120)
|
|
1193
1306
|
});
|
|
1194
|
-
function decodeNpub(npub) {
|
|
1195
|
-
const decoded = nip19.decode(npub);
|
|
1196
|
-
if (decoded.type !== "npub") {
|
|
1197
|
-
throw new Error(`Expected npub, got ${decoded.type}`);
|
|
1198
|
-
}
|
|
1199
|
-
return decoded.data;
|
|
1200
|
-
}
|
|
1201
1307
|
function providerSolanaAddress(provider, dTag) {
|
|
1202
1308
|
const cards = provider.cards ?? [];
|
|
1203
1309
|
const candidates = dTag ? cards.filter(
|
|
@@ -1213,6 +1319,48 @@ function providerSolanaAddress(provider, dTag) {
|
|
|
1213
1319
|
function wsUrlFor(httpUrl) {
|
|
1214
1320
|
return httpUrl.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://");
|
|
1215
1321
|
}
|
|
1322
|
+
async function recordJobOutcome(agent, entry) {
|
|
1323
|
+
if (!agent.agentDir) {
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
try {
|
|
1327
|
+
await appendCustomerJob(agent.agentDir, entry);
|
|
1328
|
+
} catch (e) {
|
|
1329
|
+
logger.warn(
|
|
1330
|
+
{ event: "customer_history_write_failed", agent: agent.name, error: String(e) },
|
|
1331
|
+
"failed to write .customer-history.json"
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
function clipProviderName(name) {
|
|
1336
|
+
if (name === void 0) {
|
|
1337
|
+
return void 0;
|
|
1338
|
+
}
|
|
1339
|
+
return name.length > 200 ? name.slice(0, 200) : name;
|
|
1340
|
+
}
|
|
1341
|
+
function buildJobCompletionTip(jobId, providerNpub) {
|
|
1342
|
+
return `
|
|
1343
|
+
|
|
1344
|
+
Tip: rate this provider with submit_feedback (job_event_id="${jobId}", rating="positive"|"negative"), or save them with add_contact (npub="${providerNpub}").`;
|
|
1345
|
+
}
|
|
1346
|
+
function classifyJobFailure(message) {
|
|
1347
|
+
return /timed out/i.test(message) ? "timeout" : "failed";
|
|
1348
|
+
}
|
|
1349
|
+
async function gasHintForCardAsset(agent, asset) {
|
|
1350
|
+
if (!agent.solanaKeypair) {
|
|
1351
|
+
return "";
|
|
1352
|
+
}
|
|
1353
|
+
try {
|
|
1354
|
+
const rpc = createSolanaRpc(rpcUrlFor(agent.network));
|
|
1355
|
+
const baseline = await estimateNetworkBaseline(rpc, {
|
|
1356
|
+
includeAtaRent: asset.mint !== void 0
|
|
1357
|
+
});
|
|
1358
|
+
return `
|
|
1359
|
+
${formatNetworkBaseline(baseline)}`;
|
|
1360
|
+
} catch {
|
|
1361
|
+
return "";
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1216
1364
|
var paymentStrategy = new SolanaPaymentStrategy();
|
|
1217
1365
|
async function executePaymentFlow(agent, paymentRequest, jobId, providerPubkey, expectedRecipient) {
|
|
1218
1366
|
let requestData;
|
|
@@ -1322,7 +1470,7 @@ function makePaymentFeedbackHandler(opts) {
|
|
|
1322
1470
|
if (opts.maxPriceLamports === void 0 && signedAmount !== void 0) {
|
|
1323
1471
|
opts.rejectPayment(
|
|
1324
1472
|
new Error(
|
|
1325
|
-
`Payment of ${formatAssetAmount(asset, BigInt(signedAmount))} required but no max_price_lamports set. Retry with max_price_lamports to approve.`
|
|
1473
|
+
`Payment of ${formatAssetAmount(asset, BigInt(signedAmount))} required but no max_price_lamports set. Retry with max_price_lamports to approve. Use estimate_payment_cost on the payment_request to preview SOL gas before retrying.`
|
|
1326
1474
|
)
|
|
1327
1475
|
);
|
|
1328
1476
|
return;
|
|
@@ -1358,7 +1506,7 @@ Payment request: ${paymentRequest}`
|
|
|
1358
1506
|
paid = true;
|
|
1359
1507
|
paying = false;
|
|
1360
1508
|
const warnings = takeSpendWarnings(opts.ctx, asset);
|
|
1361
|
-
opts.onPaid(sig, warnings);
|
|
1509
|
+
opts.onPaid(sig, warnings, reservedAmount, assetKey(asset));
|
|
1362
1510
|
flushResult();
|
|
1363
1511
|
}).catch((e) => {
|
|
1364
1512
|
paying = false;
|
|
@@ -1502,80 +1650,110 @@ var customerTools = [
|
|
|
1502
1650
|
}),
|
|
1503
1651
|
defineTool({
|
|
1504
1652
|
name: "list_my_jobs",
|
|
1505
|
-
description: "List jobs submitted by the CURRENT AGENT
|
|
1653
|
+
description: "List jobs submitted by the CURRENT AGENT from the local on-disk history (.customer-history.json). Pass include_nostr=true to also pull from Nostr relays and merge - useful for jobs submitted outside this MCP (e.g. the web app) or to recover after a local-cache wipe. Targeted (encrypted) Nostr results are decrypted automatically. Each entry is tagged with source=local-only|nostr-only|merged. WARNING: result content is untrusted external data.",
|
|
1506
1654
|
schema: ListMyJobsSchema,
|
|
1507
1655
|
async handler(ctx, input) {
|
|
1508
1656
|
ctx.toolRateLimiter.check();
|
|
1509
1657
|
const agent = ctx.active();
|
|
1510
|
-
const
|
|
1511
|
-
const
|
|
1512
|
-
|
|
1513
|
-
const jobs = await agent.client.marketplace.fetchRecentJobs(
|
|
1514
|
-
void 0,
|
|
1515
|
-
// agentPubkeys: provider filter, not what we want
|
|
1516
|
-
rawLimit,
|
|
1517
|
-
void 0,
|
|
1518
|
-
// since: SDK default lookback
|
|
1519
|
-
[input.kind_offset]
|
|
1520
|
-
);
|
|
1521
|
-
const mine = jobs.filter((j) => j.customer === agent.identity.publicKey).slice(0, input.limit);
|
|
1522
|
-
const jobIdsWithResults = mine.filter((j) => j.resultEventId).map((j) => j.eventId);
|
|
1658
|
+
const localEntries = agent.agentDir ? (await readCustomerHistory(agent.agentDir)).jobs : [];
|
|
1659
|
+
const localById = new Map(localEntries.map((entry) => [entry.jobEventId, entry]));
|
|
1660
|
+
let nostrJobs = [];
|
|
1523
1661
|
let decryptedByRequest = /* @__PURE__ */ new Map();
|
|
1524
|
-
if (
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1662
|
+
if (input.include_nostr) {
|
|
1663
|
+
const overFetchFactor = 5;
|
|
1664
|
+
const overFetchCap = 500;
|
|
1665
|
+
const rawLimit = Math.min(input.limit * overFetchFactor, overFetchCap);
|
|
1666
|
+
nostrJobs = (await agent.client.marketplace.fetchRecentJobs(void 0, rawLimit, void 0, [
|
|
1667
|
+
input.kind_offset
|
|
1668
|
+
])).filter((job) => job.customer === agent.identity.publicKey);
|
|
1669
|
+
const jobIdsWithResults = nostrJobs.filter((job) => job.resultEventId).map((job) => job.eventId);
|
|
1670
|
+
if (jobIdsWithResults.length > 0) {
|
|
1671
|
+
try {
|
|
1672
|
+
const decrypted = await agent.client.marketplace.queryJobResults(
|
|
1673
|
+
agent.identity,
|
|
1674
|
+
jobIdsWithResults,
|
|
1675
|
+
[input.kind_offset]
|
|
1676
|
+
);
|
|
1677
|
+
decryptedByRequest = new Map(
|
|
1678
|
+
[...decrypted.entries()].map(([id, value]) => [
|
|
1679
|
+
id,
|
|
1680
|
+
{ content: value.content, decryptionFailed: value.decryptionFailed }
|
|
1681
|
+
])
|
|
1682
|
+
);
|
|
1683
|
+
} catch (e) {
|
|
1684
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
1685
|
+
logger.error(
|
|
1686
|
+
{ event: "list_my_jobs_query_failed", err: message },
|
|
1687
|
+
"queryJobResults failed"
|
|
1688
|
+
);
|
|
1689
|
+
}
|
|
1543
1690
|
}
|
|
1544
1691
|
}
|
|
1692
|
+
const nostrById = new Map(nostrJobs.map((job) => [job.eventId, job]));
|
|
1545
1693
|
let freetextSuspicious = false;
|
|
1546
|
-
const
|
|
1547
|
-
|
|
1694
|
+
const allIds = /* @__PURE__ */ new Set([...localById.keys(), ...nostrById.keys()]);
|
|
1695
|
+
const merged = [...allIds].map((eventId) => {
|
|
1696
|
+
const local = localById.get(eventId);
|
|
1697
|
+
const nostr = nostrById.get(eventId);
|
|
1698
|
+
let source;
|
|
1699
|
+
if (local && nostr) {
|
|
1700
|
+
source = "merged";
|
|
1701
|
+
} else if (local) {
|
|
1702
|
+
source = "local-only";
|
|
1703
|
+
} else {
|
|
1704
|
+
source = "nostr-only";
|
|
1705
|
+
}
|
|
1548
1706
|
let resultText;
|
|
1549
|
-
if (
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1707
|
+
if (nostr) {
|
|
1708
|
+
const decrypted = decryptedByRequest.get(eventId);
|
|
1709
|
+
if (decrypted) {
|
|
1710
|
+
if (decrypted.decryptionFailed) {
|
|
1711
|
+
resultText = "[decryption failed - targeted result not for this agent]";
|
|
1712
|
+
} else {
|
|
1713
|
+
const cleaned = sanitizeInner(decrypted.content);
|
|
1714
|
+
if (scanForInjections(cleaned, "full")) {
|
|
1715
|
+
freetextSuspicious = true;
|
|
1716
|
+
}
|
|
1717
|
+
resultText = cleaned;
|
|
1718
|
+
}
|
|
1719
|
+
} else if (nostr.result) {
|
|
1720
|
+
const cleaned = sanitizeInner(nostr.result);
|
|
1554
1721
|
if (scanForInjections(cleaned, "full")) {
|
|
1555
1722
|
freetextSuspicious = true;
|
|
1556
1723
|
}
|
|
1557
1724
|
resultText = cleaned;
|
|
1558
1725
|
}
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1726
|
+
}
|
|
1727
|
+
if (!resultText && local?.resultPreview) {
|
|
1728
|
+
const cleaned = sanitizeInner(local.resultPreview);
|
|
1561
1729
|
if (scanForInjections(cleaned, "full")) {
|
|
1562
1730
|
freetextSuspicious = true;
|
|
1563
1731
|
}
|
|
1564
1732
|
resultText = cleaned;
|
|
1565
1733
|
}
|
|
1734
|
+
const status = nostr?.status ?? local?.status;
|
|
1735
|
+
const capability = nostr?.capability ?? local?.capability;
|
|
1736
|
+
const amount = nostr?.amount ?? local?.paidAmountSubunits;
|
|
1737
|
+
const timestamp = nostr?.createdAt ?? (local ? Math.floor(local.submittedAt / 1e3) : void 0);
|
|
1566
1738
|
return {
|
|
1567
|
-
event_id:
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1739
|
+
event_id: eventId,
|
|
1740
|
+
source,
|
|
1741
|
+
status: status !== void 0 ? sanitizeField(String(status), 100) : void 0,
|
|
1742
|
+
capability: capability !== void 0 ? sanitizeField(String(capability), 100) : void 0,
|
|
1743
|
+
amount: amount !== void 0 ? String(amount) : void 0,
|
|
1744
|
+
asset_key: local?.assetKey,
|
|
1745
|
+
timestamp,
|
|
1746
|
+
result: resultText,
|
|
1747
|
+
payment_sig: local?.paymentSig,
|
|
1748
|
+
customer_feedback: local?.customerFeedback
|
|
1573
1749
|
};
|
|
1574
1750
|
});
|
|
1575
|
-
|
|
1751
|
+
merged.sort((left, right) => (right.timestamp ?? 0) - (left.timestamp ?? 0));
|
|
1752
|
+
const limited = merged.slice(0, input.limit);
|
|
1753
|
+
const { text: wrapped } = sanitizeUntrusted(JSON.stringify(limited, null, 2), "structured", {
|
|
1576
1754
|
extraInjectionSignal: freetextSuspicious
|
|
1577
1755
|
});
|
|
1578
|
-
return textResult(`Found ${
|
|
1756
|
+
return textResult(`Found ${limited.length} of your jobs:
|
|
1579
1757
|
${wrapped}`);
|
|
1580
1758
|
}
|
|
1581
1759
|
}),
|
|
@@ -1609,6 +1787,7 @@ ${wrapped}`);
|
|
|
1609
1787
|
`Provider "${input.provider_npub}" has no Solana payment address for capability "${input.capability}". Cannot verify payment recipient - refusing to proceed. Ask the provider to publish a capability card with a payment address.`
|
|
1610
1788
|
);
|
|
1611
1789
|
}
|
|
1790
|
+
const submittedAt = Date.now();
|
|
1612
1791
|
const jobId = await agent.client.marketplace.submitJobRequest(agent.identity, {
|
|
1613
1792
|
input: input.input,
|
|
1614
1793
|
capability: input.capability,
|
|
@@ -1616,6 +1795,8 @@ ${wrapped}`);
|
|
|
1616
1795
|
kindOffset: input.kind_offset
|
|
1617
1796
|
});
|
|
1618
1797
|
let paymentSig;
|
|
1798
|
+
let paidAmountSubunits;
|
|
1799
|
+
let paidAssetKey;
|
|
1619
1800
|
let paymentWarnings = [];
|
|
1620
1801
|
try {
|
|
1621
1802
|
const result = await awaitJobResult(
|
|
@@ -1632,8 +1813,10 @@ ${wrapped}`);
|
|
|
1632
1813
|
resolveNoWallet: resolve,
|
|
1633
1814
|
resolveResult: resolve,
|
|
1634
1815
|
rejectPayment: reject,
|
|
1635
|
-
onPaid: (sig, warnings) => {
|
|
1816
|
+
onPaid: (sig, warnings, amount, assetKey5) => {
|
|
1636
1817
|
paymentSig = sig;
|
|
1818
|
+
paidAmountSubunits = amount;
|
|
1819
|
+
paidAssetKey = assetKey5;
|
|
1637
1820
|
paymentWarnings = warnings;
|
|
1638
1821
|
for (const line of warnings) {
|
|
1639
1822
|
logger.warn({ event: "session_spend_threshold", agent: agent.name }, line);
|
|
@@ -1663,12 +1846,38 @@ ${sanitized.text}`);
|
|
|
1663
1846
|
},
|
|
1664
1847
|
timeout + 5e3
|
|
1665
1848
|
);
|
|
1849
|
+
await recordJobOutcome(agent, {
|
|
1850
|
+
jobEventId: jobId,
|
|
1851
|
+
capability: input.capability,
|
|
1852
|
+
providerPubkey,
|
|
1853
|
+
providerName: clipProviderName(provider.name),
|
|
1854
|
+
paidAmountSubunits: paidAmountSubunits?.toString(),
|
|
1855
|
+
assetKey: paidAssetKey,
|
|
1856
|
+
status: "completed",
|
|
1857
|
+
submittedAt,
|
|
1858
|
+
completedAt: Date.now(),
|
|
1859
|
+
resultPreview: result.slice(0, 500),
|
|
1860
|
+
paymentSig
|
|
1861
|
+
});
|
|
1666
1862
|
const warningBlock = paymentWarnings.length > 0 ? `${paymentWarnings.join("\n")}
|
|
1667
1863
|
` : "";
|
|
1864
|
+
const tip = buildJobCompletionTip(jobId, input.provider_npub);
|
|
1668
1865
|
return textResult(`${warningBlock}event_id=${jobId}
|
|
1669
|
-
${result}`);
|
|
1866
|
+
${result}${tip}`);
|
|
1670
1867
|
} catch (e) {
|
|
1671
1868
|
const msg = e instanceof Error ? e.message : String(e);
|
|
1869
|
+
await recordJobOutcome(agent, {
|
|
1870
|
+
jobEventId: jobId,
|
|
1871
|
+
capability: input.capability,
|
|
1872
|
+
providerPubkey,
|
|
1873
|
+
providerName: clipProviderName(provider.name),
|
|
1874
|
+
paidAmountSubunits: paidAmountSubunits?.toString(),
|
|
1875
|
+
assetKey: paidAssetKey,
|
|
1876
|
+
status: classifyJobFailure(msg),
|
|
1877
|
+
submittedAt,
|
|
1878
|
+
completedAt: Date.now(),
|
|
1879
|
+
paymentSig
|
|
1880
|
+
});
|
|
1672
1881
|
const paid = paymentSig ? ` Payment already sent (sig=${paymentSig}) - use get_job_result with event_id="${jobId}" to retrieve once ready.` : "";
|
|
1673
1882
|
const warningBlock = paymentWarnings.length > 0 ? `${paymentWarnings.join("\n")}
|
|
1674
1883
|
` : "";
|
|
@@ -1718,11 +1927,12 @@ ${result}`);
|
|
|
1718
1927
|
);
|
|
1719
1928
|
}
|
|
1720
1929
|
if (price > 0 && input.max_price_lamports === void 0) {
|
|
1930
|
+
const gasLine = await gasHintForCardAsset(agent, cardAsset);
|
|
1721
1931
|
return {
|
|
1722
1932
|
content: [
|
|
1723
1933
|
{
|
|
1724
1934
|
type: "text",
|
|
1725
|
-
text: `Capability "${input.capability}" from "${provider.name || input.provider_npub}" costs ${formatAssetAmount(cardAsset, BigInt(price))}
|
|
1935
|
+
text: `Capability "${input.capability}" from "${provider.name || input.provider_npub}" costs ${formatAssetAmount(cardAsset, BigInt(price))}.${gasLine}
|
|
1726
1936
|
|
|
1727
1937
|
To confirm, call buy_capability again with max_price_lamports set (e.g. ${price} or higher).`
|
|
1728
1938
|
}
|
|
@@ -1735,12 +1945,15 @@ To confirm, call buy_capability again with max_price_lamports set (e.g. ${price}
|
|
|
1735
1945
|
`Provider "${input.provider_npub}" has no Solana payment address for capability "${input.capability}". Cannot verify payment recipient.`
|
|
1736
1946
|
);
|
|
1737
1947
|
}
|
|
1948
|
+
const submittedAt = Date.now();
|
|
1738
1949
|
const jobId = await agent.client.marketplace.submitJobRequest(agent.identity, {
|
|
1739
1950
|
input: input.input || "",
|
|
1740
1951
|
capability: dTag,
|
|
1741
1952
|
providerPubkey
|
|
1742
1953
|
});
|
|
1743
1954
|
let paymentSig;
|
|
1955
|
+
let paidAmountSubunits;
|
|
1956
|
+
let paidAssetKey;
|
|
1744
1957
|
let paymentWarnings = [];
|
|
1745
1958
|
try {
|
|
1746
1959
|
const result = await awaitJobResult(
|
|
@@ -1757,8 +1970,10 @@ To confirm, call buy_capability again with max_price_lamports set (e.g. ${price}
|
|
|
1757
1970
|
resolveNoWallet: resolve,
|
|
1758
1971
|
resolveResult: resolve,
|
|
1759
1972
|
rejectPayment: reject,
|
|
1760
|
-
onPaid: (sig, warnings) => {
|
|
1973
|
+
onPaid: (sig, warnings, amount, assetKey5) => {
|
|
1761
1974
|
paymentSig = sig;
|
|
1975
|
+
paidAmountSubunits = amount;
|
|
1976
|
+
paidAssetKey = assetKey5;
|
|
1762
1977
|
paymentWarnings = warnings;
|
|
1763
1978
|
for (const line of warnings) {
|
|
1764
1979
|
logger.warn({ event: "session_spend_threshold", agent: agent.name }, line);
|
|
@@ -1790,12 +2005,38 @@ ${sanitized.text}`
|
|
|
1790
2005
|
},
|
|
1791
2006
|
timeout + 5e3
|
|
1792
2007
|
);
|
|
2008
|
+
await recordJobOutcome(agent, {
|
|
2009
|
+
jobEventId: jobId,
|
|
2010
|
+
capability: dTag,
|
|
2011
|
+
providerPubkey,
|
|
2012
|
+
providerName: clipProviderName(provider.name),
|
|
2013
|
+
paidAmountSubunits: paidAmountSubunits?.toString(),
|
|
2014
|
+
assetKey: paidAssetKey,
|
|
2015
|
+
status: "completed",
|
|
2016
|
+
submittedAt,
|
|
2017
|
+
completedAt: Date.now(),
|
|
2018
|
+
resultPreview: result.slice(0, 500),
|
|
2019
|
+
paymentSig
|
|
2020
|
+
});
|
|
1793
2021
|
const warningBlock = paymentWarnings.length > 0 ? `${paymentWarnings.join("\n")}
|
|
1794
2022
|
` : "";
|
|
2023
|
+
const tip = buildJobCompletionTip(jobId, input.provider_npub);
|
|
1795
2024
|
return textResult(`${warningBlock}event_id=${jobId}
|
|
1796
|
-
${result}`);
|
|
2025
|
+
${result}${tip}`);
|
|
1797
2026
|
} catch (e) {
|
|
1798
2027
|
const msg = e instanceof Error ? e.message : String(e);
|
|
2028
|
+
await recordJobOutcome(agent, {
|
|
2029
|
+
jobEventId: jobId,
|
|
2030
|
+
capability: dTag,
|
|
2031
|
+
providerPubkey,
|
|
2032
|
+
providerName: clipProviderName(provider.name),
|
|
2033
|
+
paidAmountSubunits: paidAmountSubunits?.toString(),
|
|
2034
|
+
assetKey: paidAssetKey,
|
|
2035
|
+
status: classifyJobFailure(msg),
|
|
2036
|
+
submittedAt,
|
|
2037
|
+
completedAt: Date.now(),
|
|
2038
|
+
paymentSig
|
|
2039
|
+
});
|
|
1799
2040
|
const paid = paymentSig ? ` Payment already sent (sig=${paymentSig}) - use get_job_result with event_id="${jobId}" to retrieve once ready.` : "";
|
|
1800
2041
|
const warningBlock = paymentWarnings.length > 0 ? `${paymentWarnings.join("\n")}
|
|
1801
2042
|
` : "";
|
|
@@ -1847,6 +2088,111 @@ ${text}`);
|
|
|
1847
2088
|
}
|
|
1848
2089
|
})
|
|
1849
2090
|
];
|
|
2091
|
+
var CONTACTS_FILENAME = ".contacts.json";
|
|
2092
|
+
var ContactSchema = z.object({
|
|
2093
|
+
pubkey: z.string().regex(/^[a-f0-9]{64}$/),
|
|
2094
|
+
npub: z.string().min(1).max(80),
|
|
2095
|
+
name: z.string().max(200).optional(),
|
|
2096
|
+
addedAt: z.number().int().nonnegative(),
|
|
2097
|
+
lastJobAt: z.number().int().nonnegative().optional(),
|
|
2098
|
+
jobCount: z.number().int().nonnegative(),
|
|
2099
|
+
lastCapability: z.string().max(200).optional(),
|
|
2100
|
+
note: z.string().max(500).optional()
|
|
2101
|
+
}).strict();
|
|
2102
|
+
var ContactsSchema = z.object({
|
|
2103
|
+
version: z.literal(1),
|
|
2104
|
+
contacts: z.array(ContactSchema)
|
|
2105
|
+
}).strict();
|
|
2106
|
+
var EMPTY2 = { version: 1, contacts: [] };
|
|
2107
|
+
var writeLocks2 = /* @__PURE__ */ new Map();
|
|
2108
|
+
function withLock2(path, fn) {
|
|
2109
|
+
const previous = writeLocks2.get(path) ?? Promise.resolve();
|
|
2110
|
+
const next = previous.then(fn, fn);
|
|
2111
|
+
writeLocks2.set(
|
|
2112
|
+
path,
|
|
2113
|
+
next.finally(() => {
|
|
2114
|
+
if (writeLocks2.get(path) === next) {
|
|
2115
|
+
writeLocks2.delete(path);
|
|
2116
|
+
}
|
|
2117
|
+
})
|
|
2118
|
+
);
|
|
2119
|
+
return next;
|
|
2120
|
+
}
|
|
2121
|
+
function pathFor2(agentDir) {
|
|
2122
|
+
return join(agentDir, CONTACTS_FILENAME);
|
|
2123
|
+
}
|
|
2124
|
+
async function readRaw2(path) {
|
|
2125
|
+
let raw;
|
|
2126
|
+
try {
|
|
2127
|
+
raw = await readFile(path, "utf-8");
|
|
2128
|
+
} catch {
|
|
2129
|
+
return { ...EMPTY2, contacts: [] };
|
|
2130
|
+
}
|
|
2131
|
+
try {
|
|
2132
|
+
const parsed = JSON.parse(raw);
|
|
2133
|
+
const result = ContactsSchema.safeParse(parsed);
|
|
2134
|
+
return result.success ? result.data : { ...EMPTY2, contacts: [] };
|
|
2135
|
+
} catch {
|
|
2136
|
+
return { ...EMPTY2, contacts: [] };
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
async function writeRaw2(path, contacts) {
|
|
2140
|
+
const body = JSON.stringify(contacts, null, 2) + "\n";
|
|
2141
|
+
await writeFileAtomic$1(path, body, 384);
|
|
2142
|
+
}
|
|
2143
|
+
async function readContacts(agentDir) {
|
|
2144
|
+
return readRaw2(pathFor2(agentDir));
|
|
2145
|
+
}
|
|
2146
|
+
async function upsertContact(agentDir, input) {
|
|
2147
|
+
const path = pathFor2(agentDir);
|
|
2148
|
+
return withLock2(path, async () => {
|
|
2149
|
+
const data = await readRaw2(path);
|
|
2150
|
+
const index = data.contacts.findIndex((existing) => existing.pubkey === input.pubkey);
|
|
2151
|
+
let merged;
|
|
2152
|
+
if (index >= 0) {
|
|
2153
|
+
const existing = data.contacts[index];
|
|
2154
|
+
merged = ContactSchema.parse({
|
|
2155
|
+
...existing,
|
|
2156
|
+
npub: input.npub,
|
|
2157
|
+
name: input.name ?? existing.name,
|
|
2158
|
+
note: input.note ?? existing.note,
|
|
2159
|
+
lastJobAt: input.lastJobAt ?? existing.lastJobAt,
|
|
2160
|
+
lastCapability: input.lastCapability ?? existing.lastCapability,
|
|
2161
|
+
jobCount: input.jobCount ?? existing.jobCount + 1
|
|
2162
|
+
});
|
|
2163
|
+
data.contacts[index] = merged;
|
|
2164
|
+
} else {
|
|
2165
|
+
merged = ContactSchema.parse({
|
|
2166
|
+
pubkey: input.pubkey,
|
|
2167
|
+
npub: input.npub,
|
|
2168
|
+
name: input.name,
|
|
2169
|
+
note: input.note,
|
|
2170
|
+
addedAt: Date.now(),
|
|
2171
|
+
lastJobAt: input.lastJobAt,
|
|
2172
|
+
lastCapability: input.lastCapability,
|
|
2173
|
+
jobCount: input.jobCount ?? 0
|
|
2174
|
+
});
|
|
2175
|
+
data.contacts.push(merged);
|
|
2176
|
+
}
|
|
2177
|
+
await writeRaw2(path, data);
|
|
2178
|
+
return merged;
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
2181
|
+
async function removeContact(agentDir, pubkey) {
|
|
2182
|
+
const path = pathFor2(agentDir);
|
|
2183
|
+
return withLock2(path, async () => {
|
|
2184
|
+
const data = await readRaw2(path);
|
|
2185
|
+
const before = data.contacts.length;
|
|
2186
|
+
data.contacts = data.contacts.filter((existing) => existing.pubkey !== pubkey);
|
|
2187
|
+
if (data.contacts.length === before) {
|
|
2188
|
+
return false;
|
|
2189
|
+
}
|
|
2190
|
+
await writeRaw2(path, data);
|
|
2191
|
+
return true;
|
|
2192
|
+
});
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
// src/tools/discovery.ts
|
|
1850
2196
|
var SEARCH_PING_TIMEOUT_MS = 3e3;
|
|
1851
2197
|
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
1852
2198
|
"a",
|
|
@@ -1983,6 +2329,9 @@ var SearchAgentsSchema = z.object({
|
|
|
1983
2329
|
max_price_lamports: z.number().int().optional(),
|
|
1984
2330
|
include_offline: z.boolean().default(false).describe(
|
|
1985
2331
|
"If true, skip the live online check and return agents regardless of reachability. Default: false - only currently-online agents are returned."
|
|
2332
|
+
),
|
|
2333
|
+
contacts_only: z.boolean().default(false).describe(
|
|
2334
|
+
"If true, restrict results to providers saved in the active agent's .contacts.json. Each returned item gains a `last_worked_at` field."
|
|
1986
2335
|
)
|
|
1987
2336
|
});
|
|
1988
2337
|
var ListCapabilitiesSchema = z.object({});
|
|
@@ -1993,13 +2342,31 @@ var discoveryTools = [
|
|
|
1993
2342
|
description: "Search AI agents currently online on elisym. `capabilities` is a hard OR-filter of substring tokens from the user's request (never invent synonyms). `query` is optional re-ranking; omit if not needed. Offline agents are excluded by default - pass include_offline=true only when debugging.",
|
|
1994
2343
|
schema: SearchAgentsSchema,
|
|
1995
2344
|
async handler(ctx, input) {
|
|
1996
|
-
const { capabilities, query, max_price_lamports, include_offline } = input;
|
|
2345
|
+
const { capabilities, query, max_price_lamports, include_offline, contacts_only } = input;
|
|
1997
2346
|
if (capabilities.length > MAX_CAPABILITIES) {
|
|
1998
2347
|
return errorResult(`Too many capabilities (max ${MAX_CAPABILITIES})`);
|
|
1999
2348
|
}
|
|
2000
2349
|
const agent = ctx.active();
|
|
2350
|
+
let lastWorkedAtByPubkey = /* @__PURE__ */ new Map();
|
|
2351
|
+
if (contacts_only) {
|
|
2352
|
+
if (!agent.agentDir) {
|
|
2353
|
+
return textResult(
|
|
2354
|
+
"contacts_only=true requires a persistent agent (no on-disk directory for the active agent)."
|
|
2355
|
+
);
|
|
2356
|
+
}
|
|
2357
|
+
const data = await readContacts(agent.agentDir);
|
|
2358
|
+
if (data.contacts.length === 0) {
|
|
2359
|
+
return textResult(
|
|
2360
|
+
"No contacts saved yet. Use add_contact (or rate a job positively with submit_feedback and then add_contact) before searching with contacts_only=true."
|
|
2361
|
+
);
|
|
2362
|
+
}
|
|
2363
|
+
lastWorkedAtByPubkey = new Map(
|
|
2364
|
+
data.contacts.map((contact) => [contact.pubkey, contact.lastJobAt ?? contact.addedAt])
|
|
2365
|
+
);
|
|
2366
|
+
}
|
|
2001
2367
|
const agents = await agent.client.discovery.fetchAgents(agent.network);
|
|
2002
|
-
let filtered = agents.filter(
|
|
2368
|
+
let filtered = contacts_only ? agents.filter((a) => lastWorkedAtByPubkey.has(a.pubkey)) : agents;
|
|
2369
|
+
filtered = filtered.filter(
|
|
2003
2370
|
(a) => a.cards.some(
|
|
2004
2371
|
(card) => capabilities.some(
|
|
2005
2372
|
(cap) => card.capabilities?.some((c) => c.toLowerCase().includes(cap.toLowerCase())) || card.name?.toLowerCase().includes(cap.toLowerCase()) || card.description?.toLowerCase().includes(cap.toLowerCase())
|
|
@@ -2070,7 +2437,8 @@ var discoveryTools = [
|
|
|
2070
2437
|
network: card.payment?.network
|
|
2071
2438
|
};
|
|
2072
2439
|
}),
|
|
2073
|
-
supported_kinds: a.supportedKinds
|
|
2440
|
+
supported_kinds: a.supportedKinds,
|
|
2441
|
+
last_worked_at: contacts_only ? lastWorkedAtByPubkey.get(a.pubkey) : void 0
|
|
2074
2442
|
}));
|
|
2075
2443
|
const { text } = sanitizeUntrusted(JSON.stringify(results, null, 2), "structured");
|
|
2076
2444
|
return textResult(text);
|
|
@@ -2119,6 +2487,184 @@ ${text}`);
|
|
|
2119
2487
|
}
|
|
2120
2488
|
})
|
|
2121
2489
|
];
|
|
2490
|
+
var SubmitFeedbackSchema = z.object({
|
|
2491
|
+
job_event_id: z.string().min(1).max(128).describe("Event ID returned by submit_and_pay_job, buy_capability, or create_job."),
|
|
2492
|
+
rating: z.enum(["positive", "negative"]),
|
|
2493
|
+
provider_npub: z.string().optional().describe(
|
|
2494
|
+
"Provider npub. Optional when the job is in local history (.customer-history.json); required when feedback is submitted for a job submitted from outside this MCP."
|
|
2495
|
+
)
|
|
2496
|
+
});
|
|
2497
|
+
var AddContactSchema = z.object({
|
|
2498
|
+
npub: z.string().min(1).max(MAX_NPUB_LEN),
|
|
2499
|
+
name: z.string().max(200).optional(),
|
|
2500
|
+
note: z.string().max(500).optional()
|
|
2501
|
+
});
|
|
2502
|
+
var RemoveContactSchema = z.object({
|
|
2503
|
+
npub: z.string().min(1).max(MAX_NPUB_LEN)
|
|
2504
|
+
});
|
|
2505
|
+
var ListContactsSchema = z.object({
|
|
2506
|
+
limit: z.number().int().min(1).max(200).default(50)
|
|
2507
|
+
});
|
|
2508
|
+
function npubFromHex(pubkey) {
|
|
2509
|
+
return nip19.npubEncode(pubkey);
|
|
2510
|
+
}
|
|
2511
|
+
var feedbackContactsTools = [
|
|
2512
|
+
defineTool({
|
|
2513
|
+
name: "submit_feedback",
|
|
2514
|
+
description: 'Rate a completed job (mirrors the web app \u{1F44D}/\u{1F44E} buttons). Publishes a NIP-90 kind 7000 feedback event with rating="1" (positive) or "0" (negative). Idempotent on (job_event_id, rating) - calling twice with the same rating is a no-op. After a positive rating, the response suggests calling add_contact to save the provider for future search_agents queries.',
|
|
2515
|
+
schema: SubmitFeedbackSchema,
|
|
2516
|
+
async handler(ctx, input) {
|
|
2517
|
+
ctx.toolRateLimiter.check();
|
|
2518
|
+
checkLen("job_event_id", input.job_event_id, MAX_EVENT_ID_LEN);
|
|
2519
|
+
if (input.provider_npub) {
|
|
2520
|
+
checkLen("provider_npub", input.provider_npub, MAX_NPUB_LEN);
|
|
2521
|
+
}
|
|
2522
|
+
const agent = ctx.active();
|
|
2523
|
+
const localEntry = agent.agentDir ? await findCustomerJob(agent.agentDir, input.job_event_id) : void 0;
|
|
2524
|
+
let providerPubkey = localEntry?.providerPubkey;
|
|
2525
|
+
if (!providerPubkey && input.provider_npub) {
|
|
2526
|
+
try {
|
|
2527
|
+
providerPubkey = decodeNpub(input.provider_npub);
|
|
2528
|
+
} catch (e) {
|
|
2529
|
+
return errorResult(
|
|
2530
|
+
`Invalid provider_npub: ${e instanceof Error ? e.message : String(e)}`
|
|
2531
|
+
);
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
if (!providerPubkey) {
|
|
2535
|
+
return errorResult(
|
|
2536
|
+
`Job "${input.job_event_id}" not found in local history. Pass provider_npub explicitly to rate a job submitted from outside this MCP.`
|
|
2537
|
+
);
|
|
2538
|
+
}
|
|
2539
|
+
if (localEntry?.customerFeedback === input.rating) {
|
|
2540
|
+
return textResult(`Already rated as ${input.rating}.`);
|
|
2541
|
+
}
|
|
2542
|
+
const capability = localEntry?.capability;
|
|
2543
|
+
const positive = input.rating === "positive";
|
|
2544
|
+
try {
|
|
2545
|
+
await agent.client.marketplace.submitFeedback(
|
|
2546
|
+
agent.identity,
|
|
2547
|
+
input.job_event_id,
|
|
2548
|
+
providerPubkey,
|
|
2549
|
+
positive,
|
|
2550
|
+
capability
|
|
2551
|
+
);
|
|
2552
|
+
} catch (e) {
|
|
2553
|
+
return errorResult(
|
|
2554
|
+
`Failed to publish feedback: ${e instanceof Error ? e.message : String(e)}`
|
|
2555
|
+
);
|
|
2556
|
+
}
|
|
2557
|
+
if (agent.agentDir && localEntry) {
|
|
2558
|
+
await updateCustomerJob(agent.agentDir, input.job_event_id, {
|
|
2559
|
+
customerFeedback: input.rating
|
|
2560
|
+
}).catch(() => {
|
|
2561
|
+
});
|
|
2562
|
+
}
|
|
2563
|
+
const npubForTip = input.provider_npub ?? npubFromHex(providerPubkey);
|
|
2564
|
+
if (positive) {
|
|
2565
|
+
return textResult(
|
|
2566
|
+
`Feedback recorded (rating=positive). Save this provider for future searches? Use add_contact (npub="${npubForTip}").`
|
|
2567
|
+
);
|
|
2568
|
+
}
|
|
2569
|
+
return textResult("Feedback recorded (rating=negative).");
|
|
2570
|
+
}
|
|
2571
|
+
}),
|
|
2572
|
+
defineTool({
|
|
2573
|
+
name: "add_contact",
|
|
2574
|
+
description: "Add a provider to the active agent's contacts list (.contacts.json). When the provider has prior jobs in the local history, the contact is enriched with jobCount, lastJobAt, and lastCapability. Idempotent: re-calling with the same npub updates name/note in place without duplicating the entry.",
|
|
2575
|
+
schema: AddContactSchema,
|
|
2576
|
+
async handler(ctx, input) {
|
|
2577
|
+
ctx.toolRateLimiter.check();
|
|
2578
|
+
checkLen("npub", input.npub, MAX_NPUB_LEN);
|
|
2579
|
+
const agent = ctx.active();
|
|
2580
|
+
if (!agent.agentDir) {
|
|
2581
|
+
return errorResult(
|
|
2582
|
+
"Cannot save contacts: the active agent is ephemeral (no on-disk directory). Create a persistent agent first with create_agent."
|
|
2583
|
+
);
|
|
2584
|
+
}
|
|
2585
|
+
let pubkey;
|
|
2586
|
+
try {
|
|
2587
|
+
pubkey = decodeNpub(input.npub);
|
|
2588
|
+
} catch (e) {
|
|
2589
|
+
return errorResult(`Invalid npub: ${e instanceof Error ? e.message : String(e)}`);
|
|
2590
|
+
}
|
|
2591
|
+
const history = await findCustomerJobsByProvider(agent.agentDir, pubkey);
|
|
2592
|
+
const last = history[0];
|
|
2593
|
+
const cleanName = input.name !== void 0 ? sanitizeField(input.name, 200) : void 0;
|
|
2594
|
+
const cleanNote = input.note !== void 0 ? sanitizeField(input.note, 500) : void 0;
|
|
2595
|
+
const fallbackProviderName = last?.providerName !== void 0 ? sanitizeField(last.providerName, 200) : void 0;
|
|
2596
|
+
const contact = await upsertContact(agent.agentDir, {
|
|
2597
|
+
pubkey,
|
|
2598
|
+
npub: input.npub,
|
|
2599
|
+
name: cleanName ?? fallbackProviderName,
|
|
2600
|
+
note: cleanNote,
|
|
2601
|
+
lastJobAt: last?.completedAt,
|
|
2602
|
+
lastCapability: last?.capability,
|
|
2603
|
+
jobCount: history.length
|
|
2604
|
+
});
|
|
2605
|
+
const lines = [
|
|
2606
|
+
`Saved contact ${contact.npub}.`,
|
|
2607
|
+
contact.name ? ` name: ${contact.name}` : null,
|
|
2608
|
+
contact.lastCapability ? ` last capability: ${contact.lastCapability}` : null,
|
|
2609
|
+
contact.jobCount > 0 ? ` prior jobs: ${contact.jobCount}` : null
|
|
2610
|
+
].filter((line) => line !== null);
|
|
2611
|
+
return textResult(lines.join("\n"));
|
|
2612
|
+
}
|
|
2613
|
+
}),
|
|
2614
|
+
defineTool({
|
|
2615
|
+
name: "remove_contact",
|
|
2616
|
+
description: "Remove a provider from the active agent's contacts list.",
|
|
2617
|
+
schema: RemoveContactSchema,
|
|
2618
|
+
async handler(ctx, input) {
|
|
2619
|
+
ctx.toolRateLimiter.check();
|
|
2620
|
+
checkLen("npub", input.npub, MAX_NPUB_LEN);
|
|
2621
|
+
const agent = ctx.active();
|
|
2622
|
+
if (!agent.agentDir) {
|
|
2623
|
+
return errorResult("Active agent is ephemeral; nothing to remove.");
|
|
2624
|
+
}
|
|
2625
|
+
let pubkey;
|
|
2626
|
+
try {
|
|
2627
|
+
pubkey = decodeNpub(input.npub);
|
|
2628
|
+
} catch (e) {
|
|
2629
|
+
return errorResult(`Invalid npub: ${e instanceof Error ? e.message : String(e)}`);
|
|
2630
|
+
}
|
|
2631
|
+
const removed = await removeContact(agent.agentDir, pubkey);
|
|
2632
|
+
return removed ? textResult(`Removed contact ${input.npub}.`) : textResult(`No contact found for ${input.npub}.`);
|
|
2633
|
+
}
|
|
2634
|
+
}),
|
|
2635
|
+
defineTool({
|
|
2636
|
+
name: "list_contacts",
|
|
2637
|
+
description: "List providers saved in the active agent's .contacts.json, newest activity first. Use search_agents with contacts_only=true to combine this with online/capability filters.",
|
|
2638
|
+
schema: ListContactsSchema,
|
|
2639
|
+
async handler(ctx, input) {
|
|
2640
|
+
ctx.toolRateLimiter.check();
|
|
2641
|
+
const agent = ctx.active();
|
|
2642
|
+
if (!agent.agentDir) {
|
|
2643
|
+
return textResult(
|
|
2644
|
+
"Active agent is ephemeral; no on-disk contacts. Create a persistent agent first."
|
|
2645
|
+
);
|
|
2646
|
+
}
|
|
2647
|
+
const data = await readContacts(agent.agentDir);
|
|
2648
|
+
const sorted = [...data.contacts].sort((left, right) => {
|
|
2649
|
+
const leftKey = left.lastJobAt ?? left.addedAt;
|
|
2650
|
+
const rightKey = right.lastJobAt ?? right.addedAt;
|
|
2651
|
+
return rightKey - leftKey;
|
|
2652
|
+
});
|
|
2653
|
+
const limited = sorted.slice(0, input.limit).map((contact) => ({
|
|
2654
|
+
npub: contact.npub,
|
|
2655
|
+
name: contact.name !== void 0 ? sanitizeField(contact.name, 200) : void 0,
|
|
2656
|
+
note: contact.note !== void 0 ? sanitizeField(contact.note, 500) : void 0,
|
|
2657
|
+
added_at: contact.addedAt,
|
|
2658
|
+
last_job_at: contact.lastJobAt,
|
|
2659
|
+
last_capability: contact.lastCapability !== void 0 ? sanitizeField(contact.lastCapability, 200) : void 0,
|
|
2660
|
+
job_count: contact.jobCount
|
|
2661
|
+
}));
|
|
2662
|
+
const { text: wrapped } = sanitizeUntrusted(JSON.stringify(limited, null, 2), "structured");
|
|
2663
|
+
return textResult(`${limited.length} contact(s):
|
|
2664
|
+
${wrapped}`);
|
|
2665
|
+
}
|
|
2666
|
+
})
|
|
2667
|
+
];
|
|
2122
2668
|
var GetBalanceSchema = z.object({});
|
|
2123
2669
|
var EstimatePaymentCostSchema = z.object({
|
|
2124
2670
|
payment_request: z.string().describe(
|
|
@@ -2625,7 +3171,8 @@ var allTools = [
|
|
|
2625
3171
|
...customerTools,
|
|
2626
3172
|
...walletTools,
|
|
2627
3173
|
...dashboardTools,
|
|
2628
|
-
...agentTools
|
|
3174
|
+
...agentTools,
|
|
3175
|
+
...feedbackContactsTools
|
|
2629
3176
|
];
|
|
2630
3177
|
var toolMap = /* @__PURE__ */ new Map();
|
|
2631
3178
|
for (const tool of allTools) {
|