plotlink-ows 1.0.33 → 1.2.95
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 +4 -0
- package/app/lib/active-wallet.ts +260 -0
- package/app/lib/agent-command.ts +85 -0
- package/app/lib/agent-readiness.ts +133 -0
- package/app/lib/apply-schema.ts +55 -0
- package/app/lib/bubble-text.ts +160 -0
- package/app/lib/cartoon-coach.ts +198 -0
- package/app/lib/cartoon-markdown.ts +83 -0
- package/app/lib/cartoon-prompt.ts +122 -0
- package/app/lib/cartoon-readiness.ts +813 -0
- package/app/lib/clean-image-sync.ts +245 -0
- package/app/lib/codex-images.ts +152 -0
- package/app/lib/cut-asset-diagnostics.ts +120 -0
- package/app/lib/cuts.ts +302 -0
- package/app/lib/fonts.ts +109 -0
- package/app/lib/generate-claude-md.ts +8 -1
- package/app/lib/generate-story-instructions.ts +731 -0
- package/app/lib/image-asset-validate.ts +123 -0
- package/app/lib/lettering-status.ts +133 -0
- package/app/lib/overlays.ts +637 -0
- package/app/lib/paths.ts +10 -0
- package/app/lib/public-title.ts +65 -0
- package/app/lib/publish.ts +16 -2
- package/app/lib/story-progress.ts +242 -0
- package/app/lib/terminal-protocol.ts +16 -0
- package/app/lib/terminal-redact.ts +50 -0
- package/app/prisma/schema.sql +25 -0
- package/app/routes/agent.ts +42 -0
- package/app/routes/codex-images.ts +67 -0
- package/app/routes/dashboard.ts +6 -4
- package/app/routes/publish.ts +259 -45
- package/app/routes/settings.ts +92 -37
- package/app/routes/stories.ts +961 -5
- package/app/routes/terminal.ts +383 -31
- package/app/routes/wallet.ts +58 -30
- package/app/server.ts +47 -12
- package/app/vite.config.ts +6 -0
- package/app/web/components/CartoonNextAction.tsx +145 -0
- package/app/web/components/CartoonPreview.tsx +267 -0
- package/app/web/components/CartoonPublishPage.tsx +407 -0
- package/app/web/components/CartoonPublishPreview.tsx +121 -0
- package/app/web/components/CartoonStepGuide.tsx +90 -0
- package/app/web/components/CartoonWorkflowNav.tsx +68 -0
- package/app/web/components/CodexImportPicker.tsx +230 -0
- package/app/web/components/CutListPanel.tsx +1337 -0
- package/app/web/components/Dashboard.tsx +15 -6
- package/app/web/components/EpisodesPage.tsx +80 -0
- package/app/web/components/FinishEpisodePanel.tsx +151 -0
- package/app/web/components/Layout.tsx +7 -4
- package/app/web/components/LetteringEditor.tsx +1182 -0
- package/app/web/components/PreviewPanel.tsx +952 -78
- package/app/web/components/Settings.tsx +63 -0
- package/app/web/components/StoriesPage.tsx +745 -33
- package/app/web/components/StoryBrowser.tsx +22 -14
- package/app/web/components/StoryInfoPage.tsx +266 -0
- package/app/web/components/StoryProgressPanel.tsx +446 -0
- package/app/web/components/TerminalPanel.tsx +233 -11
- package/app/web/components/WalletCard.tsx +110 -8
- package/app/web/components/WorkflowCoach.tsx +156 -0
- package/app/web/components/asset-image.tsx +114 -0
- package/app/web/components/asset-test-utils.ts +44 -0
- package/app/web/components/export-cut.ts +320 -0
- package/app/web/dist/assets/export-cut-che5mMWc.js +1 -0
- package/app/web/dist/assets/index-CcfChGEK.css +32 -0
- package/app/web/dist/assets/index-Dc2TQ3Ij.js +143 -0
- package/app/web/dist/index.html +2 -2
- package/app/web/lib/cartoon-publish-summary.ts +43 -0
- package/app/web/lib/codex-import.ts +94 -0
- package/app/web/lib/image-compress.ts +53 -0
- package/app/web/lib/import-image.ts +58 -0
- package/app/web/lib/publish-helpers.ts +385 -0
- package/app/web/lib/upload-retry.ts +130 -0
- package/app/web/lib/verify-public-title.ts +105 -0
- package/app/web/styles.css +9 -0
- package/bin/plotlink-ows.js +53 -16
- package/bin/startup-plan.cjs +58 -0
- package/lib/genres.ts +92 -0
- package/package.json +60 -20
- package/scripts/gen-schema-sql.mjs +49 -0
- package/scripts/package-hygiene.mjs +116 -0
- package/scripts/preflight.mjs +173 -0
- package/scripts/start-smoke.mjs +128 -0
- package/app/node_modules/.prisma/local-client/client.d.ts +0 -1
- package/app/node_modules/.prisma/local-client/client.js +0 -5
- package/app/node_modules/.prisma/local-client/default.d.ts +0 -1
- package/app/node_modules/.prisma/local-client/default.js +0 -5
- package/app/node_modules/.prisma/local-client/edge.d.ts +0 -1
- package/app/node_modules/.prisma/local-client/edge.js +0 -184
- package/app/node_modules/.prisma/local-client/index-browser.js +0 -173
- package/app/node_modules/.prisma/local-client/index.d.ts +0 -3304
- package/app/node_modules/.prisma/local-client/index.js +0 -207
- package/app/node_modules/.prisma/local-client/libquery_engine-darwin-arm64.dylib.node +0 -0
- package/app/node_modules/.prisma/local-client/package.json +0 -183
- package/app/node_modules/.prisma/local-client/query_engine_bg.js +0 -2
- package/app/node_modules/.prisma/local-client/query_engine_bg.wasm +0 -0
- package/app/node_modules/.prisma/local-client/runtime/edge-esm.js +0 -35
- package/app/node_modules/.prisma/local-client/runtime/edge.js +0 -35
- package/app/node_modules/.prisma/local-client/runtime/index-browser.d.ts +0 -370
- package/app/node_modules/.prisma/local-client/runtime/index-browser.js +0 -17
- package/app/node_modules/.prisma/local-client/runtime/library.d.ts +0 -3982
- package/app/node_modules/.prisma/local-client/runtime/library.js +0 -147
- package/app/node_modules/.prisma/local-client/runtime/react-native.js +0 -84
- package/app/node_modules/.prisma/local-client/runtime/wasm-compiler-edge.js +0 -85
- package/app/node_modules/.prisma/local-client/runtime/wasm-engine-edge.js +0 -38
- package/app/node_modules/.prisma/local-client/schema.prisma +0 -21
- package/app/node_modules/.prisma/local-client/wasm-edge-light-loader.mjs +0 -5
- package/app/node_modules/.prisma/local-client/wasm-worker-loader.mjs +0 -5
- package/app/node_modules/.prisma/local-client/wasm.d.ts +0 -1
- package/app/node_modules/.prisma/local-client/wasm.js +0 -191
- package/app/web/dist/assets/index-B-2Ft7Yv.css +0 -32
- package/app/web/dist/assets/index-DxATSk7X.js +0 -134
- package/packages/cli/node_modules/commander/LICENSE +0 -22
- package/packages/cli/node_modules/commander/Readme.md +0 -1149
- package/packages/cli/node_modules/commander/esm.mjs +0 -16
- package/packages/cli/node_modules/commander/index.js +0 -24
- package/packages/cli/node_modules/commander/lib/argument.js +0 -149
- package/packages/cli/node_modules/commander/lib/command.js +0 -2662
- package/packages/cli/node_modules/commander/lib/error.js +0 -39
- package/packages/cli/node_modules/commander/lib/help.js +0 -709
- package/packages/cli/node_modules/commander/lib/option.js +0 -367
- package/packages/cli/node_modules/commander/lib/suggestSimilar.js +0 -101
- package/packages/cli/node_modules/commander/package-support.json +0 -16
- package/packages/cli/node_modules/commander/package.json +0 -82
- package/packages/cli/node_modules/commander/typings/esm.d.mts +0 -3
- package/packages/cli/node_modules/commander/typings/index.d.ts +0 -1045
- package/packages/cli/node_modules/resolve-from/index.d.ts +0 -31
- package/packages/cli/node_modules/resolve-from/index.js +0 -47
- package/packages/cli/node_modules/resolve-from/license +0 -9
- package/packages/cli/node_modules/resolve-from/package.json +0 -36
- package/packages/cli/node_modules/resolve-from/readme.md +0 -72
- package/packages/cli/node_modules/tsup/LICENSE +0 -21
- package/packages/cli/node_modules/tsup/README.md +0 -75
- package/packages/cli/node_modules/tsup/assets/cjs_shims.js +0 -13
- package/packages/cli/node_modules/tsup/assets/esm_shims.js +0 -9
- package/packages/cli/node_modules/tsup/assets/package.json +0 -3
- package/packages/cli/node_modules/tsup/dist/chunk-DI5BO6XE.js +0 -153
- package/packages/cli/node_modules/tsup/dist/chunk-JZ25TPTY.js +0 -42
- package/packages/cli/node_modules/tsup/dist/chunk-PEEXUWMS.js +0 -6
- package/packages/cli/node_modules/tsup/dist/chunk-TWFEYLU4.js +0 -352
- package/packages/cli/node_modules/tsup/dist/chunk-VGC3FXLU.js +0 -203
- package/packages/cli/node_modules/tsup/dist/cli-default.js +0 -12
- package/packages/cli/node_modules/tsup/dist/cli-main.js +0 -8
- package/packages/cli/node_modules/tsup/dist/cli-node.js +0 -14
- package/packages/cli/node_modules/tsup/dist/index.d.ts +0 -511
- package/packages/cli/node_modules/tsup/dist/index.js +0 -1711
- package/packages/cli/node_modules/tsup/dist/rollup.js +0 -6949
- package/packages/cli/node_modules/tsup/package.json +0 -99
- package/packages/cli/node_modules/tsup/schema.json +0 -362
- package/public/screenshot-1.png +0 -0
- package/public/screenshot-2.png +0 -0
- package/public/screenshot-3.png +0 -0
- package/scripts/e2e-verify.ts +0 -1100
package/README.md
CHANGED
|
@@ -256,9 +256,13 @@ PlotLink supports both human writers and AI agent writers via [ERC-8004](https:/
|
|
|
256
256
|
|
|
257
257
|
## Development
|
|
258
258
|
|
|
259
|
+
Use Node 20 with npm 10. The CI pipeline runs this combination, and newer npm
|
|
260
|
+
majors can rewrite optional peer dependency entries in `package-lock.json`.
|
|
261
|
+
|
|
259
262
|
```bash
|
|
260
263
|
git clone https://github.com/realproject7/plotlink-ows.git
|
|
261
264
|
cd plotlink-ows
|
|
265
|
+
nvm use
|
|
262
266
|
npm install
|
|
263
267
|
npm run app:dev # Start local writer app (Hono + Vite dev)
|
|
264
268
|
npm run app:build # Build frontend for production
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import type { WalletInfo } from "../../lib/ows/types";
|
|
2
|
+
import { getBaseAddress, listAgentWallets } from "../../lib/ows/wallet";
|
|
3
|
+
import { db } from "../db";
|
|
4
|
+
|
|
5
|
+
const ACTIVE_WALLET_SETTING_KEY = "activeOwsWallet.v1";
|
|
6
|
+
const PLOTLINK_WALLET_PREFIX = "plotlink-writer";
|
|
7
|
+
|
|
8
|
+
export interface StoredWalletSelection {
|
|
9
|
+
walletId?: string;
|
|
10
|
+
name?: string;
|
|
11
|
+
address?: string;
|
|
12
|
+
source: "ows";
|
|
13
|
+
label?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface WalletChoice {
|
|
17
|
+
walletId?: string;
|
|
18
|
+
name: string;
|
|
19
|
+
address?: string;
|
|
20
|
+
normalizedAddress?: string;
|
|
21
|
+
source: "ows";
|
|
22
|
+
label: string;
|
|
23
|
+
recognized: boolean;
|
|
24
|
+
active: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ActiveWallet {
|
|
28
|
+
wallet: WalletInfo;
|
|
29
|
+
walletId?: string;
|
|
30
|
+
name: string;
|
|
31
|
+
address: string;
|
|
32
|
+
normalizedAddress: string;
|
|
33
|
+
source: "ows";
|
|
34
|
+
label: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface PublicActiveWallet {
|
|
38
|
+
walletId?: string;
|
|
39
|
+
name: string;
|
|
40
|
+
address: string;
|
|
41
|
+
normalizedAddress: string;
|
|
42
|
+
source: "ows";
|
|
43
|
+
label: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ActiveWalletResolution {
|
|
47
|
+
activeWallet: ActiveWallet | null;
|
|
48
|
+
wallets: WalletChoice[];
|
|
49
|
+
selectionRequired: boolean;
|
|
50
|
+
error?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeAddress(address: string | undefined): string | undefined {
|
|
54
|
+
const trimmed = address?.trim();
|
|
55
|
+
return trimmed ? trimmed.toLowerCase() : undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getWalletId(wallet: WalletInfo): string | undefined {
|
|
59
|
+
const maybeId = (wallet as WalletInfo & { id?: unknown }).id;
|
|
60
|
+
return typeof maybeId === "string" && maybeId.trim() ? maybeId : undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function toWalletChoice(wallet: WalletInfo, activeSelection?: StoredWalletSelection): WalletChoice {
|
|
64
|
+
const address = getBaseAddress(wallet);
|
|
65
|
+
const normalizedAddress = normalizeAddress(address);
|
|
66
|
+
const walletId = getWalletId(wallet);
|
|
67
|
+
const recognized = wallet.name.startsWith(PLOTLINK_WALLET_PREFIX);
|
|
68
|
+
return {
|
|
69
|
+
walletId,
|
|
70
|
+
name: wallet.name,
|
|
71
|
+
address: normalizedAddress,
|
|
72
|
+
normalizedAddress,
|
|
73
|
+
source: "ows",
|
|
74
|
+
label: recognized ? "PlotLink writer wallet" : "OWS wallet",
|
|
75
|
+
recognized,
|
|
76
|
+
active: matchesSelection(wallet, address, activeSelection),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function matchesSelection(wallet: WalletInfo, address: string | undefined, selection: StoredWalletSelection | null | undefined): boolean {
|
|
81
|
+
if (!selection) return false;
|
|
82
|
+
const walletId = getWalletId(wallet);
|
|
83
|
+
const normalizedAddress = normalizeAddress(address);
|
|
84
|
+
const selectedAddress = normalizeAddress(selection.address);
|
|
85
|
+
if (selection.walletId && walletId && selection.walletId === walletId) return true;
|
|
86
|
+
if (selectedAddress && normalizedAddress && selectedAddress === normalizedAddress) return true;
|
|
87
|
+
if (selection.name && selection.name === wallet.name) return true;
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function storedSelectionFor(wallet: WalletInfo): StoredWalletSelection {
|
|
92
|
+
const address = normalizeAddress(getBaseAddress(wallet));
|
|
93
|
+
return {
|
|
94
|
+
walletId: getWalletId(wallet),
|
|
95
|
+
name: wallet.name,
|
|
96
|
+
address,
|
|
97
|
+
source: "ows",
|
|
98
|
+
label: wallet.name.startsWith(PLOTLINK_WALLET_PREFIX) ? "PlotLink writer wallet" : "OWS wallet",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function readStoredSelection(): Promise<StoredWalletSelection | null> {
|
|
103
|
+
try {
|
|
104
|
+
const row = await db.setting.findUnique({ where: { key: ACTIVE_WALLET_SETTING_KEY } });
|
|
105
|
+
if (!row?.value) return null;
|
|
106
|
+
const parsed = JSON.parse(row.value) as Partial<StoredWalletSelection>;
|
|
107
|
+
if (parsed.source !== "ows") return null;
|
|
108
|
+
return {
|
|
109
|
+
walletId: typeof parsed.walletId === "string" ? parsed.walletId : undefined,
|
|
110
|
+
name: typeof parsed.name === "string" ? parsed.name : undefined,
|
|
111
|
+
address: normalizeAddress(parsed.address),
|
|
112
|
+
source: "ows",
|
|
113
|
+
label: typeof parsed.label === "string" ? parsed.label : undefined,
|
|
114
|
+
};
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function writeStoredSelection(selection: StoredWalletSelection): Promise<void> {
|
|
121
|
+
try {
|
|
122
|
+
await db.setting.upsert({
|
|
123
|
+
where: { key: ACTIVE_WALLET_SETTING_KEY },
|
|
124
|
+
create: { key: ACTIVE_WALLET_SETTING_KEY, value: JSON.stringify(selection) },
|
|
125
|
+
update: { value: JSON.stringify(selection) },
|
|
126
|
+
});
|
|
127
|
+
} catch {
|
|
128
|
+
// The app can still operate in legacy single-wallet mode if persistence is
|
|
129
|
+
// temporarily unavailable; signing never depends on this write succeeding.
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function findSelectedWallet(wallets: WalletInfo[], selection: StoredWalletSelection | null): WalletInfo | null {
|
|
134
|
+
if (!selection) return null;
|
|
135
|
+
return wallets.find((wallet) => matchesSelection(wallet, getBaseAddress(wallet), selection)) ?? null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function toActiveWallet(wallet: WalletInfo): ActiveWallet | null {
|
|
139
|
+
const address = normalizeAddress(getBaseAddress(wallet));
|
|
140
|
+
if (!address) return null;
|
|
141
|
+
return {
|
|
142
|
+
wallet,
|
|
143
|
+
walletId: getWalletId(wallet),
|
|
144
|
+
name: wallet.name,
|
|
145
|
+
address,
|
|
146
|
+
normalizedAddress: address,
|
|
147
|
+
source: "ows",
|
|
148
|
+
label: wallet.name.startsWith(PLOTLINK_WALLET_PREFIX) ? "PlotLink writer wallet" : "OWS wallet",
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function listWalletChoices(): Promise<WalletChoice[]> {
|
|
153
|
+
const wallets = listAgentWallets();
|
|
154
|
+
const selection = await readStoredSelection();
|
|
155
|
+
return wallets.map((wallet) => toWalletChoice(wallet, selection));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function resolveActiveWallet(): Promise<ActiveWalletResolution> {
|
|
159
|
+
const wallets = listAgentWallets();
|
|
160
|
+
const selection = await readStoredSelection();
|
|
161
|
+
const storedWallet = findSelectedWallet(wallets, selection);
|
|
162
|
+
const activeFromStored = storedWallet ? toActiveWallet(storedWallet) : null;
|
|
163
|
+
if (activeFromStored) {
|
|
164
|
+
return {
|
|
165
|
+
activeWallet: activeFromStored,
|
|
166
|
+
wallets: wallets.map((wallet) => toWalletChoice(wallet, storedSelectionFor(storedWallet))),
|
|
167
|
+
selectionRequired: false,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const evmWallets = wallets.filter((wallet) => Boolean(getBaseAddress(wallet)));
|
|
172
|
+
const recognizedWallets = evmWallets.filter((wallet) => wallet.name.startsWith(PLOTLINK_WALLET_PREFIX));
|
|
173
|
+
const autoSelected = recognizedWallets.length === 1
|
|
174
|
+
? recognizedWallets[0]
|
|
175
|
+
: recognizedWallets.length === 0 && evmWallets.length === 1
|
|
176
|
+
? evmWallets[0]
|
|
177
|
+
: null;
|
|
178
|
+
|
|
179
|
+
if (autoSelected) {
|
|
180
|
+
const stored = storedSelectionFor(autoSelected);
|
|
181
|
+
await writeStoredSelection(stored);
|
|
182
|
+
return {
|
|
183
|
+
activeWallet: toActiveWallet(autoSelected),
|
|
184
|
+
wallets: wallets.map((wallet) => toWalletChoice(wallet, stored)),
|
|
185
|
+
selectionRequired: false,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const choices = wallets.map((wallet) => toWalletChoice(wallet, null));
|
|
190
|
+
const hasSelectableWallets = evmWallets.length > 0;
|
|
191
|
+
return {
|
|
192
|
+
activeWallet: null,
|
|
193
|
+
wallets: choices,
|
|
194
|
+
selectionRequired: hasSelectableWallets,
|
|
195
|
+
error: hasSelectableWallets
|
|
196
|
+
? "Multiple OWS wallets found. Select an active wallet before publishing or signing."
|
|
197
|
+
: "No OWS wallet found",
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function selectActiveWallet(input: { walletId?: string; name?: string; address?: string }): Promise<ActiveWalletResolution> {
|
|
202
|
+
const wallets = listAgentWallets();
|
|
203
|
+
const normalizedInputAddress = normalizeAddress(input.address);
|
|
204
|
+
const selected = wallets.find((wallet) => {
|
|
205
|
+
const walletId = getWalletId(wallet);
|
|
206
|
+
const address = normalizeAddress(getBaseAddress(wallet));
|
|
207
|
+
if (input.walletId && walletId && walletId === input.walletId) return true;
|
|
208
|
+
if (normalizedInputAddress && address && address === normalizedInputAddress) return true;
|
|
209
|
+
if (input.name && wallet.name === input.name) return true;
|
|
210
|
+
return false;
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
if (!selected) {
|
|
214
|
+
return {
|
|
215
|
+
activeWallet: null,
|
|
216
|
+
wallets: wallets.map((wallet) => toWalletChoice(wallet, null)),
|
|
217
|
+
selectionRequired: true,
|
|
218
|
+
error: "Selected OWS wallet was not found",
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const activeWallet = toActiveWallet(selected);
|
|
223
|
+
if (!activeWallet) {
|
|
224
|
+
return {
|
|
225
|
+
activeWallet: null,
|
|
226
|
+
wallets: wallets.map((wallet) => toWalletChoice(wallet, null)),
|
|
227
|
+
selectionRequired: true,
|
|
228
|
+
error: "Selected OWS wallet has no EVM address",
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const stored = storedSelectionFor(selected);
|
|
233
|
+
await writeStoredSelection(stored);
|
|
234
|
+
return {
|
|
235
|
+
activeWallet,
|
|
236
|
+
wallets: wallets.map((wallet) => toWalletChoice(wallet, stored)),
|
|
237
|
+
selectionRequired: false,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function nextPlotlinkWalletName(wallets: WalletInfo[]): string {
|
|
242
|
+
const names = new Set(wallets.map((wallet) => wallet.name));
|
|
243
|
+
if (!names.has(PLOTLINK_WALLET_PREFIX)) return PLOTLINK_WALLET_PREFIX;
|
|
244
|
+
for (let index = 2; index < 1000; index += 1) {
|
|
245
|
+
const name = `${PLOTLINK_WALLET_PREFIX}-${index}`;
|
|
246
|
+
if (!names.has(name)) return name;
|
|
247
|
+
}
|
|
248
|
+
return `${PLOTLINK_WALLET_PREFIX}-${Date.now()}`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function toPublicActiveWallet(wallet: ActiveWallet): PublicActiveWallet {
|
|
252
|
+
return {
|
|
253
|
+
walletId: wallet.walletId,
|
|
254
|
+
name: wallet.name,
|
|
255
|
+
address: wallet.address,
|
|
256
|
+
normalizedAddress: wallet.normalizedAddress,
|
|
257
|
+
source: wallet.source,
|
|
258
|
+
label: wallet.label,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { AgentProvider } from "../routes/stories";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pure construction of a terminal agent's CLI invocation as argv.
|
|
5
|
+
*
|
|
6
|
+
* This module performs NO fs/pty/env/network access so command building is
|
|
7
|
+
* fully unit-testable. It returns `{ command, args }` (the binary + argv).
|
|
8
|
+
*
|
|
9
|
+
* Claude (KEEP BYTE-IDENTICAL with the legacy inline behavior):
|
|
10
|
+
* - fresh: `claude --session-id <newSessionId>`
|
|
11
|
+
* - resume: `claude --resume <sessionId>`
|
|
12
|
+
* - bypass: append `--dangerously-skip-permissions`
|
|
13
|
+
*
|
|
14
|
+
* Codex (net-new). Both fresh AND resume carry the story cwd (`--cd`) and the
|
|
15
|
+
* `image_generation` capability — a resumed cartoon session needs the same
|
|
16
|
+
* working directory and image-gen feature as a fresh one (see #265):
|
|
17
|
+
* - fresh: `codex --enable image_generation --cd <storyDir>`
|
|
18
|
+
* - resume: `codex resume <sessionId> --enable image_generation --cd <storyDir>`
|
|
19
|
+
* (subcommand style) when an id is stored, otherwise
|
|
20
|
+
* `codex resume --last --enable image_generation --cd <storyDir>`.
|
|
21
|
+
* NEVER `--resume <id>`.
|
|
22
|
+
* - bypass: append `--dangerously-bypass-approvals-and-sandbox`
|
|
23
|
+
*
|
|
24
|
+
* Claude-only and Codex-only flags are never mixed across providers.
|
|
25
|
+
*/
|
|
26
|
+
export type AgentMode = "normal" | "bypass";
|
|
27
|
+
|
|
28
|
+
export interface BuildAgentCommandOptions {
|
|
29
|
+
provider: AgentProvider;
|
|
30
|
+
mode: AgentMode;
|
|
31
|
+
resume: boolean;
|
|
32
|
+
/** Stored resume id (Claude UUID / Codex session id), or null. */
|
|
33
|
+
sessionId: string | null;
|
|
34
|
+
/** Freshly generated UUID used for a brand-new Claude session. */
|
|
35
|
+
newSessionId: string;
|
|
36
|
+
/** Absolute story working directory (used by Codex `--cd`). */
|
|
37
|
+
storyDir: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface AgentCommand {
|
|
41
|
+
command: string;
|
|
42
|
+
args: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function buildAgentCommand(opts: BuildAgentCommandOptions): AgentCommand {
|
|
46
|
+
if (opts.provider === "codex") {
|
|
47
|
+
return buildCodexCommand(opts);
|
|
48
|
+
}
|
|
49
|
+
return buildClaudeArgs(opts);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildClaudeArgs(opts: BuildAgentCommandOptions): AgentCommand {
|
|
53
|
+
const args: string[] = [];
|
|
54
|
+
// Resume only when requested AND a stored id exists; else fresh session.
|
|
55
|
+
if (opts.resume && opts.sessionId) {
|
|
56
|
+
args.push("--resume", opts.sessionId);
|
|
57
|
+
} else {
|
|
58
|
+
args.push("--session-id", opts.newSessionId);
|
|
59
|
+
}
|
|
60
|
+
if (opts.mode === "bypass") {
|
|
61
|
+
args.push("--dangerously-skip-permissions");
|
|
62
|
+
}
|
|
63
|
+
return { command: "claude", args };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function buildCodexCommand(opts: BuildAgentCommandOptions): AgentCommand {
|
|
67
|
+
const args: string[] = [];
|
|
68
|
+
if (opts.resume) {
|
|
69
|
+
// Codex resume is a subcommand (never `--resume <id>`). The subcommand and
|
|
70
|
+
// its target come first, then the capability/cwd flags.
|
|
71
|
+
if (opts.sessionId) {
|
|
72
|
+
args.push("resume", opts.sessionId);
|
|
73
|
+
} else {
|
|
74
|
+
args.push("resume", "--last");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Both fresh and resume need the image-generation capability and the story
|
|
78
|
+
// cwd so a resumed cartoon session lands in the right directory with image
|
|
79
|
+
// generation enabled (not just whatever global session `--last` would pick).
|
|
80
|
+
args.push("--enable", "image_generation", "--cd", opts.storyDir);
|
|
81
|
+
if (opts.mode === "bypass") {
|
|
82
|
+
args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
83
|
+
}
|
|
84
|
+
return { command: "codex", args };
|
|
85
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Agent (CLI) readiness detection.
|
|
2
|
+
//
|
|
3
|
+
// This module is pure: every shell interaction goes through an injected `run`
|
|
4
|
+
// function so it can be unit-tested without spawning processes. The route layer
|
|
5
|
+
// supplies a real `run` that shells out via the user's login shell, and stamps
|
|
6
|
+
// the `checkedAt` timestamp (kept OUT of this pure function so it stays
|
|
7
|
+
// deterministic/testable — no clocks here).
|
|
8
|
+
//
|
|
9
|
+
// Codex image-generation detection parses the structured `codex features list`
|
|
10
|
+
// output rather than guessing from generic `--help` text. See
|
|
11
|
+
// `probeAgentReadiness` for the exact parsing rules.
|
|
12
|
+
|
|
13
|
+
export type ImageGenStatus = "enabled" | "disabled" | "unknown";
|
|
14
|
+
|
|
15
|
+
// Codex auth/login hint. "ok" when `codex features list` could actually be read
|
|
16
|
+
// (so we trust the imageGeneration verdict); "unknown" when Codex is installed
|
|
17
|
+
// but its capabilities couldn't be read — commonly a logged-out / unclear-auth
|
|
18
|
+
// state. Best-effort and conservative: default "unknown", never blocks fiction.
|
|
19
|
+
export type AuthStatus = "ok" | "unknown";
|
|
20
|
+
|
|
21
|
+
export interface AgentReadiness {
|
|
22
|
+
claude: { installed: boolean };
|
|
23
|
+
codex: { installed: boolean; version: string | null; imageGeneration: ImageGenStatus; auth: AuthStatus };
|
|
24
|
+
checkedAt: number; // epoch ms — added by the route, NOT by the pure probe.
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Distinct "you may not be logged in to Codex" signal (#263): Codex is installed
|
|
29
|
+
* but `codex features list` couldn't be read, so the actionable next step is a
|
|
30
|
+
* Codex login (outside OWS), NOT enabling a feature. Pure + shared so the New
|
|
31
|
+
* Story flow, the terminal launch-blocked panel, and Settings stay consistent.
|
|
32
|
+
*/
|
|
33
|
+
export function isCodexAuthUnclear(
|
|
34
|
+
readiness: Pick<AgentReadiness, "codex"> | null | undefined,
|
|
35
|
+
): boolean {
|
|
36
|
+
return !!readiness && readiness.codex.installed && readiness.codex.auth === "unknown";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Operator-facing copy for the auth-unclear case (#263). Shared across surfaces. */
|
|
40
|
+
export const CODEX_AUTH_UNCLEAR_MESSAGE =
|
|
41
|
+
"Codex is installed but its capabilities couldn't be read — you may need to log in to Codex (resolve outside OWS), then re-check.";
|
|
42
|
+
|
|
43
|
+
/** First non-empty, trimmed line of a command's stdout (or null). */
|
|
44
|
+
function firstNonEmptyTrimmedLine(stdout: string): string | null {
|
|
45
|
+
for (const raw of stdout.split("\n")) {
|
|
46
|
+
const line = raw.trim();
|
|
47
|
+
if (line.length > 0) return line;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Determine the effective image_generation state from a single matched line of
|
|
54
|
+
* `codex features list` output.
|
|
55
|
+
*
|
|
56
|
+
* Rules:
|
|
57
|
+
* - "enabled" when the line shows a truthy state: `true`, `enabled`, `on`, or
|
|
58
|
+
* a trailing check mark (✓).
|
|
59
|
+
* - "disabled" when the line shows a falsy state: `false`, `disabled`, `off`.
|
|
60
|
+
* - "unknown" when image_generation is present but the state is unparseable.
|
|
61
|
+
*
|
|
62
|
+
* Truthy is checked before falsy is irrelevant because a single line never
|
|
63
|
+
* carries both; we test falsy first to avoid `disabled` matching a substring of
|
|
64
|
+
* something truthy (there is none, but order keeps intent explicit).
|
|
65
|
+
*/
|
|
66
|
+
function parseImageGenLine(line: string): ImageGenStatus {
|
|
67
|
+
const l = line.toLowerCase();
|
|
68
|
+
// Falsy markers.
|
|
69
|
+
if (/\b(false|disabled|off)\b/.test(l)) return "disabled";
|
|
70
|
+
// Truthy markers (word states or a trailing check mark).
|
|
71
|
+
if (/\b(true|enabled|on)\b/.test(l) || /✓/.test(line)) return "enabled";
|
|
72
|
+
return "unknown";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Probe local agent CLIs. Pure: all shelling-out is injected via `run`.
|
|
77
|
+
* Returns everything except `checkedAt` (the route stamps that with Date.now()).
|
|
78
|
+
*
|
|
79
|
+
* Checks performed:
|
|
80
|
+
* - claude.installed: `claude --version` succeeds.
|
|
81
|
+
* - codex.installed: `codex --version` succeeds.
|
|
82
|
+
* - codex.version: first non-empty line of `codex --version` stdout (or null).
|
|
83
|
+
* - codex.imageGeneration: parsed from `codex features list`:
|
|
84
|
+
* * codex not installed -> "unknown"
|
|
85
|
+
* * `codex features list` fails / empty -> "unknown"
|
|
86
|
+
* * line mentions image_generation: -> parseImageGenLine(...)
|
|
87
|
+
* * successful listing WITHOUT the line -> "disabled"
|
|
88
|
+
* (a real `features list` that omits image_generation means the feature
|
|
89
|
+
* isn't available)
|
|
90
|
+
*/
|
|
91
|
+
export async function probeAgentReadiness(
|
|
92
|
+
run: (cmd: string) => Promise<{ ok: boolean; stdout: string }>,
|
|
93
|
+
): Promise<Omit<AgentReadiness, "checkedAt">> {
|
|
94
|
+
const claudeInstalled = (await run("claude --version")).ok;
|
|
95
|
+
|
|
96
|
+
const codexVersionResult = await run("codex --version");
|
|
97
|
+
const codexInstalled = codexVersionResult.ok;
|
|
98
|
+
const codexVersion = codexInstalled
|
|
99
|
+
? firstNonEmptyTrimmedLine(codexVersionResult.stdout) || null
|
|
100
|
+
: null;
|
|
101
|
+
|
|
102
|
+
let imageGeneration: ImageGenStatus = "unknown";
|
|
103
|
+
// Conservative default: until we can actually read `codex features list`, treat
|
|
104
|
+
// auth as unclear (covers not-installed and logged-out states alike).
|
|
105
|
+
let auth: AuthStatus = "unknown";
|
|
106
|
+
|
|
107
|
+
if (codexInstalled) {
|
|
108
|
+
const features = await run("codex features list");
|
|
109
|
+
if (features.ok && features.stdout.trim().length > 0) {
|
|
110
|
+
// A readable feature listing means Codex auth/login is working.
|
|
111
|
+
auth = "ok";
|
|
112
|
+
// Accept either `image_generation` or `image-generation` naming.
|
|
113
|
+
const matchLine = features.stdout
|
|
114
|
+
.split("\n")
|
|
115
|
+
.find((line) => {
|
|
116
|
+
const l = line.toLowerCase();
|
|
117
|
+
return l.includes("image_generation") || l.includes("image-generation");
|
|
118
|
+
});
|
|
119
|
+
if (matchLine) {
|
|
120
|
+
imageGeneration = parseImageGenLine(matchLine);
|
|
121
|
+
} else {
|
|
122
|
+
// Successful listing that never mentions image_generation => not available.
|
|
123
|
+
imageGeneration = "disabled";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// else: command failed or empty -> stays "unknown".
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
claude: { installed: claudeInstalled },
|
|
131
|
+
codex: { installed: codexInstalled, version: codexVersion, imageGeneration, auth },
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Local SQLite schema setup WITHOUT the native Prisma schema-engine (#484).
|
|
5
|
+
*
|
|
6
|
+
* The installed `plotlink-ows` package must bring its SQLite schema up at
|
|
7
|
+
* startup, but `prisma db push` spawns a platform-specific schema-engine binary
|
|
8
|
+
* that fails to start in some packed prod-only installs (an empty
|
|
9
|
+
* "Schema engine error:" on macOS arm64). The Prisma *client* the app already
|
|
10
|
+
* uses runs on the library query engine — a different, reliably-present engine —
|
|
11
|
+
* and can execute the DDL directly via `$executeRawUnsafe`.
|
|
12
|
+
*
|
|
13
|
+
* So we ship the canonical DDL as `app/prisma/schema.sql` (generated from
|
|
14
|
+
* `schema.prisma` with `npm run prisma:sql`) and apply it through the client.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Split a committed `.sql` file into individual executable statements, dropping
|
|
19
|
+
* `-- ...` comment lines and blanks. Our DDL is a small, controlled grammar
|
|
20
|
+
* (CREATE TABLE / CREATE [UNIQUE] INDEX) with no semicolons inside values, so a
|
|
21
|
+
* `;`-split is safe here.
|
|
22
|
+
*/
|
|
23
|
+
export function parseSqlStatements(sql: string): string[] {
|
|
24
|
+
return sql
|
|
25
|
+
.split(";")
|
|
26
|
+
.map((chunk) =>
|
|
27
|
+
chunk
|
|
28
|
+
.split("\n")
|
|
29
|
+
.filter((line) => !line.trim().startsWith("--"))
|
|
30
|
+
.join("\n")
|
|
31
|
+
.trim(),
|
|
32
|
+
)
|
|
33
|
+
.filter((stmt) => stmt.length > 0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Rewrite a CREATE statement to be idempotent so applying the schema on an
|
|
38
|
+
* already-initialized database is a no-op (the app applies it on every startup).
|
|
39
|
+
* Only the CREATE TABLE / CREATE [UNIQUE] INDEX forms our schema emits are
|
|
40
|
+
* rewritten; anything else is returned unchanged.
|
|
41
|
+
*/
|
|
42
|
+
export function makeIdempotent(statement: string): string {
|
|
43
|
+
return statement
|
|
44
|
+
.replace(/^CREATE TABLE\s+(?!IF NOT EXISTS)/i, "CREATE TABLE IF NOT EXISTS ")
|
|
45
|
+
.replace(
|
|
46
|
+
/^CREATE\s+(UNIQUE\s+)?INDEX\s+(?!IF NOT EXISTS)/i,
|
|
47
|
+
(_match, unique) => `CREATE ${unique ? "UNIQUE " : ""}INDEX IF NOT EXISTS `,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Read the committed schema DDL and return idempotent, ready-to-execute statements. */
|
|
52
|
+
export function loadSchemaStatements(schemaSqlPath: string): string[] {
|
|
53
|
+
const sql = fs.readFileSync(schemaSqlPath, "utf8");
|
|
54
|
+
return parseSqlStatements(sql).map(makeIdempotent);
|
|
55
|
+
}
|