lightnode-sdk 0.8.0 → 0.8.2

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/dist/add.d.ts CHANGED
@@ -119,6 +119,20 @@ export interface LayoutPatch {
119
119
  * Returns what happened so the CLI can report it; never throws.
120
120
  */
121
121
  export declare function patchLayoutWithProviders(cwd?: string): LayoutPatch;
122
+ export declare const SCAFFOLD_GLOBALS_CSS = "@import \"tailwindcss\";\n\n/* dark mode via .dark class (we default the app to dark) */\n@custom-variant dark (&:is(.dark, .dark *));\n\n/* design tokens (light) - ported from lcai-chat-v2 */\n:root {\n font-family: var(--font-inter), ui-sans-serif, system-ui, sans-serif;\n\n --background: #ffffff;\n --primary: #6767e9;\n --primary-600: #5a4fd8;\n --foreground: #09090b;\n --card: #ffffff;\n --card-foreground: #09090b;\n --popover: #ffffff;\n --popover-foreground: hsl(240 10% 3.9%);\n --primary-foreground: #fafafa;\n --secondary: hsl(240 4.8% 95.9%);\n --secondary-foreground: hsl(240 5.9% 10%);\n --muted: hsl(240 4.8% 95.9%);\n --muted-foreground: hsl(240 3.8% 46.1%);\n --accent: hsl(240 4.8% 95.9%);\n --accent-foreground: hsl(240 5.9% 10%);\n --destructive: #ef4d6a;\n --destructive-foreground: hsl(0 0% 98%);\n --success: #15bd77;\n --warning: #eaa53d;\n --border: hsl(240 5.9% 90%);\n --input: hsl(240 5.9% 90%);\n --ring: hsl(240 10% 3.9%);\n --radius: 0.625rem;\n\n --surface-base-subtle: rgba(34, 35, 42, 0.02);\n --surface-base-faint: rgba(14, 18, 27, 0.04);\n --surface-base-light: rgba(204, 206, 239, 0.16);\n --surface-elevation-light: #ffffff;\n\n --content-primary: #0f0f14;\n --content-default: #373842;\n --content-soft: #656678;\n --content-extraLight: #9798b6;\n\n --border-soft: rgba(14, 18, 27, 0.08);\n --border-light: rgba(14, 18, 27, 0.06);\n}\n\n/* design tokens (dark) */\n.dark {\n --background: #070710;\n --foreground: hsl(0 0% 98%);\n --card: #0f0f14;\n --card-foreground: hsl(0 0% 98%);\n --popover: #0f0f14;\n --popover-foreground: hsl(0 0% 98%);\n --primary: #7064e9;\n --primary-600: #8c71f6;\n --primary-foreground: hsl(0 0% 98%);\n --secondary: hsl(240 3.7% 15.9%);\n --secondary-foreground: hsl(0 0% 98%);\n --muted: hsl(240 3.7% 15.9%);\n --muted-foreground: hsl(240 5% 64.9%);\n --accent: hsl(240 3.7% 15.9%);\n --accent-foreground: hsl(0 0% 98%);\n --destructive: #fb5a76;\n --destructive-foreground: hsl(0 0% 98%);\n --success: #22d68a;\n --warning: #f5be5c;\n --border: hsl(240 3.7% 15.9%);\n --input: hsl(240 3.7% 15.9%);\n --ring: hsl(240 4.9% 83.9%);\n\n --surface-base-subtle: rgba(204, 206, 239, 0.02);\n --surface-base-faint: rgba(204, 206, 239, 0.04);\n --surface-base-light: rgba(204, 206, 239, 0.08);\n --surface-elevation-light: #0f0f14;\n\n --content-primary: #cccef0;\n --content-default: #9798b6;\n --content-soft: rgba(154, 156, 207, 0.8);\n --content-extraLight: #9798b6;\n\n --border-soft: rgba(204, 206, 239, 0.12);\n --border-light: rgba(204, 206, 239, 0.08);\n}\n\n/* theme mapping (Tailwind v4 @theme) */\n@theme inline {\n --radius-md: calc(var(--radius) - 2px);\n --radius-sm: calc(var(--radius) - 4px);\n --radius-lg: var(--radius);\n\n --color-background: var(--background);\n --color-foreground: var(--foreground);\n --color-card: var(--card);\n --color-card-foreground: var(--card-foreground);\n --color-popover: var(--popover);\n --color-popover-foreground: var(--popover-foreground);\n --color-primary: var(--primary);\n --color-primary-600: var(--primary-600);\n --color-primary-foreground: var(--primary-foreground);\n --color-secondary: var(--secondary);\n --color-secondary-foreground: var(--secondary-foreground);\n --color-muted: var(--muted);\n --color-muted-foreground: var(--muted-foreground);\n --color-accent: var(--accent);\n --color-accent-foreground: var(--accent-foreground);\n --color-destructive: var(--destructive);\n --color-destructive-foreground: var(--destructive-foreground);\n --color-success: var(--success);\n --color-warning: var(--warning);\n --color-border: var(--border);\n --color-input: var(--input);\n --color-ring: var(--ring);\n\n --color-surface-base-subtle: var(--surface-base-subtle);\n --color-surface-base-faint: var(--surface-base-faint);\n --color-surface-base-light: var(--surface-base-light);\n --color-surface-elevation-light: var(--surface-elevation-light);\n --color-surface-base-brand-default: #693ee0;\n --color-surface-base-brand-strong: #8c71f6;\n\n --color-content-primary: var(--content-primary);\n --color-content-default: var(--content-default);\n --color-content-soft: var(--content-soft);\n --color-content-extraLight: var(--content-extraLight);\n\n --color-bdr-soft: var(--border-soft);\n --color-bdr-light: var(--border-light);\n\n --color-gradient-primary: linear-gradient(270deg, #7064e9 0%, #dd00ac 100%);\n}\n\n@layer base {\n * {\n border-color: var(--border);\n }\n body {\n background-color: var(--background);\n color: var(--foreground);\n overflow-x: hidden;\n }\n html {\n overflow-x: hidden;\n }\n button {\n cursor: pointer;\n }\n button:disabled {\n cursor: not-allowed;\n }\n /* visible keyboard focus across interactive elements */\n a:focus-visible,\n button:focus-visible,\n input:focus-visible,\n select:focus-visible,\n textarea:focus-visible {\n outline: 2px solid var(--primary);\n outline-offset: 2px;\n border-radius: 6px;\n }\n}\n\n/* respect reduced-motion: kill non-essential animation */\n@media (prefers-reduced-motion: reduce) {\n *,\n ::before,\n ::after {\n animation-duration: 0.001ms !important;\n animation-iteration-count: 1 !important;\n transition-duration: 0.001ms !important;\n scroll-behavior: auto !important;\n }\n}\n\n/* ambient app background (gradient mesh behind everything) */\nbody::before {\n content: \"\";\n position: fixed;\n inset: 0;\n z-index: -1;\n pointer-events: none;\n background:\n radial-gradient(60% 50% at 50% -6%, rgba(221, 0, 172, 0.10), transparent 60%),\n radial-gradient(55% 45% at 12% -8%, rgba(112, 100, 233, 0.14), transparent 60%),\n radial-gradient(50% 40% at 88% -2%, rgba(112, 100, 233, 0.12), transparent 60%),\n radial-gradient(45% 45% at 50% 115%, rgba(112, 100, 233, 0.07), transparent 60%);\n}\n.dark body::before {\n background:\n radial-gradient(60% 50% at 50% -6%, rgba(221, 0, 172, 0.12), transparent 60%),\n radial-gradient(55% 45% at 12% -8%, rgba(112, 100, 233, 0.18), transparent 60%),\n radial-gradient(50% 40% at 88% -2%, rgba(112, 100, 233, 0.14), transparent 60%),\n radial-gradient(45% 45% at 50% 118%, rgba(112, 100, 233, 0.10), transparent 60%);\n}\n\n/* signature lcai gradient (primary buttons / accents) */\n.bg-gradient-primary {\n background-image: var(--color-gradient-primary);\n}\n.text-gradient {\n background: linear-gradient(94deg, #dd00ac 10%, #7130c3 53%, #7064e9 96%);\n -webkit-background-clip: text;\n background-clip: text;\n color: transparent;\n}\n\n/* minimal scrollbar */\n::-webkit-scrollbar {\n width: 6px;\n height: 6px;\n}\n::-webkit-scrollbar-track {\n background: transparent;\n}\n::-webkit-scrollbar-thumb {\n background: var(--border);\n border-radius: 3px;\n}\n* {\n scrollbar-width: thin;\n scrollbar-color: var(--border) transparent;\n}\n";
123
+ export interface ScaffoldWiring {
124
+ written: WrittenFile[];
125
+ /** The route now also served at `/` (e.g. "/chat-web3"), or null if nothing was wired. */
126
+ homepageRoute: string | null;
127
+ /** Whether the `dark` class was added to <html> (dark kept as the default). */
128
+ darkDefault: boolean;
129
+ }
130
+ /**
131
+ * Wire a freshly scaffolded Next.js app so the generated -web3 page is the
132
+ * homepage and the LightChain theme + dark default are in place. No-op for any
133
+ * target without a known route folder.
134
+ */
135
+ export declare function wireFreshScaffold(target: string, opts?: AddOpts): ScaffoldWiring;
122
136
  export declare function addJudge(opts?: AddOpts): {
123
137
  written: WrittenFile[];
124
138
  install: string;
package/dist/add.js CHANGED
@@ -1074,83 +1074,99 @@ export default function ChatWeb3() {
1074
1074
  }
1075
1075
 
1076
1076
  return (
1077
- <main style={{ maxWidth: 720, margin: "32px auto", padding: 16, fontFamily: "system-ui" }}>
1078
- <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 }}>
1079
- <h1>Chat (user-pays)</h1>
1080
- <ConnectButton />
1081
- </div>
1082
- <p style={{ color: "#666" }}>
1083
- Each turn signs one createSession transaction from your wallet on{" "}
1084
- <code>{network ?? "(connect a wallet)"}</code>. Fee:{" "}
1085
- <code>{feeLcai != null ? \`\${feeLcai} LCAI\` : "(fetching)"}</code> per turn plus a small gas amount.
1086
- </p>
1087
- {!address && (
1088
- <div style={{ border: "1px solid #ddd", borderRadius: 8, padding: 12, margin: "12px 0", display: "flex", alignItems: "center", gap: 12 }}>
1089
- <span>Connect a wallet to start chatting.</span>
1090
- <ConnectButton />
1077
+ <main className="mx-auto flex min-h-screen w-full max-w-2xl flex-col px-4 py-6">
1078
+ <header className="flex items-center justify-between gap-3 border-b border-border pb-4">
1079
+ <div className="min-w-0">
1080
+ <h1 className="text-base font-semibold text-foreground">Chat</h1>
1081
+ <p className="truncate text-xs text-muted-foreground">
1082
+ {network ? (
1083
+ <>Signed from your wallet on {network} · {feeLcai != null ? feeLcai + " LCAI" : "..."}/turn + gas</>
1084
+ ) : (
1085
+ "Connect a wallet on LightChain to start"
1086
+ )}
1087
+ </p>
1091
1088
  </div>
1092
- )}
1089
+ <ConnectButton />
1090
+ </header>
1093
1091
 
1094
- <div style={{ display: "flex", flexDirection: "column", gap: 8, margin: "16px 0" }}>
1095
- {turns.map((t, i) => (
1096
- <div
1097
- key={i}
1098
- style={{
1099
- alignSelf: t.role === "user" ? "flex-end" : "flex-start",
1100
- maxWidth: "85%",
1101
- borderRadius: 12,
1102
- padding: "8px 12px",
1103
- background: t.role === "user" ? "#e9e7ff" : "#f5f5f7",
1104
- }}
1105
- >
1106
- <div style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>{t.text}</div>
1107
- {t.role === "assistant" && t.submitTx ? (
1108
- <div style={{ marginTop: 6, fontSize: 11, color: "#666", display: "flex", gap: 8, flexWrap: "wrap" }}>
1109
- {t.worker && (
1110
- <a href={\`https://\${network}.lightscan.app/address/\${t.worker}\`} target="_blank" rel="noopener noreferrer">
1111
- worker
1112
- </a>
1113
- )}
1114
- {t.jobId && <span>job #{t.jobId}</span>}
1115
- {t.submitTx && (
1116
- <a href={\`https://\${network}.lightscan.app/tx/\${t.submitTx}\`} target="_blank" rel="noopener noreferrer">
1117
- submitJob
1118
- </a>
1119
- )}
1120
- {t.jobCompletedTx && (
1121
- <a href={\`https://\${network}.lightscan.app/tx/\${t.jobCompletedTx}\`} target="_blank" rel="noopener noreferrer">
1122
- completed
1123
- </a>
1124
- )}
1092
+ <div className="flex flex-1 flex-col gap-3 overflow-y-auto py-6">
1093
+ {turns.length === 0 ? (
1094
+ <div className="flex flex-1 flex-col items-center justify-center gap-3 text-center">
1095
+ <h2 className="text-2xl font-medium text-foreground">How can I help?</h2>
1096
+ <p className="max-w-sm text-sm text-muted-foreground">
1097
+ Connect your wallet and send a message. Each turn is signed and paid from your
1098
+ own wallet, no backend required.
1099
+ </p>
1100
+ {!address && (
1101
+ <div className="mt-2">
1102
+ <ConnectButton />
1125
1103
  </div>
1126
- ) : null}
1104
+ )}
1127
1105
  </div>
1128
- ))}
1106
+ ) : (
1107
+ turns.map((t, i) => (
1108
+ <div
1109
+ key={i}
1110
+ className={
1111
+ "max-w-[85%] whitespace-pre-wrap break-words rounded-2xl px-4 py-2.5 text-sm " +
1112
+ (t.role === "user"
1113
+ ? "self-end rounded-br-md bg-primary text-primary-foreground"
1114
+ : "self-start rounded-bl-md border border-border bg-card text-foreground")
1115
+ }
1116
+ >
1117
+ <div>{t.text}</div>
1118
+ {t.role === "assistant" && t.submitTx ? (
1119
+ <div className="mt-2 flex flex-wrap gap-3 text-[11px] text-muted-foreground">
1120
+ {t.worker && (
1121
+ <a className="hover:text-foreground hover:underline" href={\`https://\${network}.lightscan.app/address/\${t.worker}\`} target="_blank" rel="noopener noreferrer">
1122
+ worker
1123
+ </a>
1124
+ )}
1125
+ {t.jobId && <span>job #{t.jobId}</span>}
1126
+ {t.submitTx && (
1127
+ <a className="hover:text-foreground hover:underline" href={\`https://\${network}.lightscan.app/tx/\${t.submitTx}\`} target="_blank" rel="noopener noreferrer">
1128
+ submitJob
1129
+ </a>
1130
+ )}
1131
+ {t.jobCompletedTx && (
1132
+ <a className="hover:text-foreground hover:underline" href={\`https://\${network}.lightscan.app/tx/\${t.jobCompletedTx}\`} target="_blank" rel="noopener noreferrer">
1133
+ completed
1134
+ </a>
1135
+ )}
1136
+ </div>
1137
+ ) : null}
1138
+ </div>
1139
+ ))
1140
+ )}
1129
1141
  </div>
1130
1142
 
1131
- <textarea
1132
- value={input}
1133
- onChange={(e) => setInput(e.target.value)}
1134
- onKeyDown={(e) => {
1135
- if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); if (!busy && input.trim()) send(); }
1136
- }}
1137
- rows={2}
1138
- placeholder={turns.length === 0 ? "Say hello (cmd+enter to send)" : "Reply..."}
1139
- style={{ width: "100%", padding: 8, fontFamily: "inherit" }}
1140
- />
1141
- <button
1142
- type="button"
1143
- onClick={() => send()}
1144
- disabled={busy || !input.trim() || !address || !network}
1145
- style={{ marginTop: 8, padding: "8px 16px" }}
1146
- >
1147
- {busy ? (busyStage || "Sending...") : (turns.length === 0 ? "Send first message" : "Send")}
1148
- </button>
1149
- {err && (
1150
- <p style={{ marginTop: 8, padding: "8px 12px", border: "1px solid #f5c2c7", background: "#f8d7da", color: "#842029", borderRadius: 6 }}>
1151
- {err}
1152
- </p>
1153
- )}
1143
+ <div className="border-t border-border pt-4">
1144
+ <div className="flex items-end gap-2 rounded-2xl border border-border bg-card p-2 transition focus-within:ring-2 focus-within:ring-primary">
1145
+ <textarea
1146
+ value={input}
1147
+ onChange={(e) => setInput(e.target.value)}
1148
+ onKeyDown={(e) => {
1149
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); if (!busy && input.trim()) send(); }
1150
+ }}
1151
+ rows={1}
1152
+ placeholder={turns.length === 0 ? "Say hello (cmd+enter to send)" : "Reply... (cmd+enter)"}
1153
+ className="max-h-40 min-h-[40px] flex-1 resize-none bg-transparent px-2 py-2 text-sm text-foreground outline-none placeholder:text-muted-foreground"
1154
+ />
1155
+ <button
1156
+ type="button"
1157
+ onClick={() => send()}
1158
+ disabled={busy || !input.trim() || !address || !network}
1159
+ className="shrink-0 rounded-xl bg-gradient-primary px-4 py-2 text-sm font-medium text-white transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-40"
1160
+ >
1161
+ {busy ? (busyStage || "Sending...") : "Send"}
1162
+ </button>
1163
+ </div>
1164
+ {err && (
1165
+ <p className="mt-2 rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
1166
+ {err}
1167
+ </p>
1168
+ )}
1169
+ </div>
1154
1170
  </main>
1155
1171
  );
1156
1172
  }
@@ -2058,6 +2074,362 @@ export function patchLayoutWithProviders(cwd = process.cwd()) {
2058
2074
  }
2059
2075
  return { path: rel, patched: true };
2060
2076
  }
2077
+ // ---------------------------------------------------------------------------
2078
+ // Fresh-scaffold wiring: when `add <x>-web3` scaffolds a brand-new Next.js app
2079
+ // (bare folder), the create-next-app starter page sits at `/` and the generated
2080
+ // page sits at `/<x>-web3`, so `npm run dev` + localhost:3000 lands on the
2081
+ // starter, not the chat. This makes the generated page the homepage and ships
2082
+ // the LightChain theme so the first render is the real thing.
2083
+ //
2084
+ // Only ever invoked on a scaffold WE just created, so overwriting page.tsx and
2085
+ // globals.css is safe - there is no user content to clobber. In an existing app
2086
+ // none of this runs; the page keeps its dedicated /<x>-web3 route untouched.
2087
+ // ---------------------------------------------------------------------------
2088
+ // The LightChain chat theme (Tailwind v4 tokens, light + dark), ported from the
2089
+ // lightnode app's globals.css. Shipped as the scaffold's app/globals.css so
2090
+ // installs default to the real look instead of the create-next-app starter.
2091
+ export const SCAFFOLD_GLOBALS_CSS = `@import "tailwindcss";
2092
+
2093
+ /* dark mode via .dark class (we default the app to dark) */
2094
+ @custom-variant dark (&:is(.dark, .dark *));
2095
+
2096
+ /* design tokens (light) - ported from lcai-chat-v2 */
2097
+ :root {
2098
+ font-family: var(--font-inter), ui-sans-serif, system-ui, sans-serif;
2099
+
2100
+ --background: #ffffff;
2101
+ --primary: #6767e9;
2102
+ --primary-600: #5a4fd8;
2103
+ --foreground: #09090b;
2104
+ --card: #ffffff;
2105
+ --card-foreground: #09090b;
2106
+ --popover: #ffffff;
2107
+ --popover-foreground: hsl(240 10% 3.9%);
2108
+ --primary-foreground: #fafafa;
2109
+ --secondary: hsl(240 4.8% 95.9%);
2110
+ --secondary-foreground: hsl(240 5.9% 10%);
2111
+ --muted: hsl(240 4.8% 95.9%);
2112
+ --muted-foreground: hsl(240 3.8% 46.1%);
2113
+ --accent: hsl(240 4.8% 95.9%);
2114
+ --accent-foreground: hsl(240 5.9% 10%);
2115
+ --destructive: #ef4d6a;
2116
+ --destructive-foreground: hsl(0 0% 98%);
2117
+ --success: #15bd77;
2118
+ --warning: #eaa53d;
2119
+ --border: hsl(240 5.9% 90%);
2120
+ --input: hsl(240 5.9% 90%);
2121
+ --ring: hsl(240 10% 3.9%);
2122
+ --radius: 0.625rem;
2123
+
2124
+ --surface-base-subtle: rgba(34, 35, 42, 0.02);
2125
+ --surface-base-faint: rgba(14, 18, 27, 0.04);
2126
+ --surface-base-light: rgba(204, 206, 239, 0.16);
2127
+ --surface-elevation-light: #ffffff;
2128
+
2129
+ --content-primary: #0f0f14;
2130
+ --content-default: #373842;
2131
+ --content-soft: #656678;
2132
+ --content-extraLight: #9798b6;
2133
+
2134
+ --border-soft: rgba(14, 18, 27, 0.08);
2135
+ --border-light: rgba(14, 18, 27, 0.06);
2136
+ }
2137
+
2138
+ /* design tokens (dark) */
2139
+ .dark {
2140
+ --background: #070710;
2141
+ --foreground: hsl(0 0% 98%);
2142
+ --card: #0f0f14;
2143
+ --card-foreground: hsl(0 0% 98%);
2144
+ --popover: #0f0f14;
2145
+ --popover-foreground: hsl(0 0% 98%);
2146
+ --primary: #7064e9;
2147
+ --primary-600: #8c71f6;
2148
+ --primary-foreground: hsl(0 0% 98%);
2149
+ --secondary: hsl(240 3.7% 15.9%);
2150
+ --secondary-foreground: hsl(0 0% 98%);
2151
+ --muted: hsl(240 3.7% 15.9%);
2152
+ --muted-foreground: hsl(240 5% 64.9%);
2153
+ --accent: hsl(240 3.7% 15.9%);
2154
+ --accent-foreground: hsl(0 0% 98%);
2155
+ --destructive: #fb5a76;
2156
+ --destructive-foreground: hsl(0 0% 98%);
2157
+ --success: #22d68a;
2158
+ --warning: #f5be5c;
2159
+ --border: hsl(240 3.7% 15.9%);
2160
+ --input: hsl(240 3.7% 15.9%);
2161
+ --ring: hsl(240 4.9% 83.9%);
2162
+
2163
+ --surface-base-subtle: rgba(204, 206, 239, 0.02);
2164
+ --surface-base-faint: rgba(204, 206, 239, 0.04);
2165
+ --surface-base-light: rgba(204, 206, 239, 0.08);
2166
+ --surface-elevation-light: #0f0f14;
2167
+
2168
+ --content-primary: #cccef0;
2169
+ --content-default: #9798b6;
2170
+ --content-soft: rgba(154, 156, 207, 0.8);
2171
+ --content-extraLight: #9798b6;
2172
+
2173
+ --border-soft: rgba(204, 206, 239, 0.12);
2174
+ --border-light: rgba(204, 206, 239, 0.08);
2175
+ }
2176
+
2177
+ /* theme mapping (Tailwind v4 @theme) */
2178
+ @theme inline {
2179
+ --radius-md: calc(var(--radius) - 2px);
2180
+ --radius-sm: calc(var(--radius) - 4px);
2181
+ --radius-lg: var(--radius);
2182
+
2183
+ --color-background: var(--background);
2184
+ --color-foreground: var(--foreground);
2185
+ --color-card: var(--card);
2186
+ --color-card-foreground: var(--card-foreground);
2187
+ --color-popover: var(--popover);
2188
+ --color-popover-foreground: var(--popover-foreground);
2189
+ --color-primary: var(--primary);
2190
+ --color-primary-600: var(--primary-600);
2191
+ --color-primary-foreground: var(--primary-foreground);
2192
+ --color-secondary: var(--secondary);
2193
+ --color-secondary-foreground: var(--secondary-foreground);
2194
+ --color-muted: var(--muted);
2195
+ --color-muted-foreground: var(--muted-foreground);
2196
+ --color-accent: var(--accent);
2197
+ --color-accent-foreground: var(--accent-foreground);
2198
+ --color-destructive: var(--destructive);
2199
+ --color-destructive-foreground: var(--destructive-foreground);
2200
+ --color-success: var(--success);
2201
+ --color-warning: var(--warning);
2202
+ --color-border: var(--border);
2203
+ --color-input: var(--input);
2204
+ --color-ring: var(--ring);
2205
+
2206
+ --color-surface-base-subtle: var(--surface-base-subtle);
2207
+ --color-surface-base-faint: var(--surface-base-faint);
2208
+ --color-surface-base-light: var(--surface-base-light);
2209
+ --color-surface-elevation-light: var(--surface-elevation-light);
2210
+ --color-surface-base-brand-default: #693ee0;
2211
+ --color-surface-base-brand-strong: #8c71f6;
2212
+
2213
+ --color-content-primary: var(--content-primary);
2214
+ --color-content-default: var(--content-default);
2215
+ --color-content-soft: var(--content-soft);
2216
+ --color-content-extraLight: var(--content-extraLight);
2217
+
2218
+ --color-bdr-soft: var(--border-soft);
2219
+ --color-bdr-light: var(--border-light);
2220
+
2221
+ --color-gradient-primary: linear-gradient(270deg, #7064e9 0%, #dd00ac 100%);
2222
+ }
2223
+
2224
+ @layer base {
2225
+ * {
2226
+ border-color: var(--border);
2227
+ }
2228
+ body {
2229
+ background-color: var(--background);
2230
+ color: var(--foreground);
2231
+ overflow-x: hidden;
2232
+ }
2233
+ html {
2234
+ overflow-x: hidden;
2235
+ }
2236
+ button {
2237
+ cursor: pointer;
2238
+ }
2239
+ button:disabled {
2240
+ cursor: not-allowed;
2241
+ }
2242
+ /* visible keyboard focus across interactive elements */
2243
+ a:focus-visible,
2244
+ button:focus-visible,
2245
+ input:focus-visible,
2246
+ select:focus-visible,
2247
+ textarea:focus-visible {
2248
+ outline: 2px solid var(--primary);
2249
+ outline-offset: 2px;
2250
+ border-radius: 6px;
2251
+ }
2252
+ }
2253
+
2254
+ /* respect reduced-motion: kill non-essential animation */
2255
+ @media (prefers-reduced-motion: reduce) {
2256
+ *,
2257
+ ::before,
2258
+ ::after {
2259
+ animation-duration: 0.001ms !important;
2260
+ animation-iteration-count: 1 !important;
2261
+ transition-duration: 0.001ms !important;
2262
+ scroll-behavior: auto !important;
2263
+ }
2264
+ }
2265
+
2266
+ /* ambient app background (gradient mesh behind everything) */
2267
+ body::before {
2268
+ content: "";
2269
+ position: fixed;
2270
+ inset: 0;
2271
+ z-index: -1;
2272
+ pointer-events: none;
2273
+ background:
2274
+ radial-gradient(60% 50% at 50% -6%, rgba(221, 0, 172, 0.10), transparent 60%),
2275
+ radial-gradient(55% 45% at 12% -8%, rgba(112, 100, 233, 0.14), transparent 60%),
2276
+ radial-gradient(50% 40% at 88% -2%, rgba(112, 100, 233, 0.12), transparent 60%),
2277
+ radial-gradient(45% 45% at 50% 115%, rgba(112, 100, 233, 0.07), transparent 60%);
2278
+ }
2279
+ .dark body::before {
2280
+ background:
2281
+ radial-gradient(60% 50% at 50% -6%, rgba(221, 0, 172, 0.12), transparent 60%),
2282
+ radial-gradient(55% 45% at 12% -8%, rgba(112, 100, 233, 0.18), transparent 60%),
2283
+ radial-gradient(50% 40% at 88% -2%, rgba(112, 100, 233, 0.14), transparent 60%),
2284
+ radial-gradient(45% 45% at 50% 118%, rgba(112, 100, 233, 0.10), transparent 60%);
2285
+ }
2286
+
2287
+ /* signature lcai gradient (primary buttons / accents) */
2288
+ .bg-gradient-primary {
2289
+ background-image: var(--color-gradient-primary);
2290
+ }
2291
+ .text-gradient {
2292
+ background: linear-gradient(94deg, #dd00ac 10%, #7130c3 53%, #7064e9 96%);
2293
+ -webkit-background-clip: text;
2294
+ background-clip: text;
2295
+ color: transparent;
2296
+ }
2297
+
2298
+ /* minimal scrollbar */
2299
+ ::-webkit-scrollbar {
2300
+ width: 6px;
2301
+ height: 6px;
2302
+ }
2303
+ ::-webkit-scrollbar-track {
2304
+ background: transparent;
2305
+ }
2306
+ ::-webkit-scrollbar-thumb {
2307
+ background: var(--border);
2308
+ border-radius: 3px;
2309
+ }
2310
+ * {
2311
+ scrollbar-width: thin;
2312
+ scrollbar-color: var(--border) transparent;
2313
+ }
2314
+ `;
2315
+ /** Map an `add` target to the route folder its page lives in. */
2316
+ const WEB3_ROUTE_DIR = {
2317
+ "chat-web3": "chat-web3",
2318
+ "inference-web3": "inference-web3",
2319
+ "judge-web3": "judge-web3",
2320
+ };
2321
+ /** The app/page.tsx we drop in to make the generated page the homepage. It
2322
+ * re-exports the real page so its documented /<target> route still works. */
2323
+ function homepageReexport(dir, target) {
2324
+ return `// app/page.tsx
2325
+ // Generated by 'lightnode add ${target}'. Makes the ${dir} page the homepage so
2326
+ // 'npm run dev' + http://localhost:3000 lands on it directly. The page itself
2327
+ // still lives at app/${dir}/page.tsx and is also served at /${dir}.
2328
+ export { default } from "./${dir}/page";
2329
+ `;
2330
+ }
2331
+ /** Add the \`dark\` class to the layout's <html> element so dark stays the
2332
+ * default theme. Returns the patched source, or null if <html> was not found
2333
+ * or already carries a \`dark\` class. Handles className as a template literal,
2334
+ * a string literal, or absent. */
2335
+ function withDarkHtml(source) {
2336
+ const match = source.match(/<html\b[^>]*>/);
2337
+ if (!match)
2338
+ return null;
2339
+ const tag = match[0];
2340
+ // Already dark? Cover both className={`...dark...`} and className="...dark...".
2341
+ if (/className=\{`[^`]*\bdark\b/.test(tag) || /className=(["'])[^"']*\bdark\b/.test(tag))
2342
+ return null;
2343
+ if (/className=\{`/.test(tag)) {
2344
+ return source.replace(tag, tag.replace(/className=\{`/, "className={`dark "));
2345
+ }
2346
+ if (/className=(["'])/.test(tag)) {
2347
+ return source.replace(tag, tag.replace(/className=(["'])/, 'className=$1dark '));
2348
+ }
2349
+ // No className on <html>: add one.
2350
+ return source.replace(tag, tag.replace(/<html\b/, '<html className="dark"'));
2351
+ }
2352
+ function setDarkDefaultOnLayout(cwd) {
2353
+ const abs = findLayoutFile(cwd);
2354
+ if (!abs)
2355
+ return false;
2356
+ let source;
2357
+ try {
2358
+ source = fs.readFileSync(abs, "utf8");
2359
+ }
2360
+ catch {
2361
+ return false;
2362
+ }
2363
+ const patched = withDarkHtml(source);
2364
+ if (patched === null)
2365
+ return false;
2366
+ try {
2367
+ fs.writeFileSync(abs, patched);
2368
+ }
2369
+ catch {
2370
+ return false;
2371
+ }
2372
+ return true;
2373
+ }
2374
+ /** One-line description of what each scaffold target gives the dev. */
2375
+ const SCAFFOLD_README_WHAT = {
2376
+ "chat-web3": "A self-contained, wallet-signed chat you can drop into any project - for example, to give an agent a chat UI.",
2377
+ "inference-web3": "A self-contained, wallet-signed one-shot inference page you can drop into any project.",
2378
+ "judge-web3": "A self-contained, wallet-signed pass/fail evaluator page you can drop into any project.",
2379
+ };
2380
+ /** A real README for a freshly scaffolded app, replacing the create-next-app
2381
+ * default so the dev knows what they got and how to use it. */
2382
+ function scaffoldReadme(target, dir) {
2383
+ const what = SCAFFOLD_README_WHAT[target] ?? "A self-contained, wallet-signed page.";
2384
+ return `# LightNode ${dir}
2385
+
2386
+ Generated by \`lightnode add ${target}\`. ${what}
2387
+ No backend, no database, no API keys: each visitor signs and pays for their own
2388
+ turns from their own wallet on LightChain.
2389
+
2390
+ ## Run it
2391
+
2392
+ npm run dev
2393
+
2394
+ Open http://localhost:3000 and click **Connect wallet** (LightChain mainnet
2395
+ 9200 or testnet 8200). Free testnet LCAI: https://lightfaucet.ai
2396
+
2397
+ ## Where things live
2398
+
2399
+ - \`app/page.tsx\` - re-exports the page below as the homepage
2400
+ - \`app/${dir}/page.tsx\` - the UI (also served at /${dir}). Edit this.
2401
+ - \`app/providers.tsx\` + \`lib/wagmi.ts\` - wagmi + React Query setup
2402
+ - \`components/connect-button.tsx\` - the Connect wallet button
2403
+ - \`app/globals.css\` - the theme (light + dark design tokens)
2404
+
2405
+ ## Customize
2406
+
2407
+ It is a normal React client component using \`lightnode-sdk\` wired to the
2408
+ connected wallet. Change the model or system prompt, restyle it, or call it
2409
+ from your own agent. Builder docs: https://lightnode.app/build
2410
+ `;
2411
+ }
2412
+ /**
2413
+ * Wire a freshly scaffolded Next.js app so the generated -web3 page is the
2414
+ * homepage and the LightChain theme + dark default are in place. No-op for any
2415
+ * target without a known route folder.
2416
+ */
2417
+ export function wireFreshScaffold(target, opts = {}) {
2418
+ const cwd = opts.cwd ?? process.cwd();
2419
+ const dir = WEB3_ROUTE_DIR[target];
2420
+ const written = [];
2421
+ if (!dir)
2422
+ return { written, homepageRoute: null, darkDefault: false };
2423
+ // 1. Ship the LightChain theme, replacing the create-next-app starter globals.
2424
+ written.push(writeFile(path.join(cwd, "app/globals.css"), SCAFFOLD_GLOBALS_CSS, true));
2425
+ // 2. Make the generated page the homepage (replaces the starter page.tsx).
2426
+ written.push(writeFile(path.join(cwd, "app/page.tsx"), homepageReexport(dir, target), true));
2427
+ // 3. Replace create-next-app's default README with one about this scaffold.
2428
+ written.push(writeFile(path.join(cwd, "README.md"), scaffoldReadme(target, dir), true));
2429
+ // 4. Keep dark as the default theme (matches lightnode.app).
2430
+ const darkDefault = setDarkDefaultOnLayout(cwd);
2431
+ return { written, homepageRoute: `/${dir}`, darkDefault };
2432
+ }
2061
2433
  const NEXTJS_JUDGE_ROUTE = `// app/api/judge/route.ts
2062
2434
  // Generated by 'lightnode add judge'. See https://lightnode.app/build
2063
2435
  // The LightChallenge-style evaluator: post evidence + criteria, get a
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { LightNode, modelStatsCsv, workerStatsCsv, workerJobsCsv, runInferenceWithKey, runInferenceBatch, Agent, isStalledWorker, workerPreflight, workerWatch, WorkerOperator, isWorkerOpError, BRIDGE_ROUTE, DAO, DAO_ADDRESSES, SDK_VERSION } from "./index.js";
3
- import { addInference, addInferenceWeb3, addJudgeWeb3, addAnalyticsDashboard, addNftMint, addChat, addChatWeb3, addAgent, addJudge, addWagmiSetup, patchLayoutWithProviders } from "./add.js";
3
+ import { addInference, addInferenceWeb3, addJudgeWeb3, addAnalyticsDashboard, addNftMint, addChat, addChatWeb3, addAgent, addJudge, addWagmiSetup, patchLayoutWithProviders, wireFreshScaffold } from "./add.js";
4
4
  import { createPublicClient, createWalletClient, http, parseEther } from "viem";
5
5
  import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
6
6
  import { existsSync, readdirSync, renameSync, rmSync } from "node:fs";
@@ -47,6 +47,10 @@ function scaffoldNextApp(cwd, target) {
47
47
  "--yes", "create-next-app@latest", stageName,
48
48
  "--ts", "--app", "--no-src-dir", "--eslint", "--tailwind",
49
49
  "--use-npm", "--no-turbopack", "--import-alias", "@/*",
50
+ // Don't dump create-next-app's AGENTS.md (the nextjs-agent-rules block)
51
+ // into the user's project. Supported by current create-next-app, which is
52
+ // what `@latest` resolves to here.
53
+ "--no-agents-md",
50
54
  ];
51
55
  const r = spawnSync("npx", args, { cwd, stdio: "inherit" });
52
56
  if (r.status !== 0) {
@@ -579,9 +583,9 @@ async function main() {
579
583
  // A Next.js client page needs a Next.js app to live in. In a bare folder,
580
584
  // scaffold one first so the generated page renders instead of throwing
581
585
  // "Cannot find module 'react'". Opt out with --no-scaffold.
582
- if (NEXT_PAGE_TARGETS.has(sub ?? "") && !existsSync(join(cwd, "package.json")) && !noScaffold) {
583
- scaffoldNextApp(cwd, sub ?? "");
584
- }
586
+ const didScaffold = NEXT_PAGE_TARGETS.has(sub ?? "") && !existsSync(join(cwd, "package.json")) && !noScaffold
587
+ ? scaffoldNextApp(cwd, sub ?? "")
588
+ : false;
585
589
  // ---- write the requested files ----
586
590
  const result = sub === "analytics-dashboard" ? addAnalyticsDashboard({ template, network, force })
587
591
  : sub === "nft-mint-with-inference" ? addNftMint({ template, network, force })
@@ -609,6 +613,18 @@ async function main() {
609
613
  else
610
614
  console.log(` ⤴ ${layout.path} (${layout.reason})`);
611
615
  }
616
+ // ---- fresh scaffold only: make the generated page the homepage and ship
617
+ // the LightChain theme so localhost:3000 lands on the chat, not the
618
+ // create-next-app starter. Skipped in an existing app (nothing to clobber).
619
+ let wiring = null;
620
+ if (didScaffold && isWeb3Page) {
621
+ wiring = wireFreshScaffold(sub, { cwd });
622
+ printWritten(wiring.written);
623
+ if (wiring.homepageRoute)
624
+ console.log(` ✓ app/page.tsx (chat is now the homepage at /)`);
625
+ if (wiring.darkDefault)
626
+ console.log(` ✓ app/layout.tsx (dark theme default)`);
627
+ }
612
628
  // ---- install dependencies (opt out with --no-install) ----
613
629
  const installed = noInstall ? false : installDeps(result.install, cwd);
614
630
  // ---- next steps ----
@@ -620,7 +636,10 @@ async function main() {
620
636
  if (!installed)
621
637
  console.log(` ${result.install}`);
622
638
  console.log(` npm run dev`);
623
- console.log(` open http://localhost:3000${route} and click Connect wallet (chainId ${chainId})`);
639
+ const openPath = wiring ? "" : route; // fresh scaffold serves the page at /
640
+ console.log(` open http://localhost:3000${openPath} and click Connect wallet (chainId ${chainId})`);
641
+ if (wiring)
642
+ console.log(` (also reachable at ${route})`);
624
643
  console.log(` ${result.network === "mainnet" ? "llama3-8b costs 0.02 LCAI per call" : "testnet is free"}`);
625
644
  if (layoutNeedsManual) {
626
645
  console.log(`\nHeads up: couldn't auto-wire the layout (${layout?.reason}).`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightnode-sdk",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Read-only TypeScript client for LightChain AI: workers, jobs, models, on-chain registration, and per-model network analytics. Independent, community-built (not an official LightChain package).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",