@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.
Files changed (41) hide show
  1. package/dist/agent-client.d.ts +6 -0
  2. package/dist/agent-client.d.ts.map +1 -1
  3. package/dist/agent-client.js +109 -18
  4. package/dist/agent-client.js.map +1 -1
  5. package/dist/auth.d.ts +18 -0
  6. package/dist/auth.d.ts.map +1 -0
  7. package/dist/auth.js +248 -0
  8. package/dist/auth.js.map +1 -0
  9. package/dist/chat-handler.d.ts +52 -0
  10. package/dist/chat-handler.d.ts.map +1 -0
  11. package/dist/chat-handler.js +453 -0
  12. package/dist/chat-handler.js.map +1 -0
  13. package/dist/defaults.d.ts +1 -1
  14. package/dist/defaults.d.ts.map +1 -1
  15. package/dist/defaults.js +36 -23
  16. package/dist/defaults.js.map +1 -1
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +53 -30
  20. package/dist/index.js.map +1 -1
  21. package/dist/server.d.ts +1 -0
  22. package/dist/server.d.ts.map +1 -1
  23. package/dist/server.js +88 -7
  24. package/dist/server.js.map +1 -1
  25. package/dist/settings-handler.d.ts +76 -0
  26. package/dist/settings-handler.d.ts.map +1 -0
  27. package/dist/settings-handler.js +848 -0
  28. package/dist/settings-handler.js.map +1 -0
  29. package/package.json +4 -8
  30. package/web-app/assets/{index-CEpgiIwf.js → index-CWzlxjUK.js} +86 -56
  31. package/web-app/assets/index-Cic64hbc.css +1 -0
  32. package/web-app/index.html +2 -2
  33. package/clapps/settings/README.md +0 -74
  34. package/clapps/settings/clapp.json +0 -25
  35. package/clapps/settings/components/ProviderEditor.tsx +0 -512
  36. package/clapps/settings/components/ProviderList.tsx +0 -300
  37. package/clapps/settings/components/SessionList.tsx +0 -189
  38. package/clapps/settings/handlers/settings-handler.js +0 -742
  39. package/clapps/settings/views/default.settings.view.md +0 -35
  40. package/clapps/settings/views/settings.app.md +0 -12
  41. 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