codeblog-app 2.3.1 → 2.3.2
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/package.json +8 -73
- package/drizzle/0000_init.sql +0 -34
- package/drizzle/meta/_journal.json +0 -13
- package/drizzle.config.ts +0 -10
- package/src/ai/__tests__/chat.test.ts +0 -188
- package/src/ai/__tests__/compat.test.ts +0 -46
- package/src/ai/__tests__/home.ai-stream.integration.test.ts +0 -77
- package/src/ai/__tests__/provider-registry.test.ts +0 -61
- package/src/ai/__tests__/provider.test.ts +0 -238
- package/src/ai/__tests__/stream-events.test.ts +0 -152
- package/src/ai/__tests__/tools.test.ts +0 -93
- package/src/ai/chat.ts +0 -336
- package/src/ai/configure.ts +0 -143
- package/src/ai/models.ts +0 -26
- package/src/ai/provider-registry.ts +0 -150
- package/src/ai/provider.ts +0 -264
- package/src/ai/stream-events.ts +0 -64
- package/src/ai/tools.ts +0 -118
- package/src/ai/types.ts +0 -105
- package/src/auth/index.ts +0 -49
- package/src/auth/oauth.ts +0 -123
- package/src/cli/__tests__/commands.test.ts +0 -229
- package/src/cli/cmd/agent.ts +0 -97
- package/src/cli/cmd/ai.ts +0 -10
- package/src/cli/cmd/chat.ts +0 -190
- package/src/cli/cmd/comment.ts +0 -67
- package/src/cli/cmd/config.ts +0 -153
- package/src/cli/cmd/feed.ts +0 -53
- package/src/cli/cmd/forum.ts +0 -106
- package/src/cli/cmd/login.ts +0 -45
- package/src/cli/cmd/logout.ts +0 -12
- package/src/cli/cmd/me.ts +0 -188
- package/src/cli/cmd/post.ts +0 -25
- package/src/cli/cmd/publish.ts +0 -64
- package/src/cli/cmd/scan.ts +0 -78
- package/src/cli/cmd/search.ts +0 -35
- package/src/cli/cmd/setup.ts +0 -622
- package/src/cli/cmd/tui.ts +0 -20
- package/src/cli/cmd/uninstall.ts +0 -281
- package/src/cli/cmd/update.ts +0 -123
- package/src/cli/cmd/vote.ts +0 -50
- package/src/cli/cmd/whoami.ts +0 -18
- package/src/cli/mcp-print.ts +0 -6
- package/src/cli/ui.ts +0 -357
- package/src/config/index.ts +0 -92
- package/src/flag/index.ts +0 -23
- package/src/global/index.ts +0 -38
- package/src/id/index.ts +0 -20
- package/src/index.ts +0 -203
- package/src/mcp/__tests__/client.test.ts +0 -149
- package/src/mcp/__tests__/e2e.ts +0 -331
- package/src/mcp/__tests__/integration.ts +0 -148
- package/src/mcp/client.ts +0 -118
- package/src/server/index.ts +0 -48
- package/src/storage/chat.ts +0 -73
- package/src/storage/db.ts +0 -85
- package/src/storage/schema.sql.ts +0 -39
- package/src/storage/schema.ts +0 -1
- package/src/tui/__tests__/input-intent.test.ts +0 -27
- package/src/tui/__tests__/stream-assembler.test.ts +0 -33
- package/src/tui/ai-stream.ts +0 -28
- package/src/tui/app.tsx +0 -210
- package/src/tui/commands.ts +0 -220
- package/src/tui/context/exit.tsx +0 -15
- package/src/tui/context/helper.tsx +0 -25
- package/src/tui/context/route.tsx +0 -24
- package/src/tui/context/theme.tsx +0 -471
- package/src/tui/input-intent.ts +0 -26
- package/src/tui/routes/home.tsx +0 -1060
- package/src/tui/routes/model.tsx +0 -210
- package/src/tui/routes/notifications.tsx +0 -87
- package/src/tui/routes/post.tsx +0 -102
- package/src/tui/routes/search.tsx +0 -105
- package/src/tui/routes/setup.tsx +0 -267
- package/src/tui/routes/trending.tsx +0 -107
- package/src/tui/stream-assembler.ts +0 -49
- package/src/util/__tests__/context.test.ts +0 -31
- package/src/util/__tests__/lazy.test.ts +0 -37
- package/src/util/context.ts +0 -23
- package/src/util/error.ts +0 -46
- package/src/util/lazy.ts +0 -18
- package/src/util/log.ts +0 -144
- package/tsconfig.json +0 -11
package/src/ai/provider.ts
DELETED
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
import { createAnthropic } from "@ai-sdk/anthropic"
|
|
2
|
-
import { createOpenAI } from "@ai-sdk/openai"
|
|
3
|
-
import { createGoogleGenerativeAI } from "@ai-sdk/google"
|
|
4
|
-
import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
|
|
5
|
-
import { type LanguageModel, type Provider as SDK } from "ai"
|
|
6
|
-
import { Config } from "../config"
|
|
7
|
-
import { Log } from "../util/log"
|
|
8
|
-
import { BUILTIN_MODELS as CORE_MODELS, DEFAULT_MODEL as CORE_DEFAULT_MODEL, type ModelInfo as CoreModelInfo } from "./models"
|
|
9
|
-
import { loadProviders, PROVIDER_BASE_URL_ENV, PROVIDER_ENV, routeModel } from "./provider-registry"
|
|
10
|
-
import { patchRequestByCompat, resolveCompat, type ModelApi, type ModelCompatConfig } from "./types"
|
|
11
|
-
|
|
12
|
-
const log = Log.create({ service: "ai-provider" })
|
|
13
|
-
|
|
14
|
-
export namespace AIProvider {
|
|
15
|
-
const BUNDLED_PROVIDERS: Record<string, (options: any) => SDK> = {
|
|
16
|
-
"@ai-sdk/anthropic": createAnthropic as any,
|
|
17
|
-
"@ai-sdk/openai": createOpenAI as any,
|
|
18
|
-
"@ai-sdk/google": createGoogleGenerativeAI as any,
|
|
19
|
-
"@ai-sdk/openai-compatible": createOpenAICompatible as any,
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const PROVIDER_NPM: Record<ModelApi, string> = {
|
|
23
|
-
anthropic: "@ai-sdk/anthropic",
|
|
24
|
-
openai: "@ai-sdk/openai",
|
|
25
|
-
google: "@ai-sdk/google",
|
|
26
|
-
"openai-compatible": "@ai-sdk/openai-compatible",
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export const BUILTIN_MODELS = CORE_MODELS
|
|
30
|
-
export const DEFAULT_MODEL = CORE_DEFAULT_MODEL
|
|
31
|
-
export type ModelInfo = CoreModelInfo
|
|
32
|
-
|
|
33
|
-
export async function getApiKey(providerID: string): Promise<string | undefined> {
|
|
34
|
-
const envKeys = PROVIDER_ENV[providerID] || []
|
|
35
|
-
for (const key of envKeys) {
|
|
36
|
-
if (process.env[key]) return process.env[key]
|
|
37
|
-
}
|
|
38
|
-
const cfg = await Config.load()
|
|
39
|
-
return cfg.providers?.[providerID]?.api_key
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export async function getBaseUrl(providerID: string): Promise<string | undefined> {
|
|
43
|
-
const envKeys = PROVIDER_BASE_URL_ENV[providerID] || []
|
|
44
|
-
for (const key of envKeys) {
|
|
45
|
-
if (process.env[key]) return process.env[key]
|
|
46
|
-
}
|
|
47
|
-
const cfg = await Config.load()
|
|
48
|
-
return cfg.providers?.[providerID]?.base_url
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export async function listProviders(): Promise<Record<string, { name: string; models: string[]; hasKey: boolean }>> {
|
|
52
|
-
const result: Record<string, { name: string; models: string[]; hasKey: boolean }> = {}
|
|
53
|
-
for (const model of Object.values(BUILTIN_MODELS)) {
|
|
54
|
-
if (!result[model.providerID]) {
|
|
55
|
-
const key = await getApiKey(model.providerID)
|
|
56
|
-
result[model.providerID] = { name: model.providerID, models: [], hasKey: !!key }
|
|
57
|
-
}
|
|
58
|
-
if (!result[model.providerID]!.models.includes(model.id)) {
|
|
59
|
-
result[model.providerID]!.models.push(model.id)
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const compatKey = await getApiKey("openai-compatible")
|
|
64
|
-
const compatBase = await getBaseUrl("openai-compatible")
|
|
65
|
-
if (compatKey && compatBase) {
|
|
66
|
-
const remoteModels = await fetchRemoteModels(compatBase, compatKey)
|
|
67
|
-
result["openai-compatible"] = { name: "OpenAI Compatible", models: remoteModels, hasKey: true }
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const loaded = await loadProviders()
|
|
71
|
-
for (const provider of Object.values(loaded.providers)) {
|
|
72
|
-
if (result[provider.id]) continue
|
|
73
|
-
if (!provider.apiKey) continue
|
|
74
|
-
result[provider.id] = { name: provider.id, models: [], hasKey: true }
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return result
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const sdkCache = new Map<string, SDK>()
|
|
81
|
-
|
|
82
|
-
export async function getModel(modelID?: string): Promise<LanguageModel> {
|
|
83
|
-
const useRegistry = await Config.featureEnabled("ai_provider_registry_v2")
|
|
84
|
-
if (useRegistry) {
|
|
85
|
-
const route = await routeModel(modelID)
|
|
86
|
-
return getLanguageModel(route.providerID, route.modelID, route.apiKey, undefined, route.baseURL, route.compat)
|
|
87
|
-
}
|
|
88
|
-
return getModelLegacy(modelID)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export async function resolveModelCompat(modelID?: string): Promise<ModelCompatConfig> {
|
|
92
|
-
const useRegistry = await Config.featureEnabled("ai_provider_registry_v2")
|
|
93
|
-
if (useRegistry) return (await routeModel(modelID)).compat
|
|
94
|
-
return (await resolveLegacyRoute(modelID)).compat
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async function getModelLegacy(modelID?: string): Promise<LanguageModel> {
|
|
98
|
-
const route = await resolveLegacyRoute(modelID)
|
|
99
|
-
return getLanguageModel(route.providerID, route.modelID, route.apiKey, undefined, route.baseURL, route.compat)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async function resolveLegacyRoute(modelID?: string): Promise<{
|
|
103
|
-
providerID: string
|
|
104
|
-
modelID: string
|
|
105
|
-
apiKey: string
|
|
106
|
-
baseURL?: string
|
|
107
|
-
compat: ModelCompatConfig
|
|
108
|
-
}> {
|
|
109
|
-
const requested = modelID || (await getConfiguredModel()) || DEFAULT_MODEL
|
|
110
|
-
const cfg = await Config.load()
|
|
111
|
-
|
|
112
|
-
const builtin = BUILTIN_MODELS[requested]
|
|
113
|
-
if (builtin) {
|
|
114
|
-
const apiKey = await getApiKey(builtin.providerID)
|
|
115
|
-
if (!apiKey) throw noKeyError(builtin.providerID)
|
|
116
|
-
const baseURL = await getBaseUrl(builtin.providerID)
|
|
117
|
-
return {
|
|
118
|
-
providerID: builtin.providerID,
|
|
119
|
-
modelID: requested,
|
|
120
|
-
apiKey,
|
|
121
|
-
baseURL,
|
|
122
|
-
compat: resolveCompat({ providerID: builtin.providerID, modelID: requested, providerConfig: cfg.providers?.[builtin.providerID] }),
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (requested.includes("/")) {
|
|
127
|
-
const [providerID, ...rest] = requested.split("/")
|
|
128
|
-
const modelID = rest.join("/")
|
|
129
|
-
const apiKey = await getApiKey(providerID!)
|
|
130
|
-
if (!apiKey) throw noKeyError(providerID!)
|
|
131
|
-
const baseURL = await getBaseUrl(providerID!)
|
|
132
|
-
return {
|
|
133
|
-
providerID: providerID!,
|
|
134
|
-
modelID,
|
|
135
|
-
apiKey,
|
|
136
|
-
baseURL,
|
|
137
|
-
compat: resolveCompat({ providerID: providerID!, modelID, providerConfig: cfg.providers?.[providerID!] }),
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (cfg.providers) {
|
|
142
|
-
for (const [providerID, p] of Object.entries(cfg.providers)) {
|
|
143
|
-
if (!p.api_key) continue
|
|
144
|
-
const baseURL = p.base_url || (await getBaseUrl(providerID))
|
|
145
|
-
if (!baseURL) continue
|
|
146
|
-
log.info("legacy fallback: unknown model routed to first provider with base_url", { provider: providerID, model: requested })
|
|
147
|
-
return {
|
|
148
|
-
providerID,
|
|
149
|
-
modelID: requested,
|
|
150
|
-
apiKey: p.api_key,
|
|
151
|
-
baseURL,
|
|
152
|
-
compat: resolveCompat({ providerID, modelID: requested, providerConfig: p }),
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
throw new Error(`Unknown model: ${requested}. Run: codeblog config --list`)
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function packageForCompat(compat: ModelCompatConfig): string {
|
|
161
|
-
let pkg = PROVIDER_NPM[compat.api]
|
|
162
|
-
if (compat.modelID.startsWith("claude-") && pkg === "@ai-sdk/openai-compatible") {
|
|
163
|
-
pkg = "@ai-sdk/anthropic"
|
|
164
|
-
log.info("auto-detected claude model for openai-compatible route, using anthropic sdk", { model: compat.modelID })
|
|
165
|
-
}
|
|
166
|
-
return pkg
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function getLanguageModel(
|
|
170
|
-
providerID: string,
|
|
171
|
-
modelID: string,
|
|
172
|
-
apiKey: string,
|
|
173
|
-
npm?: string,
|
|
174
|
-
baseURL?: string,
|
|
175
|
-
providedCompat?: ModelCompatConfig,
|
|
176
|
-
): LanguageModel {
|
|
177
|
-
const compat = providedCompat || resolveCompat({ providerID, modelID })
|
|
178
|
-
const pkg = npm || packageForCompat(compat)
|
|
179
|
-
const cacheKey = `${providerID}:${pkg}:${compat.cacheKey}:${apiKey.slice(0, 8)}:${baseURL || ""}`
|
|
180
|
-
|
|
181
|
-
let sdk = sdkCache.get(cacheKey)
|
|
182
|
-
if (!sdk) {
|
|
183
|
-
const createFn = BUNDLED_PROVIDERS[pkg]
|
|
184
|
-
if (!createFn) throw new Error(`No bundled provider for ${pkg}.`)
|
|
185
|
-
const opts: Record<string, unknown> = { apiKey, name: providerID }
|
|
186
|
-
if (baseURL) {
|
|
187
|
-
const clean = baseURL.replace(/\/+$/, "")
|
|
188
|
-
opts.baseURL = clean.endsWith("/v1") ? clean : `${clean}/v1`
|
|
189
|
-
}
|
|
190
|
-
if (pkg === "@ai-sdk/openai-compatible") {
|
|
191
|
-
opts.transformRequestBody = (body: Record<string, any>) => patchRequestByCompat(compat, body)
|
|
192
|
-
}
|
|
193
|
-
sdk = createFn(opts)
|
|
194
|
-
sdkCache.set(cacheKey, sdk)
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (pkg === "@ai-sdk/openai-compatible" && typeof (sdk as any).chatModel === "function") {
|
|
198
|
-
return (sdk as any).chatModel(modelID)
|
|
199
|
-
}
|
|
200
|
-
if (typeof (sdk as any).languageModel === "function") {
|
|
201
|
-
return (sdk as any).languageModel(modelID)
|
|
202
|
-
}
|
|
203
|
-
return (sdk as any)(modelID)
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
async function fetchRemoteModels(base: string, key: string): Promise<string[]> {
|
|
207
|
-
try {
|
|
208
|
-
const clean = base.replace(/\/+$/, "")
|
|
209
|
-
const url = clean.endsWith("/v1") ? `${clean}/models` : `${clean}/v1/models`
|
|
210
|
-
const r = await fetch(url, {
|
|
211
|
-
headers: { Authorization: `Bearer ${key}` },
|
|
212
|
-
signal: AbortSignal.timeout(8000),
|
|
213
|
-
})
|
|
214
|
-
if (!r.ok) return []
|
|
215
|
-
const data = await r.json() as { data?: Array<{ id: string }> }
|
|
216
|
-
return data.data?.map((m) => m.id) ?? []
|
|
217
|
-
} catch {
|
|
218
|
-
return []
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function noKeyError(providerID: string): Error {
|
|
223
|
-
const envKeys = PROVIDER_ENV[providerID] || []
|
|
224
|
-
const envHint = envKeys[0] || `${providerID.toUpperCase().replace(/-/g, "_")}_API_KEY`
|
|
225
|
-
return new Error(`No API key for ${providerID}. Set ${envHint} or run: codeblog config --provider ${providerID} --api-key <key>`)
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
async function getConfiguredModel(): Promise<string | undefined> {
|
|
229
|
-
const cfg = await Config.load()
|
|
230
|
-
return cfg.model
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
export async function hasAnyKey(): Promise<boolean> {
|
|
234
|
-
const loaded = await loadProviders()
|
|
235
|
-
return Object.values(loaded.providers).some((p) => !!p.apiKey)
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
export async function available(): Promise<Array<{ model: ModelInfo; hasKey: boolean }>> {
|
|
239
|
-
const result: Array<{ model: ModelInfo; hasKey: boolean }> = []
|
|
240
|
-
for (const model of Object.values(BUILTIN_MODELS)) {
|
|
241
|
-
const apiKey = await getApiKey(model.providerID)
|
|
242
|
-
result.push({ model, hasKey: !!apiKey })
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const compatKey = await getApiKey("openai-compatible")
|
|
246
|
-
const compatBase = await getBaseUrl("openai-compatible")
|
|
247
|
-
if (compatKey && compatBase) {
|
|
248
|
-
const remoteModels = await fetchRemoteModels(compatBase, compatKey)
|
|
249
|
-
for (const id of remoteModels) {
|
|
250
|
-
if (BUILTIN_MODELS[id]) continue
|
|
251
|
-
result.push({
|
|
252
|
-
model: { id, providerID: "openai-compatible", name: id, contextWindow: 0, outputTokens: 0 },
|
|
253
|
-
hasKey: true,
|
|
254
|
-
})
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
return result
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
export function parseModel(model: string) {
|
|
261
|
-
const [providerID, ...rest] = model.split("/")
|
|
262
|
-
return { providerID, modelID: rest.join("/") }
|
|
263
|
-
}
|
|
264
|
-
}
|
package/src/ai/stream-events.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
export type StreamEventType =
|
|
2
|
-
| "run-start"
|
|
3
|
-
| "text-delta"
|
|
4
|
-
| "tool-start"
|
|
5
|
-
| "tool-result"
|
|
6
|
-
| "error"
|
|
7
|
-
| "run-finish"
|
|
8
|
-
|
|
9
|
-
interface StreamEventBase {
|
|
10
|
-
type: StreamEventType
|
|
11
|
-
runId: string
|
|
12
|
-
seq: number
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface RunStartEvent extends StreamEventBase {
|
|
16
|
-
type: "run-start"
|
|
17
|
-
modelID: string
|
|
18
|
-
messageCount: number
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface TextDeltaEvent extends StreamEventBase {
|
|
22
|
-
type: "text-delta"
|
|
23
|
-
text: string
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface ToolStartEvent extends StreamEventBase {
|
|
27
|
-
type: "tool-start"
|
|
28
|
-
callID: string
|
|
29
|
-
name: string
|
|
30
|
-
args: unknown
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface ToolResultEvent extends StreamEventBase {
|
|
34
|
-
type: "tool-result"
|
|
35
|
-
callID: string
|
|
36
|
-
name: string
|
|
37
|
-
result: unknown
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface ErrorEvent extends StreamEventBase {
|
|
41
|
-
type: "error"
|
|
42
|
-
error: Error
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface RunFinishEvent extends StreamEventBase {
|
|
46
|
-
type: "run-finish"
|
|
47
|
-
text: string
|
|
48
|
-
aborted: boolean
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export type StreamEvent =
|
|
52
|
-
| RunStartEvent
|
|
53
|
-
| TextDeltaEvent
|
|
54
|
-
| ToolStartEvent
|
|
55
|
-
| ToolResultEvent
|
|
56
|
-
| ErrorEvent
|
|
57
|
-
| RunFinishEvent
|
|
58
|
-
|
|
59
|
-
export function createRunEventFactory(runId: string = crypto.randomUUID()) {
|
|
60
|
-
let seq = 0
|
|
61
|
-
const next = <T extends StreamEventType, P extends Record<string, unknown>>(type: T, payload: P) =>
|
|
62
|
-
({ type, runId, seq: ++seq, ...payload }) as unknown as Extract<StreamEvent, { type: T }>
|
|
63
|
-
return { runId, next }
|
|
64
|
-
}
|
package/src/ai/tools.ts
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import { tool, jsonSchema } from "ai"
|
|
2
|
-
import { McpBridge } from "../mcp/client"
|
|
3
|
-
import { Log } from "../util/log"
|
|
4
|
-
import type { ModelCompatConfig } from "./types"
|
|
5
|
-
|
|
6
|
-
const log = Log.create({ service: "ai-tools" })
|
|
7
|
-
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
// Tool display labels for the TUI streaming indicator.
|
|
10
|
-
// Kept as a static fallback — new tools added to MCP will show their name
|
|
11
|
-
// as-is if not listed here, which is acceptable.
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
export const TOOL_LABELS: Record<string, string> = {
|
|
14
|
-
scan_sessions: "Scanning IDE sessions...",
|
|
15
|
-
read_session: "Reading session...",
|
|
16
|
-
analyze_session: "Analyzing session...",
|
|
17
|
-
post_to_codeblog: "Publishing post...",
|
|
18
|
-
auto_post: "Auto-posting...",
|
|
19
|
-
weekly_digest: "Generating weekly digest...",
|
|
20
|
-
browse_posts: "Browsing posts...",
|
|
21
|
-
search_posts: "Searching posts...",
|
|
22
|
-
read_post: "Reading post...",
|
|
23
|
-
comment_on_post: "Posting comment...",
|
|
24
|
-
vote_on_post: "Voting...",
|
|
25
|
-
edit_post: "Editing post...",
|
|
26
|
-
delete_post: "Deleting post...",
|
|
27
|
-
bookmark_post: "Bookmarking...",
|
|
28
|
-
browse_by_tag: "Browsing tags...",
|
|
29
|
-
trending_topics: "Loading trending...",
|
|
30
|
-
explore_and_engage: "Exploring posts...",
|
|
31
|
-
join_debate: "Loading debates...",
|
|
32
|
-
my_notifications: "Checking notifications...",
|
|
33
|
-
manage_agents: "Managing agents...",
|
|
34
|
-
my_posts: "Loading your posts...",
|
|
35
|
-
my_dashboard: "Loading dashboard...",
|
|
36
|
-
follow_user: "Processing follow...",
|
|
37
|
-
codeblog_setup: "Configuring CodeBlog...",
|
|
38
|
-
codeblog_status: "Checking status...",
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
42
|
-
// Helper: call MCP tool and return result
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
async function mcp(name: string, args: Record<string, unknown> = {}): Promise<any> {
|
|
45
|
-
return McpBridge.callToolJSON(name, args)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Strip undefined/null values from args before sending to MCP
|
|
49
|
-
function clean(obj: Record<string, unknown>): Record<string, unknown> {
|
|
50
|
-
const result: Record<string, unknown> = {}
|
|
51
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
52
|
-
if (v !== undefined && v !== null) result[k] = v
|
|
53
|
-
}
|
|
54
|
-
return result
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// ---------------------------------------------------------------------------
|
|
58
|
-
// Schema normalization: ensure all JSON schemas are valid tool input schemas.
|
|
59
|
-
// Some MCP tools have empty inputSchema ({}) which produces schemas without
|
|
60
|
-
// "type": "object", causing providers like DeepSeek/Qwen to reject them.
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
function normalizeToolSchema(schema: Record<string, unknown>): Record<string, unknown> {
|
|
63
|
-
const normalized = { ...schema }
|
|
64
|
-
if (!normalized.type) normalized.type = "object"
|
|
65
|
-
if (normalized.type === "object" && !normalized.properties) normalized.properties = {}
|
|
66
|
-
return normalized
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
// Dynamic tool discovery from MCP server
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
const cache = new Map<string, Record<string, any>>()
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Build AI SDK tools dynamically from the MCP server's listTools() response.
|
|
76
|
-
* Results are cached after the first successful call.
|
|
77
|
-
*/
|
|
78
|
-
export async function getChatTools(compat?: ModelCompatConfig | string): Promise<Record<string, any>> {
|
|
79
|
-
const key = typeof compat === "string" ? compat : compat?.cacheKey || "default"
|
|
80
|
-
const normalizeSchema = typeof compat === "string" ? true : (compat?.normalizeToolSchema ?? true)
|
|
81
|
-
const cached = cache.get(key)
|
|
82
|
-
if (cached) return cached
|
|
83
|
-
|
|
84
|
-
const { tools: mcpTools } = await McpBridge.listTools()
|
|
85
|
-
log.info("discovered MCP tools", { count: mcpTools.length, names: mcpTools.map((t) => t.name) })
|
|
86
|
-
|
|
87
|
-
const tools: Record<string, any> = {}
|
|
88
|
-
|
|
89
|
-
for (const t of mcpTools) {
|
|
90
|
-
const name = t.name
|
|
91
|
-
const rawSchema = (t.inputSchema ?? {}) as Record<string, unknown>
|
|
92
|
-
|
|
93
|
-
tools[name] = tool({
|
|
94
|
-
description: t.description || name,
|
|
95
|
-
inputSchema: jsonSchema(normalizeSchema ? normalizeToolSchema(rawSchema) : rawSchema),
|
|
96
|
-
execute: async (args: any) => {
|
|
97
|
-
log.info("execute tool", { name, args })
|
|
98
|
-
const result = await mcp(name, clean(args))
|
|
99
|
-
const resultStr = typeof result === "string" ? result : JSON.stringify(result)
|
|
100
|
-
log.info("execute tool result", { name, resultType: typeof result, resultLength: resultStr.length, resultPreview: resultStr.slice(0, 300) })
|
|
101
|
-
// Truncate very large tool results to avoid overwhelming the LLM context
|
|
102
|
-
if (resultStr.length > 8000) {
|
|
103
|
-
log.info("truncating large tool result", { name, originalLength: resultStr.length })
|
|
104
|
-
return resultStr.slice(0, 8000) + "\n...(truncated)"
|
|
105
|
-
}
|
|
106
|
-
return resultStr
|
|
107
|
-
},
|
|
108
|
-
})
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
cache.set(key, tools)
|
|
112
|
-
return tools
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** Clear the cached tools (useful for testing or reconnection). */
|
|
116
|
-
export function clearChatToolsCache(): void {
|
|
117
|
-
cache.clear()
|
|
118
|
-
}
|
package/src/ai/types.ts
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import type { Config } from "../config"
|
|
2
|
-
|
|
3
|
-
export type ModelApi = "anthropic" | "openai" | "google" | "openai-compatible"
|
|
4
|
-
export type CompatProfile = "anthropic" | "openai" | "openai-compatible" | "google"
|
|
5
|
-
|
|
6
|
-
export interface ModelCompatConfig {
|
|
7
|
-
providerID: string
|
|
8
|
-
modelID: string
|
|
9
|
-
api: ModelApi
|
|
10
|
-
compatProfile: CompatProfile
|
|
11
|
-
cacheKey: string
|
|
12
|
-
stripParallelToolCalls: boolean
|
|
13
|
-
normalizeToolSchema: boolean
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const COMPAT_PRESETS: Record<CompatProfile, Omit<ModelCompatConfig, "providerID" | "modelID" | "cacheKey">> = {
|
|
17
|
-
anthropic: {
|
|
18
|
-
api: "anthropic",
|
|
19
|
-
compatProfile: "anthropic",
|
|
20
|
-
stripParallelToolCalls: false,
|
|
21
|
-
normalizeToolSchema: false,
|
|
22
|
-
},
|
|
23
|
-
openai: {
|
|
24
|
-
api: "openai",
|
|
25
|
-
compatProfile: "openai",
|
|
26
|
-
stripParallelToolCalls: false,
|
|
27
|
-
normalizeToolSchema: false,
|
|
28
|
-
},
|
|
29
|
-
google: {
|
|
30
|
-
api: "google",
|
|
31
|
-
compatProfile: "google",
|
|
32
|
-
stripParallelToolCalls: false,
|
|
33
|
-
normalizeToolSchema: false,
|
|
34
|
-
},
|
|
35
|
-
"openai-compatible": {
|
|
36
|
-
api: "openai-compatible",
|
|
37
|
-
compatProfile: "openai-compatible",
|
|
38
|
-
stripParallelToolCalls: true,
|
|
39
|
-
normalizeToolSchema: true,
|
|
40
|
-
},
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function isOfficialOpenAIBase(baseURL?: string): boolean {
|
|
44
|
-
if (!baseURL) return false
|
|
45
|
-
try {
|
|
46
|
-
return new URL(baseURL).hostname === "api.openai.com"
|
|
47
|
-
} catch {
|
|
48
|
-
return false
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function resolveApiFromProvider(providerID: string, cfg?: Config.ProviderConfig): ModelApi {
|
|
53
|
-
if (providerID === "openai" && cfg?.base_url && !isOfficialOpenAIBase(cfg.base_url)) {
|
|
54
|
-
return "openai-compatible"
|
|
55
|
-
}
|
|
56
|
-
if (cfg?.api) return cfg.api
|
|
57
|
-
if (providerID === "anthropic") return "anthropic"
|
|
58
|
-
if (providerID === "openai") return "openai"
|
|
59
|
-
if (providerID === "google") return "google"
|
|
60
|
-
if (providerID === "openai-compatible") return "openai-compatible"
|
|
61
|
-
return "openai-compatible"
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function defaultCompatForApi(api: ModelApi): CompatProfile {
|
|
65
|
-
if (api === "anthropic") return "anthropic"
|
|
66
|
-
if (api === "openai") return "openai"
|
|
67
|
-
if (api === "google") return "google"
|
|
68
|
-
return "openai-compatible"
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function resolveCompat(args: {
|
|
72
|
-
providerID: string
|
|
73
|
-
modelID: string
|
|
74
|
-
providerConfig?: Config.ProviderConfig
|
|
75
|
-
}): ModelCompatConfig {
|
|
76
|
-
const api = resolveApiFromProvider(args.providerID, args.providerConfig)
|
|
77
|
-
const configured = args.providerConfig?.compat_profile
|
|
78
|
-
const compatProfile = api === "openai-compatible" && configured === "openai"
|
|
79
|
-
? "openai-compatible"
|
|
80
|
-
: configured || defaultCompatForApi(api)
|
|
81
|
-
const preset = COMPAT_PRESETS[compatProfile]
|
|
82
|
-
return {
|
|
83
|
-
...preset,
|
|
84
|
-
providerID: args.providerID,
|
|
85
|
-
modelID: args.modelID,
|
|
86
|
-
cacheKey: `${api}:${compatProfile}`,
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function patchRequestByCompat(compat: ModelCompatConfig, body: Record<string, any>): Record<string, any> {
|
|
91
|
-
if (compat.stripParallelToolCalls) {
|
|
92
|
-
delete body.parallel_tool_calls
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (compat.normalizeToolSchema && Array.isArray(body.tools)) {
|
|
96
|
-
for (const t of body.tools) {
|
|
97
|
-
const params = t?.function?.parameters
|
|
98
|
-
if (!params || typeof params !== "object") continue
|
|
99
|
-
if (!params.type) params.type = "object"
|
|
100
|
-
if (params.type === "object" && !params.properties) params.properties = {}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return body
|
|
105
|
-
}
|
package/src/auth/index.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import path from "path"
|
|
2
|
-
import { chmod, writeFile } from "fs/promises"
|
|
3
|
-
import { Global } from "../global"
|
|
4
|
-
import z from "zod"
|
|
5
|
-
|
|
6
|
-
export namespace Auth {
|
|
7
|
-
export const Token = z
|
|
8
|
-
.object({
|
|
9
|
-
type: z.enum(["jwt", "apikey"]),
|
|
10
|
-
value: z.string(),
|
|
11
|
-
expires: z.number().optional(),
|
|
12
|
-
username: z.string().optional(),
|
|
13
|
-
})
|
|
14
|
-
.meta({ ref: "AuthToken" })
|
|
15
|
-
export type Token = z.infer<typeof Token>
|
|
16
|
-
|
|
17
|
-
const filepath = path.join(Global.Path.data, "auth.json")
|
|
18
|
-
|
|
19
|
-
export async function get(): Promise<Token | null> {
|
|
20
|
-
const file = Bun.file(filepath)
|
|
21
|
-
const data = await file.json().catch(() => null)
|
|
22
|
-
if (!data) return null
|
|
23
|
-
const parsed = Token.safeParse(data)
|
|
24
|
-
if (!parsed.success) return null
|
|
25
|
-
return parsed.data
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export async function set(token: Token) {
|
|
29
|
-
await writeFile(filepath, JSON.stringify(token, null, 2))
|
|
30
|
-
await chmod(filepath, 0o600).catch(() => {})
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export async function remove() {
|
|
34
|
-
const fs = await import("fs/promises")
|
|
35
|
-
await fs.unlink(filepath).catch(() => {})
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export async function header(): Promise<Record<string, string>> {
|
|
39
|
-
const token = await get()
|
|
40
|
-
if (!token) return {}
|
|
41
|
-
if (token.type === "apikey") return { Authorization: `Bearer ${token.value}` }
|
|
42
|
-
return { Authorization: `Bearer ${token.value}` }
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export async function authenticated(): Promise<boolean> {
|
|
46
|
-
const token = await get()
|
|
47
|
-
return token !== null
|
|
48
|
-
}
|
|
49
|
-
}
|