web3agent 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.
@@ -0,0 +1,434 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ BLOCKSCOUT_DEFAULT_URL,
4
+ ETHERSCAN_DEFAULT_URL
5
+ } from "./chunk-BF4PA46E.js";
6
+
7
+ // src/cli/init.ts
8
+ import { resolve } from "path";
9
+
10
+ // src/hosts/context/index.ts
11
+ import { existsSync } from "fs";
12
+ import { mkdir, readFile, writeFile } from "fs/promises";
13
+ import { dirname, join } from "path";
14
+ var MARKER_START = "<!-- web3agent:start -->";
15
+ var MARKER_END = "<!-- web3agent:end -->";
16
+ var CONTEXT_BODY = `## Web3
17
+
18
+ This project has web3agent configured. Use the MCP tools for Web3 operations.
19
+ See: web3agent server_status, list_supported_chains for available capabilities.`;
20
+ var MANAGED_BLOCK = `${MARKER_START}
21
+ ${CONTEXT_BODY}
22
+ ${MARKER_END}`;
23
+ var CURSOR_FRONTMATTER = `---
24
+ description: Web3 capabilities
25
+ globs: []
26
+ alwaysApply: false
27
+ ---`;
28
+ function contextFilePath(host, projectDir) {
29
+ switch (host) {
30
+ case "claude":
31
+ return join(projectDir, "CLAUDE.md");
32
+ case "cursor":
33
+ return join(projectDir, ".cursor", "rules", "web3agent.mdc");
34
+ case "windsurf":
35
+ return join(projectDir, ".windsurf", "rules", "web3agent.md");
36
+ case "opencode":
37
+ return join(projectDir, "AGENTS.md");
38
+ }
39
+ }
40
+ function buildContent(host) {
41
+ if (host === "cursor") {
42
+ return `${CURSOR_FRONTMATTER}
43
+
44
+ ${MANAGED_BLOCK}
45
+ `;
46
+ }
47
+ return `${MANAGED_BLOCK}
48
+ `;
49
+ }
50
+ function replaceManagedBlock(existing, newBlock) {
51
+ const startIdx = existing.indexOf(MARKER_START);
52
+ const endIdx = existing.indexOf(MARKER_END);
53
+ if (startIdx !== -1 && endIdx !== -1) {
54
+ const before = existing.slice(0, startIdx);
55
+ const after = existing.slice(endIdx + MARKER_END.length);
56
+ return `${before}${newBlock}${after}`;
57
+ }
58
+ const trimmed = existing.trimEnd();
59
+ return `${trimmed}
60
+
61
+ ${newBlock}
62
+ `;
63
+ }
64
+ async function installContext(host, options) {
65
+ const filePath = contextFilePath(host, options.projectDir);
66
+ const newBlock = host === "cursor" ? buildContent("cursor") : MANAGED_BLOCK;
67
+ if (existsSync(filePath)) {
68
+ const existing = await readFile(filePath, "utf-8");
69
+ if (host === "cursor" || host === "windsurf") {
70
+ const updated2 = buildContent(host);
71
+ if (existing === updated2) {
72
+ return { configPath: filePath, action: "unchanged" };
73
+ }
74
+ if (options.dryRun) {
75
+ return { configPath: filePath, action: "updated", diff: `Would update ${filePath}` };
76
+ }
77
+ await writeFile(filePath, updated2, "utf-8");
78
+ return { configPath: filePath, action: "updated" };
79
+ }
80
+ const hasMarkers = existing.includes(MARKER_START) && existing.includes(MARKER_END);
81
+ const updated = replaceManagedBlock(existing, MANAGED_BLOCK);
82
+ if (existing === updated) {
83
+ return { configPath: filePath, action: "unchanged" };
84
+ }
85
+ if (options.dryRun) {
86
+ const verb = hasMarkers ? "update managed section in" : "append managed section to";
87
+ return { configPath: filePath, action: "updated", diff: `Would ${verb} ${filePath}` };
88
+ }
89
+ await writeFile(filePath, updated, "utf-8");
90
+ return { configPath: filePath, action: "updated" };
91
+ }
92
+ if (options.dryRun) {
93
+ return { configPath: filePath, action: "created", diff: `Would create ${filePath}` };
94
+ }
95
+ await mkdir(dirname(filePath), { recursive: true });
96
+ await writeFile(filePath, buildContent(host), "utf-8");
97
+ return { configPath: filePath, action: "created" };
98
+ }
99
+
100
+ // src/hosts/detect.ts
101
+ import { access } from "fs/promises";
102
+ import { homedir } from "os";
103
+ import { join as join2 } from "path";
104
+ var SUPPORTED_HOSTS = ["claude", "cursor", "windsurf", "opencode"];
105
+ async function dirExists(p) {
106
+ try {
107
+ await access(p);
108
+ return true;
109
+ } catch {
110
+ return false;
111
+ }
112
+ }
113
+ async function detectHosts(projectDir, homeDir) {
114
+ const home = homeDir ?? homedir();
115
+ const detected = [];
116
+ const checks = [
117
+ { host: "claude", paths: [join2(home, ".claude")] },
118
+ { host: "cursor", paths: [join2(projectDir, ".cursor")] },
119
+ {
120
+ host: "windsurf",
121
+ paths: [join2(projectDir, ".windsurf"), join2(home, ".codeium", "windsurf")]
122
+ },
123
+ { host: "opencode", paths: [join2(projectDir, ".opencode")] }
124
+ ];
125
+ await Promise.all(
126
+ checks.map(async ({ host, paths }) => {
127
+ for (const p of paths) {
128
+ if (await dirExists(p)) {
129
+ detected.push(host);
130
+ return;
131
+ }
132
+ }
133
+ })
134
+ );
135
+ const ordered = SUPPORTED_HOSTS.filter((h) => detected.includes(h));
136
+ return { detected: ordered, projectDir };
137
+ }
138
+ function assertSingleHost(detected, explicitHost) {
139
+ if (explicitHost) {
140
+ if (!SUPPORTED_HOSTS.includes(explicitHost)) {
141
+ throw new Error(
142
+ `Unsupported host "${explicitHost}". Supported: ${SUPPORTED_HOSTS.join(", ")}`
143
+ );
144
+ }
145
+ return explicitHost;
146
+ }
147
+ if (detected.length === 0) {
148
+ throw new Error(
149
+ `No supported agent host detected. Run with --host to specify one of: ${SUPPORTED_HOSTS.join(", ")}`
150
+ );
151
+ }
152
+ if (detected.length > 1) {
153
+ throw new Error(
154
+ `Multiple agent hosts detected: ${detected.join(", ")}. Use --host to specify which one.`
155
+ );
156
+ }
157
+ return detected[0];
158
+ }
159
+
160
+ // src/hosts/writers/claude.ts
161
+ import { existsSync as existsSync3 } from "fs";
162
+ import { homedir as homedir2 } from "os";
163
+ import { join as join3 } from "path";
164
+
165
+ // src/hosts/writers/base.ts
166
+ import { existsSync as existsSync2 } from "fs";
167
+ import { copyFile, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
168
+ import { dirname as dirname2 } from "path";
169
+ var MANAGED_KEYS = ["web3agent", "blockscout", "etherscan", "evm"];
170
+ function proxyEntries() {
171
+ return {
172
+ web3agent: {
173
+ type: "stdio",
174
+ command: "npx",
175
+ args: ["web3agent"]
176
+ }
177
+ };
178
+ }
179
+ function multiServerEntries() {
180
+ return {
181
+ web3agent: {
182
+ type: "stdio",
183
+ command: "npx",
184
+ args: ["web3agent"]
185
+ },
186
+ blockscout: {
187
+ type: "sse",
188
+ url: BLOCKSCOUT_DEFAULT_URL
189
+ },
190
+ etherscan: {
191
+ type: "sse",
192
+ url: ETHERSCAN_DEFAULT_URL
193
+ },
194
+ evm: {
195
+ command: "npx",
196
+ args: ["-y", "@mcpdotdirect/evm-mcp-server"]
197
+ }
198
+ };
199
+ }
200
+ function mergeServers(existing, incoming) {
201
+ const merged = { ...existing };
202
+ let changed = false;
203
+ for (const key of MANAGED_KEYS) {
204
+ if (key in incoming) {
205
+ const oldVal = JSON.stringify(merged[key]);
206
+ const newVal = JSON.stringify(incoming[key]);
207
+ if (oldVal !== newVal) {
208
+ merged[key] = incoming[key];
209
+ changed = true;
210
+ }
211
+ }
212
+ }
213
+ return { merged, changed };
214
+ }
215
+ async function safeWriteConfig(configPath, content, dryRun) {
216
+ const exists = existsSync2(configPath);
217
+ if (dryRun) {
218
+ return { action: exists ? "updated" : "created" };
219
+ }
220
+ await mkdir2(dirname2(configPath), { recursive: true });
221
+ let backupPath;
222
+ if (exists) {
223
+ backupPath = `${configPath}.bak`;
224
+ await copyFile(configPath, backupPath);
225
+ }
226
+ await writeFile2(configPath, content, "utf-8");
227
+ return { action: exists ? "updated" : "created", backupPath };
228
+ }
229
+ async function readJsonFile(path) {
230
+ try {
231
+ const raw = await readFile2(path, "utf-8");
232
+ return JSON.parse(raw);
233
+ } catch {
234
+ return null;
235
+ }
236
+ }
237
+ var BaseHostWriter = class {
238
+ getConfigSectionKey() {
239
+ return "mcpServers";
240
+ }
241
+ getEntries(options) {
242
+ return options.mode === "proxy" ? proxyEntries() : multiServerEntries();
243
+ }
244
+ async write(options) {
245
+ const configPath = this.getConfigPath(options);
246
+ const incoming = this.getEntries(options);
247
+ const sectionKey = this.getConfigSectionKey();
248
+ const existing = await readJsonFile(configPath);
249
+ if (existing) {
250
+ const servers = existing[sectionKey] ?? {};
251
+ const { merged, changed } = mergeServers(servers, incoming);
252
+ if (!changed) {
253
+ return { configPath, action: "unchanged" };
254
+ }
255
+ const updated = { ...existing, [sectionKey]: merged };
256
+ const content2 = `${JSON.stringify(updated, null, 2)}
257
+ `;
258
+ if (options.dryRun) {
259
+ return {
260
+ configPath,
261
+ action: "updated",
262
+ diff: `Would update ${sectionKey} in ${configPath}`
263
+ };
264
+ }
265
+ const { backupPath } = await safeWriteConfig(configPath, content2, false);
266
+ return { configPath, action: "updated", backupPath };
267
+ }
268
+ const fresh = { [sectionKey]: incoming };
269
+ const content = `${JSON.stringify(fresh, null, 2)}
270
+ `;
271
+ if (options.dryRun) {
272
+ return {
273
+ configPath,
274
+ action: "created",
275
+ diff: `Would create ${configPath}`
276
+ };
277
+ }
278
+ await safeWriteConfig(configPath, content, false);
279
+ return { configPath, action: "created" };
280
+ }
281
+ };
282
+
283
+ // src/hosts/writers/claude.ts
284
+ var ClaudeWriter = class extends BaseHostWriter {
285
+ getConfigPath(options) {
286
+ const projectLocal = join3(options.projectDir, ".mcp.json");
287
+ if (existsSync3(projectLocal)) {
288
+ return projectLocal;
289
+ }
290
+ return join3(homedir2(), ".claude", "mcp.json");
291
+ }
292
+ };
293
+
294
+ // src/hosts/writers/cursor.ts
295
+ import { join as join4 } from "path";
296
+ var CursorWriter = class extends BaseHostWriter {
297
+ getConfigPath(options) {
298
+ return join4(options.projectDir, ".cursor", "mcp.json");
299
+ }
300
+ };
301
+
302
+ // src/hosts/writers/opencode.ts
303
+ import { existsSync as existsSync4 } from "fs";
304
+ import { join as join5 } from "path";
305
+ var OpenCodeWriter = class extends BaseHostWriter {
306
+ getConfigPath(options) {
307
+ const dotOpencode = join5(options.projectDir, ".opencode", "config.json");
308
+ if (existsSync4(dotOpencode)) return dotOpencode;
309
+ const rootConfig = join5(options.projectDir, "opencode.json");
310
+ if (existsSync4(rootConfig)) return rootConfig;
311
+ return dotOpencode;
312
+ }
313
+ getConfigSectionKey() {
314
+ return "mcp";
315
+ }
316
+ getEntries(options) {
317
+ if (options.mode === "proxy") {
318
+ return {
319
+ web3agent: { type: "local", command: ["npx", "web3agent"] }
320
+ };
321
+ }
322
+ return {
323
+ web3agent: { type: "local", command: ["npx", "web3agent"] },
324
+ blockscout: { type: "sse", url: BLOCKSCOUT_DEFAULT_URL },
325
+ etherscan: { type: "sse", url: ETHERSCAN_DEFAULT_URL },
326
+ evm: { type: "local", command: ["npx", "-y", "@mcpdotdirect/evm-mcp-server"] }
327
+ };
328
+ }
329
+ };
330
+
331
+ // src/hosts/writers/windsurf.ts
332
+ import { homedir as homedir3 } from "os";
333
+ import { join as join6 } from "path";
334
+ var WindsurfWriter = class extends BaseHostWriter {
335
+ getConfigPath(_options) {
336
+ return join6(homedir3(), ".codeium", "windsurf", "mcp_config.json");
337
+ }
338
+ getEntries(options) {
339
+ if (options.mode === "proxy") {
340
+ return {
341
+ web3agent: { command: "npx", args: ["web3agent"] }
342
+ };
343
+ }
344
+ return {
345
+ web3agent: { command: "npx", args: ["web3agent"] },
346
+ blockscout: { serverUrl: BLOCKSCOUT_DEFAULT_URL },
347
+ etherscan: { serverUrl: ETHERSCAN_DEFAULT_URL },
348
+ evm: { command: "npx", args: ["-y", "@mcpdotdirect/evm-mcp-server"] }
349
+ };
350
+ }
351
+ };
352
+
353
+ // src/cli/init.ts
354
+ function parseArgs(args) {
355
+ const options = {
356
+ mode: "proxy",
357
+ project: process.cwd(),
358
+ dryRun: false
359
+ };
360
+ for (let i = 0; i < args.length; i++) {
361
+ const arg = args[i];
362
+ if (arg === "--host" && i + 1 < args.length) {
363
+ options.host = args[++i];
364
+ } else if (arg === "--mode" && i + 1 < args.length) {
365
+ options.mode = args[++i];
366
+ } else if (arg === "--project" && i + 1 < args.length) {
367
+ options.project = resolve(args[++i]);
368
+ } else if (arg === "--dry-run") {
369
+ options.dryRun = true;
370
+ }
371
+ }
372
+ return options;
373
+ }
374
+ function getWriter(host) {
375
+ switch (host) {
376
+ case "claude":
377
+ return new ClaudeWriter();
378
+ case "cursor":
379
+ return new CursorWriter();
380
+ case "windsurf":
381
+ return new WindsurfWriter();
382
+ case "opencode":
383
+ return new OpenCodeWriter();
384
+ }
385
+ }
386
+ async function runInit(args) {
387
+ const options = parseArgs(args);
388
+ const projectDir = options.project;
389
+ if (options.dryRun) {
390
+ process.stderr.write("[dry-run] No files will be modified\n");
391
+ }
392
+ const { detected } = await detectHosts(projectDir);
393
+ const host = assertSingleHost(detected, options.host);
394
+ process.stderr.write(`Configuring web3agent for ${host}...
395
+ `);
396
+ const writer = getWriter(host);
397
+ const writeResult = await writer.write({
398
+ projectDir,
399
+ mode: options.mode,
400
+ dryRun: options.dryRun
401
+ });
402
+ const contextResult = await installContext(host, {
403
+ projectDir,
404
+ mode: options.mode,
405
+ dryRun: options.dryRun
406
+ });
407
+ process.stderr.write(`
408
+ Config: ${writeResult.configPath} (${writeResult.action})
409
+ `);
410
+ if (writeResult.diff) {
411
+ process.stderr.write(` ${writeResult.diff}
412
+ `);
413
+ }
414
+ if (writeResult.backupPath) {
415
+ process.stderr.write(` Backup: ${writeResult.backupPath}
416
+ `);
417
+ }
418
+ process.stderr.write(`Context: ${contextResult.configPath} (${contextResult.action})
419
+ `);
420
+ if (contextResult.diff) {
421
+ process.stderr.write(` ${contextResult.diff}
422
+ `);
423
+ }
424
+ if (options.dryRun) {
425
+ process.stderr.write("\n[dry-run] Complete. Re-run without --dry-run to apply changes.\n");
426
+ } else {
427
+ process.stderr.write(`
428
+ Done. Restart ${host} to activate web3agent.
429
+ `);
430
+ }
431
+ }
432
+ export {
433
+ runInit
434
+ };