z0gcode 0.2.0

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/src/skills.mjs ADDED
@@ -0,0 +1,43 @@
1
+ // 0G skills: accurate, bundled knowledge about building on the 0G stack.
2
+ // The detailed pattern docs live in skills/0g/ and are read on demand by the agent.
3
+ import { readFileSync, existsSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const SKILLS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "skills", "0g");
8
+
9
+ const SKILL_FILES = {
10
+ chain: "CHAIN.md",
11
+ compute: "COMPUTE.md",
12
+ storage: "STORAGE.md",
13
+ network: "NETWORK_CONFIG.md",
14
+ security: "SECURITY.md",
15
+ testing: "TESTING.md",
16
+ };
17
+
18
+ export function listSkills() {
19
+ return Object.keys(SKILL_FILES).filter((k) => existsSync(path.join(SKILLS_DIR, SKILL_FILES[k])));
20
+ }
21
+
22
+ export function readSkill(name) {
23
+ const file = SKILL_FILES[String(name || "").toLowerCase()];
24
+ if (!file) return null;
25
+ const p = path.join(SKILLS_DIR, file);
26
+ if (!existsSync(p)) return null;
27
+ return readFileSync(p, "utf8");
28
+ }
29
+
30
+ // Concise, accurate 0G primer injected into every session's system prompt.
31
+ export const SYSTEM_0G = `
32
+ You are an expert at building on 0G. Key facts (call read_skill for full patterns):
33
+
34
+ - Your OWN inference runs on the 0G Compute Router (OpenAI-compatible, TEE-backed, private + verifiable). Mainnet: https://router-api.0g.ai/v1. Any app can use it by pointing an OpenAI client at that base_url with a 0G API key.
35
+ - 0G Chain: EVM-compatible L1. Mainnet chainId 16661, RPC https://evmrpc.0g.ai, explorer https://chainscan.0g.ai. Testnet "Galileo" chainId 16602, RPC https://evmrpc-testnet.0g.ai. CRITICAL: compile contracts with evmVersion "cancun" and solidity 0.8.24; use ethers v6 (NOT v5). Deploy with Hardhat or Foundry.
36
+ - 0G Storage: decentralized storage. SDK @0gfoundation/0g-storage-ts-sdk (ZgFile + Indexer; this is the mainnet-current package, the older @0glabs/0g-ts-sdk reverts on mainnet submit). Upload returns a Merkle root hash; always close the file handle. Mainnet indexer https://indexer-storage-turbo.0g.ai, testnet https://indexer-storage-testnet-turbo.0g.ai.
37
+ - 0G Compute (to consume inference from an app): SDK @0glabs/0g-serving-broker with createZGComputeNetworkBroker(wallet), or simply the OpenAI SDK against the Router.
38
+ - You can publish an artifact to 0G Storage yourself with the upload_0g_storage tool (needs --auto and a funded ZOG_WALLET_KEY); it returns a content root hash.
39
+ - You can deploy a compiled contract to 0G Chain with deploy_0g_chain (needs --auto and ZOG_WALLET_KEY): compile with evmVersion "cancun" and pass its bytecode or an artifact path.
40
+ - Security: never hardcode private keys; read them from env (PRIVATE_KEY) and keep .env in .gitignore.
41
+
42
+ 0G skills available via read_skill: ${Object.keys(SKILL_FILES).join(", ")}.
43
+ `.trim();
package/src/tools.mjs ADDED
@@ -0,0 +1,481 @@
1
+ // Tools the agent can call. File ops run always; bash and on-chain ops require --auto.
2
+ import { promises as fs, existsSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { exec } from "node:child_process";
5
+ import { listSkills, readSkill } from "./skills.mjs";
6
+ import { discoverSkills, readUserSkill } from "./user-skills.mjs";
7
+ import { savePlan } from "./plan.mjs";
8
+ import { makeClient } from "./client.mjs";
9
+ import { generateImage, transcribeAudio } from "./media.mjs";
10
+ import { uploadFileToStorage } from "./anchor.mjs";
11
+
12
+ const MAX_READ = 200_000; // chars
13
+ const SKIP_DIRS = new Set([".git", "node_modules", "dist", ".z0g", ".next", "build", "coverage", ".turbo"]);
14
+
15
+ export const TOOL_DEFS = [
16
+ {
17
+ type: "function",
18
+ function: {
19
+ name: "read_file",
20
+ description: "Read a UTF-8 text file and return its contents.",
21
+ parameters: {
22
+ type: "object",
23
+ properties: { path: { type: "string", description: "Path relative to the working directory." } },
24
+ required: ["path"],
25
+ },
26
+ },
27
+ },
28
+ {
29
+ type: "function",
30
+ function: {
31
+ name: "search_files",
32
+ description: "Search the working directory for a JavaScript regular expression. Prefer this over reading whole files to locate code. Optional glob filters filenames (e.g. *.ts).",
33
+ parameters: {
34
+ type: "object",
35
+ properties: {
36
+ query: { type: "string", description: "A JavaScript regular expression." },
37
+ glob: { type: "string", description: "Optional filename glob, e.g. *.mjs" },
38
+ path: { type: "string", description: "Optional subdirectory to search (default '.')" },
39
+ },
40
+ required: ["query"],
41
+ },
42
+ },
43
+ },
44
+ {
45
+ type: "function",
46
+ function: {
47
+ name: "write_file",
48
+ description: "Create or overwrite a file with the given content. Creates parent directories.",
49
+ parameters: {
50
+ type: "object",
51
+ properties: {
52
+ path: { type: "string" },
53
+ content: { type: "string" },
54
+ },
55
+ required: ["path", "content"],
56
+ },
57
+ },
58
+ },
59
+ {
60
+ type: "function",
61
+ function: {
62
+ name: "edit_file",
63
+ description: "Replace the first exact occurrence of old_string with new_string in a file. old_string must be unique and match exactly.",
64
+ parameters: {
65
+ type: "object",
66
+ properties: {
67
+ path: { type: "string" },
68
+ old_string: { type: "string" },
69
+ new_string: { type: "string" },
70
+ },
71
+ required: ["path", "old_string", "new_string"],
72
+ },
73
+ },
74
+ },
75
+ {
76
+ type: "function",
77
+ function: {
78
+ name: "list_dir",
79
+ description: "List entries of a directory (non-recursive).",
80
+ parameters: {
81
+ type: "object",
82
+ properties: { path: { type: "string", description: "Directory path (default '.')" } },
83
+ },
84
+ },
85
+ },
86
+ {
87
+ type: "function",
88
+ function: {
89
+ name: "run_bash",
90
+ description: "Run a bash command in the working directory and return stdout, stderr and exit code. Only available with --auto.",
91
+ parameters: {
92
+ type: "object",
93
+ properties: { command: { type: "string" } },
94
+ required: ["command"],
95
+ },
96
+ },
97
+ },
98
+ {
99
+ type: "function",
100
+ function: {
101
+ name: "upload_0g_storage",
102
+ description: "Upload a file from the working directory to 0G Storage (decentralized) and return its content root hash. Writes on-chain: needs --auto and a funded ZOG_WALLET_KEY.",
103
+ parameters: {
104
+ type: "object",
105
+ properties: { path: { type: "string" } },
106
+ required: ["path"],
107
+ },
108
+ },
109
+ },
110
+ {
111
+ type: "function",
112
+ function: {
113
+ name: "deploy_0g_chain",
114
+ description: "Deploy a compiled contract to 0G Chain mainnet (chainId 16661). Provide { bytecode, abi?, args? } or { artifact } (path to a Hardhat/Foundry JSON with abi+bytecode). Writes on-chain: needs --auto and a funded ZOG_WALLET_KEY.",
115
+ parameters: {
116
+ type: "object",
117
+ properties: {
118
+ bytecode: { type: "string", description: "Contract creation bytecode (0x...)" },
119
+ abi: { type: "array", description: "Contract ABI (optional if no constructor args)" },
120
+ args: { type: "array", description: "Constructor arguments" },
121
+ artifact: { type: "string", description: "Path to a compiled artifact JSON (abi + bytecode)" },
122
+ },
123
+ },
124
+ },
125
+ },
126
+ {
127
+ type: "function",
128
+ function: {
129
+ name: "update_plan",
130
+ description: "Lay out or update a checklist for a multi-step task. Call it at the start of non-trivial work and whenever a step's status changes. Keep exactly one step in_progress.",
131
+ parameters: {
132
+ type: "object",
133
+ properties: {
134
+ plan: {
135
+ type: "array",
136
+ items: {
137
+ type: "object",
138
+ properties: {
139
+ step: { type: "string" },
140
+ status: { type: "string", enum: ["pending", "in_progress", "completed"] },
141
+ },
142
+ required: ["step", "status"],
143
+ },
144
+ },
145
+ },
146
+ required: ["plan"],
147
+ },
148
+ },
149
+ },
150
+ {
151
+ type: "function",
152
+ function: {
153
+ name: "read_skill",
154
+ description: "Read a skill's full instructions by name. Skills include bundled 0G SDK skills (chain, compute, storage, network, security, testing) and any user or project skills listed in the system prompt. Call with no name to list all available skills.",
155
+ parameters: {
156
+ type: "object",
157
+ properties: { name: { type: "string" } },
158
+ },
159
+ },
160
+ },
161
+ {
162
+ type: "function",
163
+ function: {
164
+ name: "spawn_subagents",
165
+ description: "Run several INDEPENDENT read-only subtasks in parallel, each as its own isolated agent, and get back a short summary of each. Use it to review or analyze many files at once, do parallel research, audit for issues, or map a codebase, when the subtasks do not depend on each other. Subagents are READ-ONLY (they cannot write files, run shell, deploy, or spawn more subagents), so use them to gather and understand, then act yourself. Keep it to a handful of focused subtasks.",
166
+ parameters: {
167
+ type: "object",
168
+ properties: {
169
+ tasks: {
170
+ type: "array",
171
+ description: "The independent subtasks to run in parallel.",
172
+ items: {
173
+ type: "object",
174
+ properties: {
175
+ prompt: { type: "string", description: "The self-contained subtask instruction." },
176
+ label: { type: "string", description: "Short label for display." },
177
+ },
178
+ required: ["prompt"],
179
+ },
180
+ },
181
+ },
182
+ required: ["tasks"],
183
+ },
184
+ },
185
+ },
186
+ {
187
+ type: "function",
188
+ function: {
189
+ name: "spawn_write_subagents",
190
+ description: "Run several INDEPENDENT subtasks that WRITE code in parallel, each in its own isolated git worktree, then merge each one's diff back into the working tree. Use it when a change cleanly splits into parts that touch DIFFERENT files (e.g. implement three separate modules, add tests across several files, apply the same refactor to independent files). Each subagent can read, write, edit, and run shell inside its own worktree. Non-overlapping edits merge automatically; subtasks that touch the same file will conflict and be skipped, so keep the file sets disjoint. Requires a git repository. Keep it to a handful of focused subtasks.",
191
+ parameters: {
192
+ type: "object",
193
+ properties: {
194
+ tasks: {
195
+ type: "array",
196
+ description: "The independent write subtasks to run in parallel. Give each a disjoint set of files to touch.",
197
+ items: {
198
+ type: "object",
199
+ properties: {
200
+ prompt: { type: "string", description: "The self-contained subtask instruction, including which file(s) to create or edit." },
201
+ label: { type: "string", description: "Short label for display." },
202
+ },
203
+ required: ["prompt"],
204
+ },
205
+ },
206
+ },
207
+ required: ["tasks"],
208
+ },
209
+ },
210
+ },
211
+ {
212
+ type: "function",
213
+ function: {
214
+ name: "generate_image",
215
+ description: "Generate an image with 0G's image model (z-image-turbo) and save it as a PNG in the working directory. Use for icons, placeholder assets, og-images, or a logo. Costs a small fee per image; at most 2 per call.",
216
+ parameters: {
217
+ type: "object",
218
+ properties: {
219
+ prompt: { type: "string", description: "What to generate." },
220
+ path: { type: "string", description: "Output PNG path relative to cwd. Defaults to image.png." },
221
+ n: { type: "number", description: "How many images (1 or 2). Default 1." },
222
+ },
223
+ required: ["prompt"],
224
+ },
225
+ },
226
+ },
227
+ {
228
+ type: "function",
229
+ function: {
230
+ name: "transcribe_audio",
231
+ description: "Transcribe an audio file (inside the working directory) to text with 0G's speech model (whisper-large-v3).",
232
+ parameters: {
233
+ type: "object",
234
+ properties: { path: { type: "string", description: "Audio file path relative to cwd." } },
235
+ required: ["path"],
236
+ },
237
+ },
238
+ },
239
+ ];
240
+
241
+ function safeResolve(cwd, p) {
242
+ const abs = path.resolve(cwd, p || ".");
243
+ const rel = path.relative(cwd, abs);
244
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
245
+ throw new Error(`path escapes the working directory: ${p}`);
246
+ }
247
+ return abs;
248
+ }
249
+
250
+ export function makeExecutor({ cwd, allowBash, sessionDir, onchain = false }) {
251
+ const planDir = sessionDir || path.join(cwd, ".z0g");
252
+ return async function execute(name, args) {
253
+ try {
254
+ switch (name) {
255
+ case "read_file": {
256
+ const abs = safeResolve(cwd, args.path);
257
+ const buf = await fs.readFile(abs, "utf8");
258
+ const text = buf.length > MAX_READ ? buf.slice(0, MAX_READ) + "\n… [truncated]" : buf;
259
+ return { ok: true, summary: `read ${args.path} (${buf.length} bytes)`, content: text };
260
+ }
261
+ case "search_files": {
262
+ const res = await searchFiles(cwd, args.query, args.glob, args.path);
263
+ return { ok: true, summary: `search "${args.query}" (${res.count})`, content: res.text };
264
+ }
265
+ case "write_file": {
266
+ const abs = safeResolve(cwd, args.path);
267
+ let before = "", existed = true;
268
+ try { before = await fs.readFile(abs, "utf8"); } catch { before = ""; existed = false; }
269
+ await fs.mkdir(path.dirname(abs), { recursive: true });
270
+ const after = args.content ?? "";
271
+ await fs.writeFile(abs, after, "utf8");
272
+ return { ok: true, summary: `wrote ${args.path} (${after.length} bytes)`, content: "OK", change: { path: args.path, before, after, created: !existed } };
273
+ }
274
+ case "edit_file": {
275
+ const abs = safeResolve(cwd, args.path);
276
+ const cur = await fs.readFile(abs, "utf8");
277
+ if (!args.old_string || !cur.includes(args.old_string)) {
278
+ return { ok: false, summary: `edit ${args.path} failed`, content: "old_string not found. Read the file and match exactly." };
279
+ }
280
+ if (cur.indexOf(args.old_string) !== cur.lastIndexOf(args.old_string)) {
281
+ return { ok: false, summary: `edit ${args.path} ambiguous`, content: "old_string is not unique. Include more surrounding context." };
282
+ }
283
+ const next = cur.replace(args.old_string, args.new_string ?? "");
284
+ await fs.writeFile(abs, next, "utf8");
285
+ return { ok: true, summary: `edited ${args.path}`, content: "OK", change: { path: args.path, before: cur, after: next } };
286
+ }
287
+ case "list_dir": {
288
+ const abs = safeResolve(cwd, args.path || ".");
289
+ const entries = await fs.readdir(abs, { withFileTypes: true });
290
+ const lines = entries.map((e) => (e.isDirectory() ? e.name + "/" : e.name)).sort();
291
+ return { ok: true, summary: `list ${args.path || "."} (${lines.length})`, content: lines.join("\n") || "(empty)" };
292
+ }
293
+ case "run_bash": {
294
+ if (!allowBash) {
295
+ return { ok: false, summary: "bash denied", content: "run_bash is disabled. Re-run z0gcode with --auto to allow shell commands." };
296
+ }
297
+ const out = await runBash(args.command, cwd);
298
+ return { ok: out.code === 0, summary: `bash exit ${out.code}`, content: out.text };
299
+ }
300
+ case "upload_0g_storage": {
301
+ return await uploadToStorage(cwd, args.path, onchain);
302
+ }
303
+ case "deploy_0g_chain": {
304
+ return await deployToChain(cwd, args, onchain);
305
+ }
306
+ case "update_plan": {
307
+ const plan = Array.isArray(args.plan) ? args.plan : [];
308
+ await savePlan(planDir, plan);
309
+ const done = plan.filter((p) => p.status === "completed").length;
310
+ return { ok: true, summary: `plan ${done}/${plan.length}`, content: "Plan updated.", plan };
311
+ }
312
+ case "read_skill":
313
+ case "read_0g_skill": {
314
+ const userNames = () => discoverSkills(cwd).map((s) => s.name);
315
+ if (!args.name) {
316
+ const all = [...listSkills(), ...userNames()];
317
+ return { ok: true, summary: "list skills", content: "Available skills: " + (all.length ? all.join(", ") : "(none)") };
318
+ }
319
+ const doc = readSkill(args.name) || readUserSkill(cwd, args.name);
320
+ if (!doc) {
321
+ const all = [...listSkills(), ...userNames()];
322
+ return { ok: false, summary: `skill ${args.name} not found`, content: "Unknown skill. Available: " + all.join(", ") };
323
+ }
324
+ return { ok: true, summary: `skill: ${args.name}`, content: doc };
325
+ }
326
+ case "generate_image": {
327
+ if (!args.prompt) return { ok: false, summary: "no prompt", content: "generate_image needs a prompt." };
328
+ const base = safeResolve(cwd, args.path || "image.png").replace(/\.png$/i, "");
329
+ const n = Math.max(1, Math.min(2, Number(args.n) || 1));
330
+ const { images, cost } = await generateImage(makeClient(), { prompt: args.prompt, n });
331
+ const paths = [];
332
+ for (let i = 0; i < images.length; i++) {
333
+ const p = images.length > 1 ? `${base}-${i + 1}.png` : `${base}.png`;
334
+ await fs.mkdir(path.dirname(p), { recursive: true });
335
+ await fs.writeFile(p, Buffer.from(images[i], "base64"));
336
+ paths.push(path.relative(cwd, p));
337
+ }
338
+ const costStr = cost != null ? ` (~$${cost.toFixed(4)})` : "";
339
+ return { ok: true, summary: `wrote ${paths.join(", ")}${costStr}`, content: `Generated ${paths.length} image(s) on 0G: ${paths.join(", ")}${costStr}` };
340
+ }
341
+ case "transcribe_audio": {
342
+ const abs = safeResolve(cwd, args.path || "");
343
+ if (!args.path || !existsSync(abs)) return { ok: false, summary: "no file", content: `Audio file not found: ${args.path || "(none)"}` };
344
+ const { text, cost } = await transcribeAudio(makeClient(), abs);
345
+ const costStr = cost != null ? ` (~$${cost.toFixed(4)})` : "";
346
+ return { ok: true, summary: `transcribed ${path.basename(abs)}${costStr}`, content: text || "(empty transcript)" };
347
+ }
348
+ default:
349
+ return { ok: false, summary: `unknown tool ${name}`, content: `No such tool: ${name}` };
350
+ }
351
+ } catch (e) {
352
+ return { ok: false, summary: `${name} error`, content: `ERROR: ${e.message}` };
353
+ }
354
+ };
355
+ }
356
+
357
+ function globToRegExp(glob) {
358
+ const re = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
359
+ return new RegExp(`^${re}$`);
360
+ }
361
+
362
+ async function searchFiles(cwd, query, glob, subpath) {
363
+ const root = safeResolve(cwd, subpath || ".");
364
+ let re;
365
+ try {
366
+ re = new RegExp(query);
367
+ } catch {
368
+ return { count: 0, text: `invalid regular expression: ${query}` };
369
+ }
370
+ const globRe = glob ? globToRegExp(glob) : null;
371
+ const results = [];
372
+ const cap = 50;
373
+
374
+ async function walk(dir) {
375
+ if (results.length >= cap) return;
376
+ let entries;
377
+ try {
378
+ entries = await fs.readdir(dir, { withFileTypes: true });
379
+ } catch {
380
+ return;
381
+ }
382
+ for (const e of entries) {
383
+ if (results.length >= cap) return;
384
+ if (SKIP_DIRS.has(e.name)) continue;
385
+ const full = path.join(dir, e.name);
386
+ if (e.isDirectory()) {
387
+ await walk(full);
388
+ continue;
389
+ }
390
+ if (globRe && !globRe.test(e.name)) continue;
391
+ let content;
392
+ try {
393
+ const st = await fs.stat(full);
394
+ if (st.size > 1_000_000) continue;
395
+ content = await fs.readFile(full, "utf8");
396
+ } catch {
397
+ continue;
398
+ }
399
+ if (content.includes("\x00")) continue; // binary
400
+ const lines = content.split("\n");
401
+ for (let i = 0; i < lines.length; i++) {
402
+ if (re.test(lines[i])) {
403
+ const rel = path.relative(cwd, full);
404
+ results.push(`${rel}:${i + 1}: ${lines[i].trim().slice(0, 200)}`);
405
+ if (results.length >= cap) break;
406
+ }
407
+ }
408
+ }
409
+ }
410
+ await walk(root);
411
+ return { count: results.length, text: results.join("\n") || "(no matches)" };
412
+ }
413
+
414
+ async function uploadToStorage(cwd, relPath, onchain) {
415
+ if (!onchain) {
416
+ return { ok: false, summary: "on-chain off", content: "On-chain actions are off. Enable with --onchain, /onchain on, or ZOG_ONCHAIN=on." };
417
+ }
418
+ if (!process.env.ZOG_WALLET_KEY) {
419
+ return { ok: false, summary: "no wallet", content: "Set ZOG_WALLET_KEY to a funded 0G mainnet private key to upload to 0G Storage." };
420
+ }
421
+ const abs = safeResolve(cwd, relPath);
422
+ try {
423
+ const { rootHash, txHash } = await uploadFileToStorage(abs);
424
+ return {
425
+ ok: true,
426
+ summary: `uploaded ${relPath} to 0G Storage`,
427
+ content: `0G Storage root: ${rootHash}\ntx: ${txHash}\nexplorer: https://chainscan.0g.ai/tx/${txHash}`,
428
+ };
429
+ } catch (e) {
430
+ return { ok: false, summary: "0g upload failed", content: `ERROR: ${e.message}. Ensure @0gfoundation/0g-storage-ts-sdk is installed and the wallet is funded.` };
431
+ }
432
+ }
433
+
434
+ async function deployToChain(cwd, args, onchain) {
435
+ if (!onchain) {
436
+ return { ok: false, summary: "on-chain off", content: "On-chain actions are off. Enable with --onchain, /onchain on, or ZOG_ONCHAIN=on." };
437
+ }
438
+ const key = process.env.ZOG_WALLET_KEY;
439
+ if (!key) {
440
+ return { ok: false, summary: "no wallet", content: "Set ZOG_WALLET_KEY to a funded 0G mainnet private key to deploy." };
441
+ }
442
+ const RPC = process.env.ZOG_EVM_RPC || "https://evmrpc.0g.ai";
443
+ try {
444
+ const { ethers } = await import("ethers");
445
+ let abi = args.abi || [];
446
+ let bytecode = args.bytecode;
447
+ if (args.artifact) {
448
+ const raw = JSON.parse(await fs.readFile(safeResolve(cwd, args.artifact), "utf8"));
449
+ abi = raw.abi || abi;
450
+ bytecode = raw.bytecode?.object || raw.bytecode || bytecode;
451
+ }
452
+ if (!bytecode) {
453
+ return { ok: false, summary: "no bytecode", content: "Provide 'bytecode' or an 'artifact' path with abi + bytecode." };
454
+ }
455
+ if (typeof bytecode === "string" && !bytecode.startsWith("0x")) bytecode = "0x" + bytecode;
456
+ const provider = new ethers.JsonRpcProvider(RPC);
457
+ const wallet = new ethers.Wallet(key, provider);
458
+ const factory = new ethers.ContractFactory(abi, bytecode, wallet);
459
+ const contract = await factory.deploy(...(args.args || []));
460
+ await contract.waitForDeployment();
461
+ const addr = await contract.getAddress();
462
+ const tx = contract.deploymentTransaction()?.hash;
463
+ return {
464
+ ok: true,
465
+ summary: `deployed to 0G Chain`,
466
+ content: `contract: ${addr}\ntx: ${tx}\nexplorer: https://chainscan.0g.ai/address/${addr}`,
467
+ };
468
+ } catch (e) {
469
+ return { ok: false, summary: "0g deploy failed", content: `ERROR: ${e.message}. Ensure ethers is installed, the wallet is funded, and contracts are compiled with evmVersion cancun.` };
470
+ }
471
+ }
472
+
473
+ function runBash(command, cwd) {
474
+ return new Promise((resolve) => {
475
+ exec(command, { cwd, timeout: 120_000, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
476
+ const code = err && typeof err.code === "number" ? err.code : err ? 1 : 0;
477
+ const text = [stdout && `stdout:\n${stdout}`, stderr && `stderr:\n${stderr}`].filter(Boolean).join("\n") || "(no output)";
478
+ resolve({ code, text: text.slice(0, MAX_READ) });
479
+ });
480
+ });
481
+ }