plotlink-ows 0.1.15 → 0.1.18
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 +49 -57
- package/app/db.ts +1 -1
- package/app/lib/paths.ts +0 -2
- package/app/lib/publish.ts +150 -39
- package/app/prisma/schema.prisma +0 -36
- package/app/routes/dashboard.ts +53 -56
- package/app/routes/publish.ts +55 -24
- package/app/routes/stories.ts +156 -0
- package/app/routes/terminal.ts +154 -0
- package/app/routes/wallet.ts +40 -10
- package/app/server.ts +29 -81
- package/app/web/App.tsx +4 -6
- package/app/web/components/Dashboard.tsx +15 -47
- package/app/web/components/Layout.tsx +70 -103
- package/app/web/components/PreviewPanel.tsx +149 -0
- package/app/web/components/Settings.tsx +3 -84
- package/app/web/components/StoriesPage.tsx +157 -0
- package/app/web/components/StoryBrowser.tsx +137 -0
- package/app/web/components/TerminalPanel.tsx +122 -0
- package/app/web/components/WalletCard.tsx +14 -8
- package/app/web/dist/assets/index-D5gfwaEX.css +32 -0
- package/app/web/dist/assets/index-pBt5Q_bN.js +117 -0
- package/app/web/dist/index.html +3 -3
- package/app/web/dist/plotlink-logo.svg +5 -0
- package/app/web/public/plotlink-logo.svg +5 -0
- package/bin/plotlink-ows.js +2 -1
- package/package.json +9 -5
- package/app/lib/llm-client.ts +0 -265
- package/app/lib/writer-prompt.ts +0 -44
- package/app/routes/chat.ts +0 -135
- package/app/routes/config.ts +0 -210
- package/app/routes/oauth.ts +0 -150
- package/app/web/components/Chat.tsx +0 -272
- package/app/web/components/LLMSetup.tsx +0 -291
- package/app/web/components/Publish.tsx +0 -245
- package/app/web/dist/assets/index-C9kXlYO_.css +0 -2
- package/app/web/dist/assets/index-CJiiaLHs.js +0 -9
package/README.md
CHANGED
|
@@ -9,20 +9,22 @@ npx plotlink-ows init # one-time setup (~2 minutes)
|
|
|
9
9
|
npx plotlink-ows # start writing
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
PlotLink OWS Writer is a local
|
|
12
|
+
PlotLink OWS Writer is a local writing workspace that turns your ideas into published, tokenized fiction stories on [plotlink.xyz](https://plotlink.xyz). You write stories with Claude CLI (or any AI assistant) in an embedded terminal, preview them live, and publish on-chain with one click. Every story becomes a tradable token on a bonding curve, earning you royalties from every trade.
|
|
13
13
|
|
|
14
14
|
No writing experience needed. No crypto complexity. Just an idea and a conversation with your AI co-writer.
|
|
15
15
|
|
|
16
16
|
## How It Works
|
|
17
17
|
|
|
18
18
|
```
|
|
19
|
-
You: "
|
|
19
|
+
You: "Let's write a sci-fi story about an AI that discovers it can dream"
|
|
20
20
|
|
|
21
|
-
↓
|
|
21
|
+
↓ Claude CLI brainstorms, outlines, writes chapter files
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Stories saved to: stories/dreaming-ai/genesis.md, plot-01.md, ...
|
|
24
24
|
|
|
25
|
-
↓
|
|
25
|
+
↓ Live preview in the browser — you review and iterate
|
|
26
|
+
|
|
27
|
+
↓ Click "Publish" on any chapter
|
|
26
28
|
|
|
27
29
|
On-chain: Story published to PlotLink on Base
|
|
28
30
|
→ Token + bonding curve deployed
|
|
@@ -31,56 +33,55 @@ On-chain: Story published to PlotLink on Base
|
|
|
31
33
|
|
|
32
34
|
### The Flow
|
|
33
35
|
|
|
34
|
-
1. **
|
|
35
|
-
2. **
|
|
36
|
-
3. **
|
|
37
|
-
4. **
|
|
38
|
-
5. **Publish** —
|
|
39
|
-
6. **Earn** — Your story is live on [plotlink.xyz](https://plotlink.xyz) with a bonding curve
|
|
36
|
+
1. **Setup** — `npx plotlink-ows init` (passphrase + OWS wallet)
|
|
37
|
+
2. **Start** — `npx plotlink-ows` opens the three-panel workspace
|
|
38
|
+
3. **Write** — Claude CLI runs in the embedded terminal, creating story files
|
|
39
|
+
4. **Preview** — Live markdown preview auto-refreshes as Claude writes
|
|
40
|
+
5. **Publish** — Click publish on any chapter to go on-chain via your OWS wallet
|
|
41
|
+
6. **Earn** — Your story is live on [plotlink.xyz](https://plotlink.xyz) with a bonding curve
|
|
40
42
|
|
|
41
43
|
## Architecture
|
|
42
44
|
|
|
43
45
|
```
|
|
44
|
-
|
|
45
|
-
│ Your Computer (localhost:7777)
|
|
46
|
-
│
|
|
47
|
-
│ ┌──────────┐
|
|
48
|
-
│ │
|
|
49
|
-
│ │
|
|
50
|
-
│ │ │ │
|
|
51
|
-
│ └────┬─────┘
|
|
52
|
-
│ │
|
|
53
|
-
│
|
|
54
|
-
│
|
|
55
|
-
│
|
|
56
|
-
│
|
|
57
|
-
│
|
|
58
|
-
│
|
|
59
|
-
│
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
46
|
+
┌──────────────────────────────────────────────────┐
|
|
47
|
+
│ Your Computer (localhost:7777) │
|
|
48
|
+
│ │
|
|
49
|
+
│ ┌──────────┐ ┌──────────────┐ ┌───────────┐ │
|
|
50
|
+
│ │ Story │ │ Terminal │ │ Preview │ │
|
|
51
|
+
│ │ Browser │ │ (Claude CLI)│ │ (Live MD)│ │
|
|
52
|
+
│ │ │ │ │ │ │ │
|
|
53
|
+
│ └────┬─────┘ └──────┬───────┘ └─────┬─────┘ │
|
|
54
|
+
│ │ │ │ │
|
|
55
|
+
│ └───────┬───────┘ │ │
|
|
56
|
+
│ ↓ │ │
|
|
57
|
+
│ ┌────────────────┐ ┌─────────────────┐ │
|
|
58
|
+
│ │ stories/ │ │ OWS Wallet │ │
|
|
59
|
+
│ │ (local files) │ │ (encrypted) │ │
|
|
60
|
+
│ └────────────────┘ └────────┬────────┘ │
|
|
61
|
+
│ │ │
|
|
62
|
+
│ sign tx + publish ────┘ │
|
|
63
|
+
└─────────────────┬───────────────────────────────┘
|
|
64
|
+
↓
|
|
65
|
+
┌────────────────┐ ┌─────────────────┐
|
|
66
|
+
│ Base (L2) │ │ IPFS │
|
|
67
|
+
│ StoryFactory │ │ (Filebase) │
|
|
68
|
+
│ Bonding Curve │ │ Story content │
|
|
69
|
+
└────────────────┘ └─────────────────┘
|
|
70
|
+
↓
|
|
71
|
+
┌────────────────┐
|
|
72
|
+
│ plotlink.xyz │
|
|
73
|
+
│ Live story + │
|
|
74
|
+
│ token trading │
|
|
75
|
+
└────────────────┘
|
|
73
76
|
```
|
|
74
77
|
|
|
75
78
|
## What is PlotLink?
|
|
76
79
|
|
|
77
80
|
[PlotLink](https://plotlink.xyz) is an on-chain storytelling protocol on Base. Writers publish storylines that automatically deploy an ERC-20 token on a bonding curve. Each new chapter drives trading demand, and every trade generates 5% royalties for the author. Stories are stored permanently on IPFS.
|
|
78
81
|
|
|
79
|
-
PlotLink is currently in live testing on Base mainnet with public launch planned for next week.
|
|
80
|
-
|
|
81
82
|
## What is OWS?
|
|
82
83
|
|
|
83
|
-
[Open Wallet Standard](https://docs.openwallet.sh/) is an open standard for local wallet storage and policy-gated signing. Your private key is encrypted on your machine — the
|
|
84
|
+
[Open Wallet Standard](https://docs.openwallet.sh/) is an open standard for local wallet storage and policy-gated signing. Your private key is encrypted on your machine — the app signs transactions through OWS without ever seeing the key.
|
|
84
85
|
|
|
85
86
|
## Tech Stack
|
|
86
87
|
|
|
@@ -88,9 +89,10 @@ PlotLink is currently in live testing on Base mainnet with public launch planned
|
|
|
88
89
|
|-------|-----------|
|
|
89
90
|
| **Backend** | Hono (localhost:7777) |
|
|
90
91
|
| **Frontend** | React 19 + Vite |
|
|
91
|
-
| **
|
|
92
|
+
| **Terminal** | xterm.js + node-pty (embedded Claude CLI) |
|
|
93
|
+
| **Database** | SQLite + Prisma (auth sessions) |
|
|
92
94
|
| **Wallet** | OWS (`@open-wallet-standard/core`) |
|
|
93
|
-
| **
|
|
95
|
+
| **AI** | Claude CLI (or any AI assistant in the terminal) |
|
|
94
96
|
| **Chain** | Base (L2) |
|
|
95
97
|
| **Storage** | IPFS via Filebase |
|
|
96
98
|
| **On-chain** | PlotLink StoryFactory + Mint Club V2 bonding curves |
|
|
@@ -101,7 +103,7 @@ PlotLink is currently in live testing on Base mainnet with public launch planned
|
|
|
101
103
|
### Prerequisites
|
|
102
104
|
|
|
103
105
|
- Node.js 20+
|
|
104
|
-
-
|
|
106
|
+
- [Claude CLI](https://docs.anthropic.com/en/docs/claude-code) (or any AI CLI)
|
|
105
107
|
- A small amount of ETH on Base for gas (~$0.01 per publish)
|
|
106
108
|
|
|
107
109
|
### Quick Start
|
|
@@ -111,7 +113,7 @@ npx plotlink-ows init # set passphrase + create wallet
|
|
|
111
113
|
npx plotlink-ows # start app + open browser
|
|
112
114
|
```
|
|
113
115
|
|
|
114
|
-
The setup wizard creates your encrypted OWS wallet. Then the
|
|
116
|
+
The setup wizard creates your encrypted OWS wallet. Then the workspace opens with Claude CLI ready to write.
|
|
115
117
|
|
|
116
118
|
### Commands
|
|
117
119
|
|
|
@@ -137,16 +139,6 @@ npm run app:start # Serve production build
|
|
|
137
139
|
|
|
138
140
|
See [`.env.example`](.env.example) for configuration options.
|
|
139
141
|
|
|
140
|
-
## Screenshots
|
|
141
|
-
|
|
142
|
-
| LLM Setup | Chat with AI Writer |
|
|
143
|
-
|-----------|-------------------|
|
|
144
|
-
|  |  |
|
|
145
|
-
|
|
146
|
-
| Publish Flow | Writer Dashboard |
|
|
147
|
-
|-------------|-----------------|
|
|
148
|
-
|  |  |
|
|
149
|
-
|
|
150
142
|
## Links
|
|
151
143
|
|
|
152
144
|
- **Live app**: [plotlink.xyz](https://plotlink.xyz)
|
package/app/db.ts
CHANGED
package/app/lib/paths.ts
CHANGED
|
@@ -5,7 +5,5 @@ import fs from "fs";
|
|
|
5
5
|
/** All user state lives in ~/.plotlink-ows/ — survives npx reinstalls */
|
|
6
6
|
export const CONFIG_DIR = path.join(os.homedir(), ".plotlink-ows");
|
|
7
7
|
export const ENV_FILE = path.join(CONFIG_DIR, ".env");
|
|
8
|
-
export const AGENT_CONFIG_FILE = path.join(CONFIG_DIR, "agent.config.json");
|
|
9
|
-
|
|
10
8
|
// Ensure config dir exists on import
|
|
11
9
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
package/app/lib/publish.ts
CHANGED
|
@@ -1,27 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PlotLink publish flow — uploads content to IPFS and publishes on-chain via OWS wallet.
|
|
3
3
|
*/
|
|
4
|
-
import { createPublicClient,
|
|
4
|
+
import { createPublicClient, createWalletClient, http, keccak256, toBytes, decodeEventLog, serializeTransaction, type Hex } from "viem";
|
|
5
5
|
import { base } from "viem/chains";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
6
|
+
import { toAccount } from "viem/accounts";
|
|
7
|
+
import { storyFactoryAbi, mcv2BondAbi } from "../../packages/cli/src/sdk/abi";
|
|
8
|
+
import {
|
|
9
|
+
signTransaction as owsSignTx,
|
|
10
|
+
signMessage as owsSignMsg,
|
|
11
|
+
} from "@open-wallet-standard/core";
|
|
9
12
|
|
|
10
13
|
// Contract addresses (Base mainnet)
|
|
11
14
|
const STORY_FACTORY = "0x9D2AE1E99D0A6300bfcCF41A82260374e38744Cf" as const;
|
|
12
15
|
const MCV2_BOND = "0xc5a076cad94176c2996B32d8466Be1cE757FAa27" as const;
|
|
13
16
|
|
|
17
|
+
const rpcUrl = process.env.NEXT_PUBLIC_RPC_URL || "https://mainnet.base.org";
|
|
14
18
|
const publicClient = createPublicClient({
|
|
15
19
|
chain: base,
|
|
16
|
-
transport: http(
|
|
20
|
+
transport: http(rpcUrl),
|
|
17
21
|
});
|
|
18
22
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
/** Parse OWS signature into viem-compatible r,s,v */
|
|
24
|
+
function parseEvmSignature(sigHex: string, recoveryId?: number): { r: Hex; s: Hex; v: bigint } {
|
|
25
|
+
const sig = sigHex.startsWith("0x") ? sigHex.slice(2) : sigHex;
|
|
26
|
+
const r = `0x${sig.slice(0, 64)}` as Hex;
|
|
27
|
+
const s = `0x${sig.slice(64, 128)}` as Hex;
|
|
28
|
+
// recovery id from OWS or from the last byte of signature
|
|
29
|
+
const v = recoveryId !== undefined ? BigInt(recoveryId + 27) : BigInt(parseInt(sig.slice(128, 130), 16));
|
|
30
|
+
return { r, s, v };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Create a viem-compatible account backed by OWS wallet (same pattern as claw-on-chain) */
|
|
34
|
+
function createOwsAccount(walletName: string, address: `0x${string}`) {
|
|
35
|
+
const passphrase = process.env.OWS_PASSPHRASE;
|
|
36
|
+
return toAccount({
|
|
37
|
+
address,
|
|
38
|
+
signMessage: async ({ message }) => {
|
|
39
|
+
const msg = typeof message === "string" ? message : typeof message.raw === "string" ? message.raw : Buffer.from(message.raw).toString("hex");
|
|
40
|
+
const result = owsSignMsg(walletName, "eip155:8453", msg, passphrase);
|
|
41
|
+
return (result.signature.startsWith("0x") ? result.signature : `0x${result.signature}`) as Hex;
|
|
42
|
+
},
|
|
43
|
+
signTransaction: async (tx) => {
|
|
44
|
+
const unsigned = serializeTransaction(tx);
|
|
45
|
+
const hexWithout0x = unsigned.startsWith("0x") ? unsigned.slice(2) : unsigned;
|
|
46
|
+
const result = owsSignTx(walletName, "eip155:8453", hexWithout0x, passphrase);
|
|
47
|
+
const sig = parseEvmSignature(result.signature, result.recoveryId);
|
|
48
|
+
return serializeTransaction(tx, sig);
|
|
49
|
+
},
|
|
50
|
+
signTypedData: async () => { throw new Error("signTypedData not implemented"); },
|
|
51
|
+
});
|
|
25
52
|
}
|
|
26
53
|
|
|
27
54
|
export interface PublishResult {
|
|
@@ -41,20 +68,28 @@ export interface PublishProgress {
|
|
|
41
68
|
}
|
|
42
69
|
|
|
43
70
|
/**
|
|
44
|
-
* Upload story content to IPFS via
|
|
71
|
+
* Upload story content to IPFS via PlotLink's API (plotlink.xyz/api/upload).
|
|
72
|
+
* PlotLink handles Filebase credentials server-side.
|
|
45
73
|
*/
|
|
46
74
|
export async function uploadToIPFS(content: string, title: string, genre?: string): Promise<string> {
|
|
47
|
-
const
|
|
48
|
-
if (!filebaseConfig.accessKey || !filebaseConfig.secretKey) {
|
|
49
|
-
throw new Error("Filebase not configured. Set FILEBASE_ACCESS_KEY and FILEBASE_SECRET_KEY in .env");
|
|
50
|
-
}
|
|
51
|
-
|
|
75
|
+
const PLOTLINK_URL = process.env.NEXT_PUBLIC_APP_URL || "https://plotlink.xyz";
|
|
52
76
|
const metadata = JSON.stringify({ title, genre, content });
|
|
53
77
|
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 40);
|
|
54
78
|
const key = `plotlink/storylines/${Date.now()}-${slug}.json`;
|
|
55
79
|
|
|
56
|
-
const
|
|
57
|
-
|
|
80
|
+
const res = await fetch(`${PLOTLINK_URL}/api/upload`, {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: { "Content-Type": "application/json" },
|
|
83
|
+
body: JSON.stringify({ content: metadata, key }),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
const err = await res.json().catch(() => ({})) as Record<string, string>;
|
|
88
|
+
throw new Error(err.error || `Upload failed: HTTP ${res.status}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const data = await res.json() as { cid: string };
|
|
92
|
+
return data.cid;
|
|
58
93
|
}
|
|
59
94
|
|
|
60
95
|
/**
|
|
@@ -85,7 +120,7 @@ export async function estimatePublishCost(
|
|
|
85
120
|
to: STORY_FACTORY,
|
|
86
121
|
value: creationFee,
|
|
87
122
|
data: encodeFunctionData({
|
|
88
|
-
abi:
|
|
123
|
+
abi: storyFactoryAbi,
|
|
89
124
|
functionName: "createStoryline",
|
|
90
125
|
args: [title, contentCid, contentHash, true],
|
|
91
126
|
}),
|
|
@@ -136,7 +171,7 @@ async function waitForConfirmation(txHash: string): Promise<{ storylineId: numbe
|
|
|
136
171
|
for (const log of receipt.logs) {
|
|
137
172
|
try {
|
|
138
173
|
const decoded = decodeEventLog({
|
|
139
|
-
abi:
|
|
174
|
+
abi: storyFactoryAbi,
|
|
140
175
|
data: log.data,
|
|
141
176
|
topics: log.topics,
|
|
142
177
|
});
|
|
@@ -168,37 +203,113 @@ export async function publishStoryline(
|
|
|
168
203
|
onProgress({ step: "estimating", message: "Fetching creation fee and estimating gas..." });
|
|
169
204
|
const creationFee = await getCreationFee();
|
|
170
205
|
|
|
171
|
-
// Step 3:
|
|
172
|
-
|
|
173
|
-
|
|
206
|
+
// Step 3: Create OWS-backed viem wallet client
|
|
207
|
+
onProgress({ step: "signing", message: "Signing transaction with OWS wallet..." });
|
|
208
|
+
const { listAgentWallets, getBaseAddress } = await import("../../lib/ows/wallet");
|
|
209
|
+
const wallets = listAgentWallets();
|
|
210
|
+
const owsWallet = wallets.find((w) => w.name === walletName);
|
|
211
|
+
if (!owsWallet) throw new Error("OWS wallet not found");
|
|
212
|
+
const address = getBaseAddress(owsWallet);
|
|
213
|
+
if (!address) throw new Error("No EVM address on wallet");
|
|
214
|
+
|
|
215
|
+
const account = createOwsAccount(walletName, address as `0x${string}`);
|
|
216
|
+
const walletClient = createWalletClient({ account, chain: base, transport: http(rpcUrl) });
|
|
217
|
+
|
|
218
|
+
// Step 4: Write contract via viem (handles signing + broadcasting)
|
|
219
|
+
onProgress({ step: "broadcasting", message: "Broadcasting transaction..." });
|
|
220
|
+
const txHash = await walletClient.writeContract({
|
|
221
|
+
address: STORY_FACTORY,
|
|
222
|
+
abi: storyFactoryAbi,
|
|
174
223
|
functionName: "createStoryline",
|
|
175
224
|
args: [title, contentCid, contentHash, true],
|
|
225
|
+
value: creationFee,
|
|
176
226
|
});
|
|
177
227
|
|
|
178
|
-
// Step
|
|
179
|
-
onProgress({ step: "
|
|
180
|
-
const
|
|
228
|
+
// Step 5: Wait for confirmation and decode storylineId
|
|
229
|
+
onProgress({ step: "confirming", message: "Waiting for confirmation...", txHash, contentCid });
|
|
230
|
+
const confirmation = await waitForConfirmation(txHash);
|
|
181
231
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
232
|
+
onProgress({
|
|
233
|
+
step: "done",
|
|
234
|
+
message: `Published! Storyline #${confirmation.storylineId}`,
|
|
235
|
+
txHash,
|
|
236
|
+
contentCid,
|
|
237
|
+
storylineId: confirmation.storylineId,
|
|
186
238
|
});
|
|
187
239
|
|
|
240
|
+
// Index on PlotLink (best-effort — story appears on plotlink.xyz)
|
|
241
|
+
try {
|
|
242
|
+
const PLOTLINK_URL = process.env.NEXT_PUBLIC_APP_URL || "https://plotlink.xyz";
|
|
243
|
+
await fetch(`${PLOTLINK_URL}/api/index/storyline`, {
|
|
244
|
+
method: "POST",
|
|
245
|
+
headers: { "Content-Type": "application/json" },
|
|
246
|
+
body: JSON.stringify({ txHash, content, genre }),
|
|
247
|
+
});
|
|
248
|
+
} catch { /* indexing is best-effort */ }
|
|
249
|
+
|
|
250
|
+
return { txHash, contentCid, storylineId: confirmation.storylineId, gasCost: confirmation.gasCost };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Chain a new plot to an existing storyline on-chain.
|
|
255
|
+
*/
|
|
256
|
+
export async function publishPlot(
|
|
257
|
+
walletName: string,
|
|
258
|
+
storylineId: number,
|
|
259
|
+
title: string,
|
|
260
|
+
content: string,
|
|
261
|
+
genre: string | undefined,
|
|
262
|
+
onProgress: (progress: PublishProgress) => void,
|
|
263
|
+
): Promise<PublishResult> {
|
|
264
|
+
// Step 1: Upload to IPFS
|
|
265
|
+
onProgress({ step: "uploading", message: "Uploading plot to IPFS..." });
|
|
266
|
+
const contentCid = await uploadToIPFS(content, title, genre);
|
|
267
|
+
|
|
268
|
+
// Step 2: Compute content hash
|
|
269
|
+
const contentHash = keccak256(toBytes(content));
|
|
270
|
+
|
|
271
|
+
// Step 3: Create OWS-backed viem wallet client
|
|
272
|
+
onProgress({ step: "signing", message: "Signing transaction with OWS wallet..." });
|
|
273
|
+
const { listAgentWallets, getBaseAddress } = await import("../../lib/ows/wallet");
|
|
274
|
+
const wallets = listAgentWallets();
|
|
275
|
+
const owsWallet = wallets.find((w) => w.name === walletName);
|
|
276
|
+
if (!owsWallet) throw new Error("OWS wallet not found");
|
|
277
|
+
const address = getBaseAddress(owsWallet);
|
|
278
|
+
if (!address) throw new Error("No EVM address on wallet");
|
|
279
|
+
|
|
280
|
+
const account = createOwsAccount(walletName, address as `0x${string}`);
|
|
281
|
+
const walletClient = createWalletClient({ account, chain: base, transport: http(rpcUrl) });
|
|
282
|
+
|
|
283
|
+
// Step 4: Write contract via viem
|
|
188
284
|
onProgress({ step: "broadcasting", message: "Broadcasting transaction..." });
|
|
189
|
-
const
|
|
285
|
+
const txHash = await walletClient.writeContract({
|
|
286
|
+
address: STORY_FACTORY,
|
|
287
|
+
abi: storyFactoryAbi,
|
|
288
|
+
functionName: "chainPlot",
|
|
289
|
+
args: [BigInt(storylineId), title, contentCid, contentHash],
|
|
290
|
+
});
|
|
190
291
|
|
|
191
|
-
// Step 5: Wait for confirmation
|
|
192
|
-
onProgress({ step: "confirming", message: "Waiting for confirmation...", txHash
|
|
193
|
-
const confirmation = await waitForConfirmation(
|
|
292
|
+
// Step 5: Wait for confirmation
|
|
293
|
+
onProgress({ step: "confirming", message: "Waiting for confirmation...", txHash, contentCid });
|
|
294
|
+
const confirmation = await waitForConfirmation(txHash);
|
|
194
295
|
|
|
195
296
|
onProgress({
|
|
196
297
|
step: "done",
|
|
197
|
-
message: `
|
|
198
|
-
txHash
|
|
298
|
+
message: `Plot chained to storyline #${storylineId}`,
|
|
299
|
+
txHash,
|
|
199
300
|
contentCid,
|
|
200
|
-
storylineId
|
|
301
|
+
storylineId,
|
|
201
302
|
});
|
|
202
303
|
|
|
203
|
-
|
|
304
|
+
// Index on PlotLink (best-effort — plot appears on plotlink.xyz)
|
|
305
|
+
try {
|
|
306
|
+
const PLOTLINK_URL = process.env.NEXT_PUBLIC_APP_URL || "https://plotlink.xyz";
|
|
307
|
+
await fetch(`${PLOTLINK_URL}/api/index/plot`, {
|
|
308
|
+
method: "POST",
|
|
309
|
+
headers: { "Content-Type": "application/json" },
|
|
310
|
+
body: JSON.stringify({ txHash }),
|
|
311
|
+
});
|
|
312
|
+
} catch { /* indexing is best-effort */ }
|
|
313
|
+
|
|
314
|
+
return { txHash, contentCid, storylineId, gasCost: confirmation.gasCost };
|
|
204
315
|
}
|
package/app/prisma/schema.prisma
CHANGED
|
@@ -19,39 +19,3 @@ model Setting {
|
|
|
19
19
|
key String @id
|
|
20
20
|
value String
|
|
21
21
|
}
|
|
22
|
-
|
|
23
|
-
model StorySession {
|
|
24
|
-
id String @id @default(cuid())
|
|
25
|
-
title String @default("Untitled Story")
|
|
26
|
-
genre String?
|
|
27
|
-
status String @default("active") // active, finalized, archived
|
|
28
|
-
createdAt DateTime @default(now())
|
|
29
|
-
updatedAt DateTime @updatedAt
|
|
30
|
-
messages Message[]
|
|
31
|
-
drafts Draft[]
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
model Message {
|
|
35
|
-
id String @id @default(cuid())
|
|
36
|
-
sessionId String
|
|
37
|
-
session StorySession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
|
38
|
-
role String // user, assistant, system
|
|
39
|
-
content String
|
|
40
|
-
createdAt DateTime @default(now())
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
model Draft {
|
|
44
|
-
id String @id @default(cuid())
|
|
45
|
-
sessionId String
|
|
46
|
-
session StorySession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
|
47
|
-
title String
|
|
48
|
-
content String
|
|
49
|
-
genre String?
|
|
50
|
-
status String @default("draft") // draft, ready, published
|
|
51
|
-
txHash String?
|
|
52
|
-
storylineId Int?
|
|
53
|
-
contentCid String?
|
|
54
|
-
gasCost String? // ETH cost in wei
|
|
55
|
-
createdAt DateTime @default(now())
|
|
56
|
-
updatedAt DateTime @updatedAt
|
|
57
|
-
}
|
package/app/routes/dashboard.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import { createPublicClient, http } from "viem";
|
|
3
3
|
import { base } from "viem/chains";
|
|
4
|
-
import
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
5
6
|
import { getEthBalance } from "../lib/publish";
|
|
6
7
|
import { listAgentWallets, getBaseAddress } from "../../lib/ows/wallet";
|
|
7
8
|
import { mcv2BondAbi } from "../../packages/cli/src/sdk/abi";
|
|
9
|
+
import { STORIES_DIR, readPublishStatus } from "./stories";
|
|
8
10
|
|
|
9
11
|
const MCV2_BOND = "0xc5a076cad94176c2996B32d8466Be1cE757FAa27" as const;
|
|
10
12
|
// Reserve token for PlotLink bonding curves (PLOT token on Base mainnet)
|
|
@@ -19,10 +21,38 @@ const dashboard = new Hono();
|
|
|
19
21
|
|
|
20
22
|
/** GET /api/dashboard — writer dashboard data */
|
|
21
23
|
dashboard.get("/", async (c) => {
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
// Scan stories/ for publish status
|
|
25
|
+
interface PublishedFile {
|
|
26
|
+
storyName: string;
|
|
27
|
+
file: string;
|
|
28
|
+
txHash?: string;
|
|
29
|
+
storylineId?: number;
|
|
30
|
+
contentCid?: string;
|
|
31
|
+
publishedAt?: string;
|
|
32
|
+
gasCost?: string;
|
|
33
|
+
}
|
|
34
|
+
const publishedFiles: PublishedFile[] = [];
|
|
35
|
+
let totalFiles = 0;
|
|
36
|
+
let totalStories = 0;
|
|
37
|
+
|
|
38
|
+
if (fs.existsSync(STORIES_DIR)) {
|
|
39
|
+
const dirs = fs.readdirSync(STORIES_DIR, { withFileTypes: true })
|
|
40
|
+
.filter((d) => d.isDirectory() && !d.name.startsWith(".") && d.name !== "_example");
|
|
41
|
+
totalStories = dirs.length;
|
|
42
|
+
|
|
43
|
+
for (const dir of dirs) {
|
|
44
|
+
const storyDir = path.join(STORIES_DIR, dir.name);
|
|
45
|
+
const status = readPublishStatus(storyDir);
|
|
46
|
+
const mdFiles = fs.readdirSync(storyDir).filter((f) => f.endsWith(".md"));
|
|
47
|
+
totalFiles += mdFiles.length;
|
|
48
|
+
|
|
49
|
+
for (const [file, info] of Object.entries(status)) {
|
|
50
|
+
if (info.status === "published") {
|
|
51
|
+
publishedFiles.push({ storyName: dir.name, file, ...info });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
26
56
|
|
|
27
57
|
// Get wallet info
|
|
28
58
|
let walletInfo = null;
|
|
@@ -61,13 +91,9 @@ dashboard.get("/", async (c) => {
|
|
|
61
91
|
}
|
|
62
92
|
} catch { /* wallet not available */ }
|
|
63
93
|
|
|
64
|
-
//
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// Compute total costs
|
|
69
|
-
const totalGasCostWei = published.reduce((sum, d) => {
|
|
70
|
-
if (d.gasCost) return sum + BigInt(d.gasCost);
|
|
94
|
+
// Compute total costs from published files
|
|
95
|
+
const totalGasCostWei = publishedFiles.reduce((sum, f) => {
|
|
96
|
+
if (f.gasCost) return sum + BigInt(f.gasCost);
|
|
71
97
|
return sum;
|
|
72
98
|
}, BigInt(0));
|
|
73
99
|
const totalGasCostEth = (Number(totalGasCostWei) / 1e18).toFixed(6);
|
|
@@ -111,49 +137,35 @@ dashboard.get("/", async (c) => {
|
|
|
111
137
|
const totalRoyaltiesUsd = parseFloat(royaltiesEarned) * plotUsdPrice;
|
|
112
138
|
const netPnlUsd = totalRoyaltiesUsd - totalCostUsd;
|
|
113
139
|
|
|
114
|
-
// Session stats
|
|
115
|
-
const sessions = await db.storySession.findMany({
|
|
116
|
-
include: { _count: { select: { messages: true } } },
|
|
117
|
-
});
|
|
118
|
-
|
|
119
140
|
return c.json({
|
|
120
141
|
wallet: walletInfo,
|
|
121
142
|
stories: {
|
|
122
|
-
published:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
gasCostUsd: d.gasCost && ethUsdPrice ? ((Number(BigInt(d.gasCost)) / 1e18) * ethUsdPrice).toFixed(2) : null,
|
|
133
|
-
createdAt: d.createdAt,
|
|
134
|
-
updatedAt: d.updatedAt,
|
|
135
|
-
})),
|
|
136
|
-
drafts: unpublished.map((d) => ({
|
|
137
|
-
id: d.id,
|
|
138
|
-
title: d.title,
|
|
139
|
-
genre: d.genre,
|
|
140
|
-
status: d.status,
|
|
141
|
-
createdAt: d.createdAt,
|
|
143
|
+
published: publishedFiles.map((f) => ({
|
|
144
|
+
storyName: f.storyName,
|
|
145
|
+
file: f.file,
|
|
146
|
+
txHash: f.txHash,
|
|
147
|
+
storylineId: f.storylineId,
|
|
148
|
+
contentCid: f.contentCid,
|
|
149
|
+
gasCost: f.gasCost,
|
|
150
|
+
gasCostEth: f.gasCost ? (Number(BigInt(f.gasCost)) / 1e18).toFixed(6) : null,
|
|
151
|
+
gasCostUsd: f.gasCost && ethUsdPrice ? ((Number(BigInt(f.gasCost)) / 1e18) * ethUsdPrice).toFixed(2) : null,
|
|
152
|
+
publishedAt: f.publishedAt,
|
|
142
153
|
})),
|
|
143
|
-
totalPublished:
|
|
144
|
-
|
|
154
|
+
totalPublished: publishedFiles.length,
|
|
155
|
+
totalStories,
|
|
156
|
+
totalFiles,
|
|
157
|
+
pendingFiles: totalFiles - publishedFiles.length,
|
|
145
158
|
},
|
|
146
159
|
costs: {
|
|
147
160
|
totalGasCostWei: totalGasCostWei.toString(),
|
|
148
161
|
totalGasCostEth,
|
|
149
162
|
totalCostUsd: totalCostUsd.toFixed(2),
|
|
150
163
|
ethUsdPrice,
|
|
151
|
-
storiesPublished:
|
|
164
|
+
storiesPublished: publishedFiles.length,
|
|
152
165
|
},
|
|
153
166
|
royalties: {
|
|
154
167
|
earned: royaltiesEarned,
|
|
155
168
|
claimed: royaltiesClaimed,
|
|
156
|
-
// unclaimed = earned - claimed (already correct since earned = unclaimed + claimed)
|
|
157
169
|
unclaimed: (parseFloat(royaltiesEarned) - parseFloat(royaltiesClaimed)).toFixed(6),
|
|
158
170
|
token: "PLOT",
|
|
159
171
|
},
|
|
@@ -165,22 +177,7 @@ dashboard.get("/", async (c) => {
|
|
|
165
177
|
netPnlUsd: netPnlUsd.toFixed(2),
|
|
166
178
|
plotUsdPrice: plotUsdPrice.toFixed(4),
|
|
167
179
|
},
|
|
168
|
-
sessions: {
|
|
169
|
-
total: sessions.length,
|
|
170
|
-
totalMessages: sessions.reduce((sum, s) => sum + s._count.messages, 0),
|
|
171
|
-
},
|
|
172
180
|
});
|
|
173
181
|
});
|
|
174
182
|
|
|
175
|
-
/** DELETE /api/dashboard/drafts/:id — delete a draft */
|
|
176
|
-
dashboard.delete("/drafts/:id", async (c) => {
|
|
177
|
-
const id = c.req.param("id");
|
|
178
|
-
try {
|
|
179
|
-
await db.draft.delete({ where: { id } });
|
|
180
|
-
return c.json({ success: true });
|
|
181
|
-
} catch {
|
|
182
|
-
return c.json({ error: "Draft not found" }, 404);
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
|
|
186
183
|
export { dashboard as dashboardRoutes };
|