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/.env.example +41 -0
- package/LICENSE +21 -0
- package/README.md +152 -0
- package/bin/z0g.mjs +1052 -0
- package/contracts/Z0gSession.json +424 -0
- package/contracts/Z0gSession.sol +106 -0
- package/package.json +60 -0
- package/skills/0g/CHAIN.md +229 -0
- package/skills/0g/COMPUTE.md +296 -0
- package/skills/0g/NETWORK_CONFIG.md +163 -0
- package/skills/0g/SECURITY.md +174 -0
- package/skills/0g/STORAGE.md +167 -0
- package/skills/0g/TESTING.md +263 -0
- package/src/agent.mjs +434 -0
- package/src/anchor.mjs +65 -0
- package/src/checkpoints.mjs +64 -0
- package/src/client.mjs +134 -0
- package/src/commands.mjs +93 -0
- package/src/config.mjs +74 -0
- package/src/context.mjs +55 -0
- package/src/crypto.mjs +47 -0
- package/src/env.mjs +47 -0
- package/src/goal.mjs +45 -0
- package/src/inft.mjs +79 -0
- package/src/mcp-server.mjs +38 -0
- package/src/mcp.mjs +91 -0
- package/src/media.mjs +52 -0
- package/src/models-info.mjs +97 -0
- package/src/plan.mjs +21 -0
- package/src/prompt.mjs +149 -0
- package/src/provenance.mjs +46 -0
- package/src/sessions.mjs +168 -0
- package/src/settings.mjs +37 -0
- package/src/skills.mjs +43 -0
- package/src/tools.mjs +481 -0
- package/src/ui.mjs +798 -0
- package/src/user-skills.mjs +111 -0
- package/src/worktree.mjs +61 -0
package/src/client.mjs
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// Robust client for the 0G Compute Router.
|
|
2
|
+
// The Router is OpenAI-compatible but does NOT switch models on 503, so we add
|
|
3
|
+
// app-level multi-model fallback, retry/backoff, and empty-response handling.
|
|
4
|
+
import OpenAI from "openai";
|
|
5
|
+
import { CONFIG } from "./config.mjs";
|
|
6
|
+
|
|
7
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
8
|
+
|
|
9
|
+
export function makeClient() {
|
|
10
|
+
if (!CONFIG.apiKey) {
|
|
11
|
+
throw new Error("Missing ZOG_API_KEY. Get a 0G Router key at https://pc.0g.ai, then export it, put it in a project .env, or in ~/.z0gcode/.env to use it from anywhere.");
|
|
12
|
+
}
|
|
13
|
+
return new OpenAI({ baseURL: CONFIG.baseURL, apiKey: CONFIG.apiKey });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Try each model in order; retry a model on 503/429; move on to the next on hard error.
|
|
17
|
+
// Returns { message, model, usage, responseId, systemFingerprint }. Throws only if every model fails.
|
|
18
|
+
export async function complete(client, { models, messages, tools, effort }) {
|
|
19
|
+
let lastErr;
|
|
20
|
+
for (const model of models) {
|
|
21
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
22
|
+
try {
|
|
23
|
+
const body = {
|
|
24
|
+
model,
|
|
25
|
+
messages,
|
|
26
|
+
tools,
|
|
27
|
+
tool_choice: "auto",
|
|
28
|
+
max_tokens: CONFIG.maxTokens,
|
|
29
|
+
temperature: CONFIG.temperature,
|
|
30
|
+
};
|
|
31
|
+
if (effort) body.reasoning_effort = effort;
|
|
32
|
+
const res = await client.chat.completions.create(body);
|
|
33
|
+
const message = res.choices?.[0]?.message;
|
|
34
|
+
if (!message) throw new Error("empty response");
|
|
35
|
+
// A turn with neither content nor tool calls is unusable: treat as failure.
|
|
36
|
+
const hasContent = (message.content || "").trim().length > 0;
|
|
37
|
+
const hasTools = Array.isArray(message.tool_calls) && message.tool_calls.length > 0;
|
|
38
|
+
if (!hasContent && !hasTools) throw new Error("no content and no tool calls");
|
|
39
|
+
return {
|
|
40
|
+
message,
|
|
41
|
+
model,
|
|
42
|
+
usage: res.usage || null,
|
|
43
|
+
responseId: res.id || null,
|
|
44
|
+
systemFingerprint: res.system_fingerprint || null,
|
|
45
|
+
trace: res.x_0g_trace || null,
|
|
46
|
+
};
|
|
47
|
+
} catch (e) {
|
|
48
|
+
lastErr = e;
|
|
49
|
+
const status = e?.status;
|
|
50
|
+
if (status === 503) {
|
|
51
|
+
await sleep(400 * (attempt + 1));
|
|
52
|
+
continue; // retry same model: providers may recover
|
|
53
|
+
}
|
|
54
|
+
if (status === 429) {
|
|
55
|
+
await sleep(800 * (attempt + 1));
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
break; // 400/auth/other: this model won't work, try the next
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
throw lastErr || new Error("all models failed");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Streaming variant: calls onDelta(text) for content as it arrives and assembles
|
|
66
|
+
// tool calls from streamed fragments. Same fallback/retry as complete().
|
|
67
|
+
export async function completeStream(client, { models, messages, tools, onDelta, effort }) {
|
|
68
|
+
let lastErr;
|
|
69
|
+
for (const model of models) {
|
|
70
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
71
|
+
try {
|
|
72
|
+
const body = {
|
|
73
|
+
model,
|
|
74
|
+
messages,
|
|
75
|
+
tools,
|
|
76
|
+
tool_choice: "auto",
|
|
77
|
+
max_tokens: CONFIG.maxTokens,
|
|
78
|
+
temperature: CONFIG.temperature,
|
|
79
|
+
stream: true,
|
|
80
|
+
stream_options: { include_usage: true },
|
|
81
|
+
};
|
|
82
|
+
if (effort) body.reasoning_effort = effort;
|
|
83
|
+
const stream = await client.chat.completions.create(body);
|
|
84
|
+
let content = "";
|
|
85
|
+
const acc = [];
|
|
86
|
+
let usage = null;
|
|
87
|
+
let responseId = null;
|
|
88
|
+
let trace = null;
|
|
89
|
+
for await (const chunk of stream) {
|
|
90
|
+
if (chunk.id) responseId = responseId || chunk.id;
|
|
91
|
+
if (chunk.usage) usage = chunk.usage;
|
|
92
|
+
if (chunk.x_0g_trace) trace = chunk.x_0g_trace; // 0G TEE trace: provider node + request id
|
|
93
|
+
|
|
94
|
+
const delta = chunk.choices?.[0]?.delta;
|
|
95
|
+
if (!delta) continue;
|
|
96
|
+
if (delta.content) {
|
|
97
|
+
content += delta.content;
|
|
98
|
+
if (onDelta) onDelta(delta.content);
|
|
99
|
+
}
|
|
100
|
+
if (Array.isArray(delta.tool_calls)) {
|
|
101
|
+
for (const tcd of delta.tool_calls) {
|
|
102
|
+
const i = tcd.index ?? 0;
|
|
103
|
+
if (!acc[i]) acc[i] = { id: "", type: "function", function: { name: "", arguments: "" } };
|
|
104
|
+
if (tcd.id) acc[i].id = tcd.id;
|
|
105
|
+
if (tcd.function?.name) acc[i].function.name = tcd.function.name;
|
|
106
|
+
if (tcd.function?.arguments) acc[i].function.arguments += tcd.function.arguments;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const toolCalls = acc
|
|
111
|
+
.filter(Boolean)
|
|
112
|
+
.map((t, i) => ({ ...t, id: t.id || `call_${responseId || "s"}_${i}` }));
|
|
113
|
+
const hasContent = content.trim().length > 0;
|
|
114
|
+
if (!hasContent && toolCalls.length === 0) throw new Error("empty stream");
|
|
115
|
+
const message = { role: "assistant", content: content || null };
|
|
116
|
+
if (toolCalls.length) message.tool_calls = toolCalls;
|
|
117
|
+
return { message, model, usage, responseId, trace };
|
|
118
|
+
} catch (e) {
|
|
119
|
+
lastErr = e;
|
|
120
|
+
const status = e?.status;
|
|
121
|
+
if (status === 503) {
|
|
122
|
+
await sleep(400 * (attempt + 1));
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (status === 429) {
|
|
126
|
+
await sleep(800 * (attempt + 1));
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
throw lastErr || new Error("all models failed");
|
|
134
|
+
}
|
package/src/commands.mjs
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Custom slash commands and lifecycle hooks, both project-local under .z0g/.
|
|
2
|
+
//
|
|
3
|
+
// Commands: .z0g/commands/<name>.md -> /<name>. The file body is a prompt
|
|
4
|
+
// template; "$ARGUMENTS" (or {{args}}) is replaced with whatever follows the
|
|
5
|
+
// command, otherwise the args are appended. Optional frontmatter: description.
|
|
6
|
+
//
|
|
7
|
+
// Hooks: .z0g/hooks.json maps an event (preRun, postRun) to a shell command or
|
|
8
|
+
// a list of them. Hooks run shell, so they only fire with --auto.
|
|
9
|
+
import { readdirSync, readFileSync, existsSync } from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { exec } from "node:child_process";
|
|
12
|
+
import * as ui from "./ui.mjs";
|
|
13
|
+
|
|
14
|
+
const CMD_DIR = (cwd) => path.join(cwd, ".z0g", "commands");
|
|
15
|
+
|
|
16
|
+
// Parse optional --- frontmatter, returning { meta, body }.
|
|
17
|
+
function parseFrontmatter(text) {
|
|
18
|
+
const m = /^---\n([\s\S]*?)\n---\n?/.exec(text);
|
|
19
|
+
if (!m) return { meta: {}, body: text };
|
|
20
|
+
const meta = {};
|
|
21
|
+
for (const line of m[1].split("\n")) {
|
|
22
|
+
const kv = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line.trim());
|
|
23
|
+
if (kv) meta[kv[1].toLowerCase()] = kv[2].trim();
|
|
24
|
+
}
|
|
25
|
+
return { meta, body: text.slice(m[0].length) };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function loadCustomCommands(cwd) {
|
|
29
|
+
const dir = CMD_DIR(cwd);
|
|
30
|
+
if (!existsSync(dir)) return [];
|
|
31
|
+
const out = [];
|
|
32
|
+
let files = [];
|
|
33
|
+
try { files = readdirSync(dir).filter((f) => f.endsWith(".md")); } catch { return []; }
|
|
34
|
+
for (const f of files) {
|
|
35
|
+
try {
|
|
36
|
+
const { meta, body } = parseFrontmatter(readFileSync(path.join(dir, f), "utf8"));
|
|
37
|
+
const name = f.replace(/\.md$/, "").toLowerCase();
|
|
38
|
+
if (!/^[a-z0-9][a-z0-9_-]*$/.test(name)) continue;
|
|
39
|
+
out.push({ name, description: meta.description || "custom command", template: body.trim(), file: path.join(dir, f) });
|
|
40
|
+
} catch { /* skip unreadable */ }
|
|
41
|
+
}
|
|
42
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function expandTemplate(template, args) {
|
|
46
|
+
const a = (args || "").trim();
|
|
47
|
+
if (/\$ARGUMENTS|\{\{\s*args\s*\}\}/.test(template)) {
|
|
48
|
+
return template.replace(/\$ARGUMENTS|\{\{\s*args\s*\}\}/g, a);
|
|
49
|
+
}
|
|
50
|
+
return a ? template + "\n\n" + a : template;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---- hooks ----------------------------------------------------------------
|
|
54
|
+
const HOOK_EVENTS = ["preRun", "postRun"];
|
|
55
|
+
|
|
56
|
+
export function loadHooks(cwd) {
|
|
57
|
+
const file = path.join(cwd, ".z0g", "hooks.json");
|
|
58
|
+
if (!existsSync(file)) return {};
|
|
59
|
+
let raw;
|
|
60
|
+
try { raw = JSON.parse(readFileSync(file, "utf8")); } catch { return {}; }
|
|
61
|
+
const hooks = {};
|
|
62
|
+
for (const ev of HOOK_EVENTS) {
|
|
63
|
+
const v = raw[ev];
|
|
64
|
+
if (!v) continue;
|
|
65
|
+
hooks[ev] = (Array.isArray(v) ? v : [v]).map(String).filter(Boolean);
|
|
66
|
+
}
|
|
67
|
+
return hooks;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function hasHooks(hooks) {
|
|
71
|
+
return HOOK_EVENTS.some((e) => hooks[e] && hooks[e].length);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const run1 = (cmd, cwd) => new Promise((resolve) => {
|
|
75
|
+
exec(cmd, { cwd, timeout: 120000, maxBuffer: 1 << 20 }, (err, stdout, stderr) => {
|
|
76
|
+
resolve({ code: err?.code ?? 0, out: (stdout || "") + (stderr || "") });
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Run the shell commands for an event. Only fires with allowBash (--auto).
|
|
81
|
+
export async function runHooks(cwd, event, hooks, allowBash, taskText) {
|
|
82
|
+
const cmds = hooks?.[event];
|
|
83
|
+
if (!cmds || !cmds.length) return;
|
|
84
|
+
if (!allowBash) return; // hooks run shell; require --auto
|
|
85
|
+
for (const cmd of cmds) {
|
|
86
|
+
const expanded = cmd.replace(/\$TASK/g, () => (taskText || "").replace(/"/g, '\\"'));
|
|
87
|
+
console.log(ui.muted(" " + ui.GLYPH.point + " hook " + event + ": ") + ui.muted(cmd));
|
|
88
|
+
const { code, out } = await run1(expanded, cwd);
|
|
89
|
+
const text = out.trim();
|
|
90
|
+
if (text) console.log(text.split("\n").map((l) => " " + ui.muted(l)).join("\n"));
|
|
91
|
+
if (code !== 0) console.log(" " + ui.warn("hook exited " + code));
|
|
92
|
+
}
|
|
93
|
+
}
|
package/src/config.mjs
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// z0gcode configuration. 0G is baked in as the default backend.
|
|
2
|
+
// Everything can be overridden by env vars, but nothing needs to be.
|
|
3
|
+
import { loadSettings } from "./settings.mjs";
|
|
4
|
+
|
|
5
|
+
function envList(name, fallback) {
|
|
6
|
+
const v = process.env[name];
|
|
7
|
+
if (!v) return fallback;
|
|
8
|
+
return v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// User's saved preferences (e.g. a model picked with /model). Env still wins.
|
|
12
|
+
const settings = loadSettings(process.cwd());
|
|
13
|
+
|
|
14
|
+
// Reasoning effort: low | medium | high, or null (unset -> model default).
|
|
15
|
+
export const EFFORT_LEVELS = ["low", "medium", "high"];
|
|
16
|
+
export function normEffort(v) {
|
|
17
|
+
const s = String(v || "").toLowerCase().trim();
|
|
18
|
+
return EFFORT_LEVELS.includes(s) ? s : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Parse a boolean-ish value: booleans pass through; off/false/0/no -> false,
|
|
22
|
+
// anything else truthy -> true; undefined/null -> undefined (not set).
|
|
23
|
+
export function boolOf(v) {
|
|
24
|
+
if (v === undefined || v === null || v === "") return undefined;
|
|
25
|
+
if (typeof v === "boolean") return v;
|
|
26
|
+
return !/^(off|false|0|no)$/i.test(String(v).trim());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const CONFIG = {
|
|
30
|
+
// 0G Compute Router (OpenAI-compatible). Mainnet by default.
|
|
31
|
+
baseURL: process.env.ZOG_BASE_URL || "https://router-api.0g.ai/v1",
|
|
32
|
+
apiKey: process.env.ZOG_API_KEY || "",
|
|
33
|
+
|
|
34
|
+
// Default model: 0G's own in-house coding model (private + verifiable, TEE).
|
|
35
|
+
// Precedence: ZOG_MODEL env > saved setting (/model) > built-in default.
|
|
36
|
+
model: process.env.ZOG_MODEL || settings.model || "0gm-1.0-35b-a3b",
|
|
37
|
+
// App-level fallbacks: the Router does NOT switch models on 503, so we do.
|
|
38
|
+
fallbacks: envList("ZOG_FALLBACKS", ["deepseek-v4-pro", "glm-5.2", "kimi-k2.7-code"]),
|
|
39
|
+
|
|
40
|
+
maxSteps: Number(process.env.ZOG_MAX_STEPS || 30),
|
|
41
|
+
maxTokens: Number(process.env.ZOG_MAX_TOKENS || 16384),
|
|
42
|
+
temperature: Number(process.env.ZOG_TEMPERATURE || 0.2),
|
|
43
|
+
|
|
44
|
+
// Reasoning effort passed to the Router as reasoning_effort. Null = unset.
|
|
45
|
+
effort: normEffort(process.env.ZOG_EFFORT || settings.effort),
|
|
46
|
+
|
|
47
|
+
// Max subagents running at once (spawn_subagents fan-out).
|
|
48
|
+
maxParallel: Math.max(1, Number(process.env.ZOG_MAX_PARALLEL || 4)),
|
|
49
|
+
|
|
50
|
+
// Whether the spawn_subagents tool is offered. env > settings > on.
|
|
51
|
+
subagents: (() => {
|
|
52
|
+
const e = boolOf(process.env.ZOG_SUBAGENTS);
|
|
53
|
+
if (e !== undefined) return e;
|
|
54
|
+
return typeof settings.subagents === "boolean" ? settings.subagents : true;
|
|
55
|
+
})(),
|
|
56
|
+
|
|
57
|
+
// Media models on the 0G Router (image generation, transcription).
|
|
58
|
+
imageModel: process.env.ZOG_IMAGE_MODEL || "z-image-turbo",
|
|
59
|
+
transcribeModel: process.env.ZOG_TRANSCRIBE_MODEL || "whisper-large-v3",
|
|
60
|
+
|
|
61
|
+
// On-chain actions (0G Storage upload, 0G Chain deploy, session anchor) spend
|
|
62
|
+
// gas, so they are OFF by default. env > settings > off.
|
|
63
|
+
onchain: (() => {
|
|
64
|
+
const e = boolOf(process.env.ZOG_ONCHAIN);
|
|
65
|
+
if (e !== undefined) return e;
|
|
66
|
+
return typeof settings.onchain === "boolean" ? settings.onchain : false;
|
|
67
|
+
})(),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export function modelChain(preferred) {
|
|
71
|
+
const primary = preferred || CONFIG.model;
|
|
72
|
+
const chain = [primary, ...CONFIG.fallbacks.filter((m) => m !== primary)];
|
|
73
|
+
return chain;
|
|
74
|
+
}
|
package/src/context.mjs
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Project context: auto-loaded guidance the agent should always follow.
|
|
2
|
+
// Looks for AGENTS.md (the emerging standard) and .z0g/context.md in the
|
|
3
|
+
// working directory, and injects them into the system prompt.
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
const CANDIDATES = ["AGENTS.md", ".z0g/context.md"];
|
|
8
|
+
const MAX_CONTEXT = 8000; // keep the injected block small
|
|
9
|
+
|
|
10
|
+
// Return [{ source, text }] for each context file found (may be empty).
|
|
11
|
+
export function loadProjectContext(cwd) {
|
|
12
|
+
const found = [];
|
|
13
|
+
let budget = MAX_CONTEXT;
|
|
14
|
+
for (const rel of CANDIDATES) {
|
|
15
|
+
if (budget <= 0) break;
|
|
16
|
+
try {
|
|
17
|
+
let text = readFileSync(path.join(cwd, rel), "utf8").trim();
|
|
18
|
+
if (!text) continue;
|
|
19
|
+
if (text.length > budget) text = text.slice(0, budget) + "\n… [truncated]";
|
|
20
|
+
budget -= text.length;
|
|
21
|
+
found.push({ source: rel, text });
|
|
22
|
+
} catch { /* not present */ }
|
|
23
|
+
}
|
|
24
|
+
return found;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// A system-prompt block describing the project context, or "" if none.
|
|
28
|
+
export function contextPromptBlock(cwd) {
|
|
29
|
+
const found = loadProjectContext(cwd);
|
|
30
|
+
if (!found.length) return "";
|
|
31
|
+
const parts = found.map((f) => "----- " + f.source + " -----\n" + f.text);
|
|
32
|
+
return [
|
|
33
|
+
"## Project context",
|
|
34
|
+
"This project ships contributor guidance below. Treat it as authoritative:",
|
|
35
|
+
"follow its conventions, use its build/test/run commands, and respect its constraints.",
|
|
36
|
+
"",
|
|
37
|
+
parts.join("\n\n"),
|
|
38
|
+
].join("\n");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The task the agent runs for `z0g init` / `/init` to author an AGENTS.md.
|
|
42
|
+
export const INIT_TASK = [
|
|
43
|
+
"Analyze THIS project and write a concise AGENTS.md at the repository root, so future agents and contributors have the context they need.",
|
|
44
|
+
"",
|
|
45
|
+
"Investigate first (do not guess): use list_dir on the root and key folders, read package.json / pyproject.toml / go.mod / Cargo.toml / any config, read the README if present, and search_files for scripts, test setup, and entry points.",
|
|
46
|
+
"",
|
|
47
|
+
"AGENTS.md MUST contain, as short markdown sections:",
|
|
48
|
+
"1. Overview: one paragraph on what the project is and does.",
|
|
49
|
+
"2. Stack: languages, frameworks, key dependencies.",
|
|
50
|
+
"3. Commands: exact install, build, run, and test commands you actually found (do NOT invent them; if a command is missing, say so).",
|
|
51
|
+
"4. Layout: the important directories and what lives in each.",
|
|
52
|
+
"5. Conventions: anything a contributor must follow (style, patterns, gotchas) that you can infer from the code.",
|
|
53
|
+
"",
|
|
54
|
+
"Keep it under ~150 lines and specific to this repo. Create it with write_file at path 'AGENTS.md'. When done, reply with a one-line summary.",
|
|
55
|
+
].join("\n");
|
package/src/crypto.mjs
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Client-side encryption for session bundles before they go to 0G Storage.
|
|
2
|
+
// 0G Storage is public, so we encrypt with a key derived from the user's wallet
|
|
3
|
+
// private key: the content root can be public (anchored on-chain, in the INFT)
|
|
4
|
+
// while only that wallet can decrypt. AES-256-GCM (authenticated), no deps.
|
|
5
|
+
import crypto from "node:crypto";
|
|
6
|
+
|
|
7
|
+
const INFO = Buffer.from("z0gcode-session-v1");
|
|
8
|
+
|
|
9
|
+
function deriveKey(privKeyHex, salt) {
|
|
10
|
+
const pk = Buffer.from(String(privKeyHex).replace(/^0x/, ""), "hex");
|
|
11
|
+
return Buffer.from(crypto.hkdfSync("sha256", pk, salt, INFO, 32));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Encrypt a Buffer, returning a self-describing JSON envelope Buffer.
|
|
15
|
+
export function encryptEnvelope(plaintext, privKeyHex) {
|
|
16
|
+
const salt = crypto.randomBytes(16);
|
|
17
|
+
const iv = crypto.randomBytes(12);
|
|
18
|
+
const key = deriveKey(privKeyHex, salt);
|
|
19
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
|
20
|
+
const ct = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
21
|
+
const tag = cipher.getAuthTag();
|
|
22
|
+
return Buffer.from(JSON.stringify({
|
|
23
|
+
tool: "z0gcode", enc: "AES-256-GCM", kdf: "hkdf-sha256(wallet)", v: 1,
|
|
24
|
+
salt: salt.toString("base64"), iv: iv.toString("base64"),
|
|
25
|
+
tag: tag.toString("base64"), ct: ct.toString("base64"),
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Decrypt an envelope Buffer with the wallet key. Throws if the key is wrong
|
|
30
|
+
// (a different wallet) or the data was tampered with.
|
|
31
|
+
export function decryptEnvelope(envelopeBuf, privKeyHex) {
|
|
32
|
+
let e;
|
|
33
|
+
try { e = JSON.parse(envelopeBuf.toString("utf8")); } catch { throw new Error("not an encrypted z0gcode bundle"); }
|
|
34
|
+
if (!e || e.enc !== "AES-256-GCM" || !e.salt || !e.iv || !e.tag || !e.ct) throw new Error("not an encrypted z0gcode bundle");
|
|
35
|
+
const key = deriveKey(privKeyHex, Buffer.from(e.salt, "base64"));
|
|
36
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", key, Buffer.from(e.iv, "base64"));
|
|
37
|
+
decipher.setAuthTag(Buffer.from(e.tag, "base64"));
|
|
38
|
+
try {
|
|
39
|
+
return Buffer.concat([decipher.update(Buffer.from(e.ct, "base64")), decipher.final()]);
|
|
40
|
+
} catch {
|
|
41
|
+
throw new Error("decryption failed: this session was encrypted for a different wallet");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function isEncryptedEnvelope(buf) {
|
|
46
|
+
try { const e = JSON.parse(buf.toString("utf8")); return e && e.enc === "AES-256-GCM"; } catch { return false; }
|
|
47
|
+
}
|
package/src/env.mjs
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Load .env into process.env BEFORE anything reads config. Imported first in
|
|
2
|
+
// bin/z0g.mjs so config.mjs sees the vars. Dependency-free; never overrides a
|
|
3
|
+
// variable already set in the real environment.
|
|
4
|
+
//
|
|
5
|
+
// Lookup order (first found wins per variable, real env always wins):
|
|
6
|
+
// 1. the nearest .env walking up from the current directory (project-local),
|
|
7
|
+
// so `z0g` works from any subfolder of a project, not just its root.
|
|
8
|
+
// 2. ~/.z0gcode/.env (global), so a key set once works from anywhere.
|
|
9
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
|
|
13
|
+
function loadEnvFile(file) {
|
|
14
|
+
if (!file || !existsSync(file)) return;
|
|
15
|
+
try {
|
|
16
|
+
for (const raw of readFileSync(file, "utf8").split("\n")) {
|
|
17
|
+
const line = raw.trim();
|
|
18
|
+
if (!line || line.startsWith("#")) continue;
|
|
19
|
+
const eq = line.indexOf("=");
|
|
20
|
+
if (eq === -1) continue;
|
|
21
|
+
const keyPart = line.slice(0, eq).replace(/^export\s+/, "").trim();
|
|
22
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(keyPart)) continue;
|
|
23
|
+
let val = line.slice(eq + 1).trim();
|
|
24
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
25
|
+
val = val.slice(1, -1);
|
|
26
|
+
}
|
|
27
|
+
if (process.env[keyPart] === undefined) process.env[keyPart] = val;
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
// ignore a malformed .env
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Nearest .env walking up from `dir` to the filesystem root.
|
|
35
|
+
function nearestEnv(dir) {
|
|
36
|
+
let cur = path.resolve(dir);
|
|
37
|
+
for (;;) {
|
|
38
|
+
const f = path.join(cur, ".env");
|
|
39
|
+
if (existsSync(f)) return f;
|
|
40
|
+
const parent = path.dirname(cur);
|
|
41
|
+
if (parent === cur) return null;
|
|
42
|
+
cur = parent;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
loadEnvFile(nearestEnv(process.cwd())); // project-local (any ancestor)
|
|
47
|
+
loadEnvFile(path.join(os.homedir(), ".z0gcode", ".env")); // global fallback
|
package/src/goal.mjs
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Goal loop: run an objective, then a verify command; on failure feed the output
|
|
2
|
+
// back and re-run, until it passes or the iteration budget is spent.
|
|
3
|
+
import { exec } from "node:child_process";
|
|
4
|
+
import { runAgent } from "./agent.mjs";
|
|
5
|
+
import { saveMessages } from "./sessions.mjs";
|
|
6
|
+
import * as ui from "./ui.mjs";
|
|
7
|
+
|
|
8
|
+
function runCmd(cmd, cwd) {
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
exec(cmd, { cwd, timeout: 300_000, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
11
|
+
const code = err && typeof err.code === "number" ? err.code : err ? 1 : 0;
|
|
12
|
+
resolve({ code, out: `${stdout || ""}${stderr || ""}` });
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function runGoal({ client, objective, cwd, sessionId, sessionDir, allowBash, preferredModel, preferredEffort, preferredSubagents, preferredOnchain, verifyCmd, maxIters = 3, history: historyParam = null }) {
|
|
18
|
+
let history = historyParam ?? null;
|
|
19
|
+
let task = objective;
|
|
20
|
+
for (let iter = 1; iter <= maxIters; iter++) {
|
|
21
|
+
console.log(ui.section("Goal", "iteration " + iter + "/" + maxIters));
|
|
22
|
+
const res = await runAgent({ client, task, cwd, sessionDir, allowBash, preferredModel, preferredEffort, preferredSubagents, preferredOnchain, history });
|
|
23
|
+
history = res.messages;
|
|
24
|
+
if (sessionId && Array.isArray(history)) {
|
|
25
|
+
try { await saveMessages(cwd, sessionId, history); } catch {}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!verifyCmd) {
|
|
29
|
+
ui.info(" no verify command configured; done after one pass.");
|
|
30
|
+
return { ok: !!res.ok, iters: iter };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log(" " + ui.muted("verify: " + verifyCmd));
|
|
34
|
+
const v = await runCmd(verifyCmd, cwd);
|
|
35
|
+
if (v.code === 0) {
|
|
36
|
+
console.log(" " + ui.ok(ui.GLYPH.ok + " passed on iteration " + iter));
|
|
37
|
+
console.log(" " + ui.ok(ui.GLYPH.ok) + " " + ui.strong("Goal met in " + iter + " iteration" + (iter > 1 ? "s" : "") + "."));
|
|
38
|
+
return { ok: true, iters: iter };
|
|
39
|
+
}
|
|
40
|
+
console.log(" " + ui.warn(ui.GLYPH.no + " failed (exit " + v.code + ") · feeding output back"));
|
|
41
|
+
task = `The verification command \`${verifyCmd}\` failed with output:\n\n${v.out.slice(0, 4000)}\n\nFix the code so this command passes, then stop.`;
|
|
42
|
+
}
|
|
43
|
+
console.log(" " + ui.err(ui.GLYPH.no) + " " + ui.strong("Goal not met after " + maxIters + " iterations."));
|
|
44
|
+
return { ok: false, iters: maxIters };
|
|
45
|
+
}
|
package/src/inft.mjs
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Mint a verifiable z0gcode session as an NFT on 0G Chain. Each token records
|
|
2
|
+
// the 0G Storage content root of the session bundle, so an AI work session
|
|
3
|
+
// becomes an ownable, provable asset. ERC-721 based, ERC-7857-inspired.
|
|
4
|
+
// Needs a funded ZOG_WALLET_KEY and on-chain enabled.
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
const rpc = () => process.env.ZOG_EVM_RPC || "https://evmrpc.0g.ai";
|
|
9
|
+
|
|
10
|
+
function requireKey() {
|
|
11
|
+
const key = process.env.ZOG_WALLET_KEY;
|
|
12
|
+
if (!key) throw new Error("Set ZOG_WALLET_KEY to a funded 0G mainnet private key.");
|
|
13
|
+
return key;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function artifact() {
|
|
17
|
+
const p = new URL("../contracts/Z0gSession.json", import.meta.url);
|
|
18
|
+
return JSON.parse(readFileSync(p, "utf8"));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const registryPath = (cwd) => path.join(cwd, ".z0g", "inft.json");
|
|
22
|
+
|
|
23
|
+
function loadRegistry(cwd) {
|
|
24
|
+
try { return JSON.parse(readFileSync(registryPath(cwd), "utf8")); } catch { return {}; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Return the deployed contract address for this chain, deploying it once and
|
|
28
|
+
// caching it in .z0g/inft.json if needed. Returns { address, chainId, deployed }.
|
|
29
|
+
export async function deployOrLoad(cwd) {
|
|
30
|
+
const key = requireKey();
|
|
31
|
+
const { ethers } = await import("ethers");
|
|
32
|
+
const provider = new ethers.JsonRpcProvider(rpc());
|
|
33
|
+
const wallet = new ethers.Wallet(key, provider);
|
|
34
|
+
const chainId = Number((await provider.getNetwork()).chainId);
|
|
35
|
+
|
|
36
|
+
const reg = loadRegistry(cwd);
|
|
37
|
+
if (reg.address && reg.chainId === chainId) {
|
|
38
|
+
// Confirm code is present at the cached address; redeploy if not.
|
|
39
|
+
const code = await provider.getCode(reg.address);
|
|
40
|
+
if (code && code !== "0x") return { address: reg.address, chainId, deployed: false };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const art = artifact();
|
|
44
|
+
const factory = new ethers.ContractFactory(art.abi, art.bytecode, wallet);
|
|
45
|
+
const contract = await factory.deploy();
|
|
46
|
+
await contract.waitForDeployment();
|
|
47
|
+
const address = await contract.getAddress();
|
|
48
|
+
const deployTx = contract.deploymentTransaction()?.hash || null;
|
|
49
|
+
writeFileSync(registryPath(cwd), JSON.stringify({ address, chainId, deployTx, contract: art.contractName }, null, 2) + "\n");
|
|
50
|
+
return { address, chainId, deployed: true, deployTx };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Mint a session token. Returns { contract, tokenId, txHash, block, deployed, deployTx }.
|
|
54
|
+
export async function mintSession(cwd, { root, uri, to }) {
|
|
55
|
+
const key = requireKey();
|
|
56
|
+
const { ethers } = await import("ethers");
|
|
57
|
+
const { address, deployed, deployTx } = await deployOrLoad(cwd);
|
|
58
|
+
const provider = new ethers.JsonRpcProvider(rpc());
|
|
59
|
+
const wallet = new ethers.Wallet(key, provider);
|
|
60
|
+
const art = artifact();
|
|
61
|
+
const c = new ethers.Contract(address, art.abi, wallet);
|
|
62
|
+
const owner = to || wallet.address;
|
|
63
|
+
const tx = await c.mint(owner, root, uri || "");
|
|
64
|
+
const receipt = await tx.wait();
|
|
65
|
+
|
|
66
|
+
let tokenId = null;
|
|
67
|
+
const iface = new ethers.Interface(art.abi);
|
|
68
|
+
for (const log of receipt.logs || []) {
|
|
69
|
+
try {
|
|
70
|
+
const parsed = iface.parseLog(log);
|
|
71
|
+
if (parsed && parsed.name === "Minted") { tokenId = parsed.args.tokenId.toString(); break; }
|
|
72
|
+
} catch { /* not our event */ }
|
|
73
|
+
}
|
|
74
|
+
return { contract: address, tokenId, txHash: tx.hash, block: receipt?.blockNumber, owner, deployed, deployTx };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function inftRegistry(cwd) {
|
|
78
|
+
return existsSync(registryPath(cwd)) ? loadRegistry(cwd) : null;
|
|
79
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// MCP server mode: expose z0gcode's 0G tools so other agents (Claude Code, Cursor,
|
|
2
|
+
// another z0gcode) can use them. Stdio transport, so nothing may be written to
|
|
3
|
+
// stdout except the MCP protocol; logs go to stderr.
|
|
4
|
+
import { TOOL_DEFS, makeExecutor } from "./tools.mjs";
|
|
5
|
+
|
|
6
|
+
// The 0G-native tools worth exposing to other agents.
|
|
7
|
+
const EXPOSE = new Set(["read_skill", "upload_0g_storage"]);
|
|
8
|
+
|
|
9
|
+
export async function startMcpServer({ cwd, allowBash }) {
|
|
10
|
+
const { Server } = await import("@modelcontextprotocol/sdk/server/index.js");
|
|
11
|
+
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
12
|
+
const { ListToolsRequestSchema, CallToolRequestSchema } = await import("@modelcontextprotocol/sdk/types.js");
|
|
13
|
+
|
|
14
|
+
const exposed = TOOL_DEFS.filter((t) => EXPOSE.has(t.function.name));
|
|
15
|
+
const execute = makeExecutor({ cwd, allowBash });
|
|
16
|
+
|
|
17
|
+
const server = new Server({ name: "z0gcode", version: "0.2.0" }, { capabilities: { tools: {} } });
|
|
18
|
+
|
|
19
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
20
|
+
tools: exposed.map((t) => ({
|
|
21
|
+
name: t.function.name,
|
|
22
|
+
description: t.function.description,
|
|
23
|
+
inputSchema: t.function.parameters,
|
|
24
|
+
})),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
28
|
+
const { name, arguments: args } = req.params;
|
|
29
|
+
if (!EXPOSE.has(name)) {
|
|
30
|
+
return { content: [{ type: "text", text: `tool ${name} is not exposed` }], isError: true };
|
|
31
|
+
}
|
|
32
|
+
const res = await execute(name, args || {});
|
|
33
|
+
return { content: [{ type: "text", text: String(res.content ?? (res.ok ? "OK" : "ERROR")) }], isError: !res.ok };
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
await server.connect(new StdioServerTransport());
|
|
37
|
+
process.stderr.write(`z0gcode MCP server ready (tools: ${exposed.map((t) => t.function.name).join(", ")})\n`);
|
|
38
|
+
}
|