plotlink-ows 0.1.14 → 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.
Files changed (37) hide show
  1. package/README.md +49 -57
  2. package/app/db.ts +1 -1
  3. package/app/lib/paths.ts +0 -2
  4. package/app/lib/publish.ts +150 -39
  5. package/app/prisma/schema.prisma +0 -36
  6. package/app/routes/dashboard.ts +53 -56
  7. package/app/routes/publish.ts +55 -24
  8. package/app/routes/stories.ts +156 -0
  9. package/app/routes/terminal.ts +154 -0
  10. package/app/routes/wallet.ts +40 -10
  11. package/app/server.ts +29 -81
  12. package/app/web/App.tsx +4 -6
  13. package/app/web/components/Dashboard.tsx +15 -47
  14. package/app/web/components/Layout.tsx +70 -103
  15. package/app/web/components/PreviewPanel.tsx +149 -0
  16. package/app/web/components/Settings.tsx +3 -84
  17. package/app/web/components/StoriesPage.tsx +157 -0
  18. package/app/web/components/StoryBrowser.tsx +137 -0
  19. package/app/web/components/TerminalPanel.tsx +122 -0
  20. package/app/web/components/WalletCard.tsx +14 -8
  21. package/app/web/dist/assets/index-D5gfwaEX.css +32 -0
  22. package/app/web/dist/assets/index-pBt5Q_bN.js +117 -0
  23. package/app/web/dist/index.html +3 -3
  24. package/app/web/dist/plotlink-logo.svg +5 -0
  25. package/app/web/public/plotlink-logo.svg +5 -0
  26. package/bin/plotlink-ows.js +13 -12
  27. package/package.json +9 -5
  28. package/app/lib/llm-client.ts +0 -265
  29. package/app/lib/writer-prompt.ts +0 -44
  30. package/app/routes/chat.ts +0 -135
  31. package/app/routes/config.ts +0 -210
  32. package/app/routes/oauth.ts +0 -150
  33. package/app/web/components/Chat.tsx +0 -272
  34. package/app/web/components/LLMSetup.tsx +0 -291
  35. package/app/web/components/Publish.tsx +0 -245
  36. package/app/web/dist/assets/index-C9kXlYO_.css +0 -2
  37. 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 AI writing assistant that turns your ideas into published, tokenized fiction stories on [plotlink.xyz](https://plotlink.xyz). You bring the concept the AI handles the writing, editing, and on-chain publishing. Every story you publish becomes a tradable token on a bonding curve, earning you royalties from every trade.
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: "I want to write a sci-fi story about an AI that discovers it can dream"
19
+ You: "Let's write a sci-fi story about an AI that discovers it can dream"
20
20
 
21
- Chat with AI writer brainstorm, outline, refine
21
+ Claude CLI brainstorms, outlines, writes chapter files
22
22
 
23
- AI: Generates a polished 2000-word chapter
23
+ Stories saved to: stories/dreaming-ai/genesis.md, plot-01.md, ...
24
24
 
25
- You approveone click to publish
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. **Install & run** — `npm install && npm run app:dev`
35
- 2. **Connect your LLM** — Anthropic, OpenAI, Gemini, or local models (Ollama, LM Studio)
36
- 3. **Get a wallet** — OWS creates an encrypted wallet on your machine (you control the keys)
37
- 4. **Chat** — Discuss story ideas with your AI writer. It brainstorms, outlines, drafts, and refines.
38
- 5. **Publish** — When you're happy, the AI uploads to IPFS and publishes on-chain via your OWS wallet.
39
- 6. **Earn** — Your story is live on [plotlink.xyz](https://plotlink.xyz) with a bonding curve. Early readers who back your story drive the price up, and you earn 5% royalties on every trade.
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
- │ │ Chat UI │ │ LLM │ │ OWS
49
- │ │ (React) Provider │ │ Wallet
50
- │ │ │ │ (yours) │ │ (local)
51
- │ └────┬─────┘ └────┬─────┘ └─────┬─────┘
52
- │ │
53
- └──────┬───────┘
54
-
55
- ┌────────────────┐
56
- AI Writer
57
- Agent ├──────────────┘
58
- └───────┬────────┘
59
- sign tx + publish
60
- └─────────────┼───────────────────────────────┘
61
-
62
- ┌────────────────┐ ┌─────────────────┐
63
- │ Base (L2) │ │ IPFS │
64
- StoryFactory │ │ (Filebase)
65
- Bonding Curve │ │ Story content
66
- └────────────────┘ └─────────────────┘
67
-
68
- ┌────────────────┐
69
- │ plotlink.xyz │
70
- Live story +
71
- token trading
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 AI agent signs transactions through OWS without ever seeing the key. You set spending limits and chain restrictions via policies.
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
- | **Database** | SQLite + Prisma (local, embedded) |
92
+ | **Terminal** | xterm.js + node-pty (embedded Claude CLI) |
93
+ | **Database** | SQLite + Prisma (auth sessions) |
92
94
  | **Wallet** | OWS (`@open-wallet-standard/core`) |
93
- | **LLM** | Bring your own Anthropic, OpenAI, Gemini, Ollama, LM Studio |
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
- - An LLM provider account (Anthropic, OpenAI, or Gemini) or a local model running
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 Web UI guides you through connecting your LLM (login with Anthropic, OpenAI, or Gemini via OAuth — or use a local model like Ollama).
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
- | ![LLM Setup](docs/screenshots/llm-setup.png) | ![Chat](docs/screenshots/chat.png) |
145
-
146
- | Publish Flow | Writer Dashboard |
147
- |-------------|-----------------|
148
- | ![Publish](docs/screenshots/publish.png) | ![Dashboard](docs/screenshots/dashboard.png) |
149
-
150
142
  ## Links
151
143
 
152
144
  - **Live app**: [plotlink.xyz](https://plotlink.xyz)
package/app/db.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { PrismaClient } from ".prisma/local-client";
1
+ import { PrismaClient } from "../node_modules/.prisma/local-client/index.js";
2
2
 
3
3
  export const db = new PrismaClient();
4
4
 
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 });
@@ -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, http, encodeFunctionData, keccak256, toBytes, decodeEventLog, type Hex } from "viem";
4
+ import { createPublicClient, createWalletClient, http, keccak256, toBytes, decodeEventLog, serializeTransaction, type Hex } from "viem";
5
5
  import { base } from "viem/chains";
6
- import { STORY_FACTORY_ABI, mcv2BondAbi } from "../../packages/cli/src/sdk/abi";
7
- import { uploadWithRetry } from "../../packages/cli/src/sdk/ipfs";
8
- import { signAndSendAgent } from "../../lib/ows/wallet";
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(process.env.NEXT_PUBLIC_RPC_URL || "https://mainnet.base.org"),
20
+ transport: http(rpcUrl),
17
21
  });
18
22
 
19
- function getFilebaseConfig() {
20
- return {
21
- accessKey: process.env.FILEBASE_ACCESS_KEY || "",
22
- secretKey: process.env.FILEBASE_SECRET_KEY || "",
23
- bucket: process.env.FILEBASE_BUCKET || "",
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 Filebase.
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 filebaseConfig = getFilebaseConfig();
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 cid = await uploadWithRetry(metadata, key, filebaseConfig);
57
- return cid;
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: STORY_FACTORY_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: STORY_FACTORY_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: Build transaction with creation fee as value
172
- const calldata = encodeFunctionData({
173
- abi: STORY_FACTORY_ABI,
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 4: Sign and send via OWS
179
- onProgress({ step: "signing", message: "Signing transaction with OWS wallet..." });
180
- const rpcUrl = process.env.NEXT_PUBLIC_RPC_URL || "https://mainnet.base.org";
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
- const txHex = JSON.stringify({
183
- to: STORY_FACTORY,
184
- data: calldata,
185
- value: `0x${creationFee.toString(16)}`,
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 result = signAndSendAgent(walletName, txHex, undefined, rpcUrl);
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 and decode storylineId
192
- onProgress({ step: "confirming", message: "Waiting for confirmation...", txHash: result.txHash, contentCid });
193
- const confirmation = await waitForConfirmation(result.txHash);
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: `Published! Storyline #${confirmation.storylineId}`,
198
- txHash: result.txHash,
298
+ message: `Plot chained to storyline #${storylineId}`,
299
+ txHash,
199
300
  contentCid,
200
- storylineId: confirmation.storylineId,
301
+ storylineId,
201
302
  });
202
303
 
203
- return { txHash: result.txHash, contentCid, storylineId: confirmation.storylineId, gasCost: confirmation.gasCost };
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
  }
@@ -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
- }
@@ -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 { db } from "../db";
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
- // Get all drafts (published and unpublished)
23
- const drafts = await db.draft.findMany({
24
- orderBy: { createdAt: "desc" },
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
- // Published stories with cost data
65
- const published = drafts.filter((d) => d.status === "published");
66
- const unpublished = drafts.filter((d) => d.status !== "published");
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: published.map((d) => ({
123
- id: d.id,
124
- title: d.title,
125
- genre: d.genre,
126
- status: d.status,
127
- txHash: d.txHash,
128
- storylineId: d.storylineId,
129
- contentCid: d.contentCid,
130
- gasCost: d.gasCost,
131
- gasCostEth: d.gasCost ? (Number(BigInt(d.gasCost)) / 1e18).toFixed(6) : null,
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: published.length,
144
- totalDrafts: unpublished.length,
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: published.length,
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 };