@yeaft/webchat-agent 0.1.412 → 0.1.414
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/connection/message-router.js +14 -0
- package/package.json +3 -2
- package/service/index.js +10 -0
- package/unify/cli.js +208 -106
- package/unify/config-api.js +117 -0
- package/unify/config.js +174 -181
- package/unify/engine.js +22 -4
- package/unify/index.js +2 -1
- package/unify/init.js +51 -54
- package/unify/llm/adapter.js +12 -0
- package/unify/llm/router.js +148 -0
- package/unify/models.js +46 -2
- package/unify/session.js +11 -6
- package/unify/stop-hooks.js +6 -1
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config-api.js — Read/write Yeaft config.json for remote management
|
|
3
|
+
*
|
|
4
|
+
* Provides functions to get and update the LLM-related portion of
|
|
5
|
+
* ~/.yeaft/config.json via WebSocket messages from the web UI.
|
|
6
|
+
*
|
|
7
|
+
* Only exposes provider/model configuration — not internal fields
|
|
8
|
+
* like maxContinueTurns or debug that don't belong in the UI.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
import { DEFAULT_YEAFT_DIR } from './init.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Read the LLM-relevant portion of config.json.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} [dir] — Yeaft data directory
|
|
19
|
+
* @returns {{ providers, primaryModel, fastModel, language } | { error: string }}
|
|
20
|
+
*/
|
|
21
|
+
export function getLlmConfig(dir) {
|
|
22
|
+
const root = dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
|
|
23
|
+
const configPath = join(root, 'config.json');
|
|
24
|
+
|
|
25
|
+
if (!existsSync(configPath)) {
|
|
26
|
+
return { providers: [], primaryModel: null, fastModel: null, language: 'en', needsSetup: true };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const raw = readFileSync(configPath, 'utf8');
|
|
31
|
+
const json = JSON.parse(raw);
|
|
32
|
+
const providers = Array.isArray(json.providers) ? json.providers : [];
|
|
33
|
+
|
|
34
|
+
// Detect if config still has default/placeholder values (first-time setup needed)
|
|
35
|
+
const needsSetup = providers.length === 0 || providers.every(p =>
|
|
36
|
+
p.apiKey === 'proxy' || p.apiKey === '' || !p.apiKey
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
providers,
|
|
41
|
+
primaryModel: json.primaryModel || null,
|
|
42
|
+
fastModel: json.fastModel || null,
|
|
43
|
+
language: json.language || 'en',
|
|
44
|
+
needsSetup,
|
|
45
|
+
};
|
|
46
|
+
} catch (e) {
|
|
47
|
+
return { error: `Failed to read config.json: ${e.message}` };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Update the LLM-relevant portion of config.json.
|
|
53
|
+
* Merges into existing config — preserves fields like debug, maxContextTokens, etc.
|
|
54
|
+
*
|
|
55
|
+
* @param {object} update — { providers?, primaryModel?, fastModel?, language? }
|
|
56
|
+
* @param {string} [dir] — Yeaft data directory
|
|
57
|
+
* @returns {{ providers, primaryModel, fastModel, language } | { error: string }}
|
|
58
|
+
*/
|
|
59
|
+
export function updateLlmConfig(update, dir) {
|
|
60
|
+
const root = dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
|
|
61
|
+
const configPath = join(root, 'config.json');
|
|
62
|
+
|
|
63
|
+
// Read existing config (preserve non-LLM fields)
|
|
64
|
+
let existing = {};
|
|
65
|
+
if (existsSync(configPath)) {
|
|
66
|
+
try {
|
|
67
|
+
existing = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
68
|
+
} catch {
|
|
69
|
+
// Start fresh if corrupt
|
|
70
|
+
existing = {};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Validate providers structure
|
|
75
|
+
if (update.providers !== undefined) {
|
|
76
|
+
if (!Array.isArray(update.providers)) {
|
|
77
|
+
return { error: 'providers must be an array' };
|
|
78
|
+
}
|
|
79
|
+
for (const p of update.providers) {
|
|
80
|
+
if (!p.name || typeof p.name !== 'string') {
|
|
81
|
+
return { error: 'Each provider must have a name' };
|
|
82
|
+
}
|
|
83
|
+
if (!p.baseUrl || typeof p.baseUrl !== 'string') {
|
|
84
|
+
return { error: `Provider "${p.name}" must have a baseUrl` };
|
|
85
|
+
}
|
|
86
|
+
if (!Array.isArray(p.models) || p.models.length === 0) {
|
|
87
|
+
return { error: `Provider "${p.name}" must have at least one model` };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
existing.providers = update.providers;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Update model selections
|
|
94
|
+
if (update.primaryModel !== undefined) {
|
|
95
|
+
existing.primaryModel = update.primaryModel || null;
|
|
96
|
+
}
|
|
97
|
+
if (update.fastModel !== undefined) {
|
|
98
|
+
existing.fastModel = update.fastModel || null;
|
|
99
|
+
}
|
|
100
|
+
if (update.language !== undefined) {
|
|
101
|
+
existing.language = update.language;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Write back
|
|
105
|
+
try {
|
|
106
|
+
writeFileSync(configPath, JSON.stringify(existing, null, 2) + '\n', 'utf8');
|
|
107
|
+
} catch (e) {
|
|
108
|
+
return { error: `Failed to write config.json: ${e.message}` };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
providers: Array.isArray(existing.providers) ? existing.providers : [],
|
|
113
|
+
primaryModel: existing.primaryModel || null,
|
|
114
|
+
fastModel: existing.fastModel || null,
|
|
115
|
+
language: existing.language || 'en',
|
|
116
|
+
};
|
|
117
|
+
}
|
package/unify/config.js
CHANGED
|
@@ -1,38 +1,66 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* config.js — Yeaft configuration management
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* All configuration lives in ~/.yeaft/config.json:
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* {
|
|
7
|
+
* "providers": [
|
|
8
|
+
* { "name": "my-proxy", "baseUrl": "http://localhost:6628/v1", "apiKey": "proxy",
|
|
9
|
+
* "models": ["claude-sonnet-4-20250514", "gpt-5", "deepseek-chat"] }
|
|
10
|
+
* ],
|
|
11
|
+
* "primaryModel": "my-proxy/claude-sonnet-4-20250514",
|
|
12
|
+
* "fastModel": "my-proxy/claude-haiku-3-20250414",
|
|
13
|
+
* "language": "en",
|
|
14
|
+
* "debug": false,
|
|
15
|
+
* "maxContextTokens": 200000,
|
|
16
|
+
* "messageTokenBudget": 8192
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* Legacy: if config.json doesn't exist, falls back to old config.md + .env for migration.
|
|
8
20
|
*/
|
|
9
21
|
|
|
10
22
|
import { existsSync, readFileSync } from 'fs';
|
|
11
23
|
import { join } from 'path';
|
|
12
24
|
import { DEFAULT_YEAFT_DIR } from './init.js';
|
|
13
|
-
import { resolveModel } from './models.js';
|
|
25
|
+
import { resolveModel, parseModelRef } from './models.js';
|
|
14
26
|
|
|
15
27
|
/** Default configuration values. */
|
|
16
28
|
const DEFAULTS = {
|
|
17
|
-
|
|
18
|
-
fallbackModel: null,
|
|
19
|
-
language: 'en', // 'en' | 'zh'
|
|
20
|
-
apiKey: null,
|
|
21
|
-
openaiApiKey: null,
|
|
22
|
-
proxyUrl: 'http://localhost:6628',
|
|
23
|
-
baseUrl: null,
|
|
24
|
-
adapter: null, // auto-detect: 'anthropic' | 'openai' | 'proxy'
|
|
29
|
+
language: 'en',
|
|
25
30
|
debug: false,
|
|
26
31
|
dir: DEFAULT_YEAFT_DIR,
|
|
27
32
|
maxContextTokens: 200000,
|
|
28
33
|
maxOutputTokens: 16384,
|
|
29
|
-
messageTokenBudget: 8192,
|
|
30
|
-
maxContinueTurns: 3,
|
|
34
|
+
messageTokenBudget: 8192,
|
|
35
|
+
maxContinueTurns: 3,
|
|
31
36
|
};
|
|
32
37
|
|
|
38
|
+
// ─── config.json reader ─────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Read and parse ~/.yeaft/config.json.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} dir
|
|
44
|
+
* @returns {object|null} — Parsed JSON or null if not found/invalid
|
|
45
|
+
*/
|
|
46
|
+
function readConfigJson(dir) {
|
|
47
|
+
const configPath = join(dir, 'config.json');
|
|
48
|
+
if (!existsSync(configPath)) return null;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const raw = readFileSync(configPath, 'utf8');
|
|
52
|
+
return JSON.parse(raw);
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ─── Legacy support (config.md + .env) ──────────────────────────
|
|
59
|
+
|
|
33
60
|
/**
|
|
34
61
|
* Parse YAML frontmatter from a markdown file.
|
|
35
62
|
* Simple parser — handles key: value pairs, no nested objects.
|
|
63
|
+
* @deprecated Use config.json instead. Kept for migration.
|
|
36
64
|
*
|
|
37
65
|
* @param {string} content — File content
|
|
38
66
|
* @returns {Record<string, string>} — Parsed frontmatter
|
|
@@ -49,7 +77,6 @@ export function parseFrontmatter(content) {
|
|
|
49
77
|
if (colonIdx === -1) continue;
|
|
50
78
|
const key = trimmed.slice(0, colonIdx).trim();
|
|
51
79
|
let value = trimmed.slice(colonIdx + 1).trim();
|
|
52
|
-
// Parse booleans and numbers
|
|
53
80
|
if (value === 'true') value = true;
|
|
54
81
|
else if (value === 'false') value = false;
|
|
55
82
|
else if (value === 'null') value = null;
|
|
@@ -60,243 +87,209 @@ export function parseFrontmatter(content) {
|
|
|
60
87
|
}
|
|
61
88
|
|
|
62
89
|
/**
|
|
63
|
-
* Load
|
|
64
|
-
*
|
|
65
|
-
* @param {string} dir — Yeaft data directory
|
|
66
|
-
* @returns {Record<string, unknown>} — Config from file
|
|
90
|
+
* Load legacy config.md.
|
|
91
|
+
* @deprecated
|
|
67
92
|
*/
|
|
68
|
-
function
|
|
93
|
+
function loadLegacyConfigFile(dir) {
|
|
69
94
|
const configPath = join(dir, 'config.md');
|
|
70
95
|
if (!existsSync(configPath)) return {};
|
|
71
|
-
|
|
72
96
|
try {
|
|
73
|
-
|
|
74
|
-
return parseFrontmatter(content);
|
|
97
|
+
return parseFrontmatter(readFileSync(configPath, 'utf8'));
|
|
75
98
|
} catch {
|
|
76
99
|
return {};
|
|
77
100
|
}
|
|
78
101
|
}
|
|
79
102
|
|
|
80
103
|
/**
|
|
81
|
-
* Load .env file
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* ⚠️ Side-effect: mutates process.env globally. Values set by a previous
|
|
85
|
-
* call persist across subsequent loadConfig() calls within the same process.
|
|
86
|
-
* This is by design (matches dotenv behavior), but callers that need isolation
|
|
87
|
-
* (e.g. tests) must manually delete keys from process.env between calls.
|
|
88
|
-
*
|
|
89
|
-
* @param {string} dir — Directory containing .env file
|
|
104
|
+
* Load legacy .env file.
|
|
105
|
+
* @deprecated
|
|
90
106
|
*/
|
|
91
|
-
function
|
|
107
|
+
function loadLegacyEnvFile(dir) {
|
|
92
108
|
const envPath = join(dir, '.env');
|
|
93
109
|
if (!existsSync(envPath)) return;
|
|
94
|
-
|
|
95
110
|
try {
|
|
96
111
|
const content = readFileSync(envPath, 'utf8');
|
|
97
112
|
for (const line of content.split('\n')) {
|
|
98
113
|
let trimmed = line.trim();
|
|
99
|
-
// Skip empty lines and comments
|
|
100
114
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
101
|
-
// Strip optional 'export ' prefix (common in .env files)
|
|
102
115
|
if (trimmed.startsWith('export ')) trimmed = trimmed.slice(7);
|
|
103
116
|
const eqIdx = trimmed.indexOf('=');
|
|
104
117
|
if (eqIdx === -1) continue;
|
|
105
|
-
|
|
106
118
|
const key = trimmed.slice(0, eqIdx).trim();
|
|
107
119
|
let value = trimmed.slice(eqIdx + 1).trim();
|
|
108
|
-
// Remove surrounding quotes if present
|
|
109
120
|
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
110
121
|
(value.startsWith("'") && value.endsWith("'"))) {
|
|
111
122
|
value = value.slice(1, -1);
|
|
112
123
|
}
|
|
113
|
-
|
|
114
|
-
// Only set if not already defined (shell env takes precedence)
|
|
115
124
|
if (process.env[key] === undefined) {
|
|
116
125
|
process.env[key] = value;
|
|
117
126
|
}
|
|
118
127
|
}
|
|
119
128
|
} catch {
|
|
120
|
-
// Silently ignore
|
|
129
|
+
// Silently ignore
|
|
121
130
|
}
|
|
122
131
|
}
|
|
123
132
|
|
|
124
|
-
/**
|
|
125
|
-
* Helper: check if an env var is truthy.
|
|
126
|
-
* @param {string|undefined} val
|
|
127
|
-
* @returns {boolean}
|
|
128
|
-
*/
|
|
129
133
|
function isTruthy(val) {
|
|
130
134
|
return val === '1' || val === 'true' || val === 'yes';
|
|
131
135
|
}
|
|
132
136
|
|
|
133
137
|
/**
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
* @param {Record<string, unknown>} [overrides] — CLI overrides
|
|
137
|
-
* @returns {object} — Merged configuration
|
|
138
|
+
* Build config from legacy config.md + .env + env vars.
|
|
139
|
+
* @deprecated — used only when config.json doesn't exist.
|
|
138
140
|
*/
|
|
139
|
-
|
|
141
|
+
function loadLegacyConfig(dir, overrides) {
|
|
140
142
|
const env = process.env;
|
|
143
|
+
loadLegacyEnvFile(dir);
|
|
144
|
+
const fileConfig = loadLegacyConfigFile(dir);
|
|
141
145
|
|
|
142
|
-
// Determine data directory first (needed to load .env and config.md)
|
|
143
|
-
const dir = overrides.dir || env.YEAFT_DIR || DEFAULTS.dir;
|
|
144
|
-
|
|
145
|
-
// Load .env file (sets process.env for undefined keys — shell env takes precedence)
|
|
146
|
-
loadEnvFile(dir);
|
|
147
|
-
|
|
148
|
-
// Load from config.md
|
|
149
|
-
const fileConfig = loadConfigFile(dir);
|
|
150
|
-
|
|
151
|
-
// Build merged config: defaults < file < env < overrides
|
|
152
146
|
const config = {
|
|
153
|
-
model:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
language:
|
|
166
|
-
overrides.language ||
|
|
167
|
-
env.YEAFT_LANGUAGE ||
|
|
168
|
-
fileConfig.language ||
|
|
169
|
-
DEFAULTS.language,
|
|
170
|
-
|
|
171
|
-
apiKey:
|
|
172
|
-
overrides.apiKey ||
|
|
173
|
-
env.YEAFT_API_KEY ||
|
|
174
|
-
fileConfig.apiKey ||
|
|
175
|
-
DEFAULTS.apiKey,
|
|
176
|
-
|
|
177
|
-
openaiApiKey:
|
|
178
|
-
overrides.openaiApiKey ||
|
|
179
|
-
env.YEAFT_OPENAI_API_KEY ||
|
|
180
|
-
fileConfig.openaiApiKey ||
|
|
181
|
-
DEFAULTS.openaiApiKey,
|
|
182
|
-
|
|
183
|
-
proxyUrl:
|
|
184
|
-
overrides.proxyUrl ||
|
|
185
|
-
env.YEAFT_PROXY_URL ||
|
|
186
|
-
fileConfig.proxyUrl ||
|
|
187
|
-
DEFAULTS.proxyUrl,
|
|
188
|
-
|
|
189
|
-
baseUrl:
|
|
190
|
-
overrides.baseUrl ||
|
|
191
|
-
env.YEAFT_BASE_URL ||
|
|
192
|
-
fileConfig.baseUrl ||
|
|
193
|
-
DEFAULTS.baseUrl,
|
|
194
|
-
|
|
195
|
-
adapter:
|
|
196
|
-
overrides.adapter ||
|
|
197
|
-
env.YEAFT_ADAPTER ||
|
|
198
|
-
fileConfig.adapter ||
|
|
199
|
-
DEFAULTS.adapter,
|
|
200
|
-
|
|
201
|
-
debug:
|
|
202
|
-
overrides.debug !== undefined
|
|
203
|
-
? overrides.debug
|
|
204
|
-
: env.YEAFT_DEBUG !== undefined
|
|
205
|
-
? isTruthy(env.YEAFT_DEBUG)
|
|
206
|
-
: fileConfig.debug !== undefined
|
|
207
|
-
? fileConfig.debug
|
|
208
|
-
: DEFAULTS.debug,
|
|
209
|
-
|
|
147
|
+
model: overrides.model || env.YEAFT_MODEL || fileConfig.model || 'claude-sonnet-4-20250514',
|
|
148
|
+
fallbackModel: overrides.fallbackModel || env.YEAFT_FALLBACK_MODEL || fileConfig.fallbackModel || null,
|
|
149
|
+
language: overrides.language || env.YEAFT_LANGUAGE || fileConfig.language || DEFAULTS.language,
|
|
150
|
+
apiKey: overrides.apiKey || env.YEAFT_API_KEY || fileConfig.apiKey || null,
|
|
151
|
+
openaiApiKey: overrides.openaiApiKey || env.YEAFT_OPENAI_API_KEY || fileConfig.openaiApiKey || null,
|
|
152
|
+
proxyUrl: overrides.proxyUrl || env.YEAFT_PROXY_URL || fileConfig.proxyUrl || 'http://localhost:6628',
|
|
153
|
+
baseUrl: overrides.baseUrl || env.YEAFT_BASE_URL || fileConfig.baseUrl || null,
|
|
154
|
+
adapter: overrides.adapter || env.YEAFT_ADAPTER || fileConfig.adapter || null,
|
|
155
|
+
debug: overrides.debug !== undefined ? overrides.debug
|
|
156
|
+
: env.YEAFT_DEBUG !== undefined ? isTruthy(env.YEAFT_DEBUG)
|
|
157
|
+
: fileConfig.debug !== undefined ? fileConfig.debug : DEFAULTS.debug,
|
|
210
158
|
dir,
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
maxOutputTokens:
|
|
219
|
-
overrides.maxOutputTokens ??
|
|
220
|
-
fileConfig.maxOutputTokens ??
|
|
221
|
-
DEFAULTS.maxOutputTokens,
|
|
222
|
-
|
|
223
|
-
messageTokenBudget:
|
|
224
|
-
overrides.messageTokenBudget ??
|
|
225
|
-
(env.YEAFT_MESSAGE_TOKEN_BUDGET ? parseInt(env.YEAFT_MESSAGE_TOKEN_BUDGET, 10) : null) ??
|
|
226
|
-
fileConfig.messageTokenBudget ??
|
|
227
|
-
DEFAULTS.messageTokenBudget,
|
|
228
|
-
|
|
229
|
-
maxContinueTurns:
|
|
230
|
-
overrides.maxContinueTurns ??
|
|
231
|
-
fileConfig.maxContinueTurns ??
|
|
232
|
-
DEFAULTS.maxContinueTurns,
|
|
159
|
+
maxContextTokens: overrides.maxContextTokens ?? (env.YEAFT_MAX_CONTEXT ? parseInt(env.YEAFT_MAX_CONTEXT, 10) : null) ?? fileConfig.maxContextTokens ?? DEFAULTS.maxContextTokens,
|
|
160
|
+
maxOutputTokens: overrides.maxOutputTokens ?? fileConfig.maxOutputTokens ?? DEFAULTS.maxOutputTokens,
|
|
161
|
+
messageTokenBudget: overrides.messageTokenBudget ?? (env.YEAFT_MESSAGE_TOKEN_BUDGET ? parseInt(env.YEAFT_MESSAGE_TOKEN_BUDGET, 10) : null) ?? fileConfig.messageTokenBudget ?? DEFAULTS.messageTokenBudget,
|
|
162
|
+
maxContinueTurns: overrides.maxContinueTurns ?? fileConfig.maxContinueTurns ?? DEFAULTS.maxContinueTurns,
|
|
163
|
+
providers: null,
|
|
164
|
+
primaryModel: null,
|
|
165
|
+
fastModel: null,
|
|
233
166
|
};
|
|
234
167
|
|
|
235
|
-
// Auto-detect adapter
|
|
168
|
+
// Auto-detect adapter
|
|
236
169
|
if (!config.adapter) {
|
|
237
170
|
const modelInfo = resolveModel(config.model);
|
|
238
171
|
if (modelInfo) {
|
|
239
|
-
// Known model → set adapter from registry
|
|
240
172
|
config.adapter = modelInfo.adapter === 'anthropic' ? 'anthropic' : 'openai';
|
|
241
|
-
|
|
242
|
-
if (
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
// Use registry contextWindow if still at default
|
|
246
|
-
if (config.maxContextTokens === DEFAULTS.maxContextTokens) {
|
|
247
|
-
config.maxContextTokens = modelInfo.contextWindow;
|
|
248
|
-
}
|
|
249
|
-
// Use registry maxOutputTokens if still at default
|
|
250
|
-
if (config.maxOutputTokens === DEFAULTS.maxOutputTokens) {
|
|
251
|
-
config.maxOutputTokens = modelInfo.maxOutputTokens;
|
|
252
|
-
}
|
|
173
|
+
if (!config.baseUrl) config.baseUrl = modelInfo.baseUrl;
|
|
174
|
+
if (config.maxContextTokens === DEFAULTS.maxContextTokens) config.maxContextTokens = modelInfo.contextWindow;
|
|
175
|
+
if (config.maxOutputTokens === DEFAULTS.maxOutputTokens) config.maxOutputTokens = modelInfo.maxOutputTokens;
|
|
253
176
|
} else {
|
|
254
|
-
|
|
255
|
-
if (config.
|
|
256
|
-
|
|
257
|
-
} else if (config.openaiApiKey) {
|
|
258
|
-
config.adapter = 'openai';
|
|
259
|
-
} else if (config.proxyUrl) {
|
|
260
|
-
config.adapter = 'proxy';
|
|
261
|
-
}
|
|
177
|
+
if (config.apiKey) config.adapter = 'anthropic';
|
|
178
|
+
else if (config.openaiApiKey) config.adapter = 'openai';
|
|
179
|
+
else if (config.proxyUrl) config.adapter = 'proxy';
|
|
262
180
|
}
|
|
263
181
|
}
|
|
264
182
|
|
|
265
|
-
// Store resolved model info for reference
|
|
266
183
|
config.modelInfo = resolveModel(config.model) || null;
|
|
184
|
+
return config;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ─── Main: loadConfig ───────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Load full configuration.
|
|
191
|
+
*
|
|
192
|
+
* Reads ~/.yeaft/config.json first. If not found, falls back to legacy
|
|
193
|
+
* config.md + .env for backward compatibility.
|
|
194
|
+
*
|
|
195
|
+
* @param {Record<string, unknown>} [overrides] — CLI overrides
|
|
196
|
+
* @returns {object} — Merged configuration
|
|
197
|
+
*/
|
|
198
|
+
export function loadConfig(overrides = {}) {
|
|
199
|
+
// Determine data directory
|
|
200
|
+
const dir = overrides.dir || process.env.YEAFT_DIR || DEFAULTS.dir;
|
|
201
|
+
|
|
202
|
+
// Try config.json first
|
|
203
|
+
const jsonConfig = readConfigJson(dir);
|
|
204
|
+
|
|
205
|
+
if (!jsonConfig) {
|
|
206
|
+
// No config.json → legacy path
|
|
207
|
+
return loadLegacyConfig(dir, overrides);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ─── Build config from config.json ────────────────────────
|
|
211
|
+
const providers = Array.isArray(jsonConfig.providers) ? jsonConfig.providers : null;
|
|
212
|
+
|
|
213
|
+
// Resolve primary model
|
|
214
|
+
let model = 'claude-sonnet-4-20250514';
|
|
215
|
+
let primaryModel = jsonConfig.primaryModel || null;
|
|
216
|
+
if (primaryModel) {
|
|
217
|
+
const parsed = parseModelRef(primaryModel);
|
|
218
|
+
model = parsed.modelId;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Resolve fast model
|
|
222
|
+
let fastModel = jsonConfig.fastModel || primaryModel || null;
|
|
223
|
+
let fastModelId = null;
|
|
224
|
+
if (fastModel) {
|
|
225
|
+
const parsed = parseModelRef(fastModel);
|
|
226
|
+
fastModelId = parsed.modelId;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Resolve model info for context window / output limits
|
|
230
|
+
const modelInfo = resolveModel(model);
|
|
231
|
+
|
|
232
|
+
const config = {
|
|
233
|
+
// Model
|
|
234
|
+
model: overrides.model || model,
|
|
235
|
+
primaryModel: overrides.primaryModel || primaryModel,
|
|
236
|
+
fastModel: overrides.fastModel || fastModel,
|
|
237
|
+
fastModelId: fastModelId,
|
|
238
|
+
fallbackModel: jsonConfig.fallbackModel || null,
|
|
239
|
+
modelInfo: modelInfo || null,
|
|
240
|
+
|
|
241
|
+
// Providers
|
|
242
|
+
providers: providers,
|
|
243
|
+
|
|
244
|
+
// General settings
|
|
245
|
+
language: overrides.language || jsonConfig.language || DEFAULTS.language,
|
|
246
|
+
debug: overrides.debug !== undefined ? overrides.debug : (jsonConfig.debug ?? DEFAULTS.debug),
|
|
247
|
+
dir,
|
|
248
|
+
|
|
249
|
+
// Token limits
|
|
250
|
+
maxContextTokens: overrides.maxContextTokens ?? jsonConfig.maxContextTokens ?? modelInfo?.contextWindow ?? DEFAULTS.maxContextTokens,
|
|
251
|
+
maxOutputTokens: overrides.maxOutputTokens ?? jsonConfig.maxOutputTokens ?? modelInfo?.maxOutputTokens ?? DEFAULTS.maxOutputTokens,
|
|
252
|
+
messageTokenBudget: overrides.messageTokenBudget ?? jsonConfig.messageTokenBudget ?? DEFAULTS.messageTokenBudget,
|
|
253
|
+
maxContinueTurns: overrides.maxContinueTurns ?? jsonConfig.maxContinueTurns ?? DEFAULTS.maxContinueTurns,
|
|
254
|
+
|
|
255
|
+
// Legacy fields (null when using config.json)
|
|
256
|
+
apiKey: overrides.apiKey || null,
|
|
257
|
+
openaiApiKey: null,
|
|
258
|
+
proxyUrl: null,
|
|
259
|
+
baseUrl: null,
|
|
260
|
+
adapter: null,
|
|
261
|
+
};
|
|
267
262
|
|
|
268
263
|
return config;
|
|
269
264
|
}
|
|
270
265
|
|
|
266
|
+
// ─── MCP config ─────────────────────────────────────────────────
|
|
267
|
+
|
|
271
268
|
/**
|
|
272
|
-
* Load MCP server configuration
|
|
269
|
+
* Load MCP server configuration.
|
|
273
270
|
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
* "servers": [
|
|
277
|
-
* {
|
|
278
|
-
* "name": "github",
|
|
279
|
-
* "command": "npx",
|
|
280
|
-
* "args": ["@mcp/github"],
|
|
281
|
-
* "env": { "GITHUB_TOKEN": "ghp_..." }
|
|
282
|
-
* }
|
|
283
|
-
* ]
|
|
284
|
-
* }
|
|
271
|
+
* Reads from config.json "mcpServers" field first, then falls back to
|
|
272
|
+
* standalone ~/.yeaft/mcp.json for backward compatibility.
|
|
285
273
|
*
|
|
286
|
-
* @param {string} yeaftDir
|
|
274
|
+
* @param {string} yeaftDir
|
|
275
|
+
* @param {object} [jsonConfig] — Already-parsed config.json (optional, avoids re-read)
|
|
287
276
|
* @returns {{ servers: object[] }}
|
|
288
277
|
*/
|
|
289
|
-
export function loadMCPConfig(yeaftDir) {
|
|
278
|
+
export function loadMCPConfig(yeaftDir, jsonConfig) {
|
|
279
|
+
// Check config.json mcpServers field
|
|
280
|
+
if (jsonConfig && Array.isArray(jsonConfig.mcpServers)) {
|
|
281
|
+
const valid = jsonConfig.mcpServers.filter(s => s.name && s.command);
|
|
282
|
+
if (valid.length > 0) return { servers: valid };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Fallback: standalone mcp.json
|
|
290
286
|
const mcpPath = join(yeaftDir, 'mcp.json');
|
|
291
287
|
if (!existsSync(mcpPath)) return { servers: [] };
|
|
292
288
|
|
|
293
289
|
try {
|
|
294
290
|
const raw = readFileSync(mcpPath, 'utf8');
|
|
295
291
|
const parsed = JSON.parse(raw);
|
|
296
|
-
if (!parsed.servers || !Array.isArray(parsed.servers)) {
|
|
297
|
-
return { servers: [] };
|
|
298
|
-
}
|
|
299
|
-
// Each server must have at least name + command
|
|
292
|
+
if (!parsed.servers || !Array.isArray(parsed.servers)) return { servers: [] };
|
|
300
293
|
const valid = parsed.servers.filter(s => s.name && s.command);
|
|
301
294
|
return { servers: valid };
|
|
302
295
|
} catch {
|
package/unify/engine.js
CHANGED
|
@@ -80,6 +80,9 @@ export class Engine {
|
|
|
80
80
|
/** @type {string|null} */
|
|
81
81
|
#yeaftDir;
|
|
82
82
|
|
|
83
|
+
/** @type {object|null} — Config override for internal tasks (recall, consolidation, dream) using fastModel */
|
|
84
|
+
#fastConfig;
|
|
85
|
+
|
|
83
86
|
/**
|
|
84
87
|
* @param {{
|
|
85
88
|
* adapter: import('./llm/adapter.js').LLMAdapter,
|
|
@@ -105,6 +108,15 @@ export class Engine {
|
|
|
105
108
|
this.#skillManager = skillManager || null;
|
|
106
109
|
this.#mcpManager = mcpManager || null;
|
|
107
110
|
this.#yeaftDir = yeaftDir || null;
|
|
111
|
+
|
|
112
|
+
// Build fast config: uses fastModelId for internal tasks (recall, consolidation, dream)
|
|
113
|
+
// Falls back to primary model if no fastModel configured
|
|
114
|
+
const fastModelId = config.fastModelId || config.model;
|
|
115
|
+
if (fastModelId !== config.model) {
|
|
116
|
+
this.#fastConfig = { ...config, model: fastModelId };
|
|
117
|
+
} else {
|
|
118
|
+
this.#fastConfig = config;
|
|
119
|
+
}
|
|
108
120
|
}
|
|
109
121
|
|
|
110
122
|
/**
|
|
@@ -215,12 +227,12 @@ export class Engine {
|
|
|
215
227
|
// Read user profile
|
|
216
228
|
memory.profile = this.#memoryStore.readProfile();
|
|
217
229
|
|
|
218
|
-
// Recall relevant entries
|
|
230
|
+
// Recall relevant entries (uses fastModel for cheaper/faster side-queries)
|
|
219
231
|
try {
|
|
220
232
|
const result = await recall({
|
|
221
233
|
prompt,
|
|
222
234
|
adapter: this.#adapter,
|
|
223
|
-
config: this.#
|
|
235
|
+
config: this.#fastConfig,
|
|
224
236
|
memoryStore: this.#memoryStore,
|
|
225
237
|
});
|
|
226
238
|
memory.entries = result.entries;
|
|
@@ -288,7 +300,7 @@ export class Engine {
|
|
|
288
300
|
conversationStore: this.#conversationStore,
|
|
289
301
|
memoryStore: this.#memoryStore,
|
|
290
302
|
adapter: this.#adapter,
|
|
291
|
-
config: this.#
|
|
303
|
+
config: this.#fastConfig,
|
|
292
304
|
budget,
|
|
293
305
|
});
|
|
294
306
|
return { archivedCount: result.archivedCount, extractedCount: result.extractedEntries.length };
|
|
@@ -481,13 +493,16 @@ export class Engine {
|
|
|
481
493
|
// ─── Post-query: StopHooks or Legacy ─────────────
|
|
482
494
|
if (this.#yeaftDir && this.#conversationStore) {
|
|
483
495
|
// Full pipeline: persist + consolidate + dream gate
|
|
496
|
+
// Note: stopHooks uses fastConfig for consolidation/dream (cheaper internal tasks)
|
|
497
|
+
// but receives both configs — messages are persisted with primary model name
|
|
484
498
|
const hookResult = await runStopHooks({
|
|
485
499
|
yeaftDir: this.#yeaftDir,
|
|
486
500
|
mode,
|
|
487
501
|
conversationStore: this.#conversationStore,
|
|
488
502
|
memoryStore: this.#memoryStore,
|
|
489
503
|
adapter: this.#adapter,
|
|
490
|
-
config: this.#
|
|
504
|
+
config: this.#fastConfig,
|
|
505
|
+
primaryModel: this.#config.model,
|
|
491
506
|
messages: conversationMessages,
|
|
492
507
|
trace: this.#trace,
|
|
493
508
|
});
|
|
@@ -616,4 +631,7 @@ export class Engine {
|
|
|
616
631
|
|
|
617
632
|
/** @returns {string|null} */
|
|
618
633
|
get yeaftDir() { return this.#yeaftDir; }
|
|
634
|
+
|
|
635
|
+
/** @returns {object} — Config with fastModel as model (for internal tasks) */
|
|
636
|
+
get fastConfig() { return this.#fastConfig; }
|
|
619
637
|
}
|
package/unify/index.js
CHANGED
|
@@ -16,7 +16,8 @@ export {
|
|
|
16
16
|
LLMAbortError,
|
|
17
17
|
createLLMAdapter,
|
|
18
18
|
} from './llm/adapter.js';
|
|
19
|
-
export {
|
|
19
|
+
export { AdapterRouter } from './llm/router.js';
|
|
20
|
+
export { MODEL_REGISTRY, resolveModel, listModels, isKnownModel, getProviderForModel, parseModelRef } from './models.js';
|
|
20
21
|
export { buildSystemPrompt, SUPPORTED_LANGUAGES } from './prompts.js';
|
|
21
22
|
export { Engine } from './engine.js';
|
|
22
23
|
export { ConversationStore, parseMessage, estimateTokens } from './conversation/persist.js';
|