@mnemom/mnemom 0.7.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/LICENSE +191 -0
- package/README.md +123 -0
- package/dist/commands/agents.d.ts +15 -0
- package/dist/commands/agents.js +303 -0
- package/dist/commands/auth.d.ts +5 -0
- package/dist/commands/auth.js +60 -0
- package/dist/commands/card.d.ts +23 -0
- package/dist/commands/card.js +460 -0
- package/dist/commands/claim.d.ts +1 -0
- package/dist/commands/claim.js +72 -0
- package/dist/commands/init.d.ts +7 -0
- package/dist/commands/init.js +763 -0
- package/dist/commands/integrity.d.ts +1 -0
- package/dist/commands/integrity.js +49 -0
- package/dist/commands/license.d.ts +3 -0
- package/dist/commands/license.js +163 -0
- package/dist/commands/logs.d.ts +5 -0
- package/dist/commands/logs.js +73 -0
- package/dist/commands/migrate-config.d.ts +2 -0
- package/dist/commands/migrate-config.js +72 -0
- package/dist/commands/policy.d.ts +31 -0
- package/dist/commands/policy.js +543 -0
- package/dist/commands/register.d.ts +6 -0
- package/dist/commands/register.js +362 -0
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +383 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +381 -0
- package/dist/lib/api.d.ts +133 -0
- package/dist/lib/api.js +207 -0
- package/dist/lib/auth.d.ts +60 -0
- package/dist/lib/auth.js +281 -0
- package/dist/lib/config.d.ts +105 -0
- package/dist/lib/config.js +253 -0
- package/dist/lib/format.d.ts +35 -0
- package/dist/lib/format.js +60 -0
- package/dist/lib/model-cache.d.ts +16 -0
- package/dist/lib/model-cache.js +138 -0
- package/dist/lib/models.d.ts +41 -0
- package/dist/lib/models.js +357 -0
- package/dist/lib/openclaw.d.ts +221 -0
- package/dist/lib/openclaw.js +474 -0
- package/dist/lib/prompt.d.ts +26 -0
- package/dist/lib/prompt.js +150 -0
- package/dist/smoltbot-shim.d.ts +2 -0
- package/dist/smoltbot-shim.js +7 -0
- package/package.json +61 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as crypto from "node:crypto";
|
|
5
|
+
export const CONFIG_DIR = path.join(os.homedir(), ".smoltbot");
|
|
6
|
+
export const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
7
|
+
const API_URLS = {
|
|
8
|
+
production: "https://api.mnemom.ai",
|
|
9
|
+
staging: "https://api-staging.mnemom.ai",
|
|
10
|
+
local: "http://localhost:8787",
|
|
11
|
+
};
|
|
12
|
+
const GATEWAY_URLS = {
|
|
13
|
+
production: "https://gateway.mnemom.ai",
|
|
14
|
+
staging: "https://gateway-staging.mnemom.ai",
|
|
15
|
+
local: "http://localhost:8787",
|
|
16
|
+
};
|
|
17
|
+
const WEBSITE_URLS = {
|
|
18
|
+
production: "https://www.mnemom.ai",
|
|
19
|
+
staging: "https://staging.mnemom.ai",
|
|
20
|
+
local: "http://localhost:5173",
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the active environment.
|
|
24
|
+
*
|
|
25
|
+
* Resolution order:
|
|
26
|
+
* 1. `SMOLTBOT_ENV` environment variable
|
|
27
|
+
* 2. Defaults to `production`
|
|
28
|
+
*/
|
|
29
|
+
export function getEnvironment() {
|
|
30
|
+
const env = process.env.SMOLTBOT_ENV;
|
|
31
|
+
if (env === "staging" || env === "local")
|
|
32
|
+
return env;
|
|
33
|
+
return "production";
|
|
34
|
+
}
|
|
35
|
+
export function getApiUrl() {
|
|
36
|
+
return API_URLS[getEnvironment()];
|
|
37
|
+
}
|
|
38
|
+
export function getGatewayUrl() {
|
|
39
|
+
return GATEWAY_URLS[getEnvironment()];
|
|
40
|
+
}
|
|
41
|
+
export function getWebsiteUrl() {
|
|
42
|
+
return WEBSITE_URLS[getEnvironment()];
|
|
43
|
+
}
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Migration
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
/**
|
|
48
|
+
* Migrate a v1 config into v2 format.
|
|
49
|
+
* All agent-specific fields move into `agents.default`.
|
|
50
|
+
*/
|
|
51
|
+
export function migrateConfig(raw) {
|
|
52
|
+
return {
|
|
53
|
+
version: 2,
|
|
54
|
+
defaultAgent: "default",
|
|
55
|
+
gateway: raw.gateway ?? "https://gateway.mnemom.ai",
|
|
56
|
+
mnemomApiKey: raw.mnemomApiKey,
|
|
57
|
+
licenseJwt: raw.licenseJwt,
|
|
58
|
+
agents: {
|
|
59
|
+
default: {
|
|
60
|
+
agentId: raw.agentId,
|
|
61
|
+
openclawConfigured: raw.openclawConfigured,
|
|
62
|
+
providers: raw.providers,
|
|
63
|
+
configuredAt: raw.configuredAt,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Persistence
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
export function configExists() {
|
|
72
|
+
return fs.existsSync(CONFIG_FILE);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Load the config file.
|
|
76
|
+
* If the file is v1 (no `version` field), it is automatically migrated to v2
|
|
77
|
+
* and written back to disk before returning.
|
|
78
|
+
*/
|
|
79
|
+
export function loadConfig() {
|
|
80
|
+
if (!configExists()) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const content = fs.readFileSync(CONFIG_FILE, "utf-8");
|
|
85
|
+
const raw = JSON.parse(content);
|
|
86
|
+
// Detect v1: no `version` field present
|
|
87
|
+
if (!raw.version) {
|
|
88
|
+
const migrated = migrateConfig(raw);
|
|
89
|
+
saveConfig(migrated);
|
|
90
|
+
return migrated;
|
|
91
|
+
}
|
|
92
|
+
return raw;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export function saveConfig(config) {
|
|
99
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
100
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
101
|
+
}
|
|
102
|
+
// Validate write path stays within expected directory
|
|
103
|
+
const resolvedPath = path.resolve(CONFIG_FILE);
|
|
104
|
+
if (!resolvedPath.startsWith(path.resolve(CONFIG_DIR))) {
|
|
105
|
+
throw new Error("Config file path escapes expected directory");
|
|
106
|
+
}
|
|
107
|
+
// Re-serialize to sanitize any HTTP-sourced data before writing to disk
|
|
108
|
+
const sanitizedConfig = JSON.parse(JSON.stringify(config));
|
|
109
|
+
const tmpFile = `${resolvedPath}.${process.pid}.tmp`;
|
|
110
|
+
fs.writeFileSync(tmpFile, JSON.stringify(sanitizedConfig, null, 2));
|
|
111
|
+
fs.renameSync(tmpFile, resolvedPath);
|
|
112
|
+
}
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Agent resolution
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
/**
|
|
117
|
+
* Resolve the active agent config.
|
|
118
|
+
*
|
|
119
|
+
* Resolution order:
|
|
120
|
+
* 1. Explicit `agentName` parameter (--agent flag)
|
|
121
|
+
* 2. `SMOLTBOT_AGENT` environment variable
|
|
122
|
+
*
|
|
123
|
+
* Returns `null` if no agent is specified or the agent is not found.
|
|
124
|
+
* Callers must require --agent or SMOLTBOT_AGENT for agent-scoped commands.
|
|
125
|
+
*/
|
|
126
|
+
export function getActiveAgent(agentName) {
|
|
127
|
+
const config = loadConfig();
|
|
128
|
+
if (!config) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const name = agentName ??
|
|
132
|
+
process.env.SMOLTBOT_AGENT;
|
|
133
|
+
if (!name) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
return config.agents[name] ?? null;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Require an explicit agent selection. Exits with a helpful error if
|
|
140
|
+
* no agent was specified via --agent or SMOLTBOT_AGENT.
|
|
141
|
+
*
|
|
142
|
+
* Falls back to API lookup if the agent is not in local config:
|
|
143
|
+
* - smolt-XXXXXXXX IDs: public endpoint, no auth required
|
|
144
|
+
* - Names: authenticated account listing (requires `smoltbot login`)
|
|
145
|
+
*/
|
|
146
|
+
export async function requireAgent(agentName) {
|
|
147
|
+
if (!configExists()) {
|
|
148
|
+
console.error("\nsmoltbot is not initialized. Run `smoltbot init` first.\n");
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
// Fast path: local config hit — no network call needed
|
|
152
|
+
const localAgent = getActiveAgent(agentName);
|
|
153
|
+
if (localAgent)
|
|
154
|
+
return localAgent;
|
|
155
|
+
const name = agentName ?? process.env.SMOLTBOT_AGENT;
|
|
156
|
+
if (!name) {
|
|
157
|
+
console.error("\nAgent required. Use --agent <name> or set SMOLTBOT_AGENT.\n");
|
|
158
|
+
console.error("Available agents:");
|
|
159
|
+
const config = loadConfig();
|
|
160
|
+
if (config) {
|
|
161
|
+
for (const [n, a] of Object.entries(config.agents)) {
|
|
162
|
+
console.error(` ${n} (${a.agentId})`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
console.error();
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
// API fallback: resolve from account
|
|
169
|
+
try {
|
|
170
|
+
const { getAgent, getAgentByName } = await import("./api.js");
|
|
171
|
+
let found = null;
|
|
172
|
+
if (/^smolt-[0-9a-f]{8}$/.test(name)) {
|
|
173
|
+
// Public endpoint — no auth needed
|
|
174
|
+
found = await getAgent(name);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
found = await getAgentByName(name);
|
|
178
|
+
}
|
|
179
|
+
if (found) {
|
|
180
|
+
return { agentId: found.id };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
185
|
+
if (msg.includes("Not logged in")) {
|
|
186
|
+
console.error(`\n${msg}\n`);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
// Other API errors: fall through to "not found" message
|
|
190
|
+
}
|
|
191
|
+
console.error(`\nAgent not found: ${name}`);
|
|
192
|
+
console.error(`Run \`smoltbot agents add ${name}\` to register it locally,`);
|
|
193
|
+
console.error(`or check \`smoltbot agents\` to see agents in your account.\n`);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
// ID generation
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
export function generateAgentId() {
|
|
200
|
+
// New format per ADR-019 (scale/step-25b): mnm-{uuid_v4} for all new agents.
|
|
201
|
+
// crypto.randomUUID() is a Node.js built-in (>=14.17), no new dependencies.
|
|
202
|
+
return `mnm-${crypto.randomUUID()}`;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Compute the 16-char agent_hash for a given API key and optional agent name.
|
|
206
|
+
* Matches the gateway's hashApiKey() and the POST /v1/agents/:id/rekey expected format.
|
|
207
|
+
*
|
|
208
|
+
* Unnamed agent: SHA256(apiKey).slice(0, 16)
|
|
209
|
+
* Named agent: SHA256(apiKey + '|' + name).slice(0, 16)
|
|
210
|
+
*/
|
|
211
|
+
export function computeAgentHash(apiKey, name) {
|
|
212
|
+
const input = name ? `${apiKey}|${name}` : apiKey;
|
|
213
|
+
// eslint-disable-next-line -- deterministic ID derivation must match gateway SHA-256
|
|
214
|
+
return crypto.createHash("sha256").update(input).digest("hex").slice(0, 16); // lgtm[js/insufficient-password-hash]
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Derive agent ID deterministically from an API key.
|
|
218
|
+
* Uses SHA-256 to match the gateway's hashApiKey (Web Crypto SHA-256, first 16 hex chars).
|
|
219
|
+
* The agent ID is "smolt-" + first 8 hex chars of the SHA-256 digest.
|
|
220
|
+
*/
|
|
221
|
+
export function deriveAgentId(apiKey) {
|
|
222
|
+
return `smolt-${computeAgentHash(apiKey).slice(0, 8)}`;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Derive agent ID deterministically from an API key *and* a name.
|
|
226
|
+
* Allows multiple named agents to share one API key with distinct IDs.
|
|
227
|
+
* Uses SHA-256 to match the gateway's hashApiKey(apiKey + '|' + name).
|
|
228
|
+
*/
|
|
229
|
+
export function deriveAgentIdWithName(apiKey, name) {
|
|
230
|
+
return `smolt-${computeAgentHash(apiKey, name).slice(0, 8)}`;
|
|
231
|
+
}
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// Auth token helpers
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
export function saveAuthTokens(tokens) {
|
|
236
|
+
const config = loadConfig();
|
|
237
|
+
if (!config) {
|
|
238
|
+
throw new Error("Config not initialized. Run `smoltbot init` first.");
|
|
239
|
+
}
|
|
240
|
+
config.auth = tokens;
|
|
241
|
+
saveConfig(config);
|
|
242
|
+
}
|
|
243
|
+
export function clearAuthTokens() {
|
|
244
|
+
const config = loadConfig();
|
|
245
|
+
if (!config)
|
|
246
|
+
return;
|
|
247
|
+
delete config.auth;
|
|
248
|
+
saveConfig(config);
|
|
249
|
+
}
|
|
250
|
+
export function getAuthInfo() {
|
|
251
|
+
const config = loadConfig();
|
|
252
|
+
return config?.auth ?? null;
|
|
253
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type BadgeColor = "green" | "red" | "yellow" | "blue" | "cyan" | "magenta" | "white";
|
|
2
|
+
export declare const fmt: {
|
|
3
|
+
/**
|
|
4
|
+
* Bold header with "═" double-line border
|
|
5
|
+
*/
|
|
6
|
+
header(title: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Dim section divider with "─" line
|
|
9
|
+
*/
|
|
10
|
+
section(title: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Green check mark with message
|
|
13
|
+
*/
|
|
14
|
+
success(msg: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Red cross with message
|
|
17
|
+
*/
|
|
18
|
+
error(msg: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Yellow warning with message
|
|
21
|
+
*/
|
|
22
|
+
warn(msg: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Dim label with value
|
|
25
|
+
*/
|
|
26
|
+
label(key: string, val: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Syntax-highlighted JSON output
|
|
29
|
+
*/
|
|
30
|
+
json(obj: unknown): string;
|
|
31
|
+
/**
|
|
32
|
+
* Colored inline badge
|
|
33
|
+
*/
|
|
34
|
+
badge(text: string, color?: BadgeColor): string;
|
|
35
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
export const fmt = {
|
|
3
|
+
/**
|
|
4
|
+
* Bold header with "═" double-line border
|
|
5
|
+
*/
|
|
6
|
+
header(title) {
|
|
7
|
+
const line = "═".repeat(60);
|
|
8
|
+
return `\n${chalk.bold(line)}\n ${chalk.bold(title)}\n${chalk.bold(line)}`;
|
|
9
|
+
},
|
|
10
|
+
/**
|
|
11
|
+
* Dim section divider with "─" line
|
|
12
|
+
*/
|
|
13
|
+
section(title) {
|
|
14
|
+
const line = "─".repeat(50);
|
|
15
|
+
return `\n${chalk.dim(line)}\n${title}\n${chalk.dim(line)}`;
|
|
16
|
+
},
|
|
17
|
+
/**
|
|
18
|
+
* Green check mark with message
|
|
19
|
+
*/
|
|
20
|
+
success(msg) {
|
|
21
|
+
return `${chalk.green("✓")} ${msg}`;
|
|
22
|
+
},
|
|
23
|
+
/**
|
|
24
|
+
* Red cross with message
|
|
25
|
+
*/
|
|
26
|
+
error(msg) {
|
|
27
|
+
return `${chalk.red("✗")} ${msg}`;
|
|
28
|
+
},
|
|
29
|
+
/**
|
|
30
|
+
* Yellow warning with message
|
|
31
|
+
*/
|
|
32
|
+
warn(msg) {
|
|
33
|
+
return `${chalk.yellow("⚠")} ${msg}`;
|
|
34
|
+
},
|
|
35
|
+
/**
|
|
36
|
+
* Dim label with value
|
|
37
|
+
*/
|
|
38
|
+
label(key, val) {
|
|
39
|
+
return `${chalk.dim(key)} ${val}`;
|
|
40
|
+
},
|
|
41
|
+
/**
|
|
42
|
+
* Syntax-highlighted JSON output
|
|
43
|
+
*/
|
|
44
|
+
json(obj) {
|
|
45
|
+
const raw = JSON.stringify(obj, null, 2);
|
|
46
|
+
return raw
|
|
47
|
+
.replace(/"([^"]+)":/g, (_match, key) => `${chalk.cyan(`"${key}"`)}:`)
|
|
48
|
+
.replace(/: "([^"]*)"/g, (_match, val) => `: ${chalk.green(`"${val}"`)}`)
|
|
49
|
+
.replace(/: (\d+)/g, (_match, num) => `: ${chalk.yellow(num)}`)
|
|
50
|
+
.replace(/: (true|false)/g, (_match, bool) => `: ${chalk.magenta(bool)}`)
|
|
51
|
+
.replace(/: (null)/g, (_match, n) => `: ${chalk.dim(n)}`);
|
|
52
|
+
},
|
|
53
|
+
/**
|
|
54
|
+
* Colored inline badge
|
|
55
|
+
*/
|
|
56
|
+
badge(text, color = "blue") {
|
|
57
|
+
const colorFn = chalk[color] || chalk.blue;
|
|
58
|
+
return colorFn(`[${text}]`);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ModelDefinition, Provider } from "./openclaw.js";
|
|
2
|
+
/**
|
|
3
|
+
* Refresh the model cache in the background.
|
|
4
|
+
* Fetches from the gateway and saves to disk.
|
|
5
|
+
* Fails silently — never blocks the caller.
|
|
6
|
+
*/
|
|
7
|
+
export declare function refreshModelCache(): Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Get a model definition, checking: static registry -> cache -> inference fallback.
|
|
10
|
+
* This is the primary entry point for looking up model definitions.
|
|
11
|
+
*/
|
|
12
|
+
export declare function getCachedModelDefinition(modelId: string): ModelDefinition;
|
|
13
|
+
/**
|
|
14
|
+
* Get all known models from both static registry and cache.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getAllCachedModels(): Record<Provider, Record<string, ModelDefinition>>;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import { MODEL_REGISTRY, getModelDefinition as getStaticModelDefinition } from "./models.js";
|
|
5
|
+
const SMOLTBOT_DIR = path.join(os.homedir(), ".smoltbot");
|
|
6
|
+
const CACHE_FILE = path.join(SMOLTBOT_DIR, "models-cache.json");
|
|
7
|
+
const MODELS_URL = "https://gateway.mnemom.ai/models.json";
|
|
8
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
9
|
+
/**
|
|
10
|
+
* Load the cached model registry from disk.
|
|
11
|
+
* Returns null if cache doesn't exist or is expired.
|
|
12
|
+
*/
|
|
13
|
+
function loadCache() {
|
|
14
|
+
if (!fs.existsSync(CACHE_FILE)) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const content = fs.readFileSync(CACHE_FILE, "utf-8");
|
|
19
|
+
const cache = JSON.parse(content);
|
|
20
|
+
// Check TTL
|
|
21
|
+
const fetchedAt = new Date(cache.fetchedAt).getTime();
|
|
22
|
+
if (Date.now() - fetchedAt > CACHE_TTL_MS) {
|
|
23
|
+
return null; // Expired
|
|
24
|
+
}
|
|
25
|
+
return cache;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Save model registry to disk cache.
|
|
33
|
+
*/
|
|
34
|
+
function saveCache(models) {
|
|
35
|
+
if (!models || typeof models !== "object") {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
// Ensure directory exists
|
|
39
|
+
if (!fs.existsSync(SMOLTBOT_DIR)) {
|
|
40
|
+
fs.mkdirSync(SMOLTBOT_DIR, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
// Validate write path stays within expected directory
|
|
43
|
+
const resolvedCachePath = path.resolve(CACHE_FILE);
|
|
44
|
+
if (!resolvedCachePath.startsWith(path.resolve(SMOLTBOT_DIR))) {
|
|
45
|
+
throw new Error("Cache file path escapes expected directory");
|
|
46
|
+
}
|
|
47
|
+
// Re-serialize HTTP-sourced data to sanitize before writing to disk
|
|
48
|
+
const sanitizedModels = JSON.parse(JSON.stringify(models));
|
|
49
|
+
const cache = {
|
|
50
|
+
fetchedAt: new Date().toISOString(),
|
|
51
|
+
models: sanitizedModels,
|
|
52
|
+
};
|
|
53
|
+
// Atomic write: temp file + rename to prevent corruption
|
|
54
|
+
const tmpFile = `${resolvedCachePath}.${process.pid}.tmp`;
|
|
55
|
+
fs.writeFileSync(tmpFile, JSON.stringify(cache, null, 2));
|
|
56
|
+
fs.renameSync(tmpFile, resolvedCachePath);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Fetch fresh model registry from the gateway.
|
|
60
|
+
* Fails silently on network errors — returns null.
|
|
61
|
+
*/
|
|
62
|
+
async function fetchRemoteModels() {
|
|
63
|
+
try {
|
|
64
|
+
const response = await fetch(MODELS_URL, {
|
|
65
|
+
signal: AbortSignal.timeout(5000),
|
|
66
|
+
});
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const data = (await response.json());
|
|
71
|
+
// Basic validation
|
|
72
|
+
if (!data.anthropic && !data.openai && !data.gemini) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return data;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Refresh the model cache in the background.
|
|
83
|
+
* Fetches from the gateway and saves to disk.
|
|
84
|
+
* Fails silently — never blocks the caller.
|
|
85
|
+
*/
|
|
86
|
+
export async function refreshModelCache() {
|
|
87
|
+
const remote = await fetchRemoteModels();
|
|
88
|
+
if (remote) {
|
|
89
|
+
saveCache(remote);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get a model definition, checking: static registry -> cache -> inference fallback.
|
|
94
|
+
* This is the primary entry point for looking up model definitions.
|
|
95
|
+
*/
|
|
96
|
+
export function getCachedModelDefinition(modelId) {
|
|
97
|
+
// 1. Check static registry first (always up to date with code)
|
|
98
|
+
for (const provider of Object.values(MODEL_REGISTRY)) {
|
|
99
|
+
const known = provider[modelId];
|
|
100
|
+
if (known)
|
|
101
|
+
return known;
|
|
102
|
+
}
|
|
103
|
+
// 2. Check disk cache
|
|
104
|
+
const cache = loadCache();
|
|
105
|
+
if (cache) {
|
|
106
|
+
for (const provider of Object.values(cache.models)) {
|
|
107
|
+
const cached = provider[modelId];
|
|
108
|
+
if (cached)
|
|
109
|
+
return cached;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// 3. Fall back to inference (same as static getModelDefinition)
|
|
113
|
+
return getStaticModelDefinition(modelId);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Get all known models from both static registry and cache.
|
|
117
|
+
*/
|
|
118
|
+
export function getAllCachedModels() {
|
|
119
|
+
const result = {
|
|
120
|
+
anthropic: { ...MODEL_REGISTRY.anthropic },
|
|
121
|
+
openai: { ...MODEL_REGISTRY.openai },
|
|
122
|
+
gemini: { ...MODEL_REGISTRY.gemini },
|
|
123
|
+
};
|
|
124
|
+
// Merge cache (cache entries don't override static entries)
|
|
125
|
+
const cache = loadCache();
|
|
126
|
+
if (cache) {
|
|
127
|
+
for (const [provider, models] of Object.entries(cache.models)) {
|
|
128
|
+
if (!result[provider])
|
|
129
|
+
continue;
|
|
130
|
+
for (const [id, model] of Object.entries(models)) {
|
|
131
|
+
if (!(id in result[provider])) {
|
|
132
|
+
result[provider][id] = model;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ModelDefinition, Provider } from "./openclaw.js";
|
|
2
|
+
/**
|
|
3
|
+
* Multi-provider model registry.
|
|
4
|
+
* Focuses on top-tier reasoning models that OpenClaws use as substrates.
|
|
5
|
+
*/
|
|
6
|
+
export declare const MODEL_REGISTRY: Record<Provider, Record<string, ModelDefinition>>;
|
|
7
|
+
/**
|
|
8
|
+
* Backward-compatible re-export of Anthropic models.
|
|
9
|
+
*/
|
|
10
|
+
export declare const ANTHROPIC_MODELS: Record<string, ModelDefinition>;
|
|
11
|
+
/**
|
|
12
|
+
* Detect provider from a model ID string.
|
|
13
|
+
*/
|
|
14
|
+
export declare function detectProvider(modelId: string): Provider | null;
|
|
15
|
+
/**
|
|
16
|
+
* Get model definition by ID — searches all providers.
|
|
17
|
+
* Returns the definition if known, or creates a basic one if unknown.
|
|
18
|
+
*/
|
|
19
|
+
export declare function getModelDefinition(modelId: string): ModelDefinition;
|
|
20
|
+
/**
|
|
21
|
+
* Check if a model ID is a known model (any provider).
|
|
22
|
+
*/
|
|
23
|
+
export declare function isKnownModel(modelId: string): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Check if a model ID looks like an Anthropic model.
|
|
26
|
+
*/
|
|
27
|
+
export declare function isAnthropicModel(modelId: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Format a model ID into a human-readable name.
|
|
30
|
+
* Handles Anthropic, OpenAI, and Gemini model ID formats.
|
|
31
|
+
*/
|
|
32
|
+
export declare function formatModelName(modelId: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Get all known model IDs across all providers.
|
|
35
|
+
*/
|
|
36
|
+
export declare function getAllKnownModelIds(): string[];
|
|
37
|
+
/**
|
|
38
|
+
* Get the latest models per provider.
|
|
39
|
+
* Returns { anthropic: [...], openai: [...], gemini: [...] }
|
|
40
|
+
*/
|
|
41
|
+
export declare function getLatestModels(): Record<Provider, ModelDefinition[]>;
|