@cremini/skillpack 1.2.0 → 1.2.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.
package/dist/cli.js CHANGED
@@ -9,89 +9,46 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
- // src/runtime/adapters/attachment-utils.ts
12
+ // src/runtime/config.ts
13
13
  import fs5 from "fs";
14
14
  import path5 from "path";
15
- import { pipeline } from "stream/promises";
16
- import { Readable } from "stream";
17
- function getAttachmentDir(rootDir, channelId) {
18
- return path5.resolve(rootDir, "data", "sessions", channelId, ATTACHMENTS_DIR);
19
- }
20
- function sanitizeFilename(name) {
21
- return name.replace(/[/\\:*?"<>|]/g, "_").replace(/\s+/g, "_");
22
- }
23
- async function downloadAndSaveAttachment(rootDir, channelId, url, filename, mimeType, headers) {
24
- const dir = getAttachmentDir(rootDir, channelId);
25
- fs5.mkdirSync(dir, { recursive: true });
26
- const ts = Date.now();
27
- const safeName = sanitizeFilename(filename);
28
- const storedName = `${ts}-${safeName}`;
29
- const fullPath = path5.join(dir, storedName);
30
- const response = await fetch(url, { headers });
31
- if (!response.ok) {
32
- throw new Error(
33
- `Failed to download attachment from ${url}: ${response.status} ${response.statusText}`
34
- );
35
- }
36
- const body = response.body;
37
- if (!body) {
38
- throw new Error(`Empty response body when downloading ${url}`);
39
- }
40
- const nodeStream = Readable.fromWeb(body);
41
- const writeStream = fs5.createWriteStream(fullPath);
42
- await pipeline(nodeStream, writeStream);
43
- const stats = fs5.statSync(fullPath);
44
- const detectedMime = mimeType || response.headers.get("content-type")?.split(";")[0] || void 0;
45
- return {
46
- filename,
47
- localPath: fullPath,
48
- mimeType: detectedMime,
49
- size: stats.size
50
- };
51
- }
52
- function isImageMime(mimeType) {
53
- return !!mimeType && mimeType.startsWith("image/");
54
- }
55
- function formatSize(bytes) {
56
- if (bytes === void 0 || bytes === null) return "";
57
- if (bytes < 1024) return `${bytes}B`;
58
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
59
- return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
60
- }
61
- function formatAttachmentsPrompt(attachments) {
62
- if (attachments.length === 0) return "";
63
- const lines = attachments.map((a) => {
64
- const meta = [a.mimeType, formatSize(a.size)].filter(Boolean).join(", ");
65
- return `- ${a.filename} (${meta}) \u2192 ${a.localPath}`;
66
- });
67
- return `[Attachments]
68
- ${lines.join("\n")}`;
69
- }
70
- function attachmentsToImageContent(attachments) {
71
- return attachments.filter((a) => isImageMime(a.mimeType)).map((a) => {
72
- const buffer = fs5.readFileSync(a.localPath);
73
- return {
74
- type: "image",
75
- data: buffer.toString("base64"),
76
- mimeType: a.mimeType || "image/png"
77
- };
78
- });
79
- }
80
- var ATTACHMENTS_DIR;
81
- var init_attachment_utils = __esm({
82
- "src/runtime/adapters/attachment-utils.ts"() {
83
- "use strict";
84
- ATTACHMENTS_DIR = "attachments";
85
- }
86
- });
87
-
88
- // src/runtime/config.ts
89
- import fs8 from "fs";
90
- import path8 from "path";
91
- var ConfigManager, configManager;
15
+ var SUPPORTED_PROVIDERS, ConfigManager, configManager, ConfigFileAuthBackend;
92
16
  var init_config = __esm({
93
17
  "src/runtime/config.ts"() {
94
18
  "use strict";
19
+ SUPPORTED_PROVIDERS = {
20
+ openai: {
21
+ label: "OpenAI",
22
+ defaultModelId: "gpt-5.4",
23
+ authType: "api_key",
24
+ envKey: "OPENAI_API_KEY",
25
+ placeholder: "sk-proj-...",
26
+ supportsBaseUrl: true
27
+ },
28
+ anthropic: {
29
+ label: "Anthropic",
30
+ defaultModelId: "claude-opus-4-6",
31
+ authType: "api_key",
32
+ envKey: "ANTHROPIC_API_KEY",
33
+ placeholder: "sk-ant-api03-...",
34
+ supportsBaseUrl: true
35
+ },
36
+ google: {
37
+ label: "Google (Gemini)",
38
+ defaultModelId: "gemini-2.5-pro",
39
+ authType: "api_key",
40
+ envKey: "GOOGLE_API_KEY",
41
+ placeholder: "AIza...",
42
+ supportsBaseUrl: false
43
+ },
44
+ "openai-codex": {
45
+ label: "OpenAI Codex",
46
+ defaultModelId: "gpt-5.4",
47
+ authType: "oauth",
48
+ oauthProviderId: "openai-codex",
49
+ supportsBaseUrl: false
50
+ }
51
+ };
95
52
  ConfigManager = class _ConfigManager {
96
53
  static instance;
97
54
  configData = {};
@@ -105,10 +62,10 @@ var init_config = __esm({
105
62
  return _ConfigManager.instance;
106
63
  }
107
64
  load(rootDir) {
108
- this.configPath = path8.join(rootDir, "data", "config.json");
109
- if (fs8.existsSync(this.configPath)) {
65
+ this.configPath = path5.join(rootDir, "data", "config.json");
66
+ if (fs5.existsSync(this.configPath)) {
110
67
  try {
111
- this.configData = JSON.parse(fs8.readFileSync(this.configPath, "utf-8"));
68
+ this.configData = JSON.parse(fs5.readFileSync(this.configPath, "utf-8"));
112
69
  console.log(" Loaded config from data/config.json");
113
70
  } catch (err) {
114
71
  console.warn(" Warning: Failed to parse data/config.json:", err);
@@ -122,6 +79,9 @@ var init_config = __esm({
122
79
  } else if (process.env.ANTHROPIC_API_KEY) {
123
80
  apiKey = process.env.ANTHROPIC_API_KEY;
124
81
  provider = "anthropic";
82
+ } else if (process.env.GOOGLE_API_KEY) {
83
+ apiKey = process.env.GOOGLE_API_KEY;
84
+ provider = "google";
125
85
  }
126
86
  }
127
87
  this.configData.apiKey = apiKey;
@@ -133,12 +93,12 @@ var init_config = __esm({
133
93
  return this.configData;
134
94
  }
135
95
  save(rootDir, updates) {
136
- const configDir = path8.join(rootDir, "data");
96
+ const configDir = path5.join(rootDir, "data");
137
97
  if (!this.configPath) {
138
- this.configPath = path8.join(rootDir, "data", "config.json");
98
+ this.configPath = path5.join(rootDir, "data", "config.json");
139
99
  }
140
- if (!fs8.existsSync(configDir)) {
141
- fs8.mkdirSync(configDir, { recursive: true });
100
+ if (!fs5.existsSync(configDir)) {
101
+ fs5.mkdirSync(configDir, { recursive: true });
142
102
  }
143
103
  if (updates.apiKey !== void 0) this.configData.apiKey = updates.apiKey;
144
104
  if (updates.provider !== void 0) this.configData.provider = updates.provider;
@@ -160,7 +120,7 @@ var init_config = __esm({
160
120
  this.configData.scheduledJobs = updates.scheduledJobs;
161
121
  }
162
122
  try {
163
- fs8.writeFileSync(
123
+ fs5.writeFileSync(
164
124
  this.configPath,
165
125
  JSON.stringify(this.configData, null, 2),
166
126
  "utf-8"
@@ -171,6 +131,137 @@ var init_config = __esm({
171
131
  }
172
132
  };
173
133
  configManager = ConfigManager.getInstance();
134
+ ConfigFileAuthBackend = class {
135
+ constructor(configPath) {
136
+ this.configPath = configPath;
137
+ }
138
+ ensureFile() {
139
+ const dir = path5.dirname(this.configPath);
140
+ if (!fs5.existsSync(dir)) {
141
+ fs5.mkdirSync(dir, { recursive: true });
142
+ }
143
+ if (!fs5.existsSync(this.configPath)) {
144
+ fs5.writeFileSync(this.configPath, "{}", "utf-8");
145
+ }
146
+ }
147
+ readAuthJson() {
148
+ this.ensureFile();
149
+ try {
150
+ const raw = fs5.readFileSync(this.configPath, "utf-8");
151
+ const config = JSON.parse(raw);
152
+ if (config._auth && typeof config._auth === "object") {
153
+ return JSON.stringify(config._auth);
154
+ }
155
+ return void 0;
156
+ } catch {
157
+ return void 0;
158
+ }
159
+ }
160
+ writeAuthJson(authJson) {
161
+ this.ensureFile();
162
+ try {
163
+ const raw = fs5.readFileSync(this.configPath, "utf-8");
164
+ const config = JSON.parse(raw);
165
+ config._auth = JSON.parse(authJson);
166
+ fs5.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8");
167
+ } catch {
168
+ const config = { _auth: JSON.parse(authJson) };
169
+ fs5.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8");
170
+ }
171
+ }
172
+ withLock(fn) {
173
+ const current = this.readAuthJson();
174
+ const { result, next } = fn(current);
175
+ if (next !== void 0) {
176
+ this.writeAuthJson(next);
177
+ }
178
+ return result;
179
+ }
180
+ async withLockAsync(fn) {
181
+ const current = this.readAuthJson();
182
+ const { result, next } = await fn(current);
183
+ if (next !== void 0) {
184
+ this.writeAuthJson(next);
185
+ }
186
+ return result;
187
+ }
188
+ };
189
+ }
190
+ });
191
+
192
+ // src/runtime/adapters/attachment-utils.ts
193
+ import fs6 from "fs";
194
+ import path6 from "path";
195
+ import { pipeline } from "stream/promises";
196
+ import { Readable } from "stream";
197
+ function getAttachmentDir(rootDir, channelId) {
198
+ return path6.resolve(rootDir, "data", "sessions", channelId, ATTACHMENTS_DIR);
199
+ }
200
+ function sanitizeFilename(name) {
201
+ return name.replace(/[/\\:*?"<>|]/g, "_").replace(/\s+/g, "_");
202
+ }
203
+ async function downloadAndSaveAttachment(rootDir, channelId, url, filename, mimeType, headers) {
204
+ const dir = getAttachmentDir(rootDir, channelId);
205
+ fs6.mkdirSync(dir, { recursive: true });
206
+ const ts = Date.now();
207
+ const safeName = sanitizeFilename(filename);
208
+ const storedName = `${ts}-${safeName}`;
209
+ const fullPath = path6.join(dir, storedName);
210
+ const response = await fetch(url, { headers });
211
+ if (!response.ok) {
212
+ throw new Error(
213
+ `Failed to download attachment from ${url}: ${response.status} ${response.statusText}`
214
+ );
215
+ }
216
+ const body = response.body;
217
+ if (!body) {
218
+ throw new Error(`Empty response body when downloading ${url}`);
219
+ }
220
+ const nodeStream = Readable.fromWeb(body);
221
+ const writeStream = fs6.createWriteStream(fullPath);
222
+ await pipeline(nodeStream, writeStream);
223
+ const stats = fs6.statSync(fullPath);
224
+ const detectedMime = mimeType || response.headers.get("content-type")?.split(";")[0] || void 0;
225
+ return {
226
+ filename,
227
+ localPath: fullPath,
228
+ mimeType: detectedMime,
229
+ size: stats.size
230
+ };
231
+ }
232
+ function isImageMime(mimeType) {
233
+ return !!mimeType && mimeType.startsWith("image/");
234
+ }
235
+ function formatSize(bytes) {
236
+ if (bytes === void 0 || bytes === null) return "";
237
+ if (bytes < 1024) return `${bytes}B`;
238
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
239
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
240
+ }
241
+ function formatAttachmentsPrompt(attachments) {
242
+ if (attachments.length === 0) return "";
243
+ const lines = attachments.map((a) => {
244
+ const meta = [a.mimeType, formatSize(a.size)].filter(Boolean).join(", ");
245
+ return `- ${a.filename} (${meta}) \u2192 ${a.localPath}`;
246
+ });
247
+ return `[Attachments]
248
+ ${lines.join("\n")}`;
249
+ }
250
+ function attachmentsToImageContent(attachments) {
251
+ return attachments.filter((a) => isImageMime(a.mimeType)).map((a) => {
252
+ const buffer = fs6.readFileSync(a.localPath);
253
+ return {
254
+ type: "image",
255
+ data: buffer.toString("base64"),
256
+ mimeType: a.mimeType || "image/png"
257
+ };
258
+ });
259
+ }
260
+ var ATTACHMENTS_DIR;
261
+ var init_attachment_utils = __esm({
262
+ "src/runtime/adapters/attachment-utils.ts"() {
263
+ "use strict";
264
+ ATTACHMENTS_DIR = "attachments";
174
265
  }
175
266
  });
176
267
 
@@ -2057,9 +2148,10 @@ import { createServer } from "http";
2057
2148
  import { exec } from "child_process";
2058
2149
 
2059
2150
  // src/runtime/agent.ts
2151
+ init_config();
2060
2152
  init_attachment_utils();
2061
- import path7 from "path";
2062
- import fs7 from "fs";
2153
+ import path8 from "path";
2154
+ import fs8 from "fs";
2063
2155
  import { fileURLToPath } from "url";
2064
2156
  import {
2065
2157
  AuthStorage,
@@ -2071,8 +2163,8 @@ import {
2071
2163
  } from "@mariozechner/pi-coding-agent";
2072
2164
 
2073
2165
  // src/runtime/tools/send-file-tool.ts
2074
- import fs6 from "fs";
2075
- import path6 from "path";
2166
+ import fs7 from "fs";
2167
+ import path7 from "path";
2076
2168
 
2077
2169
  // node_modules/@sinclair/typebox/build/esm/type/guard/value.mjs
2078
2170
  var value_exports = {};
@@ -4712,7 +4804,7 @@ var MIME_BY_EXT = {
4712
4804
  ".ogg": "audio/ogg"
4713
4805
  };
4714
4806
  function detectMimeType(filePath) {
4715
- const ext = path6.extname(filePath).toLowerCase();
4807
+ const ext = path7.extname(filePath).toLowerCase();
4716
4808
  return MIME_BY_EXT[ext];
4717
4809
  }
4718
4810
  function createSendFileTool(fileOutputCallbackRef) {
@@ -4724,13 +4816,13 @@ function createSendFileTool(fileOutputCallbackRef) {
4724
4816
  parameters: SendFileParams,
4725
4817
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
4726
4818
  const { filePath, caption } = params;
4727
- if (!fs6.existsSync(filePath)) {
4819
+ if (!fs7.existsSync(filePath)) {
4728
4820
  return {
4729
4821
  content: [{ type: "text", text: `Error: File not found: ${filePath}` }],
4730
4822
  details: void 0
4731
4823
  };
4732
4824
  }
4733
- const stats = fs6.statSync(filePath);
4825
+ const stats = fs7.statSync(filePath);
4734
4826
  if (!stats.isFile()) {
4735
4827
  return {
4736
4828
  content: [
@@ -4739,7 +4831,7 @@ function createSendFileTool(fileOutputCallbackRef) {
4739
4831
  details: void 0
4740
4832
  };
4741
4833
  }
4742
- const filename = path6.basename(filePath);
4834
+ const filename = path7.basename(filePath);
4743
4835
  const mimeType = detectMimeType(filePath);
4744
4836
  const callback = fileOutputCallbackRef.current;
4745
4837
  if (callback) {
@@ -4925,24 +5017,24 @@ var BUILTIN_SKILL_CREATOR_TEMPLATE_DIR = fileURLToPath(
4925
5017
  new URL("../templates/builtin-skills/skill-creator", import.meta.url)
4926
5018
  );
4927
5019
  function materializeBuiltinSkillCreator(rootDir, skillsPath) {
4928
- if (!fs7.existsSync(BUILTIN_SKILL_CREATOR_TEMPLATE_DIR)) {
5020
+ if (!fs8.existsSync(BUILTIN_SKILL_CREATOR_TEMPLATE_DIR)) {
4929
5021
  log(
4930
5022
  `[PackAgent] Built-in skill-creator template missing: ${BUILTIN_SKILL_CREATOR_TEMPLATE_DIR}`
4931
5023
  );
4932
5024
  return null;
4933
5025
  }
4934
- const packConfigPath = path7.resolve(rootDir, "skillpack.json");
4935
- const skillDir = path7.resolve(skillsPath, BUILTIN_SKILL_CREATOR_NAME);
4936
- const skillPath = path7.join(skillDir, "SKILL.md");
5026
+ const packConfigPath = path8.resolve(rootDir, "skillpack.json");
5027
+ const skillDir = path8.resolve(skillsPath, BUILTIN_SKILL_CREATOR_NAME);
5028
+ const skillPath = path8.join(skillDir, "SKILL.md");
4937
5029
  const renderTemplate = (content) => content.replaceAll("{{SKILLS_PATH}}", skillsPath).replaceAll("{{PACK_CONFIG_PATH}}", packConfigPath);
4938
5030
  const copyDir = (srcDir, destDir) => {
4939
- fs7.mkdirSync(destDir, { recursive: true });
4940
- for (const entry of fs7.readdirSync(srcDir, { withFileTypes: true })) {
5031
+ fs8.mkdirSync(destDir, { recursive: true });
5032
+ for (const entry of fs8.readdirSync(srcDir, { withFileTypes: true })) {
4941
5033
  if (entry.name === ".DS_Store") {
4942
5034
  continue;
4943
5035
  }
4944
- const srcPath = path7.join(srcDir, entry.name);
4945
- const destPath = path7.join(destDir, entry.name);
5036
+ const srcPath = path8.join(srcDir, entry.name);
5037
+ const destPath = path8.join(destDir, entry.name);
4946
5038
  if (entry.isDirectory()) {
4947
5039
  copyDir(srcPath, destPath);
4948
5040
  continue;
@@ -4951,17 +5043,17 @@ function materializeBuiltinSkillCreator(rootDir, skillsPath) {
4951
5043
  continue;
4952
5044
  }
4953
5045
  if (entry.name.endsWith(".md") || entry.name.endsWith(".py")) {
4954
- const content = fs7.readFileSync(srcPath, "utf-8");
4955
- fs7.writeFileSync(destPath, renderTemplate(content), "utf-8");
5046
+ const content = fs8.readFileSync(srcPath, "utf-8");
5047
+ fs8.writeFileSync(destPath, renderTemplate(content), "utf-8");
4956
5048
  continue;
4957
5049
  }
4958
- fs7.copyFileSync(srcPath, destPath);
5050
+ fs8.copyFileSync(srcPath, destPath);
4959
5051
  }
4960
5052
  };
4961
- if (!fs7.existsSync(skillDir)) {
5053
+ if (!fs8.existsSync(skillDir)) {
4962
5054
  copyDir(BUILTIN_SKILL_CREATOR_TEMPLATE_DIR, skillDir);
4963
5055
  }
4964
- if (!fs7.existsSync(skillPath)) {
5056
+ if (!fs8.existsSync(skillPath)) {
4965
5057
  log(
4966
5058
  `[PackAgent] Materialized built-in skill-creator but SKILL.md is missing: ${skillPath}`
4967
5059
  );
@@ -5014,8 +5106,29 @@ var PackAgent = class {
5014
5106
  current: null
5015
5107
  };
5016
5108
  schedulerRef = { current: null };
5109
+ authStorage;
5017
5110
  constructor(options) {
5018
5111
  this.options = options;
5112
+ const configPath = path8.resolve(options.rootDir, "data", "config.json");
5113
+ const backend = new ConfigFileAuthBackend(configPath);
5114
+ this.authStorage = AuthStorage.fromStorage(backend);
5115
+ const providerMeta = SUPPORTED_PROVIDERS[options.provider];
5116
+ if (providerMeta?.authType === "api_key" && options.apiKey) {
5117
+ this.authStorage.setRuntimeApiKey(options.provider, options.apiKey);
5118
+ }
5119
+ }
5120
+ /** Get the shared AuthStorage instance (used by OAuth API endpoints) */
5121
+ getAuthStorage() {
5122
+ return this.authStorage;
5123
+ }
5124
+ /** Update runtime auth when provider/apiKey changes */
5125
+ updateAuth(provider, apiKey) {
5126
+ this.authStorage.removeRuntimeApiKey(this.options.provider);
5127
+ this.options.provider = provider;
5128
+ if (apiKey) {
5129
+ this.options.apiKey = apiKey;
5130
+ this.authStorage.setRuntimeApiKey(provider, apiKey);
5131
+ }
5019
5132
  }
5020
5133
  /**
5021
5134
  * Inject scheduler reference (called by server.ts after adapter init).
@@ -5039,35 +5152,32 @@ var PackAgent = class {
5039
5152
  const pendingCreation = this.pendingSessionCreations.get(channelId);
5040
5153
  if (pendingCreation) return pendingCreation;
5041
5154
  const createSessionPromise = (async () => {
5042
- const { apiKey, rootDir, provider, modelId, baseUrl } = this.options;
5043
- const authStorage = AuthStorage.inMemory({
5044
- [provider]: { type: "api_key", key: apiKey }
5045
- });
5046
- authStorage.setRuntimeApiKey(provider, apiKey);
5155
+ const { rootDir, provider, modelId, baseUrl } = this.options;
5156
+ const authStorage = this.authStorage;
5047
5157
  const modelRegistry = new ModelRegistry(authStorage);
5048
5158
  const resolvedModel = modelRegistry.find(provider, modelId);
5049
5159
  const model = resolvedModel && baseUrl ? { ...resolvedModel, baseUrl } : resolvedModel;
5050
5160
  if (resolvedModel && baseUrl) {
5051
5161
  log(`[PackAgent] Overriding ${provider}/${modelId} baseUrl -> ${baseUrl}`);
5052
5162
  }
5053
- const sessionDir = path7.resolve(
5163
+ const sessionDir = path8.resolve(
5054
5164
  rootDir,
5055
5165
  "data",
5056
5166
  "sessions",
5057
5167
  channelId
5058
5168
  );
5059
- fs7.mkdirSync(sessionDir, { recursive: true });
5169
+ fs8.mkdirSync(sessionDir, { recursive: true });
5060
5170
  const sessionManager = SessionManager.continueRecent(rootDir, sessionDir);
5061
5171
  log(`[PackAgent] Session dir: ${sessionDir}`);
5062
- const workspaceDir = path7.resolve(
5172
+ const workspaceDir = path8.resolve(
5063
5173
  rootDir,
5064
5174
  "data",
5065
5175
  "workspaces",
5066
5176
  channelId
5067
5177
  );
5068
- fs7.mkdirSync(workspaceDir, { recursive: true });
5178
+ fs8.mkdirSync(workspaceDir, { recursive: true });
5069
5179
  log(`[PackAgent] Workspace dir: ${workspaceDir}`);
5070
- const skillsPath = path7.resolve(rootDir, "skills");
5180
+ const skillsPath = path8.resolve(rootDir, "skills");
5071
5181
  log(`[PackAgent] Loading skills from: ${skillsPath}`);
5072
5182
  const materializedSkillCreator = materializeBuiltinSkillCreator(
5073
5183
  rootDir,
@@ -5249,9 +5359,9 @@ ${text}`;
5249
5359
  this.channels.delete(channelId);
5250
5360
  }
5251
5361
  const { rootDir } = this.options;
5252
- const sessionDir = path7.resolve(rootDir, "data", "sessions", channelId);
5253
- if (fs7.existsSync(sessionDir)) {
5254
- fs7.rmSync(sessionDir, { recursive: true, force: true });
5362
+ const sessionDir = path8.resolve(rootDir, "data", "sessions", channelId);
5363
+ if (fs8.existsSync(sessionDir)) {
5364
+ fs8.rmSync(sessionDir, { recursive: true, force: true });
5255
5365
  log(`[PackAgent] Cleared session dir: ${sessionDir}`);
5256
5366
  }
5257
5367
  return {
@@ -5340,6 +5450,9 @@ var WebAdapter = class {
5340
5450
  app.get("/api/config", (_req, res) => {
5341
5451
  const config = getPackConfig(rootDir);
5342
5452
  const conf = configManager.getConfig();
5453
+ const currentProvider2 = conf.provider || "openai";
5454
+ const providerMeta = SUPPORTED_PROVIDERS[currentProvider2];
5455
+ const oauthConnected = providerMeta?.authType === "oauth" ? agent.getAuthStorage().hasAuth(currentProvider2) : false;
5343
5456
  res.json({
5344
5457
  name: config.name,
5345
5458
  description: config.description,
@@ -5347,9 +5460,11 @@ var WebAdapter = class {
5347
5460
  skills: config.skills || [],
5348
5461
  hasApiKey: !!conf.apiKey,
5349
5462
  apiKey: conf.apiKey || "",
5350
- provider: conf.provider || "openai",
5463
+ provider: currentProvider2,
5351
5464
  baseUrl: conf.baseUrl || "",
5352
- adapters: conf.adapters || {}
5465
+ adapters: conf.adapters || {},
5466
+ supportedProviders: SUPPORTED_PROVIDERS,
5467
+ oauthConnected
5353
5468
  });
5354
5469
  });
5355
5470
  app.get("/api/skills", (_req, res) => {
@@ -5375,16 +5490,62 @@ var WebAdapter = class {
5375
5490
  updates.adapters = adapters;
5376
5491
  }
5377
5492
  configManager.save(rootDir, updates);
5378
- const newConf = configManager.getConfig();
5379
- const requiresRestart = getRuntimeConfigSignature(beforeConfig) !== getRuntimeConfigSignature(newConf);
5493
+ agent.updateAuth(currentProvider, apiKey);
5494
+ const afterConfig = configManager.getConfig();
5495
+ const requiresRestart = getRuntimeConfigSignature(beforeConfig) !== getRuntimeConfigSignature(afterConfig);
5380
5496
  res.json({
5381
- success: true,
5382
- provider: newConf.provider,
5383
- baseUrl: newConf.baseUrl || "",
5384
- adapters: newConf.adapters,
5497
+ ...afterConfig,
5385
5498
  requiresRestart
5386
5499
  });
5387
5500
  });
5501
+ app.post("/api/oauth/login", async (req, res) => {
5502
+ const { provider } = req.body;
5503
+ const meta = SUPPORTED_PROVIDERS[provider];
5504
+ if (!meta || meta.authType !== "oauth") {
5505
+ return res.status(400).json({ error: "Provider does not support OAuth" });
5506
+ }
5507
+ try {
5508
+ const authStorage = agent.getAuthStorage();
5509
+ let authUrl = "";
5510
+ const loginPromise = authStorage.login(provider, {
5511
+ onAuth: (info) => {
5512
+ authUrl = info.url;
5513
+ },
5514
+ onPrompt: async (prompt) => {
5515
+ return "";
5516
+ },
5517
+ onProgress: (msg) => {
5518
+ console.log(`[OAuth] ${provider} login progress: ${msg}`);
5519
+ }
5520
+ });
5521
+ await new Promise((r) => setTimeout(r, 1500));
5522
+ if (authUrl) {
5523
+ res.json({ status: "pending", authUrl });
5524
+ } else {
5525
+ res.json({ status: "pending" });
5526
+ }
5527
+ loginPromise.catch((err) => {
5528
+ console.error(`[OAuth] ${provider} login error:`, err);
5529
+ });
5530
+ } catch (err) {
5531
+ res.status(500).json({ error: String(err) });
5532
+ }
5533
+ });
5534
+ app.get("/api/oauth/status", (_req, res) => {
5535
+ const conf = configManager.getConfig();
5536
+ const provider = conf.provider || "openai";
5537
+ const meta = SUPPORTED_PROVIDERS[provider];
5538
+ if (!meta || meta.authType !== "oauth") {
5539
+ return res.json({ connected: false });
5540
+ }
5541
+ const connected = agent.getAuthStorage().hasAuth(provider);
5542
+ res.json({ connected, provider });
5543
+ });
5544
+ app.post("/api/oauth/logout", (req, res) => {
5545
+ const { provider } = req.body;
5546
+ agent.getAuthStorage().logout(provider);
5547
+ res.json({ success: true });
5548
+ });
5388
5549
  app.post("/api/runtime/restart", async (_req, res) => {
5389
5550
  const result = await lifecycle.requestRestart("web");
5390
5551
  res.status(202).json(result);
@@ -5508,8 +5669,10 @@ var WebAdapter = class {
5508
5669
  `http://${request.headers.host || "127.0.0.1"}`
5509
5670
  );
5510
5671
  const _reqProvider = url.searchParams.get("provider") || currentProvider;
5511
- if (!apiKey) {
5512
- ws.send(JSON.stringify({ error: "Please set an API key first" }));
5672
+ const providerMeta = SUPPORTED_PROVIDERS[_reqProvider];
5673
+ const hasAuth = providerMeta?.authType === "oauth" ? agent.getAuthStorage().hasAuth(_reqProvider) : !!apiKey;
5674
+ if (!hasAuth) {
5675
+ ws.send(JSON.stringify({ error: "Please configure authentication first" }));
5513
5676
  ws.close();
5514
5677
  return;
5515
5678
  }
@@ -5819,7 +5982,7 @@ async function startServer(options) {
5819
5982
  const canonicalRootDir = canonicalizeDir(rootDir);
5820
5983
  const packConfig = loadConfig(canonicalRootDir);
5821
5984
  const baseUrl = dataConfig.baseUrl?.trim() || void 0;
5822
- const modelId = provider === "anthropic" ? "claude-opus-4-6" : "gpt-5.4";
5985
+ const modelId = SUPPORTED_PROVIDERS[provider]?.defaultModelId ?? SUPPORTED_PROVIDERS.openai.defaultModelId;
5823
5986
  const packageRoot = path12.resolve(__dirname, "..");
5824
5987
  const webDir = fs13.existsSync(path12.join(rootDir, "web")) ? path12.join(rootDir, "web") : path12.join(packageRoot, "web");
5825
5988
  const app = express();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cremini/skillpack",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Pack AI Skills into Local Agents",
5
5
  "type": "module",
6
6
  "repository": {
package/web/index.html CHANGED
@@ -39,12 +39,6 @@
39
39
  <ul id="skills-list"></ul>
40
40
  </div>
41
41
 
42
- <div class="sidebar-settings-section">
43
- <button id="open-settings-btn" class="settings-trigger-btn">
44
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="btn-icon"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
45
- Settings
46
- </button>
47
- </div>
48
42
  </aside>
49
43
 
50
44
  <main id="chat-area" class="mode-welcome">
@@ -71,59 +65,6 @@
71
65
  </main>
72
66
  </div>
73
67
 
74
- <!-- Settings Dialog -->
75
- <dialog id="settings-dialog" class="settings-modal">
76
- <div class="settings-modal-header">
77
- <h2>Settings</h2>
78
- <button id="close-settings-btn" class="close-btn" aria-label="Close">
79
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
80
- </button>
81
- </div>
82
- <div class="settings-modal-body">
83
- <div class="settings-sections">
84
- <!-- General Section -->
85
- <div class="settings-section">
86
- <h3 class="section-title">General</h3>
87
- <div class="form-group">
88
- <label>Provider</label>
89
- <div class="provider-select-wrapper">
90
- <select id="provider-select">
91
- <option value="openai">OpenAI</option>
92
- <option value="anthropic">Anthropic</option>
93
- </select>
94
- </div>
95
- </div>
96
- <div class="form-group">
97
- <label>API Key</label>
98
- <input type="password" id="api-key-input" placeholder="sk-..." class="form-input" />
99
- </div>
100
- </div>
101
-
102
- <!-- IM Bots Section -->
103
- <div class="settings-section">
104
- <h3 class="section-title">IM Bots</h3>
105
- <div class="form-group">
106
- <label>Telegram Bot Token</label>
107
- <input type="password" id="telegram-token-input" placeholder="123456:ABC-DEF..." class="form-input" />
108
- </div>
109
- <div class="form-group">
110
- <label>Slack Bot Token</label>
111
- <input type="password" id="slack-bot-token-input" placeholder="xoxb-..." class="form-input" />
112
- </div>
113
- <div class="form-group">
114
- <label>Slack App Token</label>
115
- <input type="password" id="slack-app-token-input" placeholder="xapp-..." class="form-input" />
116
- </div>
117
- </div>
118
- </div>
119
- </div>
120
- <div class="settings-modal-footer">
121
- <p id="key-status" class="status-text"></p>
122
- <button id="restart-service-btn" class="secondary-btn" hidden>Restart Service</button>
123
- <button id="save-settings-btn" class="primary-btn">Save Settings</button>
124
- </div>
125
- </dialog>
126
-
127
68
  <!-- API Key Dialog -->
128
69
  <dialog id="apikey-dialog" class="settings-modal compact-modal">
129
70
  <div class="settings-modal-header">
@@ -140,12 +81,38 @@
140
81
  <select id="apikey-provider-select">
141
82
  <option value="openai">OpenAI</option>
142
83
  <option value="anthropic">Anthropic</option>
84
+ <option value="google">Google (Gemini)</option>
85
+ <option value="openai-codex">OpenAI Codex</option>
143
86
  </select>
144
87
  </div>
145
88
  </div>
146
- <div class="form-group">
147
- <label>API Key</label>
148
- <input type="password" id="apikey-input" placeholder="sk-..." class="form-input" />
89
+ <!-- API Key Section -->
90
+ <div id="apikey-apikey-section">
91
+ <div class="form-group">
92
+ <label>API Key</label>
93
+ <input type="password" id="apikey-input" placeholder="sk-..." class="form-input" />
94
+ </div>
95
+ <div class="form-group" id="apikey-baseurl-group">
96
+ <label>Custom Base URL <span class="label-hint">(optional)</span></label>
97
+ <input type="text" id="apikey-baseurl-input" placeholder="https://api.example.com/v1" class="form-input" />
98
+ </div>
99
+ </div>
100
+
101
+ <!-- OAuth Section -->
102
+ <div id="apikey-oauth-section" style="display: none;">
103
+ <div class="oauth-status-card">
104
+ <div id="oauth-status-indicator" class="oauth-disconnected">
105
+ <span class="oauth-status-dot"></span>
106
+ <span id="oauth-status-text">Not Connected</span>
107
+ </div>
108
+ <p class="oauth-hint">Login with your ChatGPT account to use Codex subscription.</p>
109
+ </div>
110
+ <button id="oauth-login-btn" class="primary-btn oauth-btn">
111
+ Login with ChatGPT
112
+ </button>
113
+ <button id="oauth-logout-btn" class="secondary-btn oauth-btn" style="display: none;">
114
+ Logout
115
+ </button>
149
116
  </div>
150
117
  </div>
151
118
  </div>
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * API Key Dialog Module
3
3
  *
4
- * 负责 Model API Key 的配置管理。
5
- * 独立的 Dialog,从原 SettingDialog 的 API Key 部分拆分出来。
4
+ * 负责 Model 认证配置管理。支持 API Key 模式和 OAuth 模式切换。
6
5
  */
7
6
  import { state } from "./config.js";
8
7
  import { saveConfigData, restartRuntime } from "./api.js";
@@ -15,6 +14,14 @@ let saveBtn;
15
14
  let restartBtn;
16
15
  let providerSelect;
17
16
  let apiKeyInput;
17
+ let baseUrlInput;
18
+ let baseUrlGroup;
19
+ let apikeySection;
20
+ let oauthSection;
21
+ let oauthLoginBtn;
22
+ let oauthLogoutBtn;
23
+ let oauthStatusIndicator;
24
+ let oauthStatusText;
18
25
  let statusEl;
19
26
 
20
27
  // --- Public API ---
@@ -27,6 +34,14 @@ export function initApiKeyDialog() {
27
34
  restartBtn = document.getElementById("restart-apikey-btn");
28
35
  providerSelect = document.getElementById("apikey-provider-select");
29
36
  apiKeyInput = document.getElementById("apikey-input");
37
+ baseUrlInput = document.getElementById("apikey-baseurl-input");
38
+ baseUrlGroup = document.getElementById("apikey-baseurl-group");
39
+ apikeySection = document.getElementById("apikey-apikey-section");
40
+ oauthSection = document.getElementById("apikey-oauth-section");
41
+ oauthLoginBtn = document.getElementById("oauth-login-btn");
42
+ oauthLogoutBtn = document.getElementById("oauth-logout-btn");
43
+ oauthStatusIndicator = document.getElementById("oauth-status-indicator");
44
+ oauthStatusText = document.getElementById("oauth-status-text");
30
45
  statusEl = document.getElementById("apikey-status");
31
46
 
32
47
  if (!dialog) return;
@@ -44,7 +59,13 @@ export function initApiKeyDialog() {
44
59
  restartBtn.addEventListener("click", handleRestart);
45
60
  }
46
61
  if (providerSelect) {
47
- providerSelect.addEventListener("change", updatePlaceholder);
62
+ providerSelect.addEventListener("change", updateProviderUI);
63
+ }
64
+ if (oauthLoginBtn) {
65
+ oauthLoginBtn.addEventListener("click", handleOAuthLogin);
66
+ }
67
+ if (oauthLogoutBtn) {
68
+ oauthLogoutBtn.addEventListener("click", handleOAuthLogout);
48
69
  }
49
70
  }
50
71
 
@@ -54,12 +75,19 @@ export function initApiKeyDialog() {
54
75
  export function updateApiKeyButton() {
55
76
  if (!openBtn) return;
56
77
  const config = state.config;
57
- if (config && config.hasApiKey) {
78
+ const currentProvider = config?.provider || "openai";
79
+ const meta = config?.supportedProviders?.[currentProvider];
80
+
81
+ const connected = meta?.authType === "oauth"
82
+ ? config?.oauthConnected
83
+ : config?.hasApiKey;
84
+
85
+ if (connected) {
58
86
  openBtn.classList.add("connected");
59
- openBtn.querySelector(".action-btn-label").textContent = "API Key Configured";
87
+ openBtn.querySelector(".action-btn-label").textContent = "Model Configured";
60
88
  } else {
61
89
  openBtn.classList.remove("connected");
62
- openBtn.querySelector(".action-btn-label").textContent = "Provide Model API Key";
90
+ openBtn.querySelector(".action-btn-label").textContent = "Provide Model Auth";
63
91
  }
64
92
  }
65
93
 
@@ -83,10 +111,11 @@ function populateForm() {
83
111
  if (config.provider && providerSelect) {
84
112
  providerSelect.value = config.provider;
85
113
  }
86
- updatePlaceholder();
114
+ updateProviderUI();
87
115
 
88
116
  setStatus("", "");
89
117
 
118
+ // API Key & BaseURL
90
119
  if (config.hasApiKey && config.apiKey) {
91
120
  apiKeyInput.value = config.apiKey;
92
121
  } else if (config.hasApiKey) {
@@ -94,27 +123,64 @@ function populateForm() {
94
123
  } else {
95
124
  apiKeyInput.value = "";
96
125
  }
126
+ if (baseUrlInput) {
127
+ baseUrlInput.value = config.baseUrl || "";
128
+ }
129
+
130
+ // OAuth Status
131
+ if (config.oauthConnected) {
132
+ updateOAuthUI(true);
133
+ } else {
134
+ updateOAuthUI(false);
135
+ }
97
136
  }
98
137
 
99
- function updatePlaceholder() {
100
- if (!providerSelect || !apiKeyInput) return;
138
+ function updateProviderUI() {
139
+ if (!providerSelect) return;
101
140
  const p = providerSelect.value;
102
- if (p === "openai") apiKeyInput.placeholder = "sk-proj-...";
103
- else if (p === "anthropic") apiKeyInput.placeholder = "sk-ant-api03-...";
104
- else apiKeyInput.placeholder = "sk-...";
141
+ const meta = state.config?.supportedProviders?.[p];
142
+
143
+ if (!meta) return;
144
+
145
+ if (meta.authType === "oauth") {
146
+ // Show OAuth section, hide API Key section
147
+ if (apikeySection) apikeySection.style.display = "none";
148
+ if (oauthSection) oauthSection.style.display = "";
149
+ if (saveBtn) saveBtn.style.display = "none";
150
+ checkOAuthStatus();
151
+ } else {
152
+ // Show API Key section, hide OAuth section
153
+ if (apikeySection) apikeySection.style.display = "";
154
+ if (oauthSection) oauthSection.style.display = "none";
155
+ if (saveBtn) saveBtn.style.display = "";
156
+
157
+ // Toggle BaseURL input
158
+ if (baseUrlGroup) {
159
+ baseUrlGroup.style.display = meta.supportsBaseUrl ? "" : "none";
160
+ }
161
+
162
+ // Update placeholder
163
+ if (apiKeyInput) {
164
+ apiKeyInput.placeholder = meta.placeholder || "sk-...";
165
+ }
166
+ }
105
167
  }
106
168
 
107
169
  async function handleSave() {
108
170
  const key = apiKeyInput.value.trim();
109
171
  const provider = providerSelect.value;
172
+ const baseUrl = baseUrlInput.value.trim();
110
173
 
111
- if (!key) {
112
- setStatus("Please enter an API key", "error");
113
- return;
114
- }
174
+ // If OAuth is selected, we don't handle it here but it shouldn't happen as save button is hidden
175
+ const meta = state.config?.supportedProviders?.[provider];
176
+ if (meta?.authType === "oauth") return;
115
177
 
116
178
  const updates = { provider };
117
- if (key !== "***************************************************" && key !== state.config.apiKey) {
179
+ if (baseUrl !== state.config.baseUrl) {
180
+ updates.baseUrl = baseUrl;
181
+ }
182
+
183
+ if (key && key !== "***************************************************" && key !== state.config.apiKey) {
118
184
  updates.key = key;
119
185
  }
120
186
 
@@ -123,33 +189,21 @@ async function handleSave() {
123
189
  const res = await saveConfigData(updates);
124
190
 
125
191
  state.config.provider = res.provider;
192
+ state.config.baseUrl = res.baseUrl || "";
126
193
  if (updates.key) {
127
194
  state.config.hasApiKey = true;
128
195
  state.config.apiKey = updates.key;
129
196
  }
130
197
 
131
- if (state.config.hasApiKey && state.config.apiKey) {
132
- apiKeyInput.value = state.config.apiKey;
133
- } else if (state.config.hasApiKey) {
134
- apiKeyInput.value = "***************************************************";
135
- } else {
136
- apiKeyInput.value = "";
137
- }
138
-
139
-
198
+ populateForm();
140
199
  state.restartRequired = !!res.requiresRestart;
141
-
142
200
  updateApiKeyButton();
143
201
 
144
202
  if (res.requiresRestart) {
145
- setStatus(
146
- "API key saved. Restart service to apply changes.",
147
- "warning",
148
- );
203
+ setStatus("Settings saved. Restart service to apply changes.", "warning");
149
204
  updateRestartButton(true);
150
205
  } else {
151
- setStatus("API key saved successfully", "success");
152
- // 延迟关闭让用户看到成功消息
206
+ setStatus("Settings saved successfully", "success");
153
207
  setTimeout(() => close(), 1200);
154
208
  }
155
209
  } catch (err) {
@@ -159,6 +213,107 @@ async function handleSave() {
159
213
  }
160
214
  }
161
215
 
216
+ async function handleOAuthLogin() {
217
+ const provider = providerSelect.value;
218
+ oauthLoginBtn.disabled = true;
219
+ setStatus("Starting OAuth login...", "warning");
220
+
221
+ try {
222
+ // Ensure provider is saved first
223
+ await saveConfigData({ provider });
224
+
225
+ const res = await fetch("/api/oauth/login", {
226
+ method: "POST",
227
+ headers: { "Content-Type": "application/json" },
228
+ body: JSON.stringify({ provider }),
229
+ });
230
+ const data = await res.json();
231
+
232
+ if (data.authUrl) {
233
+ window.open(data.authUrl, "_blank");
234
+ setStatus("Waiting for authorization in browser...", "warning");
235
+ pollOAuthStatus();
236
+ } else {
237
+ setStatus("OAuth process started. Check terminal for URL if browser didn't open.", "warning");
238
+ pollOAuthStatus();
239
+ }
240
+ } catch (err) {
241
+ setStatus("Login failed: " + err.message, "error");
242
+ oauthLoginBtn.disabled = false;
243
+ }
244
+ }
245
+
246
+ async function handleOAuthLogout() {
247
+ const provider = providerSelect.value;
248
+ try {
249
+ await fetch("/api/oauth/logout", {
250
+ method: "POST",
251
+ headers: { "Content-Type": "application/json" },
252
+ body: JSON.stringify({ provider }),
253
+ });
254
+ updateOAuthUI(false);
255
+ state.config.oauthConnected = false;
256
+ updateApiKeyButton();
257
+ setStatus("Logged out successfully", "success");
258
+ } catch (err) {
259
+ setStatus("Logout failed: " + err.message, "error");
260
+ }
261
+ }
262
+
263
+ async function checkOAuthStatus() {
264
+ try {
265
+ const res = await fetch("/api/oauth/status");
266
+ const { connected } = await res.json();
267
+ updateOAuthUI(connected);
268
+ state.config.oauthConnected = connected;
269
+ updateApiKeyButton();
270
+ } catch (err) {
271
+ console.error("Failed to check OAuth status:", err);
272
+ }
273
+ }
274
+
275
+ function pollOAuthStatus() {
276
+ const interval = setInterval(async () => {
277
+ const res = await fetch("/api/oauth/status");
278
+ const { connected } = await res.json();
279
+ if (connected) {
280
+ clearInterval(interval);
281
+ updateOAuthUI(true);
282
+ state.config.oauthConnected = true;
283
+ state.restartRequired = true;
284
+ updateApiKeyButton();
285
+ setStatus("Connected successfully!", "success");
286
+ updateRestartButton(true);
287
+ }
288
+ }, 2000);
289
+
290
+ // Timeout after 5 minutes
291
+ setTimeout(() => clearInterval(interval), 300000);
292
+ }
293
+
294
+ function updateOAuthUI(connected) {
295
+ if (connected) {
296
+ if (oauthStatusIndicator) {
297
+ oauthStatusIndicator.classList.remove("oauth-disconnected");
298
+ oauthStatusIndicator.classList.add("oauth-connected");
299
+ }
300
+ if (oauthStatusText) oauthStatusText.textContent = "Connected";
301
+ if (oauthLoginBtn) oauthLoginBtn.style.display = "none";
302
+ if (oauthLogoutBtn) oauthLogoutBtn.style.display = "";
303
+ } else {
304
+ if (oauthStatusIndicator) {
305
+ oauthStatusIndicator.classList.add("oauth-disconnected");
306
+ oauthStatusIndicator.classList.remove("oauth-connected");
307
+ }
308
+ if (oauthStatusText) oauthStatusText.textContent = "Not Connected";
309
+ if (oauthLoginBtn) {
310
+ oauthLoginBtn.style.display = "";
311
+ oauthLoginBtn.disabled = false;
312
+ }
313
+ if (oauthLogoutBtn) oauthLogoutBtn.style.display = "none";
314
+ }
315
+ }
316
+
162
317
  async function handleRestart() {
163
318
  if (!restartBtn) return;
164
319
 
package/web/js/main.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { state, loadConfig } from "./config.js";
2
- import { initSettings } from "./settings.js";
3
2
  import { initApiKeyDialog, updateApiKeyButton } from "./api-key-dialog.js";
4
3
  import { initChatAppsDialog, updateChatAppsButton } from "./chat-apps-dialog.js";
5
4
  import { initChat, showWelcome } from "./chat.js";
@@ -42,7 +41,6 @@ async function init() {
42
41
  }
43
42
 
44
43
  // Initialize all dialog modules
45
- initSettings();
46
44
  initApiKeyDialog();
47
45
  initChatAppsDialog();
48
46
  initChat();
package/web/styles.css CHANGED
@@ -72,13 +72,6 @@ body {
72
72
  line-height: 1.4;
73
73
  }
74
74
 
75
- .sidebar-settings-section {
76
- padding: 16px 20px;
77
- border-top: 1px solid var(--border-color);
78
- background: var(--bg-secondary);
79
- flex-shrink: 0;
80
- }
81
-
82
75
  .sidebar-skills-section {
83
76
  padding: 16px 20px;
84
77
  flex: 1;
@@ -874,26 +867,6 @@ body {
874
867
  width: 440px;
875
868
  }
876
869
 
877
- /* ---- Settings Modal ---- */
878
- .settings-trigger-btn {
879
- display: flex;
880
- align-items: center;
881
- justify-content: center;
882
- gap: 8px;
883
- width: 100%;
884
- padding: 12px;
885
- background: var(--bg-tertiary);
886
- border: 1px solid var(--border-color);
887
- border-radius: var(--radius-sm);
888
- color: var(--text-primary);
889
- font-weight: 500;
890
- cursor: pointer;
891
- transition: all 0.2s ease;
892
- }
893
- .settings-trigger-btn:hover {
894
- background: #ebebeb;
895
- }
896
-
897
870
  .settings-modal {
898
871
  margin: auto;
899
872
  padding: 0;
@@ -1073,3 +1046,68 @@ body {
1073
1046
  transform: none;
1074
1047
  box-shadow: none;
1075
1048
  }
1049
+ .label-hint {
1050
+ color: var(--text-secondary);
1051
+ font-size: 11px;
1052
+ font-weight: 400;
1053
+ margin-left: 4px;
1054
+ }
1055
+
1056
+ /* OAuth UI */
1057
+ #apikey-oauth-section {
1058
+ padding: 8px 0;
1059
+ }
1060
+
1061
+ .oauth-status-card {
1062
+ background: var(--bg-tertiary);
1063
+ border: 1px solid var(--border-color);
1064
+ border-radius: var(--radius-sm);
1065
+ padding: 16px;
1066
+ margin-bottom: 16px;
1067
+ }
1068
+
1069
+ #oauth-status-indicator {
1070
+ display: flex;
1071
+ align-items: center;
1072
+ gap: 8px;
1073
+ font-size: 13px;
1074
+ font-weight: 600;
1075
+ margin-bottom: 8px;
1076
+ }
1077
+
1078
+ .oauth-status-dot {
1079
+ width: 8px;
1080
+ height: 8px;
1081
+ border-radius: 50%;
1082
+ }
1083
+
1084
+ .oauth-disconnected .oauth-status-dot {
1085
+ background-color: #94a3b8;
1086
+ }
1087
+
1088
+ .oauth-connected .oauth-status-dot {
1089
+ background-color: #22c55e;
1090
+ }
1091
+
1092
+ .oauth-disconnected #oauth-status-text {
1093
+ color: var(--text-secondary);
1094
+ }
1095
+
1096
+ .oauth-connected #oauth-status-text {
1097
+ color: #16a34a;
1098
+ }
1099
+
1100
+ .oauth-hint {
1101
+ font-size: 12px;
1102
+ color: var(--text-muted);
1103
+ line-height: 1.5;
1104
+ }
1105
+
1106
+ .oauth-btn {
1107
+ width: 100%;
1108
+ margin-top: 8px;
1109
+ }
1110
+
1111
+ .oauth-btn svg {
1112
+ margin-right: 8px;
1113
+ }
@@ -1,200 +0,0 @@
1
- import { state } from "./config.js";
2
- import { restartRuntime, saveConfigData } from "./api.js";
3
-
4
- // DOM Elements
5
- let dialog;
6
- let settingsBtn;
7
- let closeBtn;
8
- let saveBtn;
9
- let providerSelect;
10
- let apiKeyInput;
11
- let telegramTokenInput;
12
- let slackBotTokenInput;
13
- let slackAppTokenInput;
14
- let keyStatus;
15
- let restartBtn;
16
-
17
- export function initSettings() {
18
- dialog = document.getElementById("settings-dialog");
19
- settingsBtn = document.getElementById("open-settings-btn");
20
- closeBtn = document.getElementById("close-settings-btn");
21
- saveBtn = document.getElementById("save-settings-btn");
22
-
23
- providerSelect = document.getElementById("provider-select");
24
- apiKeyInput = document.getElementById("api-key-input");
25
- telegramTokenInput = document.getElementById("telegram-token-input");
26
- slackBotTokenInput = document.getElementById("slack-bot-token-input");
27
- slackAppTokenInput = document.getElementById("slack-app-token-input");
28
- keyStatus = document.getElementById("key-status");
29
- restartBtn = document.getElementById("restart-service-btn");
30
-
31
- if (!dialog) return;
32
-
33
- // Open/Close dialog
34
- if (settingsBtn) {
35
- settingsBtn.addEventListener("click", () => {
36
- populateForm();
37
- dialog.showModal();
38
- });
39
- }
40
-
41
- if (closeBtn) {
42
- closeBtn.addEventListener("click", () => {
43
- dialog.close();
44
- keyStatus.textContent = ""; // clear status on close
45
- });
46
- }
47
-
48
- // Save Settings
49
- if (saveBtn) {
50
- saveBtn.addEventListener("click", handleSave);
51
- }
52
- if (restartBtn) {
53
- restartBtn.addEventListener("click", handleRestart);
54
- }
55
-
56
- // Placeholder logic
57
- if (providerSelect) {
58
- providerSelect.addEventListener("change", updatePlaceholder);
59
- }
60
- }
61
-
62
- function updatePlaceholder() {
63
- const p = providerSelect.value;
64
- if (p === "openai") apiKeyInput.placeholder = "sk-proj-...";
65
- else if (p === "anthropic") apiKeyInput.placeholder = "sk-ant-api03-...";
66
- else apiKeyInput.placeholder = "sk-...";
67
- }
68
-
69
- function populateForm() {
70
- const config = state.config;
71
- if (!config) return;
72
-
73
- if (state.restartRequired) {
74
- setStatus(
75
- "Settings saved. Restart service to apply changes.",
76
- "warning",
77
- );
78
- updateRestartButton(true);
79
- } else {
80
- setStatus("", "");
81
- updateRestartButton(false);
82
- }
83
-
84
- if (config.hasApiKey && config.apiKey) {
85
- apiKeyInput.value = config.apiKey;
86
- } else if (config.hasApiKey) {
87
- apiKeyInput.value = "***************************************************";
88
- } else {
89
- apiKeyInput.value = "";
90
- }
91
-
92
- if (config.provider) {
93
- providerSelect.value = config.provider;
94
- }
95
- updatePlaceholder();
96
-
97
- const adapters = config.adapters || {};
98
- if (adapters.telegram && adapters.telegram.token) {
99
- telegramTokenInput.value = adapters.telegram.token;
100
- } else {
101
- telegramTokenInput.value = "";
102
- }
103
-
104
- if (adapters.slack) {
105
- slackBotTokenInput.value = adapters.slack.botToken || "";
106
- slackAppTokenInput.value = adapters.slack.appToken || "";
107
- } else {
108
- slackBotTokenInput.value = "";
109
- slackAppTokenInput.value = "";
110
- }
111
- }
112
-
113
- async function handleSave() {
114
- const key = apiKeyInput.value.trim();
115
- const provider = providerSelect.value;
116
- const telegramToken = telegramTokenInput.value.trim();
117
- const slackBotToken = slackBotTokenInput.value.trim();
118
- const slackAppToken = slackAppTokenInput.value.trim();
119
-
120
- const adapters = {};
121
- if (telegramToken) adapters.telegram = { token: telegramToken };
122
- if (slackBotToken || slackAppToken) {
123
- adapters.slack = {
124
- botToken: slackBotToken || undefined,
125
- appToken: slackAppToken || undefined
126
- };
127
- }
128
-
129
- const updates = { provider, adapters };
130
- if (key && key !== "***************************************************" && key !== state.config.apiKey) {
131
- updates.key = key;
132
- }
133
-
134
- try {
135
- const res = await saveConfigData(updates);
136
-
137
- // Update local config
138
- state.config.provider = res.provider;
139
- state.config.adapters = res.adapters;
140
- if (updates.key) {
141
- state.config.hasApiKey = true;
142
- state.config.apiKey = updates.key;
143
- }
144
-
145
- if (state.config.hasApiKey && state.config.apiKey) {
146
- apiKeyInput.value = state.config.apiKey;
147
- } else if (state.config.hasApiKey) {
148
- apiKeyInput.value = "***************************************************";
149
- } else {
150
- apiKeyInput.value = "";
151
- }
152
- state.restartRequired = !!res.requiresRestart;
153
-
154
- if (res.requiresRestart) {
155
- setStatus(
156
- "Settings saved. Restart service to apply changes.",
157
- "warning",
158
- );
159
- updateRestartButton(true);
160
- return;
161
- }
162
-
163
- setStatus("Settings saved", "success");
164
- updateRestartButton(false);
165
-
166
- } catch (err) {
167
- setStatus("Save failed: " + err.message, "error");
168
- }
169
- }
170
-
171
- async function handleRestart() {
172
- if (!restartBtn) return;
173
-
174
- restartBtn.disabled = true;
175
- if (saveBtn) saveBtn.disabled = true;
176
- setStatus("Restarting service...", "warning");
177
-
178
- try {
179
- await restartRuntime();
180
- setTimeout(() => {
181
- window.location.reload();
182
- }, 6000);
183
- } catch (err) {
184
- if (saveBtn) saveBtn.disabled = false;
185
- restartBtn.disabled = false;
186
- setStatus("Restart failed: " + err.message, "error");
187
- }
188
- }
189
-
190
- function updateRestartButton(show) {
191
- if (!restartBtn) return;
192
- restartBtn.hidden = !show;
193
- restartBtn.disabled = false;
194
- }
195
-
196
- function setStatus(message, status) {
197
- if (!keyStatus) return;
198
- keyStatus.textContent = message;
199
- keyStatus.className = status ? `status-text ${status}` : "status-text";
200
- }