work-agent 0.1.0

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 (245) hide show
  1. package/README.md +234 -0
  2. package/app/(admin)/approvals/page.tsx +16 -0
  3. package/app/(admin)/audit/page.tsx +18 -0
  4. package/app/(admin)/layout.tsx +47 -0
  5. package/app/(admin)/scheduled-tasks/page.tsx +17 -0
  6. package/app/(admin)/settings/page.tsx +46 -0
  7. package/app/(admin)/skills/[name]/page.tsx +378 -0
  8. package/app/(admin)/skills/page.tsx +406 -0
  9. package/app/(admin)/statistics/page.tsx +416 -0
  10. package/app/(admin)/tickets/[id]/page.tsx +348 -0
  11. package/app/(admin)/tickets/new/page.tsx +309 -0
  12. package/app/(admin)/tickets/page.tsx +27 -0
  13. package/app/api/audit/route.ts +30 -0
  14. package/app/api/auth/feishu/callback/route.ts +72 -0
  15. package/app/api/auth/feishu/login/route.ts +17 -0
  16. package/app/api/auth/feishu/sso/route.ts +78 -0
  17. package/app/api/auth/login/route.ts +85 -0
  18. package/app/api/auth/oauth/route.ts +168 -0
  19. package/app/api/config/providers/route.ts +105 -0
  20. package/app/api/config/route.ts +115 -0
  21. package/app/api/config/status/route.ts +56 -0
  22. package/app/api/config/test/route.ts +212 -0
  23. package/app/api/documents/[id]/route.ts +88 -0
  24. package/app/api/documents/route.ts +53 -0
  25. package/app/api/health/route.ts +32 -0
  26. package/app/api/knowledge/[id]/route.ts +152 -0
  27. package/app/api/knowledge/from-session/route.ts +27 -0
  28. package/app/api/knowledge/route.ts +100 -0
  29. package/app/api/market/knowledge/[id]/route.ts +92 -0
  30. package/app/api/market/knowledge/route.ts +130 -0
  31. package/app/api/marketplace/skills/[id]/approve/route.ts +68 -0
  32. package/app/api/marketplace/skills/[id]/certify/route.ts +54 -0
  33. package/app/api/marketplace/skills/[id]/install/route.ts +180 -0
  34. package/app/api/marketplace/skills/[id]/promote-to-system/route.ts +219 -0
  35. package/app/api/marketplace/skills/[id]/rate/route.ts +90 -0
  36. package/app/api/marketplace/skills/[id]/ratings/route.ts +55 -0
  37. package/app/api/marketplace/skills/[id]/reject/route.ts +68 -0
  38. package/app/api/marketplace/skills/[id]/route.ts +177 -0
  39. package/app/api/marketplace/skills/route.ts +235 -0
  40. package/app/api/memory/route.ts +40 -0
  41. package/app/api/my/files/[id]/route.ts +52 -0
  42. package/app/api/my/files/route.ts +230 -0
  43. package/app/api/my/knowledge/route.ts +36 -0
  44. package/app/api/pi-chat/route.ts +443 -0
  45. package/app/api/recommend/route.ts +38 -0
  46. package/app/api/scheduled-tasks/[id]/execute/route.ts +132 -0
  47. package/app/api/scheduled-tasks/[id]/route.ts +165 -0
  48. package/app/api/scheduled-tasks/[id]/toggle/route.ts +53 -0
  49. package/app/api/scheduled-tasks/route.ts +101 -0
  50. package/app/api/sessions/[id]/messages/route.ts +212 -0
  51. package/app/api/sessions/route.ts +101 -0
  52. package/app/api/share/file/[id]/route.ts +37 -0
  53. package/app/api/skills/[name]/execute/route.ts +121 -0
  54. package/app/api/skills/[name]/route.ts +167 -0
  55. package/app/api/skills/create/route.ts +65 -0
  56. package/app/api/skills/generate/route.ts +405 -0
  57. package/app/api/skills/installed/route.ts +151 -0
  58. package/app/api/skills/route.ts +174 -0
  59. package/app/api/skills/translate/route.ts +40 -0
  60. package/app/api/skills/user/[name]/route.ts +159 -0
  61. package/app/api/skills/user/route.ts +90 -0
  62. package/app/api/statistics/route.ts +94 -0
  63. package/app/api/task-executions/[id]/route.ts +34 -0
  64. package/app/api/task-executions/route.ts +29 -0
  65. package/app/api/tickets/[id]/approve/route.ts +129 -0
  66. package/app/api/tickets/[id]/execute/route.ts +201 -0
  67. package/app/api/tickets/[id]/route.ts +127 -0
  68. package/app/api/tickets/route.ts +103 -0
  69. package/app/api/user/skills/route.ts +175 -0
  70. package/app/api/users/route.ts +80 -0
  71. package/app/chat/page.tsx +5 -0
  72. package/app/globals.css +84 -0
  73. package/app/h5/layout.tsx +5 -0
  74. package/app/h5/mobile-approvals-page.tsx +167 -0
  75. package/app/h5/mobile-chat-page.tsx +951 -0
  76. package/app/h5/mobile-profile-page.tsx +147 -0
  77. package/app/h5/mobile-tickets-page.tsx +121 -0
  78. package/app/h5/page.tsx +23 -0
  79. package/app/h5/ticket-action-buttons.tsx +80 -0
  80. package/app/layout.tsx +26 -0
  81. package/app/login/page.tsx +318 -0
  82. package/app/market/knowledge/[id]/page.tsx +77 -0
  83. package/app/market/knowledge/page.tsx +358 -0
  84. package/app/market/layout.tsx +29 -0
  85. package/app/market/page.tsx +18 -0
  86. package/app/market/skills/page.tsx +397 -0
  87. package/app/my/files/page.tsx +511 -0
  88. package/app/my/knowledge/[id]/page.tsx +271 -0
  89. package/app/my/knowledge/new/page.tsx +234 -0
  90. package/app/my/knowledge/page.tsx +248 -0
  91. package/app/my/layout.tsx +32 -0
  92. package/app/my/memory/page.tsx +164 -0
  93. package/app/my/page.tsx +18 -0
  94. package/app/my/scheduled-tasks/[id]/edit/page.tsx +290 -0
  95. package/app/my/scheduled-tasks/[id]/executions/page.tsx +275 -0
  96. package/app/my/scheduled-tasks/[id]/page.tsx +284 -0
  97. package/app/my/scheduled-tasks/new/page.tsx +230 -0
  98. package/app/my/scheduled-tasks/page.tsx +27 -0
  99. package/app/my/skills/[name]/page.tsx +320 -0
  100. package/app/my/skills/new/page.tsx +394 -0
  101. package/app/my/skills/page.tsx +303 -0
  102. package/app/page.tsx +2288 -0
  103. package/app/share/[sessionId]/page.tsx +226 -0
  104. package/app/share/file/[id]/page.tsx +140 -0
  105. package/bin/README.md +63 -0
  106. package/bin/generate-api-system +300 -0
  107. package/bin/postinstall.js +95 -0
  108. package/bin/work-agent.js +173 -0
  109. package/components/ai-elements/agent.tsx +142 -0
  110. package/components/ai-elements/artifact.tsx +149 -0
  111. package/components/ai-elements/attachments.tsx +427 -0
  112. package/components/ai-elements/audio-player.tsx +232 -0
  113. package/components/ai-elements/canvas.tsx +26 -0
  114. package/components/ai-elements/chain-of-thought.tsx +223 -0
  115. package/components/ai-elements/checkpoint.tsx +72 -0
  116. package/components/ai-elements/code-block.tsx +555 -0
  117. package/components/ai-elements/commit.tsx +449 -0
  118. package/components/ai-elements/confirmation.tsx +173 -0
  119. package/components/ai-elements/connection.tsx +28 -0
  120. package/components/ai-elements/context.tsx +410 -0
  121. package/components/ai-elements/controls.tsx +19 -0
  122. package/components/ai-elements/conversation.tsx +167 -0
  123. package/components/ai-elements/edge.tsx +144 -0
  124. package/components/ai-elements/environment-variables.tsx +325 -0
  125. package/components/ai-elements/file-tree.tsx +298 -0
  126. package/components/ai-elements/image.tsx +25 -0
  127. package/components/ai-elements/inline-citation.tsx +294 -0
  128. package/components/ai-elements/jsx-preview.tsx +250 -0
  129. package/components/ai-elements/message.tsx +367 -0
  130. package/components/ai-elements/mic-selector.tsx +372 -0
  131. package/components/ai-elements/model-selector.tsx +214 -0
  132. package/components/ai-elements/node.tsx +72 -0
  133. package/components/ai-elements/open-in-chat.tsx +367 -0
  134. package/components/ai-elements/package-info.tsx +235 -0
  135. package/components/ai-elements/panel.tsx +16 -0
  136. package/components/ai-elements/persona.tsx +280 -0
  137. package/components/ai-elements/plan.tsx +144 -0
  138. package/components/ai-elements/prompt-input.tsx +1341 -0
  139. package/components/ai-elements/queue.tsx +275 -0
  140. package/components/ai-elements/reasoning.tsx +355 -0
  141. package/components/ai-elements/sandbox.tsx +133 -0
  142. package/components/ai-elements/schema-display.tsx +473 -0
  143. package/components/ai-elements/shimmer.tsx +78 -0
  144. package/components/ai-elements/snippet.tsx +141 -0
  145. package/components/ai-elements/sources.tsx +78 -0
  146. package/components/ai-elements/speech-input.tsx +324 -0
  147. package/components/ai-elements/stack-trace.tsx +531 -0
  148. package/components/ai-elements/suggestion.tsx +58 -0
  149. package/components/ai-elements/task.tsx +88 -0
  150. package/components/ai-elements/terminal.tsx +277 -0
  151. package/components/ai-elements/test-results.tsx +497 -0
  152. package/components/ai-elements/tool.tsx +174 -0
  153. package/components/ai-elements/toolbar.tsx +17 -0
  154. package/components/ai-elements/transcription.tsx +126 -0
  155. package/components/ai-elements/voice-selector.tsx +525 -0
  156. package/components/ai-elements/web-preview.tsx +282 -0
  157. package/components/audit-log-list.tsx +114 -0
  158. package/components/chat/EmptyPreviewState.tsx +12 -0
  159. package/components/chat/KnowledgePickerDialog.tsx +464 -0
  160. package/components/chat/KnowledgePreview.tsx +70 -0
  161. package/components/chat/KnowledgePreviewPanel.tsx +86 -0
  162. package/components/chat/MentionInput.tsx +309 -0
  163. package/components/chat/OrganizeDialog.tsx +258 -0
  164. package/components/chat/RecommendationBanner.tsx +94 -0
  165. package/components/chat/SaveToKnowledgeDialog.tsx +193 -0
  166. package/components/chat/SkillSelector.tsx +305 -0
  167. package/components/chat/SkillSwitcher.tsx +163 -0
  168. package/components/client-layout.tsx +15 -0
  169. package/components/knowledge/KnowledgeMetadataPanel.tsx +293 -0
  170. package/components/layout-wrapper.tsx +18 -0
  171. package/components/mobile-layout.tsx +62 -0
  172. package/components/scheduled-task-list.tsx +356 -0
  173. package/components/setup-guide.tsx +484 -0
  174. package/components/sub-nav.tsx +54 -0
  175. package/components/ticket-detail-content.tsx +383 -0
  176. package/components/ticket-list.tsx +366 -0
  177. package/components/top-nav.tsx +132 -0
  178. package/components/ui/accordion.tsx +58 -0
  179. package/components/ui/alert.tsx +59 -0
  180. package/components/ui/avatar.tsx +50 -0
  181. package/components/ui/badge.tsx +36 -0
  182. package/components/ui/button-group.tsx +83 -0
  183. package/components/ui/button.tsx +57 -0
  184. package/components/ui/card.tsx +91 -0
  185. package/components/ui/carousel.tsx +262 -0
  186. package/components/ui/collapsible.tsx +11 -0
  187. package/components/ui/command.tsx +153 -0
  188. package/components/ui/dialog.tsx +122 -0
  189. package/components/ui/dropdown-menu.tsx +200 -0
  190. package/components/ui/hover-card.tsx +29 -0
  191. package/components/ui/input-group.tsx +170 -0
  192. package/components/ui/input.tsx +22 -0
  193. package/components/ui/label.tsx +26 -0
  194. package/components/ui/popover.tsx +31 -0
  195. package/components/ui/progress.tsx +28 -0
  196. package/components/ui/scroll-area.tsx +48 -0
  197. package/components/ui/select.tsx +174 -0
  198. package/components/ui/separator.tsx +31 -0
  199. package/components/ui/spinner.tsx +16 -0
  200. package/components/ui/switch.tsx +29 -0
  201. package/components/ui/table.tsx +120 -0
  202. package/components/ui/tabs.tsx +55 -0
  203. package/components/ui/textarea.tsx +22 -0
  204. package/components/ui/tooltip.tsx +30 -0
  205. package/components/welcome-guide.tsx +182 -0
  206. package/components.json +24 -0
  207. package/lib/command-parser.ts +331 -0
  208. package/lib/dangerous-commands.ts +672 -0
  209. package/lib/db.ts +2250 -0
  210. package/lib/feishu-auth.ts +135 -0
  211. package/lib/file-storage.ts +306 -0
  212. package/lib/file-tool.ts +583 -0
  213. package/lib/knowledge-tool.ts +152 -0
  214. package/lib/knowledge-types.ts +66 -0
  215. package/lib/market-client.ts +313 -0
  216. package/lib/market-db.ts +736 -0
  217. package/lib/market-types.ts +51 -0
  218. package/lib/memory-tool.ts +211 -0
  219. package/lib/memory.ts +197 -0
  220. package/lib/pi-config.ts +436 -0
  221. package/lib/pi-session.ts +799 -0
  222. package/lib/pinyin.ts +13 -0
  223. package/lib/recommendation.ts +227 -0
  224. package/lib/risk-estimator.ts +350 -0
  225. package/lib/scheduled-task-tool.ts +184 -0
  226. package/lib/scheduler-init.ts +43 -0
  227. package/lib/scheduler.ts +416 -0
  228. package/lib/secure-bash-tool.ts +413 -0
  229. package/lib/skill-engine.ts +396 -0
  230. package/lib/skill-generator.ts +269 -0
  231. package/lib/skill-loader.ts +234 -0
  232. package/lib/skill-tool.ts +188 -0
  233. package/lib/skill-types.ts +82 -0
  234. package/lib/skills-init.ts +58 -0
  235. package/lib/ticket-tool.ts +246 -0
  236. package/lib/user-skill-types.ts +30 -0
  237. package/lib/user-skills.ts +362 -0
  238. package/lib/utils.ts +6 -0
  239. package/lib/workflow.ts +154 -0
  240. package/lib/zip-tool.ts +191 -0
  241. package/next.config.js +8 -0
  242. package/package.json +106 -0
  243. package/public/.gitkeep +1 -0
  244. package/public/icon.svg +1 -0
  245. package/tsconfig.json +42 -0
@@ -0,0 +1,436 @@
1
+ /**
2
+ * PI Configuration Manager
3
+ *
4
+ * Manages loading and saving of pi-coding-agent configuration.
5
+ * Configuration files are located at ~/.pi/agent/ by default
6
+ * Can be overridden via PI_CONFIG_PATH environment variable
7
+ * - models.json: Custom model providers and API keys
8
+ * - auth.json: Runtime auth settings (fallback)
9
+ */
10
+
11
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
12
+ import { join } from 'path';
13
+ import { homedir, tmpdir } from 'os';
14
+
15
+ /**
16
+ * Get the home directory for configuration
17
+ * Priority: PI_CONFIG_PATH env > /root (for deployed environments) > HOME env > homedir()
18
+ */
19
+ function getConfigDir(): string {
20
+ // 1. Explicit override via environment variable
21
+ if (process.env.PI_CONFIG_PATH) {
22
+ return process.env.PI_CONFIG_PATH;
23
+ }
24
+
25
+ // 2. Check if /root/.pi/agent exists (deployed environment)
26
+ const rootConfigDir = '/root/.pi/agent';
27
+ if (existsSync(rootConfigDir)) {
28
+ return rootConfigDir;
29
+ }
30
+
31
+ // 3. Use HOME environment variable
32
+ if (process.env.HOME) {
33
+ return join(process.env.HOME, '.pi', 'agent');
34
+ }
35
+
36
+ // 4. Use os.homedir()
37
+ const home = homedir();
38
+ if (home && home !== '/') {
39
+ return join(home, '.pi', 'agent');
40
+ }
41
+
42
+ // 5. Fallback to /root (for production deployments)
43
+ return '/root/.pi/agent';
44
+ }
45
+
46
+ // PI Agent config directory
47
+ export const PI_CONFIG_DIR = getConfigDir();
48
+
49
+ // Configuration file paths
50
+ const MODELS_FILE = join(PI_CONFIG_DIR, 'models.json');
51
+
52
+ /**
53
+ * Model provider configuration
54
+ */
55
+ export interface ModelProvider {
56
+ baseUrl: string;
57
+ api: string;
58
+ apiKey: string;
59
+ models: Array<{ id: string }>;
60
+ }
61
+
62
+ /**
63
+ * Models.json structure
64
+ */
65
+ export interface ModelsConfig {
66
+ providers: Record<string, ModelProvider>;
67
+ }
68
+
69
+ /**
70
+ * Loaded configuration with provider ID
71
+ */
72
+ export interface ProviderConfig {
73
+ id: string;
74
+ name: string;
75
+ baseUrl: string;
76
+ api: string;
77
+ apiKey: string;
78
+ models: string[];
79
+ }
80
+
81
+ /**
82
+ * Ensure config directory exists
83
+ */
84
+ function ensureConfigDir(): void {
85
+ if (!existsSync(PI_CONFIG_DIR)) {
86
+ try {
87
+ mkdirSync(PI_CONFIG_DIR, { recursive: true });
88
+ } catch (error) {
89
+ console.error('Failed to create config directory:', error);
90
+ }
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Default provider configurations (without API keys)
96
+ */
97
+ export const DEFAULT_PROVIDERS: Record<string, Omit<ModelProvider, 'apiKey'>> = {
98
+ zhipu: {
99
+ baseUrl: 'https://open.bigmodel.cn/api/anthropic',
100
+ api: 'anthropic-messages',
101
+ models: [{ id: 'glm-4.7' }],
102
+ },
103
+ anthropic: {
104
+ baseUrl: 'https://api.anthropic.com',
105
+ api: 'anthropic-messages',
106
+ models: [{ id: 'claude-sonnet-4-20250514' }],
107
+ },
108
+ deepseek: {
109
+ baseUrl: 'https://api.deepseek.com',
110
+ api: 'openai-completions',
111
+ models: [{ id: 'deepseek-chat' }],
112
+ },
113
+ openai: {
114
+ baseUrl: 'https://api.openai.com/v1',
115
+ api: 'openai-completions',
116
+ models: [{ id: 'gpt-4o' }],
117
+ },
118
+ ollama: {
119
+ baseUrl: 'http://192.168.44.151:11434/v1',
120
+ api: 'openai-completions',
121
+ models: [{ id: 'qwen3.5:27b' }],
122
+ },
123
+ };
124
+
125
+ /**
126
+ * Get default configuration from environment variables
127
+ *
128
+ * Supported environment variables (AI_ prefix):
129
+ * - AI_PROVIDER: Provider ID (zhipu, anthropic, deepseek, openai, ollama, or custom)
130
+ * - AI_MODEL: Model ID (e.g., glm-4, claude-sonnet-4-20250514, qwen3.5:27b)
131
+ * - AI_BASE_URL: Custom API base URL
132
+ * - AI_API_KEY: Generic API key (lower priority than provider-specific keys)
133
+ *
134
+ * Legacy environment variables (still supported):
135
+ * - ZHIPU_API_KEY: Zhipu AI API key
136
+ * - ANTHROPIC_API_KEY: Anthropic API key
137
+ */
138
+ function getDefaultConfigFromEnv(): ModelsConfig | null {
139
+ // Check for new AI_ prefix environment variables first
140
+ const aiProvider = process.env.AI_PROVIDER;
141
+ const aiModel = process.env.AI_MODEL;
142
+ const aiBaseUrl = process.env.AI_BASE_URL;
143
+ const aiApiKey = process.env.AI_API_KEY;
144
+
145
+ // If AI_PROVIDER is set, use the new configuration system
146
+ if (aiProvider) {
147
+ const providerId = aiProvider.toLowerCase();
148
+ const defaultProvider = DEFAULT_PROVIDERS[providerId];
149
+
150
+ // Determine API key: provider-specific key > AI_API_KEY
151
+ let apiKey = aiApiKey;
152
+ if (providerId === 'zhipu' && process.env.ZHIPU_API_KEY) {
153
+ apiKey = process.env.ZHIPU_API_KEY;
154
+ } else if (providerId === 'anthropic' && process.env.ANTHROPIC_API_KEY) {
155
+ apiKey = process.env.ANTHROPIC_API_KEY;
156
+ }
157
+
158
+ if (!apiKey) {
159
+ console.warn(`AI_PROVIDER is set to "${providerId}" but no API key found. Set AI_API_KEY or ${providerId.toUpperCase()}_API_KEY`);
160
+ return null;
161
+ }
162
+
163
+ console.log(`Using AI_PROVIDER=${providerId} from environment`);
164
+
165
+ return {
166
+ providers: {
167
+ [providerId]: {
168
+ baseUrl: aiBaseUrl || defaultProvider?.baseUrl || '',
169
+ api: defaultProvider?.api || 'anthropic-messages',
170
+ apiKey,
171
+ models: [{ id: aiModel || defaultProvider?.models?.[0]?.id || 'default' }],
172
+ },
173
+ },
174
+ };
175
+ }
176
+
177
+ // Fallback to legacy environment variables
178
+ // Priority: ZHIPU_API_KEY > ANTHROPIC_API_KEY
179
+ const zhipuKey = process.env.ZHIPU_API_KEY;
180
+ if (zhipuKey) {
181
+ console.log('Using ZHIPU_API_KEY from environment');
182
+ return {
183
+ providers: {
184
+ zhipu: {
185
+ ...DEFAULT_PROVIDERS.zhipu,
186
+ apiKey: zhipuKey,
187
+ },
188
+ },
189
+ };
190
+ }
191
+
192
+ const anthropicKey = process.env.ANTHROPIC_API_KEY;
193
+ if (anthropicKey) {
194
+ console.log('Using ANTHROPIC_API_KEY from environment');
195
+ return {
196
+ providers: {
197
+ anthropic: {
198
+ ...DEFAULT_PROVIDERS.anthropic,
199
+ apiKey: anthropicKey,
200
+ },
201
+ },
202
+ };
203
+ }
204
+
205
+ return null;
206
+ }
207
+
208
+ /**
209
+ * Load models.json configuration
210
+ */
211
+ export function loadModelsConfig(): ModelsConfig | null {
212
+ try {
213
+ console.log('[pi-config] === Loading configuration ===');
214
+ console.log('[pi-config] HOME env:', process.env.HOME || 'not set');
215
+ console.log('[pi-config] USER env:', process.env.USER || 'not set');
216
+ console.log('[pi-config] PI_CONFIG_PATH env:', process.env.PI_CONFIG_PATH || 'not set');
217
+ console.log('[pi-config] Computed PI_CONFIG_DIR:', PI_CONFIG_DIR);
218
+ console.log('[pi-config] MODELS_FILE path:', MODELS_FILE);
219
+
220
+ if (!existsSync(MODELS_FILE)) {
221
+ console.log('[pi-config] models.json not found at', MODELS_FILE);
222
+ console.log('[pi-config] Checking environment variables...');
223
+ const envConfig = getDefaultConfigFromEnv();
224
+ if (envConfig) {
225
+ console.log('[pi-config] Using config from environment variables');
226
+ } else {
227
+ console.log('[pi-config] No configuration found!');
228
+ }
229
+ return envConfig;
230
+ }
231
+
232
+ console.log('[pi-config] models.json found, reading...');
233
+ const content = readFileSync(MODELS_FILE, 'utf-8');
234
+ console.log('[pi-config] models.json content length:', content.length);
235
+
236
+ const config = JSON.parse(content) as ModelsConfig;
237
+ console.log('[pi-config] Parsed providers:', Object.keys(config.providers || {}));
238
+
239
+ // Validate and log configuration
240
+ for (const [id, provider] of Object.entries(config.providers || {})) {
241
+ if (!provider.apiKey) {
242
+ console.warn(`[pi-config] Provider ${id} has no apiKey!`);
243
+ } else {
244
+ const key = provider.apiKey;
245
+ const maskedKey = key.length > 20
246
+ ? `${key.substring(0, 10)}...${key.substring(key.length - 10)}`
247
+ : '***';
248
+ console.log(`[pi-config] Provider ${id}:`);
249
+ console.log(`[pi-config] baseUrl: ${provider.baseUrl}`);
250
+ console.log(`[pi-config] api: ${provider.api}`);
251
+ console.log(`[pi-config] apiKey: ${maskedKey} (length: ${key.length})`);
252
+ console.log(`[pi-config] models: ${provider.models?.map(m => m.id).join(', ')}`);
253
+ }
254
+ }
255
+
256
+ return config;
257
+ } catch (error) {
258
+ console.error('[pi-config] Failed to load models.json:', error);
259
+ return getDefaultConfigFromEnv();
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Save models.json configuration
265
+ */
266
+ export function saveModelsConfig(config: ModelsConfig): boolean {
267
+ try {
268
+ ensureConfigDir();
269
+
270
+ // Validate config before saving
271
+ if (!config.providers || Object.keys(config.providers).length === 0) {
272
+ throw new Error('Invalid config: providers is required');
273
+ }
274
+
275
+ // Validate each provider
276
+ for (const [id, provider] of Object.entries(config.providers)) {
277
+ if (!provider.baseUrl || !provider.apiKey) {
278
+ throw new Error(`Invalid provider ${id}: baseUrl and apiKey are required`);
279
+ }
280
+ }
281
+
282
+ writeFileSync(MODELS_FILE, JSON.stringify(config, null, 2), 'utf-8');
283
+ console.log('Saved models.json');
284
+ return true;
285
+ } catch (error) {
286
+ console.error('Failed to save models.json:', error);
287
+ throw error;
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Get all available providers from models.json
293
+ */
294
+ export function getAvailableProviders(): ProviderConfig[] {
295
+ const config = loadModelsConfig();
296
+ if (!config || !config.providers) {
297
+ return [];
298
+ }
299
+
300
+ const providers: ProviderConfig[] = [];
301
+
302
+ for (const [id, provider] of Object.entries(config.providers)) {
303
+ providers.push({
304
+ id,
305
+ name: getProviderDisplayName(id),
306
+ baseUrl: provider.baseUrl,
307
+ api: provider.api,
308
+ apiKey: provider.apiKey,
309
+ models: provider.models?.map(m => m.id) || [],
310
+ });
311
+ }
312
+
313
+ return providers;
314
+ }
315
+
316
+ /**
317
+ * Get provider display name
318
+ */
319
+ function getProviderDisplayName(id: string): string {
320
+ const names: Record<string, string> = {
321
+ 'anthropic': 'Anthropic (Claude)',
322
+ 'openai': 'OpenAI (GPT)',
323
+ 'zhipu': '智谱 AI (GLM)',
324
+ 'deepseek': 'DeepSeek',
325
+ 'ollama': 'Ollama (Local)',
326
+ };
327
+
328
+ return names[id] || id.charAt(0).toUpperCase() + id.slice(1);
329
+ }
330
+
331
+ /**
332
+ * Get default provider (first one in the list)
333
+ */
334
+ export function getDefaultProvider(): ProviderConfig | null {
335
+ const providers = getAvailableProviders();
336
+ return providers.length > 0 ? providers[0] : null;
337
+ }
338
+
339
+ /**
340
+ * Get provider by ID
341
+ */
342
+ export function getProviderById(id: string): ProviderConfig | null {
343
+ const providers = getAvailableProviders();
344
+ return providers.find(p => p.id === id) || null;
345
+ }
346
+
347
+ /**
348
+ * Update or add a provider
349
+ */
350
+ export function upsertProvider(
351
+ id: string,
352
+ provider: Omit<ModelProvider, 'models'> & { models?: string[] }
353
+ ): ModelsConfig {
354
+ const config = loadModelsConfig() || { providers: {} };
355
+
356
+ config.providers[id] = {
357
+ baseUrl: provider.baseUrl,
358
+ api: provider.api,
359
+ apiKey: provider.apiKey,
360
+ models: (provider.models || []).map(m => ({ id: m })),
361
+ };
362
+
363
+ saveModelsConfig(config);
364
+ return config;
365
+ }
366
+
367
+ /**
368
+ * Delete a provider
369
+ */
370
+ export function deleteProvider(id: string): ModelsConfig | null {
371
+ const config = loadModelsConfig();
372
+ if (!config || !config.providers[id]) {
373
+ return null;
374
+ }
375
+
376
+ delete config.providers[id];
377
+ saveModelsConfig(config);
378
+ return config;
379
+ }
380
+
381
+ /**
382
+ * Get configuration for AuthStorage
383
+ * Returns the first available provider's API key and base URL
384
+ */
385
+ export function getAuthConfig(): { apiKey: string; baseUrl?: string } | null {
386
+ const provider = getDefaultProvider();
387
+ if (!provider) {
388
+ return null;
389
+ }
390
+
391
+ return {
392
+ apiKey: provider.apiKey,
393
+ baseUrl: provider.baseUrl,
394
+ };
395
+ }
396
+
397
+ /**
398
+ * Validate provider configuration
399
+ */
400
+ export function validateProvider(provider: Partial<ModelProvider>): { valid: boolean; errors: string[] } {
401
+ const errors: string[] = [];
402
+
403
+ if (!provider.baseUrl) {
404
+ errors.push('baseUrl is required');
405
+ }
406
+
407
+ if (!provider.apiKey) {
408
+ errors.push('apiKey is required');
409
+ }
410
+
411
+ if (!provider.api) {
412
+ errors.push('api is required');
413
+ }
414
+
415
+ // Validate URL format
416
+ if (provider.baseUrl && !isValidUrl(provider.baseUrl)) {
417
+ errors.push('baseUrl must be a valid URL');
418
+ }
419
+
420
+ return {
421
+ valid: errors.length === 0,
422
+ errors,
423
+ };
424
+ }
425
+
426
+ /**
427
+ * Check if string is a valid URL
428
+ */
429
+ function isValidUrl(url: string): boolean {
430
+ try {
431
+ new URL(url);
432
+ return true;
433
+ } catch {
434
+ return false;
435
+ }
436
+ }