@cortexkit/opencode-magic-context 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +41 -0
  2. package/dist/cli/config-paths.d.ts +2 -0
  3. package/dist/cli/config-paths.d.ts.map +1 -1
  4. package/dist/cli/doctor.d.ts +2 -0
  5. package/dist/cli/doctor.d.ts.map +1 -0
  6. package/dist/cli/setup.d.ts.map +1 -1
  7. package/dist/cli.js +8549 -125
  8. package/dist/features/builtin-commands/commands.d.ts.map +1 -1
  9. package/dist/features/magic-context/compartment-storage.d.ts.map +1 -1
  10. package/dist/features/magic-context/scheduler.d.ts.map +1 -1
  11. package/dist/features/magic-context/search.d.ts +2 -0
  12. package/dist/features/magic-context/search.d.ts.map +1 -1
  13. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
  14. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
  15. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  16. package/dist/hooks/magic-context/send-session-notification.d.ts.map +1 -1
  17. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +8653 -238
  20. package/dist/plugin/conflict-warning-hook.d.ts +24 -0
  21. package/dist/plugin/conflict-warning-hook.d.ts.map +1 -0
  22. package/dist/shared/conflict-detector.d.ts +29 -0
  23. package/dist/shared/conflict-detector.d.ts.map +1 -0
  24. package/dist/shared/conflict-fixer.d.ts +3 -0
  25. package/dist/shared/conflict-fixer.d.ts.map +1 -0
  26. package/dist/shared/tui-config.d.ts +10 -0
  27. package/dist/shared/tui-config.d.ts.map +1 -0
  28. package/dist/tools/ctx-search/tools.d.ts.map +1 -1
  29. package/dist/tui/data/context-db.d.ts +54 -0
  30. package/dist/tui/data/context-db.d.ts.map +1 -0
  31. package/package.json +20 -1
  32. package/src/tui/data/context-db.ts +584 -0
  33. package/src/tui/index.tsx +461 -0
  34. package/src/tui/slots/sidebar-content.tsx +422 -0
  35. package/src/tui/types/opencode-plugin-tui.d.ts +232 -0
@@ -0,0 +1,422 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import { createEffect, createMemo, createSignal, on, onCleanup } from "solid-js"
3
+ import type { TuiSlotPlugin, TuiPluginApi, TuiThemeCurrent } from "@opencode-ai/plugin/tui"
4
+ import { loadSidebarSnapshot, type SidebarSnapshot } from "../data/context-db"
5
+
6
+ const SINGLE_BORDER = { type: "single" } as any
7
+ const REFRESH_DEBOUNCE_MS = 150
8
+
9
+ function compactTokens(value: number): string {
10
+ if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`
11
+ if (value >= 1_000) return `${(value / 1_000).toFixed(0)}K`
12
+ return String(value)
13
+ }
14
+
15
+ function relativeTime(ms: number): string {
16
+ const diff = Date.now() - ms
17
+ if (diff < 60_000) return "just now"
18
+ if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`
19
+ if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`
20
+ return `${Math.floor(diff / 86_400_000)}d ago`
21
+ }
22
+
23
+ // Token breakdown segment colors (hardcoded hex values)
24
+ const COLORS = {
25
+ system: "#c084fc", // Purple-ish
26
+ compartments: "#60a5fa", // Blue-ish
27
+ facts: "#fbbf24", // Yellow/orange
28
+ memories: "#34d399", // Green
29
+ conversation: "#9ca3af", // Gray (will use theme.textMuted)
30
+ }
31
+
32
+ interface TokenSegment {
33
+ key: string
34
+ tokens: number
35
+ color: string
36
+ label: string
37
+ }
38
+
39
+ // Segmented token breakdown bar with legend
40
+ const TokenBreakdown = (props: {
41
+ theme: TuiThemeCurrent
42
+ snapshot: SidebarSnapshot
43
+ }) => {
44
+ const barWidth = 36
45
+
46
+ const segments = createMemo<TokenSegment[]>(() => {
47
+ const s = props.snapshot
48
+ const total = s.inputTokens || 1
49
+ const result: TokenSegment[] = []
50
+
51
+ // System Prompt (purple)
52
+ if (s.systemPromptTokens > 0) {
53
+ result.push({
54
+ key: "sys",
55
+ tokens: s.systemPromptTokens,
56
+ color: COLORS.system,
57
+ label: "System",
58
+ })
59
+ }
60
+
61
+ // Compartments (blue)
62
+ if (s.compartmentTokens > 0) {
63
+ result.push({
64
+ key: "comp",
65
+ tokens: s.compartmentTokens,
66
+ color: COLORS.compartments,
67
+ label: "Compartments",
68
+ })
69
+ }
70
+
71
+ // Facts (yellow/orange)
72
+ if (s.factTokens > 0) {
73
+ result.push({
74
+ key: "fact",
75
+ tokens: s.factTokens,
76
+ color: COLORS.facts,
77
+ label: "Facts",
78
+ })
79
+ }
80
+
81
+ // Memories (green)
82
+ if (s.memoryTokens > 0) {
83
+ result.push({
84
+ key: "mem",
85
+ tokens: s.memoryTokens,
86
+ color: COLORS.memories,
87
+ label: "Memories",
88
+ })
89
+ }
90
+
91
+ // Conversation = remaining tokens (gray)
92
+ const used = s.systemPromptTokens + s.compartmentTokens + s.factTokens + s.memoryTokens
93
+ const convTokens = Math.max(0, s.inputTokens - used)
94
+ if (convTokens > 0) {
95
+ result.push({
96
+ key: "conv",
97
+ tokens: convTokens,
98
+ color: props.theme.textMuted,
99
+ label: "Conversation",
100
+ })
101
+ }
102
+
103
+ return result
104
+ })
105
+
106
+ const totalTokens = createMemo(() => props.snapshot.inputTokens || 1)
107
+
108
+ // Calculate proportional widths for each segment
109
+ const segmentWidths = createMemo(() => {
110
+ const total = totalTokens()
111
+ const segs = segments()
112
+ if (segs.length === 0) return []
113
+
114
+ // Calculate raw proportions
115
+ const proportions = segs.map((seg) => seg.tokens / total)
116
+
117
+ // Convert to character widths (minimum 1 char if tokens > 0)
118
+ let widths = proportions.map((p) => Math.max(1, Math.round(p * barWidth)))
119
+
120
+ // Adjust to exactly barWidth
121
+ const sum = widths.reduce((a, b) => a + b, 0)
122
+ if (sum > barWidth) {
123
+ // Shrink from the largest segments
124
+ let excess = sum - barWidth
125
+ while (excess > 0) {
126
+ const maxIdx = widths.indexOf(Math.max(...widths))
127
+ if (widths[maxIdx] > 1) {
128
+ widths[maxIdx]--
129
+ excess--
130
+ } else {
131
+ break
132
+ }
133
+ }
134
+ } else if (sum < barWidth) {
135
+ // Expand the largest segments
136
+ let deficit = barWidth - sum
137
+ while (deficit > 0) {
138
+ const maxIdx = widths.indexOf(Math.max(...widths))
139
+ widths[maxIdx]++
140
+ deficit--
141
+ }
142
+ }
143
+
144
+ return widths
145
+ })
146
+
147
+ const barSegments = createMemo(() => {
148
+ const segs = segments()
149
+ const widths = segmentWidths()
150
+ return segs.map((seg, i) => ({
151
+ chars: "█".repeat(widths[i] || 0),
152
+ color: seg.color,
153
+ }))
154
+ })
155
+
156
+ return (
157
+ <box width="100%" flexDirection="column">
158
+ {/* Segmented bar */}
159
+ <box flexDirection="row">
160
+ {barSegments().map((seg, i) => (
161
+ <text key={i} fg={seg.color}>{seg.chars}</text>
162
+ ))}
163
+ </box>
164
+
165
+ {/* Legend rows */}
166
+ <box flexDirection="column" marginTop={0}>
167
+ {segments().map((seg) => {
168
+ const pct = ((seg.tokens / totalTokens()) * 100).toFixed(0)
169
+ return (
170
+ <box
171
+ key={seg.key}
172
+ width="100%"
173
+ flexDirection="row"
174
+ justifyContent="space-between"
175
+ >
176
+ <text fg={seg.color}>{seg.label}</text>
177
+ <text fg={props.theme.textMuted}>
178
+ {compactTokens(seg.tokens)} ({pct}%)
179
+ </text>
180
+ </box>
181
+ )
182
+ })}
183
+ </box>
184
+ </box>
185
+ )
186
+ }
187
+
188
+ const StatRow = (props: {
189
+ theme: TuiThemeCurrent
190
+ label: string
191
+ value: string
192
+ accent?: boolean
193
+ warning?: boolean
194
+ dim?: boolean
195
+ }) => {
196
+ const fg = createMemo(() => {
197
+ if (props.warning) return props.theme.warning
198
+ if (props.accent) return props.theme.accent
199
+ if (props.dim) return props.theme.textMuted
200
+ return props.theme.text
201
+ })
202
+
203
+ return (
204
+ <box width="100%" flexDirection="row" justifyContent="space-between">
205
+ <text fg={props.theme.textMuted}>{props.label}</text>
206
+ <text fg={fg()}>
207
+ <b>{props.value}</b>
208
+ </text>
209
+ </box>
210
+ )
211
+ }
212
+
213
+ const SectionHeader = (props: { theme: TuiThemeCurrent; title: string }) => (
214
+ <box width="100%" marginTop={1}>
215
+ <text fg={props.theme.text}>
216
+ <b>{props.title}</b>
217
+ </text>
218
+ </box>
219
+ )
220
+
221
+ const SidebarContent = (props: {
222
+ api: TuiPluginApi
223
+ sessionID: () => string
224
+ theme: TuiThemeCurrent
225
+ }) => {
226
+ const [snapshot, setSnapshot] = createSignal<SidebarSnapshot | null>(null)
227
+ let refreshTimer: ReturnType<typeof setTimeout> | undefined
228
+
229
+ const refresh = () => {
230
+ const sid = props.sessionID()
231
+ if (!sid) return
232
+ const directory = props.api.state.path.directory ?? ""
233
+ const data = loadSidebarSnapshot(sid, directory)
234
+ setSnapshot(data)
235
+ try {
236
+ props.api.renderer.requestRender()
237
+ } catch {
238
+ // Ignore render errors
239
+ }
240
+ }
241
+
242
+ const scheduleRefresh = () => {
243
+ if (refreshTimer) clearTimeout(refreshTimer)
244
+ refreshTimer = setTimeout(() => {
245
+ refreshTimer = undefined
246
+ refresh()
247
+ }, REFRESH_DEBOUNCE_MS)
248
+ }
249
+
250
+ onCleanup(() => {
251
+ if (refreshTimer) clearTimeout(refreshTimer)
252
+ })
253
+
254
+ // Refresh on session change
255
+ createEffect(
256
+ on(props.sessionID, () => {
257
+ refresh()
258
+ }),
259
+ )
260
+
261
+ // Subscribe to events for live updates
262
+ createEffect(
263
+ on(
264
+ props.sessionID,
265
+ (sessionID) => {
266
+ const unsubs = [
267
+ props.api.event.on("message.updated", (event) => {
268
+ if (event.properties.info.sessionID !== sessionID) return
269
+ scheduleRefresh()
270
+ }),
271
+ props.api.event.on("session.updated", (event) => {
272
+ if (event.properties.info.id !== sessionID) return
273
+ scheduleRefresh()
274
+ }),
275
+ props.api.event.on("message.removed", (event) => {
276
+ if (event.properties.sessionID !== sessionID) return
277
+ scheduleRefresh()
278
+ }),
279
+ ]
280
+
281
+ onCleanup(() => {
282
+ for (const unsub of unsubs) unsub()
283
+ })
284
+ },
285
+ { defer: false },
286
+ ),
287
+ )
288
+
289
+ const s = createMemo(() => snapshot())
290
+
291
+ return (
292
+ <box
293
+ width="100%"
294
+ flexDirection="column"
295
+ border={SINGLE_BORDER}
296
+ borderColor={props.theme.borderActive}
297
+ paddingTop={1}
298
+ paddingBottom={1}
299
+ paddingLeft={1}
300
+ paddingRight={1}
301
+ >
302
+ {/* Header */}
303
+ <box flexDirection="row" justifyContent="space-between" alignItems="center">
304
+ <box paddingLeft={1} paddingRight={1} backgroundColor={props.theme.accent}>
305
+ <text fg={props.theme.background}>
306
+ <b>Magic CTX</b>
307
+ </text>
308
+ </box>
309
+ <text fg={props.theme.success}>active</text>
310
+ </box>
311
+
312
+ {/* Token breakdown bar */}
313
+ {s() && s()!.inputTokens > 0 && (
314
+ <box marginTop={1}>
315
+ <TokenBreakdown theme={props.theme} snapshot={s()!} />
316
+ </box>
317
+ )}
318
+
319
+ {/* Historian section */}
320
+ <box width="100%" marginTop={1} flexDirection="row" justifyContent="space-between">
321
+ <text fg={props.theme.text}>
322
+ <b>Historian</b>
323
+ </text>
324
+ {s()?.historianRunning ? (
325
+ <text fg={props.theme.warning}>compacting ⟳</text>
326
+ ) : (
327
+ <text fg={props.theme.textMuted}>idle</text>
328
+ )}
329
+ </box>
330
+ <StatRow
331
+ theme={props.theme}
332
+ label="Compartments"
333
+ value={String(s()?.compartmentCount ?? 0)}
334
+ />
335
+ <StatRow
336
+ theme={props.theme}
337
+ label="Facts"
338
+ value={String(s()?.factCount ?? 0)}
339
+ />
340
+
341
+ {/* Memory section */}
342
+ <SectionHeader theme={props.theme} title="Memory" />
343
+ <StatRow
344
+ theme={props.theme}
345
+ label="Memories"
346
+ value={String(s()?.memoryCount ?? 0)}
347
+ accent
348
+ />
349
+ {(s()?.memoryBlockCount ?? 0) > 0 && (
350
+ <StatRow
351
+ theme={props.theme}
352
+ label="Injected"
353
+ value={String(s()!.memoryBlockCount)}
354
+ dim
355
+ />
356
+ )}
357
+
358
+ {/* Queue & Status */}
359
+ {((s()?.pendingOpsCount ?? 0) > 0 ||
360
+ (s()?.sessionNoteCount ?? 0) > 0 ||
361
+ (s()?.readySmartNoteCount ?? 0) > 0) && (
362
+ <>
363
+ <SectionHeader theme={props.theme} title="Status" />
364
+ {(s()?.pendingOpsCount ?? 0) > 0 && (
365
+ <StatRow
366
+ theme={props.theme}
367
+ label="Queue"
368
+ value={`${s()!.pendingOpsCount} pending`}
369
+ warning
370
+ />
371
+ )}
372
+ {(s()?.sessionNoteCount ?? 0) > 0 && (
373
+ <StatRow
374
+ theme={props.theme}
375
+ label="Notes"
376
+ value={String(s()!.sessionNoteCount)}
377
+ />
378
+ )}
379
+ {(s()?.readySmartNoteCount ?? 0) > 0 && (
380
+ <StatRow
381
+ theme={props.theme}
382
+ label="Smart Notes"
383
+ value={`${s()!.readySmartNoteCount} ready`}
384
+ accent
385
+ />
386
+ )}
387
+ </>
388
+ )}
389
+
390
+ {/* Dreamer */}
391
+ {s()?.lastDreamerRunAt && (
392
+ <>
393
+ <SectionHeader theme={props.theme} title="Dreamer" />
394
+ <StatRow
395
+ theme={props.theme}
396
+ label="Last run"
397
+ value={relativeTime(s()!.lastDreamerRunAt!)}
398
+ dim
399
+ />
400
+ </>
401
+ )}
402
+ </box>
403
+ )
404
+ }
405
+
406
+ export function createSidebarContentSlot(api: TuiPluginApi): TuiSlotPlugin {
407
+ return {
408
+ order: 150,
409
+ slots: {
410
+ sidebar_content: (ctx, value) => {
411
+ const theme = createMemo(() => ctx.theme.current)
412
+ return (
413
+ <SidebarContent
414
+ api={api}
415
+ sessionID={() => value.session_id}
416
+ theme={theme()}
417
+ />
418
+ )
419
+ },
420
+ },
421
+ }
422
+ }
@@ -0,0 +1,232 @@
1
+ // Type declarations for @opencode-ai/plugin/tui
2
+ // These types are not yet exported by the installed @opencode-ai/plugin package
3
+
4
+ declare module "@opencode-ai/plugin/tui" {
5
+ import type {
6
+ createOpencodeClient as createOpencodeClientV2,
7
+ Event as TuiEvent,
8
+ Message,
9
+ Part,
10
+ Provider,
11
+ Config as SdkConfig,
12
+ } from "@opencode-ai/sdk/v2";
13
+
14
+ import type { CliRenderer, RGBA } from "@opentui/core";
15
+ import type { JSX, SolidPlugin } from "@opentui/solid";
16
+
17
+ type PluginOptions = Record<string, unknown>;
18
+
19
+ export type { CliRenderer };
20
+
21
+ export type TuiThemeCurrent = {
22
+ readonly primary: RGBA;
23
+ readonly secondary: RGBA;
24
+ readonly accent: RGBA;
25
+ readonly error: RGBA;
26
+ readonly warning: RGBA;
27
+ readonly success: RGBA;
28
+ readonly info: RGBA;
29
+ readonly text: RGBA;
30
+ readonly textMuted: RGBA;
31
+ readonly background: RGBA;
32
+ readonly backgroundPanel: RGBA;
33
+ readonly backgroundElement: RGBA;
34
+ readonly backgroundMenu: RGBA;
35
+ readonly border: RGBA;
36
+ readonly borderActive: RGBA;
37
+ readonly borderSubtle: RGBA;
38
+ [key: string]: unknown;
39
+ };
40
+
41
+ export type TuiTheme = {
42
+ readonly current: TuiThemeCurrent;
43
+ has: (name: string) => boolean;
44
+ set: (name: string) => boolean;
45
+ mode: () => "dark" | "light";
46
+ readonly ready: boolean;
47
+ };
48
+
49
+ export type TuiSlotMap = {
50
+ app: Record<string, never>;
51
+ home_logo: Record<string, never>;
52
+ home_bottom: Record<string, never>;
53
+ sidebar_title: {
54
+ session_id: string;
55
+ title: string;
56
+ share_url?: string;
57
+ };
58
+ sidebar_content: {
59
+ session_id: string;
60
+ };
61
+ sidebar_footer: {
62
+ session_id: string;
63
+ };
64
+ };
65
+
66
+ export type TuiSlotContext = {
67
+ theme: TuiTheme;
68
+ };
69
+
70
+ export type TuiSlotPlugin = Omit<SolidPlugin<TuiSlotMap, TuiSlotContext>, "id"> & {
71
+ id?: never;
72
+ };
73
+
74
+ export type TuiToast = {
75
+ variant?: "info" | "success" | "warning" | "error";
76
+ title?: string;
77
+ message: string;
78
+ duration?: number;
79
+ };
80
+
81
+ export type TuiDialogStack = {
82
+ replace: (render: () => JSX.Element, onClose?: () => void) => void;
83
+ clear: () => void;
84
+ setSize: (size: "medium" | "large" | "xlarge") => void;
85
+ readonly size: "medium" | "large" | "xlarge";
86
+ readonly depth: number;
87
+ readonly open: boolean;
88
+ };
89
+
90
+ export type TuiDialogAlertProps = {
91
+ title: string;
92
+ message: string;
93
+ onConfirm?: () => void;
94
+ };
95
+
96
+ export type TuiDialogConfirmProps = {
97
+ title: string;
98
+ message: string;
99
+ onConfirm?: () => void;
100
+ onCancel?: () => void;
101
+ };
102
+
103
+ export type TuiDialogPromptProps = {
104
+ title: string;
105
+ description?: () => JSX.Element;
106
+ placeholder?: string;
107
+ value?: string;
108
+ busy?: boolean;
109
+ busyText?: string;
110
+ onConfirm?: (value: string) => void;
111
+ onCancel?: () => void;
112
+ };
113
+
114
+ export type TuiDialogSelectOption<Value = unknown> = {
115
+ title: string;
116
+ value: Value;
117
+ description?: string;
118
+ footer?: JSX.Element | string;
119
+ category?: string;
120
+ disabled?: boolean;
121
+ onSelect?: () => void;
122
+ };
123
+
124
+ export type TuiDialogSelectProps<Value = unknown> = {
125
+ title: string;
126
+ placeholder?: string;
127
+ options: TuiDialogSelectOption<Value>[];
128
+ flat?: boolean;
129
+ onMove?: (option: TuiDialogSelectOption<Value>) => void;
130
+ onFilter?: (query: string) => void;
131
+ onSelect?: (option: TuiDialogSelectOption<Value>) => void;
132
+ skipFilter?: boolean;
133
+ current?: Value;
134
+ };
135
+
136
+ export type TuiState = {
137
+ readonly ready: boolean;
138
+ readonly config: SdkConfig;
139
+ readonly provider: ReadonlyArray<Provider>;
140
+ readonly path: {
141
+ state: string;
142
+ config: string;
143
+ worktree: string;
144
+ directory: string;
145
+ };
146
+ session: {
147
+ count: () => number;
148
+ messages: (sessionID: string) => ReadonlyArray<Message>;
149
+ };
150
+ part: (messageID: string) => ReadonlyArray<Part>;
151
+ };
152
+
153
+ export type TuiEventBus = {
154
+ on: <Type extends TuiEvent["type"]>(
155
+ type: Type,
156
+ handler: (event: Extract<TuiEvent, { type: Type }>) => void,
157
+ ) => () => void;
158
+ };
159
+
160
+ export type TuiLifecycle = {
161
+ readonly signal: AbortSignal;
162
+ onDispose: (fn: () => void | Promise<void>) => () => void;
163
+ };
164
+
165
+ export type TuiPluginApi = {
166
+ app: { readonly version: string };
167
+ command: {
168
+ register: (
169
+ cb: () => Array<{
170
+ title: string;
171
+ value: string;
172
+ description?: string;
173
+ category?: string;
174
+ keybind?: string;
175
+ suggested?: boolean;
176
+ hidden?: boolean;
177
+ enabled?: boolean;
178
+ slash?: {
179
+ name: string;
180
+ aliases?: string[];
181
+ };
182
+ onSelect?: () => void;
183
+ }>,
184
+ ) => () => void;
185
+ trigger: (value: string) => void;
186
+ };
187
+ route: {
188
+ register: (
189
+ routes: Array<{
190
+ name: string;
191
+ render: (input: { params?: Record<string, unknown> }) => JSX.Element;
192
+ }>,
193
+ ) => () => void;
194
+ navigate: (name: string, params?: Record<string, unknown>) => void;
195
+ readonly current:
196
+ | { name: "home" }
197
+ | { name: "session"; params: { sessionID: string; initialPrompt?: unknown } }
198
+ | { name: string; params?: Record<string, unknown> };
199
+ };
200
+ ui: {
201
+ DialogAlert: (props: TuiDialogAlertProps) => JSX.Element;
202
+ DialogConfirm: (props: TuiDialogConfirmProps) => JSX.Element;
203
+ DialogPrompt: (props: TuiDialogPromptProps) => JSX.Element;
204
+ DialogSelect: <Value = unknown>(props: TuiDialogSelectProps<Value>) => JSX.Element;
205
+ toast: (input: TuiToast) => void;
206
+ dialog: TuiDialogStack;
207
+ };
208
+ state: TuiState;
209
+ theme: TuiTheme;
210
+ client: ReturnType<typeof createOpencodeClientV2>;
211
+ event: TuiEventBus;
212
+ renderer: CliRenderer;
213
+ slots: {
214
+ register: (plugin: TuiSlotPlugin) => string;
215
+ };
216
+ lifecycle: TuiLifecycle;
217
+ };
218
+
219
+ export type TuiPluginMeta = {
220
+ state: "first" | "updated" | "same";
221
+ id: string;
222
+ source: "file" | "npm" | "internal";
223
+ spec: string;
224
+ target: string;
225
+ };
226
+
227
+ export type TuiPlugin = (
228
+ api: TuiPluginApi,
229
+ options: PluginOptions | undefined,
230
+ meta: TuiPluginMeta,
231
+ ) => Promise<void>;
232
+ }