saeeol 1.0.7 → 1.0.9

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.7",
3
+ "version": "1.0.9",
4
4
  "name": "saeeol",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -61,21 +61,21 @@
61
61
  "@parcel/watcher-win32-arm64": "2.5.1",
62
62
  "@parcel/watcher-win32-x64": "2.5.1",
63
63
  "@standard-schema/spec": "1.0.0",
64
- "@tsconfig/bun": "catalog:",
64
+ "@tsconfig/bun": "1.0.9",
65
65
  "@types/babel__core": "7.20.5",
66
- "@types/bun": "catalog:",
67
- "@types/cross-spawn": "catalog:",
66
+ "@types/bun": "1.3.12",
67
+ "@types/cross-spawn": "6.0.6",
68
68
  "@types/mime-types": "3.0.1",
69
69
  "@types/npm-package-arg": "6.1.4",
70
70
  "@types/semver": "^7.5.8",
71
71
  "@types/turndown": "5.0.5",
72
72
  "@types/which": "3.0.4",
73
73
  "@types/yargs": "17.0.33",
74
- "@typescript/native-preview": "catalog:",
75
- "drizzle-kit": "catalog:",
76
- "drizzle-orm": "catalog:",
74
+ "@typescript/native-preview": "7.0.0-dev.20260316.1",
75
+ "drizzle-kit": "1.0.0-beta.19-d95b7a4",
76
+ "drizzle-orm": "1.0.0-beta.19-d95b7a4",
77
77
  "prettier": "3.6.2",
78
- "typescript": "catalog:",
78
+ "typescript": "5.8.2",
79
79
  "vscode-languageserver-types": "3.17.5",
80
80
  "why-is-node-running": "3.2.2",
81
81
  "zod-to-json-schema": "3.24.5",
@@ -107,31 +107,31 @@
107
107
  "@ai-sdk/xai": "3.0.82",
108
108
  "@aws-sdk/credential-providers": "3.1025.0",
109
109
  "@clack/prompts": "1.0.0-alpha.1",
110
- "@effect/opentelemetry": "catalog:",
111
- "@effect/platform-node": "catalog:",
110
+ "@effect/opentelemetry": "4.0.0-beta.57",
111
+ "@effect/platform-node": "4.0.0-beta.57",
112
112
  "@gitlab/gitlab-ai-provider": "3.6.0",
113
113
  "@hono/node-server": "1.19.13",
114
114
  "@hono/node-ws": "1.3.0",
115
115
  "@hono/standard-validator": "0.1.5",
116
- "@hono/zod-validator": "catalog:",
117
- "@lydell/node-pty": "catalog:",
116
+ "@hono/zod-validator": "0.4.2",
117
+ "@lydell/node-pty": "1.2.0-beta.10",
118
118
  "@modelcontextprotocol/sdk": "1.29.0",
119
119
  "@morphllm/morphsdk": "0.2.166",
120
120
  "@npmcli/arborist": "9.4.0",
121
121
  "@npmcli/config": "10.8.1",
122
122
  "@octokit/graphql": "9.0.2",
123
- "@octokit/rest": "catalog:",
124
- "@openauthjs/openauth": "catalog:",
123
+ "@octokit/rest": "22.0.0",
124
+ "@openauthjs/openauth": "0.0.0-20250322224806",
125
125
  "@openrouter/ai-sdk-provider": "2.8.1",
126
126
  "@opentelemetry/api": "1.9.0",
127
127
  "@opentelemetry/context-async-hooks": "2.6.1",
128
128
  "@opentelemetry/exporter-trace-otlp-http": "0.214.0",
129
129
  "@opentelemetry/sdk-trace-base": "2.6.1",
130
130
  "@opentelemetry/sdk-trace-node": "2.6.1",
131
- "@opentui/core": "catalog:",
132
- "@opentui/solid": "catalog:",
131
+ "@opentui/core": "0.2.2",
132
+ "@opentui/solid": "0.2.2",
133
133
  "@parcel/watcher": "2.5.1",
134
- "@pierre/diffs": "catalog:",
134
+ "@pierre/diffs": "1.1.0-beta.18",
135
135
  "@saeeol/boxes": "workspace:*",
136
136
  "@saeeol/core": "workspace:*",
137
137
  "@saeeol/i18n": "workspace:*",
@@ -145,7 +145,7 @@
145
145
  "@solid-primitives/scheduled": "1.5.2",
146
146
  "@standard-schema/spec": "1.0.0",
147
147
  "@zip.js/zip.js": "2.7.62",
148
- "ai": "catalog:",
148
+ "ai": "6.0.168",
149
149
  "ai-gateway-provider": "3.1.2",
150
150
  "bonjour-service": "1.3.0",
151
151
  "bun-pty": "0.4.8",
@@ -153,19 +153,19 @@
153
153
  "chokidar": "4.0.3",
154
154
  "cli-sound": "1.1.3",
155
155
  "clipboardy": "4.0.0",
156
- "cross-spawn": "catalog:",
156
+ "cross-spawn": "7.0.6",
157
157
  "decimal.js": "10.5.0",
158
- "diff": "catalog:",
159
- "drizzle-orm": "catalog:",
160
- "effect": "catalog:",
158
+ "diff": "8.0.4",
159
+ "drizzle-orm": "1.0.0-beta.19-d95b7a4",
160
+ "effect": "4.0.0-beta.57",
161
161
  "fuzzysort": "3.1.0",
162
162
  "gensync": "1.0.0-beta.2",
163
163
  "gitlab-ai-provider": "6.6.0",
164
164
  "glob": "13.0.5",
165
165
  "google-auth-library": "10.5.0",
166
166
  "gray-matter": "4.0.3",
167
- "hono": "catalog:",
168
- "hono-openapi": "catalog:",
167
+ "hono": "4.12.12",
168
+ "hono-openapi": "1.1.2",
169
169
  "iconv-lite": "0.7.2",
170
170
  "ignore": "7.0.5",
171
171
  "immer": "11.1.4",
@@ -174,33 +174,33 @@
174
174
  "minimatch": "10.2.5",
175
175
  "npm-package-arg": "13.0.2",
176
176
  "open": "10.1.2",
177
- "opentui-spinner": "catalog:",
177
+ "opentui-spinner": "0.0.6",
178
178
  "partial-json": "0.1.7",
179
- "remeda": "catalog:",
179
+ "remeda": "2.26.0",
180
180
  "ripgrep": "0.3.1",
181
181
  "rotating-file-stream": "3.2.9",
182
182
  "saeeol-gitlab-auth": "workspace:*",
183
183
  "saeeol-poe-auth": "workspace:*",
184
184
  "semver": "^7.6.3",
185
185
  "simple-git": "3.36.0",
186
- "solid-js": "catalog:",
186
+ "solid-js": "1.9.12",
187
187
  "strip-ansi": "7.1.2",
188
188
  "tree-sitter-bash": "0.25.0",
189
189
  "tree-sitter-powershell": "0.25.10",
190
190
  "tree-sitter-wasms": "^0.1.12",
191
191
  "turndown": "7.2.0",
192
- "ulid": "catalog:",
192
+ "ulid": "3.0.1",
193
193
  "venice-ai-sdk-provider": "2.0.1",
194
194
  "vscode-jsonrpc": "8.2.1",
195
195
  "web-tree-sitter": "0.25.10",
196
196
  "which": "6.0.1",
197
197
  "xdg-basedir": "5.1.0",
198
198
  "yargs": "18.0.0",
199
- "zod": "catalog:",
199
+ "zod": "4.1.8",
200
200
  "zod-to-json-schema": "3.24.5"
201
201
  },
202
202
  "overrides": {
203
- "drizzle-orm": "catalog:"
203
+ "drizzle-orm": "1.0.0-beta.19-d95b7a4"
204
204
  },
205
205
  "peerDependencies": {}
206
206
  }
@@ -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
  }
@@ -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
- if (SAEEOL_TIER === "light") {
29
- // Bun replaces SAEEOL_TIER with literal, dead-eliminates else branches
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 (SAEEOL_TIER === "code") {
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
- const item = await Npm.add(model.api.npm)
186
- if (!item.entrypoint) throw new Error(`Package ${model.api.npm} has no import entrypoint`)
187
- installedPath = item.entrypoint
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,6 +1,5 @@
1
1
  // CODE 티어 provider 맵 — LIGHT + 개발용 provider
2
2
  import type { BundledSDK } from "../../provider/provider-types"
3
- import { createSaeeol } from "@saeeol/gateway"
4
3
  import { lightProviders } from "./light"
5
4
 
6
5
  type L = () => Promise<(opts: any) => BundledSDK>