@clappstore/connect 0.7.7 → 0.7.8
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/agent-client.d.ts +6 -0
- package/dist/agent-client.d.ts.map +1 -1
- package/dist/agent-client.js +109 -18
- package/dist/agent-client.js.map +1 -1
- package/dist/auth.d.ts +18 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +248 -0
- package/dist/auth.js.map +1 -0
- package/dist/chat-handler.d.ts +52 -0
- package/dist/chat-handler.d.ts.map +1 -0
- package/dist/chat-handler.js +453 -0
- package/dist/chat-handler.js.map +1 -0
- package/dist/defaults.d.ts +1 -1
- package/dist/defaults.d.ts.map +1 -1
- package/dist/defaults.js +36 -23
- package/dist/defaults.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +53 -30
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +88 -7
- package/dist/server.js.map +1 -1
- package/dist/settings-handler.d.ts +76 -0
- package/dist/settings-handler.d.ts.map +1 -0
- package/dist/settings-handler.js +848 -0
- package/dist/settings-handler.js.map +1 -0
- package/package.json +4 -8
- package/web-app/assets/{index-CEpgiIwf.js → index-CWzlxjUK.js} +86 -56
- package/web-app/assets/index-Cic64hbc.css +1 -0
- package/web-app/index.html +2 -2
- package/clapps/settings/README.md +0 -74
- package/clapps/settings/clapp.json +0 -25
- package/clapps/settings/components/ProviderEditor.tsx +0 -512
- package/clapps/settings/components/ProviderList.tsx +0 -300
- package/clapps/settings/components/SessionList.tsx +0 -189
- package/clapps/settings/handlers/settings-handler.js +0 -742
- package/clapps/settings/views/default.settings.view.md +0 -35
- package/clapps/settings/views/settings.app.md +0 -12
- package/web-app/assets/index-BsI5PEAv.css +0 -1
|
@@ -0,0 +1,848 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { checkAuthStatus } from "./defaults.js";
|
|
6
|
+
export class SettingsHandler {
|
|
7
|
+
stateDir;
|
|
8
|
+
store;
|
|
9
|
+
authProfilesPath;
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.stateDir = options.stateDir;
|
|
12
|
+
this.store = options.store;
|
|
13
|
+
this.authProfilesPath =
|
|
14
|
+
options.authProfilesPath ??
|
|
15
|
+
resolve(homedir(), ".openclaw", "agents", "main", "agent", "auth-profiles.json");
|
|
16
|
+
}
|
|
17
|
+
/** Returns true if the intent was handled locally (should not be forwarded to ACP) */
|
|
18
|
+
handleIntent = (intent) => {
|
|
19
|
+
if (!intent.intent.startsWith("settings."))
|
|
20
|
+
return false;
|
|
21
|
+
const customName = typeof intent.payload.customName === "string"
|
|
22
|
+
? intent.payload.customName.trim()
|
|
23
|
+
: undefined;
|
|
24
|
+
const profileId = typeof intent.payload.profileId === "string"
|
|
25
|
+
? intent.payload.profileId.trim()
|
|
26
|
+
: undefined;
|
|
27
|
+
switch (intent.intent) {
|
|
28
|
+
case "settings.setAnthropicKey": {
|
|
29
|
+
const apiKey = intent.payload.apiKey;
|
|
30
|
+
if (typeof apiKey !== "string" || apiKey.trim().length === 0) {
|
|
31
|
+
console.warn("[settings] Invalid apiKey payload, ignoring");
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
this.setAnthropicKey(apiKey.trim(), customName, profileId);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
case "settings.setClaudeToken": {
|
|
38
|
+
const token = intent.payload.setupToken ?? intent.payload.token;
|
|
39
|
+
if (typeof token !== "string" || token.trim().length === 0) {
|
|
40
|
+
console.warn("[settings] Invalid token/setupToken payload, ignoring");
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
this.setClaudeToken(token.trim(), customName);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
case "settings.setOpenAIKey": {
|
|
47
|
+
const apiKey = intent.payload.apiKey;
|
|
48
|
+
if (typeof apiKey !== "string" || apiKey.trim().length === 0) {
|
|
49
|
+
console.warn("[settings] Invalid apiKey payload, ignoring");
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
this.setOpenAIKey(apiKey.trim(), customName, profileId);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
case "settings.setKimiCodingKey": {
|
|
56
|
+
const apiKey = intent.payload.apiKey;
|
|
57
|
+
if (typeof apiKey !== "string" || apiKey.trim().length === 0) {
|
|
58
|
+
console.warn("[settings] Invalid apiKey payload, ignoring");
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
this.setKimiCodingKey(apiKey.trim(), customName, profileId);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
case "settings.startOAuth": {
|
|
65
|
+
const provider = intent.payload.provider;
|
|
66
|
+
if (typeof provider !== "string" || provider.trim().length === 0) {
|
|
67
|
+
console.warn("[settings] Invalid provider payload for OAuth, ignoring");
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
this.startOAuth(provider.trim(), customName);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
case "settings.deleteProvider": {
|
|
74
|
+
if (!profileId) {
|
|
75
|
+
console.warn("[settings] Missing profileId for deleteProvider, ignoring");
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
this.deleteProvider(profileId);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
case "settings.setActiveProvider": {
|
|
82
|
+
const provider = intent.payload.provider;
|
|
83
|
+
if (typeof provider !== "string" || provider.trim().length === 0) {
|
|
84
|
+
console.warn("[settings] Invalid provider payload, ignoring");
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
this.setActiveProvider(provider.trim().toLowerCase());
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
case "settings.setActiveModel": {
|
|
91
|
+
const model = intent.payload.model;
|
|
92
|
+
if (typeof model !== "string" || model.trim().length === 0) {
|
|
93
|
+
console.warn("[settings] Invalid model payload, ignoring");
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
this.setActiveModel(model.trim().toLowerCase());
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
case "settings.listSessions": {
|
|
100
|
+
this.listSessions();
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
case "settings.resetSessionModel": {
|
|
104
|
+
const sessionKey = intent.payload.sessionKey;
|
|
105
|
+
if (typeof sessionKey !== "string" || sessionKey.trim().length === 0) {
|
|
106
|
+
console.warn("[settings] Invalid sessionKey payload, ignoring");
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
this.resetSessionModel(sessionKey.trim());
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
case "settings.applyDefaultToAll": {
|
|
113
|
+
this.applyDefaultToAllSessions();
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
default:
|
|
117
|
+
console.warn(`[settings] Unknown settings intent: ${intent.intent}`);
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
/** Generate a unique profile ID */
|
|
122
|
+
generateProfileId(provider, customName) {
|
|
123
|
+
const suffix = customName
|
|
124
|
+
? customName.toLowerCase().replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-")
|
|
125
|
+
: "manual";
|
|
126
|
+
return `${provider}:${suffix}`;
|
|
127
|
+
}
|
|
128
|
+
/** Write auth-profiles.json with the Anthropic API key */
|
|
129
|
+
setAnthropicKey(apiKey, customName, existingProfileId) {
|
|
130
|
+
let profiles = { version: 1, profiles: {} };
|
|
131
|
+
try {
|
|
132
|
+
if (existsSync(this.authProfilesPath)) {
|
|
133
|
+
profiles = JSON.parse(readFileSync(this.authProfilesPath, "utf-8"));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// Start fresh if unreadable
|
|
138
|
+
}
|
|
139
|
+
const profileId = existingProfileId ?? this.generateProfileId("anthropic", customName);
|
|
140
|
+
profiles.profiles[profileId] = {
|
|
141
|
+
type: "token",
|
|
142
|
+
provider: "anthropic",
|
|
143
|
+
token: apiKey,
|
|
144
|
+
customName: customName || "Anthropic API",
|
|
145
|
+
};
|
|
146
|
+
mkdirSync(dirname(this.authProfilesPath), { recursive: true });
|
|
147
|
+
writeFileSync(this.authProfilesPath, JSON.stringify(profiles, null, 2), "utf-8");
|
|
148
|
+
console.log(`✅ Anthropic API key saved to ${this.authProfilesPath} (profile: ${profileId})`);
|
|
149
|
+
this.writeSettingsState();
|
|
150
|
+
this.pushSettingsState();
|
|
151
|
+
checkAuthStatus(this.stateDir, this.authProfilesPath);
|
|
152
|
+
this.pushStatusState();
|
|
153
|
+
}
|
|
154
|
+
/** Set Claude subscription token via openclaw CLI */
|
|
155
|
+
setClaudeToken(token, customName) {
|
|
156
|
+
const result = spawnSync("openclaw", ["models", "auth", "paste-token", "--provider", "anthropic"], { input: token, encoding: "utf-8", timeout: 15_000 });
|
|
157
|
+
if (result.status !== 0) {
|
|
158
|
+
const msg = (result.stderr || result.error?.message || "unknown error").toString().trim();
|
|
159
|
+
console.error(`[settings] openclaw paste-token failed: ${msg}`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// Update the profile with custom name if provided
|
|
163
|
+
if (customName) {
|
|
164
|
+
try {
|
|
165
|
+
if (existsSync(this.authProfilesPath)) {
|
|
166
|
+
const profiles = JSON.parse(readFileSync(this.authProfilesPath, "utf-8"));
|
|
167
|
+
// Find the anthropic profile that was just updated
|
|
168
|
+
for (const [key, profile] of Object.entries(profiles.profiles)) {
|
|
169
|
+
if (profile.provider === "anthropic" && (profile.access || profile.refresh)) {
|
|
170
|
+
profile.customName = customName;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
writeFileSync(this.authProfilesPath, JSON.stringify(profiles, null, 2), "utf-8");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// Ignore errors updating custom name
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
console.log("✅ Claude subscription token saved via openclaw");
|
|
182
|
+
this.writeSettingsState();
|
|
183
|
+
this.pushSettingsState();
|
|
184
|
+
checkAuthStatus(this.stateDir, this.authProfilesPath);
|
|
185
|
+
this.pushStatusState();
|
|
186
|
+
}
|
|
187
|
+
/** Write auth-profiles.json with the OpenAI API key */
|
|
188
|
+
setOpenAIKey(apiKey, customName, existingProfileId) {
|
|
189
|
+
let profiles = { version: 1, profiles: {} };
|
|
190
|
+
try {
|
|
191
|
+
if (existsSync(this.authProfilesPath)) {
|
|
192
|
+
profiles = JSON.parse(readFileSync(this.authProfilesPath, "utf-8"));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// Start fresh if unreadable
|
|
197
|
+
}
|
|
198
|
+
const profileId = existingProfileId ?? this.generateProfileId("openai", customName);
|
|
199
|
+
profiles.profiles[profileId] = {
|
|
200
|
+
type: "token",
|
|
201
|
+
provider: "openai",
|
|
202
|
+
token: apiKey,
|
|
203
|
+
customName: customName || "OpenAI API",
|
|
204
|
+
};
|
|
205
|
+
// Also set the env var in openclaw.json for compatibility
|
|
206
|
+
this.setEnvVar("OPENAI_API_KEY", apiKey);
|
|
207
|
+
mkdirSync(dirname(this.authProfilesPath), { recursive: true });
|
|
208
|
+
writeFileSync(this.authProfilesPath, JSON.stringify(profiles, null, 2), "utf-8");
|
|
209
|
+
console.log(`✅ OpenAI API key saved to ${this.authProfilesPath} (profile: ${profileId})`);
|
|
210
|
+
this.writeSettingsState();
|
|
211
|
+
this.pushSettingsState();
|
|
212
|
+
checkAuthStatus(this.stateDir, this.authProfilesPath);
|
|
213
|
+
this.pushStatusState();
|
|
214
|
+
}
|
|
215
|
+
/** Write auth-profiles.json with the Kimi Coding API key */
|
|
216
|
+
setKimiCodingKey(apiKey, customName, existingProfileId) {
|
|
217
|
+
let profiles = { version: 1, profiles: {} };
|
|
218
|
+
try {
|
|
219
|
+
if (existsSync(this.authProfilesPath)) {
|
|
220
|
+
profiles = JSON.parse(readFileSync(this.authProfilesPath, "utf-8"));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
// Start fresh if unreadable
|
|
225
|
+
}
|
|
226
|
+
const profileId = existingProfileId ?? this.generateProfileId("kimi-coding", customName);
|
|
227
|
+
profiles.profiles[profileId] = {
|
|
228
|
+
type: "token",
|
|
229
|
+
provider: "kimi-coding",
|
|
230
|
+
token: apiKey,
|
|
231
|
+
customName: customName || "Kimi Coding",
|
|
232
|
+
};
|
|
233
|
+
// Also set the env var in openclaw.json for compatibility
|
|
234
|
+
this.setEnvVar("KIMI_API_KEY", apiKey);
|
|
235
|
+
mkdirSync(dirname(this.authProfilesPath), { recursive: true });
|
|
236
|
+
writeFileSync(this.authProfilesPath, JSON.stringify(profiles, null, 2), "utf-8");
|
|
237
|
+
console.log(`✅ Kimi Coding API key saved to ${this.authProfilesPath} (profile: ${profileId})`);
|
|
238
|
+
this.writeSettingsState();
|
|
239
|
+
this.pushSettingsState();
|
|
240
|
+
checkAuthStatus(this.stateDir, this.authProfilesPath);
|
|
241
|
+
this.pushStatusState();
|
|
242
|
+
}
|
|
243
|
+
/** Start OAuth flow for a provider */
|
|
244
|
+
startOAuth(provider, customName) {
|
|
245
|
+
const result = spawnSync("openclaw", ["models", "auth", "login", "--provider", provider], { encoding: "utf-8", timeout: 30_000, stdio: "inherit" });
|
|
246
|
+
if (result.status !== 0) {
|
|
247
|
+
console.error(`[settings] OAuth login failed for ${provider}`);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
// Update with custom name if provided
|
|
251
|
+
if (customName) {
|
|
252
|
+
try {
|
|
253
|
+
if (existsSync(this.authProfilesPath)) {
|
|
254
|
+
const profiles = JSON.parse(readFileSync(this.authProfilesPath, "utf-8"));
|
|
255
|
+
for (const [_key, profile] of Object.entries(profiles.profiles)) {
|
|
256
|
+
if (profile.provider === provider && (profile.access || profile.refresh)) {
|
|
257
|
+
profile.customName = customName;
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
writeFileSync(this.authProfilesPath, JSON.stringify(profiles, null, 2), "utf-8");
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
// Ignore errors
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
console.log(`✅ OAuth login completed for ${provider}`);
|
|
269
|
+
this.writeSettingsState();
|
|
270
|
+
this.pushSettingsState();
|
|
271
|
+
checkAuthStatus(this.stateDir, this.authProfilesPath);
|
|
272
|
+
this.pushStatusState();
|
|
273
|
+
}
|
|
274
|
+
/** Delete a provider profile */
|
|
275
|
+
deleteProvider(profileId) {
|
|
276
|
+
try {
|
|
277
|
+
if (!existsSync(this.authProfilesPath)) {
|
|
278
|
+
console.warn(`[settings] No auth-profiles.json found`);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const profiles = JSON.parse(readFileSync(this.authProfilesPath, "utf-8"));
|
|
282
|
+
if (!profiles.profiles[profileId]) {
|
|
283
|
+
console.warn(`[settings] Profile "${profileId}" not found`);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
delete profiles.profiles[profileId];
|
|
287
|
+
writeFileSync(this.authProfilesPath, JSON.stringify(profiles, null, 2), "utf-8");
|
|
288
|
+
console.log(`✅ Deleted provider profile: ${profileId}`);
|
|
289
|
+
this.writeSettingsState();
|
|
290
|
+
this.pushSettingsState();
|
|
291
|
+
checkAuthStatus(this.stateDir, this.authProfilesPath);
|
|
292
|
+
this.pushStatusState();
|
|
293
|
+
}
|
|
294
|
+
catch (err) {
|
|
295
|
+
console.error(`[settings] Failed to delete provider: ${err}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/** Set an environment variable in openclaw.json */
|
|
299
|
+
setEnvVar(key, value) {
|
|
300
|
+
try {
|
|
301
|
+
const configPath = resolve(homedir(), ".openclaw", "openclaw.json");
|
|
302
|
+
let config = {};
|
|
303
|
+
if (existsSync(configPath)) {
|
|
304
|
+
config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
305
|
+
}
|
|
306
|
+
if (!config.env || typeof config.env !== "object") {
|
|
307
|
+
config.env = {};
|
|
308
|
+
}
|
|
309
|
+
config.env[key] = value;
|
|
310
|
+
if (config.meta && typeof config.meta === "object") {
|
|
311
|
+
config.meta.lastTouchedAt = new Date().toISOString();
|
|
312
|
+
}
|
|
313
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
314
|
+
console.log(`[settings] Set ${key} in openclaw.json`);
|
|
315
|
+
}
|
|
316
|
+
catch (err) {
|
|
317
|
+
console.warn(`[settings] Failed to set env var ${key}: ${err}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/** Set the active AI provider by choosing its first configured model */
|
|
321
|
+
setActiveProvider(provider) {
|
|
322
|
+
// Provider may be a full profile ID (e.g. "anthropic:anthropic-theforever")
|
|
323
|
+
// or a base provider name. Extract base provider for model lookup.
|
|
324
|
+
const baseProvider = provider.includes(":") ? provider.split(":")[0] : provider;
|
|
325
|
+
const modelId = this.getFirstModelForProvider(baseProvider);
|
|
326
|
+
if (!modelId) {
|
|
327
|
+
console.warn(`[settings] Unknown provider or no models available: ${baseProvider}`);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
this.setActiveModel(modelId);
|
|
331
|
+
}
|
|
332
|
+
/** Set the active model system-wide */
|
|
333
|
+
setActiveModel(modelId) {
|
|
334
|
+
const result = spawnSync("openclaw", ["models", "set", modelId], { encoding: "utf-8", timeout: 20_000 });
|
|
335
|
+
if (result.status !== 0) {
|
|
336
|
+
const msg = (result.stderr || result.error?.message || "unknown error").toString().trim();
|
|
337
|
+
console.error(`[settings] Failed to set default model: ${msg}`);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
this.clearAgentModelOverrides(modelId);
|
|
341
|
+
console.log(`✅ Active model set system-wide to ${modelId}`);
|
|
342
|
+
this.writeSettingsState();
|
|
343
|
+
this.pushSettingsState();
|
|
344
|
+
}
|
|
345
|
+
/** Clear per-agent model overrides so all agents use the system default */
|
|
346
|
+
clearAgentModelOverrides(newModelId) {
|
|
347
|
+
try {
|
|
348
|
+
const configPath = resolve(homedir(), ".openclaw", "openclaw.json");
|
|
349
|
+
if (!existsSync(configPath))
|
|
350
|
+
return;
|
|
351
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
352
|
+
// Update agents.list to use the new model (or remove overrides)
|
|
353
|
+
if (Array.isArray(config.agents?.list)) {
|
|
354
|
+
let changed = false;
|
|
355
|
+
for (const agent of config.agents.list) {
|
|
356
|
+
if (agent.model && agent.model !== newModelId) {
|
|
357
|
+
// Set all agents to use the same model for system-wide consistency
|
|
358
|
+
agent.model = newModelId;
|
|
359
|
+
changed = true;
|
|
360
|
+
console.log(`[settings] Updated agent "${agent.id}" model to ${newModelId}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (changed) {
|
|
364
|
+
// Update lastTouchedAt timestamp
|
|
365
|
+
if (config.meta) {
|
|
366
|
+
config.meta.lastTouchedAt = new Date().toISOString();
|
|
367
|
+
}
|
|
368
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
369
|
+
console.log(`[settings] Saved openclaw.json with updated agent models`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
catch (err) {
|
|
374
|
+
console.warn(`[settings] Failed to update agent model overrides: ${err}`);
|
|
375
|
+
}
|
|
376
|
+
// Patch all active sessions to use the new model
|
|
377
|
+
this.patchAllSessionModels(newModelId);
|
|
378
|
+
}
|
|
379
|
+
/** Set model override for all active sessions via /model command */
|
|
380
|
+
patchAllSessionModels(newModelId) {
|
|
381
|
+
try {
|
|
382
|
+
// First, list all sessions
|
|
383
|
+
const listResult = spawnSync("openclaw", ["gateway", "call", "sessions.list", "--params", '{"limit": 100}', "--json"], { encoding: "utf-8", timeout: 15_000 });
|
|
384
|
+
if (listResult.status !== 0) {
|
|
385
|
+
console.warn(`[settings] Failed to list sessions: ${listResult.stderr}`);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
let sessions = [];
|
|
389
|
+
try {
|
|
390
|
+
const parsed = JSON.parse(listResult.stdout);
|
|
391
|
+
sessions = parsed.sessions || [];
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
console.warn(`[settings] Failed to parse sessions list`);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
// Only send /model to main/direct sessions (not cron, hooks, subagents)
|
|
398
|
+
const mainSessions = sessions.filter(s => s.kind === "direct" || s.key.endsWith(":main"));
|
|
399
|
+
for (const session of mainSessions) {
|
|
400
|
+
const idempotencyKey = `clapps-model-${session.key}-${Date.now()}`;
|
|
401
|
+
const sendResult = spawnSync("openclaw", [
|
|
402
|
+
"gateway", "call", "chat.send",
|
|
403
|
+
"--params", JSON.stringify({
|
|
404
|
+
sessionKey: session.key,
|
|
405
|
+
message: `/model ${newModelId}`,
|
|
406
|
+
idempotencyKey,
|
|
407
|
+
}),
|
|
408
|
+
], { encoding: "utf-8", timeout: 10_000 });
|
|
409
|
+
if (sendResult.status === 0) {
|
|
410
|
+
console.log(`[settings] Sent /model to session "${session.key}"`);
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
console.warn(`[settings] Failed to send /model to "${session.key}": ${sendResult.stderr}`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
catch (err) {
|
|
418
|
+
console.warn(`[settings] Failed to update session models: ${err}`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
/** Get the active model from OpenClaw config */
|
|
422
|
+
getActiveModel() {
|
|
423
|
+
try {
|
|
424
|
+
const configPath = resolve(homedir(), ".openclaw", "openclaw.json");
|
|
425
|
+
if (!existsSync(configPath))
|
|
426
|
+
return null;
|
|
427
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
428
|
+
const primary = config?.agents?.defaults?.model?.primary;
|
|
429
|
+
if (typeof primary === "string" && primary.includes("/")) {
|
|
430
|
+
const [provider, ...rest] = primary.split("/");
|
|
431
|
+
return { provider, model: rest.join("/") };
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
// Ignore errors
|
|
436
|
+
}
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
/** Read available models grouped by provider from OpenClaw config */
|
|
440
|
+
getModelCatalogByProvider() {
|
|
441
|
+
const map = new Map();
|
|
442
|
+
try {
|
|
443
|
+
const configPath = resolve(homedir(), ".openclaw", "openclaw.json");
|
|
444
|
+
if (!existsSync(configPath))
|
|
445
|
+
return map;
|
|
446
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
447
|
+
const models = config?.agents?.defaults?.models ?? {};
|
|
448
|
+
for (const [modelId, meta] of Object.entries(models)) {
|
|
449
|
+
if (typeof modelId !== "string" || !modelId.includes("/"))
|
|
450
|
+
continue;
|
|
451
|
+
const [provider] = modelId.split("/");
|
|
452
|
+
if (!provider)
|
|
453
|
+
continue;
|
|
454
|
+
const label = this.formatModelLabel(modelId, meta);
|
|
455
|
+
const existing = map.get(provider) ?? [];
|
|
456
|
+
existing.push({ id: modelId, label });
|
|
457
|
+
map.set(provider, existing);
|
|
458
|
+
}
|
|
459
|
+
for (const [provider, entries] of map) {
|
|
460
|
+
entries.sort((a, b) => a.label.localeCompare(b.label));
|
|
461
|
+
map.set(provider, entries);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
// Ignore parse errors and return empty catalog
|
|
466
|
+
}
|
|
467
|
+
return map;
|
|
468
|
+
}
|
|
469
|
+
/** Return first available model id for a provider */
|
|
470
|
+
getFirstModelForProvider(provider) {
|
|
471
|
+
const modelsByProvider = this.getModelCatalogByProvider();
|
|
472
|
+
const normalizedProviderAliases = {
|
|
473
|
+
"openai codex": "openai-codex",
|
|
474
|
+
"google gemini": "gemini",
|
|
475
|
+
"kimi coding": "kimi-coding",
|
|
476
|
+
"kimi k2": "nvidia",
|
|
477
|
+
glm5: "nvidia",
|
|
478
|
+
};
|
|
479
|
+
const normalizedProvider = normalizedProviderAliases[provider] ?? provider;
|
|
480
|
+
const models = modelsByProvider.get(normalizedProvider) ?? [];
|
|
481
|
+
return models[0]?.id ?? null;
|
|
482
|
+
}
|
|
483
|
+
/** Build a human-readable model label */
|
|
484
|
+
formatModelLabel(modelId, meta) {
|
|
485
|
+
const alias = typeof meta?.alias === "string" ? meta.alias : "";
|
|
486
|
+
const [, ...rest] = modelId.split("/");
|
|
487
|
+
const shortId = rest.join("/") || modelId;
|
|
488
|
+
return alias ? `${alias} (${shortId})` : shortId;
|
|
489
|
+
}
|
|
490
|
+
/** Read OpenClaw auth-profiles.json and extract configured providers */
|
|
491
|
+
getConfiguredProviders() {
|
|
492
|
+
const providers = [];
|
|
493
|
+
const activeModel = this.getActiveModel();
|
|
494
|
+
const modelsByProvider = this.getModelCatalogByProvider();
|
|
495
|
+
try {
|
|
496
|
+
if (!existsSync(this.authProfilesPath)) {
|
|
497
|
+
return providers;
|
|
498
|
+
}
|
|
499
|
+
const data = JSON.parse(readFileSync(this.authProfilesPath, "utf-8"));
|
|
500
|
+
// Build provider info for each profile
|
|
501
|
+
for (const [profileId, profile] of Object.entries(data.profiles)) {
|
|
502
|
+
let maskedCredential = "";
|
|
503
|
+
let mode = profile.type;
|
|
504
|
+
let authType = "api-key";
|
|
505
|
+
if (profile.token) {
|
|
506
|
+
maskedCredential = this.maskCredential(profile.token);
|
|
507
|
+
mode = "token";
|
|
508
|
+
// Detect if it's a subscription token vs API key
|
|
509
|
+
authType = this.detectAuthType(profileId, profile);
|
|
510
|
+
}
|
|
511
|
+
else if (profile.key) {
|
|
512
|
+
maskedCredential = this.maskCredential(profile.key);
|
|
513
|
+
mode = "api_key";
|
|
514
|
+
authType = "api-key";
|
|
515
|
+
}
|
|
516
|
+
else if (profile.access) {
|
|
517
|
+
maskedCredential = "OAuth connected";
|
|
518
|
+
mode = "oauth";
|
|
519
|
+
authType = "subscription";
|
|
520
|
+
}
|
|
521
|
+
if (maskedCredential) {
|
|
522
|
+
// Derive base provider from profile ID prefix (e.g. "anthropic" from "anthropic:anthropic-theforever")
|
|
523
|
+
// profile.provider may contain account-specific names that don't match the model catalog
|
|
524
|
+
const baseProvider = profileId.includes(":") ? profileId.split(":")[0] : profile.provider;
|
|
525
|
+
// Check if this provider is the active one
|
|
526
|
+
const isActive = activeModel?.provider === baseProvider;
|
|
527
|
+
// Use custom name if available; otherwise include profile identifier to avoid duplicate labels
|
|
528
|
+
const profileIdentifier = profileId.includes(":") ? profileId.split(":")[1] : profileId;
|
|
529
|
+
const baseName = this.formatProviderName(baseProvider);
|
|
530
|
+
const displayName = profile.customName || `${baseName} · ${profileIdentifier}`;
|
|
531
|
+
providers.push({
|
|
532
|
+
id: profileId, // Use the full profile ID for editing
|
|
533
|
+
name: displayName,
|
|
534
|
+
configured: true,
|
|
535
|
+
mode,
|
|
536
|
+
authType,
|
|
537
|
+
maskedCredential,
|
|
538
|
+
active: isActive,
|
|
539
|
+
models: modelsByProvider.get(baseProvider) ?? [],
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
// Sort so active provider comes first, then by name
|
|
544
|
+
providers.sort((a, b) => {
|
|
545
|
+
if (a.active && !b.active)
|
|
546
|
+
return -1;
|
|
547
|
+
if (!a.active && b.active)
|
|
548
|
+
return 1;
|
|
549
|
+
return a.name.localeCompare(b.name);
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
catch (err) {
|
|
553
|
+
console.warn(`[settings] Failed to read auth-profiles.json: ${err}`);
|
|
554
|
+
}
|
|
555
|
+
return providers;
|
|
556
|
+
}
|
|
557
|
+
/** Detect whether a profile uses API key or subscription auth */
|
|
558
|
+
detectAuthType(profileId, profile) {
|
|
559
|
+
// If has OAuth tokens, it's subscription/oauth
|
|
560
|
+
if (profile.access || profile.refresh) {
|
|
561
|
+
return "subscription";
|
|
562
|
+
}
|
|
563
|
+
// Check profile ID for hints
|
|
564
|
+
const lowerProfileId = profileId.toLowerCase();
|
|
565
|
+
if (lowerProfileId.includes("-sub") || lowerProfileId.includes("sub-")) {
|
|
566
|
+
return "subscription";
|
|
567
|
+
}
|
|
568
|
+
// Check token format for Anthropic
|
|
569
|
+
if (profile.provider === "anthropic" && profile.token) {
|
|
570
|
+
// API keys: sk-ant-api03-...
|
|
571
|
+
// OAuth tokens: sk-ant-oat-...
|
|
572
|
+
// Setup tokens have different patterns
|
|
573
|
+
if (profile.token.startsWith("sk-ant-api")) {
|
|
574
|
+
return "api-key";
|
|
575
|
+
}
|
|
576
|
+
if (profile.token.startsWith("sk-ant-oat") || profile.token.startsWith("sk-ant-sid")) {
|
|
577
|
+
return "subscription";
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
// Check for OpenAI Codex (always subscription-based)
|
|
581
|
+
if (profile.provider === "openai-codex") {
|
|
582
|
+
return "subscription";
|
|
583
|
+
}
|
|
584
|
+
// Default to api-key
|
|
585
|
+
return "api-key";
|
|
586
|
+
}
|
|
587
|
+
/** Mask a credential for display */
|
|
588
|
+
maskCredential(credential) {
|
|
589
|
+
if (credential.length <= 12)
|
|
590
|
+
return "***";
|
|
591
|
+
return credential.slice(0, 7) + "..." + credential.slice(-4);
|
|
592
|
+
}
|
|
593
|
+
/** Format provider name for display */
|
|
594
|
+
formatProviderName(provider) {
|
|
595
|
+
const names = {
|
|
596
|
+
anthropic: "Anthropic",
|
|
597
|
+
openai: "OpenAI",
|
|
598
|
+
"openai-codex": "OpenAI Codex",
|
|
599
|
+
gemini: "Google Gemini",
|
|
600
|
+
google: "Google",
|
|
601
|
+
"kimi-coding": "Kimi Coding",
|
|
602
|
+
nvidia: "NVIDIA",
|
|
603
|
+
ollama: "Ollama",
|
|
604
|
+
};
|
|
605
|
+
return names[provider] ?? provider.charAt(0).toUpperCase() + provider.slice(1);
|
|
606
|
+
}
|
|
607
|
+
/** Refresh settings state from disk (call periodically to detect external changes) */
|
|
608
|
+
refreshSettingsState() {
|
|
609
|
+
this.writeSettingsState();
|
|
610
|
+
this.pushSettingsState();
|
|
611
|
+
// Also refresh sessions
|
|
612
|
+
this.listSessions();
|
|
613
|
+
}
|
|
614
|
+
/** List all active sessions with their current model */
|
|
615
|
+
listSessions() {
|
|
616
|
+
try {
|
|
617
|
+
const listResult = spawnSync("openclaw", ["gateway", "call", "sessions.list", "--params", '{"limit": 50}', "--json"], { encoding: "utf-8", timeout: 15_000 });
|
|
618
|
+
if (listResult.status !== 0) {
|
|
619
|
+
console.warn(`[settings] Failed to list sessions: ${listResult.stderr}`);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
let rawSessions = [];
|
|
623
|
+
try {
|
|
624
|
+
const parsed = JSON.parse(listResult.stdout);
|
|
625
|
+
rawSessions = parsed.sessions || [];
|
|
626
|
+
}
|
|
627
|
+
catch {
|
|
628
|
+
console.warn(`[settings] Failed to parse sessions list`);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
const globalDefault = this.getActiveModel();
|
|
632
|
+
const globalModelId = globalDefault
|
|
633
|
+
? `${globalDefault.provider}/${globalDefault.model}`
|
|
634
|
+
: null;
|
|
635
|
+
// Transform sessions for the UI
|
|
636
|
+
const sessions = rawSessions
|
|
637
|
+
.filter(s => s.kind === "direct") // Only show direct chat sessions
|
|
638
|
+
.map(s => {
|
|
639
|
+
// Combine modelProvider and model into full model ID
|
|
640
|
+
const sessionModel = s.modelProvider && s.model
|
|
641
|
+
? `${s.modelProvider}/${s.model}`
|
|
642
|
+
: s.model || globalModelId;
|
|
643
|
+
const isOverride = sessionModel !== globalModelId;
|
|
644
|
+
return {
|
|
645
|
+
key: s.key,
|
|
646
|
+
label: this.formatSessionLabel(s.key, s.agentId, s.origin?.label, s.displayName),
|
|
647
|
+
model: sessionModel,
|
|
648
|
+
modelLabel: this.formatModelLabelFromId(sessionModel),
|
|
649
|
+
isOverride,
|
|
650
|
+
lastUpdated: s.updatedAt ? new Date(s.updatedAt).toISOString() : undefined,
|
|
651
|
+
};
|
|
652
|
+
})
|
|
653
|
+
.sort((a, b) => {
|
|
654
|
+
// Sort overrides first, then by last updated
|
|
655
|
+
if (a.isOverride && !b.isOverride)
|
|
656
|
+
return -1;
|
|
657
|
+
if (!a.isOverride && b.isOverride)
|
|
658
|
+
return 1;
|
|
659
|
+
return 0;
|
|
660
|
+
});
|
|
661
|
+
// Write to state
|
|
662
|
+
const statePath = resolve(this.stateDir, "sessions.json");
|
|
663
|
+
const state = {
|
|
664
|
+
version: Date.now(),
|
|
665
|
+
timestamp: new Date().toISOString(),
|
|
666
|
+
state: {
|
|
667
|
+
sessions,
|
|
668
|
+
globalModel: globalModelId,
|
|
669
|
+
},
|
|
670
|
+
};
|
|
671
|
+
writeFileSync(statePath, JSON.stringify(state, null, 2), "utf-8");
|
|
672
|
+
this.store.setState("sessions", state);
|
|
673
|
+
}
|
|
674
|
+
catch (err) {
|
|
675
|
+
console.warn(`[settings] Failed to list sessions: ${err}`);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
/** Format a session key into a human-readable label */
|
|
679
|
+
formatSessionLabel(key, agentId, originLabel, displayName) {
|
|
680
|
+
// Use origin label if available (e.g., "Robin Spottiswoode (@robin_blocks)")
|
|
681
|
+
if (originLabel) {
|
|
682
|
+
// Extract just the name part before any ID/handle info
|
|
683
|
+
const namePart = originLabel.split(" (")[0].split(" @")[0].split(" id:")[0];
|
|
684
|
+
if (namePart && namePart.length > 0 && namePart.length < 30) {
|
|
685
|
+
return namePart;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
// Use display name if available
|
|
689
|
+
if (displayName) {
|
|
690
|
+
return displayName;
|
|
691
|
+
}
|
|
692
|
+
// Fall back to parsing the key
|
|
693
|
+
const parts = key.split(":");
|
|
694
|
+
if (parts[0] === "agent") {
|
|
695
|
+
return agentId || parts[1] || key;
|
|
696
|
+
}
|
|
697
|
+
// Channel-based session
|
|
698
|
+
const channelNames = {
|
|
699
|
+
telegram: "Telegram",
|
|
700
|
+
discord: "Discord",
|
|
701
|
+
whatsapp: "WhatsApp",
|
|
702
|
+
signal: "Signal",
|
|
703
|
+
slack: "Slack",
|
|
704
|
+
irc: "IRC",
|
|
705
|
+
};
|
|
706
|
+
return channelNames[parts[0]] || parts[0];
|
|
707
|
+
}
|
|
708
|
+
/** Format a model ID into a human-readable label */
|
|
709
|
+
formatModelLabelFromId(modelId) {
|
|
710
|
+
if (!modelId)
|
|
711
|
+
return "Unknown";
|
|
712
|
+
const aliases = {
|
|
713
|
+
"anthropic/claude-opus-4-5": "Claude Opus 4.5",
|
|
714
|
+
"anthropic/claude-opus-4-6": "Claude Opus 4.6",
|
|
715
|
+
"anthropic/claude-sonnet-4-5": "Claude Sonnet 4.5",
|
|
716
|
+
"openai-codex/gpt-5.3-codex": "Codex (GPT-5.3)",
|
|
717
|
+
"openai/gpt-5.2": "GPT-5.2",
|
|
718
|
+
"kimi-coding/k2p5": "Kimi K2.5",
|
|
719
|
+
};
|
|
720
|
+
return aliases[modelId] || modelId.split("/").pop() || modelId;
|
|
721
|
+
}
|
|
722
|
+
/** Reset a session's model override to use the global default */
|
|
723
|
+
resetSessionModel(sessionKey) {
|
|
724
|
+
const globalDefault = this.getActiveModel();
|
|
725
|
+
if (!globalDefault) {
|
|
726
|
+
console.warn(`[settings] No global default model configured`);
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
const modelId = `${globalDefault.provider}/${globalDefault.model}`;
|
|
730
|
+
const result = spawnSync("openclaw", [
|
|
731
|
+
"gateway", "call", "chat.send",
|
|
732
|
+
"--params", JSON.stringify({
|
|
733
|
+
sessionKey,
|
|
734
|
+
message: `/model ${modelId}`,
|
|
735
|
+
idempotencyKey: `clapps-reset-${sessionKey}-${Date.now()}`,
|
|
736
|
+
}),
|
|
737
|
+
], { encoding: "utf-8", timeout: 10_000 });
|
|
738
|
+
if (result.status === 0) {
|
|
739
|
+
console.log(`[settings] Reset session "${sessionKey}" to default model`);
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
console.warn(`[settings] Failed to reset session "${sessionKey}": ${result.stderr}`);
|
|
743
|
+
}
|
|
744
|
+
// Refresh session list
|
|
745
|
+
setTimeout(() => this.listSessions(), 1000);
|
|
746
|
+
}
|
|
747
|
+
/** Apply the global default model to all active sessions */
|
|
748
|
+
applyDefaultToAllSessions() {
|
|
749
|
+
const globalDefault = this.getActiveModel();
|
|
750
|
+
if (!globalDefault) {
|
|
751
|
+
console.warn(`[settings] No global default model configured`);
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
const modelId = `${globalDefault.provider}/${globalDefault.model}`;
|
|
755
|
+
this.patchAllSessionModels(modelId);
|
|
756
|
+
// Refresh session list after a delay
|
|
757
|
+
setTimeout(() => this.listSessions(), 2000);
|
|
758
|
+
}
|
|
759
|
+
/** Write settings.json state with provider status */
|
|
760
|
+
writeSettingsState() {
|
|
761
|
+
const providers = this.getConfiguredProviders();
|
|
762
|
+
const isConfigured = providers.length > 0;
|
|
763
|
+
const activeModel = this.getActiveModel();
|
|
764
|
+
// Find the active provider
|
|
765
|
+
const activeProvider = providers.find((p) => p.active);
|
|
766
|
+
// Get sessions data to include in settings state
|
|
767
|
+
const sessionsData = this.getSessionsData();
|
|
768
|
+
const state = {
|
|
769
|
+
version: Date.now(),
|
|
770
|
+
timestamp: new Date().toISOString(),
|
|
771
|
+
state: {
|
|
772
|
+
active: {
|
|
773
|
+
isConfigured,
|
|
774
|
+
provider: activeProvider?.name ?? null,
|
|
775
|
+
model: activeModel ? `${activeModel.provider}/${activeModel.model}` : null,
|
|
776
|
+
},
|
|
777
|
+
configuredProviders: providers,
|
|
778
|
+
// Include sessions directly in settings state for component access
|
|
779
|
+
sessions: sessionsData,
|
|
780
|
+
},
|
|
781
|
+
};
|
|
782
|
+
const statePath = resolve(this.stateDir, "settings.json");
|
|
783
|
+
writeFileSync(statePath, JSON.stringify(state, null, 2), "utf-8");
|
|
784
|
+
}
|
|
785
|
+
/** Get sessions data without writing to separate file */
|
|
786
|
+
getSessionsData() {
|
|
787
|
+
try {
|
|
788
|
+
const listResult = spawnSync("openclaw", ["gateway", "call", "sessions.list", "--params", '{"limit": 50}', "--json"], { encoding: "utf-8", timeout: 15_000 });
|
|
789
|
+
if (listResult.status !== 0) {
|
|
790
|
+
return { sessions: [], globalModel: null };
|
|
791
|
+
}
|
|
792
|
+
let rawSessions = [];
|
|
793
|
+
try {
|
|
794
|
+
const parsed = JSON.parse(listResult.stdout);
|
|
795
|
+
rawSessions = parsed.sessions || [];
|
|
796
|
+
}
|
|
797
|
+
catch {
|
|
798
|
+
return { sessions: [], globalModel: null };
|
|
799
|
+
}
|
|
800
|
+
const globalDefault = this.getActiveModel();
|
|
801
|
+
const globalModelId = globalDefault
|
|
802
|
+
? `${globalDefault.provider}/${globalDefault.model}`
|
|
803
|
+
: null;
|
|
804
|
+
const sessions = rawSessions
|
|
805
|
+
.filter(s => s.kind === "direct")
|
|
806
|
+
.map(s => {
|
|
807
|
+
const sessionModel = s.modelProvider && s.model
|
|
808
|
+
? `${s.modelProvider}/${s.model}`
|
|
809
|
+
: s.model || globalModelId;
|
|
810
|
+
const isOverride = sessionModel !== globalModelId;
|
|
811
|
+
return {
|
|
812
|
+
key: s.key,
|
|
813
|
+
label: this.formatSessionLabel(s.key, s.agentId, s.origin?.label, s.displayName),
|
|
814
|
+
model: sessionModel,
|
|
815
|
+
modelLabel: this.formatModelLabelFromId(sessionModel),
|
|
816
|
+
isOverride,
|
|
817
|
+
lastUpdated: s.updatedAt ? new Date(s.updatedAt).toISOString() : undefined,
|
|
818
|
+
};
|
|
819
|
+
})
|
|
820
|
+
.sort((a, b) => {
|
|
821
|
+
if (a.isOverride && !b.isOverride)
|
|
822
|
+
return -1;
|
|
823
|
+
if (!a.isOverride && b.isOverride)
|
|
824
|
+
return 1;
|
|
825
|
+
return 0;
|
|
826
|
+
});
|
|
827
|
+
return { sessions, globalModel: globalModelId };
|
|
828
|
+
}
|
|
829
|
+
catch {
|
|
830
|
+
return { sessions: [], globalModel: null };
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
/** Push settings state to in-memory store */
|
|
834
|
+
pushSettingsState() {
|
|
835
|
+
const statePath = resolve(this.stateDir, "settings.json");
|
|
836
|
+
const content = readFileSync(statePath, "utf-8");
|
|
837
|
+
this.store.setState("settings", JSON.parse(content));
|
|
838
|
+
}
|
|
839
|
+
/** Push _status state to in-memory store */
|
|
840
|
+
pushStatusState() {
|
|
841
|
+
const statePath = resolve(this.stateDir, "_status.json");
|
|
842
|
+
if (existsSync(statePath)) {
|
|
843
|
+
const content = readFileSync(statePath, "utf-8");
|
|
844
|
+
this.store.setState("_status", JSON.parse(content));
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
//# sourceMappingURL=settings-handler.js.map
|