@meowlynxsea/koi 0.1.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 (109) hide show
  1. package/LICENSE +34 -0
  2. package/NOTICE +35 -0
  3. package/README.md +15 -0
  4. package/bin/koi +12 -0
  5. package/dist/highlights-eq9cgrbb.scm +604 -0
  6. package/dist/highlights-ghv9g403.scm +205 -0
  7. package/dist/highlights-hk7bwhj4.scm +284 -0
  8. package/dist/highlights-r812a2qc.scm +150 -0
  9. package/dist/highlights-x6tmsnaa.scm +115 -0
  10. package/dist/injections-73j83es3.scm +27 -0
  11. package/dist/main.js +489918 -0
  12. package/dist/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
  13. package/dist/tree-sitter-markdown-411r6y9b.wasm +0 -0
  14. package/dist/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
  15. package/dist/tree-sitter-typescript-zxjzwt75.wasm +0 -0
  16. package/dist/tree-sitter-zig-e78zbjpm.wasm +0 -0
  17. package/package.json +51 -0
  18. package/src/agent/check-permissions.ts +239 -0
  19. package/src/agent/hooks/message-utils.ts +305 -0
  20. package/src/agent/hooks/types.ts +32 -0
  21. package/src/agent/hooks.ts +1560 -0
  22. package/src/agent/mode.ts +163 -0
  23. package/src/agent/monitor-registry.ts +308 -0
  24. package/src/agent/permission-ui.ts +71 -0
  25. package/src/agent/plan-ui.ts +74 -0
  26. package/src/agent/question-ui.ts +58 -0
  27. package/src/agent/session-fork.ts +299 -0
  28. package/src/agent/session-snapshots.ts +216 -0
  29. package/src/agent/session-store.ts +649 -0
  30. package/src/agent/session-tasks.ts +305 -0
  31. package/src/agent/session.ts +27 -0
  32. package/src/agent/subagent-registry.ts +176 -0
  33. package/src/agent/subagent.ts +194 -0
  34. package/src/agent/tool-orchestration.ts +55 -0
  35. package/src/agent/tools.ts +8 -0
  36. package/src/cli/args.ts +6 -0
  37. package/src/cli/commands.ts +5 -0
  38. package/src/commands/skills/index.ts +23 -0
  39. package/src/config/models.ts +6 -0
  40. package/src/config/settings.ts +392 -0
  41. package/src/main.tsx +64 -0
  42. package/src/services/mcp/client.ts +194 -0
  43. package/src/services/mcp/config.ts +232 -0
  44. package/src/services/mcp/connection-manager.ts +258 -0
  45. package/src/services/mcp/index.ts +80 -0
  46. package/src/services/mcp/mcp-commands.ts +114 -0
  47. package/src/services/mcp/stdio-transport.ts +246 -0
  48. package/src/services/mcp/types.ts +155 -0
  49. package/src/skills/SkillsMenu.tsx +370 -0
  50. package/src/skills/bundled/batch.ts +106 -0
  51. package/src/skills/bundled/debug.ts +86 -0
  52. package/src/skills/bundled/loremIpsum.ts +101 -0
  53. package/src/skills/bundled/remember.ts +97 -0
  54. package/src/skills/bundled/simplify.ts +100 -0
  55. package/src/skills/bundled/skillify.ts +123 -0
  56. package/src/skills/bundled/stuck.ts +101 -0
  57. package/src/skills/bundled/updateConfig.ts +228 -0
  58. package/src/skills/bundled.ts +46 -0
  59. package/src/skills/frontmatter.ts +179 -0
  60. package/src/skills/index.ts +87 -0
  61. package/src/skills/invoke.ts +231 -0
  62. package/src/skills/loader.ts +710 -0
  63. package/src/skills/substitution.ts +169 -0
  64. package/src/skills/types.ts +201 -0
  65. package/src/tools/agent.ts +143 -0
  66. package/src/tools/ask-user-question.ts +46 -0
  67. package/src/tools/bash.ts +148 -0
  68. package/src/tools/edit.ts +164 -0
  69. package/src/tools/glob.ts +102 -0
  70. package/src/tools/grep.ts +248 -0
  71. package/src/tools/index.ts +73 -0
  72. package/src/tools/list-mcp-resources.ts +74 -0
  73. package/src/tools/ls.ts +85 -0
  74. package/src/tools/mcp.ts +76 -0
  75. package/src/tools/monitor.ts +159 -0
  76. package/src/tools/plan-mode.ts +134 -0
  77. package/src/tools/read-mcp-resource.ts +79 -0
  78. package/src/tools/read.ts +137 -0
  79. package/src/tools/skill.ts +176 -0
  80. package/src/tools/task.ts +349 -0
  81. package/src/tools/types.ts +52 -0
  82. package/src/tools/webfetch-domains.ts +239 -0
  83. package/src/tools/webfetch.ts +533 -0
  84. package/src/tools/write.ts +101 -0
  85. package/src/tui/app.tsx +1178 -0
  86. package/src/tui/components/chat-panel.tsx +1071 -0
  87. package/src/tui/components/command-panel.tsx +261 -0
  88. package/src/tui/components/confirm-modal.tsx +135 -0
  89. package/src/tui/components/connect-modal.tsx +435 -0
  90. package/src/tui/components/connecting-modal.tsx +167 -0
  91. package/src/tui/components/edit-pending-modal.tsx +103 -0
  92. package/src/tui/components/exit-modal.tsx +131 -0
  93. package/src/tui/components/fork-modal.tsx +377 -0
  94. package/src/tui/components/image-preview-modal.tsx +141 -0
  95. package/src/tui/components/image-utils.ts +128 -0
  96. package/src/tui/components/info-bar.tsx +103 -0
  97. package/src/tui/components/input-box.tsx +352 -0
  98. package/src/tui/components/mcp/MCPSettings.tsx +386 -0
  99. package/src/tui/components/mcp/index.ts +7 -0
  100. package/src/tui/components/model-modal.tsx +310 -0
  101. package/src/tui/components/pending-area.tsx +88 -0
  102. package/src/tui/components/rename-modal.tsx +119 -0
  103. package/src/tui/components/session-modal.tsx +233 -0
  104. package/src/tui/components/side-bar.tsx +349 -0
  105. package/src/tui/components/tool-output.ts +6 -0
  106. package/src/tui/hooks/user-prompt-history.ts +114 -0
  107. package/src/tui/theme.ts +63 -0
  108. package/src/types/commands.ts +80 -0
  109. package/src/types/cross-spawn.d.ts +24 -0
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Tool Orchestration — 工具调度器
3
+ *
4
+ * Manages execution ordering: read-only tools run concurrently,
5
+ * write tools are serialized via an internal mutex.
6
+ *
7
+ * All Koi custom tools register with executionMode: "parallel" to avoid
8
+ * Pi's all-sequential fallback, then write tools self-coordinate here.
9
+ */
10
+
11
+ import { isReadOnlyTool } from "../tools/types.js";
12
+
13
+ /** 写入工具互斥锁 — Promise 链实现串行队列 */
14
+ export class WriteToolMutex {
15
+ private queue: Promise<void> = Promise.resolve();
16
+
17
+ async acquire(): Promise<() => void> {
18
+ let release: (() => void) | undefined;
19
+ const promise = new Promise<void>((resolve) => {
20
+ release = resolve;
21
+ });
22
+ const previous = this.queue;
23
+ this.queue = this.queue.then(() => promise);
24
+ await previous;
25
+ return release!;
26
+ }
27
+ }
28
+
29
+ export const globalWriteMutex = new WriteToolMutex();
30
+
31
+ /** 分类工具调用为只读 / 写入两组 */
32
+ export function classifyToolCalls<T extends { name: string }>(
33
+ calls: T[]
34
+ ): { readonly: T[]; write: T[] } {
35
+ const readonly: T[] = [];
36
+ const write: T[] = [];
37
+ for (const call of calls) {
38
+ if (isReadOnlyTool(call.name)) {
39
+ readonly.push(call);
40
+ } else {
41
+ write.push(call);
42
+ }
43
+ }
44
+ return { readonly, write };
45
+ }
46
+
47
+ /** 在 execute() 内部包装写入工具,确保串行 */
48
+ export async function withWriteLock<T>(fn: () => Promise<T>): Promise<T> {
49
+ const release = await globalWriteMutex.acquire();
50
+ try {
51
+ return await fn();
52
+ } finally {
53
+ release();
54
+ }
55
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Custom Tool Registry
3
+ *
4
+ * Pi coding tools are loaded automatically via createAgentSession.
5
+ * Custom koi-specific tools can be registered here in the future.
6
+ */
7
+
8
+ export {};
@@ -0,0 +1,6 @@
1
+ /**
2
+ * CLI Argument Parser
3
+ *
4
+ * Uses commander (or custom parser) to define flags, options,
5
+ * and sub-commands for the koi binary.
6
+ */
@@ -0,0 +1,5 @@
1
+ /**
2
+ * CLI Commands Registry
3
+ *
4
+ * Defines all sub-commands (e.g. `koi chat`, `koi config`, `koi export`).
5
+ */
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Skills Command Entry Point
3
+ *
4
+ * Registers the /skills command for the command palette.
5
+ */
6
+
7
+ import type { Command } from "../../types/commands.js";
8
+
9
+ const skillsCommand: Command = {
10
+ id: "skills",
11
+ name: "Skills",
12
+ description: "List and manage available skills",
13
+ keywords: ["skill", "skills", "commands", "slash"],
14
+ action: async (context) => {
15
+ // This will be handled by the app's command panel
16
+ if (context.onOpenSkillsModal) {
17
+ context.onOpenSkillsModal();
18
+ }
19
+ return { success: true };
20
+ },
21
+ };
22
+
23
+ export default skillsCommand;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Model Registry
3
+ *
4
+ * Wraps Pi's ModelRegistry with custom model aliases, fallback chains,
5
+ * and provider-specific authentication.
6
+ */
@@ -0,0 +1,392 @@
1
+ /**
2
+ * Settings / Configuration Manager
3
+ *
4
+ * Persists user preferences and bridges to Pi infrastructure:
5
+ * - Koi settings: session title, current model reference, provider configs
6
+ * - Pi AuthStorage: credential storage for agent session
7
+ * - Pi ModelRegistry: model discovery and API key resolution
8
+ * - Pi SettingsManager: compaction, retry, and runtime settings
9
+ */
10
+
11
+ import fs from "fs";
12
+ import path from "path";
13
+ import os from "os";
14
+ import {
15
+ getProviders,
16
+ getModels,
17
+ completeSimple,
18
+ type KnownProvider,
19
+ type Model,
20
+ type Api,
21
+ } from "@mariozechner/pi-ai";
22
+ import { getErrorMessage } from "../tools/types.js";
23
+ import { getOAuthProvider } from "@mariozechner/pi-ai/oauth";
24
+ import {
25
+ AuthStorage,
26
+ ModelRegistry,
27
+ SettingsManager,
28
+ } from "@mariozechner/pi-coding-agent";
29
+
30
+ export interface ModelRef {
31
+ provider: string;
32
+ modelId: string;
33
+ }
34
+
35
+ export type AuthMethod = "apikey" | "oauth";
36
+
37
+ export interface ProviderConfig {
38
+ provider: string;
39
+ authMethod: AuthMethod;
40
+ credential: string; // api key or oauth token
41
+ }
42
+
43
+ interface SettingsFile {
44
+ version: number;
45
+ sessionTitle: string;
46
+ providers: Record<string, ProviderConfig>;
47
+ currentModel: ModelRef | null;
48
+ auxiliaryModel?: ModelRef | null;
49
+ }
50
+
51
+ const CONFIG_DIR = path.join(os.homedir(), ".config", "koi");
52
+ const SETTINGS_PATH = path.join(CONFIG_DIR, "settings.json");
53
+ const PI_AGENT_DIR = path.join(CONFIG_DIR, "pi");
54
+
55
+ let sessionTitle = "New Session";
56
+ let providerConfigs = new Map<string, ProviderConfig>();
57
+ let currentModel: ModelRef | null = null;
58
+ let auxiliaryModel: ModelRef | null = null;
59
+
60
+ // Pi infrastructure (lazy-initialized)
61
+ let piAuthStorage: AuthStorage | null = null;
62
+ let piModelRegistry: ModelRegistry | null = null;
63
+ let piSettingsManager: SettingsManager | null = null;
64
+
65
+ function ensureConfigDir(): void {
66
+ if (!fs.existsSync(CONFIG_DIR)) {
67
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
68
+ }
69
+ if (!fs.existsSync(PI_AGENT_DIR)) {
70
+ fs.mkdirSync(PI_AGENT_DIR, { recursive: true, mode: 0o700 });
71
+ }
72
+ }
73
+
74
+ function initPiInfrastructure(): void {
75
+ if (piAuthStorage && piModelRegistry && piSettingsManager) return;
76
+ ensureConfigDir();
77
+ piAuthStorage = AuthStorage.create(path.join(PI_AGENT_DIR, "auth.json"));
78
+ piModelRegistry = ModelRegistry.create(
79
+ piAuthStorage,
80
+ path.join(PI_AGENT_DIR, "models.json")
81
+ );
82
+ piSettingsManager = SettingsManager.create(process.cwd(), PI_AGENT_DIR);
83
+ }
84
+
85
+ function syncCredentialsToPi(): void {
86
+ if (!piAuthStorage) return;
87
+ for (const [provider, config] of providerConfigs) {
88
+ if (config.authMethod === "apikey") {
89
+ piAuthStorage.set(provider, { type: "api_key", key: config.credential });
90
+ } else if (config.authMethod === "oauth") {
91
+ // OAuth tokens from koi settings lack refresh token; store as api_key
92
+ // so ModelRegistry can resolve them without OAuth refresh flow.
93
+ piAuthStorage.set(provider, { type: "api_key", key: config.credential });
94
+ }
95
+ }
96
+ }
97
+
98
+ /* ───────── Pi infrastructure accessors ───────── */
99
+
100
+ export function getPiAuthStorage(): AuthStorage {
101
+ initPiInfrastructure();
102
+ return piAuthStorage!;
103
+ }
104
+
105
+ export function getPiModelRegistry(): ModelRegistry {
106
+ initPiInfrastructure();
107
+ piModelRegistry!.refresh();
108
+ return piModelRegistry!;
109
+ }
110
+
111
+ export function getPiSettingsManager(): SettingsManager {
112
+ initPiInfrastructure();
113
+ return piSettingsManager!;
114
+ }
115
+
116
+ /* ───────── Pi model resolution ───────── */
117
+
118
+ export function getCurrentPiModel(): Model<Api> | undefined {
119
+ const ref = getCurrentModel();
120
+ if (!ref) return undefined;
121
+ return getPiModelRegistry().find(ref.provider, ref.modelId);
122
+ }
123
+
124
+ export function getAvailablePiModels(): Model<Api>[] {
125
+ return getPiModelRegistry().getAvailable();
126
+ }
127
+
128
+ export function resolvePiModel(ref: ModelRef): Model<Api> | undefined {
129
+ return getPiModelRegistry().find(ref.provider, ref.modelId);
130
+ }
131
+
132
+ /* ───────── Pi SettingsManager proxies ───────── */
133
+
134
+ export function getCompactionSettings() {
135
+ return getPiSettingsManager().getCompactionSettings();
136
+ }
137
+
138
+ export function setCompactionEnabled(enabled: boolean) {
139
+ getPiSettingsManager().setCompactionEnabled(enabled);
140
+ }
141
+
142
+ export function getRetrySettings() {
143
+ return getPiSettingsManager().getRetrySettings();
144
+ }
145
+
146
+ export function setRetryEnabled(enabled: boolean) {
147
+ getPiSettingsManager().setRetryEnabled(enabled);
148
+ }
149
+
150
+ /* ───────── Koi settings I/O ───────── */
151
+
152
+ export function saveSettings(): void {
153
+ try {
154
+ ensureConfigDir();
155
+ const data: SettingsFile = {
156
+ version: 1,
157
+ sessionTitle,
158
+ providers: Object.fromEntries(providerConfigs),
159
+ currentModel,
160
+ auxiliaryModel,
161
+ };
162
+ const json = JSON.stringify(data, null, 2);
163
+ fs.writeFileSync(SETTINGS_PATH, json + "\n", { mode: 0o600 });
164
+ fs.chmodSync(SETTINGS_PATH, 0o600);
165
+ } catch {
166
+ // Silently ignore write errors so the TUI never crashes on save.
167
+ }
168
+ }
169
+
170
+ export function loadSettings(): void {
171
+ initPiInfrastructure();
172
+
173
+ try {
174
+ if (!fs.existsSync(SETTINGS_PATH)) {
175
+ return;
176
+ }
177
+ const raw = fs.readFileSync(SETTINGS_PATH, "utf-8");
178
+ const data = JSON.parse(raw) as SettingsFile;
179
+
180
+ if (data.sessionTitle) {
181
+ sessionTitle = data.sessionTitle;
182
+ }
183
+ if (data.providers) {
184
+ providerConfigs = new Map(Object.entries(data.providers));
185
+ }
186
+ if (data.currentModel) {
187
+ currentModel = data.currentModel;
188
+ }
189
+ if (data.auxiliaryModel) {
190
+ auxiliaryModel = data.auxiliaryModel;
191
+ }
192
+ syncCredentialsToPi();
193
+ } catch {
194
+ // If the file is missing, corrupt, or unreadable, start fresh.
195
+ }
196
+ }
197
+
198
+ /* ───────── Session title ───────── */
199
+
200
+ export function getSessionTitle(): string {
201
+ return sessionTitle;
202
+ }
203
+
204
+ export function setSessionTitle(title: string): void {
205
+ sessionTitle = title;
206
+ saveSettings();
207
+ }
208
+
209
+ /* ───────── Provider configuration ───────── */
210
+
211
+ export function configureProvider(config: ProviderConfig): void {
212
+ providerConfigs.set(config.provider, config);
213
+ saveSettings();
214
+ // Sync to Pi AuthStorage so agent sessions can resolve API keys
215
+ if (config.authMethod === "apikey") {
216
+ getPiAuthStorage().set(config.provider, {
217
+ type: "api_key",
218
+ key: config.credential,
219
+ });
220
+ } else {
221
+ getPiAuthStorage().set(config.provider, {
222
+ type: "api_key",
223
+ key: config.credential,
224
+ });
225
+ }
226
+ }
227
+
228
+ export function removeProvider(provider: string): void {
229
+ providerConfigs.delete(provider);
230
+ saveSettings();
231
+ getPiAuthStorage().remove(provider);
232
+ }
233
+
234
+ export function isProviderConfigured(provider: string): boolean {
235
+ return providerConfigs.has(provider);
236
+ }
237
+
238
+ export function getProviderConfig(
239
+ provider: string
240
+ ): ProviderConfig | undefined {
241
+ return providerConfigs.get(provider);
242
+ }
243
+
244
+ export function getConfiguredProviders(): string[] {
245
+ return Array.from(providerConfigs.keys());
246
+ }
247
+
248
+ /* ───────── Current model (koi reference) ───────── */
249
+
250
+ export function getCurrentModel(): ModelRef | null {
251
+ return currentModel;
252
+ }
253
+
254
+ export function setCurrentModel(ref: ModelRef | null): void {
255
+ currentModel = ref;
256
+ saveSettings();
257
+ }
258
+
259
+ export function getAuxiliaryModel(): ModelRef | null {
260
+ return auxiliaryModel;
261
+ }
262
+
263
+ export function setAuxiliaryModel(ref: ModelRef | null): void {
264
+ auxiliaryModel = ref;
265
+ saveSettings();
266
+ }
267
+
268
+ /* ───────── Model discovery (via pi-ai, for modals) ───────── */
269
+
270
+ export function getAllProviders(): string[] {
271
+ return getProviders();
272
+ }
273
+
274
+ export function getProviderModels(provider: string): Model<Api>[] {
275
+ return getModels(provider as KnownProvider);
276
+ }
277
+
278
+ /* ───────── Credential validation ───────── */
279
+
280
+ export async function validateProviderCredential(
281
+ provider: string,
282
+ credential: string
283
+ ): Promise<{ valid: boolean; error?: string }> {
284
+ try {
285
+ const models = getModels(provider as KnownProvider);
286
+ if (!models || models.length === 0) {
287
+ return { valid: false, error: "No models available for this provider" };
288
+ }
289
+
290
+ let model: Model<Api> = models[0]!;
291
+ let apiKey = credential;
292
+
293
+ const oauthProvider = getOAuthProvider(provider);
294
+ if (oauthProvider) {
295
+ const fakeCreds = {
296
+ access: credential,
297
+ refresh: credential,
298
+ expires: Date.now() + 86400000,
299
+ };
300
+ apiKey = oauthProvider.getApiKey(fakeCreds);
301
+ if (oauthProvider.modifyModels) {
302
+ const modified = oauthProvider.modifyModels(models, fakeCreds);
303
+ if (modified.length > 0) {
304
+ model = modified[0]!;
305
+ }
306
+ }
307
+ }
308
+
309
+ const message = await completeSimple(
310
+ model,
311
+ {
312
+ messages: [{ role: "user", content: "Hi", timestamp: Date.now() }],
313
+ },
314
+ {
315
+ apiKey,
316
+ maxTokens: 1,
317
+ maxRetries: 0,
318
+ timeoutMs: 15000,
319
+ }
320
+ );
321
+
322
+ if (message.stopReason === "error" || message.stopReason === "aborted") {
323
+ return {
324
+ valid: false,
325
+ error: message.errorMessage || "API request failed",
326
+ };
327
+ }
328
+
329
+ return { valid: true };
330
+ } catch (err: unknown) {
331
+ return { valid: false, error: getErrorMessage(err) };
332
+ }
333
+ }
334
+
335
+ /* ───────── Auxiliary model call ───────── */
336
+
337
+ /**
338
+ * Call the auxiliary model with a system prompt and user messages.
339
+ * Returns the model's text response or null if the model is not configured or call fails.
340
+ */
341
+ export async function callAuxiliaryModel(
342
+ systemPrompt: string,
343
+ userMessages: Array<{ role: "user"; content: string; timestamp: number }>
344
+ ): Promise<string | null> {
345
+ const ref = getAuxiliaryModel();
346
+ if (!ref) {
347
+ return null;
348
+ }
349
+
350
+ const model = resolvePiModel(ref);
351
+ if (!model) {
352
+ return null;
353
+ }
354
+
355
+ const registry = getPiModelRegistry();
356
+ const auth = await registry.getApiKeyAndHeaders(model);
357
+ if (!auth.ok) {
358
+ return null;
359
+ }
360
+
361
+ try {
362
+ const result = await completeSimple(
363
+ model,
364
+ {
365
+ systemPrompt,
366
+ messages: userMessages,
367
+ },
368
+ {
369
+ apiKey: auth.apiKey,
370
+ headers: auth.headers,
371
+ maxTokens: 1024,
372
+ maxRetries: 1,
373
+ timeoutMs: 30000,
374
+ }
375
+ );
376
+
377
+ fs.appendFileSync("/tmp/koi-debug.log", `[callAuxiliaryModel] result.stopReason: ${result.stopReason}, content: ${JSON.stringify(result.content)}\n`);
378
+
379
+ if (result.stopReason === "error" || result.stopReason === "aborted") {
380
+ return null;
381
+ }
382
+
383
+ const text = result.content
384
+ .filter((block): block is { type: "text"; text: string } => block.type === "text")
385
+ .map((block) => block.text)
386
+ .join("");
387
+ fs.appendFileSync("/tmp/koi-debug.log", `[callAuxiliaryModel] joined text: "${text}"\n`);
388
+ return text;
389
+ } catch {
390
+ return null;
391
+ }
392
+ }
package/src/main.tsx ADDED
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Koi - Main Entry Point
3
+ *
4
+ * Bootstraps the OpenTUI React application.
5
+ */
6
+
7
+ import { createCliRenderer } from "@opentui/core";
8
+ import { createRoot } from "@opentui/react";
9
+ import { DialogProvider } from "@opentui-ui/dialog/react";
10
+ import { App } from "./tui/app.js";
11
+ import { loadSettings } from "./config/settings.js";
12
+
13
+ export async function main(): Promise<void> {
14
+ loadSettings();
15
+ const renderer = await createCliRenderer({ exitOnCtrlC: false });
16
+ createRoot(renderer).render(
17
+ <DialogProvider>
18
+ <App
19
+ onExit={() => {
20
+ renderer.destroy();
21
+ process.exit(0);
22
+ }}
23
+ />
24
+ </DialogProvider>
25
+ );
26
+
27
+ // Ensure terminal state is restored on unexpected exits
28
+ const cleanup = () => {
29
+ try {
30
+ renderer.destroy();
31
+ } catch {
32
+ // ignore cleanup errors during shutdown
33
+ }
34
+ };
35
+ process.on("exit", cleanup);
36
+ process.on("SIGINT", () => {
37
+ cleanup();
38
+ process.exit(0);
39
+ });
40
+ process.on("SIGTERM", () => {
41
+ cleanup();
42
+ process.exit(0);
43
+ });
44
+ process.on("uncaughtException", (err) => {
45
+ cleanup();
46
+ console.error(err);
47
+ process.exit(1);
48
+ });
49
+ process.on("unhandledRejection", (reason) => {
50
+ cleanup();
51
+ console.error("Unhandled rejection:", reason);
52
+ process.exit(1);
53
+ });
54
+
55
+ // Keep process alive until exit
56
+ await new Promise(() => {});
57
+ }
58
+
59
+ if (import.meta.main) {
60
+ main().catch((err: unknown) => {
61
+ console.error(err);
62
+ process.exit(1);
63
+ });
64
+ }