lightnode-sdk 0.7.20 → 0.8.1

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 CHANGED
@@ -484,11 +484,15 @@ npx lightnode add judge-web3 # evaluator UI, wallet-signed
484
484
  npx lightnode add wagmi-setup # wallet wiring: lib/wagmi + providers + connect button
485
485
  ```
486
486
 
487
- The `*-web3` scaffolders and `wagmi-setup` write Next.js pages, so run them
488
- inside a Next.js app (`npx create-next-app@latest .` first if you have none).
487
+ The `*-web3` scaffolders are one command end to end: run in an empty folder and
488
+ they scaffold a Next.js app, write the page with a wired Connect button, bundle
489
+ the wagmi config + providers + connect button, wrap your layout with
490
+ `<Providers>`, and `npm install` the deps. Run inside an existing Next.js app
491
+ and they skip the scaffold and just add what's missing. Opt out of the
492
+ automation with `--no-scaffold` and `--no-install`.
489
493
 
490
494
  All `add` commands accept `--template auto|nextjs-api|hono|node`,
491
- `--net testnet|mainnet`, and `--force`.
495
+ `--net testnet|mainnet`, `--force`, `--no-install`, and `--no-scaffold`.
492
496
 
493
497
  > If `add <name>` reports an unknown target, your `npx` cache is serving an
494
498
  > older CLI. Force the current release: `npx lightnode-sdk@latest add <name>`.
package/dist/add.d.ts CHANGED
@@ -109,6 +109,30 @@ export declare function addWagmiSetup(opts?: AddOpts): {
109
109
  template: Template;
110
110
  network: Network;
111
111
  };
112
+ export interface LayoutPatch {
113
+ path: string;
114
+ patched: boolean;
115
+ reason?: string;
116
+ }
117
+ /**
118
+ * Patch the project's root layout to import and wrap children in <Providers>.
119
+ * Returns what happened so the CLI can report it; never throws.
120
+ */
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/* 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;
112
136
  export declare function addJudge(opts?: AddOpts): {
113
137
  written: WrittenFile[];
114
138
  install: string;
package/dist/add.js CHANGED
@@ -964,6 +964,7 @@ const NEXTJS_CHAT_WEB3_PAGE = `// app/chat-web3/page.tsx
964
964
  import { useEffect, useState } from "react";
965
965
  import { useAccount, useWalletClient, usePublicClient } from "wagmi";
966
966
  import { siweSignIn, GatewayClient, runInference, estimateJobFee, NETWORKS } from "lightnode-sdk";
967
+ import { ConnectButton } from "@/components/connect-button";
967
968
 
968
969
  type Turn = {
969
970
  role: "user" | "assistant";
@@ -1074,16 +1075,19 @@ export default function ChatWeb3() {
1074
1075
 
1075
1076
  return (
1076
1077
  <main style={{ maxWidth: 720, margin: "32px auto", padding: 16, fontFamily: "system-ui" }}>
1077
- <h1>Chat (user-pays)</h1>
1078
+ <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 }}>
1079
+ <h1>Chat (user-pays)</h1>
1080
+ <ConnectButton />
1081
+ </div>
1078
1082
  <p style={{ color: "#666" }}>
1079
1083
  Each turn signs one createSession transaction from your wallet on{" "}
1080
1084
  <code>{network ?? "(connect a wallet)"}</code>. Fee:{" "}
1081
1085
  <code>{feeLcai != null ? \`\${feeLcai} LCAI\` : "(fetching)"}</code> per turn plus a small gas amount.
1082
1086
  </p>
1083
1087
  {!address && (
1084
- <div style={{ border: "1px solid #ddd", borderRadius: 8, padding: 12, margin: "12px 0" }}>
1085
- Connect a wallet to start chatting. (Use whichever connector your app exposes - e.g. RainbowKit,
1086
- ConnectKit, Reown AppKit, or wagmi&apos;s useConnect directly.)
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 />
1087
1091
  </div>
1088
1092
  )}
1089
1093
 
@@ -1166,6 +1170,7 @@ const NEXTJS_INFERENCE_WEB3_PAGE = `// app/inference-web3/page.tsx
1166
1170
  import { useEffect, useState } from "react";
1167
1171
  import { useAccount, useWalletClient, usePublicClient } from "wagmi";
1168
1172
  import { siweSignIn, GatewayClient, runInference, estimateJobFee, NETWORKS } from "lightnode-sdk";
1173
+ import { ConnectButton } from "@/components/connect-button";
1169
1174
 
1170
1175
  type Result = {
1171
1176
  answer: string;
@@ -1254,15 +1259,19 @@ export default function InferenceWeb3() {
1254
1259
 
1255
1260
  return (
1256
1261
  <main style={{ maxWidth: 720, margin: "32px auto", padding: 16, fontFamily: "system-ui" }}>
1257
- <h1>Inference (user-pays)</h1>
1262
+ <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 }}>
1263
+ <h1>Inference (user-pays)</h1>
1264
+ <ConnectButton />
1265
+ </div>
1258
1266
  <p style={{ color: "#666" }}>
1259
1267
  Signs one encrypted inference from your wallet on{" "}
1260
1268
  <code>{network ?? "(connect a wallet)"}</code>. Fee:{" "}
1261
1269
  <code>{feeLcai != null ? \`\${feeLcai} LCAI\` : "(fetching)"}</code> per call plus a small gas amount.
1262
1270
  </p>
1263
1271
  {!address && (
1264
- <div style={{ border: "1px solid #ddd", borderRadius: 8, padding: 12, margin: "12px 0" }}>
1265
- Connect a wallet to run inference. Drop &lt;ConnectButton /&gt; here or wherever your app exposes one.
1272
+ <div style={{ border: "1px solid #ddd", borderRadius: 8, padding: 12, margin: "12px 0", display: "flex", alignItems: "center", gap: 12 }}>
1273
+ <span>Connect a wallet to run inference.</span>
1274
+ <ConnectButton />
1266
1275
  </div>
1267
1276
  )}
1268
1277
 
@@ -1321,6 +1330,7 @@ const NEXTJS_JUDGE_WEB3_PAGE = `// app/judge-web3/page.tsx
1321
1330
  import { useEffect, useState } from "react";
1322
1331
  import { useAccount, useWalletClient, usePublicClient } from "wagmi";
1323
1332
  import { siweSignIn, GatewayClient, runInference, estimateJobFee, NETWORKS } from "lightnode-sdk";
1333
+ import { ConnectButton } from "@/components/connect-button";
1324
1334
 
1325
1335
  type Verdict = {
1326
1336
  passed: boolean;
@@ -1434,15 +1444,19 @@ Reply with STRICT JSON only, matching: { "passed": boolean, "confidence": 0-1, "
1434
1444
 
1435
1445
  return (
1436
1446
  <main style={{ maxWidth: 720, margin: "32px auto", padding: 16, fontFamily: "system-ui" }}>
1437
- <h1>AI Judge (user-pays)</h1>
1447
+ <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 }}>
1448
+ <h1>AI Judge (user-pays)</h1>
1449
+ <ConnectButton />
1450
+ </div>
1438
1451
  <p style={{ color: "#666" }}>
1439
1452
  Each submission signs one inference from your wallet on{" "}
1440
1453
  <code>{network ?? "(connect a wallet)"}</code>. Cost:{" "}
1441
1454
  <code>{feeLcai != null ? \`\${feeLcai} LCAI\` : "(fetching)"}</code> plus gas. Verdict comes back with on-chain proof.
1442
1455
  </p>
1443
1456
  {!address && (
1444
- <div style={{ border: "1px solid #ddd", borderRadius: 8, padding: 12, margin: "12px 0" }}>
1445
- Connect a wallet to submit. Drop &lt;ConnectButton /&gt; here or wherever your app exposes one.
1457
+ <div style={{ border: "1px solid #ddd", borderRadius: 8, padding: 12, margin: "12px 0", display: "flex", alignItems: "center", gap: 12 }}>
1458
+ <span>Connect a wallet to submit.</span>
1459
+ <ConnectButton />
1446
1460
  </div>
1447
1461
  )}
1448
1462
 
@@ -1983,6 +1997,372 @@ export function addWagmiSetup(opts = {}) {
1983
1997
  network,
1984
1998
  };
1985
1999
  }
2000
+ function findLayoutFile(cwd) {
2001
+ const candidates = ["app/layout.tsx", "app/layout.jsx", "src/app/layout.tsx", "src/app/layout.jsx"];
2002
+ for (const rel of candidates) {
2003
+ const abs = path.join(cwd, rel);
2004
+ if (fs.existsSync(abs))
2005
+ return abs;
2006
+ }
2007
+ return null;
2008
+ }
2009
+ function withProvidersImport(source) {
2010
+ if (/from\s+["']\.\/providers["']/.test(source))
2011
+ return source;
2012
+ const importLine = `import { Providers } from "./providers";`;
2013
+ const lines = source.split("\n");
2014
+ let lastImport = -1;
2015
+ for (let i = 0; i < lines.length; i++) {
2016
+ if (/^\s*import\s/.test(lines[i]))
2017
+ lastImport = i;
2018
+ }
2019
+ if (lastImport === -1)
2020
+ return `${importLine}\n${source}`;
2021
+ return [...lines.slice(0, lastImport + 1), importLine, ...lines.slice(lastImport + 1)].join("\n");
2022
+ }
2023
+ function withWrappedChildren(source) {
2024
+ if (/<Providers>/.test(source))
2025
+ return source;
2026
+ if (!source.includes("{children}"))
2027
+ return null;
2028
+ return source.replace("{children}", "<Providers>{children}</Providers>");
2029
+ }
2030
+ /**
2031
+ * Patch the project's root layout to import and wrap children in <Providers>.
2032
+ * Returns what happened so the CLI can report it; never throws.
2033
+ */
2034
+ export function patchLayoutWithProviders(cwd = process.cwd()) {
2035
+ const abs = findLayoutFile(cwd);
2036
+ if (!abs)
2037
+ return { path: "app/layout.tsx", patched: false, reason: "no layout file found" };
2038
+ const rel = path.relative(cwd, abs) || abs;
2039
+ let source;
2040
+ try {
2041
+ source = fs.readFileSync(abs, "utf8");
2042
+ }
2043
+ catch (e) {
2044
+ return { path: rel, patched: false, reason: `could not read layout (${e.message})` };
2045
+ }
2046
+ if (/<Providers>/.test(source) && /from\s+["']\.\/providers["']/.test(source)) {
2047
+ return { path: rel, patched: false, reason: "already wrapped with <Providers>" };
2048
+ }
2049
+ const wrapped = withWrappedChildren(withProvidersImport(source));
2050
+ if (wrapped === null) {
2051
+ return { path: rel, patched: false, reason: "no {children} found - wrap with <Providers> manually" };
2052
+ }
2053
+ try {
2054
+ fs.writeFileSync(abs, wrapped);
2055
+ }
2056
+ catch (e) {
2057
+ return { path: rel, patched: false, reason: `could not write layout (${e.message})` };
2058
+ }
2059
+ return { path: rel, patched: true };
2060
+ }
2061
+ // ---------------------------------------------------------------------------
2062
+ // Fresh-scaffold wiring: when `add <x>-web3` scaffolds a brand-new Next.js app
2063
+ // (bare folder), the create-next-app starter page sits at `/` and the generated
2064
+ // page sits at `/<x>-web3`, so `npm run dev` + localhost:3000 lands on the
2065
+ // starter, not the chat. This makes the generated page the homepage and ships
2066
+ // the LightChain theme so the first render is the real thing.
2067
+ //
2068
+ // Only ever invoked on a scaffold WE just created, so overwriting page.tsx and
2069
+ // globals.css is safe - there is no user content to clobber. In an existing app
2070
+ // none of this runs; the page keeps its dedicated /<x>-web3 route untouched.
2071
+ // ---------------------------------------------------------------------------
2072
+ // The LightChain chat theme (Tailwind v4 tokens, light + dark), ported from the
2073
+ // lightnode app's globals.css. Shipped as the scaffold's app/globals.css so
2074
+ // installs default to the real look instead of the create-next-app starter.
2075
+ export const SCAFFOLD_GLOBALS_CSS = `@import "tailwindcss";
2076
+
2077
+ /* dark mode via .dark class (we default the app to dark) */
2078
+ @custom-variant dark (&:is(.dark, .dark *));
2079
+
2080
+ /* design tokens (light) - ported from lcai-chat-v2 */
2081
+ :root {
2082
+ font-family: var(--font-inter), ui-sans-serif, system-ui, sans-serif;
2083
+
2084
+ --background: #ffffff;
2085
+ --primary: #6767e9;
2086
+ --primary-600: #5a4fd8;
2087
+ --foreground: #09090b;
2088
+ --card: #ffffff;
2089
+ --card-foreground: #09090b;
2090
+ --popover: #ffffff;
2091
+ --popover-foreground: hsl(240 10% 3.9%);
2092
+ --primary-foreground: #fafafa;
2093
+ --secondary: hsl(240 4.8% 95.9%);
2094
+ --secondary-foreground: hsl(240 5.9% 10%);
2095
+ --muted: hsl(240 4.8% 95.9%);
2096
+ --muted-foreground: hsl(240 3.8% 46.1%);
2097
+ --accent: hsl(240 4.8% 95.9%);
2098
+ --accent-foreground: hsl(240 5.9% 10%);
2099
+ --destructive: #ef4d6a;
2100
+ --destructive-foreground: hsl(0 0% 98%);
2101
+ --success: #15bd77;
2102
+ --warning: #eaa53d;
2103
+ --border: hsl(240 5.9% 90%);
2104
+ --input: hsl(240 5.9% 90%);
2105
+ --ring: hsl(240 10% 3.9%);
2106
+ --radius: 0.625rem;
2107
+
2108
+ --surface-base-subtle: rgba(34, 35, 42, 0.02);
2109
+ --surface-base-faint: rgba(14, 18, 27, 0.04);
2110
+ --surface-base-light: rgba(204, 206, 239, 0.16);
2111
+ --surface-elevation-light: #ffffff;
2112
+
2113
+ --content-primary: #0f0f14;
2114
+ --content-default: #373842;
2115
+ --content-soft: #656678;
2116
+ --content-extraLight: #9798b6;
2117
+
2118
+ --border-soft: rgba(14, 18, 27, 0.08);
2119
+ --border-light: rgba(14, 18, 27, 0.06);
2120
+ }
2121
+
2122
+ /* design tokens (dark) */
2123
+ .dark {
2124
+ --background: #070710;
2125
+ --foreground: hsl(0 0% 98%);
2126
+ --card: #0f0f14;
2127
+ --card-foreground: hsl(0 0% 98%);
2128
+ --popover: #0f0f14;
2129
+ --popover-foreground: hsl(0 0% 98%);
2130
+ --primary: #7064e9;
2131
+ --primary-600: #8c71f6;
2132
+ --primary-foreground: hsl(0 0% 98%);
2133
+ --secondary: hsl(240 3.7% 15.9%);
2134
+ --secondary-foreground: hsl(0 0% 98%);
2135
+ --muted: hsl(240 3.7% 15.9%);
2136
+ --muted-foreground: hsl(240 5% 64.9%);
2137
+ --accent: hsl(240 3.7% 15.9%);
2138
+ --accent-foreground: hsl(0 0% 98%);
2139
+ --destructive: #fb5a76;
2140
+ --destructive-foreground: hsl(0 0% 98%);
2141
+ --success: #22d68a;
2142
+ --warning: #f5be5c;
2143
+ --border: hsl(240 3.7% 15.9%);
2144
+ --input: hsl(240 3.7% 15.9%);
2145
+ --ring: hsl(240 4.9% 83.9%);
2146
+
2147
+ --surface-base-subtle: rgba(204, 206, 239, 0.02);
2148
+ --surface-base-faint: rgba(204, 206, 239, 0.04);
2149
+ --surface-base-light: rgba(204, 206, 239, 0.08);
2150
+ --surface-elevation-light: #0f0f14;
2151
+
2152
+ --content-primary: #cccef0;
2153
+ --content-default: #9798b6;
2154
+ --content-soft: rgba(154, 156, 207, 0.8);
2155
+ --content-extraLight: #9798b6;
2156
+
2157
+ --border-soft: rgba(204, 206, 239, 0.12);
2158
+ --border-light: rgba(204, 206, 239, 0.08);
2159
+ }
2160
+
2161
+ /* theme mapping (Tailwind v4 @theme) */
2162
+ @theme inline {
2163
+ --radius-md: calc(var(--radius) - 2px);
2164
+ --radius-sm: calc(var(--radius) - 4px);
2165
+ --radius-lg: var(--radius);
2166
+
2167
+ --color-background: var(--background);
2168
+ --color-foreground: var(--foreground);
2169
+ --color-card: var(--card);
2170
+ --color-card-foreground: var(--card-foreground);
2171
+ --color-popover: var(--popover);
2172
+ --color-popover-foreground: var(--popover-foreground);
2173
+ --color-primary: var(--primary);
2174
+ --color-primary-600: var(--primary-600);
2175
+ --color-primary-foreground: var(--primary-foreground);
2176
+ --color-secondary: var(--secondary);
2177
+ --color-secondary-foreground: var(--secondary-foreground);
2178
+ --color-muted: var(--muted);
2179
+ --color-muted-foreground: var(--muted-foreground);
2180
+ --color-accent: var(--accent);
2181
+ --color-accent-foreground: var(--accent-foreground);
2182
+ --color-destructive: var(--destructive);
2183
+ --color-destructive-foreground: var(--destructive-foreground);
2184
+ --color-success: var(--success);
2185
+ --color-warning: var(--warning);
2186
+ --color-border: var(--border);
2187
+ --color-input: var(--input);
2188
+ --color-ring: var(--ring);
2189
+
2190
+ --color-surface-base-subtle: var(--surface-base-subtle);
2191
+ --color-surface-base-faint: var(--surface-base-faint);
2192
+ --color-surface-base-light: var(--surface-base-light);
2193
+ --color-surface-elevation-light: var(--surface-elevation-light);
2194
+ --color-surface-base-brand-default: #693ee0;
2195
+ --color-surface-base-brand-strong: #8c71f6;
2196
+
2197
+ --color-content-primary: var(--content-primary);
2198
+ --color-content-default: var(--content-default);
2199
+ --color-content-soft: var(--content-soft);
2200
+ --color-content-extraLight: var(--content-extraLight);
2201
+
2202
+ --color-bdr-soft: var(--border-soft);
2203
+ --color-bdr-light: var(--border-light);
2204
+
2205
+ --color-gradient-primary: linear-gradient(270deg, #7064e9 0%, #dd00ac 100%);
2206
+ }
2207
+
2208
+ @layer base {
2209
+ * {
2210
+ border-color: var(--border);
2211
+ }
2212
+ body {
2213
+ background-color: var(--background);
2214
+ color: var(--foreground);
2215
+ overflow-x: hidden;
2216
+ }
2217
+ html {
2218
+ overflow-x: hidden;
2219
+ }
2220
+ button {
2221
+ cursor: pointer;
2222
+ }
2223
+ button:disabled {
2224
+ cursor: not-allowed;
2225
+ }
2226
+ /* visible keyboard focus across interactive elements */
2227
+ a:focus-visible,
2228
+ button:focus-visible,
2229
+ input:focus-visible,
2230
+ select:focus-visible,
2231
+ textarea:focus-visible {
2232
+ outline: 2px solid var(--primary);
2233
+ outline-offset: 2px;
2234
+ border-radius: 6px;
2235
+ }
2236
+ }
2237
+
2238
+ /* respect reduced-motion: kill non-essential animation */
2239
+ @media (prefers-reduced-motion: reduce) {
2240
+ *,
2241
+ ::before,
2242
+ ::after {
2243
+ animation-duration: 0.001ms !important;
2244
+ animation-iteration-count: 1 !important;
2245
+ transition-duration: 0.001ms !important;
2246
+ scroll-behavior: auto !important;
2247
+ }
2248
+ }
2249
+
2250
+ /* ambient app background (gradient mesh behind everything) */
2251
+ body::before {
2252
+ content: "";
2253
+ position: fixed;
2254
+ inset: 0;
2255
+ z-index: -1;
2256
+ pointer-events: none;
2257
+ background:
2258
+ radial-gradient(60% 50% at 50% -6%, rgba(221, 0, 172, 0.10), transparent 60%),
2259
+ radial-gradient(55% 45% at 12% -8%, rgba(112, 100, 233, 0.14), transparent 60%),
2260
+ radial-gradient(50% 40% at 88% -2%, rgba(112, 100, 233, 0.12), transparent 60%),
2261
+ radial-gradient(45% 45% at 50% 115%, rgba(112, 100, 233, 0.07), transparent 60%);
2262
+ }
2263
+ .dark body::before {
2264
+ background:
2265
+ radial-gradient(60% 50% at 50% -6%, rgba(221, 0, 172, 0.12), transparent 60%),
2266
+ radial-gradient(55% 45% at 12% -8%, rgba(112, 100, 233, 0.18), transparent 60%),
2267
+ radial-gradient(50% 40% at 88% -2%, rgba(112, 100, 233, 0.14), transparent 60%),
2268
+ radial-gradient(45% 45% at 50% 118%, rgba(112, 100, 233, 0.10), transparent 60%);
2269
+ }
2270
+
2271
+ /* minimal scrollbar */
2272
+ ::-webkit-scrollbar {
2273
+ width: 6px;
2274
+ height: 6px;
2275
+ }
2276
+ ::-webkit-scrollbar-track {
2277
+ background: transparent;
2278
+ }
2279
+ ::-webkit-scrollbar-thumb {
2280
+ background: var(--border);
2281
+ border-radius: 3px;
2282
+ }
2283
+ * {
2284
+ scrollbar-width: thin;
2285
+ scrollbar-color: var(--border) transparent;
2286
+ }
2287
+ `;
2288
+ /** Map an `add` target to the route folder its page lives in. */
2289
+ const WEB3_ROUTE_DIR = {
2290
+ "chat-web3": "chat-web3",
2291
+ "inference-web3": "inference-web3",
2292
+ "judge-web3": "judge-web3",
2293
+ };
2294
+ /** The app/page.tsx we drop in to make the generated page the homepage. It
2295
+ * re-exports the real page so its documented /<target> route still works. */
2296
+ function homepageReexport(dir, target) {
2297
+ return `// app/page.tsx
2298
+ // Generated by 'lightnode add ${target}'. Makes the ${dir} page the homepage so
2299
+ // 'npm run dev' + http://localhost:3000 lands on it directly. The page itself
2300
+ // still lives at app/${dir}/page.tsx and is also served at /${dir}.
2301
+ export { default } from "./${dir}/page";
2302
+ `;
2303
+ }
2304
+ /** Add the \`dark\` class to the layout's <html> element so dark stays the
2305
+ * default theme. Returns the patched source, or null if <html> was not found
2306
+ * or already carries a \`dark\` class. Handles className as a template literal,
2307
+ * a string literal, or absent. */
2308
+ function withDarkHtml(source) {
2309
+ const match = source.match(/<html\b[^>]*>/);
2310
+ if (!match)
2311
+ return null;
2312
+ const tag = match[0];
2313
+ // Already dark? Cover both className={`...dark...`} and className="...dark...".
2314
+ if (/className=\{`[^`]*\bdark\b/.test(tag) || /className=(["'])[^"']*\bdark\b/.test(tag))
2315
+ return null;
2316
+ if (/className=\{`/.test(tag)) {
2317
+ return source.replace(tag, tag.replace(/className=\{`/, "className={`dark "));
2318
+ }
2319
+ if (/className=(["'])/.test(tag)) {
2320
+ return source.replace(tag, tag.replace(/className=(["'])/, 'className=$1dark '));
2321
+ }
2322
+ // No className on <html>: add one.
2323
+ return source.replace(tag, tag.replace(/<html\b/, '<html className="dark"'));
2324
+ }
2325
+ function setDarkDefaultOnLayout(cwd) {
2326
+ const abs = findLayoutFile(cwd);
2327
+ if (!abs)
2328
+ return false;
2329
+ let source;
2330
+ try {
2331
+ source = fs.readFileSync(abs, "utf8");
2332
+ }
2333
+ catch {
2334
+ return false;
2335
+ }
2336
+ const patched = withDarkHtml(source);
2337
+ if (patched === null)
2338
+ return false;
2339
+ try {
2340
+ fs.writeFileSync(abs, patched);
2341
+ }
2342
+ catch {
2343
+ return false;
2344
+ }
2345
+ return true;
2346
+ }
2347
+ /**
2348
+ * Wire a freshly scaffolded Next.js app so the generated -web3 page is the
2349
+ * homepage and the LightChain theme + dark default are in place. No-op for any
2350
+ * target without a known route folder.
2351
+ */
2352
+ export function wireFreshScaffold(target, opts = {}) {
2353
+ const cwd = opts.cwd ?? process.cwd();
2354
+ const dir = WEB3_ROUTE_DIR[target];
2355
+ const written = [];
2356
+ if (!dir)
2357
+ return { written, homepageRoute: null, darkDefault: false };
2358
+ // 1. Ship the LightChain theme, replacing the create-next-app starter globals.
2359
+ written.push(writeFile(path.join(cwd, "app/globals.css"), SCAFFOLD_GLOBALS_CSS, true));
2360
+ // 2. Make the generated page the homepage (replaces the starter page.tsx).
2361
+ written.push(writeFile(path.join(cwd, "app/page.tsx"), homepageReexport(dir, target), true));
2362
+ // 3. Keep dark as the default theme (matches lightnode.app).
2363
+ const darkDefault = setDarkDefaultOnLayout(cwd);
2364
+ return { written, homepageRoute: `/${dir}`, darkDefault };
2365
+ }
1986
2366
  const NEXTJS_JUDGE_ROUTE = `// app/api/judge/route.ts
1987
2367
  // Generated by 'lightnode add judge'. See https://lightnode.app/build
1988
2368
  // The LightChallenge-style evaluator: post evidence + criteria, get a
package/dist/cli.js CHANGED
@@ -1,10 +1,11 @@
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 } 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
- import { existsSync } from "node:fs";
6
+ import { existsSync, readdirSync, renameSync, rmSync } from "node:fs";
7
7
  import { join } from "node:path";
8
+ import { spawnSync } from "node:child_process";
8
9
  function flag(name) {
9
10
  const i = process.argv.indexOf(name);
10
11
  return i >= 0 ? process.argv[i + 1] : undefined;
@@ -19,6 +20,71 @@ function die(msg) {
19
20
  }
20
21
  const lcai = (wei) => (wei ? Number(BigInt(wei)) / 1e18 : 0);
21
22
  const rate = (r) => (r == null ? "-" : `${Math.round(r * 100)}%`);
23
+ // `add` targets that are always a Next.js client page. In a bare folder these
24
+ // get a real Next.js app scaffolded first so the generated page can render.
25
+ const NEXT_PAGE_TARGETS = new Set(["chat-web3", "inference-web3", "judge-web3", "wagmi-setup"]);
26
+ function printWritten(files) {
27
+ for (const f of files) {
28
+ if (f.skipped)
29
+ console.log(` ⤴ ${f.path} (skipped - ${f.reason})`);
30
+ else
31
+ console.log(` ✓ ${f.path}`);
32
+ }
33
+ }
34
+ /**
35
+ * Scaffold a Next.js app into cwd via create-next-app. Returns true on success.
36
+ *
37
+ * We scaffold into a fixed-name subfolder and move the files up rather than
38
+ * passing ".", because create-next-app derives the project name from the
39
+ * target and rejects names npm won't allow (capital letters, leading dots,
40
+ * etc.) - which would make this fail in any folder like "MyApp". The subfolder
41
+ * name is one we control, so that validation always passes.
42
+ */
43
+ function scaffoldNextApp(cwd, target) {
44
+ console.log(`\nNo Next.js app here yet - scaffolding one with create-next-app...\n`);
45
+ const stageName = "lightnode-next-app-stage";
46
+ const args = [
47
+ "--yes", "create-next-app@latest", stageName,
48
+ "--ts", "--app", "--no-src-dir", "--eslint", "--tailwind",
49
+ "--use-npm", "--no-turbopack", "--import-alias", "@/*",
50
+ ];
51
+ const r = spawnSync("npx", args, { cwd, stdio: "inherit" });
52
+ if (r.status !== 0) {
53
+ console.error(`\ncreate-next-app did not complete (exit ${r.status ?? "?"}).`);
54
+ console.error(`If this folder already had files, scaffold manually then re-run:`);
55
+ console.error(` npx create-next-app@latest . && npx lightnode-sdk@latest add ${target}`);
56
+ return false;
57
+ }
58
+ return relocateScaffold(join(cwd, stageName), cwd, target);
59
+ }
60
+ /** Move every entry from the staged scaffold dir up into cwd, then remove it. */
61
+ function relocateScaffold(from, cwd, target) {
62
+ try {
63
+ for (const entry of readdirSync(from)) {
64
+ const dest = join(cwd, entry);
65
+ if (existsSync(dest))
66
+ continue; // never clobber files the user already had
67
+ renameSync(join(from, entry), dest);
68
+ }
69
+ rmSync(from, { recursive: true, force: true });
70
+ return true;
71
+ }
72
+ catch (e) {
73
+ console.error(`\nScaffolded into ${from} but could not move it up (${e.message}).`);
74
+ console.error(`Move its contents into this folder, then re-run: npx lightnode-sdk@latest add ${target}`);
75
+ return false;
76
+ }
77
+ }
78
+ /** Run an `npm install ...` line in cwd. The line is an internal constant
79
+ * (never user input), so running it through a shell is safe here. */
80
+ function installDeps(installLine, cwd) {
81
+ console.log(`\nInstalling dependencies: ${installLine}\n`);
82
+ const r = spawnSync(installLine, { cwd, stdio: "inherit", shell: true });
83
+ if (r.status === 0)
84
+ return true;
85
+ console.error(`\nDependency install failed (exit ${r.status ?? "?"}). Run it yourself:\n ${installLine}`);
86
+ return false;
87
+ }
22
88
  const HELP = `lightnode <command> [--net mainnet|testnet]
23
89
 
24
90
  Run one inference (needs PRIVATE_KEY in env):
@@ -505,6 +571,18 @@ async function main() {
505
571
  }
506
572
  die(lines.join("\n"));
507
573
  }
574
+ // ---- one-command setup: flags + optional scaffold ----
575
+ const noInstall = process.argv.includes("--no-install");
576
+ const noScaffold = process.argv.includes("--no-scaffold");
577
+ const cwd = process.cwd();
578
+ const isWeb3Page = sub === "chat-web3" || sub === "inference-web3" || sub === "judge-web3";
579
+ // A Next.js client page needs a Next.js app to live in. In a bare folder,
580
+ // scaffold one first so the generated page renders instead of throwing
581
+ // "Cannot find module 'react'". Opt out with --no-scaffold.
582
+ const didScaffold = NEXT_PAGE_TARGETS.has(sub ?? "") && !existsSync(join(cwd, "package.json")) && !noScaffold
583
+ ? scaffoldNextApp(cwd, sub ?? "")
584
+ : false;
585
+ // ---- write the requested files ----
508
586
  const result = sub === "analytics-dashboard" ? addAnalyticsDashboard({ template, network, force })
509
587
  : sub === "nft-mint-with-inference" ? addNftMint({ template, network, force })
510
588
  : sub === "chat-web3" ? addChatWeb3({ template, network, force })
@@ -516,71 +594,68 @@ async function main() {
516
594
  : sub === "judge" ? addJudge({ template, network, force })
517
595
  : addInference({ template, network, force });
518
596
  console.log(`▶ add ${sub} (${result.template} template, default network ${result.network})`);
519
- for (const f of result.written) {
520
- if (f.skipped)
521
- console.log(` ⤴ ${f.path} (skipped - ${f.reason})`);
597
+ printWritten(result.written);
598
+ // ---- web3 pages: bundle the wagmi wiring + wrap the root layout so the
599
+ // page's <ConnectButton /> and wagmi hooks have a provider to resolve. ----
600
+ let layout = null;
601
+ if (isWeb3Page) {
602
+ const wagmi = addWagmiSetup({ template: result.template, network, force });
603
+ printWritten(wagmi.written);
604
+ }
605
+ if (isWeb3Page || sub === "wagmi-setup") {
606
+ layout = patchLayoutWithProviders(cwd);
607
+ if (layout.patched)
608
+ console.log(` ✓ ${layout.path} (wrapped children with <Providers>)`);
522
609
  else
523
- console.log(` ${f.path}`);
524
- }
525
- const anyWritten = result.written.some((f) => !f.skipped);
526
- if (!anyWritten) {
527
- console.log("\nNothing to do - all target files already exist. Pass --force to overwrite.");
528
- }
529
- else {
530
- // The *-web3 pages and wagmi-setup are Next.js React files. If no
531
- // Next.js app was detected (e.g. an empty folder), nothing can render
532
- // what we just wrote - surface that before the numbered steps so the
533
- // user scaffolds an app first instead of chasing a non-running page.
534
- const isNextOnly = sub === "chat-web3" || sub === "inference-web3" || sub === "judge-web3" || sub === "wagmi-setup";
535
- const hasPackageJson = existsSync(join(process.cwd(), "package.json"));
536
- if (isNextOnly && result.template !== "nextjs-api") {
537
- console.log(`\nNo Next.js app detected in this folder. ${sub} is a Next.js page, so`);
538
- console.log(`create one here first, then re-run this command:`);
539
- console.log(` npx create-next-app@latest .`);
540
- }
541
- else if (!hasPackageJson) {
542
- // A scaffolded script dropped into a bare folder (no package.json)
543
- // gives the editor nothing to resolve Node/ws types against - the
544
- // user sees "Cannot find name 'process'" everywhere. Initialize a
545
- // project first so the install below lands in a real node_modules.
546
- console.log(`\nNo package.json in this folder yet, so your editor can't resolve types`);
547
- console.log(`(you'd see "Cannot find name 'process'" etc.). Initialize a project first:`);
548
- console.log(` npm init -y`);
549
- }
550
- console.log(`\nNext steps (these files were added to your CURRENT folder, not a new project):`);
551
- console.log(` 1. ${result.install}`);
552
- if (sub === "wagmi-setup") {
553
- console.log(` 2. Import Providers in app/layout.tsx and wrap children:`);
554
- console.log(` import { Providers } from "./providers";`);
555
- console.log(` // <body><Providers>{children}</Providers></body>`);
556
- console.log(` 3. Drop <ConnectButton /> anywhere you want a connect UI:`);
557
- console.log(` import { ConnectButton } from "@/components/connect-button";`);
558
- console.log(` 4. You can now use any wagmi hook (useAccount, useWalletClient, ...).`);
559
- console.log(` Wallets on chains other than 9200/8200 will be prompted to switch.`);
610
+ console.log(` ${layout.path} (${layout.reason})`);
611
+ }
612
+ // ---- fresh scaffold only: make the generated page the homepage and ship
613
+ // the LightChain theme so localhost:3000 lands on the chat, not the
614
+ // create-next-app starter. Skipped in an existing app (nothing to clobber).
615
+ let wiring = null;
616
+ if (didScaffold && isWeb3Page) {
617
+ wiring = wireFreshScaffold(sub, { cwd });
618
+ printWritten(wiring.written);
619
+ if (wiring.homepageRoute)
620
+ console.log(` ✓ app/page.tsx (chat is now the homepage at /)`);
621
+ if (wiring.darkDefault)
622
+ console.log(` ✓ app/layout.tsx (dark theme default)`);
623
+ }
624
+ // ---- install dependencies (opt out with --no-install) ----
625
+ const installed = noInstall ? false : installDeps(result.install, cwd);
626
+ // ---- next steps ----
627
+ const layoutNeedsManual = layout != null && !layout.patched && !/already/.test(layout.reason ?? "");
628
+ if (isWeb3Page) {
629
+ const route = sub === "chat-web3" ? "/chat-web3" : sub === "inference-web3" ? "/inference-web3" : "/judge-web3";
630
+ const chainId = result.network === "mainnet" ? "9200" : "8200";
631
+ console.log(`\n${installed ? " Done - deps installed, wagmi + layout wired. Just run it:" : "Files written. Next:"}`);
632
+ if (!installed)
633
+ console.log(` ${result.install}`);
634
+ console.log(` npm run dev`);
635
+ const openPath = wiring ? "" : route; // fresh scaffold serves the page at /
636
+ console.log(` open http://localhost:3000${openPath} and click Connect wallet (chainId ${chainId})`);
637
+ if (wiring)
638
+ console.log(` (also reachable at ${route})`);
639
+ console.log(` ${result.network === "mainnet" ? "llama3-8b costs 0.02 LCAI per call" : "testnet is free"}`);
640
+ if (layoutNeedsManual) {
641
+ console.log(`\nHeads up: couldn't auto-wire the layout (${layout?.reason}).`);
642
+ console.log(`Wrap {children} with <Providers> in app/layout.tsx (import from "./providers").`);
560
643
  }
561
- else if (sub === "chat-web3" || sub === "inference-web3" || sub === "judge-web3") {
562
- // *-web3 variants have no PRIVATE_KEY (each visitor pays their own way).
563
- const needsWagmi = result.needsWagmi;
564
- const route = sub === "chat-web3" ? "/chat-web3"
565
- : sub === "inference-web3" ? "/inference-web3"
566
- : "/judge-web3";
567
- if (needsWagmi) {
568
- console.log(` 2. Get wagmi wired up with one command:`);
569
- console.log(` npx lightnode add wagmi-setup`);
570
- console.log(` (drops lib/wagmi.ts + app/providers.tsx + components/connect-button.tsx)`);
571
- console.log(` 3. Wrap your layout with <Providers> and drop <ConnectButton /> on the page.`);
572
- console.log(` 4. npm run dev, open ${route}, connect on chainId ${result.network === "mainnet" ? "9200" : "8200"}.`);
573
- console.log(` Mainnet llama3-8b is 0.02 LCAI per call; testnet is free from https://lightfaucet.ai`);
574
- }
575
- else {
576
- console.log(` 2. npm run dev, open ${route}`);
577
- console.log(` 3. Connect a wallet on LightChain ${result.network === "mainnet" ? "mainnet (chainId 9200)" : "testnet (chainId 8200)"}.`);
578
- console.log(` Mainnet llama3-8b is 0.02 LCAI per call; testnet is free from https://lightfaucet.ai`);
579
- }
580
- console.log(`\n Note: ${sub} has NO server-side route, so it scales infinitely on`);
581
- console.log(` static hosting (Vercel/Netlify/Cloudflare Pages free tier all work).`);
644
+ console.log(`\n No server-side route - deploy static (Vercel/Netlify/Cloudflare free tier all work).`);
645
+ }
646
+ else if (sub === "wagmi-setup") {
647
+ console.log(`\n${installed ? "✓ Done - deps installed and layout wired." : "Files written. Run: " + result.install}`);
648
+ console.log(`\nUse it: import { ConnectButton } from "@/components/connect-button"; drop <ConnectButton /> anywhere.`);
649
+ console.log(`Any wagmi hook (useAccount, useWalletClient, ...) now works app-wide. Off-network wallets get a switch prompt.`);
650
+ if (layoutNeedsManual) {
651
+ console.log(`\nHeads up: couldn't auto-wire the layout (${layout?.reason}). Wrap {children} with <Providers> in app/layout.tsx.`);
582
652
  }
583
- else if (sub === "nft-mint-with-inference" || sub === "inference" || sub === "chat" || sub === "agent" || sub === "judge") {
653
+ }
654
+ else {
655
+ // server-paid + read-only targets keep the detailed guidance.
656
+ console.log(`\nNext steps:`);
657
+ console.log(` 1. ${installed ? "(done) " : ""}${result.install}`);
658
+ if (sub === "nft-mint-with-inference" || sub === "inference" || sub === "chat" || sub === "agent" || sub === "judge") {
584
659
  console.log(` 2. cp .env.example .env (and put a funded ${result.network} PRIVATE_KEY in it)`);
585
660
  if (sub === "agent" && result.template === "nextjs-api") {
586
661
  console.log(` 3. Set CRON_SECRET in your Vercel env vars + edit AGENT_TASK in .env`);
@@ -638,8 +713,6 @@ async function main() {
638
713
  }
639
714
  }
640
715
  // Hosting note: the Docker setup we shipped is the recommended path.
641
- // The managed platforms (Vercel etc.) are the fallback if a builder is
642
- // already committed to one.
643
716
  if (result.template === "nextjs-api"
644
717
  && (sub === "inference" || sub === "chat" || sub === "judge")) {
645
718
  console.log(`\n Hosting: a mainnet inference takes 60-90s. The Dockerfile + docker-compose.yml`);
@@ -648,13 +721,13 @@ async function main() {
648
721
  console.log(` Don't use Vercel Hobby (10s cap, every call times out). Vercel Pro works at`);
649
722
  console.log(` 60s if you'd rather stay on Vercel. See LIGHTNODE-HOSTING.md for the full table.`);
650
723
  }
651
- if (result.network === "testnet") {
652
- console.log(`\nNo wallet yet? Make one: npx lightnode wallet new then fund it free below.`);
653
- }
654
- console.log(`\nFree testnet LCAI: https://lightfaucet.ai`);
655
- console.log(`Builder docs: https://lightnode.app/build`);
656
- console.log(`New to all this? See GETTING-STARTED.md in the lightnode repo.`);
657
724
  }
725
+ if (result.network === "testnet") {
726
+ console.log(`\nNo wallet yet? Make one: npx lightnode wallet new then fund it free below.`);
727
+ }
728
+ console.log(`\nFree testnet LCAI: https://lightfaucet.ai`);
729
+ console.log(`Builder docs: https://lightnode.app/build`);
730
+ console.log(`New to all this? See GETTING-STARTED.md in the lightnode repo.`);
658
731
  break;
659
732
  }
660
733
  case "batch": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightnode-sdk",
3
- "version": "0.7.20",
3
+ "version": "0.8.1",
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",