lightnode-sdk 0.4.0 → 0.4.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/dist/add.d.ts CHANGED
@@ -36,6 +36,12 @@ export declare function addAnalyticsDashboard(opts?: AddOpts): {
36
36
  template: Template;
37
37
  network: Network;
38
38
  };
39
+ export declare function addAgent(opts?: AddOpts): {
40
+ written: WrittenFile[];
41
+ install: string;
42
+ template: Template;
43
+ network: Network;
44
+ };
39
45
  export declare function addChat(opts?: AddOpts): {
40
46
  written: WrittenFile[];
41
47
  install: string;
package/dist/add.js CHANGED
@@ -897,6 +897,175 @@ while (true) {
897
897
  }
898
898
  }
899
899
  `;
900
+ // ---------------------------------------------------------------------------
901
+ // `lightnode add agent` - drop in a scheduled/loop inference scaffold. Good
902
+ // for daily summarizers, monitoring agents, cron jobs that run inference on a
903
+ // fixed cadence. For Next.js: a /api/agent route that Vercel Cron (or any
904
+ // cron-style trigger) can hit on schedule. For Node: a standalone script that
905
+ // runs inference on an interval until you kill it.
906
+ // ---------------------------------------------------------------------------
907
+ const NEXTJS_AGENT_ROUTE = `// app/api/agent/route.ts
908
+ // Generated by 'lightnode add agent'.
909
+ //
910
+ // Set up Vercel Cron in vercel.json:
911
+ // { "crons": [{ "path": "/api/agent", "schedule": "0 9 * * *" }] }
912
+ // That hits this route every day at 09:00 UTC.
913
+ //
914
+ // Auth your cron call: Vercel Cron sends a Bearer token in the Authorization
915
+ // header that you can verify here against CRON_SECRET. See:
916
+ // https://vercel.com/docs/cron-jobs/manage-cron-jobs#securing-cron-jobs
917
+ import { NextResponse } from "next/server";
918
+ import WS from "ws";
919
+ import { createPublicClient, createWalletClient, http } from "viem";
920
+ import { privateKeyToAccount } from "viem/accounts";
921
+ import { LightNode, GatewayClient, runInference, consumerGatewayUrl, type NetworkId } from "lightnode-sdk";
922
+
923
+ export const runtime = "nodejs";
924
+ export const dynamic = "force-dynamic";
925
+ export const maxDuration = 120;
926
+
927
+ const NETWORK = (process.env.NETWORK ?? "testnet") as NetworkId;
928
+ const MODEL = process.env.MODEL ?? "llama3-8b";
929
+ const TASK_PROMPT = process.env.AGENT_TASK ?? "Summarize today's news in 3 bullet points.";
930
+
931
+ export async function GET(req: Request) {
932
+ // Verify Vercel Cron sent this. Set CRON_SECRET in your Vercel env vars; the
933
+ // platform automatically injects it as the Bearer token on cron-fired requests.
934
+ const auth = req.headers.get("authorization");
935
+ if (process.env.CRON_SECRET && auth !== \`Bearer \${process.env.CRON_SECRET}\`) {
936
+ return NextResponse.json({ error: "unauthorized" }, { status: 401 });
937
+ }
938
+ if (!process.env.PRIVATE_KEY?.startsWith("0x")) {
939
+ return NextResponse.json({ error: "PRIVATE_KEY not configured" }, { status: 500 });
940
+ }
941
+
942
+ const ln = new LightNode(NETWORK);
943
+ const acct = privateKeyToAccount(process.env.PRIVATE_KEY as \`0x\${string}\`);
944
+ const chain = { id: ln.network.chainId, name: ln.network.label, nativeCurrency: { name: "LCAI", symbol: "LCAI", decimals: 18 }, rpcUrls: { default: { http: [ln.network.rpc] } } };
945
+ const publicClient = createPublicClient({ transport: http(ln.network.rpc), chain });
946
+ const wallet = createWalletClient({ account: acct, transport: http(ln.network.rpc), chain });
947
+
948
+ // SIWE -> JWT (one handshake per agent run).
949
+ const ch = await (await fetch(\`\${consumerGatewayUrl(NETWORK)}/api/auth/challenge?address=\${acct.address}\`)).json() as { message?: string };
950
+ if (!ch.message) return NextResponse.json({ error: "auth challenge failed" }, { status: 502 });
951
+ const sig = await wallet.signMessage({ message: ch.message });
952
+ 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 };
953
+ if (!verify.token) return NextResponse.json({ error: "auth verify failed" }, { status: 502 });
954
+ const gateway = new GatewayClient({ network: NETWORK, bearer: verify.token });
955
+
956
+ // ---- your agent's logic ------------------------------------------------
957
+ // Replace this block with whatever your scheduled task should do. By default
958
+ // it just runs a single inference call with the AGENT_TASK prompt and stores
959
+ // the result; you might fetch upstream data first, run multiple turns, post
960
+ // results to Slack/Discord/a DB, etc.
961
+ try {
962
+ const result = await runInference({
963
+ prompt: TASK_PROMPT,
964
+ gateway, wallet, publicClient, network: ln.network,
965
+ model: MODEL, WebSocket: WS, maxRetries: 2,
966
+ });
967
+ // TODO: persist result.answer somewhere durable (DB, S3, send to Slack, etc.).
968
+ return NextResponse.json({
969
+ ok: true,
970
+ answer: result.answer,
971
+ txs: result.txs,
972
+ jobId: result.jobId.toString(),
973
+ ranAt: new Date().toISOString(),
974
+ });
975
+ } catch (e) {
976
+ return NextResponse.json({ ok: false, error: (e as Error).message }, { status: 500 });
977
+ }
978
+ }
979
+ `;
980
+ const NEXTJS_AGENT_VERCEL_JSON = `{
981
+ "crons": [
982
+ {
983
+ "path": "/api/agent",
984
+ "schedule": "0 9 * * *"
985
+ }
986
+ ]
987
+ }
988
+ `;
989
+ const NODE_AGENT_SCRIPT = `// agent.ts
990
+ // Generated by 'lightnode add agent'. A long-running script that runs inference
991
+ // on a fixed cadence. Use it under pm2, systemd, a Docker container - anywhere
992
+ // you'd run a daemon.
993
+ // npm install lightnode-sdk viem ws
994
+ // AGENT_INTERVAL_MS=3600000 tsx agent.ts
995
+ import WS from "ws";
996
+ import { createPublicClient, createWalletClient, http } from "viem";
997
+ import { privateKeyToAccount } from "viem/accounts";
998
+ import { LightNode, GatewayClient, runInference, consumerGatewayUrl, isStalledWorker, type NetworkId } from "lightnode-sdk";
999
+
1000
+ const NETWORK = (process.env.NETWORK ?? "testnet") as NetworkId;
1001
+ const MODEL = process.env.MODEL ?? "llama3-8b";
1002
+ const TASK_PROMPT = process.env.AGENT_TASK ?? "Summarize today's news in 3 bullet points.";
1003
+ const INTERVAL_MS = Number(process.env.AGENT_INTERVAL_MS ?? 24 * 60 * 60 * 1000); // default daily
1004
+ const PRIVATE_KEY = process.env.PRIVATE_KEY as \`0x\${string}\` | undefined;
1005
+ if (!PRIVATE_KEY?.startsWith("0x") || PRIVATE_KEY.length !== 66) { console.error("set PRIVATE_KEY in .env"); process.exit(1); }
1006
+
1007
+ const ln = new LightNode(NETWORK);
1008
+ const acct = privateKeyToAccount(PRIVATE_KEY);
1009
+ const chain = { id: ln.network.chainId, name: ln.network.label, nativeCurrency: { name: "LCAI", symbol: "LCAI", decimals: 18 }, rpcUrls: { default: { http: [ln.network.rpc] } } };
1010
+ const publicClient = createPublicClient({ transport: http(ln.network.rpc), chain });
1011
+ const wallet = createWalletClient({ account: acct, transport: http(ln.network.rpc), chain });
1012
+
1013
+ // One SIWE handshake per process; refreshed lazily when the JWT expires.
1014
+ async function freshGateway() {
1015
+ const ch = await (await fetch(\`\${consumerGatewayUrl(NETWORK)}/api/auth/challenge?address=\${acct.address}\`)).json() as { message?: string };
1016
+ if (!ch.message) throw new Error("auth challenge failed");
1017
+ const sig = await wallet.signMessage({ message: ch.message });
1018
+ 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 };
1019
+ if (!verify.token) throw new Error("auth verify failed");
1020
+ return new GatewayClient({ network: NETWORK, bearer: verify.token });
1021
+ }
1022
+
1023
+ let gateway = await freshGateway();
1024
+ console.log(\`▶ agent ready on \${NETWORK}, interval=\${(INTERVAL_MS / 1000 / 60).toFixed(1)} min, task=\${TASK_PROMPT.slice(0, 60)}…\\n\`);
1025
+
1026
+ async function tick() {
1027
+ console.log(\`[\${new Date().toISOString()}] tick\`);
1028
+ try {
1029
+ const result = await runInference({
1030
+ prompt: TASK_PROMPT,
1031
+ gateway, wallet, publicClient, network: ln.network,
1032
+ model: MODEL, WebSocket: WS, maxRetries: 2,
1033
+ });
1034
+ // ---- your agent's output sink: write to a DB, post to Slack, etc. ----
1035
+ console.log(\`[\${new Date().toISOString()}] answer:\\n\${result.answer}\\n (createSession=\${result.txs.createSession.slice(0, 14)}…, jobCompleted=\${result.txs.jobCompleted.slice(0, 14)}…)\\n\`);
1036
+ } catch (e) {
1037
+ if (isStalledWorker(e)) console.error(\`[\${new Date().toISOString()}] all workers stalled this round; protocol refunds, skipping\`);
1038
+ else if ((e as Error).message.includes("auth")) { console.warn("JWT expired; re-authing"); gateway = await freshGateway(); }
1039
+ else console.error(\`[\${new Date().toISOString()}] tick failed:\`, (e as Error).message);
1040
+ }
1041
+ }
1042
+
1043
+ await tick(); // run once immediately
1044
+ setInterval(tick, INTERVAL_MS);
1045
+
1046
+ // Keep alive forever.
1047
+ process.on("SIGINT", () => { console.log("\\n▶ agent stopped"); process.exit(0); });
1048
+ `;
1049
+ export function addAgent(opts = {}) {
1050
+ const cwd = opts.cwd ?? process.cwd();
1051
+ const network = opts.network ?? "testnet";
1052
+ const template = opts.template && opts.template !== "auto" ? opts.template : detectTemplate(cwd);
1053
+ const force = !!opts.force;
1054
+ const written = [];
1055
+ if (template === "nextjs-api") {
1056
+ written.push(writeFile(path.join(cwd, "app/api/agent/route.ts"), NEXTJS_AGENT_ROUTE, force));
1057
+ // Only add vercel.json if there isn't one; merging is too fragile to do
1058
+ // blindly here (we'd risk clobbering the user's existing config).
1059
+ if (!fs.existsSync(path.join(cwd, "vercel.json"))) {
1060
+ written.push(writeFile(path.join(cwd, "vercel.json"), NEXTJS_AGENT_VERCEL_JSON, force));
1061
+ }
1062
+ }
1063
+ else {
1064
+ written.push(writeFile(path.join(cwd, "agent.ts"), NODE_AGENT_SCRIPT, force));
1065
+ }
1066
+ written.push(writeFile(path.join(cwd, ".env.example"), ENV_EXAMPLE(network), force));
1067
+ return { written, install: `npm install ${depsNeeded(template).join(" ")}`, template, network };
1068
+ }
900
1069
  export function addChat(opts = {}) {
901
1070
  const cwd = opts.cwd ?? process.cwd();
902
1071
  const network = opts.network ?? "testnet";
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { LightNode, modelStatsCsv, workerStatsCsv, workerJobsCsv } from "./index.js";
3
- import { addInference, addAnalyticsDashboard, addNftMint, addChat } from "./add.js";
3
+ import { addInference, addAnalyticsDashboard, addNftMint, addChat, addAgent } from "./add.js";
4
4
  function flag(name) {
5
5
  const i = process.argv.indexOf(name);
6
6
  return i >= 0 ? process.argv[i + 1] : undefined;
@@ -28,6 +28,7 @@ const HELP = `lightnode <command> [--net mainnet|testnet]
28
28
 
29
29
  add inference end-to-end encrypted inference route/script
30
30
  add chat chat-style UI with conversation history
31
+ add agent scheduled/loop inference (cron-style)
31
32
  add analytics-dashboard read-only network + worker analytics page
32
33
  add nft-mint-with-inference AI-generated NFT metadata (provenance on-chain)
33
34
  (all add commands: [--template auto|nextjs-api|hono|node] [--force])
@@ -103,7 +104,7 @@ async function main() {
103
104
  const template = flag("--template") ?? "auto";
104
105
  const force = process.argv.includes("--force");
105
106
  const network = (net === "mainnet" ? "mainnet" : "testnet");
106
- const known = ["inference", "chat", "analytics-dashboard", "nft-mint-with-inference"];
107
+ const known = ["inference", "chat", "agent", "analytics-dashboard", "nft-mint-with-inference"];
107
108
  if (!known.includes(sub ?? "")) {
108
109
  die(`usage: lightnode add <${known.join("|")}> [--template auto|nextjs-api|hono|node] [--net testnet|mainnet] [--force]`);
109
110
  }
@@ -113,7 +114,9 @@ async function main() {
113
114
  ? addNftMint({ template, network, force })
114
115
  : sub === "chat"
115
116
  ? addChat({ template, network, force })
116
- : addInference({ template, network, force });
117
+ : sub === "agent"
118
+ ? addAgent({ template, network, force })
119
+ : addInference({ template, network, force });
117
120
  console.log(`▶ add ${sub} (${result.template} template, default network ${result.network})`);
118
121
  for (const f of result.written) {
119
122
  if (f.skipped)
@@ -128,9 +131,16 @@ async function main() {
128
131
  else {
129
132
  console.log(`\nNext steps:`);
130
133
  console.log(` 1. ${result.install}`);
131
- if (sub === "nft-mint-with-inference" || sub === "inference" || sub === "chat") {
134
+ if (sub === "nft-mint-with-inference" || sub === "inference" || sub === "chat" || sub === "agent") {
132
135
  console.log(` 2. cp .env.example .env (and put a funded ${result.network} PRIVATE_KEY in it)`);
133
- if (sub === "chat" && result.template === "nextjs-api") {
136
+ if (sub === "agent" && result.template === "nextjs-api") {
137
+ console.log(` 3. Set CRON_SECRET in your Vercel env vars + edit AGENT_TASK in .env`);
138
+ console.log(` 4. Deploy. Vercel Cron fires /api/agent on the schedule in vercel.json`);
139
+ }
140
+ else if (sub === "agent") {
141
+ console.log(` 3. AGENT_INTERVAL_MS=3600000 tsx agent.ts # or run under pm2/systemd`);
142
+ }
143
+ else if (sub === "chat" && result.template === "nextjs-api") {
134
144
  console.log(` 3. Make sure /api/inference is mounted too (run: npx lightnode add inference)`);
135
145
  console.log(` 4. npm run dev, open /chat`);
136
146
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightnode-sdk",
3
- "version": "0.4.0",
3
+ "version": "0.4.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",