@pnlmarket/mcp-server 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +216 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +83 -0
- package/dist/install.d.ts +1 -0
- package/dist/install.js +168 -0
- package/dist/lib/output.d.ts +95 -0
- package/dist/lib/output.js +175 -0
- package/dist/lib/passphrase.d.ts +16 -0
- package/dist/lib/passphrase.js +57 -0
- package/dist/lib/pnl-api.d.ts +65 -0
- package/dist/lib/pnl-api.js +89 -0
- package/dist/lib/sign.d.ts +40 -0
- package/dist/lib/sign.js +126 -0
- package/dist/lib/wallet.d.ts +74 -0
- package/dist/lib/wallet.js +405 -0
- package/dist/tools/browse-markets.d.ts +12 -0
- package/dist/tools/browse-markets.js +91 -0
- package/dist/tools/claim-now.d.ts +10 -0
- package/dist/tools/claim-now.js +113 -0
- package/dist/tools/claim.d.ts +10 -0
- package/dist/tools/claim.js +43 -0
- package/dist/tools/export-keypair.d.ts +10 -0
- package/dist/tools/export-keypair.js +25 -0
- package/dist/tools/get-market.d.ts +10 -0
- package/dist/tools/get-market.js +58 -0
- package/dist/tools/help.d.ts +7 -0
- package/dist/tools/help.js +54 -0
- package/dist/tools/init.d.ts +7 -0
- package/dist/tools/init.js +69 -0
- package/dist/tools/notify.d.ts +12 -0
- package/dist/tools/notify.js +150 -0
- package/dist/tools/pitch-idea.d.ts +38 -0
- package/dist/tools/pitch-idea.js +176 -0
- package/dist/tools/pitch-now.d.ts +39 -0
- package/dist/tools/pitch-now.js +179 -0
- package/dist/tools/restore.d.ts +11 -0
- package/dist/tools/restore.js +45 -0
- package/dist/tools/set-username.d.ts +10 -0
- package/dist/tools/set-username.js +87 -0
- package/dist/tools/unlock.d.ts +17 -0
- package/dist/tools/unlock.js +47 -0
- package/dist/tools/vote-now.d.ts +13 -0
- package/dist/tools/vote-now.js +146 -0
- package/dist/tools/vote.d.ts +12 -0
- package/dist/tools/vote.js +49 -0
- package/dist/tools/wallet.d.ts +7 -0
- package/dist/tools/wallet.js +40 -0
- package/package.json +64 -0
- package/skills/README.md +45 -0
- package/skills/pnl-browse/SKILL.md +30 -0
- package/skills/pnl-claim/SKILL.md +60 -0
- package/skills/pnl-claim-now/SKILL.md +67 -0
- package/skills/pnl-export/SKILL.md +39 -0
- package/skills/pnl-help/SKILL.md +17 -0
- package/skills/pnl-init/SKILL.md +24 -0
- package/skills/pnl-lock/SKILL.md +17 -0
- package/skills/pnl-name/SKILL.md +32 -0
- package/skills/pnl-notify/SKILL.md +57 -0
- package/skills/pnl-pitch/SKILL.md +83 -0
- package/skills/pnl-pitch-now/SKILL.md +88 -0
- package/skills/pnl-restore/SKILL.md +38 -0
- package/skills/pnl-unlock/SKILL.md +28 -0
- package/skills/pnl-vote/SKILL.md +48 -0
- package/skills/pnl-vote-now/SKILL.md +68 -0
- package/skills/pnl-wallet/SKILL.md +22 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getMarket } from '../lib/pnl-api.js';
|
|
3
|
+
import { Badge, headline, code, kvTable, next, reply } from '../lib/output.js';
|
|
4
|
+
// ─── pnl_claim ──────────────────────────────────────────────────
|
|
5
|
+
//
|
|
6
|
+
// Deep-link mode. Returns the /market/<id>?claim=1 URL — the market
|
|
7
|
+
// detail page detects the query param, opens the claim panel, and
|
|
8
|
+
// the user signs in their browser wallet.
|
|
9
|
+
//
|
|
10
|
+
// For autosign (no browser), use pnl_claim_now.
|
|
11
|
+
export const claimInputSchema = {
|
|
12
|
+
marketId: z
|
|
13
|
+
.string()
|
|
14
|
+
.min(1)
|
|
15
|
+
.describe("Market id from pnl_browse_markets or your wallet's history. Accepts the Mongo id or the on-chain market address. The market must be resolved (YES wins, NO wins, or refund) and the wallet must have an unclaimed position."),
|
|
16
|
+
};
|
|
17
|
+
const ClaimInput = z.object(claimInputSchema);
|
|
18
|
+
function getApiBase() {
|
|
19
|
+
const raw = process.env.PNL_API_BASE_URL?.trim();
|
|
20
|
+
if (!raw)
|
|
21
|
+
return 'https://pnl.market';
|
|
22
|
+
return raw.endsWith('/') ? raw.slice(0, -1) : raw;
|
|
23
|
+
}
|
|
24
|
+
export async function callClaim(rawInput) {
|
|
25
|
+
const { marketId } = ClaimInput.parse(rawInput ?? {});
|
|
26
|
+
const base = getApiBase();
|
|
27
|
+
// Quick sanity fetch — surface the resolution + token mint info in
|
|
28
|
+
// the reply so the user knows what they're about to claim.
|
|
29
|
+
let market;
|
|
30
|
+
try {
|
|
31
|
+
market = await getMarket(marketId);
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
return reply(headline(`${Badge.err} Couldn't load market \`${marketId}\``), `Check the id with \`pnl_browse_markets\`. Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
35
|
+
}
|
|
36
|
+
const url = `${base}/market/${encodeURIComponent(marketId)}?claim=1`;
|
|
37
|
+
const resLabel = market.resolution || market.status || 'unknown';
|
|
38
|
+
return reply(headline(`${Badge.draft} Claim ready — ${market.name ?? marketId}`), `Open this URL to confirm and sign in your browser wallet:`, code(url), kvTable([
|
|
39
|
+
['Market', market.name ?? marketId],
|
|
40
|
+
['Resolution', resLabel],
|
|
41
|
+
['Phase', market.phase ?? '—'],
|
|
42
|
+
]), `The market detail page detects \`?claim=1\` and opens the claim panel. Sign the \`claim_rewards\` tx in your wallet — confirms in ~5-15s.`, next('Open the URL in a browser to sign.'));
|
|
43
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const exportKeypairInputSchema: {
|
|
3
|
+
readonly confirm: z.ZodLiteral<"EXPORT">;
|
|
4
|
+
};
|
|
5
|
+
export declare function callExportKeypair(rawInput: unknown): Promise<{
|
|
6
|
+
content: Array<{
|
|
7
|
+
type: "text";
|
|
8
|
+
text: string;
|
|
9
|
+
}>;
|
|
10
|
+
}>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { hasWallet, isUnlocked, exportToFile } from '../lib/wallet.js';
|
|
3
|
+
import { Badge, headline, code, inline, next, reply, quote, truncAddress } from '../lib/output.js';
|
|
4
|
+
export const exportKeypairInputSchema = {
|
|
5
|
+
confirm: z
|
|
6
|
+
.literal('EXPORT')
|
|
7
|
+
.describe('Must be the literal string "EXPORT". Deliberate friction step — the user has to explicitly ask before the agent will dump the secret.'),
|
|
8
|
+
};
|
|
9
|
+
const ExportKeypairInput = z.object(exportKeypairInputSchema);
|
|
10
|
+
export async function callExportKeypair(rawInput) {
|
|
11
|
+
ExportKeypairInput.parse(rawInput ?? {});
|
|
12
|
+
if (!hasWallet()) {
|
|
13
|
+
return reply(headline(`${Badge.warn} No PNL wallet to export.`), `Run ${inline('/pnl-init')} first.`);
|
|
14
|
+
}
|
|
15
|
+
if (!isUnlocked()) {
|
|
16
|
+
return reply(headline(`${Badge.locked} Wallet is locked.`), `Export needs the secret in memory. Run \`/pnl-unlock\` first — passphrase comes from \`PNL_PASSPHRASE\` env or OS-native dialog.`, next('`/pnl-unlock`, then `/pnl-export`.'));
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const { path, address } = exportToFile();
|
|
20
|
+
return reply(headline(`${Badge.ok} Exported · ${truncAddress(address)}`), `**File:**`, code(path), 'Open the file, copy the contents into your password manager (it has both base58 for Phantom-import and the Solana CLI JSON array), then delete:', code(`rm '${path}'`, 'bash'), quote('Anyone who reads this file can spend all SOL on the wallet. The file is mode 0600 (only your user can read it on this machine), but cloud-backup tools that sync ~/.config would expose it. Delete after the secret is in your password manager.'), next('Save the secret, then `rm` the file.'));
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
return reply(headline(`${Badge.err} Export failed.`), e instanceof Error ? e.message : String(e));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getMarket, marketUrl } from '../lib/pnl-api.js';
|
|
3
|
+
import { Badge, headline, kvTable, quote, next, reply, truncAddress, code } from '../lib/output.js';
|
|
4
|
+
export const getMarketInputSchema = {
|
|
5
|
+
marketId: z
|
|
6
|
+
.string()
|
|
7
|
+
.min(1)
|
|
8
|
+
.describe("The market id (from pnl_browse_markets) or the on-chain base58 market address. The /api/markets/<id> endpoint accepts either."),
|
|
9
|
+
};
|
|
10
|
+
const GetMarketInput = z.object(getMarketInputSchema);
|
|
11
|
+
function fmtSol(lamports) {
|
|
12
|
+
if (lamports == null)
|
|
13
|
+
return null;
|
|
14
|
+
const sol = lamports / 1e9;
|
|
15
|
+
if (sol < 0.001)
|
|
16
|
+
return null;
|
|
17
|
+
return `${sol.toFixed(sol < 1 ? 3 : 2)} SOL`;
|
|
18
|
+
}
|
|
19
|
+
function statusBadge(status, displayStatus) {
|
|
20
|
+
const s = (displayStatus || status || '').toLowerCase();
|
|
21
|
+
if (s.includes('active'))
|
|
22
|
+
return Badge.live;
|
|
23
|
+
if (s.includes('yes'))
|
|
24
|
+
return 'YES won';
|
|
25
|
+
if (s.includes('no'))
|
|
26
|
+
return 'NO won';
|
|
27
|
+
if (s.includes('refund'))
|
|
28
|
+
return 'refunded';
|
|
29
|
+
if (s.includes('expired') || s.includes('awaiting'))
|
|
30
|
+
return Badge.pending;
|
|
31
|
+
return displayStatus || status || '—';
|
|
32
|
+
}
|
|
33
|
+
export async function callGetMarket(rawInput) {
|
|
34
|
+
const { marketId } = GetMarketInput.parse(rawInput ?? {});
|
|
35
|
+
const m = await getMarket(marketId);
|
|
36
|
+
const symbol = m.tokenSymbol ? `$${m.tokenSymbol}` : '';
|
|
37
|
+
const yesPct = m.yesPercentage != null ? `${Math.round(m.yesPercentage)}% YES` : '';
|
|
38
|
+
const status = statusBadge(m.status, m.displayStatus);
|
|
39
|
+
const headerBits = [m.name, symbol && `· ${symbol}`, status && `· ${status}`, yesPct && `· ${yesPct}`]
|
|
40
|
+
.filter(Boolean)
|
|
41
|
+
.join(' ');
|
|
42
|
+
const founder = m.founderDisplayName || m.founderUsername;
|
|
43
|
+
const yesPool = fmtSol(m.yesPool ?? m.totalYesStake);
|
|
44
|
+
const noPool = fmtSol(m.noPool ?? m.totalNoStake);
|
|
45
|
+
return reply(headline(headerBits), kvTable([
|
|
46
|
+
['Founder', founder ?? null],
|
|
47
|
+
['Category', [m.category, m.stage].filter(Boolean).join(' · ') || null],
|
|
48
|
+
['Status', `${status}${m.phase ? ` (${m.phase})` : ''}`],
|
|
49
|
+
['YES support', m.yesPercentage != null ? `${Math.round(m.yesPercentage)}%` : null],
|
|
50
|
+
['Pools', yesPool || noPool ? `YES ${yesPool ?? '—'} · NO ${noPool ?? '—'}` : null],
|
|
51
|
+
['Target pool', m.targetPool ? `${m.targetPool}${m.poolProgressPercentage != null ? ` (${Math.round(m.poolProgressPercentage)}% filled)` : ''}` : null],
|
|
52
|
+
['Voters', m.totalParticipants != null ? String(m.totalParticipants) : null],
|
|
53
|
+
['Time left', m.timeLeft ?? (m.expiryTime ? m.expiryTime : null)],
|
|
54
|
+
['On-chain market', m.marketAddress ? `\`${truncAddress(m.marketAddress, 8, 6)}\`` : null],
|
|
55
|
+
['Token mint', m.tokenMint ? `\`${truncAddress(m.tokenMint, 8, 6)}\`` : null],
|
|
56
|
+
['Pump.fun token', m.pumpFunTokenAddress ? `\`${truncAddress(m.pumpFunTokenAddress, 8, 6)}\`` : null],
|
|
57
|
+
]), m.description ? quote(m.description.trim()) : null, `**Open on PNL:** ${marketUrl(m.id)}`, m.marketAddress ? code(m.marketAddress) : null, next('`/pnl-vote` to stake YES/NO (coming in Phase B), or share the URL.'));
|
|
58
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { hasWallet, unlockStatus } from '../lib/wallet.js';
|
|
2
|
+
import { Badge, headline, table, heading, next, reply } from '../lib/output.js';
|
|
3
|
+
// ─── pnl_help ────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// Discoverability tool. Lists every PNL command + the typical flow
|
|
6
|
+
// a new user follows. Surfaces context-aware suggestions based on
|
|
7
|
+
// current state (does the user have a wallet? is it unlocked?).
|
|
8
|
+
export const helpInputSchema = {};
|
|
9
|
+
export async function callHelp(_rawInput) {
|
|
10
|
+
const wallet = hasWallet();
|
|
11
|
+
const { unlocked } = wallet ? unlockStatus() : { unlocked: false };
|
|
12
|
+
let situation;
|
|
13
|
+
let nextHint;
|
|
14
|
+
if (!wallet) {
|
|
15
|
+
situation = 'No wallet on this machine yet.';
|
|
16
|
+
nextHint = '`/pnl-init` to create one (or `/pnl-restore` if you have a BIP39 phrase).';
|
|
17
|
+
}
|
|
18
|
+
else if (!unlocked) {
|
|
19
|
+
situation = `Wallet exists · ${Badge.locked}`;
|
|
20
|
+
nextHint = '`/pnl-unlock` to enable signing, or `/pnl-pitch` to draft an idea (no unlock needed).';
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
situation = `Wallet exists · ${Badge.unlocked}`;
|
|
24
|
+
nextHint = '`/pnl-pitch` to post an idea, `/pnl-wallet` for balance, `/pnl-browse` to see what\'s live.';
|
|
25
|
+
}
|
|
26
|
+
return reply(headline('PNL on the terminal — command reference'), `**Now:** ${situation}`, heading('Wallet (local, encrypted, BIP39-recoverable)'), table(['Command', 'What it does'], [
|
|
27
|
+
['`/pnl-init`', 'First-run: generate keypair + 12-word mnemonic, encrypt with passphrase'],
|
|
28
|
+
['`/pnl-wallet`', 'Address, balance, lock status, autosign cap (no unlock needed)'],
|
|
29
|
+
['`/pnl-unlock`', 'Decrypt secret in memory for 5m (default). Passphrase via OS dialog or env'],
|
|
30
|
+
['`/pnl-lock`', 'Wipe cached secret immediately'],
|
|
31
|
+
['`/pnl-restore`', 'Rebuild wallet on a new machine from a BIP39 mnemonic'],
|
|
32
|
+
['`/pnl-export`', 'Write secret to a 0600 file for password-manager backup (never to chat)'],
|
|
33
|
+
]), heading('Identity'), table(['Command', 'What it does'], [
|
|
34
|
+
['`/pnl-name`', 'Claim or rename your PNL username (signature-auth, no Privy / no email)'],
|
|
35
|
+
]), heading('Markets — deep-link mode (browser signs)'), table(['Command', 'What it does'], [
|
|
36
|
+
['`/pnl-browse`', 'List live conviction markets — YES%, pool, votes, founder'],
|
|
37
|
+
['`/pnl-pitch`', 'Draft a market from current conversation context, return deep-link to confirm'],
|
|
38
|
+
['`/pnl-vote`', 'Stake YES or NO on an existing market (deep-link to confirm in wallet)'],
|
|
39
|
+
]), heading('Markets — autosign mode (MCP signs locally, no browser)'), table(['Command', 'What it does'], [
|
|
40
|
+
['`/pnl-pitch-now`', `Pitch + create the market in one shot (wallet must be ${Badge.unlocked}, cost within autosign cap)`],
|
|
41
|
+
['`/pnl-vote-now`', `Stake YES or NO without leaving the terminal (wallet must be ${Badge.unlocked}, amount within autosign cap)`],
|
|
42
|
+
['`/pnl-claim-now`', `Claim rewards on a resolved market (wallet must be ${Badge.unlocked}, no cap — claim is a withdrawal)`],
|
|
43
|
+
]), `_Autosign cap defaults to 0.05 SOL (override per-call with \`autosignCapSol\` or update \`~/.config/pnl/config.json\`). Claim has no cap — it's a withdrawal of funds the program already owes you._`, heading('Claim (deep-link mode)'), table(['Command', 'What it does'], [
|
|
44
|
+
['`/pnl-claim`', 'Open the claim panel on /market/<id> — sign in browser wallet'],
|
|
45
|
+
]), heading('Activity'), table(['Command', 'What it does'], [
|
|
46
|
+
['`/pnl-notify`', 'New notifications since last check (votes on your markets, resolutions, claim-ready). Includes profile URL.'],
|
|
47
|
+
]), heading('Typical first run'), [
|
|
48
|
+
'1. `/pnl-init` — set up wallet, write down the 12-word phrase',
|
|
49
|
+
'2. Send ≥ 0.05 SOL to the deposit address from Phantom / Solflare / exchange',
|
|
50
|
+
'3. `/pnl-unlock` — passphrase via OS dialog (or set `PNL_PASSPHRASE` in mcp config)',
|
|
51
|
+
'4. `/pnl-name` — pick a username',
|
|
52
|
+
'5. `/pnl-pitch` — post your first idea',
|
|
53
|
+
].join('\n'), next(nextHint));
|
|
54
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createWallet, hasWallet, getAddress, getBalanceSol, loadConfig, unlockWith, isUsingHostedRpc, writeMnemonicToFile, WALLET_PATHS, } from '../lib/wallet.js';
|
|
3
|
+
import { promptPassphrase } from '../lib/passphrase.js';
|
|
4
|
+
import { PublicKey } from '@solana/web3.js';
|
|
5
|
+
import { Badge, headline, kvTable, code, inline, next, reply, hr, heading, quote, } from '../lib/output.js';
|
|
6
|
+
// ─── pnl_init ────────────────────────────────────────────────────
|
|
7
|
+
//
|
|
8
|
+
// First-run setup. BIP39 mnemonic + Ed25519 keypair at
|
|
9
|
+
// m/44'/501'/0'/0' (Phantom-compatible). Secret encrypted with
|
|
10
|
+
// scrypt + AES-256-GCM, stored at ~/.config/pnl/wallet.enc (0600).
|
|
11
|
+
// Passphrase pulled from PNL_PASSPHRASE env or OS-native dialog;
|
|
12
|
+
// the mnemonic is displayed once and never persisted.
|
|
13
|
+
export const initInputSchema = {};
|
|
14
|
+
const InitInput = z.object(initInputSchema);
|
|
15
|
+
export async function callInit(rawInput) {
|
|
16
|
+
InitInput.parse(rawInput ?? {});
|
|
17
|
+
if (hasWallet()) {
|
|
18
|
+
const address = getAddress();
|
|
19
|
+
const config = loadConfig();
|
|
20
|
+
let balance = '(unknown)';
|
|
21
|
+
try {
|
|
22
|
+
balance = `${(await getBalanceSol(new PublicKey(address))).toFixed(4)} SOL`;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
/* leave as unknown */
|
|
26
|
+
}
|
|
27
|
+
return reply(headline(`Wallet already initialized · ${balance}`), kvTable([
|
|
28
|
+
['Address', `\`${address}\``],
|
|
29
|
+
['Balance', balance],
|
|
30
|
+
['Autosign cap', `${config.autosignCapSol} SOL`],
|
|
31
|
+
['Wallet file', `\`${WALLET_PATHS.wallet}\``],
|
|
32
|
+
]), `To use the wallet for signing, run ${inline('/pnl-unlock')} — passphrase comes from your ${inline('PNL_PASSPHRASE')} env or an OS-native dialog. Never typed in chat.`, next('`/pnl-wallet` for current state, `/pnl-pitch` to post an idea.'));
|
|
33
|
+
}
|
|
34
|
+
// Fresh setup. Two-prompt confirm to catch typos.
|
|
35
|
+
const passphrase = promptPassphrase({
|
|
36
|
+
title: 'PNL Wallet — Setup',
|
|
37
|
+
prompt: 'Choose a passphrase for your new PNL wallet. You\'ll enter it again to confirm.',
|
|
38
|
+
confirm: true,
|
|
39
|
+
});
|
|
40
|
+
const { address, mnemonic } = createWallet(passphrase);
|
|
41
|
+
unlockWith(passphrase, 30); // auto-unlock for the next 30min
|
|
42
|
+
// The 12-word recovery phrase is the keys to the wallet. We MUST NOT
|
|
43
|
+
// return it in the agent's reply — that text would flow through the
|
|
44
|
+
// LLM API + sit in Claude Code's session transcript on disk. Instead,
|
|
45
|
+
// write it to a 0600 file and tell the user the path so they can
|
|
46
|
+
// `cat` it locally + move it to their password manager + delete.
|
|
47
|
+
const { path: mnemonicPath } = writeMnemonicToFile(mnemonic, address);
|
|
48
|
+
let balanceLine = '';
|
|
49
|
+
try {
|
|
50
|
+
const sol = await getBalanceSol(new PublicKey(address));
|
|
51
|
+
balanceLine = `${sol.toFixed(4)} SOL`;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
/* skip */
|
|
55
|
+
}
|
|
56
|
+
return reply(headline(`Wallet created ${Badge.ok}`), kvTable([
|
|
57
|
+
['Deposit address', `\`${address}\``],
|
|
58
|
+
['Phantom deep-link', `\`solana:${address}\``],
|
|
59
|
+
balanceLine ? ['Balance', balanceLine] : null,
|
|
60
|
+
['Status', `${Badge.unlocked} 30m`],
|
|
61
|
+
].filter((r) => Array.isArray(r))), hr, heading('Recovery phrase — written to disk, NOT shown here'), `The 12-word BIP39 recovery phrase was written to ${inline(mnemonicPath)} (mode 0600). Open the file locally to read it — it is NOT in this transcript by design, because anything in this reply flows through the LLM API + is logged by Claude Code.`, code(`cat "${mnemonicPath}"`), quote('This 12-word phrase is the ONLY way to recover your wallet if you lose this machine. After you have moved it to a password manager / paper backup, DELETE the file: `rm "' + mnemonicPath + '"`. Anyone with the phrase can spend the funds.'), hr, heading('Next'), [
|
|
62
|
+
`1. \`cat\` the recovery file above and move the 12 words to your password manager / paper backup.`,
|
|
63
|
+
`2. ${inline(`rm "${mnemonicPath}"`)} when you are done so the cleartext mnemonic is not sitting in \`~/.config/pnl/exports/\`.`,
|
|
64
|
+
`3. Fund the wallet by sending ≥ 0.05 SOL to the deposit address from any Solana wallet.`,
|
|
65
|
+
`4. The wallet is unlocked for 30 minutes. After that, run \`/pnl-unlock\` to sign more transactions.`,
|
|
66
|
+
].join('\n'), isUsingHostedRpc()
|
|
67
|
+
? `_RPC: pnl.market (hosted) — heavy use? Grab a free Helius key at helius.dev and set ${inline('PNL_RPC_URL')} in your Claude Code mcp config to skip the shared rate limit._`
|
|
68
|
+
: null, `Files: ${inline(WALLET_PATHS.wallet)} (mode 0600) · ${inline(WALLET_PATHS.exports)} (mode 0700)`, next('`/pnl-wallet` to see the address again, `/pnl-pitch` once funded.'));
|
|
69
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const notifyInputSchema: {
|
|
3
|
+
readonly limit: z.ZodOptional<z.ZodNumber>;
|
|
4
|
+
readonly all: z.ZodOptional<z.ZodBoolean>;
|
|
5
|
+
readonly unreadOnly: z.ZodOptional<z.ZodBoolean>;
|
|
6
|
+
};
|
|
7
|
+
export declare function callNotify(rawInput: unknown): Promise<{
|
|
8
|
+
content: Array<{
|
|
9
|
+
type: "text";
|
|
10
|
+
text: string;
|
|
11
|
+
}>;
|
|
12
|
+
}>;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
5
|
+
import { hasWallet, getAddress } from '../lib/wallet.js';
|
|
6
|
+
import { Badge, headline, table, inline, truncAddress, heading, next, reply, } from '../lib/output.js';
|
|
7
|
+
// ─── pnl_notify ──────────────────────────────────────────────────
|
|
8
|
+
//
|
|
9
|
+
// Pull recent notifications for the local wallet from PNL's
|
|
10
|
+
// /api/notifications endpoint and format them for the agent. Stateful
|
|
11
|
+
// via ~/.config/pnl/last-seen.json: tracks the most recent notification
|
|
12
|
+
// id this tool has surfaced so subsequent calls only show new ones
|
|
13
|
+
// (with --all to override).
|
|
14
|
+
//
|
|
15
|
+
// The agent typically invokes this when the user says "what's new on
|
|
16
|
+
// PNL", "any updates on my market", or via /pnl-notify. Works without
|
|
17
|
+
// any always-on process — pure on-demand poll.
|
|
18
|
+
const LAST_SEEN_FILE = join(homedir(), '.config', 'pnl', 'last-seen.json');
|
|
19
|
+
function loadLastSeen() {
|
|
20
|
+
try {
|
|
21
|
+
if (!existsSync(LAST_SEEN_FILE))
|
|
22
|
+
return { perWallet: {} };
|
|
23
|
+
const raw = readFileSync(LAST_SEEN_FILE, 'utf8');
|
|
24
|
+
const parsed = JSON.parse(raw);
|
|
25
|
+
if (parsed && typeof parsed === 'object' && parsed.perWallet)
|
|
26
|
+
return parsed;
|
|
27
|
+
return { perWallet: {} };
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return { perWallet: {} };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function saveLastSeen(state) {
|
|
34
|
+
const dir = join(homedir(), '.config', 'pnl');
|
|
35
|
+
if (!existsSync(dir))
|
|
36
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
37
|
+
writeFileSync(LAST_SEEN_FILE, JSON.stringify(state, null, 2));
|
|
38
|
+
}
|
|
39
|
+
function getApiBase() {
|
|
40
|
+
const raw = process.env.PNL_API_BASE_URL?.trim();
|
|
41
|
+
if (!raw)
|
|
42
|
+
return 'https://pnl.market';
|
|
43
|
+
return raw.endsWith('/') ? raw.slice(0, -1) : raw;
|
|
44
|
+
}
|
|
45
|
+
export const notifyInputSchema = {
|
|
46
|
+
limit: z
|
|
47
|
+
.number()
|
|
48
|
+
.int()
|
|
49
|
+
.min(1)
|
|
50
|
+
.max(50)
|
|
51
|
+
.optional()
|
|
52
|
+
.describe('Max notifications to return per call. Default 10.'),
|
|
53
|
+
all: z
|
|
54
|
+
.boolean()
|
|
55
|
+
.optional()
|
|
56
|
+
.describe('When true, show ALL notifications regardless of last-seen state (useful for "show me the full activity feed"). When false (default), only return notifications newer than the last time pnl_notify was called.'),
|
|
57
|
+
unreadOnly: z
|
|
58
|
+
.boolean()
|
|
59
|
+
.optional()
|
|
60
|
+
.describe('Only return unread notifications. Default true.'),
|
|
61
|
+
};
|
|
62
|
+
const NotifyInput = z.object(notifyInputSchema);
|
|
63
|
+
const TYPE_BADGE = {
|
|
64
|
+
vote_result: '[vote]',
|
|
65
|
+
token_launched: '[live]',
|
|
66
|
+
vote_reminder: '[!]',
|
|
67
|
+
reward_earned: '[ok]',
|
|
68
|
+
project_update: '[update]',
|
|
69
|
+
weekly_digest: '[digest]',
|
|
70
|
+
community_milestone: '[milestone]',
|
|
71
|
+
market_resolved: '[resolved]',
|
|
72
|
+
claim_ready: '[claim]',
|
|
73
|
+
pool_complete: '[pool]',
|
|
74
|
+
founder_voice_live: '[live]',
|
|
75
|
+
};
|
|
76
|
+
export async function callNotify(rawInput) {
|
|
77
|
+
const input = NotifyInput.parse(rawInput ?? {});
|
|
78
|
+
const limit = input.limit ?? 10;
|
|
79
|
+
const unreadOnly = input.unreadOnly ?? true;
|
|
80
|
+
const showAll = input.all ?? false;
|
|
81
|
+
if (!hasWallet()) {
|
|
82
|
+
return reply(headline('No wallet on this machine yet.'), `Run ${inline('pnl_init')} to set one up. Notifications are scoped to a wallet address — without one there's nothing to fetch.`);
|
|
83
|
+
}
|
|
84
|
+
const address = getAddress();
|
|
85
|
+
const base = getApiBase();
|
|
86
|
+
const qs = new URLSearchParams({ wallet: address, limit: String(Math.min(limit * 2, 50)) });
|
|
87
|
+
if (unreadOnly)
|
|
88
|
+
qs.set('unread', 'true');
|
|
89
|
+
let payload;
|
|
90
|
+
try {
|
|
91
|
+
const res = await fetch(`${base}/api/notifications?${qs.toString()}`, {
|
|
92
|
+
headers: {
|
|
93
|
+
Accept: 'application/json',
|
|
94
|
+
'User-Agent': 'pnl-mcp-server/0.3.0 (+https://docs.pnl.market)',
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
if (!res.ok) {
|
|
98
|
+
throw new Error(`/api/notifications returned ${res.status} ${res.statusText}`);
|
|
99
|
+
}
|
|
100
|
+
payload = (await res.json());
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
return reply(headline(`${Badge.err} Couldn't fetch notifications`), `Error: ${e instanceof Error ? e.message : String(e)}`, `Profile: ${base}/profile/${address}`);
|
|
104
|
+
}
|
|
105
|
+
const all = payload.notifications ?? [];
|
|
106
|
+
const state = loadLastSeen();
|
|
107
|
+
const lastSeenAt = state.perWallet[address];
|
|
108
|
+
const filtered = showAll
|
|
109
|
+
? all
|
|
110
|
+
: lastSeenAt
|
|
111
|
+
? all.filter((n) => (n.createdAt ?? '') > lastSeenAt)
|
|
112
|
+
: all;
|
|
113
|
+
const toShow = filtered.slice(0, limit);
|
|
114
|
+
// Bump last-seen to the newest createdAt in the API response (not
|
|
115
|
+
// just toShow) so we don't re-surface notifications we deliberately
|
|
116
|
+
// truncated with `limit`.
|
|
117
|
+
if (all.length > 0) {
|
|
118
|
+
const newest = all
|
|
119
|
+
.map((n) => n.createdAt ?? '')
|
|
120
|
+
.filter(Boolean)
|
|
121
|
+
.sort()
|
|
122
|
+
.pop();
|
|
123
|
+
if (newest) {
|
|
124
|
+
state.perWallet[address] = newest;
|
|
125
|
+
saveLastSeen(state);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const profileUrl = `${base}/profile/${address}`;
|
|
129
|
+
const profileLink = `[your profile](${profileUrl})`;
|
|
130
|
+
if (toShow.length === 0) {
|
|
131
|
+
if (showAll) {
|
|
132
|
+
return reply(headline(`${Badge.ok} No notifications`), `Your inbox for \`${truncAddress(address)}\` is empty.`, `${profileLink} · ${inline(profileUrl)}`, next('Stake on a market with `/pnl-vote` to start seeing activity here.'));
|
|
133
|
+
}
|
|
134
|
+
return reply(headline(`${Badge.ok} Nothing new since last check`), `Total unread: ${payload.unreadCount ?? 0}. Pass \`all: true\` to see the full inbox or visit ${profileLink}.`);
|
|
135
|
+
}
|
|
136
|
+
const rows = toShow.map((n) => {
|
|
137
|
+
const badge = TYPE_BADGE[n.type ?? ''] ?? '[ping]';
|
|
138
|
+
const when = n.createdAt ? new Date(n.createdAt).toISOString().slice(0, 16).replace('T', ' ') : '—';
|
|
139
|
+
const title = (n.title ?? '(no title)').slice(0, 70);
|
|
140
|
+
const ctx = n.marketId && typeof n.marketId === 'object' && n.marketId.marketName
|
|
141
|
+
? `\`${n.marketId.marketName.slice(0, 30)}\``
|
|
142
|
+
: n.projectId && typeof n.projectId === 'object' && n.projectId.tokenSymbol
|
|
143
|
+
? `\`$${n.projectId.tokenSymbol}\``
|
|
144
|
+
: '—';
|
|
145
|
+
return [badge, when, title, ctx];
|
|
146
|
+
});
|
|
147
|
+
return reply(headline(`${Badge.live} ${toShow.length} ${toShow.length === 1 ? 'update' : 'updates'} · ${truncAddress(address)}`), `${payload.unreadCount ?? 0} unread total · viewing ${showAll ? 'all' : 'new since last check'}.`, table(['Type', 'When (UTC)', 'Title', 'Market / Token'], rows), heading('Open in browser'), `${profileLink}: ${inline(profileUrl)}`, next(showAll
|
|
148
|
+
? 'Mark items read in the browser — or stake / claim from here with `/pnl-vote-now` / `/pnl-claim-now`.'
|
|
149
|
+
: `Re-run with ${inline('all: true')} to see everything, or open ${profileLink} to mark items read.`));
|
|
150
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const pitchIdeaInputSchema: {
|
|
3
|
+
readonly name: z.ZodString;
|
|
4
|
+
readonly description: z.ZodString;
|
|
5
|
+
readonly tokenSymbol: z.ZodString;
|
|
6
|
+
readonly category: z.ZodEnum<["DeFi", "NFT", "Gaming", "DAO", "AI/ML", "Infrastructure", "Social", "Meme", "Creator", "Healthcare", "Science", "Education", "Finance", "Commerce", "Real Estate", "Energy", "Media", "Manufacturing", "Mobility", "Other"]>;
|
|
7
|
+
readonly projectType: z.ZodEnum<["Protocol", "Application", "Platform", "Service", "Tool"]>;
|
|
8
|
+
readonly projectStage: z.ZodEnum<["Idea", "MVP", "Beta", "Production", "Scaling", "Prototype", "Launched"]>;
|
|
9
|
+
readonly teamSize: z.ZodNumber;
|
|
10
|
+
readonly targetPoolSol: z.ZodNumber;
|
|
11
|
+
readonly durationDays: z.ZodNumber;
|
|
12
|
+
readonly projectImageUrl: z.ZodOptional<z.ZodString>;
|
|
13
|
+
readonly pitchVideoUrl: z.ZodOptional<z.ZodString>;
|
|
14
|
+
readonly twitterHandle: z.ZodOptional<z.ZodString>;
|
|
15
|
+
readonly location: z.ZodOptional<z.ZodString>;
|
|
16
|
+
readonly provenance: z.ZodOptional<z.ZodObject<{
|
|
17
|
+
source: z.ZodEnum<["claude-code", "cursor", "cline", "codex", "other"]>;
|
|
18
|
+
excerpt: z.ZodString;
|
|
19
|
+
codeSnippet: z.ZodOptional<z.ZodString>;
|
|
20
|
+
timestamp: z.ZodOptional<z.ZodString>;
|
|
21
|
+
}, "strip", z.ZodTypeAny, {
|
|
22
|
+
source: "claude-code" | "cursor" | "cline" | "codex" | "other";
|
|
23
|
+
excerpt: string;
|
|
24
|
+
codeSnippet?: string | undefined;
|
|
25
|
+
timestamp?: string | undefined;
|
|
26
|
+
}, {
|
|
27
|
+
source: "claude-code" | "cursor" | "cline" | "codex" | "other";
|
|
28
|
+
excerpt: string;
|
|
29
|
+
codeSnippet?: string | undefined;
|
|
30
|
+
timestamp?: string | undefined;
|
|
31
|
+
}>>;
|
|
32
|
+
};
|
|
33
|
+
export declare function callPitchIdea(rawInput: unknown): Promise<{
|
|
34
|
+
content: Array<{
|
|
35
|
+
type: "text";
|
|
36
|
+
text: string;
|
|
37
|
+
}>;
|
|
38
|
+
}>;
|