@clappstore/connect 0.6.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/clapps/settings/README.md +74 -0
  2. package/clapps/settings/clapp.json +25 -0
  3. package/clapps/settings/components/ProviderEditor.tsx +512 -0
  4. package/clapps/settings/components/ProviderList.tsx +300 -0
  5. package/clapps/settings/components/SessionList.tsx +189 -0
  6. package/clapps/settings/handlers/settings-handler.js +760 -0
  7. package/clapps/settings/views/default.settings.view.md +38 -0
  8. package/clapps/settings/views/settings.app.md +12 -0
  9. package/dist/clapp-handler.d.ts +16 -0
  10. package/dist/clapp-handler.d.ts.map +1 -0
  11. package/dist/clapp-handler.js +2 -0
  12. package/dist/clapp-handler.js.map +1 -0
  13. package/dist/clapp-loader.d.ts +3 -0
  14. package/dist/clapp-loader.d.ts.map +1 -0
  15. package/dist/clapp-loader.js +61 -0
  16. package/dist/clapp-loader.js.map +1 -0
  17. package/dist/defaults.d.ts +9 -5
  18. package/dist/defaults.d.ts.map +1 -1
  19. package/dist/defaults.js +130 -79
  20. package/dist/defaults.js.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +57 -89
  24. package/dist/index.js.map +1 -1
  25. package/dist/server.d.ts +14 -0
  26. package/dist/server.d.ts.map +1 -0
  27. package/dist/server.js +204 -0
  28. package/dist/server.js.map +1 -0
  29. package/package.json +13 -5
  30. package/web-app/assets/index-BYIO2GGA.css +1 -0
  31. package/web-app/assets/index-kbnfuSZC.js +128 -0
  32. package/web-app/index.html +13 -0
  33. package/dist/agent-client.d.ts +0 -28
  34. package/dist/agent-client.d.ts.map +0 -1
  35. package/dist/agent-client.js +0 -159
  36. package/dist/agent-client.js.map +0 -1
  37. package/dist/agent-handler.d.ts +0 -18
  38. package/dist/agent-handler.d.ts.map +0 -1
  39. package/dist/agent-handler.js +0 -17
  40. package/dist/agent-handler.js.map +0 -1
  41. package/dist/credentials.d.ts +0 -5
  42. package/dist/credentials.d.ts.map +0 -1
  43. package/dist/credentials.js +0 -32
  44. package/dist/credentials.js.map +0 -1
  45. package/dist/intent-poller.d.ts +0 -22
  46. package/dist/intent-poller.d.ts.map +0 -1
  47. package/dist/intent-poller.js +0 -51
  48. package/dist/intent-poller.js.map +0 -1
  49. package/dist/relay-client.d.ts +0 -16
  50. package/dist/relay-client.d.ts.map +0 -1
  51. package/dist/relay-client.js +0 -121
  52. package/dist/relay-client.js.map +0 -1
  53. package/dist/settings-handler.d.ts +0 -26
  54. package/dist/settings-handler.d.ts.map +0 -1
  55. package/dist/settings-handler.js +0 -149
  56. package/dist/settings-handler.js.map +0 -1
  57. package/dist/state-watcher.d.ts +0 -23
  58. package/dist/state-watcher.d.ts.map +0 -1
  59. package/dist/state-watcher.js +0 -121
  60. package/dist/state-watcher.js.map +0 -1
@@ -0,0 +1,760 @@
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
+ const authProfilesPath = resolve(homedir(), ".openclaw", "agents", "main", "agent", "auth-profiles.json");
6
+ // --- Helper functions (closures over ctx) ---
7
+ function generateProfileId(provider, customName) {
8
+ const suffix = customName
9
+ ? customName.toLowerCase().replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-")
10
+ : "manual";
11
+ return `${provider}:${suffix}`;
12
+ }
13
+ function maskCredential(credential) {
14
+ if (credential.length <= 12)
15
+ return "***";
16
+ return credential.slice(0, 7) + "..." + credential.slice(-4);
17
+ }
18
+ function formatProviderName(provider) {
19
+ const names = {
20
+ anthropic: "Anthropic",
21
+ openai: "OpenAI",
22
+ "openai-codex": "OpenAI Codex",
23
+ gemini: "Google Gemini",
24
+ google: "Google",
25
+ "kimi-coding": "Kimi Coding",
26
+ nvidia: "NVIDIA",
27
+ ollama: "Ollama",
28
+ };
29
+ return names[provider] ?? provider.charAt(0).toUpperCase() + provider.slice(1);
30
+ }
31
+ function formatModelLabel(modelId, meta) {
32
+ const alias = typeof meta?.alias === "string" ? meta.alias : "";
33
+ const [, ...rest] = modelId.split("/");
34
+ const shortId = rest.join("/") || modelId;
35
+ return alias ? `${alias} (${shortId})` : shortId;
36
+ }
37
+ function formatModelLabelFromId(modelId) {
38
+ if (!modelId)
39
+ return "Unknown";
40
+ const aliases = {
41
+ "anthropic/claude-opus-4-5": "Claude Opus 4.5",
42
+ "anthropic/claude-opus-4-6": "Claude Opus 4.6",
43
+ "anthropic/claude-sonnet-4-5": "Claude Sonnet 4.5",
44
+ "openai-codex/gpt-5.3-codex": "Codex (GPT-5.3)",
45
+ "openai/gpt-5.2": "GPT-5.2",
46
+ "kimi-coding/k2p5": "Kimi K2.5",
47
+ };
48
+ return aliases[modelId] || modelId.split("/").pop() || modelId;
49
+ }
50
+ function formatSessionLabel(key, agentId, originLabel, displayName) {
51
+ if (originLabel) {
52
+ const namePart = originLabel.split(" (")[0].split(" @")[0].split(" id:")[0];
53
+ if (namePart && namePart.length > 0 && namePart.length < 30) {
54
+ return namePart;
55
+ }
56
+ }
57
+ if (displayName)
58
+ return displayName;
59
+ const parts = key.split(":");
60
+ if (parts[0] === "agent")
61
+ return agentId || parts[1] || key;
62
+ const channelNames = {
63
+ telegram: "Telegram",
64
+ discord: "Discord",
65
+ whatsapp: "WhatsApp",
66
+ signal: "Signal",
67
+ slack: "Slack",
68
+ irc: "IRC",
69
+ };
70
+ return channelNames[parts[0]] || parts[0];
71
+ }
72
+ function detectAuthType(profileId, profile) {
73
+ if (profile.access || profile.refresh)
74
+ return "subscription";
75
+ const lowerProfileId = profileId.toLowerCase();
76
+ if (lowerProfileId.includes("-sub") || lowerProfileId.includes("sub-"))
77
+ return "subscription";
78
+ if (profile.provider === "anthropic" && profile.token) {
79
+ if (profile.token.startsWith("sk-ant-api"))
80
+ return "api-key";
81
+ if (profile.token.startsWith("sk-ant-oat") || profile.token.startsWith("sk-ant-sid"))
82
+ return "subscription";
83
+ }
84
+ if (profile.provider === "openai-codex")
85
+ return "subscription";
86
+ return "api-key";
87
+ }
88
+ function getActiveModel() {
89
+ try {
90
+ const configPath = resolve(homedir(), ".openclaw", "openclaw.json");
91
+ if (!existsSync(configPath))
92
+ return null;
93
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
94
+ const primary = config?.agents?.defaults?.model?.primary;
95
+ if (typeof primary === "string" && primary.includes("/")) {
96
+ const [provider, ...rest] = primary.split("/");
97
+ return { provider, model: rest.join("/") };
98
+ }
99
+ }
100
+ catch {
101
+ // Ignore
102
+ }
103
+ return null;
104
+ }
105
+ function getModelCatalogByProvider() {
106
+ const map = new Map();
107
+ try {
108
+ const configPath = resolve(homedir(), ".openclaw", "openclaw.json");
109
+ if (!existsSync(configPath))
110
+ return map;
111
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
112
+ const models = config?.agents?.defaults?.models ?? {};
113
+ for (const [modelId, meta] of Object.entries(models)) {
114
+ if (typeof modelId !== "string" || !modelId.includes("/"))
115
+ continue;
116
+ const [provider] = modelId.split("/");
117
+ if (!provider)
118
+ continue;
119
+ const label = formatModelLabel(modelId, meta);
120
+ const existing = map.get(provider) ?? [];
121
+ existing.push({ id: modelId, label });
122
+ map.set(provider, existing);
123
+ }
124
+ for (const [provider, entries] of map) {
125
+ entries.sort((a, b) => a.label.localeCompare(b.label));
126
+ map.set(provider, entries);
127
+ }
128
+ }
129
+ catch {
130
+ // Ignore
131
+ }
132
+ return map;
133
+ }
134
+ function getFirstModelForProvider(provider) {
135
+ const modelsByProvider = getModelCatalogByProvider();
136
+ const normalizedProviderAliases = {
137
+ "openai codex": "openai-codex",
138
+ "google gemini": "gemini",
139
+ "kimi coding": "kimi-coding",
140
+ "kimi k2": "nvidia",
141
+ glm5: "nvidia",
142
+ };
143
+ const normalizedProvider = normalizedProviderAliases[provider] ?? provider;
144
+ const models = modelsByProvider.get(normalizedProvider) ?? [];
145
+ return models[0]?.id ?? null;
146
+ }
147
+ function getConfiguredProviders() {
148
+ const providers = [];
149
+ const activeModel = getActiveModel();
150
+ const modelsByProvider = getModelCatalogByProvider();
151
+ try {
152
+ if (!existsSync(authProfilesPath))
153
+ return providers;
154
+ const data = JSON.parse(readFileSync(authProfilesPath, "utf-8"));
155
+ for (const [profileId, profile] of Object.entries(data.profiles)) {
156
+ let maskedCred = "";
157
+ let mode = profile.type;
158
+ let authType = "api-key";
159
+ if (profile.token) {
160
+ maskedCred = maskCredential(profile.token);
161
+ mode = "token";
162
+ authType = detectAuthType(profileId, profile);
163
+ }
164
+ else if (profile.key) {
165
+ maskedCred = maskCredential(profile.key);
166
+ mode = "api_key";
167
+ authType = "api-key";
168
+ }
169
+ else if (profile.access) {
170
+ maskedCred = "OAuth connected";
171
+ mode = "oauth";
172
+ authType = "subscription";
173
+ }
174
+ if (maskedCred) {
175
+ const isActive = activeModel?.provider === profile.provider;
176
+ const profileIdentifier = profileId.includes(":") ? profileId.split(":")[1] : profileId;
177
+ const baseName = formatProviderName(profile.provider);
178
+ const displayName = profile.customName || `${baseName} · ${profileIdentifier}`;
179
+ providers.push({
180
+ id: profileId,
181
+ name: displayName,
182
+ configured: true,
183
+ mode,
184
+ authType,
185
+ maskedCredential: maskedCred,
186
+ active: isActive,
187
+ models: modelsByProvider.get(profile.provider) ?? [],
188
+ });
189
+ }
190
+ }
191
+ providers.sort((a, b) => {
192
+ if (a.active && !b.active)
193
+ return -1;
194
+ if (!a.active && b.active)
195
+ return 1;
196
+ return a.name.localeCompare(b.name);
197
+ });
198
+ }
199
+ catch (err) {
200
+ console.warn(`[settings] Failed to read auth-profiles.json: ${err}`);
201
+ }
202
+ return providers;
203
+ }
204
+ function setEnvVar(key, value) {
205
+ try {
206
+ const configPath = resolve(homedir(), ".openclaw", "openclaw.json");
207
+ let config = {};
208
+ if (existsSync(configPath)) {
209
+ config = JSON.parse(readFileSync(configPath, "utf-8"));
210
+ }
211
+ if (!config.env || typeof config.env !== "object") {
212
+ config.env = {};
213
+ }
214
+ config.env[key] = value;
215
+ if (config.meta && typeof config.meta === "object") {
216
+ config.meta.lastTouchedAt = new Date().toISOString();
217
+ }
218
+ writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
219
+ console.log(`[settings] Set ${key} in openclaw.json`);
220
+ }
221
+ catch (err) {
222
+ console.warn(`[settings] Failed to set env var ${key}: ${err}`);
223
+ }
224
+ }
225
+ function getSessionsData() {
226
+ try {
227
+ const listResult = spawnSync("openclaw", ["gateway", "call", "sessions.list", "--params", '{"limit": 50}', "--json"], { encoding: "utf-8", timeout: 15_000 });
228
+ if (listResult.status !== 0)
229
+ return { sessions: [], globalModel: null };
230
+ let rawSessions = [];
231
+ try {
232
+ const parsed = JSON.parse(listResult.stdout);
233
+ rawSessions = parsed.sessions || [];
234
+ }
235
+ catch {
236
+ return { sessions: [], globalModel: null };
237
+ }
238
+ const globalDefault = getActiveModel();
239
+ const globalModelId = globalDefault
240
+ ? `${globalDefault.provider}/${globalDefault.model}`
241
+ : null;
242
+ const sessions = rawSessions
243
+ .filter((s) => s.kind === "direct")
244
+ .map((s) => {
245
+ const sessionModel = s.modelProvider && s.model
246
+ ? `${s.modelProvider}/${s.model}`
247
+ : s.model || globalModelId;
248
+ const isOverride = sessionModel !== globalModelId;
249
+ return {
250
+ key: s.key,
251
+ label: formatSessionLabel(s.key, s.agentId, s.origin?.label, s.displayName),
252
+ model: sessionModel,
253
+ modelLabel: formatModelLabelFromId(sessionModel),
254
+ isOverride,
255
+ lastUpdated: s.updatedAt ? new Date(s.updatedAt).toISOString() : undefined,
256
+ };
257
+ })
258
+ .sort((a, b) => {
259
+ if (a.isOverride && !b.isOverride)
260
+ return -1;
261
+ if (!a.isOverride && b.isOverride)
262
+ return 1;
263
+ return 0;
264
+ });
265
+ return { sessions, globalModel: globalModelId };
266
+ }
267
+ catch {
268
+ return { sessions: [], globalModel: null };
269
+ }
270
+ }
271
+ // === Factory ===
272
+ export default function createSettingsHandler(ctx) {
273
+ function writeSettingsState() {
274
+ const providers = getConfiguredProviders();
275
+ const isConfigured = providers.length > 0;
276
+ const activeModel = getActiveModel();
277
+ const activeProvider = providers.find((p) => p.active);
278
+ const sessionsData = getSessionsData();
279
+ const state = {
280
+ version: Date.now(),
281
+ timestamp: new Date().toISOString(),
282
+ state: {
283
+ active: {
284
+ isConfigured,
285
+ provider: activeProvider?.name ?? null,
286
+ model: activeModel ? `${activeModel.provider}/${activeModel.model}` : null,
287
+ },
288
+ configuredProviders: providers,
289
+ sessions: sessionsData,
290
+ },
291
+ };
292
+ const statePath = resolve(ctx.stateDir, "settings.json");
293
+ writeFileSync(statePath, JSON.stringify(state, null, 2), "utf-8");
294
+ }
295
+ function pushSettingsState() {
296
+ const statePath = resolve(ctx.stateDir, "settings.json");
297
+ const content = readFileSync(statePath, "utf-8");
298
+ ctx.setState("settings", JSON.parse(content));
299
+ }
300
+ function pushStatusState() {
301
+ const statePath = resolve(ctx.stateDir, "_status.json");
302
+ if (existsSync(statePath)) {
303
+ const content = readFileSync(statePath, "utf-8");
304
+ ctx.setState("_status", JSON.parse(content));
305
+ }
306
+ }
307
+ function afterCredentialChange() {
308
+ writeSettingsState();
309
+ pushSettingsState();
310
+ ctx.checkAuthStatus();
311
+ pushStatusState();
312
+ }
313
+ function patchAllSessionModels(newModelId) {
314
+ try {
315
+ const listResult = spawnSync("openclaw", ["gateway", "call", "sessions.list", "--params", '{"limit": 100}', "--json"], { encoding: "utf-8", timeout: 15_000 });
316
+ if (listResult.status !== 0) {
317
+ console.warn(`[settings] Failed to list sessions: ${listResult.stderr}`);
318
+ return;
319
+ }
320
+ let sessions = [];
321
+ try {
322
+ const parsed = JSON.parse(listResult.stdout);
323
+ sessions = parsed.sessions || [];
324
+ }
325
+ catch {
326
+ console.warn(`[settings] Failed to parse sessions list`);
327
+ return;
328
+ }
329
+ const mainSessions = sessions.filter((s) => s.kind === "direct" || s.key.endsWith(":main"));
330
+ for (const session of mainSessions) {
331
+ const idempotencyKey = `clapps-model-${session.key}-${Date.now()}`;
332
+ const sendResult = spawnSync("openclaw", [
333
+ "gateway",
334
+ "call",
335
+ "chat.send",
336
+ "--params",
337
+ JSON.stringify({
338
+ sessionKey: session.key,
339
+ message: `/model ${newModelId}`,
340
+ idempotencyKey,
341
+ }),
342
+ ], { encoding: "utf-8", timeout: 10_000 });
343
+ if (sendResult.status === 0) {
344
+ console.log(`[settings] Sent /model to session "${session.key}"`);
345
+ }
346
+ else {
347
+ console.warn(`[settings] Failed to send /model to "${session.key}": ${sendResult.stderr}`);
348
+ }
349
+ }
350
+ }
351
+ catch (err) {
352
+ console.warn(`[settings] Failed to update session models: ${err}`);
353
+ }
354
+ }
355
+ function clearAgentModelOverrides(newModelId) {
356
+ try {
357
+ const configPath = resolve(homedir(), ".openclaw", "openclaw.json");
358
+ if (!existsSync(configPath))
359
+ return;
360
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
361
+ if (Array.isArray(config.agents?.list)) {
362
+ let changed = false;
363
+ for (const agent of config.agents.list) {
364
+ if (agent.model && agent.model !== newModelId) {
365
+ agent.model = newModelId;
366
+ changed = true;
367
+ console.log(`[settings] Updated agent "${agent.id}" model to ${newModelId}`);
368
+ }
369
+ }
370
+ if (changed) {
371
+ if (config.meta)
372
+ config.meta.lastTouchedAt = new Date().toISOString();
373
+ writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
374
+ console.log(`[settings] Saved openclaw.json with updated agent models`);
375
+ }
376
+ }
377
+ }
378
+ catch (err) {
379
+ console.warn(`[settings] Failed to update agent model overrides: ${err}`);
380
+ }
381
+ patchAllSessionModels(newModelId);
382
+ }
383
+ function listSessions() {
384
+ try {
385
+ const listResult = spawnSync("openclaw", ["gateway", "call", "sessions.list", "--params", '{"limit": 50}', "--json"], { encoding: "utf-8", timeout: 15_000 });
386
+ if (listResult.status !== 0) {
387
+ console.warn(`[settings] Failed to list sessions: ${listResult.stderr}`);
388
+ return;
389
+ }
390
+ let rawSessions = [];
391
+ try {
392
+ const parsed = JSON.parse(listResult.stdout);
393
+ rawSessions = parsed.sessions || [];
394
+ }
395
+ catch {
396
+ console.warn(`[settings] Failed to parse sessions list`);
397
+ return;
398
+ }
399
+ const globalDefault = getActiveModel();
400
+ const globalModelId = globalDefault
401
+ ? `${globalDefault.provider}/${globalDefault.model}`
402
+ : null;
403
+ const sessions = rawSessions
404
+ .filter((s) => s.kind === "direct")
405
+ .map((s) => {
406
+ const sessionModel = s.modelProvider && s.model
407
+ ? `${s.modelProvider}/${s.model}`
408
+ : s.model || globalModelId;
409
+ const isOverride = sessionModel !== globalModelId;
410
+ return {
411
+ key: s.key,
412
+ label: formatSessionLabel(s.key, s.agentId, s.origin?.label, s.displayName),
413
+ model: sessionModel,
414
+ modelLabel: formatModelLabelFromId(sessionModel),
415
+ isOverride,
416
+ lastUpdated: s.updatedAt ? new Date(s.updatedAt).toISOString() : undefined,
417
+ };
418
+ })
419
+ .sort((a, b) => {
420
+ if (a.isOverride && !b.isOverride)
421
+ return -1;
422
+ if (!a.isOverride && b.isOverride)
423
+ return 1;
424
+ return 0;
425
+ });
426
+ const statePath = resolve(ctx.stateDir, "sessions.json");
427
+ const state = {
428
+ version: Date.now(),
429
+ timestamp: new Date().toISOString(),
430
+ state: { sessions, globalModel: globalModelId },
431
+ };
432
+ writeFileSync(statePath, JSON.stringify(state, null, 2), "utf-8");
433
+ ctx.setState("sessions", state);
434
+ }
435
+ catch (err) {
436
+ console.warn(`[settings] Failed to list sessions: ${err}`);
437
+ }
438
+ }
439
+ // --- Credential setters ---
440
+ function setAnthropicKey(apiKey, customName, existingProfileId) {
441
+ let profiles = { version: 1, profiles: {} };
442
+ try {
443
+ if (existsSync(authProfilesPath)) {
444
+ profiles = JSON.parse(readFileSync(authProfilesPath, "utf-8"));
445
+ }
446
+ }
447
+ catch {
448
+ // Start fresh
449
+ }
450
+ const profileId = existingProfileId ?? generateProfileId("anthropic", customName);
451
+ profiles.profiles[profileId] = {
452
+ type: "token",
453
+ provider: "anthropic",
454
+ token: apiKey,
455
+ customName: customName || "Anthropic API",
456
+ };
457
+ mkdirSync(dirname(authProfilesPath), { recursive: true });
458
+ writeFileSync(authProfilesPath, JSON.stringify(profiles, null, 2), "utf-8");
459
+ console.log(`✅ Anthropic API key saved to ${authProfilesPath} (profile: ${profileId})`);
460
+ afterCredentialChange();
461
+ }
462
+ function setClaudeToken(token, customName) {
463
+ const result = spawnSync("openclaw", ["models", "auth", "paste-token", "--provider", "anthropic"], { input: token, encoding: "utf-8", timeout: 15_000 });
464
+ if (result.status !== 0) {
465
+ const msg = (result.stderr || result.error?.message || "unknown error").toString().trim();
466
+ console.error(`[settings] openclaw paste-token failed: ${msg}`);
467
+ return;
468
+ }
469
+ if (customName) {
470
+ try {
471
+ if (existsSync(authProfilesPath)) {
472
+ const profiles = JSON.parse(readFileSync(authProfilesPath, "utf-8"));
473
+ for (const [_key, profile] of Object.entries(profiles.profiles)) {
474
+ if (profile.provider === "anthropic" && (profile.access || profile.refresh)) {
475
+ profile.customName = customName;
476
+ break;
477
+ }
478
+ }
479
+ writeFileSync(authProfilesPath, JSON.stringify(profiles, null, 2), "utf-8");
480
+ }
481
+ }
482
+ catch {
483
+ // Ignore
484
+ }
485
+ }
486
+ console.log("✅ Claude subscription token saved via openclaw");
487
+ afterCredentialChange();
488
+ }
489
+ function setOpenAIKey(apiKey, customName, existingProfileId) {
490
+ let profiles = { version: 1, profiles: {} };
491
+ try {
492
+ if (existsSync(authProfilesPath)) {
493
+ profiles = JSON.parse(readFileSync(authProfilesPath, "utf-8"));
494
+ }
495
+ }
496
+ catch {
497
+ // Start fresh
498
+ }
499
+ const profileId = existingProfileId ?? generateProfileId("openai", customName);
500
+ profiles.profiles[profileId] = {
501
+ type: "token",
502
+ provider: "openai",
503
+ token: apiKey,
504
+ customName: customName || "OpenAI API",
505
+ };
506
+ setEnvVar("OPENAI_API_KEY", apiKey);
507
+ mkdirSync(dirname(authProfilesPath), { recursive: true });
508
+ writeFileSync(authProfilesPath, JSON.stringify(profiles, null, 2), "utf-8");
509
+ console.log(`✅ OpenAI API key saved to ${authProfilesPath} (profile: ${profileId})`);
510
+ afterCredentialChange();
511
+ }
512
+ function setKimiCodingKey(apiKey, customName, existingProfileId) {
513
+ let profiles = { version: 1, profiles: {} };
514
+ try {
515
+ if (existsSync(authProfilesPath)) {
516
+ profiles = JSON.parse(readFileSync(authProfilesPath, "utf-8"));
517
+ }
518
+ }
519
+ catch {
520
+ // Start fresh
521
+ }
522
+ const profileId = existingProfileId ?? generateProfileId("kimi-coding", customName);
523
+ profiles.profiles[profileId] = {
524
+ type: "token",
525
+ provider: "kimi-coding",
526
+ token: apiKey,
527
+ customName: customName || "Kimi Coding",
528
+ };
529
+ setEnvVar("KIMI_API_KEY", apiKey);
530
+ mkdirSync(dirname(authProfilesPath), { recursive: true });
531
+ writeFileSync(authProfilesPath, JSON.stringify(profiles, null, 2), "utf-8");
532
+ console.log(`✅ Kimi Coding API key saved to ${authProfilesPath} (profile: ${profileId})`);
533
+ afterCredentialChange();
534
+ }
535
+ function startOAuth(provider, customName) {
536
+ const result = spawnSync("openclaw", ["models", "auth", "login", "--provider", provider], { encoding: "utf-8", timeout: 30_000, stdio: "inherit" });
537
+ if (result.status !== 0) {
538
+ console.error(`[settings] OAuth login failed for ${provider}`);
539
+ return;
540
+ }
541
+ if (customName) {
542
+ try {
543
+ if (existsSync(authProfilesPath)) {
544
+ const profiles = JSON.parse(readFileSync(authProfilesPath, "utf-8"));
545
+ for (const [_key, profile] of Object.entries(profiles.profiles)) {
546
+ if (profile.provider === provider && (profile.access || profile.refresh)) {
547
+ profile.customName = customName;
548
+ break;
549
+ }
550
+ }
551
+ writeFileSync(authProfilesPath, JSON.stringify(profiles, null, 2), "utf-8");
552
+ }
553
+ }
554
+ catch {
555
+ // Ignore
556
+ }
557
+ }
558
+ console.log(`✅ OAuth login completed for ${provider}`);
559
+ afterCredentialChange();
560
+ }
561
+ function deleteProvider(profileId) {
562
+ try {
563
+ if (!existsSync(authProfilesPath)) {
564
+ console.warn(`[settings] No auth-profiles.json found`);
565
+ return;
566
+ }
567
+ const profiles = JSON.parse(readFileSync(authProfilesPath, "utf-8"));
568
+ if (!profiles.profiles[profileId]) {
569
+ console.warn(`[settings] Profile "${profileId}" not found`);
570
+ return;
571
+ }
572
+ delete profiles.profiles[profileId];
573
+ writeFileSync(authProfilesPath, JSON.stringify(profiles, null, 2), "utf-8");
574
+ console.log(`✅ Deleted provider profile: ${profileId}`);
575
+ afterCredentialChange();
576
+ }
577
+ catch (err) {
578
+ console.error(`[settings] Failed to delete provider: ${err}`);
579
+ }
580
+ }
581
+ function setActiveProvider(provider) {
582
+ const modelId = getFirstModelForProvider(provider);
583
+ if (!modelId) {
584
+ console.warn(`[settings] Unknown provider or no models available: ${provider}`);
585
+ return;
586
+ }
587
+ setActiveModel(modelId);
588
+ }
589
+ function setActiveModel(modelId) {
590
+ const result = spawnSync("openclaw", ["models", "set", modelId], {
591
+ encoding: "utf-8",
592
+ timeout: 20_000,
593
+ });
594
+ if (result.status !== 0) {
595
+ const msg = (result.stderr || result.error?.message || "unknown error").toString().trim();
596
+ console.error(`[settings] Failed to set default model: ${msg}`);
597
+ return;
598
+ }
599
+ clearAgentModelOverrides(modelId);
600
+ console.log(`✅ Active model set system-wide to ${modelId}`);
601
+ writeSettingsState();
602
+ pushSettingsState();
603
+ }
604
+ function resetSessionModel(sessionKey) {
605
+ const globalDefault = getActiveModel();
606
+ if (!globalDefault) {
607
+ console.warn(`[settings] No global default model configured`);
608
+ return;
609
+ }
610
+ const modelId = `${globalDefault.provider}/${globalDefault.model}`;
611
+ const result = spawnSync("openclaw", [
612
+ "gateway",
613
+ "call",
614
+ "chat.send",
615
+ "--params",
616
+ JSON.stringify({
617
+ sessionKey,
618
+ message: `/model ${modelId}`,
619
+ idempotencyKey: `clapps-reset-${sessionKey}-${Date.now()}`,
620
+ }),
621
+ ], { encoding: "utf-8", timeout: 10_000 });
622
+ if (result.status === 0) {
623
+ console.log(`[settings] Reset session "${sessionKey}" to default model`);
624
+ }
625
+ else {
626
+ console.warn(`[settings] Failed to reset session "${sessionKey}": ${result.stderr}`);
627
+ }
628
+ setTimeout(() => listSessions(), 1000);
629
+ }
630
+ function applyDefaultToAllSessions() {
631
+ const globalDefault = getActiveModel();
632
+ if (!globalDefault) {
633
+ console.warn(`[settings] No global default model configured`);
634
+ return;
635
+ }
636
+ const modelId = `${globalDefault.provider}/${globalDefault.model}`;
637
+ patchAllSessionModels(modelId);
638
+ setTimeout(() => listSessions(), 2000);
639
+ }
640
+ function refreshSettingsState() {
641
+ writeSettingsState();
642
+ pushSettingsState();
643
+ listSessions();
644
+ }
645
+ // --- Handler interface ---
646
+ return {
647
+ handleIntent(intent) {
648
+ if (!intent.intent.startsWith("settings."))
649
+ return false;
650
+ const customName = typeof intent.payload.customName === "string"
651
+ ? intent.payload.customName.trim()
652
+ : undefined;
653
+ const profileId = typeof intent.payload.profileId === "string"
654
+ ? intent.payload.profileId.trim()
655
+ : undefined;
656
+ switch (intent.intent) {
657
+ case "settings.setAnthropicKey": {
658
+ const apiKey = intent.payload.apiKey;
659
+ if (typeof apiKey !== "string" || apiKey.trim().length === 0) {
660
+ console.warn("[settings] Invalid apiKey payload, ignoring");
661
+ return true;
662
+ }
663
+ setAnthropicKey(apiKey.trim(), customName, profileId);
664
+ return true;
665
+ }
666
+ case "settings.setClaudeToken": {
667
+ const token = intent.payload.setupToken ?? intent.payload.token;
668
+ if (typeof token !== "string" || token.trim().length === 0) {
669
+ console.warn("[settings] Invalid token/setupToken payload, ignoring");
670
+ return true;
671
+ }
672
+ setClaudeToken(token.trim(), customName);
673
+ return true;
674
+ }
675
+ case "settings.setOpenAIKey": {
676
+ const apiKey = intent.payload.apiKey;
677
+ if (typeof apiKey !== "string" || apiKey.trim().length === 0) {
678
+ console.warn("[settings] Invalid apiKey payload, ignoring");
679
+ return true;
680
+ }
681
+ setOpenAIKey(apiKey.trim(), customName, profileId);
682
+ return true;
683
+ }
684
+ case "settings.setKimiCodingKey": {
685
+ const apiKey = intent.payload.apiKey;
686
+ if (typeof apiKey !== "string" || apiKey.trim().length === 0) {
687
+ console.warn("[settings] Invalid apiKey payload, ignoring");
688
+ return true;
689
+ }
690
+ setKimiCodingKey(apiKey.trim(), customName, profileId);
691
+ return true;
692
+ }
693
+ case "settings.startOAuth": {
694
+ const provider = intent.payload.provider;
695
+ if (typeof provider !== "string" || provider.trim().length === 0) {
696
+ console.warn("[settings] Invalid provider payload for OAuth, ignoring");
697
+ return true;
698
+ }
699
+ startOAuth(provider.trim(), customName);
700
+ return true;
701
+ }
702
+ case "settings.deleteProvider": {
703
+ if (!profileId) {
704
+ console.warn("[settings] Missing profileId for deleteProvider, ignoring");
705
+ return true;
706
+ }
707
+ deleteProvider(profileId);
708
+ return true;
709
+ }
710
+ case "settings.setActiveProvider": {
711
+ const provider = intent.payload.provider;
712
+ if (typeof provider !== "string" || provider.trim().length === 0) {
713
+ console.warn("[settings] Invalid provider payload, ignoring");
714
+ return true;
715
+ }
716
+ setActiveProvider(provider.trim().toLowerCase());
717
+ return true;
718
+ }
719
+ case "settings.setActiveModel": {
720
+ const model = intent.payload.model;
721
+ if (typeof model !== "string" || model.trim().length === 0) {
722
+ console.warn("[settings] Invalid model payload, ignoring");
723
+ return true;
724
+ }
725
+ setActiveModel(model.trim().toLowerCase());
726
+ return true;
727
+ }
728
+ case "settings.listSessions": {
729
+ listSessions();
730
+ return true;
731
+ }
732
+ case "settings.resetSessionModel": {
733
+ const sessionKey = intent.payload.sessionKey;
734
+ if (typeof sessionKey !== "string" || sessionKey.trim().length === 0) {
735
+ console.warn("[settings] Invalid sessionKey payload, ignoring");
736
+ return true;
737
+ }
738
+ resetSessionModel(sessionKey.trim());
739
+ return true;
740
+ }
741
+ case "settings.applyDefaultToAll": {
742
+ applyDefaultToAllSessions();
743
+ return true;
744
+ }
745
+ default:
746
+ console.warn(`[settings] Unknown settings intent: ${intent.intent}`);
747
+ return true;
748
+ }
749
+ },
750
+ onConnect() {
751
+ refreshSettingsState();
752
+ },
753
+ refresh() {
754
+ refreshSettingsState();
755
+ },
756
+ init() {
757
+ writeSettingsState();
758
+ },
759
+ };
760
+ }