@kevin5251984/guild 0.2.12

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 (70) hide show
  1. package/LICENSE +21 -0
  2. package/bin/guildd.mjs +20 -0
  3. package/cordis.yml +24 -0
  4. package/package.json +52 -0
  5. package/src/agent-file.ts +125 -0
  6. package/src/browser.ts +668 -0
  7. package/src/catalog/default-bots.ts +263 -0
  8. package/src/catalog/skills.ts +128 -0
  9. package/src/catalog/subagents.ts +70 -0
  10. package/src/chat-parts.ts +71 -0
  11. package/src/cli-args.ts +75 -0
  12. package/src/cli.ts +60 -0
  13. package/src/compact.ts +355 -0
  14. package/src/cordis.d.ts +40 -0
  15. package/src/db.ts +653 -0
  16. package/src/generate.ts +673 -0
  17. package/src/handlers.ts +1623 -0
  18. package/src/harness.ts +326 -0
  19. package/src/host-agents.ts +137 -0
  20. package/src/host-browse.ts +199 -0
  21. package/src/host-skills.ts +150 -0
  22. package/src/image-gen.ts +270 -0
  23. package/src/index.ts +12 -0
  24. package/src/llm.ts +993 -0
  25. package/src/mcp.ts +563 -0
  26. package/src/memory.ts +159 -0
  27. package/src/mention.ts +176 -0
  28. package/src/oauth.ts +1474 -0
  29. package/src/plugins/api.ts +8 -0
  30. package/src/plugins/chat.ts +31 -0
  31. package/src/plugins/harness.ts +77 -0
  32. package/src/plugins/llm.ts +50 -0
  33. package/src/plugins/mcp.ts +58 -0
  34. package/src/plugins/memory.ts +42 -0
  35. package/src/plugins/oauth.ts +47 -0
  36. package/src/plugins/server.ts +126 -0
  37. package/src/plugins/store.ts +29 -0
  38. package/src/plugins/tools.ts +79 -0
  39. package/src/public/buddy.js +432 -0
  40. package/src/public/chat.css +3045 -0
  41. package/src/public/chat.html +5834 -0
  42. package/src/public/favicon-16.png +0 -0
  43. package/src/public/favicon-16.svg +10 -0
  44. package/src/public/favicon-32.png +0 -0
  45. package/src/public/favicon.ico +0 -0
  46. package/src/public/favicon.svg +13 -0
  47. package/src/public/i18n.js +663 -0
  48. package/src/public/index.html +143 -0
  49. package/src/public/library.html +678 -0
  50. package/src/public/mcp-add.html +126 -0
  51. package/src/public/md.js +332 -0
  52. package/src/public/rpg/inn-street.jpg +0 -0
  53. package/src/public/settings.html +795 -0
  54. package/src/public/skills-add.html +212 -0
  55. package/src/public/studio.html +1181 -0
  56. package/src/public/style.css +1678 -0
  57. package/src/public/subagents-add.html +152 -0
  58. package/src/router.ts +978 -0
  59. package/src/send-budget.ts +52 -0
  60. package/src/server.ts +1 -0
  61. package/src/skill-import.ts +250 -0
  62. package/src/slash.ts +15 -0
  63. package/src/start.ts +103 -0
  64. package/src/store.ts +1208 -0
  65. package/src/subagent.ts +355 -0
  66. package/src/tools.ts +818 -0
  67. package/src/trajectory.ts +339 -0
  68. package/src/usage.ts +111 -0
  69. package/vendor/protocol/package.json +19 -0
  70. package/vendor/protocol/src/index.ts +159 -0
@@ -0,0 +1,795 @@
1
+ <!doctype html>
2
+ <html lang="zh-Hant">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <link rel="icon" href="/favicon.ico" sizes="32x32" />
7
+ <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
8
+ <link rel="icon" href="/favicon-32.png" type="image/png" sizes="32x32" />
9
+ <link rel="icon" href="/favicon-16.png" type="image/png" sizes="16x16" />
10
+ <title>Guild — 模型</title>
11
+ <link rel="stylesheet" href="/style.css" />
12
+ <script src="/i18n.js"></script>
13
+ </head>
14
+ <body class="app settings-page" data-i18n-page="settings" data-section="main">
15
+ <script>
16
+ (function () {
17
+ var p = location.pathname;
18
+ document.body.setAttribute(
19
+ "data-section",
20
+ /\/keys\/?$/.test(p) ? "keys" : /\/subs\/?$/.test(p) ? "subs" : "main",
21
+ );
22
+ })();
23
+ </script>
24
+ <aside class="sidebar">
25
+ <div class="side-brand"><a class="logo" href="/">Guild</a></div>
26
+ <nav class="side-nav">
27
+ <a href="/" data-i18n="nav.chat">大廳</a>
28
+ <a href="/studio" data-i18n="nav.studio">編制</a>
29
+ <a href="/library" data-i18n="nav.library">工坊</a>
30
+ <div class="side-group">
31
+ <span class="side-group-label" data-i18n="nav.settings">模型</span>
32
+ <a class="side-sub" href="/settings" data-sec="main" data-i18n="settings.sub.main">主模型</a>
33
+ <a class="side-sub" href="/settings/subs" data-sec="subs" data-i18n="settings.sub.subs">訂閱</a>
34
+ <a class="side-sub" href="/settings/keys" data-sec="keys" data-i18n="settings.sub.keys">API Key</a>
35
+ </div>
36
+ </nav>
37
+ <div class="sidebar-resizer" data-i18n-title="sidebar.resize" title="拖曳調整寬度"></div>
38
+ </aside>
39
+ <section class="pane">
40
+ <header class="pane-head">
41
+ <h1 data-i18n="settings.title">模型</h1>
42
+ <div class="locale-switch">
43
+ <button type="button" data-locale="zh-Hant" data-i18n="lang.zh">中</button>
44
+ <button type="button" data-locale="en" data-i18n="lang.en">EN</button>
45
+ </div>
46
+ </header>
47
+ <div class="page-body">
48
+ <p id="flash" class="muted"></p>
49
+ <p class="sub" data-i18n="settings.lede">第一次:先連訂閱或填 API key,再套用主模型。沒有模型就不能用。訂閱走 OAuth;API key 寫入 ~/.guild/models.json。</p>
50
+
51
+ <section class="panel" data-sec-panel="main">
52
+ <h2 data-i18n="settings.main">主模型</h2>
53
+ <p class="sub" data-i18n="settings.mainHint">新對話預設用這個。下面每一列會寫出實際在跑的模型。</p>
54
+ <div class="row pick">
55
+ <div>
56
+ <label data-i18n="settings.provider">供應商</label>
57
+ <select id="provider"></select>
58
+ </div>
59
+ <div>
60
+ <label data-i18n="model">模型</label>
61
+ <select id="model"></select>
62
+ </div>
63
+ <button type="button" id="apply" data-i18n="settings.apply">套用</button>
64
+ </div>
65
+ <p class="main-now" id="main-now"></p>
66
+ <div class="toggles">
67
+ <span>Reasoning</span>
68
+ <select id="reasoning">
69
+ <option value="minimal">Minimal</option>
70
+ <option value="low">Low</option>
71
+ <option value="medium">Medium</option>
72
+ <option value="high">High</option>
73
+ </select>
74
+ <label><input type="checkbox" id="fast" /> Fast</label>
75
+ </div>
76
+ <div class="panel-head" style="margin-top:1rem">
77
+ <h2 data-i18n="settings.aux">輔助模型</h2>
78
+ <button type="button" class="linkish" id="reset-aux" data-i18n="settings.auxReset">全部改回主模型</button>
79
+ </div>
80
+ <p class="sub" data-i18n="settings.auxHint">只有 Vision、Web extract、SubAgent 可以另選模型。其餘用途一律跟主模型。</p>
81
+ <div id="aux"></div>
82
+ </section>
83
+
84
+ <section class="panel" data-sec-panel="subs">
85
+ <h2 data-i18n="settings.accounts">連接帳號</h2>
86
+ <p class="sub" data-i18n="settings.accountsHint">用訂閱登入,不必複製金鑰。瀏覽器完成授權後會自動刷新 token。</p>
87
+ <div id="accounts"></div>
88
+ <div id="oauth-panel" hidden>
89
+ <p id="oauth-copy" data-i18n="settings.oauthCode">在瀏覽器開啟授權頁,輸入代碼:</p>
90
+ <p class="codebox" id="user-code"></p>
91
+ <p><a id="verify-link" target="_blank" rel="noreferrer" data-i18n="settings.openAuth">開啟授權頁</a></p>
92
+ <div id="paste-wrap" hidden>
93
+ <label data-i18n="settings.pasteHint">若瀏覽器在別台機器,貼上導向網址或 code</label>
94
+ <input id="paste-code" placeholder="http://localhost:…/?code= 或授權碼" />
95
+ <button type="button" id="paste-go" data-i18n="settings.finishLogin">完成登入</button>
96
+ </div>
97
+ </div>
98
+ </section>
99
+
100
+ <section class="panel" id="keys" data-sec-panel="keys">
101
+ <div class="panel-head">
102
+ <div>
103
+ <h2 data-i18n="settings.keys">API 金鑰供應商</h2>
104
+ <p class="sub" data-i18n="settings.keysHint">點名稱切換供應商。要加新的,點「+ 新增」。金鑰可用 $ENV 或直接貼上。</p>
105
+ </div>
106
+ </div>
107
+ <div class="key-tabs" id="provider-tabs"></div>
108
+ <div id="providers"></div>
109
+ </section>
110
+ </div>
111
+ </section>
112
+ <dialog class="pick" id="aux-dialog">
113
+ <h2 id="aux-title">更改輔助模型</h2>
114
+ <label data-i18n="settings.provider">供應商</label>
115
+ <select id="aux-provider"></select>
116
+ <label data-i18n="model">模型</label>
117
+ <select id="aux-model"></select>
118
+ <div class="dialog-actions">
119
+ <button type="button" class="secondary" id="aux-cancel" data-i18n="cancel">取消</button>
120
+ <button type="button" id="aux-save" data-i18n="settings.apply">套用</button>
121
+ </div>
122
+ </dialog>
123
+ <dialog class="pick add-provider" id="add-provider-dialog">
124
+ <h2 data-i18n="settings.addProvider">新增供應商</h2>
125
+ <p class="sub" data-i18n="settings.pickTemplate">選一家模板,會帶好 endpoint 與環境變數名稱。</p>
126
+ <div class="preset-grid" id="preset-grid"></div>
127
+ <div class="dialog-actions">
128
+ <button type="button" class="secondary" id="add-provider-cancel" data-i18n="cancel">取消</button>
129
+ </div>
130
+ </dialog>
131
+ <script>
132
+ let data = {
133
+ default: null,
134
+ reasoning: "medium",
135
+ fast: false,
136
+ aux: {},
137
+ auxRoles: [],
138
+ providers: [],
139
+ subscriptions: [],
140
+ picker: [],
141
+ recent: [],
142
+ active: null,
143
+ };
144
+ let pendingOauth = null;
145
+ let auxRole = null;
146
+ let keyTab = 0;
147
+ const PRESETS = {
148
+ openai: {
149
+ id: "openai",
150
+ name: "OpenAI",
151
+ baseUrl: "https://api.openai.com/v1",
152
+ api: "openai-completions",
153
+ apiKey: "$OPENAI_API_KEY",
154
+ models: [{ id: "gpt-4.1-mini", name: "GPT-4.1 mini" }, { id: "gpt-4.1", name: "GPT-4.1" }],
155
+ },
156
+ anthropic: {
157
+ id: "anthropic",
158
+ name: "Anthropic",
159
+ baseUrl: "https://api.anthropic.com",
160
+ api: "anthropic-messages",
161
+ apiKey: "$ANTHROPIC_API_KEY",
162
+ models: [{ id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5" }],
163
+ },
164
+ xai: {
165
+ id: "xai",
166
+ name: "xAI",
167
+ baseUrl: "https://api.x.ai/v1",
168
+ api: "openai-completions",
169
+ apiKey: "$XAI_API_KEY",
170
+ models: [{ id: "grok-4.6", name: "Grok 4.6" }, { id: "grok-4-fast", name: "Grok 4 Fast" }],
171
+ },
172
+ openrouter: {
173
+ id: "openrouter",
174
+ name: "OpenRouter",
175
+ baseUrl: "https://openrouter.ai/api/v1",
176
+ api: "openai-completions",
177
+ apiKey: "$OPENROUTER_API_KEY",
178
+ models: [{ id: "anthropic/claude-sonnet-4", name: "Claude Sonnet 4" }],
179
+ },
180
+ ollama: {
181
+ id: "ollama",
182
+ name: "Ollama",
183
+ baseUrl: "http://localhost:11434/v1",
184
+ api: "openai-completions",
185
+ apiKey: "ollama",
186
+ models: [{ id: "llama3.1:8b", name: "Llama 3.1 8B" }],
187
+ },
188
+ google: {
189
+ id: "google",
190
+ name: "Google AI Studio",
191
+ baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
192
+ api: "openai-completions",
193
+ apiKey: "$GEMINI_API_KEY",
194
+ models: [{ id: "gemini-2.5-flash", name: "Gemini 2.5 Flash" }],
195
+ },
196
+ groq: {
197
+ id: "groq",
198
+ name: "Groq",
199
+ baseUrl: "https://api.groq.com/openai/v1",
200
+ api: "openai-completions",
201
+ apiKey: "$GROQ_API_KEY",
202
+ models: [{ id: "llama-3.3-70b-versatile", name: "Llama 3.3 70B" }],
203
+ },
204
+ deepseek: {
205
+ id: "deepseek",
206
+ name: "DeepSeek",
207
+ baseUrl: "https://api.deepseek.com/v1",
208
+ api: "openai-completions",
209
+ apiKey: "$DEEPSEEK_API_KEY",
210
+ models: [{ id: "deepseek-chat", name: "DeepSeek Chat" }],
211
+ },
212
+ custom: {
213
+ id: "custom",
214
+ name: "Custom",
215
+ baseUrl: "https://api.example.com/v1",
216
+ api: "openai-completions",
217
+ apiKey: "",
218
+ models: [{ id: "my-model", name: "" }],
219
+ },
220
+ };
221
+
222
+ function flash(kind, text) {
223
+ const el = document.getElementById("flash");
224
+ el.className = kind;
225
+ el.textContent = text;
226
+ }
227
+ function escapeHtml(value) {
228
+ return String(value)
229
+ .replace(/&/g, "&amp;")
230
+ .replace(/</g, "&lt;")
231
+ .replace(/>/g, "&gt;")
232
+ .replace(/"/g, "&quot;");
233
+ }
234
+ function pickerGroups() {
235
+ return data.picker || [];
236
+ }
237
+ function fillSelect(sel, groups, preferred) {
238
+ sel.innerHTML = groups
239
+ .map((p) => {
240
+ const mark = p.ready ? "" : t("settings.notLoggedIn");
241
+ return (
242
+ '<option value="' +
243
+ escapeHtml(p.id) +
244
+ '"' +
245
+ (p.ready ? "" : " data-locked=1") +
246
+ ">" +
247
+ escapeHtml((p.name || p.id) + mark) +
248
+ "</option>"
249
+ );
250
+ })
251
+ .join("");
252
+ const pid = (preferred && preferred.provider) || groups.find((p) => p.ready)?.id || groups[0]?.id || "";
253
+ if (pid) sel.value = pid;
254
+ fillModels(sel, document.getElementById(sel.id === "provider" ? "model" : "aux-model"), preferred);
255
+ }
256
+ function fillModels(psel, msel, preferred) {
257
+ const group = pickerGroups().find((p) => p.id === psel.value);
258
+ msel.innerHTML = ((group && group.models) || [])
259
+ .map(
260
+ (m) =>
261
+ '<option value="' +
262
+ escapeHtml(m.id) +
263
+ '">' +
264
+ escapeHtml(m.name || m.id) +
265
+ "</option>",
266
+ )
267
+ .join("");
268
+ if (preferred && preferred.provider === psel.value) msel.value = preferred.model;
269
+ }
270
+ function currentRef() {
271
+ const provider = document.getElementById("provider").value;
272
+ const model = document.getElementById("model").value;
273
+ if (!provider || !model) return null;
274
+ return { provider, model };
275
+ }
276
+ function groupReady(id) {
277
+ return Boolean((data.picker || []).find((p) => p.id === id && p.ready));
278
+ }
279
+ function modelLabel(ref) {
280
+ if (!ref || !ref.provider || !ref.model) return t("settings.noDefault");
281
+ const group = pickerGroups().find((p) => p.id === ref.provider);
282
+ const hit = ((group && group.models) || []).find((m) => m.id === ref.model);
283
+ const name = (hit && hit.name) || ref.model;
284
+ const vendor = (group && group.name) || ref.provider;
285
+ return name + " · " + vendor;
286
+ }
287
+ function modelWire(ref) {
288
+ if (!ref || !ref.provider || !ref.model) return "";
289
+ return ref.provider + " / " + ref.model;
290
+ }
291
+ function effectiveAux(roleId) {
292
+ const own = (data.aux || {})[roleId];
293
+ if (own && own.provider && own.model) {
294
+ return { ref: own, inherited: false };
295
+ }
296
+ return { ref: data.default, inherited: true };
297
+ }
298
+ function uniqueId(base) {
299
+ const used = new Set((data.providers || []).map((p) => p.id));
300
+ if (!used.has(base)) return base;
301
+ let n = 2;
302
+ while (used.has(base + "-" + n)) n += 1;
303
+ return base + "-" + n;
304
+ }
305
+ function renderAccounts() {
306
+ document.getElementById("accounts").innerHTML = (data.subscriptions || [])
307
+ .map((s) => {
308
+ const badge = s.connected || s.ready
309
+ ? '<span class="badge">' + t("settings.connected") + "</span>"
310
+ : s.pending
311
+ ? '<span class="badge wait">' + t("settings.pending") + "</span>"
312
+ : "";
313
+ const actions = s.connected || s.ready
314
+ ? '<button type="button" class="secondary" data-logout="' + s.id + '">' + t("settings.logout") + "</button>"
315
+ : '<button type="button" data-login="' + s.id + '">' + t("settings.connect") + "</button>";
316
+ return (
317
+ '<div class="account"><div class="account-copy"><strong>' +
318
+ escapeHtml(s.name) +
319
+ badge +
320
+ "</strong><span class='sub'>" +
321
+ escapeHtml(s.hint || "") +
322
+ (s.importHint ? " · " + escapeHtml(s.importHint) : "") +
323
+ "</span>" +
324
+ (s.loginHint ? '<p class="warn">' + escapeHtml(s.loginHint) + "</p>" : "") +
325
+ "</div><div class='account-act'>" +
326
+ actions +
327
+ "</div></div>"
328
+ );
329
+ })
330
+ .join("");
331
+ }
332
+ function renderPicker() {
333
+ fillSelect(document.getElementById("provider"), pickerGroups(), data.default);
334
+ document.getElementById("reasoning").value = data.reasoning || "medium";
335
+ document.getElementById("fast").checked = Boolean(data.fast);
336
+ const mainNow = document.getElementById("main-now");
337
+ if (mainNow) {
338
+ mainNow.textContent = t("settings.currentMain", {
339
+ name: modelLabel(data.default),
340
+ });
341
+ }
342
+ const rows = (data.auxRoles || [])
343
+ .map((role) => {
344
+ const eff = effectiveAux(role.id);
345
+ const revert = eff.inherited
346
+ ? ""
347
+ : '<button type="button" class="linkish" data-aux-main="' +
348
+ role.id +
349
+ '">' +
350
+ t("settings.useMain") +
351
+ "</button>";
352
+ const source = eff.inherited
353
+ ? '<span class="badge">' + t("settings.inherited") + "</span>"
354
+ : '<span class="badge wait">' + t("settings.override") + "</span>";
355
+ const wire = modelWire(eff.ref);
356
+ return (
357
+ '<div class="aux-row">' +
358
+ '<div class="aux-task"><strong>' +
359
+ escapeHtml(role.name) +
360
+ '</strong><span class="tag">' +
361
+ escapeHtml(role.hint) +
362
+ "</span></div>" +
363
+ '<div class="aux-now"><b>' +
364
+ escapeHtml(modelLabel(eff.ref)) +
365
+ "</b>" +
366
+ (wire ? '<span class="id">' + escapeHtml(wire) + "</span>" : "") +
367
+ "</div>" +
368
+ '<div class="aux-act">' +
369
+ source +
370
+ revert +
371
+ '<button type="button" class="linkish" data-aux-change="' +
372
+ role.id +
373
+ '">' +
374
+ t("settings.change") +
375
+ "</button></div></div>"
376
+ );
377
+ })
378
+ .join("");
379
+ document.getElementById("aux").innerHTML =
380
+ '<div class="aux-head-row"><span>' +
381
+ t("settings.task") +
382
+ "</span><span>" +
383
+ t("settings.using") +
384
+ "</span><span></span></div>" +
385
+ rows;
386
+ }
387
+ function maskApiKey(value) {
388
+ const key = String(value || "");
389
+ if (key.length <= 10) return key;
390
+ return key.slice(0, 5) + "…" + key.slice(-5);
391
+ }
392
+ function storedBadge(p) {
393
+ if (p.stored === "env") return '<span class="badge">env</span>';
394
+ if (p.stored === "literal") return '<span class="badge">' + t("settings.filled") + "</span>";
395
+ return '<span class="badge empty">' + t("settings.emptyKey") + "</span>";
396
+ }
397
+ function cardHtml(p, index) {
398
+ const models = (p.models || [])
399
+ .map((m, i) => {
400
+ return (
401
+ '<div class="model-row"><input data-p="' + index + '" data-m="' + i +
402
+ '" data-f="id" value="' + escapeHtml(m.id || "") +
403
+ '" placeholder="model id" /><input data-p="' + index + '" data-m="' + i +
404
+ '" data-f="name" value="' + escapeHtml(m.name || "") +
405
+ '" placeholder="' + t("settings.displayName") + '" /><button type="button" class="secondary" data-del-model="' +
406
+ index + ":" + i + '">×</button></div>'
407
+ );
408
+ })
409
+ .join("");
410
+ return (
411
+ '<div class="provider-card" data-index="' + index + '">' +
412
+ '<div class="card-head"><h3>' + escapeHtml(p.name || p.id) + storedBadge(p) +
413
+ '</h3><button type="button" class="secondary" data-del="' + index + '">' + t("settings.delete") + "</button></div>" +
414
+ "<label>ID</label><input data-p=\"" + index + '" data-f="id" value="' + escapeHtml(p.id) + '" />' +
415
+ "<label>" + t("settings.name") + "</label><input data-p=\"" + index + '" data-f="name" value="' + escapeHtml(p.name || "") + '" />' +
416
+ "<label>baseUrl</label><input data-p=\"" + index + '" data-f="baseUrl" value="' + escapeHtml(p.baseUrl || "") + '" />' +
417
+ '<div class="row-2"><div><label>API</label><select data-p="' + index + '" data-f="api">' +
418
+ '<option value="openai-completions"' + (p.api !== "anthropic-messages" && p.api !== "openai-responses" ? " selected" : "") + ">openai-completions</option>" +
419
+ '<option value="anthropic-messages"' + (p.api === "anthropic-messages" ? " selected" : "") + ">anthropic-messages</option>" +
420
+ '<option value="openai-responses"' + (p.api === "openai-responses" ? " selected" : "") + ">openai-responses</option></select></div>" +
421
+ '<div><label>API key</label><input type="text" data-p="' + index + '" data-f="apiKey" spellcheck="false" autocomplete="off" autocapitalize="off" value="' +
422
+ escapeHtml(p.apiKeyPreview || "") + '" placeholder="$ENV_VAR 或 sk-…" /></div></div>' +
423
+ "<label>" + t("model") + "</label>" + models +
424
+ '<button type="button" class="secondary" data-add-model="' + index + '" style="margin-top:0.55rem">' + t("settings.addModel") + "</button>" +
425
+ '<div class="card-foot"><p class="hint">' + t("settings.saveKeyHint") + "</p>" +
426
+ '<button type="button" data-save-keys="' + index + '">' +
427
+ t("settings.saveKey") +
428
+ "</button></div></div>"
429
+ );
430
+ }
431
+ function flushVisibleCard() {
432
+ const card = document.querySelector(".provider-card");
433
+ if (!card) return;
434
+ const index = Number(card.getAttribute("data-index"));
435
+ const p = (data.providers || [])[index];
436
+ if (!p) return;
437
+ const id = card.querySelector('[data-f="id"]').value.trim();
438
+ if (id) p.id = id;
439
+ p.name = card.querySelector('[data-f="name"]').value.trim();
440
+ p.baseUrl = card.querySelector('[data-f="baseUrl"]').value.trim();
441
+ p.api = card.querySelector('[data-f="api"]').value;
442
+ const key = card.querySelector('[data-f="apiKey"]').value.trim();
443
+ if (key && key !== (p.apiKeyPreview || "")) {
444
+ p.apiKey = key;
445
+ p.apiKeyPreview = maskApiKey(key);
446
+ }
447
+ const models = [];
448
+ card.querySelectorAll(".model-row").forEach((row) => {
449
+ const mid = row.querySelector('[data-f="id"]').value.trim();
450
+ if (!mid) return;
451
+ models.push({
452
+ id: mid,
453
+ name: row.querySelector('[data-f="name"]').value.trim(),
454
+ });
455
+ });
456
+ p.models = models;
457
+ }
458
+ function collectKeys() {
459
+ flushVisibleCard();
460
+ const providers = {};
461
+ (data.providers || []).forEach((p) => {
462
+ if (!p.id) return;
463
+ providers[p.id] = {
464
+ name: p.name || "",
465
+ baseUrl: p.baseUrl || "",
466
+ api: p.api || "openai-completions",
467
+ apiKey: p.apiKey || "",
468
+ models: p.models || [],
469
+ };
470
+ });
471
+ return providers;
472
+ }
473
+ function presetLabel(key, preset) {
474
+ return key === "custom" ? t("settings.custom") : (preset && preset.name) || key;
475
+ }
476
+ function presetHost(url) {
477
+ try {
478
+ return new URL(url).host;
479
+ } catch (err) {
480
+ return "";
481
+ }
482
+ }
483
+ function renderPresetGrid() {
484
+ const grid = document.getElementById("preset-grid");
485
+ if (!grid) return;
486
+ grid.innerHTML = Object.keys(PRESETS)
487
+ .map((key) => {
488
+ const preset = PRESETS[key];
489
+ const host = key === "custom" ? "" : presetHost(preset.baseUrl);
490
+ return (
491
+ '<button type="button" data-preset="' +
492
+ escapeHtml(key) +
493
+ '">' +
494
+ escapeHtml(presetLabel(key, preset)) +
495
+ (host ? '<span class="hint">' + escapeHtml(host) + "</span>" : "") +
496
+ "</button>"
497
+ );
498
+ })
499
+ .join("");
500
+ }
501
+ function addFromPreset(key) {
502
+ flushVisibleCard();
503
+ const preset = PRESETS[key] || PRESETS.custom;
504
+ const copy = JSON.parse(JSON.stringify(preset));
505
+ copy.id = uniqueId(copy.id);
506
+ copy.name = presetLabel(key, copy);
507
+ copy.stored = copy.apiKey
508
+ ? String(copy.apiKey).startsWith("$")
509
+ ? "env"
510
+ : "empty"
511
+ : "empty";
512
+ data.providers.push(copy);
513
+ keyTab = data.providers.length - 1;
514
+ render();
515
+ }
516
+ function renderKeys() {
517
+ const list = data.providers || [];
518
+ if (keyTab < 0 || keyTab >= list.length) keyTab = Math.max(0, list.length - 1);
519
+ const chips = list
520
+ .map((p, i) => {
521
+ return (
522
+ '<button type="button" class="key-tab' +
523
+ (i === keyTab ? " on" : "") +
524
+ '" data-key-tab="' +
525
+ i +
526
+ '">' +
527
+ escapeHtml(p.name || p.id) +
528
+ storedBadge(p) +
529
+ "</button>"
530
+ );
531
+ })
532
+ .join("");
533
+ document.getElementById("provider-tabs").innerHTML =
534
+ chips +
535
+ '<button type="button" class="key-tab add" data-add-provider="1">' +
536
+ t("settings.addChip") +
537
+ "</button>";
538
+ const current = list[keyTab];
539
+ document.getElementById("providers").innerHTML = current
540
+ ? cardHtml(current, keyTab)
541
+ : '<p class="sub">' + t("settings.noProviders") + "</p>";
542
+ }
543
+ function settingsSection() {
544
+ const path = location.pathname.replace(/\/$/, "") || "/";
545
+ if (path.endsWith("/keys")) return "keys";
546
+ if (path.endsWith("/subs")) return "subs";
547
+ return "main";
548
+ }
549
+ function showSection() {
550
+ const sec = settingsSection();
551
+ document.body.setAttribute("data-section", sec);
552
+ document.querySelectorAll(".side-sub").forEach((a) => {
553
+ a.classList.toggle("on", a.getAttribute("data-sec") === sec);
554
+ });
555
+ const titles = {
556
+ main: t("settings.sub.main"),
557
+ subs: t("settings.sub.subs"),
558
+ keys: t("settings.sub.keys"),
559
+ };
560
+ const h1 = document.querySelector(".pane-head h1");
561
+ if (h1) h1.textContent = titles[sec] || t("settings.title");
562
+ document.title = "Guild — " + (titles[sec] || t("settings.title"));
563
+ }
564
+ function render() {
565
+ showSection();
566
+ renderAccounts();
567
+ renderPicker();
568
+ renderKeys();
569
+ renderPresetGrid();
570
+ }
571
+ async function load() {
572
+ const res = await fetch("/settings/models");
573
+ data = await res.json();
574
+ render();
575
+ }
576
+ async function applyPicker(extra) {
577
+ const ref = currentRef();
578
+ if (ref && !groupReady(ref.provider)) {
579
+ flash("error", t("settings.notReady"));
580
+ return;
581
+ }
582
+ const res = await fetch("/settings/models", {
583
+ method: "PUT",
584
+ headers: { "content-type": "application/json" },
585
+ body: JSON.stringify({
586
+ default: ref,
587
+ reasoning: document.getElementById("reasoning").value,
588
+ fast: document.getElementById("fast").checked,
589
+ aux: extra && extra.aux !== undefined ? extra.aux : data.aux || {},
590
+ }),
591
+ });
592
+ const body = await res.json();
593
+ if (!res.ok) { flash("error", body.error || "apply failed"); return; }
594
+ data = body;
595
+ render();
596
+ flash("health", t("settings.applied", { name: ref ? ref.provider + " / " + ref.model : t("settings.appliedDefault") }));
597
+ }
598
+ document.getElementById("provider").addEventListener("change", () => {
599
+ fillModels(
600
+ document.getElementById("provider"),
601
+ document.getElementById("model"),
602
+ data.default,
603
+ );
604
+ });
605
+ document.getElementById("apply").addEventListener("click", () => applyPicker());
606
+ document.getElementById("reset-aux").addEventListener("click", async () => {
607
+ await applyPicker({ aux: {} });
608
+ });
609
+ document.getElementById("aux").addEventListener("click", async (event) => {
610
+ const main = event.target.closest("[data-aux-main]");
611
+ if (main) {
612
+ const role = main.getAttribute("data-aux-main");
613
+ const aux = { ...(data.aux || {}) };
614
+ aux[role] = null;
615
+ await applyPicker({ aux });
616
+ return;
617
+ }
618
+ const change = event.target.closest("[data-aux-change]");
619
+ if (!change) return;
620
+ auxRole = change.getAttribute("data-aux-change");
621
+ const role = (data.auxRoles || []).find((r) => r.id === auxRole);
622
+ document.getElementById("aux-title").textContent = t("settings.auxChangeName", { name: (role && role.name) || auxRole });
623
+ fillSelect(
624
+ document.getElementById("aux-provider"),
625
+ pickerGroups(),
626
+ (data.aux || {})[auxRole] || data.default,
627
+ );
628
+ document.getElementById("aux-dialog").showModal();
629
+ });
630
+ document.getElementById("aux-provider").addEventListener("change", () => {
631
+ fillModels(
632
+ document.getElementById("aux-provider"),
633
+ document.getElementById("aux-model"),
634
+ (data.aux || {})[auxRole],
635
+ );
636
+ });
637
+ document.getElementById("aux-cancel").addEventListener("click", () => {
638
+ document.getElementById("aux-dialog").close();
639
+ });
640
+ document.getElementById("aux-save").addEventListener("click", async () => {
641
+ const provider = document.getElementById("aux-provider").value;
642
+ const model = document.getElementById("aux-model").value;
643
+ if (!groupReady(provider)) {
644
+ flash("error", t("settings.connectFirst"));
645
+ return;
646
+ }
647
+ const aux = { ...(data.aux || {}) };
648
+ aux[auxRole] = { provider, model };
649
+ document.getElementById("aux-dialog").close();
650
+ await applyPicker({ aux });
651
+ });
652
+ document.getElementById("accounts").addEventListener("click", async (event) => {
653
+ const login = event.target.closest("[data-login]");
654
+ const logout = event.target.closest("[data-logout]");
655
+ if (login) {
656
+ const id = login.getAttribute("data-login");
657
+ const res = await fetch("/settings/oauth/" + id + "/login", { method: "POST" });
658
+ const body = await res.json();
659
+ if (!res.ok) { flash("error", body.error || "login failed"); return; }
660
+ pendingOauth = id;
661
+ showOauthPanel(body);
662
+ flash("muted", t("settings.waitingAuth"));
663
+ poll(id);
664
+ }
665
+ if (logout) {
666
+ await fetch("/settings/oauth/" + logout.getAttribute("data-logout") + "/logout", { method: "POST" });
667
+ await load();
668
+ }
669
+ });
670
+ function showOauthPanel(body) {
671
+ const panel = document.getElementById("oauth-panel");
672
+ panel.hidden = false;
673
+ const code = body.userCode || "";
674
+ document.getElementById("user-code").textContent = code;
675
+ document.getElementById("user-code").hidden = !code;
676
+ document.getElementById("oauth-copy").textContent = code
677
+ ? t("settings.oauthCode")
678
+ : t("settings.oauthBrowser");
679
+ const link = document.getElementById("verify-link");
680
+ link.href = body.verificationUri || "#";
681
+ const paste = document.getElementById("paste-wrap");
682
+ paste.hidden = body.flow !== "pkce";
683
+ document.getElementById("paste-code").value = "";
684
+ }
685
+ async function poll(id) {
686
+ const res = await fetch("/settings/oauth/" + id + "/poll", { method: "POST" });
687
+ const body = await res.json();
688
+ if (body.connected || body.ready) {
689
+ document.getElementById("oauth-panel").hidden = true;
690
+ flash("health", t("settings.connectedId", { id: id }));
691
+ await load();
692
+ return;
693
+ }
694
+ if (body.error) {
695
+ flash("error", body.error);
696
+ return;
697
+ }
698
+ if (body.pending) setTimeout(() => poll(id), 3000);
699
+ }
700
+ document.getElementById("paste-go").addEventListener("click", async () => {
701
+ if (!pendingOauth) return;
702
+ const res = await fetch("/settings/oauth/" + pendingOauth + "/complete", {
703
+ method: "POST",
704
+ headers: { "content-type": "application/json" },
705
+ body: JSON.stringify({ url: document.getElementById("paste-code").value }),
706
+ });
707
+ const body = await res.json();
708
+ if (!res.ok) { flash("error", body.error || "complete failed"); return; }
709
+ document.getElementById("oauth-panel").hidden = true;
710
+ flash("health", t("settings.connectedId", { id: pendingOauth }));
711
+ await load();
712
+ });
713
+ document.getElementById("provider-tabs").addEventListener("click", (event) => {
714
+ if (event.target.closest("[data-add-provider]")) {
715
+ renderPresetGrid();
716
+ document.getElementById("add-provider-dialog").showModal();
717
+ return;
718
+ }
719
+ const tab = event.target.closest("[data-key-tab]");
720
+ if (!tab) return;
721
+ flushVisibleCard();
722
+ keyTab = Number(tab.getAttribute("data-key-tab"));
723
+ renderKeys();
724
+ });
725
+ document.getElementById("preset-grid").addEventListener("click", (event) => {
726
+ const btn = event.target.closest("[data-preset]");
727
+ if (!btn) return;
728
+ addFromPreset(btn.getAttribute("data-preset"));
729
+ document.getElementById("add-provider-dialog").close();
730
+ });
731
+ document.getElementById("add-provider-cancel").addEventListener("click", () => {
732
+ document.getElementById("add-provider-dialog").close();
733
+ });
734
+ async function saveProviderCard(index) {
735
+ const card = document.querySelector('.provider-card[data-index="' + index + '"]');
736
+ const label = card
737
+ ? (card.querySelector('[data-f="name"]')?.value ||
738
+ card.querySelector('[data-f="id"]')?.value ||
739
+ t("settings.thisProvider")).trim()
740
+ : t("settings.thisProvider");
741
+ const res = await fetch("/settings/models", {
742
+ method: "PUT",
743
+ headers: { "content-type": "application/json" },
744
+ body: JSON.stringify({ providers: collectKeys() }),
745
+ });
746
+ const body = await res.json();
747
+ if (!res.ok) { flash("error", body.error || "save failed"); return; }
748
+ data = body;
749
+ render();
750
+ flash("health", t("settings.savedKeyFlash", { name: label }));
751
+ }
752
+ document.getElementById("providers").addEventListener("focusin", (event) => {
753
+ const input = event.target.closest('[data-f="apiKey"]');
754
+ if (input) input.select();
755
+ });
756
+ document.getElementById("providers").addEventListener("focusout", (event) => {
757
+ const input = event.target.closest('[data-f="apiKey"]');
758
+ if (!input) return;
759
+ flushVisibleCard();
760
+ const card = input.closest(".provider-card");
761
+ const index = card ? Number(card.getAttribute("data-index")) : -1;
762
+ const p = (data.providers || [])[index];
763
+ if (p) input.value = p.apiKeyPreview || "";
764
+ });
765
+ document.getElementById("providers").addEventListener("click", (event) => {
766
+ const el = event.target;
767
+ if (el.closest("[data-save-keys]")) {
768
+ const i = Number(el.closest("[data-save-keys]").getAttribute("data-save-keys"));
769
+ saveProviderCard(i);
770
+ return;
771
+ }
772
+ if (el.closest("[data-del]")) {
773
+ const i = Number(el.closest("[data-del]").getAttribute("data-del"));
774
+ data.providers.splice(i, 1);
775
+ if (keyTab >= data.providers.length) keyTab = Math.max(0, data.providers.length - 1);
776
+ render();
777
+ }
778
+ if (el.closest("[data-add-model]")) {
779
+ flushVisibleCard();
780
+ const i = Number(el.closest("[data-add-model]").getAttribute("data-add-model"));
781
+ data.providers[i].models.push({ id: "", name: "" });
782
+ render();
783
+ }
784
+ if (el.closest("[data-del-model]")) {
785
+ flushVisibleCard();
786
+ const [pi, mi] = el.closest("[data-del-model]").getAttribute("data-del-model").split(":").map(Number);
787
+ data.providers[pi].models.splice(mi, 1);
788
+ render();
789
+ }
790
+ });
791
+ window.addEventListener("guild-locale", render);
792
+ load().catch((err) => flash("error", err.message));
793
+ </script>
794
+ </body>
795
+ </html>