@phantom/openclaw-plugin 0.1.0 → 0.1.1

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/README.md CHANGED
@@ -6,6 +6,55 @@ Direct integration with Phantom wallet for OpenClaw agents. This plugin wraps th
6
6
 
7
7
  The Phantom OpenClaw Plugin provides native integration with Phantom wallet functionality. Instead of being a generic MCP bridge, it directly integrates the Phantom MCP Server tools as OpenClaw tools, providing a seamless experience for AI agents.
8
8
 
9
+ ## Quick Start
10
+
11
+ Get up and running in under 5 minutes:
12
+
13
+ ### Installation Checklist
14
+
15
+ - [ ] **Step 1:** Get your App ID from [phantom.com/portal](https://phantom.com/portal)
16
+ - Sign in with Gmail or Apple
17
+ - Click "Create App"
18
+ - Go to Dashboard → View App → Redirect URLs
19
+ - Add `http://localhost:8080/callback` as a redirect URL
20
+ - Navigate to "Phantom Connect" tab
21
+ - Copy your App ID
22
+
23
+ - [ ] **Step 2:** Install the plugin
24
+
25
+ ```bash
26
+ openclaw plugins install @phantom/openclaw-plugin
27
+ ```
28
+
29
+ - [ ] **Step 3:** Configure in `~/.openclaw/openclaw.json`
30
+
31
+ ```json
32
+ {
33
+ "plugins": {
34
+ "enabled": true,
35
+ "entries": {
36
+ "phantom-openclaw-plugin": {
37
+ "enabled": true,
38
+ "config": {
39
+ "PHANTOM_APP_ID": "your_app_id_from_portal"
40
+ }
41
+ }
42
+ }
43
+ }
44
+ }
45
+ ```
46
+
47
+ - [ ] **Step 4:** Restart OpenClaw
48
+
49
+ - [ ] **Step 5:** Test with your agent
50
+ ```text
51
+ Ask: "What are my Phantom wallet addresses?"
52
+ ```
53
+
54
+ **⚠️ Important:** Use the same email address for both the Phantom Portal and OpenClaw authentication!
55
+
56
+ See [Prerequisites](#prerequisites) below for detailed setup instructions.
57
+
9
58
  ## Features
10
59
 
11
60
  - **Direct Integration**: Built on top of `@phantom/mcp-server` for reliable wallet operations
@@ -20,7 +69,11 @@ Before using this plugin, you **must** obtain an App ID from the Phantom Portal:
20
69
  1. **Visit the Phantom Portal**: Go to [phantom.com/portal](https://phantom.com/portal)
21
70
  2. **Sign in**: Use your Gmail or Apple account to sign in
22
71
  3. **Create an App**: Click "Create App" and fill in the required details
23
- 4. **Get Your App ID**: Navigate to the "Phantom Connect" tab to find your App ID
72
+ 4. **Configure Redirect URL**:
73
+ - Navigate to Dashboard → View App → Redirect URLs
74
+ - Add `http://localhost:8080/callback` as a redirect URL
75
+ - This allows the OAuth callback to work correctly
76
+ 5. **Get Your App ID**: Navigate to the "Phantom Connect" tab to find your App ID
24
77
  - Your app is automatically approved for development use
25
78
  - Copy the App ID for the configuration below
26
79
 
@@ -88,6 +141,7 @@ Sign an arbitrary message with the Phantom wallet.
88
141
  **Parameters:**
89
142
 
90
143
  - `message` (string, required): The message to sign
144
+ - `networkId` (string, required): Network identifier (e.g., "solana:mainnet", "eip155:1")
91
145
  - `derivationIndex` (number, optional): Derivation index for the wallet (default: 0)
92
146
 
93
147
  **Example:**
@@ -95,6 +149,7 @@ Sign an arbitrary message with the Phantom wallet.
95
149
  ```json
96
150
  {
97
151
  "message": "Hello, Phantom!",
152
+ "networkId": "solana:mainnet",
98
153
  "derivationIndex": 0
99
154
  }
100
155
  ```
@@ -105,17 +160,18 @@ Sign a blockchain transaction.
105
160
 
106
161
  **Parameters:**
107
162
 
108
- - `transaction` (string, required): Base64-encoded transaction data
163
+ - `transaction` (string, required): The transaction to sign (format depends on chain: base64url for Solana, RLP-encoded hex for Ethereum)
164
+ - `networkId` (string, required): Network identifier (e.g., "solana:mainnet", "eip155:1" for Ethereum mainnet)
109
165
  - `derivationIndex` (number, optional): Derivation index for the wallet (default: 0)
110
- - `chain` (string, optional): Blockchain chain identifier
166
+ - `account` (string, optional): Specific account address to use for simulation/signing
111
167
 
112
168
  **Example:**
113
169
 
114
170
  ```json
115
171
  {
116
- "transaction": "base64-encoded-transaction-data",
117
- "derivationIndex": 0,
118
- "chain": "solana"
172
+ "transaction": "base64url-encoded-transaction-data",
173
+ "networkId": "solana:mainnet",
174
+ "derivationIndex": 0
119
175
  }
120
176
  ```
121
177
 
@@ -192,6 +248,36 @@ Fetch a Solana swap quote from Phantom's quotes API. Optionally execute the swap
192
248
 
193
249
  **⚠️ Warning:** When `execute: true`, this tool submits transactions immediately and irreversibly.
194
250
 
251
+ ## Network IDs Reference
252
+
253
+ Network identifiers follow the CAIP-2/CAIP-10 format. Here are the supported networks:
254
+
255
+ ### Solana
256
+
257
+ - Mainnet: `solana:mainnet`
258
+ - Devnet: `solana:devnet`
259
+ - Testnet: `solana:testnet`
260
+
261
+ ### Ethereum / EVM Chains
262
+
263
+ - Ethereum Mainnet: `eip155:1`
264
+ - Ethereum Sepolia: `eip155:11155111`
265
+ - Polygon Mainnet: `eip155:137`
266
+ - Polygon Amoy: `eip155:80002`
267
+ - Base Mainnet: `eip155:8453`
268
+ - Base Sepolia: `eip155:84532`
269
+ - Arbitrum One: `eip155:42161`
270
+ - Arbitrum Sepolia: `eip155:421614`
271
+
272
+ ### Bitcoin
273
+
274
+ - Mainnet: `bip122:000000000019d6689c085ae165831e93`
275
+
276
+ ### Sui
277
+
278
+ - Mainnet: `sui:mainnet`
279
+ - Testnet: `sui:testnet`
280
+
195
281
  ## Authentication
196
282
 
197
283
  On first use, the plugin will automatically initiate the Phantom OAuth flow:
@@ -0,0 +1,34 @@
1
+ /**
2
+ * OpenClaw Plugin API types
3
+ */
4
+ /**
5
+ * OpenClaw Plugin API interface
6
+ */
7
+ type OpenClawApi = {
8
+ /** Plugin configuration provided by OpenClaw */
9
+ config?: Record<string, unknown>;
10
+ /** Register a tool with OpenClaw */
11
+ registerTool: (definition: {
12
+ name: string;
13
+ description: string;
14
+ parameters: unknown;
15
+ execute: (id: string, params: Record<string, unknown>) => Promise<{
16
+ content: unknown;
17
+ isError?: boolean;
18
+ }>;
19
+ }) => void;
20
+ };
21
+
22
+ /**
23
+ * Phantom OpenClaw Plugin
24
+ *
25
+ * Integrates Phantom wallet operations directly with OpenClaw agents
26
+ * by wrapping the Phantom MCP Server tools.
27
+ */
28
+
29
+ /**
30
+ * Plugin registration function
31
+ */
32
+ declare function register(api: OpenClawApi): Promise<void>;
33
+
34
+ export { register as default };
package/dist/index.js ADDED
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ default: () => register
24
+ });
25
+ module.exports = __toCommonJS(src_exports);
26
+
27
+ // src/session.ts
28
+ var import_mcp_server = require("@phantom/mcp-server");
29
+ var PluginSession = class {
30
+ constructor(options = {}) {
31
+ this.initialized = false;
32
+ this.initializingPromise = null;
33
+ this.sessionManager = new import_mcp_server.SessionManager({
34
+ appId: options.appId ?? "phantom-openclaw",
35
+ callbackPort: options.callbackPort,
36
+ sessionDir: options.sessionDir
37
+ });
38
+ }
39
+ /**
40
+ * Initialize the session (authenticate if needed)
41
+ * Thread-safe: concurrent calls will await the same initialization promise
42
+ */
43
+ async initialize() {
44
+ if (this.initialized) {
45
+ return;
46
+ }
47
+ if (this.initializingPromise) {
48
+ return this.initializingPromise;
49
+ }
50
+ this.initializingPromise = this.sessionManager.initialize().then(() => {
51
+ this.initialized = true;
52
+ }).catch((error) => {
53
+ this.initializingPromise = null;
54
+ throw error;
55
+ });
56
+ return this.initializingPromise;
57
+ }
58
+ /**
59
+ * Get the authenticated PhantomClient
60
+ */
61
+ getClient() {
62
+ if (!this.initialized) {
63
+ throw new Error("Session not initialized. Call initialize() first.");
64
+ }
65
+ return this.sessionManager.getClient();
66
+ }
67
+ /**
68
+ * Get the current session data
69
+ */
70
+ getSession() {
71
+ if (!this.initialized) {
72
+ throw new Error("Session not initialized. Call initialize() first.");
73
+ }
74
+ return this.sessionManager.getSession();
75
+ }
76
+ };
77
+
78
+ // src/tools/register-tools.ts
79
+ var import_typebox = require("@sinclair/typebox");
80
+ var import_mcp_server2 = require("@phantom/mcp-server");
81
+ function convertSchema(mcpSchema) {
82
+ return import_typebox.Type.Object(
83
+ Object.fromEntries(Object.entries(mcpSchema.properties || {}).map(([key, _value]) => [key, import_typebox.Type.Unknown()]))
84
+ );
85
+ }
86
+ function registerPhantomTools(api, session) {
87
+ for (const mcpTool of import_mcp_server2.tools) {
88
+ api.registerTool({
89
+ name: mcpTool.name,
90
+ description: mcpTool.description,
91
+ parameters: convertSchema(mcpTool.inputSchema),
92
+ async execute(_id, params) {
93
+ const createLogger = (prefix) => ({
94
+ info: (msg) => console.info(`[${prefix}] ${msg}`),
95
+ // eslint-disable-line no-console
96
+ error: (msg) => console.error(`[${prefix}] ${msg}`),
97
+ // eslint-disable-line no-console
98
+ debug: (msg) => console.debug(`[${prefix}] ${msg}`),
99
+ // eslint-disable-line no-console
100
+ child: (name) => createLogger(`${prefix}:${name}`)
101
+ });
102
+ const context = {
103
+ client: session.getClient(),
104
+ session: session.getSession(),
105
+ logger: createLogger(mcpTool.name)
106
+ };
107
+ try {
108
+ const result = await mcpTool.handler(params, context);
109
+ const normalized = result ?? null;
110
+ return {
111
+ content: [
112
+ {
113
+ type: "text",
114
+ text: typeof normalized === "string" ? normalized : JSON.stringify(normalized, null, 2)
115
+ }
116
+ ]
117
+ };
118
+ } catch (error) {
119
+ const errorMessage = error instanceof Error ? error.message : String(error);
120
+ return {
121
+ content: [
122
+ {
123
+ type: "text",
124
+ text: JSON.stringify({ error: errorMessage }, null, 2)
125
+ }
126
+ ],
127
+ isError: true
128
+ };
129
+ }
130
+ }
131
+ });
132
+ }
133
+ }
134
+
135
+ // src/index.ts
136
+ var sessionInstance = null;
137
+ function getSession(config) {
138
+ if (!sessionInstance) {
139
+ const appId = typeof config?.PHANTOM_APP_ID === "string" ? config.PHANTOM_APP_ID : void 0;
140
+ let callbackPort;
141
+ if (typeof config?.PHANTOM_CALLBACK_PORT === "number") {
142
+ callbackPort = config.PHANTOM_CALLBACK_PORT;
143
+ } else if (typeof config?.PHANTOM_CALLBACK_PORT === "string") {
144
+ const parsed = parseInt(config.PHANTOM_CALLBACK_PORT, 10);
145
+ if (!isNaN(parsed) && parsed > 0 && parsed <= 65535) {
146
+ callbackPort = parsed;
147
+ }
148
+ }
149
+ sessionInstance = new PluginSession({
150
+ appId,
151
+ callbackPort
152
+ });
153
+ }
154
+ return sessionInstance;
155
+ }
156
+ function resetSession() {
157
+ sessionInstance = null;
158
+ }
159
+ async function register(api) {
160
+ try {
161
+ const session = getSession(api.config);
162
+ await session.initialize();
163
+ registerPhantomTools(api, session);
164
+ } catch (error) {
165
+ console.error("Failed to initialize Phantom OpenClaw plugin:", error);
166
+ resetSession();
167
+ throw error;
168
+ }
169
+ }
170
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/session.ts","../src/tools/register-tools.ts"],"sourcesContent":["/**\n * Phantom OpenClaw Plugin\n *\n * Integrates Phantom wallet operations directly with OpenClaw agents\n * by wrapping the Phantom MCP Server tools.\n */\n\nimport type { OpenClawApi } from \"./client/types.js\";\nimport { PluginSession } from \"./session.js\";\nimport { registerPhantomTools } from \"./tools/register-tools.js\";\n\n// Singleton session instance\nlet sessionInstance: PluginSession | null = null;\n\n/**\n * Get or create the plugin session with configuration\n */\nfunction getSession(config?: Record<string, unknown>): PluginSession {\n if (!sessionInstance) {\n // Extract configuration from OpenClaw API config\n const appId = typeof config?.PHANTOM_APP_ID === \"string\" ? config.PHANTOM_APP_ID : undefined;\n\n // Parse callback port - accept both number and numeric string\n let callbackPort: number | undefined;\n if (typeof config?.PHANTOM_CALLBACK_PORT === \"number\") {\n callbackPort = config.PHANTOM_CALLBACK_PORT;\n } else if (typeof config?.PHANTOM_CALLBACK_PORT === \"string\") {\n const parsed = parseInt(config.PHANTOM_CALLBACK_PORT, 10);\n if (!isNaN(parsed) && parsed > 0 && parsed <= 65535) {\n callbackPort = parsed;\n }\n }\n\n sessionInstance = new PluginSession({\n appId,\n callbackPort,\n });\n }\n return sessionInstance;\n}\n\n/**\n * Reset the session singleton (used for cleanup on initialization failure)\n */\nfunction resetSession(): void {\n sessionInstance = null;\n}\n\n/**\n * Plugin registration function\n */\nexport default async function register(api: OpenClawApi) {\n try {\n // Initialize session (authenticate if needed)\n const session = getSession(api.config);\n await session.initialize();\n\n // Register all Phantom MCP tools\n registerPhantomTools(api, session);\n } catch (error) {\n console.error(\"Failed to initialize Phantom OpenClaw plugin:\", error); // eslint-disable-line no-console\n // Reset singleton so next attempt gets a fresh instance\n resetSession();\n throw error;\n }\n}\n","/**\n * Session management for Phantom OpenClaw plugin\n * Wraps the SessionManager from @phantom/mcp-server\n */\n\nimport { SessionManager } from \"@phantom/mcp-server\";\nimport type { PhantomClient, SessionData } from \"@phantom/mcp-server\";\n\n/**\n * Configuration options for PluginSession\n */\nexport interface PluginSessionOptions {\n /** Application identifier from Phantom Portal */\n appId?: string;\n /** OAuth callback port (default: 8080) */\n callbackPort?: number;\n /** Directory to store session data (default: ~/.phantom-mcp) */\n sessionDir?: string;\n}\n\n/**\n * Plugin session manager\n * Handles authentication and provides access to PhantomClient\n */\nexport class PluginSession {\n private sessionManager: SessionManager;\n private initialized = false;\n private initializingPromise: Promise<void> | null = null;\n\n constructor(options: PluginSessionOptions = {}) {\n // Initialize SessionManager with configuration\n this.sessionManager = new SessionManager({\n appId: options.appId ?? \"phantom-openclaw\",\n callbackPort: options.callbackPort,\n sessionDir: options.sessionDir,\n });\n }\n\n /**\n * Initialize the session (authenticate if needed)\n * Thread-safe: concurrent calls will await the same initialization promise\n */\n async initialize(): Promise<void> {\n if (this.initialized) {\n return;\n }\n\n // If already initializing, return the existing promise\n if (this.initializingPromise) {\n return this.initializingPromise;\n }\n\n // Create and store the initialization promise\n this.initializingPromise = this.sessionManager\n .initialize()\n .then(() => {\n this.initialized = true;\n })\n .catch(error => {\n // Clear promise on error so subsequent calls can retry\n this.initializingPromise = null;\n throw error;\n });\n\n return this.initializingPromise;\n }\n\n /**\n * Get the authenticated PhantomClient\n */\n getClient(): PhantomClient {\n if (!this.initialized) {\n throw new Error(\"Session not initialized. Call initialize() first.\");\n }\n return this.sessionManager.getClient();\n }\n\n /**\n * Get the current session data\n */\n getSession(): SessionData {\n if (!this.initialized) {\n throw new Error(\"Session not initialized. Call initialize() first.\");\n }\n return this.sessionManager.getSession();\n }\n}\n","/**\n * Register Phantom MCP tools as OpenClaw tools\n */\n\nimport { Type } from \"@sinclair/typebox\";\nimport { tools } from \"@phantom/mcp-server\";\nimport type { OpenClawApi } from \"../client/types.js\";\nimport type { PluginSession } from \"../session.js\";\n\n/**\n * Convert MCP tool JSON schema to TypeBox schema\n */\nfunction convertSchema(mcpSchema: any): any {\n // For now, use Type.Unknown() - we could do more sophisticated conversion\n return Type.Object(\n Object.fromEntries(Object.entries(mcpSchema.properties || {}).map(([key, _value]) => [key, Type.Unknown()])),\n );\n}\n\n/**\n * Register all Phantom MCP tools with OpenClaw\n */\nexport function registerPhantomTools(api: OpenClawApi, session: PluginSession): void {\n for (const mcpTool of tools) {\n api.registerTool({\n name: mcpTool.name,\n description: mcpTool.description,\n parameters: convertSchema(mcpTool.inputSchema),\n async execute(_id: string, params: Record<string, unknown>) {\n // Create tool context for MCP tool with recursive logger\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const createLogger = (prefix: string): any => ({\n info: (msg: string) => console.info(`[${prefix}] ${msg}`), // eslint-disable-line no-console\n error: (msg: string) => console.error(`[${prefix}] ${msg}`), // eslint-disable-line no-console\n debug: (msg: string) => console.debug(`[${prefix}] ${msg}`), // eslint-disable-line no-console\n child: (name: string) => createLogger(`${prefix}:${name}`),\n });\n\n const context = {\n client: session.getClient(),\n session: session.getSession(),\n logger: createLogger(mcpTool.name),\n };\n\n try {\n // Execute the MCP tool handler\n const result = await mcpTool.handler(params, context);\n\n // Return in OpenClaw format with defensive handling for undefined\n const normalized = result ?? null;\n return {\n content: [\n {\n type: \"text\" as const,\n text: typeof normalized === \"string\" ? normalized : JSON.stringify(normalized, null, 2),\n },\n ],\n };\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify({ error: errorMessage }, null, 2),\n },\n ],\n isError: true,\n };\n }\n },\n });\n }\n}\n"],"mappings":"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","names":["import_mcp_server"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,147 @@
1
+ // src/session.ts
2
+ import { SessionManager } from "@phantom/mcp-server";
3
+ var PluginSession = class {
4
+ constructor(options = {}) {
5
+ this.initialized = false;
6
+ this.initializingPromise = null;
7
+ this.sessionManager = new SessionManager({
8
+ appId: options.appId ?? "phantom-openclaw",
9
+ callbackPort: options.callbackPort,
10
+ sessionDir: options.sessionDir
11
+ });
12
+ }
13
+ /**
14
+ * Initialize the session (authenticate if needed)
15
+ * Thread-safe: concurrent calls will await the same initialization promise
16
+ */
17
+ async initialize() {
18
+ if (this.initialized) {
19
+ return;
20
+ }
21
+ if (this.initializingPromise) {
22
+ return this.initializingPromise;
23
+ }
24
+ this.initializingPromise = this.sessionManager.initialize().then(() => {
25
+ this.initialized = true;
26
+ }).catch((error) => {
27
+ this.initializingPromise = null;
28
+ throw error;
29
+ });
30
+ return this.initializingPromise;
31
+ }
32
+ /**
33
+ * Get the authenticated PhantomClient
34
+ */
35
+ getClient() {
36
+ if (!this.initialized) {
37
+ throw new Error("Session not initialized. Call initialize() first.");
38
+ }
39
+ return this.sessionManager.getClient();
40
+ }
41
+ /**
42
+ * Get the current session data
43
+ */
44
+ getSession() {
45
+ if (!this.initialized) {
46
+ throw new Error("Session not initialized. Call initialize() first.");
47
+ }
48
+ return this.sessionManager.getSession();
49
+ }
50
+ };
51
+
52
+ // src/tools/register-tools.ts
53
+ import { Type } from "@sinclair/typebox";
54
+ import { tools } from "@phantom/mcp-server";
55
+ function convertSchema(mcpSchema) {
56
+ return Type.Object(
57
+ Object.fromEntries(Object.entries(mcpSchema.properties || {}).map(([key, _value]) => [key, Type.Unknown()]))
58
+ );
59
+ }
60
+ function registerPhantomTools(api, session) {
61
+ for (const mcpTool of tools) {
62
+ api.registerTool({
63
+ name: mcpTool.name,
64
+ description: mcpTool.description,
65
+ parameters: convertSchema(mcpTool.inputSchema),
66
+ async execute(_id, params) {
67
+ const createLogger = (prefix) => ({
68
+ info: (msg) => console.info(`[${prefix}] ${msg}`),
69
+ // eslint-disable-line no-console
70
+ error: (msg) => console.error(`[${prefix}] ${msg}`),
71
+ // eslint-disable-line no-console
72
+ debug: (msg) => console.debug(`[${prefix}] ${msg}`),
73
+ // eslint-disable-line no-console
74
+ child: (name) => createLogger(`${prefix}:${name}`)
75
+ });
76
+ const context = {
77
+ client: session.getClient(),
78
+ session: session.getSession(),
79
+ logger: createLogger(mcpTool.name)
80
+ };
81
+ try {
82
+ const result = await mcpTool.handler(params, context);
83
+ const normalized = result ?? null;
84
+ return {
85
+ content: [
86
+ {
87
+ type: "text",
88
+ text: typeof normalized === "string" ? normalized : JSON.stringify(normalized, null, 2)
89
+ }
90
+ ]
91
+ };
92
+ } catch (error) {
93
+ const errorMessage = error instanceof Error ? error.message : String(error);
94
+ return {
95
+ content: [
96
+ {
97
+ type: "text",
98
+ text: JSON.stringify({ error: errorMessage }, null, 2)
99
+ }
100
+ ],
101
+ isError: true
102
+ };
103
+ }
104
+ }
105
+ });
106
+ }
107
+ }
108
+
109
+ // src/index.ts
110
+ var sessionInstance = null;
111
+ function getSession(config) {
112
+ if (!sessionInstance) {
113
+ const appId = typeof config?.PHANTOM_APP_ID === "string" ? config.PHANTOM_APP_ID : void 0;
114
+ let callbackPort;
115
+ if (typeof config?.PHANTOM_CALLBACK_PORT === "number") {
116
+ callbackPort = config.PHANTOM_CALLBACK_PORT;
117
+ } else if (typeof config?.PHANTOM_CALLBACK_PORT === "string") {
118
+ const parsed = parseInt(config.PHANTOM_CALLBACK_PORT, 10);
119
+ if (!isNaN(parsed) && parsed > 0 && parsed <= 65535) {
120
+ callbackPort = parsed;
121
+ }
122
+ }
123
+ sessionInstance = new PluginSession({
124
+ appId,
125
+ callbackPort
126
+ });
127
+ }
128
+ return sessionInstance;
129
+ }
130
+ function resetSession() {
131
+ sessionInstance = null;
132
+ }
133
+ async function register(api) {
134
+ try {
135
+ const session = getSession(api.config);
136
+ await session.initialize();
137
+ registerPhantomTools(api, session);
138
+ } catch (error) {
139
+ console.error("Failed to initialize Phantom OpenClaw plugin:", error);
140
+ resetSession();
141
+ throw error;
142
+ }
143
+ }
144
+ export {
145
+ register as default
146
+ };
147
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/session.ts","../src/tools/register-tools.ts","../src/index.ts"],"sourcesContent":["/**\n * Session management for Phantom OpenClaw plugin\n * Wraps the SessionManager from @phantom/mcp-server\n */\n\nimport { SessionManager } from \"@phantom/mcp-server\";\nimport type { PhantomClient, SessionData } from \"@phantom/mcp-server\";\n\n/**\n * Configuration options for PluginSession\n */\nexport interface PluginSessionOptions {\n /** Application identifier from Phantom Portal */\n appId?: string;\n /** OAuth callback port (default: 8080) */\n callbackPort?: number;\n /** Directory to store session data (default: ~/.phantom-mcp) */\n sessionDir?: string;\n}\n\n/**\n * Plugin session manager\n * Handles authentication and provides access to PhantomClient\n */\nexport class PluginSession {\n private sessionManager: SessionManager;\n private initialized = false;\n private initializingPromise: Promise<void> | null = null;\n\n constructor(options: PluginSessionOptions = {}) {\n // Initialize SessionManager with configuration\n this.sessionManager = new SessionManager({\n appId: options.appId ?? \"phantom-openclaw\",\n callbackPort: options.callbackPort,\n sessionDir: options.sessionDir,\n });\n }\n\n /**\n * Initialize the session (authenticate if needed)\n * Thread-safe: concurrent calls will await the same initialization promise\n */\n async initialize(): Promise<void> {\n if (this.initialized) {\n return;\n }\n\n // If already initializing, return the existing promise\n if (this.initializingPromise) {\n return this.initializingPromise;\n }\n\n // Create and store the initialization promise\n this.initializingPromise = this.sessionManager\n .initialize()\n .then(() => {\n this.initialized = true;\n })\n .catch(error => {\n // Clear promise on error so subsequent calls can retry\n this.initializingPromise = null;\n throw error;\n });\n\n return this.initializingPromise;\n }\n\n /**\n * Get the authenticated PhantomClient\n */\n getClient(): PhantomClient {\n if (!this.initialized) {\n throw new Error(\"Session not initialized. Call initialize() first.\");\n }\n return this.sessionManager.getClient();\n }\n\n /**\n * Get the current session data\n */\n getSession(): SessionData {\n if (!this.initialized) {\n throw new Error(\"Session not initialized. Call initialize() first.\");\n }\n return this.sessionManager.getSession();\n }\n}\n","/**\n * Register Phantom MCP tools as OpenClaw tools\n */\n\nimport { Type } from \"@sinclair/typebox\";\nimport { tools } from \"@phantom/mcp-server\";\nimport type { OpenClawApi } from \"../client/types.js\";\nimport type { PluginSession } from \"../session.js\";\n\n/**\n * Convert MCP tool JSON schema to TypeBox schema\n */\nfunction convertSchema(mcpSchema: any): any {\n // For now, use Type.Unknown() - we could do more sophisticated conversion\n return Type.Object(\n Object.fromEntries(Object.entries(mcpSchema.properties || {}).map(([key, _value]) => [key, Type.Unknown()])),\n );\n}\n\n/**\n * Register all Phantom MCP tools with OpenClaw\n */\nexport function registerPhantomTools(api: OpenClawApi, session: PluginSession): void {\n for (const mcpTool of tools) {\n api.registerTool({\n name: mcpTool.name,\n description: mcpTool.description,\n parameters: convertSchema(mcpTool.inputSchema),\n async execute(_id: string, params: Record<string, unknown>) {\n // Create tool context for MCP tool with recursive logger\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const createLogger = (prefix: string): any => ({\n info: (msg: string) => console.info(`[${prefix}] ${msg}`), // eslint-disable-line no-console\n error: (msg: string) => console.error(`[${prefix}] ${msg}`), // eslint-disable-line no-console\n debug: (msg: string) => console.debug(`[${prefix}] ${msg}`), // eslint-disable-line no-console\n child: (name: string) => createLogger(`${prefix}:${name}`),\n });\n\n const context = {\n client: session.getClient(),\n session: session.getSession(),\n logger: createLogger(mcpTool.name),\n };\n\n try {\n // Execute the MCP tool handler\n const result = await mcpTool.handler(params, context);\n\n // Return in OpenClaw format with defensive handling for undefined\n const normalized = result ?? null;\n return {\n content: [\n {\n type: \"text\" as const,\n text: typeof normalized === \"string\" ? normalized : JSON.stringify(normalized, null, 2),\n },\n ],\n };\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify({ error: errorMessage }, null, 2),\n },\n ],\n isError: true,\n };\n }\n },\n });\n }\n}\n","/**\n * Phantom OpenClaw Plugin\n *\n * Integrates Phantom wallet operations directly with OpenClaw agents\n * by wrapping the Phantom MCP Server tools.\n */\n\nimport type { OpenClawApi } from \"./client/types.js\";\nimport { PluginSession } from \"./session.js\";\nimport { registerPhantomTools } from \"./tools/register-tools.js\";\n\n// Singleton session instance\nlet sessionInstance: PluginSession | null = null;\n\n/**\n * Get or create the plugin session with configuration\n */\nfunction getSession(config?: Record<string, unknown>): PluginSession {\n if (!sessionInstance) {\n // Extract configuration from OpenClaw API config\n const appId = typeof config?.PHANTOM_APP_ID === \"string\" ? config.PHANTOM_APP_ID : undefined;\n\n // Parse callback port - accept both number and numeric string\n let callbackPort: number | undefined;\n if (typeof config?.PHANTOM_CALLBACK_PORT === \"number\") {\n callbackPort = config.PHANTOM_CALLBACK_PORT;\n } else if (typeof config?.PHANTOM_CALLBACK_PORT === \"string\") {\n const parsed = parseInt(config.PHANTOM_CALLBACK_PORT, 10);\n if (!isNaN(parsed) && parsed > 0 && parsed <= 65535) {\n callbackPort = parsed;\n }\n }\n\n sessionInstance = new PluginSession({\n appId,\n callbackPort,\n });\n }\n return sessionInstance;\n}\n\n/**\n * Reset the session singleton (used for cleanup on initialization failure)\n */\nfunction resetSession(): void {\n sessionInstance = null;\n}\n\n/**\n * Plugin registration function\n */\nexport default async function register(api: OpenClawApi) {\n try {\n // Initialize session (authenticate if needed)\n const session = getSession(api.config);\n await session.initialize();\n\n // Register all Phantom MCP tools\n registerPhantomTools(api, session);\n } catch (error) {\n console.error(\"Failed to initialize Phantom OpenClaw plugin:\", error); // eslint-disable-line no-console\n // Reset singleton so next attempt gets a fresh instance\n resetSession();\n throw error;\n }\n}\n"],"mappings":";AAKA,SAAS,sBAAsB;AAmBxB,IAAM,gBAAN,MAAoB;AAAA,EAKzB,YAAY,UAAgC,CAAC,GAAG;AAHhD,SAAQ,cAAc;AACtB,SAAQ,sBAA4C;AAIlD,SAAK,iBAAiB,IAAI,eAAe;AAAA,MACvC,OAAO,QAAQ,SAAS;AAAA,MACxB,cAAc,QAAQ;AAAA,MACtB,YAAY,QAAQ;AAAA,IACtB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,aAA4B;AAChC,QAAI,KAAK,aAAa;AACpB;AAAA,IACF;AAGA,QAAI,KAAK,qBAAqB;AAC5B,aAAO,KAAK;AAAA,IACd;AAGA,SAAK,sBAAsB,KAAK,eAC7B,WAAW,EACX,KAAK,MAAM;AACV,WAAK,cAAc;AAAA,IACrB,CAAC,EACA,MAAM,WAAS;AAEd,WAAK,sBAAsB;AAC3B,YAAM;AAAA,IACR,CAAC;AAEH,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,YAA2B;AACzB,QAAI,CAAC,KAAK,aAAa;AACrB,YAAM,IAAI,MAAM,mDAAmD;AAAA,IACrE;AACA,WAAO,KAAK,eAAe,UAAU;AAAA,EACvC;AAAA;AAAA;AAAA;AAAA,EAKA,aAA0B;AACxB,QAAI,CAAC,KAAK,aAAa;AACrB,YAAM,IAAI,MAAM,mDAAmD;AAAA,IACrE;AACA,WAAO,KAAK,eAAe,WAAW;AAAA,EACxC;AACF;;;AClFA,SAAS,YAAY;AACrB,SAAS,aAAa;AAOtB,SAAS,cAAc,WAAqB;AAE1C,SAAO,KAAK;AAAA,IACV,OAAO,YAAY,OAAO,QAAQ,UAAU,cAAc,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,KAAK,MAAM,MAAM,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC;AAAA,EAC7G;AACF;AAKO,SAAS,qBAAqB,KAAkB,SAA8B;AACnF,aAAW,WAAW,OAAO;AAC3B,QAAI,aAAa;AAAA,MACf,MAAM,QAAQ;AAAA,MACd,aAAa,QAAQ;AAAA,MACrB,YAAY,cAAc,QAAQ,WAAW;AAAA,MAC7C,MAAM,QAAQ,KAAa,QAAiC;AAG1D,cAAM,eAAe,CAAC,YAAyB;AAAA,UAC7C,MAAM,CAAC,QAAgB,QAAQ,KAAK,IAAI,WAAW,KAAK;AAAA;AAAA,UACxD,OAAO,CAAC,QAAgB,QAAQ,MAAM,IAAI,WAAW,KAAK;AAAA;AAAA,UAC1D,OAAO,CAAC,QAAgB,QAAQ,MAAM,IAAI,WAAW,KAAK;AAAA;AAAA,UAC1D,OAAO,CAAC,SAAiB,aAAa,GAAG,UAAU,MAAM;AAAA,QAC3D;AAEA,cAAM,UAAU;AAAA,UACd,QAAQ,QAAQ,UAAU;AAAA,UAC1B,SAAS,QAAQ,WAAW;AAAA,UAC5B,QAAQ,aAAa,QAAQ,IAAI;AAAA,QACnC;AAEA,YAAI;AAEF,gBAAM,SAAS,MAAM,QAAQ,QAAQ,QAAQ,OAAO;AAGpD,gBAAM,aAAa,UAAU;AAC7B,iBAAO;AAAA,YACL,SAAS;AAAA,cACP;AAAA,gBACE,MAAM;AAAA,gBACN,MAAM,OAAO,eAAe,WAAW,aAAa,KAAK,UAAU,YAAY,MAAM,CAAC;AAAA,cACxF;AAAA,YACF;AAAA,UACF;AAAA,QACF,SAAS,OAAP;AACA,gBAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC1E,iBAAO;AAAA,YACL,SAAS;AAAA,cACP;AAAA,gBACE,MAAM;AAAA,gBACN,MAAM,KAAK,UAAU,EAAE,OAAO,aAAa,GAAG,MAAM,CAAC;AAAA,cACvD;AAAA,YACF;AAAA,YACA,SAAS;AAAA,UACX;AAAA,QACF;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AACF;;;AC7DA,IAAI,kBAAwC;AAK5C,SAAS,WAAW,QAAiD;AACnE,MAAI,CAAC,iBAAiB;AAEpB,UAAM,QAAQ,OAAO,QAAQ,mBAAmB,WAAW,OAAO,iBAAiB;AAGnF,QAAI;AACJ,QAAI,OAAO,QAAQ,0BAA0B,UAAU;AACrD,qBAAe,OAAO;AAAA,IACxB,WAAW,OAAO,QAAQ,0BAA0B,UAAU;AAC5D,YAAM,SAAS,SAAS,OAAO,uBAAuB,EAAE;AACxD,UAAI,CAAC,MAAM,MAAM,KAAK,SAAS,KAAK,UAAU,OAAO;AACnD,uBAAe;AAAA,MACjB;AAAA,IACF;AAEA,sBAAkB,IAAI,cAAc;AAAA,MAClC;AAAA,MACA;AAAA,IACF,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAKA,SAAS,eAAqB;AAC5B,oBAAkB;AACpB;AAKA,eAAO,SAAgC,KAAkB;AACvD,MAAI;AAEF,UAAM,UAAU,WAAW,IAAI,MAAM;AACrC,UAAM,QAAQ,WAAW;AAGzB,yBAAqB,KAAK,OAAO;AAAA,EACnC,SAAS,OAAP;AACA,YAAQ,MAAM,iDAAiD,KAAK;AAEpE,iBAAa;AACb,UAAM;AAAA,EACR;AACF;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phantom/openclaw-plugin",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "OpenClaw plugin that bridges tool calls to Phantom's MCP server for wallet operations.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -41,7 +41,7 @@
41
41
  "typescript": "^5.0.4"
42
42
  },
43
43
  "dependencies": {
44
- "@phantom/mcp-server": "workspace:^",
44
+ "@phantom/mcp-server": "^0.1.2",
45
45
  "@sinclair/typebox": "^0.32.0"
46
46
  },
47
47
  "files": [
@@ -52,4 +52,4 @@
52
52
  "publishConfig": {
53
53
  "directory": "_release/package"
54
54
  }
55
- }
55
+ }