saeeol 1.0.6 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "name": "saeeol",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
package/script/build.ts CHANGED
@@ -55,7 +55,99 @@ const baselineFlag = process.argv.includes("--baseline")
55
55
  const skipInstall = process.argv.includes("--skip-install")
56
56
  const sourcemapsFlag = process.argv.includes("--sourcemaps")
57
57
  const plugin = createSolidTransformPlugin()
58
- async function copyTreeSitterWasms(outputDir: string) {
58
+
59
+ // ══════════════════════════════════════════════════════════════════
60
+ // Tier-based tree-sitter WASM selection
61
+ //
62
+ // LIGHT = 5 langs (markdown, json, yaml, toml, tsx) ~8 MB
63
+ // CODE = 15 langs (+ python, rust, go, c, cpp...) ~18 MB
64
+ // MASTER = all 37 langs ~50 MB
65
+ // ══════════════════════════════════════════════════════════════════
66
+
67
+ // ══════════════════════════════════════════════════════════════════
68
+ // Tier-based external packages
69
+ // These packages are excluded from the bundle entirely
70
+ // ══════════════════════════════════════════════════════════════════
71
+
72
+ const allProviderPkgs = [
73
+ "@ai-sdk/amazon-bedrock",
74
+ "@ai-sdk/anthropic",
75
+ "@ai-sdk/azure",
76
+ "@ai-sdk/google",
77
+ "@ai-sdk/google-vertex",
78
+ "@ai-sdk/openai",
79
+ "@ai-sdk/openai-compatible",
80
+ "@openrouter/ai-sdk-provider",
81
+ "@ai-sdk/xai",
82
+ "@ai-sdk/mistral",
83
+ "@ai-sdk/groq",
84
+ "@ai-sdk/deepinfra",
85
+ "@ai-sdk/cerebras",
86
+ "@ai-sdk/cohere",
87
+ "@ai-sdk/gateway",
88
+ "@ai-sdk/togetherai",
89
+ "@ai-sdk/perplexity",
90
+ "@ai-sdk/vercel",
91
+ "@ai-sdk/alibaba",
92
+ "gitlab-ai-provider",
93
+ "venice-ai-sdk-provider",
94
+ ]
95
+
96
+ const lightRequired = new Set([
97
+ "@ai-sdk/anthropic",
98
+ "@ai-sdk/openai",
99
+ "@ai-sdk/openai-compatible",
100
+ "@ai-sdk/google",
101
+ "@saeeol/gateway",
102
+ ])
103
+
104
+ const codeRequired = new Set([
105
+ ...lightRequired,
106
+ "@ai-sdk/amazon-bedrock",
107
+ "@ai-sdk/azure",
108
+ "@ai-sdk/google-vertex",
109
+ "@openrouter/ai-sdk-provider",
110
+ "@ai-sdk/groq",
111
+ "@ai-sdk/deepinfra",
112
+ "@ai-sdk/gateway",
113
+ "@ai-sdk/alibaba",
114
+ "@ai-sdk/cerebras",
115
+ ])
116
+
117
+ function tierExternals(tier: string): string[] {
118
+ if (tier === "master") return []
119
+ const required = tier === "light" ? lightRequired : codeRequired
120
+ return allProviderPkgs.filter((p) => !required.has(p))
121
+ }
122
+
123
+ const tierArg = process.argv.find((a) => a.startsWith("--tier="))?.split("=")[1] ?? "master"
124
+
125
+ const treeSitterLanguages: Record<string, string[]> = {
126
+ light: [
127
+ "markdown", "markdown_inline",
128
+ "json",
129
+ "yaml",
130
+ "toml",
131
+ "tsx", "typescript",
132
+ ],
133
+ code: [
134
+ "markdown", "markdown_inline",
135
+ "json",
136
+ "yaml",
137
+ "toml",
138
+ "tsx", "typescript",
139
+ "python",
140
+ "rust",
141
+ "go",
142
+ "c", "cpp",
143
+ "java",
144
+ "javascript",
145
+ "bash",
146
+ ],
147
+ master: [], // empty = all
148
+ }
149
+
150
+ async function copyTreeSitterWasms(outputDir: string, tier: string) {
59
151
  const runtimeWasmPath = require.resolve("web-tree-sitter/tree-sitter.wasm")
60
152
  const languagePackagePath = require.resolve("tree-sitter-wasms/package.json")
61
153
  const languageWasmDir = path.join(path.dirname(languagePackagePath), "out")
@@ -64,13 +156,21 @@ async function copyTreeSitterWasms(outputDir: string) {
64
156
  await fs.promises.mkdir(targetDir, { recursive: true })
65
157
  await fs.promises.copyFile(runtimeWasmPath, path.join(targetDir, "tree-sitter.wasm"))
66
158
 
67
- const languageWasmFiles = (await fs.promises.readdir(languageWasmDir)).filter((file) => file.endsWith(".wasm"))
159
+ const allWasmFiles = (await fs.promises.readdir(languageWasmDir)).filter((file) => file.endsWith(".wasm"))
160
+ const allowed = treeSitterLanguages[tier]
161
+
162
+ const filesToCopy = allowed.length === 0
163
+ ? allWasmFiles
164
+ : allWasmFiles.filter((file) => {
165
+ const langName = file.replace("tree-sitter-", "").replace(".wasm", "")
166
+ return allowed.some((a) => langName === a || langName.startsWith(a + "-"))
167
+ })
68
168
 
69
169
  await Promise.all(
70
- languageWasmFiles.map((file) => fs.promises.copyFile(path.join(languageWasmDir, file), path.join(targetDir, file))),
170
+ filesToCopy.map((file) => fs.promises.copyFile(path.join(languageWasmDir, file), path.join(targetDir, file))),
71
171
  )
72
172
 
73
- console.log(`copied ${languageWasmFiles.length + 1} tree-sitter wasm files to ${targetDir}`)
173
+ console.log(`copied ${filesToCopy.length + 1} tree-sitter wasm files to ${targetDir} (tier=${tier})`)
74
174
  }
75
175
 
76
176
  const allTargets: {
@@ -192,7 +292,7 @@ for (const item of targets) {
192
292
  tsconfig: "./tsconfig.json",
193
293
  plugins: [plugin],
194
294
  sourcemap: Script.release ? "none" : "external",
195
- external: ["node-gyp", ...LanceDBRuntime.external],
295
+ external: ["node-gyp", ...LanceDBRuntime.external, ...tierExternals(tierArg)],
196
296
  format: "esm",
197
297
  minify: true,
198
298
  splitting: true,
@@ -216,10 +316,11 @@ for (const item of targets) {
216
316
  SAEEOL_CHANNEL: `'${Script.channel}'`,
217
317
  SAEEOL_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "",
218
318
  SAEEOL_BUILD_KIND: Script.release ? `'release'` : `'source'`,
319
+ SAEEOL_TIER: `'${tierArg}'`,
219
320
  },
220
321
  })
221
322
 
222
- await copyTreeSitterWasms(path.resolve(dir, `dist/${name}/bin`))
323
+ await copyTreeSitterWasms(path.resolve(dir, `dist/${name}/bin`), tierArg)
223
324
  if (item.os === "linux") {
224
325
  const interpreters: Record<string, string> = {
225
326
  x64: "/lib64/ld-linux-x86-64.so.2",
@@ -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 | 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 |
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
 
@@ -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.command(ProvidersListCommand).command(ProvidersLoginCommand).command(ProvidersLogoutCommand).demandCommand(),
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
- <text fg={theme.textMuted} wrapMode="none">
133
- {[item().context, item().cost].filter(Boolean).join(" · ")}
134
- </text>
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 pct = model?.limit.context ? `${Math.round((tokens / model.limit.context) * 100)}%` : undefined
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: pct ? `${Locale.number(tokens)} (${pct})` : Locale.number(tokens),
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
- // ── LIGHT: core ────────────────────────────────────────────────────
15
- import { TuiThreadCommand } from "./cli/cmd/tui/thread"
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
- import type { Tier } from "./addons/types"
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
- // LIGHT: core (TUI 채팅 + 프롬프트 + 세션)
240
- // ════════════════════════════════════════════════════════════════
241
- .command(TuiThreadCommand) // [default] TUI 채팅
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
- // CODE: tools (MCP + 플러그인)
269
- // ════════════════════════════════════════════════════════════════
270
- .command(McpCommand) // mcp MCP 서버 관리
271
- .command(PluginCommand) // plugin 플러그인
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
  }
@@ -1,4 +1,3 @@
1
- import { SAEEOL_BUNDLED_PROVIDERS } from "@/saeeol/provider/provider"
2
1
  import type { BundledSDK } from "./provider-types"
3
2
 
4
3
  export function shouldUseCopilotResponsesApi(modelID: string): boolean {
@@ -11,30 +10,32 @@ export function useLanguageModel(sdk: any) {
11
10
  return sdk.responses === undefined && sdk.chat === undefined
12
11
  }
13
12
 
14
- export const BUNDLED_PROVIDERS: Record<string, () => Promise<(opts: any) => BundledSDK>> = {
15
- "@ai-sdk/amazon-bedrock": () => import("@ai-sdk/amazon-bedrock").then((m) => m.createAmazonBedrock),
16
- "@ai-sdk/anthropic": () => import("@ai-sdk/anthropic").then((m) => m.createAnthropic),
17
- "@ai-sdk/azure": () => import("@ai-sdk/azure").then((m) => m.createAzure),
18
- "@ai-sdk/google": () => import("@ai-sdk/google").then((m) => m.createGoogleGenerativeAI),
19
- "@ai-sdk/google-vertex": () => import("@ai-sdk/google-vertex").then((m) => m.createVertex),
20
- "@ai-sdk/google-vertex/anthropic": () =>
21
- import("@ai-sdk/google-vertex/anthropic").then((m) => m.createVertexAnthropic),
22
- "@ai-sdk/openai": () => import("@ai-sdk/openai").then((m) => m.createOpenAI),
23
- "@ai-sdk/openai-compatible": () => import("@ai-sdk/openai-compatible").then((m) => m.createOpenAICompatible),
24
- "@openrouter/ai-sdk-provider": () => import("@openrouter/ai-sdk-provider").then((m) => m.createOpenRouter),
25
- "@ai-sdk/xai": () => import("@ai-sdk/xai").then((m) => m.createXai),
26
- "@ai-sdk/mistral": () => import("@ai-sdk/mistral").then((m) => m.createMistral),
27
- "@ai-sdk/groq": () => import("@ai-sdk/groq").then((m) => m.createGroq),
28
- "@ai-sdk/deepinfra": () => import("@ai-sdk/deepinfra").then((m) => m.createDeepInfra),
29
- "@ai-sdk/cerebras": () => import("@ai-sdk/cerebras").then((m) => m.createCerebras),
30
- "@ai-sdk/cohere": () => import("@ai-sdk/cohere").then((m) => m.createCohere),
31
- "@ai-sdk/gateway": () => import("@ai-sdk/gateway").then((m) => m.createGateway),
32
- "@ai-sdk/togetherai": () => import("@ai-sdk/togetherai").then((m) => m.createTogetherAI),
33
- "@ai-sdk/perplexity": () => import("@ai-sdk/perplexity").then((m) => m.createPerplexity),
34
- "@ai-sdk/vercel": () => import("@ai-sdk/vercel").then((m) => m.createVercel),
35
- "@ai-sdk/alibaba": () => import("@ai-sdk/alibaba").then((m) => m.createAlibaba),
36
- "gitlab-ai-provider": () => import("gitlab-ai-provider").then((m) => m.createGitLab),
37
- "@ai-sdk/github-copilot": () => import("./sdk/copilot/copilot-provider").then((m) => m.createOpenaiCompatible),
38
- "venice-ai-sdk-provider": () => import("venice-ai-sdk-provider").then((m) => m.createVenice),
39
- ...SAEEOL_BUNDLED_PROVIDERS,
13
+ // ╔══════════════════════════════════════════════════════════════════╗
14
+ // ║ 티어별 provider 로딩 ║
15
+ // ║ ║
16
+ // ║ SAEEOL_TIER는 build.ts define으로 "light"|"code"|"master" 주입 ║
17
+ // ║ Bun은 dead branch를 제거하므로 다른 티어의 import() 제외됨 ║
18
+ // ╚══════════════════════════════════════════════════════════════════╝
19
+
20
+ declare const SAEEOL_TIER: string
21
+
22
+ type ProviderLoader = () => Promise<(opts: any) => BundledSDK>
23
+
24
+ // 티어별 파일에서 각각 서로 다른 provider 집합을 import
25
+ // Bun은 조건에 맞지 않는 branch의 import() dead code로 처리
26
+
27
+ function getProviders(): Record<string, ProviderLoader> {
28
+ const tier = typeof SAEEOL_TIER === "string" ? SAEEOL_TIER : "master"
29
+ if (tier === "light") {
30
+ const m = require("./tiers/light") as typeof import("./tiers/light")
31
+ return m.lightProviders
32
+ }
33
+ if (tier === "code") {
34
+ const m = require("./tiers/code") as typeof import("./tiers/code")
35
+ return m.codeProviders
36
+ }
37
+ const m = require("./tiers/master") as typeof import("./tiers/master")
38
+ return m.masterProviders
40
39
  }
40
+
41
+ export const BUNDLED_PROVIDERS: Record<string, ProviderLoader> = getProviders()