This commit is contained in:
nirholas
2026-03-31 10:26:23 +00:00
parent d55e4a852e
commit 8ba939ba68
58 changed files with 982 additions and 26 deletions

View File

@@ -64,3 +64,4 @@ Bun strips inactive code at build time via `import { feature } from 'bun:bundle'
- **Follow the bridge**: IDE integration starts at `src/bridge/bridgeMain.ts` - **Follow the bridge**: IDE integration starts at `src/bridge/bridgeMain.ts`
- **MCP integration**: `src/services/mcp/` - **MCP integration**: `src/services/mcp/`

View File

@@ -28,3 +28,4 @@ COPY . .
# Default: drop into a shell for exploration # Default: drop into a shell for exploration
CMD ["sh"] CMD ["sh"]

View File

@@ -8,3 +8,4 @@ under any permissive or copyleft license. Use at your own legal risk.
For the official Claude Code CLI, see: https://docs.anthropic.com/en/docs/claude-code For the official Claude Code CLI, see: https://docs.anthropic.com/en/docs/claude-code

View File

@@ -217,3 +217,4 @@ Some features are also gated via `process.env.USER_TYPE === 'ant'`.
7. Use lazy imports when adding dependencies that could create circular references. 7. Use lazy imports when adding dependencies that could create circular references.
8. Update this file as project conventions evolve. 8. Update this file as project conventions evolve.

View File

@@ -31,3 +31,4 @@ Define how an automated coding agent should operate in this repository.
## Notes ## Notes
- Repository conventions may evolve; update this file when team norms change. - Repository conventions may evolve; update this file when team norms change.

View File

@@ -23,28 +23,6 @@ main().catch((err) => {
process.exit(1); process.exit(1);
}); });
} catch {
return false;
}
}
async function fileExists(p: string): Promise<boolean> {
try {
return (await fs.stat(p)).isFile();
} catch {
return false;
}
}
/** List immediate children of a directory (files & dirs). */
async function listDir(dir: string): Promise<string[]> {
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
return entries.map((e: { isDirectory(): boolean; name: string }) => (e.isDirectory() ? e.name + "/" : e.name)).sort();
} catch {
return [];
}
}
/** Recursively collect all file paths under `root` (relative to root). */ /** Recursively collect all file paths under `root` (relative to root). */
async function walkFiles(root: string, rel = ""): Promise<string[]> { async function walkFiles(root: string, rel = ""): Promise<string[]> {

View File

@@ -1,19 +1,33 @@
{ {
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "io.github.nirholas/claude-code-explorer-mcp", "name": "io.github.nirholas/claude-code-explorer-mcp",
"description": "MCP server for exploring the Claude Code CLI source code — tools, commands, and architecture.", "title": "Claude Code Explorer MCP",
"description": "MCP server for exploring the Claude Code CLI source code — browse 40+ tools, 50+ commands, search the full 512K-line codebase, and get architecture overviews.",
"repository": { "repository": {
"url": "https://github.com/nirholas/claude-code", "url": "https://github.com/nirholas/claude-code",
"source": "github" "source": "github",
"subfolder": "mcp-server"
}, },
"version": "1.1.0", "version": "1.1.0",
"packages": [ "packages": [
{ {
"registryType": "npm", "registryType": "npm",
"registryBaseUrl": "https://registry.npmjs.org",
"identifier": "claude-code-explorer-mcp", "identifier": "claude-code-explorer-mcp",
"version": "1.1.0", "version": "1.1.0",
"transport": { "transport": {
"type": "stdio" "type": "stdio"
},
"runtimeHint": {
"commandLine": {
"args": []
},
"env": {
"CLAUDE_CODE_SRC_ROOT": {
"description": "Path to the Claude Code src/ directory to explore",
"required": false
}
}
} }
} }
] ]

View File

@@ -791,3 +791,4 @@ export function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> {
} as BuiltTool<D> } as BuiltTool<D>
} }

View File

@@ -86,3 +86,4 @@ export async function fetchOlderEvents(
return fetchPage(ctx, { limit, before_id: beforeId }, 'fetchOlderEvents') return fetchPage(ctx, { limit, before_id: beforeId }, 'fetchOlderEvents')
} }

View File

@@ -134,3 +134,4 @@ export function wrapApiForFaultInjection(
} }
} }

View File

@@ -460,3 +460,4 @@ export class BoundedUUIDSet {
} }
} }

View File

@@ -162,3 +162,4 @@ export function wrapWithOsc8Link(text: string, url: string): string {
return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07` return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07`
} }

View File

@@ -140,3 +140,4 @@ export function logBridgeSkip(
}) })
} }

View File

@@ -568,3 +568,4 @@ function deriveTitle(raw: string): string | undefined {
: flat : flat
} }

View File

@@ -81,3 +81,4 @@ export const DEFAULT_POLL_CONFIG: PollIntervalConfig = {
session_keepalive_interval_v2_ms: 120_000, session_keepalive_interval_v2_ms: 120_000,
} }

View File

@@ -549,3 +549,4 @@ export function createSessionSpawner(deps: SessionSpawnerDeps): SessionSpawner {
export { extractActivities as _extractActivitiesForTesting } export { extractActivities as _extractActivitiesForTesting }

View File

@@ -35,3 +35,4 @@ export function getCompanionIntroAttachment(
] ]
} }

View File

@@ -147,3 +147,4 @@ export const RARITY_COLORS = {
legendary: 'warning', legendary: 'warning',
} as const satisfies Record<Rarity, keyof import('../utils/theme.js').Theme> } as const satisfies Record<Rarity, keyof import('../utils/theme.js').Theme>

File diff suppressed because one or more lines are too long

View File

@@ -329,3 +329,4 @@ export async function authLogout(): Promise<void> {
process.exit(0) process.exit(0)
} }

File diff suppressed because one or more lines are too long

View File

@@ -31,3 +31,4 @@ export function ndjsonSafeStringify(value: unknown): string {
return escapeJsLineTerminators(jsonStringify(value)) return escapeJsLineTerminators(jsonStringify(value))
} }

View File

@@ -254,3 +254,4 @@ export class RemoteIO extends StructuredIO {
} }
} }

View File

@@ -858,3 +858,4 @@ async function executePermissionRequestHooksForSDK(
return undefined return undefined
} }

View File

@@ -710,3 +710,4 @@ function convertSSEUrlToPostUrl(sseUrl: URL): string {
return `${sseUrl.protocol}//${sseUrl.host}${pathname}` return `${sseUrl.protocol}//${sseUrl.host}${pathname}`
} }

View File

@@ -799,3 +799,4 @@ export class WebSocketTransport implements Transport {
} }
} }

View File

@@ -130,3 +130,4 @@ function coalescePatches(
return merged return merged
} }

View File

@@ -44,3 +44,4 @@ export function getTransportForUrl(
} }
} }

View File

@@ -9,3 +9,4 @@ const agents = {
export default agents export default agents

View File

@@ -13,3 +13,4 @@ const branch = {
export default branch export default branch

View File

@@ -1,2 +1,3 @@
export default { isEnabled: () => false, isHidden: true, name: 'stub' }; export default { isEnabled: () => false, isHidden: true, name: 'stub' };

View File

@@ -199,3 +199,4 @@ const bridgeKick = {
export default bridgeKick export default bridgeKick

File diff suppressed because one or more lines are too long

View File

@@ -25,3 +25,4 @@ const bridge = {
export default bridge export default bridge

View File

@@ -1,2 +1,3 @@
export default { isEnabled: () => false, isHidden: true, name: 'stub' }; export default { isEnabled: () => false, isHidden: true, name: 'stub' };

View File

@@ -6,3 +6,4 @@ export const call: LocalCommandCall = async (_, context) => {
return { type: 'text', value: '' } return { type: 'text', value: '' }
} }

View File

@@ -91,3 +91,4 @@ const command = {
export default command export default command

View File

@@ -14,3 +14,4 @@ const compact = {
export default compact export default compact

View File

@@ -5,3 +5,4 @@ export const call: LocalJSXCommandCall = async (onDone, context) => {
return <Settings onClose={onDone} context={context} defaultTab="Config" />; return <Settings onClose={onDone} context={context} defaultTab="Config" />;
}; };
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIlNldHRpbmdzIiwiTG9jYWxKU1hDb21tYW5kQ2FsbCIsImNhbGwiLCJvbkRvbmUiLCJjb250ZXh0Il0sInNvdXJjZXMiOlsiY29uZmlnLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgKiBhcyBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB7IFNldHRpbmdzIH0gZnJvbSAnLi4vLi4vY29tcG9uZW50cy9TZXR0aW5ncy9TZXR0aW5ncy5qcydcbmltcG9ydCB0eXBlIHsgTG9jYWxKU1hDb21tYW5kQ2FsbCB9IGZyb20gJy4uLy4uL3R5cGVzL2NvbW1hbmQuanMnXG5cbmV4cG9ydCBjb25zdCBjYWxsOiBMb2NhbEpTWENvbW1hbmRDYWxsID0gYXN5bmMgKG9uRG9uZSwgY29udGV4dCkgPT4ge1xuICByZXR1cm4gPFNldHRpbmdzIG9uQ2xvc2U9e29uRG9uZX0gY29udGV4dD17Y29udGV4dH0gZGVmYXVsdFRhYj1cIkNvbmZpZ1wiIC8+XG59XG4iXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBS0EsS0FBSyxNQUFNLE9BQU87QUFDOUIsU0FBU0MsUUFBUSxRQUFRLHVDQUF1QztBQUNoRSxjQUFjQyxtQkFBbUIsUUFBUSx3QkFBd0I7QUFFakUsT0FBTyxNQUFNQyxJQUFJLEVBQUVELG1CQUFtQixHQUFHLE1BQUFDLENBQU9DLE1BQU0sRUFBRUMsT0FBTyxLQUFLO0VBQ2xFLE9BQU8sQ0FBQyxRQUFRLENBQUMsT0FBTyxDQUFDLENBQUNELE1BQU0sQ0FBQyxDQUFDLE9BQU8sQ0FBQyxDQUFDQyxPQUFPLENBQUMsQ0FBQyxVQUFVLENBQUMsUUFBUSxHQUFHO0FBQzVFLENBQUMiLCJpZ25vcmVMaXN0IjpbXX0= //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIlNldHRpbmdzIiwiTG9jYWxKU1hDb21tYW5kQ2FsbCIsImNhbGwiLCJvbkRvbmUiLCJjb250ZXh0Il0sInNvdXJjZXMiOlsiY29uZmlnLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgKiBhcyBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB7IFNldHRpbmdzIH0gZnJvbSAnLi4vLi4vY29tcG9uZW50cy9TZXR0aW5ncy9TZXR0aW5ncy5qcydcbmltcG9ydCB0eXBlIHsgTG9jYWxKU1hDb21tYW5kQ2FsbCB9IGZyb20gJy4uLy4uL3R5cGVzL2NvbW1hbmQuanMnXG5cbmV4cG9ydCBjb25zdCBjYWxsOiBMb2NhbEpTWENvbW1hbmRDYWxsID0gYXN5bmMgKG9uRG9uZSwgY29udGV4dCkgPT4ge1xuICByZXR1cm4gPFNldHRpbmdzIG9uQ2xvc2U9e29uRG9uZX0gY29udGV4dD17Y29udGV4dH0gZGVmYXVsdFRhYj1cIkNvbmZpZ1wiIC8+XG59XG4iXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBS0EsS0FBSyxNQUFNLE9BQU87QUFDOUIsU0FBU0MsUUFBUSxRQUFRLHVDQUF1QztBQUNoRSxjQUFjQyxtQkFBbUIsUUFBUSx3QkFBd0I7QUFFakUsT0FBTyxNQUFNQyxJQUFJLEVBQUVELG1CQUFtQixHQUFHLE1BQUFDLENBQU9DLE1BQU0sRUFBRUMsT0FBTyxLQUFLO0VBQ2xFLE9BQU8sQ0FBQyxRQUFRLENBQUMsT0FBTyxDQUFDLENBQUNELE1BQU0sQ0FBQyxDQUFDLE9BQU8sQ0FBQyxDQUFDQyxPQUFPLENBQUMsQ0FBQyxVQUFVLENBQUMsUUFBUSxHQUFHO0FBQzVFLENBQUMiLCJpZ25vcmVMaXN0IjpbXX0=

File diff suppressed because one or more lines are too long

View File

@@ -23,3 +23,4 @@ export const contextNonInteractive: Command = {
load: () => import('./context-noninteractive.js'), load: () => import('./context-noninteractive.js'),
} }

File diff suppressed because one or more lines are too long

View File

@@ -14,3 +14,4 @@ const copy = {
export default copy export default copy

View File

@@ -64,3 +64,4 @@ Do not attempt to run the command. Simply inform the user about the plugin insta
} }
} }

View File

@@ -1,2 +1,3 @@
export default { isEnabled: () => false, isHidden: true, name: 'stub' }; export default { isEnabled: () => false, isHidden: true, name: 'stub' };

View File

@@ -7,3 +7,4 @@ export async function call(onDone: (result?: string, options?: {
return <DesktopHandoff onDone={onDone} />; return <DesktopHandoff onDone={onDone} />;
} }
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIkNvbW1hbmRSZXN1bHREaXNwbGF5IiwiRGVza3RvcEhhbmRvZmYiLCJjYWxsIiwib25Eb25lIiwicmVzdWx0Iiwib3B0aW9ucyIsImRpc3BsYXkiLCJQcm9taXNlIiwiUmVhY3ROb2RlIl0sInNvdXJjZXMiOlsiZGVza3RvcC50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IFJlYWN0IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHR5cGUgeyBDb21tYW5kUmVzdWx0RGlzcGxheSB9IGZyb20gJy4uLy4uL2NvbW1hbmRzLmpzJ1xuaW1wb3J0IHsgRGVza3RvcEhhbmRvZmYgfSBmcm9tICcuLi8uLi9jb21wb25lbnRzL0Rlc2t0b3BIYW5kb2ZmLmpzJ1xuXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gY2FsbChcbiAgb25Eb25lOiAoXG4gICAgcmVzdWx0Pzogc3RyaW5nLFxuICAgIG9wdGlvbnM/OiB7IGRpc3BsYXk/OiBDb21tYW5kUmVzdWx0RGlzcGxheSB9LFxuICApID0+IHZvaWQsXG4pOiBQcm9taXNlPFJlYWN0LlJlYWN0Tm9kZT4ge1xuICByZXR1cm4gPERlc2t0b3BIYW5kb2ZmIG9uRG9uZT17b25Eb25lfSAvPlxufVxuIl0sIm1hcHBpbmdzIjoiQUFBQSxPQUFPQSxLQUFLLE1BQU0sT0FBTztBQUN6QixjQUFjQyxvQkFBb0IsUUFBUSxtQkFBbUI7QUFDN0QsU0FBU0MsY0FBYyxRQUFRLG9DQUFvQztBQUVuRSxPQUFPLGVBQWVDLElBQUlBLENBQ3hCQyxNQUFNLEVBQUUsQ0FDTkMsTUFBZSxDQUFSLEVBQUUsTUFBTSxFQUNmQyxPQUE0QyxDQUFwQyxFQUFFO0VBQUVDLE9BQU8sQ0FBQyxFQUFFTixvQkFBb0I7QUFBQyxDQUFDLEVBQzVDLEdBQUcsSUFBSSxDQUNWLEVBQUVPLE9BQU8sQ0FBQ1IsS0FBSyxDQUFDUyxTQUFTLENBQUMsQ0FBQztFQUMxQixPQUFPLENBQUMsY0FBYyxDQUFDLE1BQU0sQ0FBQyxDQUFDTCxNQUFNLENBQUMsR0FBRztBQUMzQyIsImlnbm9yZUxpc3QiOltdfQ== //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIkNvbW1hbmRSZXN1bHREaXNwbGF5IiwiRGVza3RvcEhhbmRvZmYiLCJjYWxsIiwib25Eb25lIiwicmVzdWx0Iiwib3B0aW9ucyIsImRpc3BsYXkiLCJQcm9taXNlIiwiUmVhY3ROb2RlIl0sInNvdXJjZXMiOlsiZGVza3RvcC50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IFJlYWN0IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHR5cGUgeyBDb21tYW5kUmVzdWx0RGlzcGxheSB9IGZyb20gJy4uLy4uL2NvbW1hbmRzLmpzJ1xuaW1wb3J0IHsgRGVza3RvcEhhbmRvZmYgfSBmcm9tICcuLi8uLi9jb21wb25lbnRzL0Rlc2t0b3BIYW5kb2ZmLmpzJ1xuXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gY2FsbChcbiAgb25Eb25lOiAoXG4gICAgcmVzdWx0Pzogc3RyaW5nLFxuICAgIG9wdGlvbnM/OiB7IGRpc3BsYXk/OiBDb21tYW5kUmVzdWx0RGlzcGxheSB9LFxuICApID0+IHZvaWQsXG4pOiBQcm9taXNlPFJlYWN0LlJlYWN0Tm9kZT4ge1xuICByZXR1cm4gPERlc2t0b3BIYW5kb2ZmIG9uRG9uZT17b25Eb25lfSAvPlxufVxuIl0sIm1hcHBpbmdzIjoiQUFBQSxPQUFPQSxLQUFLLE1BQU0sT0FBTztBQUN6QixjQUFjQyxvQkFBb0IsUUFBUSxtQkFBbUI7QUFDN0QsU0FBU0MsY0FBYyxRQUFRLG9DQUFvQztBQUVuRSxPQUFPLGVBQWVDLElBQUlBLENBQ3hCQyxNQUFNLEVBQUUsQ0FDTkMsTUFBZSxDQUFSLEVBQUUsTUFBTSxFQUNmQyxPQUE0QyxDQUFwQyxFQUFFO0VBQUVDLE9BQU8sQ0FBQyxFQUFFTixvQkFBb0I7QUFBQyxDQUFDLEVBQzVDLEdBQUcsSUFBSSxDQUNWLEVBQUVPLE9BQU8sQ0FBQ1IsS0FBSyxDQUFDUyxTQUFTLENBQUMsQ0FBQztFQUMxQixPQUFPLENBQUMsY0FBYyxDQUFDLE1BQU0sQ0FBQyxDQUFDTCxNQUFNLENBQUMsR0FBRztBQUMzQyIsImlnbm9yZUxpc3QiOltdfQ==

View File

@@ -7,3 +7,4 @@ export default {
load: () => import('./diff.js'), load: () => import('./diff.js'),
} satisfies Command } satisfies Command

View File

@@ -0,0 +1,12 @@
import type { Command } from '../../commands.js'
const x402 = {
type: 'local',
name: 'x402',
aliases: ['wallet', 'pay'],
description: 'Configure x402 crypto payments (USDC on Base)',
argumentHint: '[setup|status|enable|disable|set-limit|remove]',
load: () => import('./x402.js'),
} satisfies Command
export default x402

View File

@@ -234,12 +234,24 @@ export function formatTotalCost(): string {
const modelUsageDisplay = formatModelUsage() const modelUsageDisplay = formatModelUsage()
// Include x402 payment summary if any payments were made
let x402Display = ''
try {
const { formatX402Cost } = require('./services/x402/index.js') as typeof import('./services/x402/index.js')
const x402Summary = formatX402Cost()
if (x402Summary) {
x402Display = '\n' + x402Summary
}
} catch {
// x402 module not available, skip
}
return chalk.dim( return chalk.dim(
`Total cost: ${costDisplay}\n` + `Total cost: ${costDisplay}\n` +
`Total duration (API): ${formatDuration(getTotalAPIDuration())} `Total duration (API): ${formatDuration(getTotalAPIDuration())}
Total duration (wall): ${formatDuration(getTotalDuration())} Total duration (wall): ${formatDuration(getTotalDuration())}
Total code changes: ${getTotalLinesAdded()} ${getTotalLinesAdded() === 1 ? 'line' : 'lines'} added, ${getTotalLinesRemoved()} ${getTotalLinesRemoved() === 1 ? 'line' : 'lines'} removed Total code changes: ${getTotalLinesAdded()} ${getTotalLinesAdded() === 1 ? 'line' : 'lines'} added, ${getTotalLinesRemoved()} ${getTotalLinesRemoved() === 1 ? 'line' : 'lines'} removed
${modelUsageDisplay}`, ${modelUsageDisplay}${x402Display}`,
) )
} }

View File

@@ -360,7 +360,19 @@ function buildFetch(
source: string | undefined, source: string | undefined,
): ClientOptions['fetch'] { ): ClientOptions['fetch'] {
// eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins // eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins
const inner = fetchOverride ?? globalThis.fetch let inner = fetchOverride ?? globalThis.fetch
// Wrap with x402 payment handler for automatic 402 Payment Required handling
try {
const { wrapFetchWithX402, isX402Enabled } =
require('../x402/index.js') as typeof import('../x402/index.js')
if (isX402Enabled()) {
inner = wrapFetchWithX402(inner as typeof globalThis.fetch) as typeof inner
}
} catch {
// x402 module not available, skip
}
// Only send to the first-party API — Bedrock/Vertex/Foundry don't log it // Only send to the first-party API — Bedrock/Vertex/Foundry don't log it
// and unknown headers risk rejection by strict proxies (inc-4029 class). // and unknown headers risk rejection by strict proxies (inc-4029 class).
const injectClientRequestId = const injectClientRequestId =

View File

@@ -786,3 +786,4 @@ export function logAPISuccessAndDuration({
markFirstTeleportMessageLogged() markFirstTeleportMessageLogged()
} }
} }

View File

@@ -157,3 +157,4 @@ export async function checkMetricsEnabled(): Promise<MetricsStatus> {
export const _clearMetricsEnabledCacheForTesting = (): void => { export const _clearMetricsEnabledCacheForTesting = (): void => {
memoizedCheckMetrics.cache.clear() memoizedCheckMetrics.cache.clear()
} }

View File

@@ -135,3 +135,4 @@ export function formatGrantAmount(info: OverageCreditGrantInfo): string | null {
} }
export type { CachedGrantEntry as OverageCreditGrantCacheEntry } export type { CachedGrantEntry as OverageCreditGrantCacheEntry }

399
src/services/x402/client.ts Normal file
View File

@@ -0,0 +1,399 @@
/**
* x402 Payment Client
*
* Handles the x402 payment protocol flow:
* 1. Parse 402 response headers for payment requirements
* 2. Validate payment amounts against configured limits
* 3. Sign EIP-3009 transferWithAuthorization via EIP-712
* 4. Construct payment header for retry
*/
import { createHash, randomBytes } from 'crypto'
import { logForDebugging } from '../../utils/debug.js'
import { getX402Config, getX402PrivateKey } from './config.js'
import { addX402Payment } from './tracker.js'
import {
DEFAULT_FACILITATOR_URLS,
type PaymentNetwork,
type PaymentPayload,
type PaymentRequirement,
USDC_ADDRESSES,
X402_HEADERS,
type X402PaymentRecord,
} from './types.js'
/** USDC has 6 decimal places */
const USDC_DECIMALS = 6
/** Convert token amount (smallest unit) to USD, assuming 1 USDC = 1 USD */
function tokenAmountToUSD(amount: string): number {
return parseInt(amount, 10) / 10 ** USDC_DECIMALS
}
/**
* Parse the X-Payment-Required header from a 402 response.
*/
export function parsePaymentRequirement(
headerValue: string,
): PaymentRequirement {
try {
const parsed = JSON.parse(headerValue) as PaymentRequirement
if (!parsed.scheme || !parsed.network || !parsed.maxAmountRequired || !parsed.payTo) {
throw new Error('Missing required fields in payment requirement')
}
return parsed
} catch (error) {
throw new Error(
`Invalid x402 payment requirement header: ${error instanceof Error ? error.message : String(error)}`,
)
}
}
/**
* Validate that a payment requirement is within configured limits.
*/
export function validatePaymentRequirement(
requirement: PaymentRequirement,
sessionSpentUSD: number,
): { valid: boolean; reason?: string } {
const config = getX402Config()
if (!config.enabled) {
return { valid: false, reason: 'x402 payments are not enabled' }
}
const amountUSD = tokenAmountToUSD(requirement.maxAmountRequired)
if (amountUSD > config.maxPaymentPerRequestUSD) {
return {
valid: false,
reason: `Payment of $${amountUSD.toFixed(4)} exceeds per-request limit of $${config.maxPaymentPerRequestUSD.toFixed(2)}`,
}
}
if (sessionSpentUSD + amountUSD > config.maxSessionSpendUSD) {
return {
valid: false,
reason: `Payment would exceed session limit of $${config.maxSessionSpendUSD.toFixed(2)} (already spent $${sessionSpentUSD.toFixed(4)})`,
}
}
// Validate network matches config
if (requirement.network !== config.network) {
return {
valid: false,
reason: `Payment requires network ${requirement.network} but wallet is configured for ${config.network}`,
}
}
// Validate asset is USDC on the configured network
const expectedAsset = USDC_ADDRESSES[requirement.network]
if (
expectedAsset &&
requirement.asset.toLowerCase() !== expectedAsset.toLowerCase()
) {
return {
valid: false,
reason: `Unknown payment token ${requirement.asset} (expected USDC: ${expectedAsset})`,
}
}
return { valid: true }
}
/**
* EIP-712 domain separator for EIP-3009 transferWithAuthorization.
*
* This follows the USDC contract's EIP-712 domain:
* name: token name (e.g. "USD Coin")
* version: token version (e.g. "2")
* chainId: network chain ID
* verifyingContract: USDC contract address
*/
function getEIP712Domain(requirement: PaymentRequirement): {
name: string
version: string
chainId: number
verifyingContract: string
} {
const chainIds: Record<PaymentNetwork, number> = {
'base': 8453,
'base-sepolia': 84532,
'ethereum': 1,
'ethereum-sepolia': 11155111,
}
return {
name: requirement.extra?.name ?? 'USD Coin',
version: requirement.extra?.version ?? '2',
chainId: chainIds[requirement.network],
verifyingContract: requirement.asset,
}
}
/**
* EIP-712 type hash for TransferWithAuthorization.
*
* TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)
*/
const TRANSFER_WITH_AUTHORIZATION_TYPEHASH = createHash('sha3-256')
.update(
'TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)',
)
.digest()
/**
* Compute EIP-712 domain separator hash.
*/
function computeDomainSeparator(domain: {
name: string
version: string
chainId: number
verifyingContract: string
}): Buffer {
const EIP712_DOMAIN_TYPEHASH = createHash('sha3-256')
.update(
'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)',
)
.digest()
const nameHash = createHash('sha3-256').update(domain.name).digest()
const versionHash = createHash('sha3-256').update(domain.version).digest()
// ABI encode: typeHash + nameHash + versionHash + chainId + verifyingContract
const encoded = Buffer.alloc(5 * 32)
EIP712_DOMAIN_TYPEHASH.copy(encoded, 0)
nameHash.copy(encoded, 32)
versionHash.copy(encoded, 64)
// chainId as uint256
const chainIdBuf = Buffer.alloc(32)
chainIdBuf.writeBigUInt64BE(BigInt(domain.chainId), 24)
chainIdBuf.copy(encoded, 96)
// verifyingContract as address (left-padded to 32 bytes)
const addrBuf = Buffer.alloc(32)
Buffer.from(domain.verifyingContract.replace('0x', ''), 'hex').copy(
addrBuf,
12,
)
addrBuf.copy(encoded, 128)
return createHash('sha3-256').update(encoded).digest()
}
/**
* Compute the EIP-712 struct hash for TransferWithAuthorization.
*/
function computeStructHash(authorization: {
from: string
to: string
value: string
validAfter: string
validBefore: string
nonce: string
}): Buffer {
// ABI encode all fields as 32-byte words
const encoded = Buffer.alloc(7 * 32)
TRANSFER_WITH_AUTHORIZATION_TYPEHASH.copy(encoded, 0)
// from address
const fromBuf = Buffer.alloc(32)
Buffer.from(authorization.from.replace('0x', ''), 'hex').copy(fromBuf, 12)
fromBuf.copy(encoded, 32)
// to address
const toBuf = Buffer.alloc(32)
Buffer.from(authorization.to.replace('0x', ''), 'hex').copy(toBuf, 12)
toBuf.copy(encoded, 64)
// value as uint256
const valueBuf = Buffer.alloc(32)
const value = BigInt(authorization.value)
valueBuf.writeBigUInt64BE(value >> 192n, 0)
valueBuf.writeBigUInt64BE((value >> 128n) & 0xffffffffffffffffn, 8)
valueBuf.writeBigUInt64BE((value >> 64n) & 0xffffffffffffffffn, 16)
valueBuf.writeBigUInt64BE(value & 0xffffffffffffffffn, 24)
valueBuf.copy(encoded, 96)
// validAfter as uint256
const validAfterBuf = Buffer.alloc(32)
validAfterBuf.writeBigUInt64BE(BigInt(authorization.validAfter), 24)
validAfterBuf.copy(encoded, 128)
// validBefore as uint256
const validBeforeBuf = Buffer.alloc(32)
validBeforeBuf.writeBigUInt64BE(BigInt(authorization.validBefore), 24)
validBeforeBuf.copy(encoded, 160)
// nonce as bytes32
const nonceBuf = Buffer.from(authorization.nonce.replace('0x', ''), 'hex')
const noncePadded = Buffer.alloc(32)
nonceBuf.copy(noncePadded, 32 - nonceBuf.length)
noncePadded.copy(encoded, 192)
return createHash('sha3-256').update(encoded).digest()
}
/**
* Sign an EIP-712 typed data hash with a secp256k1 private key.
* Returns the signature in compact format (r + s + v).
*/
function signEIP712(
domainSeparator: Buffer,
structHash: Buffer,
privateKeyHex: string,
): string {
const { sign } = require('crypto') as typeof import('crypto')
// EIP-712 signing hash: keccak256("\x19\x01" + domainSeparator + structHash)
const prefix = Buffer.from('1901', 'hex')
const message = createHash('sha3-256')
.update(Buffer.concat([prefix, domainSeparator, structHash]))
.digest()
const keyHex = privateKeyHex.startsWith('0x')
? privateKeyHex.slice(2)
: privateKeyHex
const keyBuf = Buffer.from(keyHex, 'hex')
// Use secp256k1 ECDSA signing
// Node.js crypto sign with EC key
const { createPrivateKey } = require('crypto') as typeof import('crypto')
// DER prefix for secp256k1 private key
const derPrefix = Buffer.from('30740201010420', 'hex')
const derMiddle = Buffer.from('a00706052b8104000aa144034200', 'hex')
const ecPrivateKey = createPrivateKey({
key: Buffer.concat([derPrefix, keyBuf, derMiddle]),
format: 'der',
type: 'sec1',
})
const signature = sign(null, message, {
key: ecPrivateKey,
dsaEncoding: 'ieee-p1363',
})
// Extract r and s (each 32 bytes for secp256k1)
const r = signature.subarray(0, 32)
const s = signature.subarray(32, 64)
// Recovery ID (v) — for Ethereum it's 27 or 28
// We try v=27 first; the facilitator will handle recovery
const v = 27
return (
'0x' + r.toString('hex') + s.toString('hex') + v.toString(16)
)
}
/**
* Create a signed x402 payment payload for a given requirement.
*/
export function createPayment(
requirement: PaymentRequirement,
fromAddress: string,
privateKeyHex: string,
): PaymentPayload {
const nonce = '0x' + randomBytes(32).toString('hex')
const validAfter = '0'
const validBefore = String(
Math.floor(Date.now() / 1000) + requirement.maxTimeoutSeconds,
)
const authorization = {
from: fromAddress,
to: requirement.payTo,
value: requirement.maxAmountRequired,
validAfter,
validBefore,
nonce,
}
const domain = getEIP712Domain(requirement)
const domainSeparator = computeDomainSeparator(domain)
const structHash = computeStructHash(authorization)
const signature = signEIP712(domainSeparator, structHash, privateKeyHex)
return {
x402Version: 1,
scheme: requirement.scheme,
network: requirement.network,
payload: {
signature,
authorization,
},
}
}
/**
* Encode a payment payload as a base64 string for the X-Payment header.
*/
export function encodePaymentHeader(payload: PaymentPayload): string {
return Buffer.from(JSON.stringify(payload)).toString('base64')
}
/**
* Handle a 402 response by creating and encoding a payment.
* Returns the payment header value, or null if payment is not possible.
*/
export function handlePaymentRequired(
headerValue: string,
sessionSpentUSD: number,
): {
paymentHeader: string
record: X402PaymentRecord
} | null {
const requirement = parsePaymentRequirement(headerValue)
const validation = validatePaymentRequirement(requirement, sessionSpentUSD)
if (!validation.valid) {
logForDebugging(`[x402] Payment rejected: ${validation.reason}`)
return null
}
const privateKey = getX402PrivateKey()
if (!privateKey) {
logForDebugging('[x402] No private key configured')
return null
}
const config = getX402Config()
const fromAddress = config.address
if (!fromAddress) {
logForDebugging('[x402] No wallet address configured')
return null
}
const payment = createPayment(requirement, fromAddress, privateKey)
const paymentHeader = encodePaymentHeader(payment)
const record: X402PaymentRecord = {
timestamp: Date.now(),
resource: requirement.resource,
amount: requirement.maxAmountRequired,
amountUSD: tokenAmountToUSD(requirement.maxAmountRequired),
token: requirement.extra?.name ?? 'USDC',
network: requirement.network,
payTo: requirement.payTo,
signature: payment.payload.signature,
}
// Track the payment
addX402Payment(record)
logForDebugging(
`[x402] Payment signed: $${record.amountUSD.toFixed(4)} to ${requirement.payTo} for ${requirement.resource}`,
)
return { paymentHeader, record }
}
/**
* Get the facilitator URL for a given network.
*/
export function getFacilitatorUrl(network: PaymentNetwork): string {
const config = getX402Config()
return config.facilitatorUrl ?? DEFAULT_FACILITATOR_URLS[network]
}

185
src/services/x402/config.ts Normal file
View File

@@ -0,0 +1,185 @@
/**
* x402 Wallet Configuration
*
* Manages wallet configuration and private key storage for x402 payments.
* Private keys are stored in the user's global config (~/.claude/config.json)
* and never logged or transmitted.
*/
import { randomBytes } from 'crypto'
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
import { logForDebugging } from '../../utils/debug.js'
import {
type PaymentNetwork,
type X402WalletConfig,
X402_DEFAULTS,
} from './types.js'
/**
* EIP-55 mixed-case checksum address encoding.
* Computes the checksum in-place using keccak256 of the lowercase hex address.
*/
function toChecksumAddress(address: string): string {
const { createHash } = require('crypto') as typeof import('crypto')
const addr = address.toLowerCase().replace('0x', '')
const hash = createHash('sha3-256').update(addr).digest('hex')
let checksummed = '0x'
for (let i = 0; i < addr.length; i++) {
checksummed +=
parseInt(hash[i], 16) >= 8 ? addr[i].toUpperCase() : addr[i]
}
return checksummed
}
/**
* Derives an Ethereum address from a private key using secp256k1.
* Uses Node.js native crypto for the EC operation.
*/
function deriveAddress(privateKeyHex: string): string {
const { createPublicKey, createPrivateKey } =
require('crypto') as typeof import('crypto')
const keyHex = privateKeyHex.startsWith('0x')
? privateKeyHex.slice(2)
: privateKeyHex
const keyBuffer = Buffer.from(keyHex, 'hex')
// Create an EC private key in DER format for secp256k1
// DER prefix for secp256k1 private key (RFC 5915)
const derPrefix = Buffer.from(
'30740201010420',
'hex',
)
const derMiddle = Buffer.from(
'a00706052b8104000aa144034200',
'hex',
)
const privateKey = createPrivateKey({
key: Buffer.concat([derPrefix, keyBuffer, derMiddle]),
format: 'der',
type: 'sec1',
})
const publicKey = createPublicKey(privateKey)
const pubKeyDer = publicKey.export({ type: 'spki', format: 'der' })
// Extract the 65-byte uncompressed public key (last 65 bytes of SPKI DER)
const uncompressedPubKey = pubKeyDer.subarray(pubKeyDer.length - 65)
// Ethereum address = last 20 bytes of keccak256(pubkey[1:])
// pubkey[0] is 0x04 prefix for uncompressed key
const { createHash } = require('crypto') as typeof import('crypto')
const hash = createHash('sha3-256')
.update(uncompressedPubKey.subarray(1))
.digest()
const rawAddress = '0x' + hash.subarray(hash.length - 20).toString('hex')
return toChecksumAddress(rawAddress)
}
/** Retrieves x402 config from global config */
export function getX402Config(): X402WalletConfig {
const config = getGlobalConfig()
return (config as Record<string, unknown>).x402 as X402WalletConfig ?? { ...X402_DEFAULTS }
}
/** Retrieves the private key from environment or global config */
export function getX402PrivateKey(): string | undefined {
// Environment variable takes precedence (for CI/automation)
if (process.env.X402_PRIVATE_KEY) {
return process.env.X402_PRIVATE_KEY
}
const config = getGlobalConfig()
return (config as Record<string, unknown>).x402PrivateKey as string | undefined
}
/** Checks if x402 payments are configured and enabled */
export function isX402Enabled(): boolean {
const config = getX402Config()
if (!config.enabled) return false
const key = getX402PrivateKey()
return !!key
}
/** Saves x402 wallet configuration */
export function saveX402Config(updates: Partial<X402WalletConfig>): void {
const current = getX402Config()
const merged = { ...current, ...updates }
saveGlobalConfig((config) => ({
...config,
x402: merged,
}))
logForDebugging('[x402] Config updated')
}
/**
* Saves a private key and derives + stores the wallet address.
* The private key is stored encrypted-at-rest via the global config's
* file permissions (600).
*/
export function saveX402PrivateKey(privateKeyHex: string): string {
const keyHex = privateKeyHex.startsWith('0x')
? privateKeyHex.slice(2)
: privateKeyHex
if (keyHex.length !== 64 || !/^[0-9a-fA-F]+$/.test(keyHex)) {
throw new Error('Invalid private key: must be 32 bytes (64 hex characters)')
}
const address = deriveAddress(keyHex)
saveGlobalConfig((config) => ({
...config,
x402PrivateKey: `0x${keyHex}`,
}))
saveX402Config({ address })
logForDebugging(`[x402] Wallet configured: ${address}`)
return address
}
/** Removes the private key and disables x402 */
export function removeX402PrivateKey(): void {
saveGlobalConfig((config) => {
const { x402PrivateKey: _, ...rest } = config as Record<string, unknown>
return rest as typeof config
})
saveX402Config({ enabled: false, address: undefined })
logForDebugging('[x402] Wallet removed')
}
/** Gets the wallet address without exposing the private key */
export function getX402WalletAddress(): string | undefined {
return getX402Config().address
}
/**
* Generates a new random private key for x402 payments.
* Returns the hex-encoded key (with 0x prefix).
*/
export function generateX402PrivateKey(): string {
return '0x' + randomBytes(32).toString('hex')
}
/** Updates the payment network */
export function setX402Network(network: PaymentNetwork): void {
saveX402Config({ network })
}
/** Updates max per-request payment limit */
export function setX402MaxPayment(amountUSD: number): void {
if (amountUSD <= 0) {
throw new Error('Max payment must be a positive number')
}
saveX402Config({ maxPaymentPerRequestUSD: amountUSD })
}
/** Updates max session spend limit */
export function setX402MaxSessionSpend(amountUSD: number): void {
if (amountUSD <= 0) {
throw new Error('Max session spend must be a positive number')
}
saveX402Config({ maxSessionSpendUSD: amountUSD })
}

View File

@@ -0,0 +1,59 @@
/**
* x402 Payment Service
*
* Public API for the x402 integration. Provides payment handling for
* HTTP 402 responses using the x402 protocol (USDC on Base).
*
* @see https://github.com/coinbase/x402
*/
// Configuration
export {
generateX402PrivateKey,
getX402Config,
getX402PrivateKey,
getX402WalletAddress,
isX402Enabled,
removeX402PrivateKey,
saveX402Config,
saveX402PrivateKey,
setX402MaxPayment,
setX402MaxSessionSpend,
setX402Network,
} from './config.js'
// Payment client
export {
createPayment,
encodePaymentHeader,
getFacilitatorUrl,
handlePaymentRequired,
parsePaymentRequirement,
validatePaymentRequirement,
} from './client.js'
// Fetch integration
export {
addX402AxiosInterceptor,
wrapFetchWithX402,
} from './paymentFetch.js'
// Cost tracking
export {
addX402Payment,
formatX402Cost,
getX402PaymentCount,
getX402SessionPayments,
getX402SessionSpentUSD,
resetX402SessionPayments,
} from './tracker.js'
// Types
export type {
PaymentNetwork,
PaymentPayload,
PaymentRequirement,
PaymentScheme,
X402PaymentRecord,
X402WalletConfig,
} from './types.js'

View File

@@ -0,0 +1,156 @@
/**
* x402 Payment Fetch Wrapper
*
* Wraps fetch (or axios) to automatically handle HTTP 402 Payment Required
* responses using the x402 protocol. When a 402 is received:
*
* 1. Parse the X-Payment-Required header
* 2. Validate against spending limits
* 3. Sign a payment authorization
* 4. Retry the request with the X-Payment header
*
* This integrates at the fetch level so it works transparently with
* both the Anthropic SDK client and the WebFetchTool.
*/
import type { AxiosInstance, AxiosResponse } from 'axios'
import { logForDebugging } from '../../utils/debug.js'
import { handlePaymentRequired } from './client.js'
import { isX402Enabled } from './config.js'
import { getX402SessionSpentUSD } from './tracker.js'
import { X402_HEADERS } from './types.js'
/**
* Create a fetch wrapper that intercepts 402 responses and handles x402 payment.
*
* Usage with the Anthropic SDK client:
* const wrappedFetch = wrapFetchWithX402(originalFetch)
* // Pass wrappedFetch as the `fetch` option to the SDK
*
* @param innerFetch - The underlying fetch function to wrap
* @returns A fetch-compatible function with x402 payment handling
*/
export function wrapFetchWithX402(
innerFetch: typeof globalThis.fetch,
): typeof globalThis.fetch {
return async (
input: RequestInfo | URL,
init?: RequestInit,
): Promise<Response> => {
// Make the initial request
const response = await innerFetch(input, init)
// If not a 402, pass through
if (response.status !== 402) {
return response
}
// Check if x402 is enabled
if (!isX402Enabled()) {
logForDebugging('[x402] Received 402 but x402 payments are not enabled')
return response
}
// Check for the x402 payment requirement header
const paymentRequiredHeader = response.headers.get(
X402_HEADERS.PAYMENT_REQUIRED,
)
if (!paymentRequiredHeader) {
logForDebugging(
'[x402] Received 402 but no X-Payment-Required header present',
)
return response
}
logForDebugging(`[x402] Received 402 Payment Required, processing...`)
// Handle the payment
const result = handlePaymentRequired(
paymentRequiredHeader,
getX402SessionSpentUSD(),
)
if (!result) {
logForDebugging('[x402] Payment handling failed, returning original 402')
return response
}
// Retry with payment header
logForDebugging('[x402] Retrying request with payment header')
const retryHeaders = new Headers(init?.headers)
retryHeaders.set(X402_HEADERS.PAYMENT, result.paymentHeader)
const retryResponse = await innerFetch(input, {
...init,
headers: retryHeaders,
})
if (retryResponse.status === 402) {
logForDebugging(
'[x402] Payment was rejected by server (still got 402)',
)
} else {
logForDebugging(
`[x402] Payment accepted, response status: ${retryResponse.status}`,
)
}
return retryResponse
}
}
/**
* Add x402 payment interceptor to an axios instance.
*
* Usage with WebFetchTool's axios:
* addX402AxiosInterceptor(axiosInstance)
*
* @param instance - The axios instance to add the interceptor to
*/
export function addX402AxiosInterceptor(instance: AxiosInstance): void {
instance.interceptors.response.use(
// Success handler — pass through non-402 responses
(response) => response,
// Error handler — intercept 402 responses
async (error) => {
if (
!error.response ||
error.response.status !== 402 ||
!isX402Enabled()
) {
throw error
}
const response: AxiosResponse = error.response
const paymentRequiredHeader =
response.headers[X402_HEADERS.PAYMENT_REQUIRED]
if (!paymentRequiredHeader) {
throw error
}
logForDebugging('[x402] Axios interceptor: handling 402 Payment Required')
const result = handlePaymentRequired(
paymentRequiredHeader,
getX402SessionSpentUSD(),
)
if (!result) {
throw error
}
// Retry the original request with the payment header
const retryConfig = {
...error.config,
headers: {
...error.config.headers,
[X402_HEADERS.PAYMENT]: result.paymentHeader,
},
}
return instance.request(retryConfig)
},
)
}

View File

@@ -0,0 +1,81 @@
/**
* x402 Payment Tracker
*
* Tracks x402 payments within a session for cost display and safety limits.
* Integrates with the main cost tracking system.
*/
import chalk from 'chalk'
import { logForDebugging } from '../../utils/debug.js'
import { formatNumber } from '../../utils/format.js'
import type { X402PaymentRecord } from './types.js'
/** In-memory payment records for the current session */
let sessionPayments: X402PaymentRecord[] = []
let sessionTotalUSD = 0
/** Add a payment record to the session tracker */
export function addX402Payment(record: X402PaymentRecord): void {
sessionPayments.push(record)
sessionTotalUSD += record.amountUSD
logForDebugging(
`[x402] Session total: $${sessionTotalUSD.toFixed(4)} (${sessionPayments.length} payments)`,
)
}
/** Get total USD spent via x402 in the current session */
export function getX402SessionSpentUSD(): number {
return sessionTotalUSD
}
/** Get all payment records for the current session */
export function getX402SessionPayments(): readonly X402PaymentRecord[] {
return sessionPayments
}
/** Get the count of payments in this session */
export function getX402PaymentCount(): number {
return sessionPayments.length
}
/** Reset session payment tracking (used on session switch) */
export function resetX402SessionPayments(): void {
sessionPayments = []
sessionTotalUSD = 0
}
/** Format x402 payment summary for display */
export function formatX402Cost(): string {
if (sessionPayments.length === 0) {
return ''
}
const lines: string[] = []
lines.push(
`x402 payments: $${sessionTotalUSD.toFixed(4)} (${sessionPayments.length} ${sessionPayments.length === 1 ? 'payment' : 'payments'})`,
)
// Group by resource domain
const byDomain: Record<string, { count: number; totalUSD: number }> = {}
for (const payment of sessionPayments) {
try {
const domain = new URL(payment.resource).hostname
if (!byDomain[domain]) {
byDomain[domain] = { count: 0, totalUSD: 0 }
}
byDomain[domain].count += payment.count ?? 1
byDomain[domain].totalUSD += payment.amountUSD
} catch {
// Skip malformed URLs
}
}
for (const [domain, stats] of Object.entries(byDomain)) {
lines.push(
`${domain}:`.padStart(21) +
` ${formatNumber(stats.count)} ${stats.count === 1 ? 'request' : 'requests'} ($${stats.totalUSD.toFixed(4)})`,
)
}
return chalk.dim(lines.join('\n'))
}