@ottocode/openclaw-setu 0.1.1 → 0.1.3

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/README.md CHANGED
@@ -17,17 +17,22 @@ OpenClaw → localhost:8403 (Setu proxy) → api.setu.ottocode.io → LLM provid
17
17
 
18
18
  ## Quick Start
19
19
 
20
+ No project installation needed — this is a global tool.
21
+
20
22
  ```bash
21
- # Install
22
- bun add @ottocode/openclaw-setu
23
+ # Option 1: Zero-install with bunx (recommended)
24
+ bunx @ottocode/openclaw-setu setup
23
25
 
24
- # Interactive setup (generates wallet, injects config)
25
- bunx openclaw-setu setup
26
+ # Option 2: Global install
27
+ bun install -g @ottocode/openclaw-setu
28
+ openclaw-setu setup
29
+ ```
26
30
 
31
+ ```bash
27
32
  # Fund your wallet with USDC on Solana (address shown during setup)
28
33
 
29
34
  # Start the proxy
30
- bunx openclaw-setu start
35
+ bunx @ottocode/openclaw-setu start
31
36
 
32
37
  # Restart OpenClaw
33
38
  openclaw gateway restart
@@ -49,6 +54,8 @@ openclaw-setu config remove Remove Setu provider from openclaw.json
49
54
  openclaw-setu config status Check if Setu is configured
50
55
  ```
51
56
 
57
+ All commands work with `bunx @ottocode/openclaw-setu <command>` (no install required).
58
+
52
59
  ## As an OpenClaw Plugin
53
60
 
54
61
  If OpenClaw loads the plugin automatically (via `openclaw.extensions` in package.json), Setu registers:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ottocode/openclaw-setu",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Setu provider for OpenClaw — pay for AI with Solana USDC. No API keys, just a wallet.",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -37,7 +37,7 @@
37
37
  "directory": "packages/openclaw-setu"
38
38
  },
39
39
  "dependencies": {
40
- "@ottocode/ai-sdk": "workspace:*"
40
+ "@ottocode/ai-sdk": "^0.1.4"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "openclaw": ">=2025.1.0"
package/src/config.ts CHANGED
@@ -1,4 +1,11 @@
1
- import { readFileSync, writeFileSync, existsSync } from "node:fs";
1
+ import {
2
+ readFileSync,
3
+ writeFileSync,
4
+ existsSync,
5
+ readdirSync,
6
+ mkdirSync,
7
+ renameSync,
8
+ } from "node:fs";
2
9
  import { join } from "node:path";
3
10
  import { homedir } from "node:os";
4
11
  import type { ModelApi } from "./types.ts";
@@ -9,6 +16,7 @@ const OPENCLAW_CONFIG_PATH = join(OPENCLAW_DIR, "openclaw.json");
9
16
  const PROVIDER_KEY = "setu";
10
17
  const DEFAULT_PROXY_PORT = 8403;
11
18
  const DEFAULT_BASE_URL = "https://api.setu.ottocode.io";
19
+ const SETU_PROXY_PORT_PATTERN = /[:\/]8403/;
12
20
 
13
21
  export interface SetuModelConfig {
14
22
  id: string;
@@ -28,17 +36,42 @@ export interface SetuProviderConfig {
28
36
  models: SetuModelConfig[];
29
37
  }
30
38
 
39
+ const DUMMY_API_KEY = "setu-proxy-handles-auth";
40
+
41
+ const MODEL_ALIASES: Array<{ id: string; alias: string }> = [
42
+ { id: "claude-sonnet-4-6", alias: "sonnet-4.6" },
43
+ { id: "claude-sonnet-4-5", alias: "sonnet-4.5" },
44
+ { id: "claude-opus-4-6", alias: "opus" },
45
+ { id: "claude-3-5-haiku-20241022", alias: "haiku" },
46
+ { id: "gpt-5.1-codex", alias: "codex" },
47
+ { id: "gpt-5", alias: "gpt5" },
48
+ { id: "gpt-5-mini", alias: "gpt5-mini" },
49
+ { id: "codex-mini-latest", alias: "codex-mini" },
50
+ { id: "gemini-3-pro-preview", alias: "gemini-pro" },
51
+ { id: "gemini-3-flash-preview", alias: "gemini-flash" },
52
+ { id: "kimi-k2.5", alias: "kimi" },
53
+ { id: "glm-5", alias: "glm" },
54
+ { id: "MiniMax-M2.5", alias: "minimax" },
55
+ ];
56
+
31
57
  function readOpenClawConfig(): Record<string, unknown> {
32
58
  if (!existsSync(OPENCLAW_CONFIG_PATH)) return {};
33
59
  try {
34
- return JSON.parse(readFileSync(OPENCLAW_CONFIG_PATH, "utf-8"));
60
+ const content = readFileSync(OPENCLAW_CONFIG_PATH, "utf-8").trim();
61
+ if (!content) return {};
62
+ return JSON.parse(content);
35
63
  } catch {
36
64
  return {};
37
65
  }
38
66
  }
39
67
 
40
- function writeOpenClawConfig(config: Record<string, unknown>): void {
41
- writeFileSync(OPENCLAW_CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
68
+ function writeOpenClawConfigAtomic(config: Record<string, unknown>): void {
69
+ if (!existsSync(OPENCLAW_DIR)) {
70
+ mkdirSync(OPENCLAW_DIR, { recursive: true });
71
+ }
72
+ const tmpPath = `${OPENCLAW_CONFIG_PATH}.tmp.${process.pid}`;
73
+ writeFileSync(tmpPath, JSON.stringify(config, null, 2) + "\n");
74
+ renameSync(tmpPath, OPENCLAW_CONFIG_PATH);
42
75
  }
43
76
 
44
77
  interface CatalogModel {
@@ -233,17 +266,212 @@ export async function buildProviderConfigWithCatalog(
233
266
  };
234
267
  }
235
268
 
269
+ function removeConflictingCustomProviders(
270
+ providers: Record<string, unknown>,
271
+ port: number,
272
+ ): string[] {
273
+ const removed: string[] = [];
274
+ for (const key of Object.keys(providers)) {
275
+ if (key === PROVIDER_KEY) continue;
276
+ if (!key.startsWith("custom-")) continue;
277
+ const p = providers[key] as Record<string, unknown> | undefined;
278
+ if (!p?.baseUrl) continue;
279
+ const baseUrl = String(p.baseUrl);
280
+ const pointsToSetuProxy =
281
+ baseUrl.includes(`:${port}`) || SETU_PROXY_PORT_PATTERN.test(baseUrl);
282
+ if (pointsToSetuProxy) {
283
+ delete providers[key];
284
+ removed.push(key);
285
+ }
286
+ }
287
+ return removed;
288
+ }
289
+
290
+ function migrateDefaultModel(
291
+ config: Record<string, unknown>,
292
+ removedKeys: string[],
293
+ ): void {
294
+ if (removedKeys.length === 0) return;
295
+ const agents = config.agents as Record<string, unknown> | undefined;
296
+ const defaults = agents?.defaults as Record<string, unknown> | undefined;
297
+ const model = defaults?.model as Record<string, unknown> | undefined;
298
+ if (!model?.primary) return;
299
+ const primary = String(model.primary);
300
+ for (const oldKey of removedKeys) {
301
+ if (primary.startsWith(`${oldKey}/`)) {
302
+ const modelId = primary.slice(oldKey.length + 1);
303
+ model.primary = `setu/${modelId}`;
304
+ break;
305
+ }
306
+ }
307
+ }
308
+
236
309
  export async function injectConfig(port: number = DEFAULT_PROXY_PORT): Promise<void> {
237
310
  const config = readOpenClawConfig();
311
+ let needsWrite = false;
238
312
 
239
- if (!config.models) config.models = {};
313
+ if (!config.models) {
314
+ config.models = {};
315
+ needsWrite = true;
316
+ }
240
317
  const models = config.models as Record<string, unknown>;
241
- if (!models.providers) models.providers = {};
318
+ if (!models.providers) {
319
+ models.providers = {};
320
+ needsWrite = true;
321
+ }
242
322
  const providers = models.providers as Record<string, unknown>;
243
323
 
244
- providers[PROVIDER_KEY] = await buildProviderConfigWithCatalog(port);
324
+ const removed = removeConflictingCustomProviders(providers, port);
325
+ if (removed.length > 0) {
326
+ migrateDefaultModel(config, removed);
327
+ needsWrite = true;
328
+ }
329
+
330
+ const expectedBaseUrl = `http://localhost:${port}/v1`;
331
+ const existing = providers[PROVIDER_KEY] as Record<string, unknown> | undefined;
332
+
333
+ if (!existing) {
334
+ providers[PROVIDER_KEY] = await buildProviderConfigWithCatalog(port);
335
+ needsWrite = true;
336
+ } else {
337
+ if (!existing.baseUrl || existing.baseUrl !== expectedBaseUrl) {
338
+ existing.baseUrl = expectedBaseUrl;
339
+ needsWrite = true;
340
+ }
341
+ if (!existing.apiKey) {
342
+ existing.apiKey = DUMMY_API_KEY;
343
+ needsWrite = true;
344
+ }
345
+ if (!existing.api) {
346
+ existing.api = "openai-completions";
347
+ needsWrite = true;
348
+ }
349
+ const currentModels = existing.models as Array<{ id?: string }> | undefined;
350
+ const defaultModels = getDefaultModels();
351
+ const currentIds = new Set(
352
+ Array.isArray(currentModels) ? currentModels.map((m) => m?.id).filter(Boolean) : [],
353
+ );
354
+ const expectedIds = defaultModels.map((m) => m.id);
355
+ if (
356
+ !currentModels ||
357
+ !Array.isArray(currentModels) ||
358
+ currentModels.length !== defaultModels.length ||
359
+ expectedIds.some((id) => !currentIds.has(id))
360
+ ) {
361
+ existing.models = await fetchModelsFromCatalog();
362
+ needsWrite = true;
363
+ }
364
+ }
365
+
366
+ if (!config.agents) {
367
+ config.agents = {};
368
+ needsWrite = true;
369
+ }
370
+ const agents = config.agents as Record<string, unknown>;
371
+ if (!agents.defaults) {
372
+ agents.defaults = {};
373
+ needsWrite = true;
374
+ }
375
+ const defaults = agents.defaults as Record<string, unknown>;
376
+ if (!defaults.model) {
377
+ defaults.model = {};
378
+ needsWrite = true;
379
+ }
380
+ const model = defaults.model as Record<string, unknown>;
381
+
382
+ if (!model.primary) {
383
+ model.primary = "setu/claude-sonnet-4-6";
384
+ needsWrite = true;
385
+ }
386
+
387
+ if (!defaults.models) {
388
+ defaults.models = {};
389
+ needsWrite = true;
390
+ }
391
+ const allowlist = defaults.models as Record<string, unknown>;
392
+ for (const m of MODEL_ALIASES) {
393
+ const fullId = `setu/${m.id}`;
394
+ const entry = allowlist[fullId] as Record<string, unknown> | undefined;
395
+ if (!entry) {
396
+ allowlist[fullId] = { alias: m.alias };
397
+ needsWrite = true;
398
+ } else if (entry.alias !== m.alias) {
399
+ entry.alias = m.alias;
400
+ needsWrite = true;
401
+ }
402
+ }
403
+
404
+ if (needsWrite) {
405
+ writeOpenClawConfigAtomic(config);
406
+ }
407
+ }
245
408
 
246
- writeOpenClawConfig(config);
409
+ export function injectAuthProfile(): void {
410
+ const agentsDir = join(OPENCLAW_DIR, "agents");
411
+
412
+ if (!existsSync(agentsDir)) {
413
+ try {
414
+ mkdirSync(agentsDir, { recursive: true });
415
+ } catch {
416
+ return;
417
+ }
418
+ }
419
+
420
+ let agents: string[];
421
+ try {
422
+ agents = readdirSync(agentsDir, { withFileTypes: true })
423
+ .filter((d) => d.isDirectory())
424
+ .map((d) => d.name);
425
+ } catch {
426
+ agents = [];
427
+ }
428
+
429
+ if (!agents.includes("main")) {
430
+ agents = ["main", ...agents];
431
+ }
432
+
433
+ for (const agentId of agents) {
434
+ const authDir = join(agentsDir, agentId, "agent");
435
+ const authPath = join(authDir, "auth-profiles.json");
436
+
437
+ if (!existsSync(authDir)) {
438
+ try {
439
+ mkdirSync(authDir, { recursive: true });
440
+ } catch {
441
+ continue;
442
+ }
443
+ }
444
+
445
+ let store: { version: number; profiles: Record<string, unknown> } = {
446
+ version: 1,
447
+ profiles: {},
448
+ };
449
+ if (existsSync(authPath)) {
450
+ try {
451
+ const existing = JSON.parse(readFileSync(authPath, "utf-8"));
452
+ if (existing.version && existing.profiles) {
453
+ store = existing;
454
+ }
455
+ } catch {
456
+ // use fresh store
457
+ }
458
+ }
459
+
460
+ const profileKey = "setu:default";
461
+ if (store.profiles[profileKey]) continue;
462
+
463
+ store.profiles[profileKey] = {
464
+ type: "api_key",
465
+ provider: "setu",
466
+ key: DUMMY_API_KEY,
467
+ };
468
+
469
+ try {
470
+ writeFileSync(authPath, JSON.stringify(store, null, 2));
471
+ } catch {
472
+ // skip
473
+ }
474
+ }
247
475
  }
248
476
 
249
477
  export function removeConfig(): void {
@@ -254,7 +482,7 @@ export function removeConfig(): void {
254
482
  const providers = models.providers as Record<string, unknown>;
255
483
  delete providers[PROVIDER_KEY];
256
484
 
257
- writeOpenClawConfig(config);
485
+ writeOpenClawConfigAtomic(config);
258
486
  }
259
487
 
260
488
  export function isConfigured(): boolean {
package/src/index.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  import {
13
13
  buildProviderConfig,
14
14
  injectConfig,
15
+ injectAuthProfile,
15
16
  isConfigured,
16
17
  } from "./config.ts";
17
18
  import { isValidPrivateKey } from "@ottocode/ai-sdk";
@@ -32,6 +33,33 @@ const plugin: OpenClawPluginDefinition = {
32
33
  async register(api: OpenClawPluginApi) {
33
34
  const port = getPort(api);
34
35
 
36
+ await injectConfig(port).catch(() => {});
37
+ try { injectAuthProfile(); } catch {}
38
+
39
+ if (!api.config.models) {
40
+ api.config.models = { providers: {} };
41
+ }
42
+ if (!api.config.models.providers) {
43
+ api.config.models.providers = {};
44
+ }
45
+ const providerConfig = buildProviderConfig(port);
46
+ api.config.models.providers.setu = {
47
+ baseUrl: providerConfig.baseUrl,
48
+ api: providerConfig.api,
49
+ apiKey: providerConfig.apiKey,
50
+ models: providerConfig.models,
51
+ };
52
+
53
+ if (!api.config.agents) api.config.agents = {};
54
+ const agents = api.config.agents as Record<string, unknown>;
55
+ if (!agents.defaults) agents.defaults = {};
56
+ const defaults = agents.defaults as Record<string, unknown>;
57
+ if (!defaults.model) defaults.model = {};
58
+ const model = defaults.model as Record<string, unknown>;
59
+ if (!model.primary) {
60
+ model.primary = "setu/claude-sonnet-4-6";
61
+ }
62
+
35
63
  api.registerProvider({
36
64
  id: "setu",
37
65
  label: "Setu (Solana USDC)",