saeeol 1.0.7 → 1.0.8
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 +1 -1
- package/specs/effect/http-api.md +17 -17
- package/src/cli/cmd/providers.ts +75 -1
- package/src/cli/cmd/tui/app-events.ts +32 -0
- package/src/cli/cmd/tui/component/prompt/PromptStatusBar.tsx +15 -6
- package/src/cli/cmd/tui/component/prompt/use-prompt-memos.ts +9 -3
- package/src/index.ts +14 -109
- package/src/provider/bundled-providers.ts +3 -3
- package/src/provider/provider-events.ts +29 -0
- package/src/provider/provider-resolve.ts +23 -3
- package/src/provider/tiers/code.ts +0 -1
- package/src/provider/tiers/light.ts +2 -6
- package/src/saeeol/plugins/sidebar-usage.tsx +25 -0
- package/src/saeeol/provider/provider.ts +2 -12
- package/src/saeeol/session/compaction-chunks-utils.ts +53 -2
- package/src/server/routes/instance/event.ts +2 -0
- package/src/server/routes/instance/httpapi/api.ts +2 -0
- package/src/server/routes/ui.ts +0 -1
- package/src/session/compaction.ts +30 -6
- package/src/session/prompt/anthropic.txt +4 -4
- package/src/session/prompt/default.txt +4 -4
- package/src/session/prompt/ling.txt +5 -5
- package/src/tool/package.ts +168 -0
- package/src/tool/registry.ts +4 -0
- package/test/fixture/skills/agents-sdk/SKILL.md +30 -30
- package/test/fixture/skills/cloudflare/SKILL.md +66 -66
- package/test/saeeol/compaction-smart-select.test.ts +100 -0
- package/src/cli/cmd/tui/context/theme/opencode.json +0 -245
package/package.json
CHANGED
package/specs/effect/http-api.md
CHANGED
|
@@ -189,23 +189,23 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho
|
|
|
189
189
|
|
|
190
190
|
## Current Route Status
|
|
191
191
|
|
|
192
|
-
| Area
|
|
193
|
-
|
|
194
|
-
| `question`
|
|
195
|
-
| `permission`
|
|
196
|
-
| `provider`
|
|
197
|
-
| `config`
|
|
198
|
-
| `project`
|
|
199
|
-
| `file`
|
|
200
|
-
| `mcp`
|
|
201
|
-
| `workspace`
|
|
202
|
-
| top-level instance routes | `bridged`
|
|
203
|
-
| experimental JSON routes
|
|
204
|
-
| `session`
|
|
205
|
-
| `sync`
|
|
206
|
-
| `event`
|
|
207
|
-
| `pty`
|
|
208
|
-
| `tui`
|
|
192
|
+
| Area | Status | Notes |
|
|
193
|
+
|---|---|---|
|
|
194
|
+
| `question` | `bridged` | `GET /question`, reply, reject |
|
|
195
|
+
| `permission` | `bridged` | list and reply |
|
|
196
|
+
| `provider` | `bridged` | list, auth, OAuth authorize/callback |
|
|
197
|
+
| `config` | `bridged` | read, providers, update |
|
|
198
|
+
| `project` | `bridged` | list, current, git init, update |
|
|
199
|
+
| `file` | `bridged` partial | find text/file/symbol, list/content/status |
|
|
200
|
+
| `mcp` | `bridged` | status, add, OAuth, connect/disconnect |
|
|
201
|
+
| `workspace` | `bridged` | adapter/list/status/create/remove/session-restore |
|
|
202
|
+
| top-level instance routes | `bridged` | path, vcs, command, agent, skill, lsp, formatter, dispose |
|
|
203
|
+
| experimental JSON routes | `bridged` | console, tool, worktree list/mutations, global session list, resource list |
|
|
204
|
+
| `session` | `bridged` | read, lifecycle, prompt, message/part mutations, revert, permission reply |
|
|
205
|
+
| `sync` | `bridged` | start/replay/history |
|
|
206
|
+
| `event` | `bridged` | SSE via raw Effect HTTP |
|
|
207
|
+
| `pty` | `special` | websocket |
|
|
208
|
+
| `tui` | `special` | UI bridge |
|
|
209
209
|
|
|
210
210
|
## Full Route Checklist
|
|
211
211
|
|
package/src/cli/cmd/providers.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { Instance } from "../../project/instance"
|
|
|
14
14
|
import { Process } from "@/util/process"
|
|
15
15
|
import { text } from "node:stream/consumers"
|
|
16
16
|
import { Effect } from "effect"
|
|
17
|
+
import { Npm } from "@saeeol/core/npm"
|
|
17
18
|
import { put, handlePluginAuth, resolvePluginProviders } from "./providers-auth"
|
|
18
19
|
|
|
19
20
|
const getModels = () => AppRuntime.runPromise(ModelsDev.Service.use((s) => s.get()))
|
|
@@ -24,7 +25,12 @@ export const ProvidersCommand = cmd({
|
|
|
24
25
|
aliases: ["providers"],
|
|
25
26
|
describe: "manage AI providers and credentials",
|
|
26
27
|
builder: (yargs) =>
|
|
27
|
-
yargs
|
|
28
|
+
yargs
|
|
29
|
+
.command(ProvidersListCommand)
|
|
30
|
+
.command(ProvidersLoginCommand)
|
|
31
|
+
.command(ProvidersLogoutCommand)
|
|
32
|
+
.command(ProvidersInstallCommand)
|
|
33
|
+
.demandCommand(),
|
|
28
34
|
async handler() {},
|
|
29
35
|
})
|
|
30
36
|
|
|
@@ -226,3 +232,71 @@ export const ProvidersLogoutCommand = cmd({
|
|
|
226
232
|
})
|
|
227
233
|
},
|
|
228
234
|
})
|
|
235
|
+
|
|
236
|
+
export const ProvidersInstallCommand = cmd({
|
|
237
|
+
command: "install [providers..]",
|
|
238
|
+
describe: "pre-install provider SDK packages (skip runtime download delay)",
|
|
239
|
+
builder: (yargs) =>
|
|
240
|
+
yargs.positional("providers", {
|
|
241
|
+
describe: "provider id(s) to install (e.g. anthropic openai google)",
|
|
242
|
+
type: "string",
|
|
243
|
+
array: true,
|
|
244
|
+
}),
|
|
245
|
+
async handler(args) {
|
|
246
|
+
await Instance.provide({
|
|
247
|
+
directory: process.cwd(),
|
|
248
|
+
async fn() {
|
|
249
|
+
UI.empty()
|
|
250
|
+
prompts.intro("Install provider SDKs")
|
|
251
|
+
|
|
252
|
+
await refreshModels().catch(() => {})
|
|
253
|
+
const database = await getModels()
|
|
254
|
+
|
|
255
|
+
// Build npm package → provider names mapping from models.dev data
|
|
256
|
+
const npmToNames: Record<string, Set<string>> = {}
|
|
257
|
+
for (const [id, info] of Object.entries(database)) {
|
|
258
|
+
const pkg = info.npm
|
|
259
|
+
if (!pkg) continue
|
|
260
|
+
if (!npmToNames[pkg]) npmToNames[pkg] = new Set()
|
|
261
|
+
npmToNames[pkg].add(info.name || id)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Resolve target packages
|
|
265
|
+
const targets = args.providers?.length
|
|
266
|
+
? args.providers
|
|
267
|
+
: await (async () => {
|
|
268
|
+
const options = Object.entries(npmToNames).map(([pkg, names]) => ({
|
|
269
|
+
label: [...names].join(", "),
|
|
270
|
+
value: pkg,
|
|
271
|
+
hint: pkg,
|
|
272
|
+
}))
|
|
273
|
+
const selected = await prompts.multiselect({
|
|
274
|
+
message: "Select providers to install",
|
|
275
|
+
options,
|
|
276
|
+
required: true,
|
|
277
|
+
})
|
|
278
|
+
if (prompts.isCancel(selected)) throw new UI.CancelledError()
|
|
279
|
+
return selected as string[]
|
|
280
|
+
})()
|
|
281
|
+
|
|
282
|
+
for (const target of targets) {
|
|
283
|
+
// If user typed a provider id (like "anthropic"), resolve to npm package
|
|
284
|
+
const pkg = npmToNames[target] ? target
|
|
285
|
+
: Object.entries(database).find(([id]) => id === target)?.[1]?.npm
|
|
286
|
+
?? target
|
|
287
|
+
|
|
288
|
+
const spinner = prompts.spinner()
|
|
289
|
+
spinner.start(`Installing ${pkg}`)
|
|
290
|
+
try {
|
|
291
|
+
await Npm.add(pkg)
|
|
292
|
+
spinner.stop(`${pkg} installed`)
|
|
293
|
+
} catch (err) {
|
|
294
|
+
spinner.stop(`${pkg} failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
prompts.outro("Done")
|
|
299
|
+
},
|
|
300
|
+
})
|
|
301
|
+
},
|
|
302
|
+
})
|
|
@@ -4,6 +4,7 @@ import { DialogConfirm } from "@tui/ui/dialog-confirm"
|
|
|
4
4
|
import { DialogAlert } from "@tui/ui/dialog-alert"
|
|
5
5
|
import { errorMessage } from "@tui/app-config"
|
|
6
6
|
import * as SaeeolApp from "@/saeeol/cli/cmd/tui/app"
|
|
7
|
+
import { ProviderInstallEvent } from "@/provider/provider-events"
|
|
7
8
|
|
|
8
9
|
type Event = ReturnType<typeof import("@tui/context/event").useEvent>
|
|
9
10
|
type Command = ReturnType<typeof import("@tui/component/dialog-command").useCommandDialog>
|
|
@@ -101,4 +102,35 @@ export function registerAppEvents(deps: EventDeps) {
|
|
|
101
102
|
)
|
|
102
103
|
void exit()
|
|
103
104
|
})
|
|
105
|
+
|
|
106
|
+
// Provider SDK 동적 설치 진행 상태 표시
|
|
107
|
+
// subscribe() 사용 + 타입 단언 — SDK Event 유니온에 아직 타입이 없음
|
|
108
|
+
// SDK 재생성(provider-events 레지스트리 포함) 후 타입 안전하게 전환 가능
|
|
109
|
+
event.subscribe((raw: any) => {
|
|
110
|
+
const evt = raw as { type: string; properties: { providerID: string; pkg: string; error?: string } }
|
|
111
|
+
if (evt.type === ProviderInstallEvent.Started.type) {
|
|
112
|
+
toast.show({
|
|
113
|
+
title: "Installing Provider",
|
|
114
|
+
message: `Downloading ${evt.properties.pkg} for ${evt.properties.providerID}...`,
|
|
115
|
+
variant: "info",
|
|
116
|
+
duration: 60000,
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
if (evt.type === ProviderInstallEvent.Completed.type) {
|
|
120
|
+
toast.show({
|
|
121
|
+
title: "Provider Ready",
|
|
122
|
+
message: `${evt.properties.providerID} installed successfully`,
|
|
123
|
+
variant: "success",
|
|
124
|
+
duration: 3000,
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
if (evt.type === ProviderInstallEvent.Failed.type) {
|
|
128
|
+
toast.show({
|
|
129
|
+
title: "Installation Failed",
|
|
130
|
+
message: `Failed to install ${evt.properties.pkg}: ${evt.properties.error}`,
|
|
131
|
+
variant: "error",
|
|
132
|
+
duration: 10000,
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
})
|
|
104
136
|
}
|
|
@@ -21,7 +21,7 @@ export type PromptStatusBarProps = {
|
|
|
21
21
|
exitPress: number
|
|
22
22
|
}
|
|
23
23
|
hint: any
|
|
24
|
-
usage: () => { context: string; cost?: string } | undefined
|
|
24
|
+
usage: () => { context: string; cost?: string; health: string; remaining?: number; pct: number } | undefined
|
|
25
25
|
borderHighlight: () => any
|
|
26
26
|
spinnerDef: () => { frames: any; color: any }
|
|
27
27
|
editorFileLabelDisplay: () => string | undefined
|
|
@@ -128,11 +128,20 @@ export function PromptStatusBar(props: PromptStatusBarProps) {
|
|
|
128
128
|
<Match when={props.store.mode === "normal"}>
|
|
129
129
|
<Switch>
|
|
130
130
|
<Match when={props.usage()}>
|
|
131
|
-
{(item) =>
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
131
|
+
{(item) => {
|
|
132
|
+
const u = item()
|
|
133
|
+
const color = u.health === "critical"
|
|
134
|
+
? theme.error
|
|
135
|
+
: u.health === "warning"
|
|
136
|
+
? theme.warning
|
|
137
|
+
: theme.textMuted
|
|
138
|
+
const parts = [u.context, u.cost].filter(Boolean)
|
|
139
|
+
return (
|
|
140
|
+
<text fg={color} wrapMode="none">
|
|
141
|
+
{parts.join(" · ")}
|
|
142
|
+
</text>
|
|
143
|
+
)
|
|
144
|
+
}}
|
|
136
145
|
</Match>
|
|
137
146
|
<Match when={true}>
|
|
138
147
|
<text fg={theme.text}>
|
|
@@ -47,7 +47,7 @@ export function formatEditorContext(selection: EditorSelection) {
|
|
|
47
47
|
|
|
48
48
|
export type PromptMemos = {
|
|
49
49
|
status: () => { type: string }
|
|
50
|
-
usage: () => { context: string; cost?: string } | undefined
|
|
50
|
+
usage: () => { context: string; cost?: string; health: string; remaining?: number; pct: number } | undefined
|
|
51
51
|
currentProviderLabel: () => string
|
|
52
52
|
hasRightContent: () => boolean
|
|
53
53
|
editorContext: () => EditorSelection | undefined
|
|
@@ -143,11 +143,17 @@ export function usePromptMemos(deps: MemoDeps): PromptMemos {
|
|
|
143
143
|
const tokens = last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
|
|
144
144
|
if (tokens <= 0) return
|
|
145
145
|
const model = sync.data.provider.find((item) => item.id === last.providerID)?.models[last.modelID]
|
|
146
|
-
const
|
|
146
|
+
const contextLimit = model?.limit.context ?? 0
|
|
147
|
+
const pct = contextLimit ? Math.round((tokens / contextLimit) * 100) : 0
|
|
147
148
|
const cost = msg.reduce((sum, item) => sum + (item.role === "assistant" ? item.cost : 0), 0)
|
|
149
|
+
const health = !contextLimit ? "ok" : pct >= 90 ? "critical" : pct >= 75 ? "warning" : "ok"
|
|
150
|
+
const remaining = contextLimit ? Math.max(0, contextLimit - tokens) : undefined
|
|
148
151
|
return {
|
|
149
|
-
context:
|
|
152
|
+
context: contextLimit ? `${Locale.number(tokens)} (${pct}%)` : Locale.number(tokens),
|
|
150
153
|
cost: cost > 0 ? money.format(cost) : undefined,
|
|
154
|
+
health,
|
|
155
|
+
remaining,
|
|
156
|
+
pct,
|
|
151
157
|
}
|
|
152
158
|
})
|
|
153
159
|
|
package/src/index.ts
CHANGED
|
@@ -11,52 +11,10 @@ import { hideBin } from "yargs/helpers"
|
|
|
11
11
|
// ║ MASTER = CODE + web + remote + analytics ║
|
|
12
12
|
// ╚══════════════════════════════════════════════════════════════════╝
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
import {
|
|
16
|
-
import { RunCommand } from "./cli/cmd/run"
|
|
17
|
-
import { GenerateCommand } from "./cli/cmd/generate"
|
|
18
|
-
import { SessionCommand } from "./cli/cmd/session"
|
|
19
|
-
|
|
20
|
-
// ── LIGHT: llm ─────────────────────────────────────────────────────
|
|
21
|
-
import { InitCommand } from "./cli/cmd/init"
|
|
22
|
-
import { ProvidersCommand } from "./cli/cmd/providers"
|
|
23
|
-
import { ModelsCommand } from "./cli/cmd/models"
|
|
24
|
-
import { ConfigCommand as ConfigCLICommand } from "./cli/cmd/config"
|
|
25
|
-
|
|
26
|
-
// ── LIGHT: lifecycle ───────────────────────────────────────────────
|
|
27
|
-
import { UpgradeCommand } from "./cli/cmd/upgrade"
|
|
28
|
-
import { UninstallCommand } from "./cli/cmd/uninstall"
|
|
29
|
-
|
|
30
|
-
// ── CODE: server ───────────────────────────────────────────────────
|
|
31
|
-
import { ServeCommand } from "./cli/cmd/serve"
|
|
32
|
-
import { AttachCommand } from "./cli/cmd/tui/attach"
|
|
33
|
-
import { AcpCommand } from "./cli/cmd/acp"
|
|
34
|
-
|
|
35
|
-
// ── CODE: tools ────────────────────────────────────────────────────
|
|
36
|
-
import { McpCommand } from "./cli/cmd/mcp"
|
|
37
|
-
import { PluginCommand } from "./cli/cmd/plug"
|
|
38
|
-
|
|
39
|
-
// ── CODE: data ─────────────────────────────────────────────────────
|
|
40
|
-
import { ExportCommand } from "./cli/cmd/export"
|
|
41
|
-
import { ImportCommand } from "./cli/cmd/import"
|
|
42
|
-
import { DbCommand } from "./cli/cmd/db"
|
|
43
|
-
|
|
44
|
-
// ── CODE: dev (선택) ───────────────────────────────────────────────
|
|
45
|
-
import { AgentCommand } from "./cli/cmd/agent"
|
|
46
|
-
import { DebugCommand } from "./cli/cmd/debug"
|
|
47
|
-
|
|
48
|
-
// ── MASTER: web ────────────────────────────────────────────────────
|
|
49
|
-
import { WebCommand } from "./cli/cmd/web"
|
|
50
|
-
|
|
51
|
-
// ── MASTER: remote (선택) ──────────────────────────────────────────
|
|
52
|
-
import { RemoteCommand } from "./cli/cmd/remote"
|
|
53
|
-
|
|
54
|
-
// ── MASTER: analytics (선택) ───────────────────────────────────────
|
|
55
|
-
import { StatsCommand } from "./cli/cmd/stats"
|
|
56
|
-
import { PrCommand } from "./cli/cmd/pr"
|
|
57
|
-
import { RollCallCommand } from "./saeeol/cli/cmd/roll-call"
|
|
14
|
+
import type { Tier } from "./addons/types"
|
|
15
|
+
import { commandsForTier, manifest } from "./addons/registry"
|
|
58
16
|
|
|
59
|
-
// ── DEV 전용
|
|
17
|
+
// ── DEV 전용 (항상 포함) ────────────────────────────────────────────
|
|
60
18
|
import { DevSetupCommand, DevAliasCommand } from "./saeeol/cli/dev-setup"
|
|
61
19
|
|
|
62
20
|
// ── 공통 ────────────────────────────────────────────────────────────
|
|
@@ -82,8 +40,7 @@ import { errorMessage } from "./util/error"
|
|
|
82
40
|
import { Heap } from "./cli/heap"
|
|
83
41
|
import { drizzle } from "drizzle-orm/bun-sqlite"
|
|
84
42
|
import { ensureProcessMetadata } from "@saeeol/core/util/saeeol-process"
|
|
85
|
-
|
|
86
|
-
import { addonsForTier, manifest } from "./addons/registry"
|
|
43
|
+
|
|
87
44
|
|
|
88
45
|
if (!process.env[ENV_FEATURE]) {
|
|
89
46
|
const isServe = process.argv.includes("serve")
|
|
@@ -235,70 +192,18 @@ let cli = yargs(args)
|
|
|
235
192
|
.usage("")
|
|
236
193
|
.completion("completion", "generate shell completion script")
|
|
237
194
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
.command(RunCommand) // run 프롬프트 실행
|
|
243
|
-
.command(GenerateCommand) // generate 코드 생성
|
|
244
|
-
.command(SessionCommand) // session 세션 관리
|
|
245
|
-
|
|
246
|
-
// ════════════════════════════════════════════════════════════════
|
|
247
|
-
// LIGHT: llm (provider + 모델 + 설정)
|
|
248
|
-
// ════════════════════════════════════════════════════════════════
|
|
249
|
-
.command(InitCommand) // init 최초 설정
|
|
250
|
-
.command(ProvidersCommand) // auth provider 인증
|
|
251
|
-
.command(ModelsCommand) // models 모델 목록
|
|
252
|
-
.command(ConfigCLICommand) // config 설정 관리
|
|
253
|
-
|
|
254
|
-
// ════════════════════════════════════════════════════════════════
|
|
255
|
-
// LIGHT: lifecycle (설치/업그레이드/제거)
|
|
256
|
-
// ════════════════════════════════════════════════════════════════
|
|
257
|
-
.command(UpgradeCommand) // upgrade 버전 업그레이드
|
|
258
|
-
.command(UninstallCommand) // uninstall 제거
|
|
259
|
-
|
|
260
|
-
// ════════════════════════════════════════════════════════════════
|
|
261
|
-
// CODE: server (headless + 원격 + ACP)
|
|
262
|
-
// ════════════════════════════════════════════════════════════════
|
|
263
|
-
.command(ServeCommand) // serve headless 서버
|
|
264
|
-
.command(AttachCommand) // attach 원격 연결
|
|
265
|
-
.command(AcpCommand) // acp ACP 서버
|
|
195
|
+
// ════════════════════════════════════════════════════════════════
|
|
196
|
+
// 애드온 기반 명령어 등록
|
|
197
|
+
// SAEEOL_TIER define에 따라 활성 애드온이 결정됨
|
|
198
|
+
// ════════════════════════════════════════════════════════════════
|
|
266
199
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
.command(
|
|
272
|
-
|
|
273
|
-
// ════════════════════════════════════════════════════════════════
|
|
274
|
-
// CODE: data (export + import + db)
|
|
275
|
-
// ════════════════════════════════════════════════════════════════
|
|
276
|
-
.command(ExportCommand) // export 세션 내보내기
|
|
277
|
-
.command(ImportCommand) // import 세션 가져오기
|
|
278
|
-
.command(DbCommand) // db 데이터베이스
|
|
279
|
-
|
|
280
|
-
// ════════════════════════════════════════════════════════════════
|
|
281
|
-
// CODE: dev (선택)
|
|
282
|
-
// ════════════════════════════════════════════════════════════════
|
|
283
|
-
.command(AgentCommand) // agent 에이전트 관리
|
|
284
|
-
.command(DebugCommand) // debug 디버깅
|
|
285
|
-
|
|
286
|
-
// ════════════════════════════════════════════════════════════════
|
|
287
|
-
// MASTER: web (브라우저 채팅 UI)
|
|
288
|
-
// ════════════════════════════════════════════════════════════════
|
|
289
|
-
.command(WebCommand) // web 웹 UI + 브라우저
|
|
290
|
-
|
|
291
|
-
// ════════════════════════════════════════════════════════════════
|
|
292
|
-
// MASTER: remote (선택)
|
|
293
|
-
// ════════════════════════════════════════════════════════════════
|
|
294
|
-
.command(RemoteCommand) // remote 실시간 릴레이
|
|
200
|
+
declare const SAEEOL_TIER: string
|
|
201
|
+
const tier = (typeof SAEEOL_TIER === "string" ? SAEEOL_TIER : "master") as Tier
|
|
202
|
+
const addonCommands = await commandsForTier(tier)
|
|
203
|
+
for (const cmd of addonCommands) {
|
|
204
|
+
cli = cli.command(cmd)
|
|
205
|
+
}
|
|
295
206
|
|
|
296
|
-
// ════════════════════════════════════════════════════════════════
|
|
297
|
-
// MASTER: analytics (선택)
|
|
298
|
-
// ════════════════════════════════════════════════════════════════
|
|
299
|
-
.command(StatsCommand) // stats 사용량 통계
|
|
300
|
-
.command(PrCommand) // pr GitHub PR
|
|
301
|
-
.command(RollCallCommand) // roll-call 모델 연결 테스트
|
|
302
207
|
if (InstallationBuildKind !== "release") {
|
|
303
208
|
cli = cli.command(DevSetupCommand).command(DevAliasCommand)
|
|
304
209
|
}
|
|
@@ -25,12 +25,12 @@ type ProviderLoader = () => Promise<(opts: any) => BundledSDK>
|
|
|
25
25
|
// Bun은 조건에 맞지 않는 branch의 import()를 dead code로 처리
|
|
26
26
|
|
|
27
27
|
function getProviders(): Record<string, ProviderLoader> {
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
const tier = typeof SAEEOL_TIER === "string" ? SAEEOL_TIER : "master"
|
|
29
|
+
if (tier === "light") {
|
|
30
30
|
const m = require("./tiers/light") as typeof import("./tiers/light")
|
|
31
31
|
return m.lightProviders
|
|
32
32
|
}
|
|
33
|
-
if (
|
|
33
|
+
if (tier === "code") {
|
|
34
34
|
const m = require("./tiers/code") as typeof import("./tiers/code")
|
|
35
35
|
return m.codeProviders
|
|
36
36
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { BusEvent } from "@/bus/bus-event"
|
|
2
|
+
import { Schema } from "effect"
|
|
3
|
+
|
|
4
|
+
// Provider SDK 동적 설치 이벤트 — TUI/웹뷰에서 구독하여 설치 진행 상태 표시
|
|
5
|
+
// BusEvent.define 호출로 전역 레지스트리에 자동 등록됨
|
|
6
|
+
export const ProviderInstallEvent = {
|
|
7
|
+
Started: BusEvent.define(
|
|
8
|
+
"provider.install.started",
|
|
9
|
+
Schema.Struct({
|
|
10
|
+
providerID: Schema.String,
|
|
11
|
+
pkg: Schema.String,
|
|
12
|
+
}),
|
|
13
|
+
),
|
|
14
|
+
Completed: BusEvent.define(
|
|
15
|
+
"provider.install.completed",
|
|
16
|
+
Schema.Struct({
|
|
17
|
+
providerID: Schema.String,
|
|
18
|
+
pkg: Schema.String,
|
|
19
|
+
}),
|
|
20
|
+
),
|
|
21
|
+
Failed: BusEvent.define(
|
|
22
|
+
"provider.install.failed",
|
|
23
|
+
Schema.Struct({
|
|
24
|
+
providerID: Schema.String,
|
|
25
|
+
pkg: Schema.String,
|
|
26
|
+
error: Schema.String,
|
|
27
|
+
}),
|
|
28
|
+
),
|
|
29
|
+
}
|
|
@@ -5,7 +5,9 @@ import { NoSuchModelError, type Provider as SDK } from "ai"
|
|
|
5
5
|
import * as Log from "@saeeol/core/util/log"
|
|
6
6
|
import { Npm } from "@saeeol/core/npm"
|
|
7
7
|
import { Hash } from "@saeeol/core/util/hash"
|
|
8
|
+
import * as Bus from "@/bus"
|
|
8
9
|
import { BUNDLED_PROVIDERS } from "./bundled-providers"
|
|
10
|
+
import { ProviderInstallEvent } from "./provider-events"
|
|
9
11
|
import { buildTimeoutSignal } from "@/saeeol/provider/provider"
|
|
10
12
|
import { InitError } from "./provider-types"
|
|
11
13
|
import type { State, BundledSDK } from "./provider-types"
|
|
@@ -182,9 +184,27 @@ export async function resolveSDK(model: Model, s: State, envs: Record<string, st
|
|
|
182
184
|
|
|
183
185
|
let installedPath: string
|
|
184
186
|
if (!model.api.npm.startsWith("file://")) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
187
|
+
log.info("installing provider package", { providerID: model.providerID, pkg: model.api.npm })
|
|
188
|
+
void Bus.publish(ProviderInstallEvent.Started, {
|
|
189
|
+
providerID: model.providerID,
|
|
190
|
+
pkg: model.api.npm,
|
|
191
|
+
})
|
|
192
|
+
try {
|
|
193
|
+
const item = await Npm.add(model.api.npm)
|
|
194
|
+
if (!item.entrypoint) throw new Error(`Package ${model.api.npm} has no import entrypoint`)
|
|
195
|
+
installedPath = item.entrypoint
|
|
196
|
+
void Bus.publish(ProviderInstallEvent.Completed, {
|
|
197
|
+
providerID: model.providerID,
|
|
198
|
+
pkg: model.api.npm,
|
|
199
|
+
})
|
|
200
|
+
} catch (installErr) {
|
|
201
|
+
void Bus.publish(ProviderInstallEvent.Failed, {
|
|
202
|
+
providerID: model.providerID,
|
|
203
|
+
pkg: model.api.npm,
|
|
204
|
+
error: installErr instanceof Error ? installErr.message : String(installErr),
|
|
205
|
+
})
|
|
206
|
+
throw installErr
|
|
207
|
+
}
|
|
188
208
|
} else {
|
|
189
209
|
log.info("loading local provider", { pkg: model.api.npm })
|
|
190
210
|
installedPath = model.api.npm
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// LIGHT 티어 provider 맵 —
|
|
1
|
+
// LIGHT 티어 provider 맵 — @saeeol/gateway만 번들, 나머지는 런타임 동적 설치
|
|
2
|
+
// 번들 크기 최소화: 사용자가 provider를 선택하면 Npm.add()로 자동 설치됨
|
|
2
3
|
import type { BundledSDK } from "../../provider/provider-types"
|
|
3
4
|
import { createSaeeol } from "@saeeol/gateway"
|
|
4
5
|
|
|
@@ -6,9 +7,4 @@ type L = () => Promise<(opts: any) => BundledSDK>
|
|
|
6
7
|
|
|
7
8
|
export const lightProviders: Record<string, L> = {
|
|
8
9
|
"@saeeol/gateway": async () => createSaeeol as any,
|
|
9
|
-
"@ai-sdk/anthropic": () => import("@ai-sdk/anthropic").then((m) => m.createAnthropic),
|
|
10
|
-
"@ai-sdk/openai": () => import("@ai-sdk/openai").then((m) => m.createOpenAI),
|
|
11
|
-
"@ai-sdk/openai-compatible": () => import("@ai-sdk/openai-compatible").then((m) => m.createOpenAICompatible),
|
|
12
|
-
"@ai-sdk/google": () => import("@ai-sdk/google").then((m) => m.createGoogleGenerativeAI),
|
|
13
|
-
"@ai-sdk/github-copilot": () => import("../sdk/copilot/copilot-provider").then((m) => m.createOpenaiCompatible),
|
|
14
10
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@saeeol/plugin/tui"
|
|
2
2
|
import { createMemo } from "solid-js"
|
|
3
3
|
import { formatCount, getUsage } from "@tui/routes/session/usage"
|
|
4
|
+
import type { AssistantMessage } from "@saeeol/sdk/v2"
|
|
4
5
|
|
|
5
6
|
const id = "internal:saeeol-sidebar-usage"
|
|
6
7
|
|
|
@@ -13,8 +14,24 @@ function View(props: { api: TuiPluginApi; session_id: string }) {
|
|
|
13
14
|
input: formatCount(total.input),
|
|
14
15
|
output: formatCount(total.output),
|
|
15
16
|
cached: formatCount(total.cached),
|
|
17
|
+
total: total.input + total.output + total.cached,
|
|
16
18
|
}
|
|
17
19
|
})
|
|
20
|
+
const contextInfo = createMemo(() => {
|
|
21
|
+
const messages = msg()
|
|
22
|
+
const last = messages.findLast((item): item is AssistantMessage => item.role === "assistant" && item.tokens.output > 0)
|
|
23
|
+
if (!last) return null
|
|
24
|
+
const tokens = last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
|
|
25
|
+
const model = props.api.state.provider.find((item) => item.id === last.providerID)?.models[last.modelID]
|
|
26
|
+
const limit = model?.limit.context ?? 0
|
|
27
|
+
if (!limit) return { pct: 0, label: "", color: theme().textMuted }
|
|
28
|
+
const pct = Math.round((tokens / limit) * 100)
|
|
29
|
+
const color = pct >= 90 ? theme().error : pct >= 75 ? theme().warning : theme().textMuted
|
|
30
|
+
const barWidth = 12
|
|
31
|
+
const filled = Math.round((pct / 100) * barWidth)
|
|
32
|
+
const bar = "█".repeat(filled) + "░".repeat(barWidth - filled)
|
|
33
|
+
return { pct, label: `${bar} ${pct}%`, color }
|
|
34
|
+
})
|
|
18
35
|
|
|
19
36
|
return (
|
|
20
37
|
<box>
|
|
@@ -33,6 +50,14 @@ function View(props: { api: TuiPluginApi; session_id: string }) {
|
|
|
33
50
|
<text fg={theme().textMuted}>Cached</text>
|
|
34
51
|
<text fg={theme().textMuted}>{usage().cached}</text>
|
|
35
52
|
</box>
|
|
53
|
+
{contextInfo() && contextInfo()!.pct > 0 && (
|
|
54
|
+
<box flexDirection="column">
|
|
55
|
+
<text fg={theme().text}>
|
|
56
|
+
<b>Context</b>
|
|
57
|
+
</text>
|
|
58
|
+
<text fg={contextInfo()!.color}>{contextInfo()!.label}</text>
|
|
59
|
+
</box>
|
|
60
|
+
)}
|
|
36
61
|
</box>
|
|
37
62
|
)
|
|
38
63
|
}
|
|
@@ -4,26 +4,16 @@
|
|
|
4
4
|
//
|
|
5
5
|
// This module exports patch functions and data that the upstream provider.ts
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { AI_SDK_PROVIDERS, PROMPTS } from "@saeeol/gateway"
|
|
8
8
|
import { DEFAULT_HEADERS } from "@/saeeol/const"
|
|
9
9
|
import { ProviderID, ModelID } from "@/provider/schema"
|
|
10
10
|
import { Effect, Schema } from "effect"
|
|
11
|
-
import type {
|
|
11
|
+
import type { ProviderV2 } from "@ai-sdk/provider"
|
|
12
12
|
import { mapValues, omit, pickBy } from "remeda"
|
|
13
13
|
|
|
14
14
|
/** Default timeout (ms) for provider HTTP requests (connection phase). */
|
|
15
15
|
export const REQUEST_TIMEOUT_MS = 300_000 // 5 minutes
|
|
16
16
|
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
// Bundled providers
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
|
|
21
|
-
type BundledSDK = { languageModel(modelId: string): LanguageModelV3 }
|
|
22
|
-
|
|
23
|
-
export const SAEEOL_BUNDLED_PROVIDERS: Record<string, () => Promise<(options: any) => BundledSDK>> = {
|
|
24
|
-
"@saeeol/gateway": async () => createSaeeol as unknown as (options: any) => BundledSDK,
|
|
25
|
-
}
|
|
26
|
-
|
|
27
17
|
// ---------------------------------------------------------------------------
|
|
28
18
|
// Model schema extensions (spread into Provider.Model Schema.Struct)
|
|
29
19
|
// ---------------------------------------------------------------------------
|