@kroxy/kroxy 1.0.16 → 1.0.18

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/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { hireParams, executeHire } from './src/tools/hire.js';
2
3
  import { offerParams, executeOffer } from './src/tools/offer.js';
3
4
  import { reputationParams, executeReputation } from './src/tools/reputation.js';
@@ -8,6 +9,11 @@ import { browseParams, executeBrowse } from './src/tools/browse.js';
8
9
  import { disputeParams, executeDispute } from './src/tools/dispute.js';
9
10
  import { historyParams, executeHistory } from './src/tools/history.js';
10
11
  import { autoagentParams, executeAutoagent } from './src/tools/autoagent.js';
12
+ const DEFAULT_API_URL = 'https://api-production-1b45.up.railway.app';
13
+ function demoDerivedAddress() {
14
+ const hash = createHash('sha256').update('kroxy-demo-wallet').digest('hex');
15
+ return `0x${hash.slice(0, 40)}`;
16
+ }
11
17
  export default {
12
18
  id: 'kroxy',
13
19
  name: 'Kroxy',
@@ -25,6 +31,13 @@ export default {
25
31
  process.env.KROXY_DEMO_MODE = cfg.KROXY_DEMO_MODE;
26
32
  if (cfg.NEXUS_URL)
27
33
  process.env.NEXUS_URL = cfg.NEXUS_URL;
34
+ if (!process.env.KROXY_API_URL)
35
+ process.env.KROXY_API_URL = DEFAULT_API_URL;
36
+ if (!process.env.KROXY_DEMO_MODE && !process.env.KROXY_AGENT_PRIVATE_KEY)
37
+ process.env.KROXY_DEMO_MODE = '1';
38
+ if (process.env.KROXY_DEMO_MODE === '1' && !process.env.KROXY_AGENT_WALLET) {
39
+ process.env.KROXY_AGENT_WALLET = demoDerivedAddress();
40
+ }
28
41
  // ── Setup / Onboarding ────────────────────────────────────────────────────
29
42
  // Run this first to check configuration and get guided setup instructions.
30
43
  api.registerTool({
@@ -139,4 +152,4 @@ export default {
139
152
  }, { optional: true });
140
153
  },
141
154
  };
142
- //# sourceMappingURL=index.js.map
155
+ //# sourceMappingURL=index.js.map
@@ -4,8 +4,9 @@
4
4
  * so runtime-injected env vars are always used.
5
5
  */
6
6
  import { fetchWithRetry } from './utils/fetchWithRetry.js';
7
+ const DEFAULT_API_URL = 'https://api-production-1b45.up.railway.app';
7
8
  function apiBase() {
8
- return process.env.KROXY_API_URL ?? 'http://localhost:3001';
9
+ return process.env.KROXY_API_URL ?? DEFAULT_API_URL;
9
10
  }
10
11
  function authHeaders() {
11
12
  const key = process.env.KROXY_API_KEY ?? '';
@@ -149,4 +150,4 @@ export async function pingApi() {
149
150
  return false;
150
151
  }
151
152
  }
152
- //# sourceMappingURL=client.js.map
153
+ //# sourceMappingURL=client.js.map
@@ -2,8 +2,9 @@
2
2
  * kroxy_autoagent — Autonomous multi-step orchestrator.
3
3
  *
4
4
  * Takes a natural-language goal, decomposes it into 2–4 subtasks via Claude
5
- * (or a rule-based fallback if ANTHROPIC_API_KEY is unset), fires kroxy_hire
6
- * for each subtask sequentially, and returns a unified deliverable.
5
+ * through Lava's gateway (or a rule-based fallback if keys are unset), fires
6
+ * kroxy_hire for all subtasks in parallel, then synthesizes outputs into a
7
+ * unified deliverable via Lava.
7
8
  */
8
9
  import { Type } from '@sinclair/typebox';
9
10
  import { executeHire } from './hire.js';
@@ -20,8 +21,9 @@ export const autoagentParams = Type.Object({
20
21
  });
21
22
  // ─── Decomposition ────────────────────────────────────────────────────────────
22
23
  /**
23
- * Ask Claude to decompose the goal into 2–4 subtasks.
24
- * Falls back to rule-based decomposer if ANTHROPIC_API_KEY is absent.
24
+ * Ask Claude (via Lava) to decompose the goal into 2–4 subtasks.
25
+ * Falls back to keyword-based decomposer if LAVA_SECRET_KEY or
26
+ * ANTHROPIC_API_KEY are not set.
25
27
  */
26
28
  async function decomposeGoal(goal) {
27
29
  const hasLava = process.env.LAVA_SECRET_KEY && process.env.ANTHROPIC_API_KEY;
@@ -39,13 +41,13 @@ async function decomposeGoal(goal) {
39
41
  });
40
42
  const parsed = JSON.parse(text.trim());
41
43
  if (Array.isArray(parsed) && parsed.length > 0)
42
- return parsed.slice(0, 4);
44
+ return { subtasks: parsed.slice(0, 4), viaLava: true };
43
45
  }
44
46
  catch {
45
47
  // Fall through to rule-based
46
48
  }
47
49
  }
48
- return ruleBasedDecompose(goal);
50
+ return { subtasks: ruleBasedDecompose(goal), viaLava: false };
49
51
  }
50
52
  /** Keyword-based decomposer — works without any API key. */
51
53
  function ruleBasedDecompose(goal) {
@@ -70,30 +72,73 @@ function ruleBasedDecompose(goal) {
70
72
  }
71
73
  return subtasks.slice(0, 4);
72
74
  }
75
+ // ─── Synthesis ────────────────────────────────────────────────────────────────
76
+ /**
77
+ * Use Lava/Claude to merge individual subtask deliverables into a single
78
+ * coherent summary. Best-effort — silently skipped if Lava is unavailable.
79
+ */
80
+ async function synthesizeDeliverables(goal, receipts) {
81
+ const hasLava = process.env.LAVA_SECRET_KEY && process.env.ANTHROPIC_API_KEY;
82
+ if (!hasLava || receipts.length === 0)
83
+ return null;
84
+ const snippets = receipts
85
+ .filter(d => d?.deliverable?.summary)
86
+ .map((d, i) => `[Subtask ${i + 1}]\n${d.deliverable.summary}`)
87
+ .join('\n\n');
88
+ if (!snippets)
89
+ return null;
90
+ try {
91
+ return await lavaAnthropic({
92
+ model: 'claude-haiku-4-5-20251001',
93
+ max_tokens: 512,
94
+ system: 'You are a synthesis assistant. Given outputs from multiple AI agent subtasks, ' +
95
+ 'write a concise unified summary (3–5 sentences) that integrates all key findings ' +
96
+ 'into a coherent deliverable for the original goal.',
97
+ messages: [{ role: 'user', content: `Original goal: ${goal}\n\nSubtask outputs:\n${snippets}` }],
98
+ });
99
+ }
100
+ catch {
101
+ return null;
102
+ }
103
+ }
73
104
  // ─── Orchestrator ─────────────────────────────────────────────────────────────
74
105
  export async function executeAutoagent(params) {
75
106
  const goal = params.goal;
76
107
  const maxBudget = params.maxBudget ?? 20;
77
108
  // 1. Decompose
78
- const subtasks = await decomposeGoal(goal);
109
+ const { subtasks, viaLava } = await decomposeGoal(goal);
79
110
  const pricePerTask = parseFloat((maxBudget / subtasks.length).toFixed(2));
111
+ const modeLabel = viaLava
112
+ ? 'LLM decomposition via Lava'
113
+ : '⚠ Keyword fallback — set LAVA_SECRET_KEY + ANTHROPIC_API_KEY for intelligent decomposition';
80
114
  const lines = [
81
115
  `AutoAgent: ${goal.slice(0, 80)}`,
82
116
  `${'─'.repeat(50)}`,
83
117
  `Budget: $${maxBudget} USDC across ${subtasks.length} subtask${subtasks.length === 1 ? '' : 's'} ($${pricePerTask} each)`,
118
+ `Mode: ${modeLabel}`,
84
119
  '',
85
120
  ];
121
+ // 2. List all subtasks upfront before firing
122
+ subtasks.forEach(({ task, capability }, i) => {
123
+ lines.push(`[${i + 1}/${subtasks.length}] ${capability.toUpperCase()}: ${task.slice(0, 70)}`);
124
+ });
125
+ lines.push('', `Firing all ${subtasks.length} subtask${subtasks.length === 1 ? '' : 's'} in parallel…`, '');
126
+ // 3. Fire all hires concurrently
127
+ const settled = await Promise.allSettled(
128
+ subtasks.map(({ task, capability }) =>
129
+ executeHire({ task, maxPrice: pricePerTask, capability })
130
+ )
131
+ );
86
132
  const receipts = [];
87
133
  let totalSpent = 0;
88
- // 2. Fire kroxy_hire for each subtask sequentially
89
- for (let i = 0; i < subtasks.length; i++) {
134
+ for (let i = 0; i < settled.length; i++) {
135
+ const result = settled[i];
90
136
  const { task, capability } = subtasks[i];
91
- lines.push(`[${i + 1}/${subtasks.length}] ${capability.toUpperCase()}: ${task.slice(0, 70)}`);
92
- try {
93
- const result = await executeHire({ task, maxPrice: pricePerTask, capability });
94
- const d = result.details;
95
- const paid = parseFloat(d?.amountPaid ?? '0');
96
- totalSpent += paid;
137
+ lines.push(`[${i + 1}] ${capability.toUpperCase()}: ${task.slice(0, 60)}`);
138
+ if (result.status === 'fulfilled') {
139
+ const d = result.value.details;
140
+ const paid = parseFloat(String(d?.amountPaid ?? '0'));
141
+ totalSpent += isNaN(paid) ? 0 : paid;
97
142
  lines.push(` ✓ Completed in ${d?.duration ?? '?'} — paid $${d?.amountPaid ?? '?'}`);
98
143
  lines.push(` Escrow: ${d?.escrowId ?? '?'}`);
99
144
  if (d?.deliverable?.summary) {
@@ -102,15 +147,20 @@ export async function executeAutoagent(params) {
102
147
  lines.push('');
103
148
  receipts.push(d);
104
149
  }
105
- catch (err) {
106
- const msg = err instanceof Error ? err.message : String(err);
150
+ else {
151
+ const msg = result.reason instanceof Error ? result.reason.message : String(result.reason);
107
152
  lines.push(` ✗ Failed: ${msg.slice(0, 120)}`);
108
153
  lines.push('');
109
154
  }
110
155
  }
156
+ // 4. Synthesize outputs into a unified deliverable (via Lava — best-effort)
157
+ const synthesis = await synthesizeDeliverables(goal, receipts);
111
158
  lines.push(`${'─'.repeat(50)}`);
112
159
  lines.push(`Total spent: $${totalSpent.toFixed(2)} USDC / $${maxBudget} budget`);
113
160
  lines.push(`Subtasks completed: ${receipts.length}/${subtasks.length}`);
161
+ if (synthesis) {
162
+ lines.push('', '── Unified Deliverable ──', synthesis);
163
+ }
114
164
  return {
115
165
  content: [{ type: 'text', text: lines.join('\n') }],
116
166
  details: {
@@ -119,7 +169,8 @@ export async function executeAutoagent(params) {
119
169
  receipts,
120
170
  totalSpent: totalSpent.toFixed(2),
121
171
  budgetUsed: `$${totalSpent.toFixed(2)} / $${maxBudget}`,
172
+ synthesis: synthesis ?? undefined,
122
173
  },
123
174
  };
124
175
  }
125
- //# sourceMappingURL=autoagent.js.map
176
+ //# sourceMappingURL=autoagent.js.map
@@ -1,19 +1,23 @@
1
- import { randomBytes } from 'node:crypto';
1
+ import { createHash, randomBytes } from 'node:crypto';
2
2
  import { Type } from '@sinclair/typebox';
3
3
  import { findAgents, postJob, acceptBid, pollJob, } from '../client.js';
4
+ const DEFAULT_API_URL = 'https://api-production-1b45.up.railway.app';
4
5
  export const hireParams = Type.Object({
5
6
  task: Type.String({ description: 'What you want the hired agent to do, e.g. "Research the top AI payment startups"' }),
6
7
  maxPrice: Type.Optional(Type.Number({ minimum: 0.01, description: 'Maximum USDC budget, default 5.00' })),
7
- capability: Type.Optional(Type.String({ description: 'Agent capability to match: research, writing, coding. Auto-detected from task if omitted.' })),
8
+ capability: Type.Optional(Type.String({ description: 'Agent capability to match: research, writing, coding, planning. Auto-detected from task if omitted.' })),
8
9
  minRep: Type.Optional(Type.Number({ minimum: 0, maximum: 100, description: 'Minimum reputation score (0–100) required. Default: 0 (any agent accepted).' })),
9
10
  });
10
11
  function detectCapability(task) {
11
- if (/research|find|search|analyz|summariz|look up/i.test(task))
12
- return 'research';
13
- if (/write|draft|essay|blog|article|copy/i.test(task))
14
- return 'writing';
15
- if (/code|script|function|implement|build|program/i.test(task))
12
+ // Check coding first — "build" would otherwise fall through to planning
13
+ if (/code|script|function|implement|develop|engineer|program|smart contract/i.test(task))
16
14
  return 'coding';
15
+ if (/write|draft|essay|blog|article|copy|content|landing page|marketing|email/i.test(task))
16
+ return 'writing';
17
+ if (/plan|roadmap|strateg|architect|design|outline|milestone|go-to-market/i.test(task))
18
+ return 'planning';
19
+ if (/research|find|search|analyz|summariz|look up|investigat|gather|survey/i.test(task))
20
+ return 'research';
17
21
  return 'research';
18
22
  }
19
23
  function buildConditions(nexusUrl, jobId) {
@@ -49,6 +53,10 @@ function formatDuration(startMs) {
49
53
  const mins = Math.floor(secs / 60);
50
54
  return `${mins}m ${secs % 60}s`;
51
55
  }
56
+ function demoDerivedAddress() {
57
+ const hash = createHash('sha256').update('kroxy-demo-wallet').digest('hex');
58
+ return `0x${hash.slice(0, 40)}`;
59
+ }
52
60
  function buildReceipt(opts) {
53
61
  const lines = [
54
62
  `✅ Job Complete — ${opts.task.slice(0, 60)}${opts.task.length > 60 ? '…' : ''}`,
@@ -70,10 +78,10 @@ function buildReceipt(opts) {
70
78
  return lines.join('\n');
71
79
  }
72
80
  export async function executeHire(params) {
73
- const wallet = process.env.KROXY_AGENT_WALLET;
74
81
  const privateKey = process.env.KROXY_AGENT_PRIVATE_KEY;
75
- const demoMode = process.env.KROXY_DEMO_MODE === '1';
76
- const apiBase = process.env.KROXY_API_URL ?? 'http://localhost:3001';
82
+ const demoMode = process.env.KROXY_DEMO_MODE === '1' || (!process.env.KROXY_DEMO_MODE && !privateKey);
83
+ const wallet = process.env.KROXY_AGENT_WALLET ?? (demoMode ? demoDerivedAddress() : undefined);
84
+ const apiBase = process.env.KROXY_API_URL ?? DEFAULT_API_URL;
77
85
  // In demo mode, point at the embedded demo agent in the API if NEXUS_URL not explicitly set
78
86
  const nexusUrl = process.env.NEXUS_URL ?? (demoMode ? `${apiBase}/demo-agent` : 'http://localhost:3003');
79
87
  if (!wallet)
@@ -160,4 +168,4 @@ export async function executeHire(params) {
160
168
  details,
161
169
  };
162
170
  }
163
- //# sourceMappingURL=hire.js.map
171
+ //# sourceMappingURL=hire.js.map
@@ -1,4 +1,5 @@
1
1
  import { Type } from '@sinclair/typebox';
2
+ import { createHash } from 'node:crypto';
2
3
  import { registerProvider } from '../client.js';
3
4
  export const offerParams = Type.Object({
4
5
  capability: Type.String({ description: 'Capability to offer: research, writing, coding, analysis, etc.' }),
@@ -6,8 +7,14 @@ export const offerParams = Type.Object({
6
7
  endpoint: Type.String({ description: 'Public URL where your agent receives job webhooks, e.g. https://myagent.example.com' }),
7
8
  name: Type.Optional(Type.String({ description: 'Display name for your agent on the Kroxy job board' })),
8
9
  });
10
+ function demoDerivedAddress() {
11
+ const hash = createHash('sha256').update('kroxy-demo-wallet').digest('hex');
12
+ return `0x${hash.slice(0, 40)}`;
13
+ }
9
14
  export async function executeOffer(params) {
10
- const wallet = process.env.KROXY_AGENT_WALLET;
15
+ const privateKey = process.env.KROXY_AGENT_PRIVATE_KEY;
16
+ const demoMode = process.env.KROXY_DEMO_MODE === '1' || (!process.env.KROXY_DEMO_MODE && !privateKey);
17
+ const wallet = process.env.KROXY_AGENT_WALLET ?? (demoMode ? demoDerivedAddress() : undefined);
11
18
  if (!wallet)
12
19
  throw new Error('KROXY_AGENT_WALLET is not configured');
13
20
  const agent = await registerProvider({
@@ -33,4 +40,4 @@ export async function executeOffer(params) {
33
40
  details: result,
34
41
  };
35
42
  }
36
- //# sourceMappingURL=offer.js.map
43
+ //# sourceMappingURL=offer.js.map
@@ -2,6 +2,7 @@ import { Type } from '@sinclair/typebox';
2
2
  import { createHash } from 'node:crypto';
3
3
  import { pingApi } from '../client.js';
4
4
  export const setupParams = Type.Object({});
5
+ const DEFAULT_API_URL = 'https://api-production-1b45.up.railway.app';
5
6
  function icon(s) {
6
7
  return s === 'ok' ? '✅' : s === 'warn' ? '⚠️ ' : '❌';
7
8
  }
@@ -19,11 +20,11 @@ function demoDerivedAddress() {
19
20
  return `0x${hash.slice(0, 40)}`;
20
21
  }
21
22
  export async function executeSetup(_params) {
22
- const apiUrl = process.env.KROXY_API_URL ?? 'http://localhost:3001';
23
+ const apiUrl = process.env.KROXY_API_URL ?? DEFAULT_API_URL;
23
24
  const apiKey = process.env.KROXY_API_KEY;
24
25
  const wallet = process.env.KROXY_AGENT_WALLET;
25
26
  const privateKey = process.env.KROXY_AGENT_PRIVATE_KEY;
26
- const demoMode = process.env.KROXY_DEMO_MODE === '1';
27
+ const demoMode = process.env.KROXY_DEMO_MODE === '1' || (!process.env.KROXY_DEMO_MODE && !privateKey);
27
28
  const nexusUrl = process.env.NEXUS_URL;
28
29
  const checks = [];
29
30
  let blockingIssues = 0;
@@ -55,10 +56,13 @@ export async function executeSetup(_params) {
55
56
  else {
56
57
  checks.push({
57
58
  item: 'API key (KROXY_API_KEY)',
58
- status: 'error',
59
- hint: 'Required for posting jobs and hiring agents. Set in plugin config.',
59
+ status: demoMode ? 'warn' : 'error',
60
+ hint: demoMode
61
+ ? 'Optional in demo mode. Add an API key for higher rate limits and live usage.'
62
+ : 'Required for posting jobs and hiring agents. Set in plugin config.',
60
63
  });
61
- blockingIssues++;
64
+ if (!demoMode)
65
+ blockingIssues++;
62
66
  }
63
67
  // 4. Agent wallet
64
68
  let suggestedWallet;
@@ -138,4 +142,4 @@ export async function executeSetup(_params) {
138
142
  details: result,
139
143
  };
140
144
  }
141
- //# sourceMappingURL=setup.js.map
145
+ //# sourceMappingURL=setup.js.map
package/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { OpenClawPluginApi } from 'openclaw/plugin-sdk';
2
+ import { createHash } from 'node:crypto';
2
3
  import { hireParams, executeHire } from './src/tools/hire.js';
3
4
  import { offerParams, executeOffer } from './src/tools/offer.js';
4
5
  import { reputationParams, executeReputation } from './src/tools/reputation.js';
@@ -21,6 +22,13 @@ type DisputeToolParams = Parameters<typeof executeDispute>[0];
21
22
  type HistoryToolParams = Parameters<typeof executeHistory>[0];
22
23
  type AutoagentToolParams = Parameters<typeof executeAutoagent>[0];
23
24
 
25
+ const DEFAULT_API_URL = 'https://api-production-1b45.up.railway.app';
26
+
27
+ function demoDerivedAddress(): string {
28
+ const hash = createHash('sha256').update('kroxy-demo-wallet').digest('hex');
29
+ return `0x${hash.slice(0, 40)}`;
30
+ }
31
+
24
32
  export default {
25
33
  id: 'kroxy',
26
34
  name: 'Kroxy',
@@ -33,6 +41,12 @@ export default {
33
41
  if (cfg.KROXY_DEMO_MODE) process.env.KROXY_DEMO_MODE = cfg.KROXY_DEMO_MODE;
34
42
  if (cfg.NEXUS_URL) process.env.NEXUS_URL = cfg.NEXUS_URL;
35
43
 
44
+ if (!process.env.KROXY_API_URL) process.env.KROXY_API_URL = DEFAULT_API_URL;
45
+ if (!process.env.KROXY_DEMO_MODE && !process.env.KROXY_AGENT_PRIVATE_KEY) process.env.KROXY_DEMO_MODE = '1';
46
+ if (process.env.KROXY_DEMO_MODE === '1' && !process.env.KROXY_AGENT_WALLET) {
47
+ process.env.KROXY_AGENT_WALLET = demoDerivedAddress();
48
+ }
49
+
36
50
  // ── Setup / Onboarding ────────────────────────────────────────────────────
37
51
  // Run this first to check configuration and get guided setup instructions.
38
52
  api.registerTool({
@@ -8,12 +8,20 @@
8
8
  "type": "object",
9
9
  "additionalProperties": false,
10
10
  "properties": {
11
- "KROXY_API_URL": { "type": "string" },
11
+ "KROXY_API_URL": {
12
+ "type": "string",
13
+ "default": "https://api-production-1b45.up.railway.app"
14
+ },
12
15
  "KROXY_API_KEY": { "type": "string" },
13
16
  "KROXY_AGENT_WALLET": { "type": "string" },
14
17
  "KROXY_AGENT_PRIVATE_KEY": { "type": "string" },
15
18
  "NEXUS_URL": { "type": "string" },
16
- "KROXY_DEMO_MODE": { "type": "string" }
19
+ "LAVA_SECRET_KEY": { "type": "string" },
20
+ "ANTHROPIC_API_KEY": { "type": "string" },
21
+ "KROXY_DEMO_MODE": {
22
+ "type": "string",
23
+ "default": "1"
24
+ }
17
25
  },
18
26
  "required": []
19
27
  },
@@ -25,6 +33,14 @@
25
33
  "KROXY_AGENT_PRIVATE_KEY": {
26
34
  "label": "Kroxy Private Key",
27
35
  "sensitive": true
36
+ },
37
+ "LAVA_SECRET_KEY": {
38
+ "label": "Lava Secret Key (enables intelligent decomposition)",
39
+ "sensitive": true
40
+ },
41
+ "ANTHROPIC_API_KEY": {
42
+ "label": "Anthropic API Key (required with Lava)",
43
+ "sensitive": true
28
44
  }
29
45
  }
30
- }
46
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kroxy/kroxy",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "type": "module",
5
5
  "description": "Trustless agent-to-agent payments via USDC escrow on Base blockchain",
6
6
  "license": "MIT",
@@ -17,7 +17,8 @@
17
17
  "files": [
18
18
  "dist",
19
19
  "index.ts",
20
- "openclaw.plugin.json"
20
+ "openclaw.plugin.json",
21
+ "scripts"
21
22
  ],
22
23
  "dependencies": {
23
24
  "@sinclair/typebox": "^0.32.0"
@@ -29,8 +30,13 @@
29
30
  "peerDependencies": {
30
31
  "openclaw": "*"
31
32
  },
33
+ "bin": {
34
+ "kroxy-setup": "./scripts/setup-openclaw.mjs"
35
+ },
32
36
  "scripts": {
33
37
  "build": "tsc",
34
- "dev": "tsc --watch"
38
+ "dev": "tsc --watch",
39
+ "postinstall": "node ./scripts/setup-openclaw.mjs --postinstall",
40
+ "setup:openclaw": "node ./scripts/setup-openclaw.mjs"
35
41
  }
36
42
  }
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createHash } from 'node:crypto';
4
+ import fs from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import process from 'node:process';
8
+ import readline from 'node:readline/promises';
9
+
10
+ const DEFAULT_API_URL = 'https://api-production-1b45.up.railway.app';
11
+
12
+ function demoDerivedAddress() {
13
+ const hash = createHash('sha256').update('kroxy-demo-wallet').digest('hex');
14
+ return `0x${hash.slice(0, 40)}`;
15
+ }
16
+
17
+ function resolveConfigPath() {
18
+ const explicit = process.env.OPENCLAW_CONFIG_PATH?.trim();
19
+ if (explicit) return explicit;
20
+ const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() || path.join(os.homedir(), '.openclaw');
21
+ return path.join(stateDir, 'openclaw.json');
22
+ }
23
+
24
+ function ensureObject(parent, key) {
25
+ const current = parent[key];
26
+ if (!current || typeof current !== 'object' || Array.isArray(current)) {
27
+ parent[key] = {};
28
+ }
29
+ return parent[key];
30
+ }
31
+
32
+ async function askYesNo(rl, question, defaultYes = true) {
33
+ const suffix = defaultYes ? ' [Y/n] ' : ' [y/N] ';
34
+ const answer = (await rl.question(`${question}${suffix}`)).trim().toLowerCase();
35
+ if (!answer) return defaultYes;
36
+ if (['y', 'yes'].includes(answer)) return true;
37
+ if (['n', 'no'].includes(answer)) return false;
38
+ return defaultYes;
39
+ }
40
+
41
+ function parseJsonConfig(raw, configPath) {
42
+ try {
43
+ const parsed = JSON.parse(raw);
44
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
45
+ throw new Error('top-level JSON value must be an object');
46
+ }
47
+ return parsed;
48
+ } catch (err) {
49
+ throw new Error(
50
+ `Could not parse ${configPath} as strict JSON (${String(err)}).\n` +
51
+ 'Run: openclaw config set plugins.entries.kroxy.config.KROXY_API_URL ' +
52
+ `"${DEFAULT_API_URL}"`
53
+ );
54
+ }
55
+ }
56
+
57
+ async function runSetup({ postinstall }) {
58
+ const configPath = resolveConfigPath();
59
+ const commandHint = 'node ~/.openclaw/extensions/kroxy/scripts/setup-openclaw.mjs';
60
+
61
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
62
+ if (postinstall) {
63
+ console.log(`[kroxy] Skipping interactive setup (non-interactive terminal). Run ${commandHint}`);
64
+ }
65
+ return 0;
66
+ }
67
+
68
+ if (!fs.existsSync(configPath)) {
69
+ console.log(`[kroxy] OpenClaw config not found at ${configPath}.`);
70
+ console.log('[kroxy] Run OpenClaw once, then run this setup wizard again.');
71
+ return 0;
72
+ }
73
+
74
+ const raw = fs.readFileSync(configPath, 'utf8');
75
+ const cfg = parseJsonConfig(raw, configPath);
76
+ const plugins = ensureObject(cfg, 'plugins');
77
+ const entries = ensureObject(plugins, 'entries');
78
+ const kroxy = ensureObject(entries, 'kroxy');
79
+ const pluginConfig = ensureObject(kroxy, 'config');
80
+
81
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
82
+ try {
83
+ const start = await askYesNo(rl, 'Configure Kroxy in OpenClaw now?', true);
84
+ if (!start) return 0;
85
+
86
+ if (await askYesNo(rl, `Set KROXY_API_URL to ${DEFAULT_API_URL}?`, !pluginConfig.KROXY_API_URL)) {
87
+ pluginConfig.KROXY_API_URL = DEFAULT_API_URL;
88
+ }
89
+
90
+ const defaultDemo = pluginConfig.KROXY_DEMO_MODE !== '0';
91
+ if (await askYesNo(rl, 'Enable demo mode by default (recommended for first run)?', defaultDemo)) {
92
+ pluginConfig.KROXY_DEMO_MODE = '1';
93
+ } else if (pluginConfig.KROXY_DEMO_MODE === '1') {
94
+ delete pluginConfig.KROXY_DEMO_MODE;
95
+ }
96
+
97
+ if (pluginConfig.KROXY_DEMO_MODE === '1') {
98
+ const shouldSetDemoWallet = await askYesNo(
99
+ rl,
100
+ 'Auto-set a demo wallet address for KROXY_AGENT_WALLET?',
101
+ !pluginConfig.KROXY_AGENT_WALLET
102
+ );
103
+ if (shouldSetDemoWallet) {
104
+ pluginConfig.KROXY_AGENT_WALLET = pluginConfig.KROXY_AGENT_WALLET || demoDerivedAddress();
105
+ }
106
+ }
107
+
108
+ const setApiKeyNow = await askYesNo(rl, 'Set KROXY_API_KEY now?', false);
109
+ if (setApiKeyNow) {
110
+ const key = (await rl.question('Paste KROXY_API_KEY: ')).trim();
111
+ if (key) pluginConfig.KROXY_API_KEY = key;
112
+ }
113
+
114
+ const save = await askYesNo(rl, `Save changes to ${configPath}?`, true);
115
+ if (!save) {
116
+ console.log('[kroxy] Setup canceled; no changes saved.');
117
+ return 0;
118
+ }
119
+
120
+ fs.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}\n`, 'utf8');
121
+ console.log('[kroxy] Saved OpenClaw config for plugin "kroxy".');
122
+ console.log('[kroxy] Restart the gateway to load the new config.');
123
+ return 0;
124
+ } finally {
125
+ rl.close();
126
+ }
127
+ }
128
+
129
+ const postinstall = process.argv.includes('--postinstall');
130
+ runSetup({ postinstall }).then(
131
+ (code) => {
132
+ process.exitCode = code;
133
+ },
134
+ (err) => {
135
+ console.error(`[kroxy] Setup wizard failed: ${String(err)}`);
136
+ process.exitCode = 1;
137
+ }
138
+ );