lightnode-sdk 0.10.3 → 0.10.5
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.js +169 -110
- package/dist/cli.js +4 -6
- package/package.json +1 -1
package/dist/add.js
CHANGED
|
@@ -739,126 +739,159 @@ process.exit(0);
|
|
|
739
739
|
// formats every prior turn into the next prompt so the model has context.
|
|
740
740
|
// ---------------------------------------------------------------------------
|
|
741
741
|
const NEXTJS_CHAT_PAGE = `// app/chat/page.tsx
|
|
742
|
-
// Generated by 'lightnode add chat'.
|
|
742
|
+
// Generated by 'lightnode add chat'. Server-paid: your funded PRIVATE_KEY (in
|
|
743
|
+
// .env) pays each turn; the streaming /api/inference route runs the inference.
|
|
743
744
|
"use client";
|
|
744
|
-
import { useState } from "react";
|
|
745
|
+
import { useEffect, useRef, useState } from "react";
|
|
746
|
+
import { Streamdown } from "streamdown";
|
|
747
|
+
import { LcaiMark } from "@/components/lcai-mark";
|
|
745
748
|
|
|
746
|
-
type Turn = { role: "user" | "assistant"; text: string;
|
|
749
|
+
type Turn = { role: "user" | "assistant"; text: string; streaming?: boolean; jobCompleted?: string };
|
|
747
750
|
|
|
748
751
|
export default function Chat() {
|
|
749
752
|
const [turns, setTurns] = useState<Turn[]>([]);
|
|
750
753
|
const [draft, setDraft] = useState("");
|
|
751
754
|
const [busy, setBusy] = useState(false);
|
|
755
|
+
const [err, setErr] = useState<string | null>(null);
|
|
756
|
+
const endRef = useRef<HTMLDivElement>(null);
|
|
757
|
+
|
|
758
|
+
useEffect(() => {
|
|
759
|
+
endRef.current?.scrollIntoView({ behavior: busy ? "auto" : "smooth", block: "nearest" });
|
|
760
|
+
}, [turns, busy]);
|
|
752
761
|
|
|
753
762
|
async function send() {
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
setTurns(next);
|
|
757
|
-
setDraft("");
|
|
763
|
+
const next = draft.trim();
|
|
764
|
+
if (!next || busy) return;
|
|
758
765
|
setBusy(true);
|
|
759
|
-
|
|
760
|
-
|
|
766
|
+
setErr(null);
|
|
767
|
+
const history: Turn[] = [...turns, { role: "user", text: next }];
|
|
768
|
+
setTurns([...history, { role: "assistant", text: "", streaming: true }]);
|
|
769
|
+
setDraft("");
|
|
770
|
+
const patch = (p: Partial<Turn>) =>
|
|
771
|
+
setTurns((prev) => prev.map((t, i) => (i === prev.length - 1 ? { ...t, ...p } : t)));
|
|
761
772
|
try {
|
|
762
|
-
const prompt =
|
|
763
|
-
next.map((t) => (t.role === "user" ? \`User: \${t.text}\` : \`Assistant: \${t.text}\`)).join("\\n") +
|
|
764
|
-
"\\nAssistant:";
|
|
773
|
+
const prompt = history.map((t) => (t.role === "user" ? "User: " : "Assistant: ") + t.text).join("\\n") + "\\nAssistant:";
|
|
765
774
|
const r = await fetch("/api/inference", {
|
|
766
775
|
method: "POST",
|
|
767
776
|
headers: { "Content-Type": "application/json" },
|
|
768
777
|
body: JSON.stringify({ prompt, stream: true }),
|
|
769
778
|
});
|
|
770
779
|
if (!r.ok || !r.body) {
|
|
771
|
-
const
|
|
772
|
-
throw new Error(
|
|
780
|
+
const raw = await r.text().catch(() => "");
|
|
781
|
+
if (/PRIVATE_KEY/i.test(raw)) throw new Error("No PRIVATE_KEY yet. Put a funded mainnet key in .env, then restart 'npm run dev'.");
|
|
782
|
+
throw new Error("route returned " + r.status + (raw ? ": " + raw.slice(0, 160) : ""));
|
|
773
783
|
}
|
|
774
784
|
const reader = r.body.getReader();
|
|
775
785
|
const decoder = new TextDecoder();
|
|
776
786
|
let assembled = "";
|
|
777
|
-
let
|
|
778
|
-
|
|
787
|
+
let jobCompleted: string | undefined;
|
|
788
|
+
for (;;) {
|
|
779
789
|
const { done, value } = await reader.read();
|
|
780
790
|
if (done) break;
|
|
781
791
|
const text = decoder.decode(value, { stream: true });
|
|
782
|
-
// The route
|
|
783
|
-
// delimiter (a NUL byte we put between the streamed prose and the
|
|
784
|
-
// metadata). Anything before is body; anything after is metadata.
|
|
792
|
+
// The route appends one JSON line of metadata after a NUL delimiter.
|
|
785
793
|
const nulIdx = text.indexOf("\\u0000");
|
|
786
794
|
if (nulIdx === -1) {
|
|
787
795
|
assembled += text;
|
|
788
|
-
|
|
796
|
+
patch({ text: assembled });
|
|
789
797
|
} else {
|
|
790
798
|
assembled += text.slice(0, nulIdx);
|
|
791
|
-
try {
|
|
792
|
-
|
|
799
|
+
try { jobCompleted = (JSON.parse(text.slice(nulIdx + 1)) as { jobCompleted?: string }).jobCompleted; } catch { /* ignore */ }
|
|
800
|
+
patch({ text: assembled, jobCompleted });
|
|
793
801
|
}
|
|
794
802
|
}
|
|
803
|
+
patch({ streaming: false, jobCompleted });
|
|
795
804
|
} catch (e) {
|
|
796
|
-
//
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
));
|
|
805
|
+
setTurns(history); // drop the empty assistant bubble; show the error below
|
|
806
|
+
setDraft(next);
|
|
807
|
+
setErr((e as Error).message);
|
|
800
808
|
} finally {
|
|
801
809
|
setBusy(false);
|
|
802
810
|
}
|
|
803
811
|
}
|
|
804
812
|
|
|
805
813
|
return (
|
|
806
|
-
<main
|
|
807
|
-
<
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
<div
|
|
813
|
-
{turns.
|
|
814
|
-
<div
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
whiteSpace: "pre-wrap",
|
|
823
|
-
lineHeight: 1.5,
|
|
824
|
-
}}
|
|
825
|
-
>
|
|
826
|
-
<div style={{ fontSize: 11, color: "#888", marginBottom: 4, textTransform: "uppercase", letterSpacing: 0.5 }}>
|
|
827
|
-
{t.role}
|
|
828
|
-
</div>
|
|
829
|
-
{t.text}
|
|
830
|
-
{t.txs && (
|
|
831
|
-
<div style={{ marginTop: 8, fontSize: 11, color: "#888", fontFamily: "monospace" }}>
|
|
832
|
-
jobCompleted: {t.txs.jobCompleted?.slice(0, 18)}…
|
|
833
|
-
</div>
|
|
834
|
-
)}
|
|
835
|
-
</div>
|
|
836
|
-
))}
|
|
837
|
-
{busy && (
|
|
838
|
-
<div style={{ padding: 14, color: "#888", fontStyle: "italic" }}>
|
|
839
|
-
running encrypted inference…
|
|
814
|
+
<main className="mx-auto flex min-h-screen w-full max-w-2xl flex-col px-4 py-6">
|
|
815
|
+
<header className="border-b border-border pb-4">
|
|
816
|
+
<h1 className="font-semibold text-foreground">Chat</h1>
|
|
817
|
+
<p className="text-xs text-muted-foreground">Server-paid - your funded wallet covers each turn (~0.02 LCAI on mainnet, free on testnet).</p>
|
|
818
|
+
</header>
|
|
819
|
+
|
|
820
|
+
<div className="flex flex-1 flex-col gap-6 overflow-y-auto py-6">
|
|
821
|
+
{turns.length === 0 ? (
|
|
822
|
+
<div className="flex flex-1 flex-col items-center justify-center gap-4 text-center">
|
|
823
|
+
<LcaiMark className="size-12" />
|
|
824
|
+
<h2 className="text-3xl font-medium tracking-tight text-foreground">
|
|
825
|
+
Start talking with <span className="text-gradient">AI Chat</span>
|
|
826
|
+
</h2>
|
|
827
|
+
<p className="max-w-sm text-sm text-muted-foreground">
|
|
828
|
+
Your server signs each turn with the PRIVATE_KEY in .env, so visitors never need a wallet.
|
|
829
|
+
</p>
|
|
840
830
|
</div>
|
|
831
|
+
) : (
|
|
832
|
+
turns.map((t, i) =>
|
|
833
|
+
t.role === "user" ? (
|
|
834
|
+
<div key={i} className="flex justify-end">
|
|
835
|
+
<div className="w-fit max-w-[85%] whitespace-pre-wrap break-words rounded-2xl bg-surface-base-faint px-4 py-2.5 text-sm text-foreground">
|
|
836
|
+
{t.text}
|
|
837
|
+
</div>
|
|
838
|
+
</div>
|
|
839
|
+
) : (
|
|
840
|
+
<div key={i} className="flex gap-3">
|
|
841
|
+
<LcaiMark className="mt-0.5 size-7 shrink-0" />
|
|
842
|
+
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
|
843
|
+
{t.text ? (
|
|
844
|
+
t.streaming ? (
|
|
845
|
+
<div className="whitespace-pre-wrap break-words text-sm leading-relaxed text-foreground">{t.text}</div>
|
|
846
|
+
) : (
|
|
847
|
+
<div className="max-w-none text-sm leading-relaxed text-foreground [&_*:first-child]:mt-0 [&_*:last-child]:mb-0">
|
|
848
|
+
<Streamdown>{t.text}</Streamdown>
|
|
849
|
+
</div>
|
|
850
|
+
)
|
|
851
|
+
) : (
|
|
852
|
+
<div className="animate-pulse-dot pt-1 text-sm text-muted-foreground">Thinking...</div>
|
|
853
|
+
)}
|
|
854
|
+
{t.jobCompleted ? (
|
|
855
|
+
<div className="text-[11px] text-muted-foreground">committed on-chain · <code className="font-mono">{t.jobCompleted.slice(0, 12)}…</code></div>
|
|
856
|
+
) : null}
|
|
857
|
+
</div>
|
|
858
|
+
</div>
|
|
859
|
+
)
|
|
860
|
+
)
|
|
841
861
|
)}
|
|
862
|
+
<div ref={endRef} />
|
|
842
863
|
</div>
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
864
|
+
|
|
865
|
+
{err ? (
|
|
866
|
+
<p className="mb-3 rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">{err}</p>
|
|
867
|
+
) : null}
|
|
868
|
+
|
|
869
|
+
<div className="rounded-2xl border border-border bg-card p-3">
|
|
870
|
+
<div className="flex items-start gap-2">
|
|
871
|
+
<LcaiMark className="mt-2 size-5 shrink-0" />
|
|
872
|
+
<textarea
|
|
873
|
+
value={draft}
|
|
874
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
875
|
+
onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }}
|
|
876
|
+
rows={1}
|
|
877
|
+
placeholder="Send a message..."
|
|
878
|
+
className="max-h-40 min-h-[44px] w-full resize-none bg-transparent px-2 py-2 text-sm text-foreground outline-none placeholder:text-muted-foreground"
|
|
879
|
+
/>
|
|
880
|
+
</div>
|
|
881
|
+
<div className="mt-1 flex justify-end">
|
|
882
|
+
<button
|
|
883
|
+
type="button"
|
|
884
|
+
onClick={() => send()}
|
|
885
|
+
disabled={busy || !draft.trim()}
|
|
886
|
+
className="flex size-9 items-center justify-center rounded-full bg-gradient-primary text-white transition hover:brightness-110 disabled:cursor-not-allowed disabled:bg-none disabled:bg-muted disabled:text-muted-foreground"
|
|
887
|
+
aria-label="Send"
|
|
888
|
+
>
|
|
889
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M12 19V5M5 12l7-7 7 7" /></svg>
|
|
890
|
+
</button>
|
|
891
|
+
</div>
|
|
858
892
|
</div>
|
|
859
|
-
<p
|
|
860
|
-
|
|
861
|
-
See <code>LIGHTNODE-HOSTING.md</code> for picking a host that handles 60-90s function calls.
|
|
893
|
+
<p className="mt-3 text-[11px] text-muted-foreground">
|
|
894
|
+
Each call is signed server-side with the PRIVATE_KEY in <code className="font-mono">.env</code>. See <code className="font-mono">LIGHTNODE-HOSTING.md</code> for a host that handles 60-90s calls.
|
|
862
895
|
</p>
|
|
863
896
|
</main>
|
|
864
897
|
);
|
|
@@ -1776,55 +1809,46 @@ Reply with STRICT JSON only, matching: { "passed": boolean, "confidence": 0-1, "
|
|
|
1776
1809
|
}
|
|
1777
1810
|
`;
|
|
1778
1811
|
const NODE_CHAT_REPL = `// chat-repl.ts
|
|
1779
|
-
// Generated by 'lightnode add chat'. Interactive chat
|
|
1780
|
-
//
|
|
1781
|
-
// tsx chat-repl.ts
|
|
1812
|
+
// Generated by 'lightnode add chat'. Interactive multi-turn chat in your terminal.
|
|
1813
|
+
// cp .env.example .env (put a funded PRIVATE_KEY in it)
|
|
1814
|
+
// npx tsx chat-repl.ts
|
|
1782
1815
|
import * as readline from "node:readline/promises";
|
|
1783
1816
|
import { stdin as input, stdout as output } from "node:process";
|
|
1784
|
-
import
|
|
1785
|
-
import { createPublicClient, createWalletClient, http } from "viem";
|
|
1786
|
-
import { privateKeyToAccount } from "viem/accounts";
|
|
1787
|
-
import { LightNode, runInference, GatewayClient, consumerGatewayUrl, type NetworkId } from "lightnode-sdk";
|
|
1817
|
+
import { runInferenceWithKey, type NetworkId } from "lightnode-sdk";
|
|
1788
1818
|
|
|
1789
|
-
const NETWORK = (process.env.NETWORK ?? "
|
|
1819
|
+
const NETWORK = (process.env.NETWORK ?? "mainnet") as NetworkId;
|
|
1790
1820
|
const MODEL = process.env.MODEL ?? "llama3-8b";
|
|
1821
|
+
const SYSTEM = "You are a concise assistant. Reply in one or two short sentences.";
|
|
1791
1822
|
const PRIVATE_KEY = process.env.PRIVATE_KEY as \`0x\${string}\` | undefined;
|
|
1792
|
-
if (!PRIVATE_KEY?.startsWith("0x") || PRIVATE_KEY.length !== 66) {
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
const chain = { id: ln.network.chainId, name: ln.network.label, nativeCurrency: { name: "LCAI", symbol: "LCAI", decimals: 18 }, rpcUrls: { default: { http: [ln.network.rpc] } } };
|
|
1797
|
-
const pub = createPublicClient({ transport: http(ln.network.rpc), chain });
|
|
1798
|
-
const wal = createWalletClient({ account: acct, transport: http(ln.network.rpc), chain });
|
|
1799
|
-
|
|
1800
|
-
// One SIWE handshake per process; the JWT is reused across all turns.
|
|
1801
|
-
const ch = await (await fetch(\`\${consumerGatewayUrl(NETWORK)}/api/auth/challenge?address=\${acct.address}\`)).json() as { message?: string };
|
|
1802
|
-
if (!ch.message) throw new Error("auth challenge failed");
|
|
1803
|
-
const sig = await wal.signMessage({ message: ch.message });
|
|
1804
|
-
const verify = await (await fetch(\`\${consumerGatewayUrl(NETWORK)}/api/auth/verify\`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: ch.message, signature: sig }) })).json() as { token?: string };
|
|
1805
|
-
if (!verify.token) throw new Error("auth verify failed");
|
|
1806
|
-
const gateway = new GatewayClient({ network: NETWORK, bearer: verify.token });
|
|
1823
|
+
if (!PRIVATE_KEY?.startsWith("0x") || PRIVATE_KEY.length !== 66) {
|
|
1824
|
+
console.error("Set a funded PRIVATE_KEY in .env (see .env.example).");
|
|
1825
|
+
process.exit(1);
|
|
1826
|
+
}
|
|
1807
1827
|
|
|
1808
1828
|
const rl = readline.createInterface({ input, output });
|
|
1809
1829
|
const turns: { role: "user" | "assistant"; text: string }[] = [];
|
|
1810
|
-
console.log(
|
|
1830
|
+
console.log(\`Chat on \${NETWORK} (\${MODEL}). Your funded key pays each turn. Ctrl+C to exit.\\n\`);
|
|
1811
1831
|
|
|
1812
1832
|
while (true) {
|
|
1813
|
-
const user = (await rl.question("> ")).trim();
|
|
1833
|
+
const user = (await rl.question("you > ")).trim();
|
|
1814
1834
|
if (!user) continue;
|
|
1815
1835
|
turns.push({ role: "user", text: user });
|
|
1816
|
-
const prompt = turns.map((t) => (t.role === "user" ?
|
|
1836
|
+
const prompt = SYSTEM + "\\n\\n" + turns.map((t) => (t.role === "user" ? "User: " : "Assistant: ") + t.text).join("\\n\\n") + "\\n\\nAssistant:";
|
|
1817
1837
|
try {
|
|
1818
|
-
process.stdout.write(" ");
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1838
|
+
process.stdout.write("ai > ");
|
|
1839
|
+
// runInferenceWithKey builds the viem clients, runs SIWE, and auto-loads
|
|
1840
|
+
// 'ws' in Node - no manual client wiring (and no type casts) needed.
|
|
1841
|
+
const { answer } = await runInferenceWithKey({
|
|
1842
|
+
network: NETWORK,
|
|
1843
|
+
privateKey: PRIVATE_KEY,
|
|
1844
|
+
model: MODEL,
|
|
1845
|
+
prompt,
|
|
1822
1846
|
onChunk: (chunk) => process.stdout.write(chunk),
|
|
1823
1847
|
});
|
|
1824
1848
|
process.stdout.write("\\n\\n");
|
|
1825
1849
|
turns.push({ role: "assistant", text: answer });
|
|
1826
1850
|
} catch (e) {
|
|
1827
|
-
console.log(
|
|
1851
|
+
console.log(" (error: " + (e as Error).message + ")\\n");
|
|
1828
1852
|
}
|
|
1829
1853
|
}
|
|
1830
1854
|
`;
|
|
@@ -2009,6 +2033,7 @@ export function addChat(opts = {}) {
|
|
|
2009
2033
|
// can run the whole stack locally (or anywhere Docker runs) with one
|
|
2010
2034
|
// command. No external host signup, no function-timeout fights.
|
|
2011
2035
|
written.push(writeFile(path.join(cwd, "app/chat/page.tsx"), NEXTJS_CHAT_PAGE, force));
|
|
2036
|
+
written.push(writeFile(path.join(cwd, "components/lcai-mark.tsx"), LCAI_MARK_FILE, force));
|
|
2012
2037
|
written.push(writeFile(path.join(cwd, "app/api/inference/route.ts"), NEXTJS_INFERENCE_STREAM_ROUTE, force));
|
|
2013
2038
|
written.push(writeFile(path.join(cwd, "LIGHTNODE-HOSTING.md"), HOSTING_GUIDE, force));
|
|
2014
2039
|
written.push(writeFile(path.join(cwd, "Dockerfile"), NEXTJS_DOCKERFILE, force));
|
|
@@ -2019,7 +2044,9 @@ export function addChat(opts = {}) {
|
|
|
2019
2044
|
written.push(writeFile(path.join(cwd, "chat-repl.ts"), NODE_CHAT_REPL, force));
|
|
2020
2045
|
}
|
|
2021
2046
|
written.push(writeFile(path.join(cwd, ".env.example"), ENV_EXAMPLE(network), force));
|
|
2022
|
-
|
|
2047
|
+
// The nextjs chat page renders markdown with streamdown; the node REPL doesn't.
|
|
2048
|
+
const install = template === "nextjs-api" ? installLine(template) + " streamdown" : installLine(template);
|
|
2049
|
+
return { written, install, template, network };
|
|
2023
2050
|
}
|
|
2024
2051
|
/**
|
|
2025
2052
|
* `lightnode add chat-web3` - the user-pays counterpart to addChat.
|
|
@@ -2611,6 +2638,7 @@ body::before {
|
|
|
2611
2638
|
`;
|
|
2612
2639
|
/** Map an `add` target to the route folder its page lives in. */
|
|
2613
2640
|
const WEB3_ROUTE_DIR = {
|
|
2641
|
+
"chat": "chat",
|
|
2614
2642
|
"chat-web3": "chat-web3",
|
|
2615
2643
|
"inference-web3": "inference-web3",
|
|
2616
2644
|
"judge-web3": "judge-web3",
|
|
@@ -2677,6 +2705,37 @@ const SCAFFOLD_README_WHAT = {
|
|
|
2677
2705
|
/** A real README for a freshly scaffolded app, replacing the create-next-app
|
|
2678
2706
|
* default so the dev knows what they got and how to use it. */
|
|
2679
2707
|
function scaffoldReadme(target, dir) {
|
|
2708
|
+
if (target === "chat") {
|
|
2709
|
+
return `# LightNode chat (server-paid)
|
|
2710
|
+
|
|
2711
|
+
Generated by \`lightnode add chat\`. A streaming chatbot where YOUR funded wallet
|
|
2712
|
+
pays for every visitor's turn - users never touch a wallet (the classic SaaS
|
|
2713
|
+
chatbot pattern).
|
|
2714
|
+
|
|
2715
|
+
## Run it
|
|
2716
|
+
|
|
2717
|
+
cp .env.example .env # then put a funded mainnet PRIVATE_KEY in it
|
|
2718
|
+
npm run dev
|
|
2719
|
+
|
|
2720
|
+
Open http://localhost:3000 (the chat is the homepage; also served at /chat).
|
|
2721
|
+
Mainnet llama3-8b is ~0.02 LCAI per turn. Free testnet LCAI: https://lightfaucet.ai
|
|
2722
|
+
|
|
2723
|
+
## Where things live
|
|
2724
|
+
|
|
2725
|
+
- \`app/page.tsx\` - re-exports the chat as the homepage
|
|
2726
|
+
- \`app/chat/page.tsx\` - the streaming chat UI (also at /chat). Edit this.
|
|
2727
|
+
- \`app/api/inference/route.ts\` - the streaming server route (uses PRIVATE_KEY)
|
|
2728
|
+
- \`.env.example\` - PRIVATE_KEY (+ NETWORK, MODEL)
|
|
2729
|
+
- \`Dockerfile\` + \`docker-compose.yml\` - run the whole stack with no function timeout
|
|
2730
|
+
- \`LIGHTNODE-HOSTING.md\` - deploy notes
|
|
2731
|
+
|
|
2732
|
+
## Customize
|
|
2733
|
+
|
|
2734
|
+
Change the model or system prompt in \`app/api/inference/route.ts\`. The page is a
|
|
2735
|
+
normal React client component streaming from your route. Builder docs:
|
|
2736
|
+
https://lightnode.app/build
|
|
2737
|
+
`;
|
|
2738
|
+
}
|
|
2680
2739
|
const what = SCAFFOLD_README_WHAT[target] ?? "A self-contained, wallet-signed page.";
|
|
2681
2740
|
return `# LightNode ${dir}
|
|
2682
2741
|
|
package/dist/cli.js
CHANGED
|
@@ -583,7 +583,7 @@ async function main() {
|
|
|
583
583
|
// A Next.js client page needs a Next.js app to live in. In a bare folder,
|
|
584
584
|
// scaffold one first so the generated page renders instead of throwing
|
|
585
585
|
// "Cannot find module 'react'". Opt out with --no-scaffold.
|
|
586
|
-
const didScaffold = NEXT_PAGE_TARGETS.has(sub ?? "") && !existsSync(join(cwd, "package.json")) && !noScaffold
|
|
586
|
+
const didScaffold = (NEXT_PAGE_TARGETS.has(sub ?? "") || sub === "chat") && !existsSync(join(cwd, "package.json")) && !noScaffold
|
|
587
587
|
? scaffoldNextApp(cwd, sub ?? "")
|
|
588
588
|
: false;
|
|
589
589
|
// ---- write the requested files ----
|
|
@@ -617,7 +617,7 @@ async function main() {
|
|
|
617
617
|
// the LightChain theme so localhost:3000 lands on the chat, not the
|
|
618
618
|
// create-next-app starter. Skipped in an existing app (nothing to clobber).
|
|
619
619
|
let wiring = null;
|
|
620
|
-
if (didScaffold && isWeb3Page) {
|
|
620
|
+
if (didScaffold && (isWeb3Page || sub === "chat")) {
|
|
621
621
|
wiring = wireFreshScaffold(sub, { cwd });
|
|
622
622
|
printWritten(wiring.written);
|
|
623
623
|
if (wiring.homepageRoute)
|
|
@@ -669,10 +669,8 @@ async function main() {
|
|
|
669
669
|
console.log(` 3. AGENT_INTERVAL_MS=3600000 npx tsx agent.ts # or run under pm2/systemd`);
|
|
670
670
|
}
|
|
671
671
|
else if (sub === "chat" && result.template === "nextjs-api") {
|
|
672
|
-
console.log(` 3.
|
|
673
|
-
console.log(`
|
|
674
|
-
console.log(` b) npm run dev # local dev only`);
|
|
675
|
-
console.log(` Open http://localhost:3000/chat`);
|
|
672
|
+
console.log(` 3. npm run dev # then open http://localhost:3000${wiring ? "" : "/chat"}`);
|
|
673
|
+
console.log(` (or: docker compose up --build to run the whole stack with no function timeout)`);
|
|
676
674
|
}
|
|
677
675
|
else if (sub === "chat") {
|
|
678
676
|
console.log(` 3. npx tsx chat-repl.ts (interactive terminal chat)`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lightnode-sdk",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.5",
|
|
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",
|