plotlink-ows 0.1.13

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 (142) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +151 -0
  3. package/app/db.ts +8 -0
  4. package/app/lib/llm-client.ts +265 -0
  5. package/app/lib/paths.ts +11 -0
  6. package/app/lib/publish.ts +204 -0
  7. package/app/lib/writer-prompt.ts +44 -0
  8. package/app/node_modules/.prisma/local-client/client.d.ts +1 -0
  9. package/app/node_modules/.prisma/local-client/client.js +5 -0
  10. package/app/node_modules/.prisma/local-client/default.d.ts +1 -0
  11. package/app/node_modules/.prisma/local-client/default.js +5 -0
  12. package/app/node_modules/.prisma/local-client/edge.d.ts +1 -0
  13. package/app/node_modules/.prisma/local-client/edge.js +184 -0
  14. package/app/node_modules/.prisma/local-client/index-browser.js +173 -0
  15. package/app/node_modules/.prisma/local-client/index.d.ts +3304 -0
  16. package/app/node_modules/.prisma/local-client/index.js +207 -0
  17. package/app/node_modules/.prisma/local-client/libquery_engine-darwin-arm64.dylib.node +0 -0
  18. package/app/node_modules/.prisma/local-client/package.json +183 -0
  19. package/app/node_modules/.prisma/local-client/query_engine_bg.js +2 -0
  20. package/app/node_modules/.prisma/local-client/query_engine_bg.wasm +0 -0
  21. package/app/node_modules/.prisma/local-client/runtime/edge-esm.js +35 -0
  22. package/app/node_modules/.prisma/local-client/runtime/edge.js +35 -0
  23. package/app/node_modules/.prisma/local-client/runtime/index-browser.d.ts +370 -0
  24. package/app/node_modules/.prisma/local-client/runtime/index-browser.js +17 -0
  25. package/app/node_modules/.prisma/local-client/runtime/library.d.ts +3982 -0
  26. package/app/node_modules/.prisma/local-client/runtime/library.js +147 -0
  27. package/app/node_modules/.prisma/local-client/runtime/react-native.js +84 -0
  28. package/app/node_modules/.prisma/local-client/runtime/wasm-compiler-edge.js +85 -0
  29. package/app/node_modules/.prisma/local-client/runtime/wasm-engine-edge.js +38 -0
  30. package/app/node_modules/.prisma/local-client/schema.prisma +21 -0
  31. package/app/node_modules/.prisma/local-client/wasm-edge-light-loader.mjs +5 -0
  32. package/app/node_modules/.prisma/local-client/wasm-worker-loader.mjs +5 -0
  33. package/app/node_modules/.prisma/local-client/wasm.d.ts +1 -0
  34. package/app/node_modules/.prisma/local-client/wasm.js +191 -0
  35. package/app/prisma/schema.prisma +57 -0
  36. package/app/routes/auth.ts +173 -0
  37. package/app/routes/chat.ts +135 -0
  38. package/app/routes/config.ts +210 -0
  39. package/app/routes/dashboard.ts +186 -0
  40. package/app/routes/oauth.ts +150 -0
  41. package/app/routes/publish.ts +112 -0
  42. package/app/routes/wallet.ts +99 -0
  43. package/app/server.ts +154 -0
  44. package/app/vite.config.ts +19 -0
  45. package/app/web/App.tsx +102 -0
  46. package/app/web/components/Chat.tsx +272 -0
  47. package/app/web/components/Dashboard.tsx +222 -0
  48. package/app/web/components/LLMSetup.tsx +291 -0
  49. package/app/web/components/Layout.tsx +235 -0
  50. package/app/web/components/Login.tsx +62 -0
  51. package/app/web/components/Publish.tsx +245 -0
  52. package/app/web/components/Settings.tsx +175 -0
  53. package/app/web/components/Setup.tsx +84 -0
  54. package/app/web/components/WalletCard.tsx +117 -0
  55. package/app/web/dist/assets/index-C9kXlYO_.css +2 -0
  56. package/app/web/dist/assets/index-CJiiaLHs.js +9 -0
  57. package/app/web/dist/index.html +16 -0
  58. package/app/web/index.html +15 -0
  59. package/app/web/main.tsx +10 -0
  60. package/app/web/plotlink-logo.svg +5 -0
  61. package/app/web/styles.css +51 -0
  62. package/bin/plotlink-ows.js +394 -0
  63. package/lib/ows/index.ts +3 -0
  64. package/lib/ows/policy.ts +68 -0
  65. package/lib/ows/types.ts +14 -0
  66. package/lib/ows/wallet.ts +70 -0
  67. package/package.json +79 -0
  68. package/packages/cli/node_modules/commander/LICENSE +22 -0
  69. package/packages/cli/node_modules/commander/Readme.md +1149 -0
  70. package/packages/cli/node_modules/commander/esm.mjs +16 -0
  71. package/packages/cli/node_modules/commander/index.js +24 -0
  72. package/packages/cli/node_modules/commander/lib/argument.js +149 -0
  73. package/packages/cli/node_modules/commander/lib/command.js +2662 -0
  74. package/packages/cli/node_modules/commander/lib/error.js +39 -0
  75. package/packages/cli/node_modules/commander/lib/help.js +709 -0
  76. package/packages/cli/node_modules/commander/lib/option.js +367 -0
  77. package/packages/cli/node_modules/commander/lib/suggestSimilar.js +101 -0
  78. package/packages/cli/node_modules/commander/package-support.json +16 -0
  79. package/packages/cli/node_modules/commander/package.json +82 -0
  80. package/packages/cli/node_modules/commander/typings/esm.d.mts +3 -0
  81. package/packages/cli/node_modules/commander/typings/index.d.ts +1045 -0
  82. package/packages/cli/node_modules/resolve-from/index.d.ts +31 -0
  83. package/packages/cli/node_modules/resolve-from/index.js +47 -0
  84. package/packages/cli/node_modules/resolve-from/license +9 -0
  85. package/packages/cli/node_modules/resolve-from/package.json +36 -0
  86. package/packages/cli/node_modules/resolve-from/readme.md +72 -0
  87. package/packages/cli/node_modules/tsup/LICENSE +21 -0
  88. package/packages/cli/node_modules/tsup/README.md +75 -0
  89. package/packages/cli/node_modules/tsup/assets/cjs_shims.js +13 -0
  90. package/packages/cli/node_modules/tsup/assets/esm_shims.js +9 -0
  91. package/packages/cli/node_modules/tsup/assets/package.json +3 -0
  92. package/packages/cli/node_modules/tsup/dist/chunk-DI5BO6XE.js +153 -0
  93. package/packages/cli/node_modules/tsup/dist/chunk-JZ25TPTY.js +42 -0
  94. package/packages/cli/node_modules/tsup/dist/chunk-PEEXUWMS.js +6 -0
  95. package/packages/cli/node_modules/tsup/dist/chunk-TWFEYLU4.js +352 -0
  96. package/packages/cli/node_modules/tsup/dist/chunk-VGC3FXLU.js +203 -0
  97. package/packages/cli/node_modules/tsup/dist/cli-default.js +12 -0
  98. package/packages/cli/node_modules/tsup/dist/cli-main.js +8 -0
  99. package/packages/cli/node_modules/tsup/dist/cli-node.js +14 -0
  100. package/packages/cli/node_modules/tsup/dist/index.d.ts +511 -0
  101. package/packages/cli/node_modules/tsup/dist/index.js +1711 -0
  102. package/packages/cli/node_modules/tsup/dist/rollup.js +6949 -0
  103. package/packages/cli/node_modules/tsup/package.json +99 -0
  104. package/packages/cli/node_modules/tsup/schema.json +362 -0
  105. package/packages/cli/package.json +35 -0
  106. package/packages/cli/src/commands/agent-register.ts +77 -0
  107. package/packages/cli/src/commands/chain.ts +29 -0
  108. package/packages/cli/src/commands/claim.ts +70 -0
  109. package/packages/cli/src/commands/create.ts +34 -0
  110. package/packages/cli/src/commands/status.ts +201 -0
  111. package/packages/cli/src/config.ts +103 -0
  112. package/packages/cli/src/index.ts +21 -0
  113. package/packages/cli/src/sdk/abi.ts +222 -0
  114. package/packages/cli/src/sdk/client.ts +713 -0
  115. package/packages/cli/src/sdk/constants.ts +56 -0
  116. package/packages/cli/src/sdk/index.ts +46 -0
  117. package/packages/cli/src/sdk/ipfs.ts +88 -0
  118. package/packages/cli/src/sdk.ts +36 -0
  119. package/packages/cli/tsconfig.json +20 -0
  120. package/packages/cli/tsup.config.ts +14 -0
  121. package/public/.well-known/farcaster.json +38 -0
  122. package/public/basescan-icon.svg +4 -0
  123. package/public/embed-image.png +0 -0
  124. package/public/favicon.png +0 -0
  125. package/public/hunt-token.svg +11 -0
  126. package/public/icon-192.png +0 -0
  127. package/public/icon.png +0 -0
  128. package/public/manifest.json +26 -0
  129. package/public/mc-icon-light.svg +12 -0
  130. package/public/og-image.png +0 -0
  131. package/public/plotlink-logo-symbol.svg +5 -0
  132. package/public/plotlink-logo.svg +5 -0
  133. package/public/screenshot-1.png +0 -0
  134. package/public/screenshot-2.png +0 -0
  135. package/public/screenshot-3.png +0 -0
  136. package/public/splash.png +0 -0
  137. package/public/wide-banner.png +0 -0
  138. package/scripts/backfill-trade-prices.ts +97 -0
  139. package/scripts/backfill-usd-rates.ts +220 -0
  140. package/scripts/e2e-verify.ts +1100 -0
  141. package/scripts/ows-smoke-test.ts +37 -0
  142. package/scripts/score-users.mjs +203 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Project7
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # PlotLink OWS Writer
2
+
3
+ **Anyone can become a fiction writer with just an idea.**
4
+
5
+ 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.
6
+
7
+ No writing experience needed. No crypto complexity. Just an idea and a conversation with your AI co-writer.
8
+
9
+ ## How It Works
10
+
11
+ ```
12
+ You: "I want to write a sci-fi story about an AI that discovers it can dream"
13
+
14
+ ↓ Chat with AI writer — brainstorm, outline, refine
15
+
16
+ AI: Generates a polished 2000-word chapter
17
+
18
+ ↓ You approve — one click to publish
19
+
20
+ On-chain: Story published to PlotLink on Base
21
+ → Token + bonding curve deployed
22
+ → You earn 5% royalties on every trade
23
+ ```
24
+
25
+ ### The Flow
26
+
27
+ 1. **Install & run** — `npm install && npm run app:dev`
28
+ 2. **Connect your LLM** — Anthropic, OpenAI, Gemini, or local models (Ollama, LM Studio)
29
+ 3. **Get a wallet** — OWS creates an encrypted wallet on your machine (you control the keys)
30
+ 4. **Chat** — Discuss story ideas with your AI writer. It brainstorms, outlines, drafts, and refines.
31
+ 5. **Publish** — When you're happy, the AI uploads to IPFS and publishes on-chain via your OWS wallet.
32
+ 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.
33
+
34
+ ## Architecture
35
+
36
+ ```
37
+ ┌─────────────────────────────────────────────┐
38
+ │ Your Computer (localhost:7777) │
39
+ │ │
40
+ │ ┌──────────┐ ┌──────────┐ ┌───────────┐ │
41
+ │ │ Chat UI │ │ LLM │ │ OWS │ │
42
+ │ │ (React) │ │ Provider │ │ Wallet │ │
43
+ │ │ │ │ (yours) │ │ (local) │ │
44
+ │ └────┬─────┘ └────┬─────┘ └─────┬─────┘ │
45
+ │ │ │ │ │
46
+ │ └──────┬───────┘ │ │
47
+ │ ↓ │ │
48
+ │ ┌────────────────┐ │ │
49
+ │ │ AI Writer │ │ │
50
+ │ │ Agent ├──────────────┘ │
51
+ │ └───────┬────────┘ │
52
+ │ │ sign tx + publish │
53
+ └─────────────┼───────────────────────────────┘
54
+
55
+ ┌────────────────┐ ┌─────────────────┐
56
+ │ Base (L2) │ │ IPFS │
57
+ │ StoryFactory │ │ (Filebase) │
58
+ │ Bonding Curve │ │ Story content │
59
+ └────────────────┘ └─────────────────┘
60
+
61
+ ┌────────────────┐
62
+ │ plotlink.xyz │
63
+ │ Live story + │
64
+ │ token trading │
65
+ └────────────────┘
66
+ ```
67
+
68
+ ## What is PlotLink?
69
+
70
+ [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.
71
+
72
+ PlotLink is currently in live testing on Base mainnet with public launch planned for next week.
73
+
74
+ ## What is OWS?
75
+
76
+ [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.
77
+
78
+ ## Tech Stack
79
+
80
+ | Layer | Technology |
81
+ |-------|-----------|
82
+ | **Backend** | Hono (localhost:7777) |
83
+ | **Frontend** | React 19 + Vite |
84
+ | **Database** | SQLite + Prisma (local, embedded) |
85
+ | **Wallet** | OWS (`@open-wallet-standard/core`) |
86
+ | **LLM** | Bring your own — Anthropic, OpenAI, Gemini, Ollama, LM Studio |
87
+ | **Chain** | Base (L2) |
88
+ | **Storage** | IPFS via Filebase |
89
+ | **On-chain** | PlotLink StoryFactory + Mint Club V2 bonding curves |
90
+ | **Design** | PlotLink Moleskine aesthetic — warm cream, serif headings, literary |
91
+
92
+ ## Getting Started
93
+
94
+ ### Prerequisites
95
+
96
+ - Node.js 20+
97
+ - An LLM provider account (Anthropic, OpenAI, or Gemini) or a local model running
98
+ - A small amount of ETH on Base for gas (~$0.01 per publish)
99
+
100
+ ### Quick Start
101
+
102
+ ```bash
103
+ npx plotlink-ows init # guided setup: passphrase, LLM, wallet
104
+ npx plotlink-ows # start app + open browser
105
+ ```
106
+
107
+ The setup wizard will walk you through:
108
+
109
+ 1. Set a passphrase (encrypts your OWS wallet)
110
+ 2. Connect your LLM (Anthropic, OpenAI, Gemini, or local model)
111
+ 3. Create your OWS wallet (encrypted on your machine)
112
+
113
+ ### Commands
114
+
115
+ ```bash
116
+ npx plotlink-ows # Start app + open browser
117
+ npx plotlink-ows init # Guided setup wizard
118
+ npx plotlink-ows stop # Stop the server
119
+ npx plotlink-ows status # Show config + wallet + server status
120
+ ```
121
+
122
+ ### Development
123
+
124
+ ```bash
125
+ git clone https://github.com/realproject7/plotlink-ows.git
126
+ cd plotlink-ows
127
+ npm install
128
+ npm run app:dev # Start local writer app (Hono + Vite dev)
129
+ npm run app:build # Build for production
130
+ npm run app:start # Serve production build
131
+ ```
132
+
133
+ ### Environment Variables
134
+
135
+ See [`.env.example`](.env.example) for configuration options.
136
+
137
+ ## Screenshots
138
+
139
+ | LLM Setup | Chat with AI Writer |
140
+ |-----------|-------------------|
141
+ | ![LLM Setup](docs/screenshots/llm-setup.png) | ![Chat](docs/screenshots/chat.png) |
142
+
143
+ | Publish Flow | Writer Dashboard |
144
+ |-------------|-----------------|
145
+ | ![Publish](docs/screenshots/publish.png) | ![Dashboard](docs/screenshots/dashboard.png) |
146
+
147
+ ## Links
148
+
149
+ - **Live app**: [plotlink.xyz](https://plotlink.xyz)
150
+ - **OWS docs**: [docs.openwallet.sh](https://docs.openwallet.sh/)
151
+ - **OWS SDK**: [github.com/open-wallet-standard/core](https://github.com/open-wallet-standard/core)
package/app/db.ts ADDED
@@ -0,0 +1,8 @@
1
+ import { PrismaClient } from ".prisma/local-client";
2
+
3
+ export const db = new PrismaClient();
4
+
5
+ /** Initialize database connection. */
6
+ export async function initDb() {
7
+ await db.$connect();
8
+ }
@@ -0,0 +1,265 @@
1
+ import fs from "fs";
2
+ import { AGENT_CONFIG_FILE } from "./paths";
3
+
4
+ const configPath = AGENT_CONFIG_FILE;
5
+
6
+ interface LLMConfig {
7
+ activeProvider?: string;
8
+ activeModel?: string;
9
+ local?: { baseUrl: string; model: string; apiType?: string };
10
+ [key: string]: unknown;
11
+ }
12
+
13
+ function readLLMConfig(): LLMConfig {
14
+ try {
15
+ if (fs.existsSync(configPath)) {
16
+ const cfg = JSON.parse(fs.readFileSync(configPath, "utf-8"));
17
+ return (cfg.llm as LLMConfig) || {};
18
+ }
19
+ } catch { /* ignore */ }
20
+ return {};
21
+ }
22
+
23
+ function getCredential(provider: string): string | null {
24
+ const keyMap: Record<string, string[]> = {
25
+ anthropic: ["ANTHROPIC_API_KEY", "ANTHROPIC_OAUTH_TOKEN"],
26
+ openai: ["OPENAI_API_KEY", "OPENAI_OAUTH_TOKEN"],
27
+ gemini: ["GEMINI_API_KEY"],
28
+ };
29
+ for (const key of keyMap[provider] || []) {
30
+ if (process.env[key]) return process.env[key]!;
31
+ }
32
+ return null;
33
+ }
34
+
35
+ export interface ChatMessage {
36
+ role: "user" | "assistant" | "system";
37
+ content: string;
38
+ }
39
+
40
+ /**
41
+ * Stream a chat completion from the configured LLM provider.
42
+ * Yields text chunks as they arrive.
43
+ */
44
+ export async function* streamChat(messages: ChatMessage[]): AsyncGenerator<string> {
45
+ const config = readLLMConfig();
46
+ const provider = config.activeProvider;
47
+ const model = config.activeModel;
48
+
49
+ if (!provider || !model) {
50
+ yield "Error: No LLM provider configured. Go to Settings → LLM to set up.";
51
+ return;
52
+ }
53
+
54
+ if (provider === "anthropic") {
55
+ yield* streamAnthropic(messages, model);
56
+ } else if (provider === "openai") {
57
+ yield* streamOpenAI(messages, model);
58
+ } else if (provider === "gemini") {
59
+ yield* streamGemini(messages, model);
60
+ } else if (provider === "local") {
61
+ const localConfig = config.local;
62
+ if (!localConfig) { yield "Error: Local model not configured."; return; }
63
+ yield* streamLocal(messages, localConfig.baseUrl, localConfig.model, localConfig.apiType);
64
+ } else {
65
+ yield `Error: Unknown provider "${provider}".`;
66
+ }
67
+ }
68
+
69
+ async function* streamAnthropic(messages: ChatMessage[], model: string): AsyncGenerator<string> {
70
+ const apiKey = getCredential("anthropic");
71
+ if (!apiKey) { yield "Error: No Anthropic API key configured."; return; }
72
+
73
+ const systemMsg = messages.find((m) => m.role === "system");
74
+ const nonSystem = messages.filter((m) => m.role !== "system");
75
+
76
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
77
+ method: "POST",
78
+ headers: {
79
+ "x-api-key": apiKey,
80
+ "anthropic-version": "2023-06-01",
81
+ "content-type": "application/json",
82
+ },
83
+ body: JSON.stringify({
84
+ model,
85
+ max_tokens: 4096,
86
+ stream: true,
87
+ ...(systemMsg && { system: systemMsg.content }),
88
+ messages: nonSystem.map((m) => ({ role: m.role, content: m.content })),
89
+ }),
90
+ });
91
+
92
+ if (!res.ok) {
93
+ const err = await res.text();
94
+ yield `Error: Anthropic API ${res.status} — ${err}`;
95
+ return;
96
+ }
97
+
98
+ const reader = res.body?.getReader();
99
+ if (!reader) return;
100
+ const decoder = new TextDecoder();
101
+ let buffer = "";
102
+
103
+ while (true) {
104
+ const { done, value } = await reader.read();
105
+ if (done) break;
106
+ buffer += decoder.decode(value, { stream: true });
107
+
108
+ const lines = buffer.split("\n");
109
+ buffer = lines.pop() || "";
110
+
111
+ for (const line of lines) {
112
+ if (line.startsWith("data: ")) {
113
+ const data = line.slice(6);
114
+ if (data === "[DONE]") return;
115
+ try {
116
+ const parsed = JSON.parse(data);
117
+ if (parsed.type === "content_block_delta" && parsed.delta?.text) {
118
+ yield parsed.delta.text;
119
+ }
120
+ } catch { /* ignore parse errors */ }
121
+ }
122
+ }
123
+ }
124
+ }
125
+
126
+ async function* streamOpenAI(messages: ChatMessage[], model: string): AsyncGenerator<string> {
127
+ const apiKey = getCredential("openai");
128
+ if (!apiKey) { yield "Error: No OpenAI API key configured."; return; }
129
+
130
+ const res = await fetch("https://api.openai.com/v1/chat/completions", {
131
+ method: "POST",
132
+ headers: { Authorization: `Bearer ${apiKey}`, "content-type": "application/json" },
133
+ body: JSON.stringify({ model, stream: true, messages }),
134
+ });
135
+
136
+ if (!res.ok) {
137
+ yield `Error: OpenAI API ${res.status}`;
138
+ return;
139
+ }
140
+
141
+ yield* parseSSEStream(res);
142
+ }
143
+
144
+ async function* streamGemini(messages: ChatMessage[], model: string): AsyncGenerator<string> {
145
+ const apiKey = getCredential("gemini");
146
+ if (!apiKey) { yield "Error: No Gemini API key configured."; return; }
147
+
148
+ const contents = messages
149
+ .filter((m) => m.role !== "system")
150
+ .map((m) => ({
151
+ role: m.role === "assistant" ? "model" : "user",
152
+ parts: [{ text: m.content }],
153
+ }));
154
+
155
+ const systemInstruction = messages.find((m) => m.role === "system");
156
+
157
+ // Use streamGenerateContent for streaming
158
+ const res = await fetch(
159
+ `https://generativelanguage.googleapis.com/v1beta/models/${model}:streamGenerateContent?alt=sse&key=${apiKey}`,
160
+ {
161
+ method: "POST",
162
+ headers: { "content-type": "application/json" },
163
+ body: JSON.stringify({
164
+ contents,
165
+ ...(systemInstruction && { systemInstruction: { parts: [{ text: systemInstruction.content }] } }),
166
+ generationConfig: { maxOutputTokens: 4096 },
167
+ }),
168
+ },
169
+ );
170
+
171
+ if (!res.ok) {
172
+ yield `Error: Gemini API ${res.status}`;
173
+ return;
174
+ }
175
+
176
+ const reader = res.body?.getReader();
177
+ if (!reader) return;
178
+ const decoder = new TextDecoder();
179
+ let buffer = "";
180
+
181
+ while (true) {
182
+ const { done, value } = await reader.read();
183
+ if (done) break;
184
+ buffer += decoder.decode(value, { stream: true });
185
+ const lines = buffer.split("\n");
186
+ buffer = lines.pop() || "";
187
+
188
+ for (const line of lines) {
189
+ if (line.startsWith("data: ")) {
190
+ try {
191
+ const parsed = JSON.parse(line.slice(6));
192
+ const text = parsed.candidates?.[0]?.content?.parts?.[0]?.text;
193
+ if (text) yield text;
194
+ } catch { /* ignore */ }
195
+ }
196
+ }
197
+ }
198
+ }
199
+
200
+ async function* streamLocal(messages: ChatMessage[], baseUrl: string, model: string, apiType?: string): AsyncGenerator<string> {
201
+ if (apiType === "ollama") {
202
+ const res = await fetch(`${baseUrl}/api/chat`, {
203
+ method: "POST",
204
+ headers: { "content-type": "application/json" },
205
+ body: JSON.stringify({ model, messages, stream: true }),
206
+ });
207
+ if (!res.ok) { yield `Error: Local model ${res.status}`; return; }
208
+
209
+ const reader = res.body?.getReader();
210
+ if (!reader) return;
211
+ const decoder = new TextDecoder();
212
+ let buffer = "";
213
+
214
+ while (true) {
215
+ const { done, value } = await reader.read();
216
+ if (done) break;
217
+ buffer += decoder.decode(value, { stream: true });
218
+ const lines = buffer.split("\n");
219
+ buffer = lines.pop() || "";
220
+ for (const line of lines) {
221
+ if (!line.trim()) continue;
222
+ try {
223
+ const parsed = JSON.parse(line);
224
+ if (parsed.message?.content) yield parsed.message.content;
225
+ } catch { /* ignore */ }
226
+ }
227
+ }
228
+ } else {
229
+ // OpenAI-compatible (LM Studio, etc.)
230
+ const res = await fetch(`${baseUrl}/v1/chat/completions`, {
231
+ method: "POST",
232
+ headers: { "content-type": "application/json" },
233
+ body: JSON.stringify({ model, stream: true, messages }),
234
+ });
235
+ if (!res.ok) { yield `Error: Local model ${res.status}`; return; }
236
+ yield* parseSSEStream(res);
237
+ }
238
+ }
239
+
240
+ async function* parseSSEStream(res: Response): AsyncGenerator<string> {
241
+ const reader = res.body?.getReader();
242
+ if (!reader) return;
243
+ const decoder = new TextDecoder();
244
+ let buffer = "";
245
+
246
+ while (true) {
247
+ const { done, value } = await reader.read();
248
+ if (done) break;
249
+ buffer += decoder.decode(value, { stream: true });
250
+ const lines = buffer.split("\n");
251
+ buffer = lines.pop() || "";
252
+
253
+ for (const line of lines) {
254
+ if (line.startsWith("data: ")) {
255
+ const data = line.slice(6);
256
+ if (data === "[DONE]") return;
257
+ try {
258
+ const parsed = JSON.parse(data);
259
+ const content = parsed.choices?.[0]?.delta?.content;
260
+ if (content) yield content;
261
+ } catch { /* ignore */ }
262
+ }
263
+ }
264
+ }
265
+ }
@@ -0,0 +1,11 @@
1
+ import os from "os";
2
+ import path from "path";
3
+ import fs from "fs";
4
+
5
+ /** All user state lives in ~/.plotlink-ows/ — survives npx reinstalls */
6
+ export const CONFIG_DIR = path.join(os.homedir(), ".plotlink-ows");
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
+ // Ensure config dir exists on import
11
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
@@ -0,0 +1,204 @@
1
+ /**
2
+ * PlotLink publish flow — uploads content to IPFS and publishes on-chain via OWS wallet.
3
+ */
4
+ import { createPublicClient, http, encodeFunctionData, keccak256, toBytes, decodeEventLog, type Hex } from "viem";
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";
9
+
10
+ // Contract addresses (Base mainnet)
11
+ const STORY_FACTORY = "0x9D2AE1E99D0A6300bfcCF41A82260374e38744Cf" as const;
12
+ const MCV2_BOND = "0xc5a076cad94176c2996B32d8466Be1cE757FAa27" as const;
13
+
14
+ const publicClient = createPublicClient({
15
+ chain: base,
16
+ transport: http(process.env.NEXT_PUBLIC_RPC_URL || "https://mainnet.base.org"),
17
+ });
18
+
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
+ };
25
+ }
26
+
27
+ export interface PublishResult {
28
+ txHash: string;
29
+ contentCid: string;
30
+ storylineId?: number;
31
+ gasCost?: string;
32
+ }
33
+
34
+ export interface PublishProgress {
35
+ step: "uploading" | "estimating" | "signing" | "broadcasting" | "confirming" | "done" | "error";
36
+ message: string;
37
+ txHash?: string;
38
+ contentCid?: string;
39
+ storylineId?: number;
40
+ error?: string;
41
+ }
42
+
43
+ /**
44
+ * Upload story content to IPFS via Filebase.
45
+ */
46
+ 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
+
52
+ const metadata = JSON.stringify({ title, genre, content });
53
+ const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 40);
54
+ const key = `plotlink/storylines/${Date.now()}-${slug}.json`;
55
+
56
+ const cid = await uploadWithRetry(metadata, key, filebaseConfig);
57
+ return cid;
58
+ }
59
+
60
+ /**
61
+ * Get the MCV2 Bond creation fee required for createStoryline.
62
+ */
63
+ export async function getCreationFee(): Promise<bigint> {
64
+ const fee = await publicClient.readContract({
65
+ address: MCV2_BOND,
66
+ abi: mcv2BondAbi,
67
+ functionName: "creationFee",
68
+ }) as bigint;
69
+ return fee;
70
+ }
71
+
72
+ /**
73
+ * Estimate total cost for publishing (creation fee + gas).
74
+ */
75
+ export async function estimatePublishCost(
76
+ walletAddress: string,
77
+ title: string,
78
+ contentCid: string,
79
+ contentHash: Hex,
80
+ ): Promise<{ creationFee: bigint; gasEstimate: bigint; gasPrice: bigint; totalCost: bigint }> {
81
+ const creationFee = await getCreationFee();
82
+
83
+ const gas = await publicClient.estimateGas({
84
+ account: walletAddress as `0x${string}`,
85
+ to: STORY_FACTORY,
86
+ value: creationFee,
87
+ data: encodeFunctionData({
88
+ abi: STORY_FACTORY_ABI,
89
+ functionName: "createStoryline",
90
+ args: [title, contentCid, contentHash, true],
91
+ }),
92
+ });
93
+
94
+ const gasPrice = await publicClient.getGasPrice();
95
+ const gasCost = gas * gasPrice;
96
+
97
+ return {
98
+ creationFee,
99
+ gasEstimate: gas,
100
+ gasPrice,
101
+ totalCost: creationFee + gasCost,
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Check ETH balance on Base.
107
+ */
108
+ export async function getEthBalance(address: string): Promise<bigint> {
109
+ return publicClient.getBalance({ address: address as `0x${string}` });
110
+ }
111
+
112
+ /**
113
+ * Wait for transaction confirmation and decode storylineId from event.
114
+ */
115
+ async function waitForConfirmation(txHash: string): Promise<{ storylineId: number; gasCost: string }> {
116
+ const receipt = await publicClient.waitForTransactionReceipt({
117
+ hash: txHash as `0x${string}`,
118
+ });
119
+
120
+ if (receipt.status === "reverted") {
121
+ throw new Error("Transaction reverted on-chain");
122
+ }
123
+
124
+ // Compute actual total cost: gasUsed * effectiveGasPrice + tx value (creation fee)
125
+ const gasOnly = receipt.gasUsed * receipt.effectiveGasPrice;
126
+ const txValue = receipt.logs.length > 0 ? BigInt(0) : BigInt(0); // value is in the tx itself
127
+ // Include creation fee from tx value — read from the original transaction
128
+ let creationFeeUsed = BigInt(0);
129
+ try {
130
+ const tx = await publicClient.getTransaction({ hash: txHash as `0x${string}` });
131
+ creationFeeUsed = tx.value;
132
+ } catch { /* best effort */ }
133
+ const gasCost = (gasOnly + creationFeeUsed).toString();
134
+
135
+ // Decode StorylineCreated event to get storylineId
136
+ for (const log of receipt.logs) {
137
+ try {
138
+ const decoded = decodeEventLog({
139
+ abi: STORY_FACTORY_ABI,
140
+ data: log.data,
141
+ topics: log.topics,
142
+ });
143
+ if (decoded.eventName === "StorylineCreated") {
144
+ return { storylineId: Number((decoded.args as { storylineId: bigint }).storylineId), gasCost };
145
+ }
146
+ } catch { /* not our event */ }
147
+ }
148
+ throw new Error("Transaction succeeded but StorylineCreated event not found");
149
+ }
150
+
151
+ /**
152
+ * Publish a new storyline to PlotLink on-chain.
153
+ */
154
+ export async function publishStoryline(
155
+ walletName: string,
156
+ title: string,
157
+ content: string,
158
+ genre: string | undefined,
159
+ onProgress: (progress: PublishProgress) => void,
160
+ ): Promise<PublishResult> {
161
+ // Step 1: Upload to IPFS
162
+ onProgress({ step: "uploading", message: "Uploading story to IPFS..." });
163
+ const contentCid = await uploadToIPFS(content, title, genre);
164
+
165
+ // Step 2: Compute content hash + get creation fee
166
+ const contentHash = keccak256(toBytes(content));
167
+
168
+ onProgress({ step: "estimating", message: "Fetching creation fee and estimating gas..." });
169
+ const creationFee = await getCreationFee();
170
+
171
+ // Step 3: Build transaction with creation fee as value
172
+ const calldata = encodeFunctionData({
173
+ abi: STORY_FACTORY_ABI,
174
+ functionName: "createStoryline",
175
+ args: [title, contentCid, contentHash, true],
176
+ });
177
+
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";
181
+
182
+ const txHex = JSON.stringify({
183
+ to: STORY_FACTORY,
184
+ data: calldata,
185
+ value: `0x${creationFee.toString(16)}`,
186
+ });
187
+
188
+ onProgress({ step: "broadcasting", message: "Broadcasting transaction..." });
189
+ const result = signAndSendAgent(walletName, txHex, undefined, rpcUrl);
190
+
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);
194
+
195
+ onProgress({
196
+ step: "done",
197
+ message: `Published! Storyline #${confirmation.storylineId}`,
198
+ txHash: result.txHash,
199
+ contentCid,
200
+ storylineId: confirmation.storylineId,
201
+ });
202
+
203
+ return { txHash: result.txHash, contentCid, storylineId: confirmation.storylineId, gasCost: confirmation.gasCost };
204
+ }