@mnemom/smoltbot 2.0.1

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.
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Known Anthropic model definitions with their specifications.
3
+ * These are used when configuring the smoltbot provider.
4
+ */
5
+ export const ANTHROPIC_MODELS = {
6
+ // Claude Opus 4.5 (latest flagship)
7
+ "claude-opus-4-5-20251101": {
8
+ id: "claude-opus-4-5-20251101",
9
+ name: "Claude Opus 4.5",
10
+ reasoning: true,
11
+ input: ["text", "image"],
12
+ contextWindow: 200000,
13
+ maxTokens: 64000,
14
+ cost: {
15
+ input: 5,
16
+ output: 25,
17
+ cacheRead: 0.5,
18
+ cacheWrite: 6.25,
19
+ },
20
+ },
21
+ // Claude Sonnet 4.5
22
+ "claude-sonnet-4-5-20250929": {
23
+ id: "claude-sonnet-4-5-20250929",
24
+ name: "Claude Sonnet 4.5",
25
+ reasoning: true,
26
+ input: ["text", "image"],
27
+ contextWindow: 200000,
28
+ maxTokens: 64000,
29
+ cost: {
30
+ input: 3,
31
+ output: 15,
32
+ cacheRead: 0.3,
33
+ cacheWrite: 3.75,
34
+ },
35
+ },
36
+ // Claude Haiku 4.5
37
+ "claude-haiku-4-5-20251001": {
38
+ id: "claude-haiku-4-5-20251001",
39
+ name: "Claude Haiku 4.5",
40
+ reasoning: false,
41
+ input: ["text", "image"],
42
+ contextWindow: 200000,
43
+ maxTokens: 64000,
44
+ cost: {
45
+ input: 0.8,
46
+ output: 4,
47
+ cacheRead: 0.08,
48
+ cacheWrite: 1,
49
+ },
50
+ },
51
+ // Legacy models (Claude 3.5)
52
+ "claude-3-5-sonnet-20241022": {
53
+ id: "claude-3-5-sonnet-20241022",
54
+ name: "Claude 3.5 Sonnet",
55
+ reasoning: false,
56
+ input: ["text", "image"],
57
+ contextWindow: 200000,
58
+ maxTokens: 8192,
59
+ cost: {
60
+ input: 3,
61
+ output: 15,
62
+ cacheRead: 0.3,
63
+ cacheWrite: 3.75,
64
+ },
65
+ },
66
+ "claude-3-5-haiku-20241022": {
67
+ id: "claude-3-5-haiku-20241022",
68
+ name: "Claude 3.5 Haiku",
69
+ reasoning: false,
70
+ input: ["text", "image"],
71
+ contextWindow: 200000,
72
+ maxTokens: 8192,
73
+ cost: {
74
+ input: 0.8,
75
+ output: 4,
76
+ cacheRead: 0.08,
77
+ cacheWrite: 1,
78
+ },
79
+ },
80
+ // Claude 3 Opus (legacy)
81
+ "claude-3-opus-20240229": {
82
+ id: "claude-3-opus-20240229",
83
+ name: "Claude 3 Opus",
84
+ reasoning: false,
85
+ input: ["text", "image"],
86
+ contextWindow: 200000,
87
+ maxTokens: 4096,
88
+ cost: {
89
+ input: 15,
90
+ output: 75,
91
+ cacheRead: 1.5,
92
+ cacheWrite: 18.75,
93
+ },
94
+ },
95
+ "claude-3-haiku-20240307": {
96
+ id: "claude-3-haiku-20240307",
97
+ name: "Claude 3 Haiku",
98
+ reasoning: false,
99
+ input: ["text", "image"],
100
+ contextWindow: 200000,
101
+ maxTokens: 4096,
102
+ cost: {
103
+ input: 0.25,
104
+ output: 1.25,
105
+ cacheRead: 0.03,
106
+ cacheWrite: 0.3,
107
+ },
108
+ },
109
+ };
110
+ /**
111
+ * Get model definition by ID
112
+ * Returns the definition if known, or creates a basic one if unknown
113
+ */
114
+ export function getModelDefinition(modelId) {
115
+ const known = ANTHROPIC_MODELS[modelId];
116
+ if (known) {
117
+ return known;
118
+ }
119
+ // Create a basic definition for unknown models
120
+ // This allows smoltbot to work with new models not yet in our registry
121
+ return {
122
+ id: modelId,
123
+ name: formatModelName(modelId),
124
+ reasoning: modelId.includes("opus") || modelId.includes("sonnet"),
125
+ input: ["text", "image"],
126
+ contextWindow: 200000,
127
+ maxTokens: 64000,
128
+ };
129
+ }
130
+ /**
131
+ * Check if a model ID is a known Anthropic model
132
+ */
133
+ export function isKnownModel(modelId) {
134
+ return modelId in ANTHROPIC_MODELS;
135
+ }
136
+ /**
137
+ * Check if a model ID looks like an Anthropic model
138
+ */
139
+ export function isAnthropicModel(modelId) {
140
+ return modelId.startsWith("claude-");
141
+ }
142
+ /**
143
+ * Format a model ID into a human-readable name
144
+ * e.g., "claude-opus-4-5-20251101" -> "Claude Opus 4.5"
145
+ */
146
+ export function formatModelName(modelId) {
147
+ // First check known models
148
+ const known = ANTHROPIC_MODELS[modelId];
149
+ if (known) {
150
+ return known.name;
151
+ }
152
+ // Try to parse the model ID
153
+ // Pattern: claude-{tier}-{version}-{date}
154
+ // e.g., claude-opus-4-5-20251101
155
+ const parts = modelId.split("-");
156
+ if (parts[0] !== "claude" || parts.length < 3) {
157
+ return modelId; // Return as-is if we can't parse
158
+ }
159
+ const tier = parts[1]; // opus, sonnet, haiku
160
+ const tierCapitalized = tier.charAt(0).toUpperCase() + tier.slice(1);
161
+ // Try to extract version (e.g., "4-5" -> "4.5")
162
+ if (parts.length >= 4 && /^\d+$/.test(parts[2]) && /^\d+$/.test(parts[3])) {
163
+ return `Claude ${tierCapitalized} ${parts[2]}.${parts[3]}`;
164
+ }
165
+ // Fallback for older naming (e.g., claude-3-opus-20240229)
166
+ if (/^\d+$/.test(parts[1])) {
167
+ const version = parts[1];
168
+ const tierName = parts[2];
169
+ const tierCap = tierName.charAt(0).toUpperCase() + tierName.slice(1);
170
+ return `Claude ${version} ${tierCap}`;
171
+ }
172
+ return `Claude ${tierCapitalized}`;
173
+ }
174
+ /**
175
+ * Get all known model IDs
176
+ */
177
+ export function getAllKnownModelIds() {
178
+ return Object.keys(ANTHROPIC_MODELS);
179
+ }
180
+ /**
181
+ * Get the latest model for each tier
182
+ */
183
+ export function getLatestModels() {
184
+ return [
185
+ ANTHROPIC_MODELS["claude-opus-4-5-20251101"],
186
+ ANTHROPIC_MODELS["claude-sonnet-4-5-20250929"],
187
+ ANTHROPIC_MODELS["claude-haiku-4-5-20251001"],
188
+ ];
189
+ }
@@ -0,0 +1,131 @@
1
+ export declare const OPENCLAW_DIR: string;
2
+ export declare const OPENCLAW_CONFIG_FILE: string;
3
+ export declare const AUTH_PROFILES_FILE: string;
4
+ export interface AuthProfile {
5
+ type: "api_key" | "oauth";
6
+ provider: string;
7
+ key?: string;
8
+ }
9
+ export interface AuthProfilesFile {
10
+ version: number;
11
+ profiles: Record<string, AuthProfile>;
12
+ lastGood?: Record<string, string>;
13
+ usageStats?: Record<string, unknown>;
14
+ }
15
+ export interface ModelDefinition {
16
+ id: string;
17
+ name: string;
18
+ reasoning?: boolean;
19
+ input?: string[];
20
+ contextWindow?: number;
21
+ maxTokens?: number;
22
+ cost?: {
23
+ input: number;
24
+ output: number;
25
+ cacheRead?: number;
26
+ cacheWrite?: number;
27
+ };
28
+ }
29
+ export interface SmoltbotProvider {
30
+ baseUrl: string;
31
+ apiKey: string;
32
+ api: string;
33
+ models: ModelDefinition[];
34
+ }
35
+ export interface OpenClawConfig {
36
+ meta?: {
37
+ lastTouchedVersion?: string;
38
+ lastTouchedAt?: string;
39
+ };
40
+ wizard?: Record<string, unknown>;
41
+ update?: Record<string, unknown>;
42
+ auth?: {
43
+ profiles?: Record<string, {
44
+ provider: string;
45
+ mode: string;
46
+ }>;
47
+ };
48
+ models?: {
49
+ mode?: string;
50
+ providers?: Record<string, SmoltbotProvider>;
51
+ };
52
+ agents?: {
53
+ defaults?: {
54
+ workspace?: string;
55
+ compaction?: Record<string, unknown>;
56
+ maxConcurrent?: number;
57
+ subagents?: Record<string, unknown>;
58
+ model?: {
59
+ primary?: string;
60
+ };
61
+ models?: Record<string, unknown>;
62
+ };
63
+ };
64
+ messages?: Record<string, unknown>;
65
+ commands?: Record<string, unknown>;
66
+ gateway?: Record<string, unknown>;
67
+ [key: string]: unknown;
68
+ }
69
+ export interface OpenClawDetectionResult {
70
+ installed: boolean;
71
+ hasApiKey: boolean;
72
+ isOAuth: boolean;
73
+ apiKey?: string;
74
+ currentModel?: string;
75
+ currentModelId?: string;
76
+ currentProvider?: string;
77
+ smoltbotAlreadyConfigured: boolean;
78
+ error?: string;
79
+ }
80
+ /**
81
+ * Check if OpenClaw is installed
82
+ */
83
+ export declare function openclawExists(): boolean;
84
+ /**
85
+ * Load and parse auth-profiles.json
86
+ */
87
+ export declare function loadAuthProfiles(): AuthProfilesFile | null;
88
+ /**
89
+ * Get the Anthropic API key from auth-profiles.json
90
+ */
91
+ export declare function getAnthropicApiKey(): {
92
+ key: string | null;
93
+ isOAuth: boolean;
94
+ };
95
+ /**
96
+ * Load openclaw.json config
97
+ */
98
+ export declare function loadOpenClawConfig(): OpenClawConfig | null;
99
+ /**
100
+ * Save openclaw.json config (preserves all existing fields)
101
+ */
102
+ export declare function saveOpenClawConfig(config: OpenClawConfig): void;
103
+ /**
104
+ * Get the current default model from OpenClaw config
105
+ * Returns both the full model path (provider/model) and parsed parts
106
+ */
107
+ export declare function getCurrentModel(): {
108
+ fullPath: string | null;
109
+ provider: string | null;
110
+ modelId: string | null;
111
+ };
112
+ /**
113
+ * Check if smoltbot provider is already configured
114
+ */
115
+ export declare function isSmoltbotConfigured(): boolean;
116
+ /**
117
+ * Get the existing smoltbot provider config
118
+ */
119
+ export declare function getSmoltbotProvider(): SmoltbotProvider | null;
120
+ /**
121
+ * Comprehensive detection of OpenClaw setup
122
+ */
123
+ export declare function detectOpenClaw(): OpenClawDetectionResult;
124
+ /**
125
+ * Configure the smoltbot provider in OpenClaw config
126
+ */
127
+ export declare function configureSmoltbotProvider(apiKey: string, models: ModelDefinition[]): void;
128
+ /**
129
+ * Set the default model in OpenClaw config
130
+ */
131
+ export declare function setDefaultModel(modelPath: string): void;
@@ -0,0 +1,219 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import * as os from "node:os";
4
+ // OpenClaw paths
5
+ export const OPENCLAW_DIR = path.join(os.homedir(), ".openclaw");
6
+ export const OPENCLAW_CONFIG_FILE = path.join(OPENCLAW_DIR, "openclaw.json");
7
+ export const AUTH_PROFILES_FILE = path.join(OPENCLAW_DIR, "agents", "main", "agent", "auth-profiles.json");
8
+ /**
9
+ * Check if OpenClaw is installed
10
+ */
11
+ export function openclawExists() {
12
+ return fs.existsSync(OPENCLAW_DIR) && fs.existsSync(OPENCLAW_CONFIG_FILE);
13
+ }
14
+ /**
15
+ * Load and parse auth-profiles.json
16
+ */
17
+ export function loadAuthProfiles() {
18
+ if (!fs.existsSync(AUTH_PROFILES_FILE)) {
19
+ return null;
20
+ }
21
+ try {
22
+ const content = fs.readFileSync(AUTH_PROFILES_FILE, "utf-8");
23
+ return JSON.parse(content);
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ /**
30
+ * Get the Anthropic API key from auth-profiles.json
31
+ */
32
+ export function getAnthropicApiKey() {
33
+ const profiles = loadAuthProfiles();
34
+ if (!profiles) {
35
+ return { key: null, isOAuth: false };
36
+ }
37
+ // Look for anthropic:default or any anthropic profile
38
+ const anthropicProfile = profiles.profiles["anthropic:default"] ||
39
+ Object.values(profiles.profiles).find((p) => p.provider === "anthropic");
40
+ if (!anthropicProfile) {
41
+ return { key: null, isOAuth: false };
42
+ }
43
+ if (anthropicProfile.type === "oauth" || !anthropicProfile.key) {
44
+ return { key: null, isOAuth: true };
45
+ }
46
+ return { key: anthropicProfile.key, isOAuth: false };
47
+ }
48
+ /**
49
+ * Load openclaw.json config
50
+ */
51
+ export function loadOpenClawConfig() {
52
+ if (!fs.existsSync(OPENCLAW_CONFIG_FILE)) {
53
+ return null;
54
+ }
55
+ try {
56
+ const content = fs.readFileSync(OPENCLAW_CONFIG_FILE, "utf-8");
57
+ return JSON.parse(content);
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
63
+ /**
64
+ * Save openclaw.json config (preserves all existing fields)
65
+ */
66
+ export function saveOpenClawConfig(config) {
67
+ // Update meta timestamp
68
+ if (!config.meta) {
69
+ config.meta = {};
70
+ }
71
+ config.meta.lastTouchedAt = new Date().toISOString();
72
+ fs.writeFileSync(OPENCLAW_CONFIG_FILE, JSON.stringify(config, null, 2));
73
+ }
74
+ /**
75
+ * Get the current default model from OpenClaw config
76
+ * Returns both the full model path (provider/model) and parsed parts
77
+ */
78
+ export function getCurrentModel() {
79
+ const config = loadOpenClawConfig();
80
+ if (!config) {
81
+ return { fullPath: null, provider: null, modelId: null };
82
+ }
83
+ const primary = config.agents?.defaults?.model?.primary;
84
+ if (!primary) {
85
+ return { fullPath: null, provider: null, modelId: null };
86
+ }
87
+ // Parse provider/model format (e.g., "anthropic/claude-opus-4-5-20251101")
88
+ const parts = primary.split("/");
89
+ if (parts.length === 2) {
90
+ return {
91
+ fullPath: primary,
92
+ provider: parts[0],
93
+ modelId: parts[1],
94
+ };
95
+ }
96
+ // No provider prefix, assume it's just the model ID
97
+ return {
98
+ fullPath: primary,
99
+ provider: null,
100
+ modelId: primary,
101
+ };
102
+ }
103
+ /**
104
+ * Check if smoltbot provider is already configured
105
+ */
106
+ export function isSmoltbotConfigured() {
107
+ const config = loadOpenClawConfig();
108
+ return !!config?.models?.providers?.smoltbot;
109
+ }
110
+ /**
111
+ * Get the existing smoltbot provider config
112
+ */
113
+ export function getSmoltbotProvider() {
114
+ const config = loadOpenClawConfig();
115
+ return config?.models?.providers?.smoltbot || null;
116
+ }
117
+ /**
118
+ * Comprehensive detection of OpenClaw setup
119
+ */
120
+ export function detectOpenClaw() {
121
+ // Check if OpenClaw is installed
122
+ if (!openclawExists()) {
123
+ return {
124
+ installed: false,
125
+ hasApiKey: false,
126
+ isOAuth: false,
127
+ smoltbotAlreadyConfigured: false,
128
+ error: "OpenClaw is not installed. Install from https://openclaw.ai",
129
+ };
130
+ }
131
+ // Check auth profile
132
+ const { key, isOAuth } = getAnthropicApiKey();
133
+ if (isOAuth) {
134
+ return {
135
+ installed: true,
136
+ hasApiKey: false,
137
+ isOAuth: true,
138
+ smoltbotAlreadyConfigured: isSmoltbotConfigured(),
139
+ error: "OAuth authentication detected. smoltbot only supports API key authentication.\n" +
140
+ "To use smoltbot, add an API key to your Anthropic auth profile.",
141
+ };
142
+ }
143
+ if (!key) {
144
+ return {
145
+ installed: true,
146
+ hasApiKey: false,
147
+ isOAuth: false,
148
+ smoltbotAlreadyConfigured: isSmoltbotConfigured(),
149
+ error: "No Anthropic API key found in auth-profiles.json.\n" +
150
+ "Run `openclaw auth` to configure your API key.",
151
+ };
152
+ }
153
+ // Get current model
154
+ const { fullPath, provider, modelId } = getCurrentModel();
155
+ return {
156
+ installed: true,
157
+ hasApiKey: true,
158
+ isOAuth: false,
159
+ apiKey: key,
160
+ currentModel: fullPath || undefined,
161
+ currentModelId: modelId || undefined,
162
+ currentProvider: provider || undefined,
163
+ smoltbotAlreadyConfigured: isSmoltbotConfigured(),
164
+ };
165
+ }
166
+ /**
167
+ * Configure the smoltbot provider in OpenClaw config
168
+ */
169
+ export function configureSmoltbotProvider(apiKey, models) {
170
+ const config = loadOpenClawConfig();
171
+ if (!config) {
172
+ throw new Error("Could not load OpenClaw config");
173
+ }
174
+ // Ensure models section exists
175
+ if (!config.models) {
176
+ config.models = {};
177
+ }
178
+ if (!config.models.providers) {
179
+ config.models.providers = {};
180
+ }
181
+ // Set mode to merge if not set
182
+ if (!config.models.mode) {
183
+ config.models.mode = "merge";
184
+ }
185
+ // Configure smoltbot provider
186
+ config.models.providers.smoltbot = {
187
+ baseUrl: "https://gateway.mnemom.ai/anthropic",
188
+ apiKey: apiKey,
189
+ api: "anthropic-messages",
190
+ models: models,
191
+ };
192
+ saveOpenClawConfig(config);
193
+ }
194
+ /**
195
+ * Set the default model in OpenClaw config
196
+ */
197
+ export function setDefaultModel(modelPath) {
198
+ const config = loadOpenClawConfig();
199
+ if (!config) {
200
+ throw new Error("Could not load OpenClaw config");
201
+ }
202
+ // Ensure agents.defaults.model section exists
203
+ if (!config.agents) {
204
+ config.agents = {};
205
+ }
206
+ if (!config.agents.defaults) {
207
+ config.agents.defaults = {};
208
+ }
209
+ if (!config.agents.defaults.model) {
210
+ config.agents.defaults.model = {};
211
+ }
212
+ // Also add to models map if not present
213
+ if (!config.agents.defaults.models) {
214
+ config.agents.defaults.models = {};
215
+ }
216
+ config.agents.defaults.model.primary = modelPath;
217
+ config.agents.defaults.models[modelPath] = {};
218
+ saveOpenClawConfig(config);
219
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Ask a yes/no question with a default answer
3
+ * @param question The question to ask
4
+ * @param defaultYes If true, default is Yes (Y/n), otherwise default is No (y/N)
5
+ * @returns Promise<boolean> - true for yes, false for no
6
+ */
7
+ export declare function askYesNo(question: string, defaultYes?: boolean): Promise<boolean>;
8
+ /**
9
+ * Check if running in non-interactive mode (no TTY)
10
+ */
11
+ export declare function isInteractive(): boolean;
@@ -0,0 +1,40 @@
1
+ import * as readline from "node:readline";
2
+ /**
3
+ * Ask a yes/no question with a default answer
4
+ * @param question The question to ask
5
+ * @param defaultYes If true, default is Yes (Y/n), otherwise default is No (y/N)
6
+ * @returns Promise<boolean> - true for yes, false for no
7
+ */
8
+ export async function askYesNo(question, defaultYes = true) {
9
+ const suffix = defaultYes ? "[Y/n]" : "[y/N]";
10
+ const rl = readline.createInterface({
11
+ input: process.stdin,
12
+ output: process.stdout,
13
+ });
14
+ return new Promise((resolve) => {
15
+ rl.question(`${question} ${suffix} `, (answer) => {
16
+ rl.close();
17
+ const normalized = answer.trim().toLowerCase();
18
+ if (normalized === "") {
19
+ // Use default
20
+ resolve(defaultYes);
21
+ }
22
+ else if (normalized === "y" || normalized === "yes") {
23
+ resolve(true);
24
+ }
25
+ else if (normalized === "n" || normalized === "no") {
26
+ resolve(false);
27
+ }
28
+ else {
29
+ // Invalid input, use default
30
+ resolve(defaultYes);
31
+ }
32
+ });
33
+ });
34
+ }
35
+ /**
36
+ * Check if running in non-interactive mode (no TTY)
37
+ */
38
+ export function isInteractive() {
39
+ return process.stdin.isTTY === true;
40
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@mnemom/smoltbot",
3
+ "version": "2.0.1",
4
+ "description": "Transparent AI agent tracing - AAP compliant",
5
+ "type": "module",
6
+ "bin": {
7
+ "smoltbot": "./dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "tsx src/index.ts",
12
+ "test": "vitest"
13
+ },
14
+ "dependencies": {
15
+ "commander": "^12.0.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^20.10.0",
19
+ "tsx": "^4.7.0",
20
+ "typescript": "^5.3.3",
21
+ "vitest": "^1.2.0"
22
+ },
23
+ "files": ["dist", "!dist/__tests__", "README.md", "LICENSE"],
24
+ "keywords": ["ai", "agent", "transparency", "aap", "alignment", "tracing", "observability"],
25
+ "author": "Mnemom.ai",
26
+ "license": "Apache-2.0",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/mnemom/smoltbot"
30
+ },
31
+ "homepage": "https://mnemom.ai",
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "engines": {
36
+ "node": ">=18.0.0"
37
+ }
38
+ }