agentikit 0.0.9 → 0.0.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 (107) hide show
  1. package/README.md +129 -214
  2. package/dist/index.d.ts +8 -2
  3. package/dist/index.js +4 -1
  4. package/dist/src/asset-spec.d.ts +2 -0
  5. package/dist/src/asset-spec.js +22 -3
  6. package/dist/src/asset-type-handler.d.ts +27 -0
  7. package/dist/src/asset-type-handler.js +33 -0
  8. package/dist/src/cli.js +201 -75
  9. package/dist/src/common.d.ts +6 -1
  10. package/dist/src/common.js +18 -4
  11. package/dist/src/config-cli.d.ts +9 -0
  12. package/dist/src/config-cli.js +473 -0
  13. package/dist/src/config.d.ts +19 -6
  14. package/dist/src/config.js +139 -29
  15. package/dist/src/db.d.ts +46 -0
  16. package/dist/src/db.js +299 -0
  17. package/dist/src/embedder.js +12 -7
  18. package/dist/src/github.d.ts +4 -0
  19. package/dist/src/github.js +19 -0
  20. package/dist/src/handlers/agent-handler.d.ts +2 -0
  21. package/dist/src/handlers/agent-handler.js +26 -0
  22. package/dist/src/handlers/command-handler.d.ts +2 -0
  23. package/dist/src/handlers/command-handler.js +23 -0
  24. package/dist/src/handlers/index.d.ts +6 -0
  25. package/dist/src/handlers/index.js +23 -0
  26. package/dist/src/handlers/knowledge-handler.d.ts +2 -0
  27. package/dist/src/handlers/knowledge-handler.js +56 -0
  28. package/dist/src/handlers/markdown-helpers.d.ts +7 -0
  29. package/dist/src/handlers/markdown-helpers.js +15 -0
  30. package/dist/src/handlers/script-handler.d.ts +2 -0
  31. package/dist/src/handlers/script-handler.js +78 -0
  32. package/dist/src/handlers/skill-handler.d.ts +2 -0
  33. package/dist/src/handlers/skill-handler.js +30 -0
  34. package/dist/src/handlers/tool-handler.d.ts +2 -0
  35. package/dist/src/handlers/tool-handler.js +58 -0
  36. package/dist/src/indexer.d.ts +1 -23
  37. package/dist/src/indexer.js +162 -155
  38. package/dist/src/init.d.ts +2 -2
  39. package/dist/src/init.js +21 -9
  40. package/dist/src/llm.js +4 -3
  41. package/dist/src/metadata.d.ts +0 -1
  42. package/dist/src/metadata.js +6 -64
  43. package/dist/src/origin-resolve.d.ts +19 -0
  44. package/dist/src/origin-resolve.js +53 -0
  45. package/dist/src/registry-install.d.ts +2 -2
  46. package/dist/src/registry-install.js +142 -35
  47. package/dist/src/registry-resolve.js +90 -22
  48. package/dist/src/registry-search.d.ts +22 -0
  49. package/dist/src/registry-search.js +231 -97
  50. package/dist/src/registry-types.d.ts +9 -2
  51. package/dist/src/stash-add.js +4 -4
  52. package/dist/src/stash-clone.d.ts +22 -0
  53. package/dist/src/stash-clone.js +83 -0
  54. package/dist/src/stash-ref.d.ts +27 -3
  55. package/dist/src/stash-ref.js +63 -24
  56. package/dist/src/stash-registry.js +12 -12
  57. package/dist/src/stash-resolve.js +3 -0
  58. package/dist/src/stash-search.js +168 -164
  59. package/dist/src/stash-show.d.ts +1 -1
  60. package/dist/src/stash-show.js +28 -96
  61. package/dist/src/stash-source.d.ts +24 -0
  62. package/dist/src/stash-source.js +81 -0
  63. package/dist/src/stash-types.d.ts +14 -4
  64. package/dist/src/stash.d.ts +6 -0
  65. package/dist/src/stash.js +3 -0
  66. package/dist/src/tool-runner.d.ts +1 -1
  67. package/dist/src/tool-runner.js +18 -5
  68. package/package.json +7 -2
  69. package/src/asset-spec.ts +20 -4
  70. package/src/asset-type-handler.ts +77 -0
  71. package/src/cli.ts +213 -82
  72. package/src/common.ts +23 -5
  73. package/src/config-cli.ts +499 -0
  74. package/src/config.ts +160 -38
  75. package/src/db.ts +411 -0
  76. package/src/embedder.ts +22 -11
  77. package/src/github.ts +21 -0
  78. package/src/handlers/agent-handler.ts +32 -0
  79. package/src/handlers/command-handler.ts +29 -0
  80. package/src/handlers/index.ts +25 -0
  81. package/src/handlers/knowledge-handler.ts +62 -0
  82. package/src/handlers/markdown-helpers.ts +19 -0
  83. package/src/handlers/script-handler.ts +92 -0
  84. package/src/handlers/skill-handler.ts +37 -0
  85. package/src/handlers/tool-handler.ts +71 -0
  86. package/src/indexer.ts +208 -187
  87. package/src/init.ts +17 -9
  88. package/src/llm.ts +4 -3
  89. package/src/metadata.ts +5 -65
  90. package/src/origin-resolve.ts +67 -0
  91. package/src/registry-install.ts +158 -42
  92. package/src/registry-resolve.ts +92 -23
  93. package/src/registry-search.ts +288 -98
  94. package/src/registry-types.ts +10 -2
  95. package/src/stash-add.ts +14 -17
  96. package/src/stash-clone.ts +127 -0
  97. package/src/stash-ref.ts +84 -26
  98. package/src/stash-registry.ts +12 -12
  99. package/src/stash-resolve.ts +3 -0
  100. package/src/stash-search.ts +202 -184
  101. package/src/stash-show.ts +33 -90
  102. package/src/stash-source.ts +103 -0
  103. package/src/stash-types.ts +14 -4
  104. package/src/stash.ts +8 -0
  105. package/src/tool-runner.ts +18 -5
  106. package/dist/src/similarity.d.ts +0 -34
  107. package/src/similarity.ts +0 -271
@@ -0,0 +1,499 @@
1
+ import {
2
+ DEFAULT_CONFIG,
3
+ type AgentikitConfig,
4
+ type EmbeddingConnectionConfig,
5
+ type LlmConnectionConfig,
6
+ } from "./config"
7
+ import { EMBEDDING_DIM } from "./db"
8
+
9
+ export type ConfigProviderScope = "embedding" | "llm"
10
+
11
+ interface ProviderPreset<TConfig> {
12
+ name: string
13
+ description: string
14
+ config?: TConfig
15
+ }
16
+
17
+ const LOCAL_EMBEDDING_MODEL = "Xenova/all-MiniLM-L6-v2"
18
+ const DEFAULT_LLM_TEMPERATURE = 0.3
19
+ const DEFAULT_LLM_MAX_TOKENS = 512
20
+
21
+ const EMBEDDING_PROVIDER_PRESETS: Record<string, ProviderPreset<EmbeddingConnectionConfig>> = {
22
+ local: {
23
+ name: "local",
24
+ description: "Built-in local embeddings via @xenova/transformers.",
25
+ },
26
+ ollama: {
27
+ name: "ollama",
28
+ description: "Local Ollama embedding endpoint.",
29
+ config: {
30
+ provider: "ollama",
31
+ endpoint: "http://localhost:11434/v1/embeddings",
32
+ model: "nomic-embed-text",
33
+ dimension: EMBEDDING_DIM,
34
+ },
35
+ },
36
+ openai: {
37
+ name: "openai",
38
+ description: "OpenAI-compatible embeddings API.",
39
+ config: {
40
+ provider: "openai",
41
+ endpoint: "https://api.openai.com/v1/embeddings",
42
+ model: "text-embedding-3-small",
43
+ dimension: EMBEDDING_DIM,
44
+ },
45
+ },
46
+ }
47
+
48
+ const LLM_PROVIDER_PRESETS: Record<string, ProviderPreset<LlmConnectionConfig>> = {
49
+ disabled: {
50
+ name: "disabled",
51
+ description: "Disable LLM metadata enhancement.",
52
+ },
53
+ ollama: {
54
+ name: "ollama",
55
+ description: "Local Ollama chat completions endpoint.",
56
+ config: {
57
+ provider: "ollama",
58
+ endpoint: "http://localhost:11434/v1/chat/completions",
59
+ model: "llama3.2",
60
+ temperature: DEFAULT_LLM_TEMPERATURE,
61
+ maxTokens: DEFAULT_LLM_MAX_TOKENS,
62
+ },
63
+ },
64
+ openai: {
65
+ name: "openai",
66
+ description: "OpenAI-compatible chat completions API.",
67
+ config: {
68
+ provider: "openai",
69
+ endpoint: "https://api.openai.com/v1/chat/completions",
70
+ model: "gpt-4o-mini",
71
+ temperature: DEFAULT_LLM_TEMPERATURE,
72
+ maxTokens: DEFAULT_LLM_MAX_TOKENS,
73
+ },
74
+ },
75
+ }
76
+
77
+ export function parseConfigValue(key: string, value: string): Partial<AgentikitConfig> {
78
+ switch (key) {
79
+ case "semanticSearch":
80
+ if (value !== "true" && value !== "false") {
81
+ throw new Error(`Invalid value for semanticSearch: expected "true" or "false"`)
82
+ }
83
+ return { semanticSearch: value === "true" }
84
+ case "mountedStashDirs":
85
+ try {
86
+ const parsed = JSON.parse(value)
87
+ if (!Array.isArray(parsed)) throw new Error("expected JSON array")
88
+ return { mountedStashDirs: parsed.filter((d: unknown): d is string => typeof d === "string") }
89
+ } catch {
90
+ throw new Error(`Invalid value for mountedStashDirs: expected JSON array (e.g. '["/path/a","/path/b"]')`)
91
+ }
92
+ case "embedding":
93
+ return { embedding: parseEmbeddingConnectionValue(value) }
94
+ case "llm":
95
+ return { llm: parseLlmConnectionValue(value) }
96
+ default:
97
+ throw new Error(`Unknown config key: ${key}`)
98
+ }
99
+ }
100
+
101
+ export function getConfigValue(config: AgentikitConfig, key: string): unknown {
102
+ switch (key) {
103
+ case "semanticSearch":
104
+ return config.semanticSearch
105
+ case "mountedStashDirs":
106
+ return [...config.mountedStashDirs]
107
+ case "embedding":
108
+ return maskSecrets(getEmbeddingDisplayConfig(config))
109
+ case "embedding.provider":
110
+ return getEmbeddingProvider(config)
111
+ case "embedding.endpoint":
112
+ return getEmbeddingDisplayConfig(config).endpoint ?? null
113
+ case "embedding.model":
114
+ return getEmbeddingDisplayConfig(config).model ?? null
115
+ case "embedding.dimension":
116
+ return getEmbeddingDisplayConfig(config).dimension ?? null
117
+ case "embedding.apiKey":
118
+ return maskSecret(getEmbeddingDisplayConfig(config).apiKey) ?? null
119
+ case "llm":
120
+ return maskSecrets(getLlmDisplayConfig(config))
121
+ case "llm.provider":
122
+ return getLlmProvider(config)
123
+ case "llm.endpoint":
124
+ return getLlmDisplayConfig(config).endpoint ?? null
125
+ case "llm.model":
126
+ return getLlmDisplayConfig(config).model ?? null
127
+ case "llm.temperature":
128
+ return getLlmDisplayConfig(config).temperature ?? null
129
+ case "llm.maxTokens":
130
+ return getLlmDisplayConfig(config).maxTokens ?? null
131
+ case "llm.apiKey":
132
+ return maskSecret(getLlmDisplayConfig(config).apiKey) ?? null
133
+ default:
134
+ throw new Error(`Unknown config key: ${key}`)
135
+ }
136
+ }
137
+
138
+ export function setConfigValue(config: AgentikitConfig, key: string, rawValue: string): AgentikitConfig {
139
+ switch (key) {
140
+ case "semanticSearch":
141
+ case "mountedStashDirs":
142
+ case "embedding":
143
+ case "llm":
144
+ return { ...config, ...parseConfigValue(key, rawValue) }
145
+ case "embedding.provider":
146
+ return useProvider(config, "embedding", rawValue)
147
+ case "embedding.endpoint":
148
+ return {
149
+ ...config,
150
+ embedding: {
151
+ ...requireEmbeddingConfig(config),
152
+ endpoint: requireNonEmptyString(rawValue, key),
153
+ },
154
+ }
155
+ case "embedding.model":
156
+ return {
157
+ ...config,
158
+ embedding: {
159
+ ...requireEmbeddingConfig(config),
160
+ model: requireNonEmptyString(rawValue, key),
161
+ },
162
+ }
163
+ case "embedding.dimension":
164
+ return {
165
+ ...config,
166
+ embedding: {
167
+ ...requireEmbeddingConfig(config),
168
+ dimension: parsePositiveInteger(rawValue, key),
169
+ },
170
+ }
171
+ case "embedding.apiKey":
172
+ return {
173
+ ...config,
174
+ embedding: {
175
+ ...requireEmbeddingConfig(config),
176
+ apiKey: requireNonEmptyString(rawValue, key),
177
+ },
178
+ }
179
+ case "llm.provider":
180
+ return useProvider(config, "llm", rawValue)
181
+ case "llm.endpoint":
182
+ return {
183
+ ...config,
184
+ llm: {
185
+ ...requireLlmConfig(config),
186
+ endpoint: requireNonEmptyString(rawValue, key),
187
+ },
188
+ }
189
+ case "llm.model":
190
+ return {
191
+ ...config,
192
+ llm: {
193
+ ...requireLlmConfig(config),
194
+ model: requireNonEmptyString(rawValue, key),
195
+ },
196
+ }
197
+ case "llm.temperature":
198
+ return {
199
+ ...config,
200
+ llm: {
201
+ ...requireLlmConfig(config),
202
+ temperature: parseNumber(rawValue, key),
203
+ },
204
+ }
205
+ case "llm.maxTokens":
206
+ return {
207
+ ...config,
208
+ llm: {
209
+ ...requireLlmConfig(config),
210
+ maxTokens: parsePositiveInteger(rawValue, key),
211
+ },
212
+ }
213
+ case "llm.apiKey":
214
+ return {
215
+ ...config,
216
+ llm: {
217
+ ...requireLlmConfig(config),
218
+ apiKey: requireNonEmptyString(rawValue, key),
219
+ },
220
+ }
221
+ default:
222
+ throw new Error(`Unknown config key: ${key}`)
223
+ }
224
+ }
225
+
226
+ export function unsetConfigValue(config: AgentikitConfig, key: string): AgentikitConfig {
227
+ switch (key) {
228
+ case "embedding":
229
+ return { ...config, embedding: undefined }
230
+ case "embedding.apiKey":
231
+ if (!config.embedding) return config
232
+ return { ...config, embedding: omitKey(config.embedding, "apiKey") }
233
+ case "embedding.dimension":
234
+ if (!config.embedding) return config
235
+ return { ...config, embedding: omitKey(config.embedding, "dimension") }
236
+ case "embedding.provider":
237
+ if (!config.embedding) return config
238
+ return { ...config, embedding: omitKey(config.embedding, "provider") }
239
+ case "llm":
240
+ return { ...config, llm: undefined }
241
+ case "llm.apiKey":
242
+ if (!config.llm) return config
243
+ return { ...config, llm: omitKey(config.llm, "apiKey") }
244
+ case "llm.temperature":
245
+ if (!config.llm) return config
246
+ return { ...config, llm: omitKey(config.llm, "temperature") }
247
+ case "llm.maxTokens":
248
+ if (!config.llm) return config
249
+ return { ...config, llm: omitKey(config.llm, "maxTokens") }
250
+ case "llm.provider":
251
+ if (!config.llm) return config
252
+ return { ...config, llm: omitKey(config.llm, "provider") }
253
+ default:
254
+ throw new Error(`Unknown or unsupported unset key: ${key}`)
255
+ }
256
+ }
257
+
258
+ export function listConfig(config: AgentikitConfig): Record<string, unknown> {
259
+ return {
260
+ ...DEFAULT_CONFIG,
261
+ ...maskSecrets(config),
262
+ embedding: maskSecrets(getEmbeddingDisplayConfig(config)),
263
+ llm: maskSecrets(getLlmDisplayConfig(config)),
264
+ }
265
+ }
266
+
267
+ export function listProviders(scope: ConfigProviderScope, config: AgentikitConfig): Array<Record<string, unknown>> {
268
+ const currentProvider = scope === "embedding" ? getEmbeddingProvider(config) : getLlmProvider(config)
269
+ const presets = scope === "embedding" ? EMBEDDING_PROVIDER_PRESETS : LLM_PROVIDER_PRESETS
270
+ return Object.values(presets).map((preset) => ({
271
+ name: preset.name,
272
+ description: preset.description,
273
+ current: preset.name === currentProvider,
274
+ ...(preset.config ? maskSecrets(preset.config) : {}),
275
+ }))
276
+ }
277
+
278
+ export function useProvider(config: AgentikitConfig, scope: ConfigProviderScope, providerName: string): AgentikitConfig {
279
+ if (scope === "embedding") {
280
+ const preset = EMBEDDING_PROVIDER_PRESETS[providerName]
281
+ if (!preset) {
282
+ throw new Error(`Unknown embedding provider: ${providerName}`)
283
+ }
284
+ if (!preset.config) {
285
+ return { ...config, embedding: undefined }
286
+ }
287
+ return { ...config, embedding: { ...preset.config } }
288
+ }
289
+
290
+ const preset = LLM_PROVIDER_PRESETS[providerName]
291
+ if (!preset) {
292
+ throw new Error(`Unknown llm provider: ${providerName}`)
293
+ }
294
+ if (!preset.config) {
295
+ return { ...config, llm: undefined }
296
+ }
297
+ return { ...config, llm: { ...preset.config } }
298
+ }
299
+
300
+ function getEmbeddingProvider(config: AgentikitConfig): string {
301
+ if (!config.embedding) return "local"
302
+ if (config.embedding.provider) return config.embedding.provider
303
+ if (matchesPreset(config.embedding, EMBEDDING_PROVIDER_PRESETS.ollama.config)) return "ollama"
304
+ if (matchesPreset(config.embedding, EMBEDDING_PROVIDER_PRESETS.openai.config)) return "openai"
305
+ return "custom"
306
+ }
307
+
308
+ function getLlmProvider(config: AgentikitConfig): string {
309
+ if (!config.llm) return "disabled"
310
+ if (config.llm.provider) return config.llm.provider
311
+ if (matchesPreset(config.llm, LLM_PROVIDER_PRESETS.ollama.config)) return "ollama"
312
+ if (matchesPreset(config.llm, LLM_PROVIDER_PRESETS.openai.config)) return "openai"
313
+ return "custom"
314
+ }
315
+
316
+ function getEmbeddingDisplayConfig(config: AgentikitConfig): Record<string, unknown> {
317
+ if (!config.embedding) {
318
+ return {
319
+ provider: "local",
320
+ model: LOCAL_EMBEDDING_MODEL,
321
+ dimension: EMBEDDING_DIM,
322
+ }
323
+ }
324
+ return {
325
+ provider: getEmbeddingProvider(config),
326
+ endpoint: config.embedding.endpoint,
327
+ model: config.embedding.model,
328
+ dimension: config.embedding.dimension,
329
+ apiKey: config.embedding.apiKey,
330
+ }
331
+ }
332
+
333
+ function getLlmDisplayConfig(config: AgentikitConfig): Record<string, unknown> {
334
+ if (!config.llm) {
335
+ return {
336
+ provider: "disabled",
337
+ }
338
+ }
339
+ return {
340
+ provider: getLlmProvider(config),
341
+ endpoint: config.llm.endpoint,
342
+ model: config.llm.model,
343
+ temperature: config.llm.temperature ?? DEFAULT_LLM_TEMPERATURE,
344
+ maxTokens: config.llm.maxTokens ?? DEFAULT_LLM_MAX_TOKENS,
345
+ apiKey: config.llm.apiKey,
346
+ }
347
+ }
348
+
349
+ function parseEmbeddingConnectionValue(value: string): EmbeddingConnectionConfig | undefined {
350
+ if (value === "null" || value === "") return undefined
351
+ const parsed = parseJsonObject(value, "embedding", {
352
+ endpoint: "http://localhost:11434/v1/embeddings",
353
+ model: "nomic-embed-text",
354
+ })
355
+ const result: EmbeddingConnectionConfig = {
356
+ endpoint: asRequiredString(parsed.endpoint, "embedding", "endpoint"),
357
+ model: asRequiredString(parsed.model, "embedding", "model"),
358
+ }
359
+ if (typeof parsed.provider === "string" && parsed.provider) result.provider = parsed.provider
360
+ if (parsed.dimension !== undefined) result.dimension = parseUnknownPositiveInteger(parsed.dimension, "embedding.dimension")
361
+ if (typeof parsed.apiKey === "string" && parsed.apiKey) result.apiKey = parsed.apiKey
362
+ return result
363
+ }
364
+
365
+ function parseLlmConnectionValue(value: string): LlmConnectionConfig | undefined {
366
+ if (value === "null" || value === "") return undefined
367
+ const parsed = parseJsonObject(value, "llm", {
368
+ endpoint: "http://localhost:11434/v1/chat/completions",
369
+ model: "llama3.2",
370
+ })
371
+ const result: LlmConnectionConfig = {
372
+ endpoint: asRequiredString(parsed.endpoint, "llm", "endpoint"),
373
+ model: asRequiredString(parsed.model, "llm", "model"),
374
+ }
375
+ if (typeof parsed.provider === "string" && parsed.provider) result.provider = parsed.provider
376
+ if (parsed.temperature !== undefined) result.temperature = parseUnknownNumber(parsed.temperature, "llm.temperature")
377
+ if (parsed.maxTokens !== undefined) result.maxTokens = parseUnknownPositiveInteger(parsed.maxTokens, "llm.maxTokens")
378
+ if (typeof parsed.apiKey === "string" && parsed.apiKey) result.apiKey = parsed.apiKey
379
+ return result
380
+ }
381
+
382
+ function parseJsonObject(
383
+ value: string,
384
+ key: string,
385
+ example: { endpoint: string; model: string },
386
+ ): Record<string, unknown> {
387
+ let parsed: unknown
388
+ try {
389
+ parsed = JSON.parse(value)
390
+ } catch {
391
+ throw new Error(
392
+ `Invalid value for ${key}: expected JSON object with endpoint and model`
393
+ + ` (e.g. '{"endpoint":"${example.endpoint}","model":"${example.model}"}')`,
394
+ )
395
+ }
396
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
397
+ throw new Error(`Invalid value for ${key}: expected a JSON object`)
398
+ }
399
+ return parsed as Record<string, unknown>
400
+ }
401
+
402
+ function asRequiredString(value: unknown, key: string, field: string): string {
403
+ if (typeof value !== "string" || !value) {
404
+ throw new Error(`Invalid value for ${key}: "${field}" is a required string field`)
405
+ }
406
+ return value
407
+ }
408
+
409
+ function requireEmbeddingConfig(config: AgentikitConfig): EmbeddingConnectionConfig {
410
+ if (!config.embedding) {
411
+ throw new Error("Embedding provider is using the built-in local default. Run `akm config use embedding <provider>` first.")
412
+ }
413
+ return config.embedding
414
+ }
415
+
416
+ function requireLlmConfig(config: AgentikitConfig): LlmConnectionConfig {
417
+ if (!config.llm) {
418
+ throw new Error("LLM provider is disabled. Run `akm config use llm <provider>` first.")
419
+ }
420
+ return config.llm
421
+ }
422
+
423
+ function requireNonEmptyString(value: string, key: string): string {
424
+ if (!value) {
425
+ throw new Error(`Invalid value for ${key}: expected a non-empty string`)
426
+ }
427
+ return value
428
+ }
429
+
430
+ function parseNumber(value: string, key: string): number {
431
+ const parsed = Number(value)
432
+ if (!Number.isFinite(parsed)) {
433
+ throw new Error(`Invalid value for ${key}: expected a number`)
434
+ }
435
+ return parsed
436
+ }
437
+
438
+ function parsePositiveInteger(value: string, key: string): number {
439
+ const trimmed = value.trim()
440
+ if (!/^[1-9]\d*$/.test(trimmed)) {
441
+ throw new Error(`Invalid value for ${key}: expected a positive integer`)
442
+ }
443
+ const parsed = Number(trimmed)
444
+ if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed <= 0) {
445
+ throw new Error(`Invalid value for ${key}: expected a positive integer`)
446
+ }
447
+ return parsed
448
+ }
449
+
450
+ function parseUnknownNumber(value: unknown, key: string): number {
451
+ if (typeof value !== "number" || !Number.isFinite(value)) {
452
+ throw new Error(`Invalid value for ${key}: expected a number`)
453
+ }
454
+ return value
455
+ }
456
+
457
+ function parseUnknownPositiveInteger(value: unknown, key: string): number {
458
+ if (
459
+ typeof value !== "number" ||
460
+ !Number.isFinite(value) ||
461
+ !Number.isInteger(value) ||
462
+ value <= 0
463
+ ) {
464
+ throw new Error(`Invalid value for ${key}: expected a positive integer`)
465
+ }
466
+ return value
467
+ }
468
+
469
+ function matchesPreset<TConfig extends { endpoint: string; model: string }>(
470
+ current: TConfig,
471
+ preset?: TConfig,
472
+ ): boolean {
473
+ if (!preset) return false
474
+ return current.endpoint === preset.endpoint && current.model === preset.model
475
+ }
476
+
477
+ function omitKey<T extends object, K extends keyof T>(value: T, key: K): Omit<T, K> {
478
+ const copy = { ...value }
479
+ delete copy[key]
480
+ return copy
481
+ }
482
+
483
+ function maskSecret(value: unknown): unknown {
484
+ return typeof value === "string" && value ? "***" : value
485
+ }
486
+
487
+ function maskSecrets<T>(value: T): T {
488
+ if (Array.isArray(value)) {
489
+ return value.map((item) => maskSecrets(item)) as T
490
+ }
491
+ if (value && typeof value === "object") {
492
+ const result: Record<string, unknown> = {}
493
+ for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
494
+ result[key] = key === "apiKey" ? maskSecret(entry) : maskSecrets(entry)
495
+ }
496
+ return result as T
497
+ }
498
+ return value
499
+ }