aegiscode 3.1.8 → 3.1.10

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 (206) hide show
  1. package/README.md +23 -15
  2. package/dist/main.js +549 -552
  3. package/package.json +4 -2
  4. package/src/agent/Agent.ts +903 -0
  5. package/src/agent/SimpleAgent.ts +48 -0
  6. package/src/agent/index.ts +54 -0
  7. package/src/agent/orchestrator/AppBuilder.ts +443 -0
  8. package/src/agent/orchestrator/CouncilAgent.ts +310 -0
  9. package/src/agent/orchestrator/DiscussionRoom.ts +462 -0
  10. package/src/agent/orchestrator/OrchestratorAgent.ts +637 -0
  11. package/src/agent/orchestrator/index.ts +38 -0
  12. package/src/agent/orchestrator/utils.ts +397 -0
  13. package/src/agent/pricing.ts +115 -0
  14. package/src/agent/router.ts +74 -0
  15. package/src/agent/routerStats.ts +121 -0
  16. package/src/agent/types.ts +318 -0
  17. package/src/auth/login.ts +383 -0
  18. package/src/cli/config.ts +189 -0
  19. package/src/cli/index.ts +17 -0
  20. package/src/cli/middleware.ts +119 -0
  21. package/src/cli/types.ts +75 -0
  22. package/src/config/ConfigManager.ts +587 -0
  23. package/src/config/index.ts +7 -0
  24. package/src/config/types.ts +584 -0
  25. package/src/context/CompactionService.ts +300 -0
  26. package/src/context/ContextManager.ts +450 -0
  27. package/src/context/FileAnalyzer.ts +267 -0
  28. package/src/context/TokenCounter.ts +265 -0
  29. package/src/context/index.ts +27 -0
  30. package/src/context/storage/CacheStore.ts +176 -0
  31. package/src/context/storage/JSONLStore.ts +201 -0
  32. package/src/context/storage/MemoryStore.ts +205 -0
  33. package/src/context/storage/PersistentStore.ts +327 -0
  34. package/src/context/storage/index.ts +9 -0
  35. package/src/context/storage/pathUtils.ts +114 -0
  36. package/src/context/test.ts +309 -0
  37. package/src/context/types.ts +268 -0
  38. package/src/hooks/HookExecutor.ts +434 -0
  39. package/src/hooks/HookManager.ts +596 -0
  40. package/src/hooks/HookService.ts +269 -0
  41. package/src/hooks/Matcher.ts +157 -0
  42. package/src/hooks/index.ts +63 -0
  43. package/src/hooks/types.ts +424 -0
  44. package/src/main.tsx +596 -0
  45. package/src/mcp/HealthMonitor.ts +150 -0
  46. package/src/mcp/McpClient.ts +491 -0
  47. package/src/mcp/McpRegistry.ts +321 -0
  48. package/src/mcp/createMcpTool.ts +251 -0
  49. package/src/mcp/index.ts +15 -0
  50. package/src/mcp/server.ts +334 -0
  51. package/src/mcp/test-server.ts +88 -0
  52. package/src/mcp/test.ts +372 -0
  53. package/src/mcp/types.ts +247 -0
  54. package/src/memory/AgentMemoryBus.ts +432 -0
  55. package/src/memory/CloudSync.ts +99 -0
  56. package/src/memory/DriveSync.ts +106 -0
  57. package/src/memory/SharedMemory.ts +951 -0
  58. package/src/memory/index.ts +14 -0
  59. package/src/memory/machineFingerprint.ts +40 -0
  60. package/src/orchestrator/SubAgentMetadata.ts +136 -0
  61. package/src/prompts/builder.ts +213 -0
  62. package/src/prompts/default.ts +144 -0
  63. package/src/prompts/index.ts +16 -0
  64. package/src/prompts/plan.ts +64 -0
  65. package/src/prompts/test.ts +78 -0
  66. package/src/services/AnthropicChatService.ts +341 -0
  67. package/src/services/ChatService.ts +347 -0
  68. package/src/services/ClaudeCliChatService.ts +256 -0
  69. package/src/services/CloudSync.ts +168 -0
  70. package/src/services/CostLedger.ts +211 -0
  71. package/src/services/Heartbeat.ts +135 -0
  72. package/src/services/LearningCollector.ts +291 -0
  73. package/src/services/OllamaInstaller.ts +342 -0
  74. package/src/services/VersionChecker.ts +445 -0
  75. package/src/services/index.ts +57 -0
  76. package/src/services/streaming/OpenAIEventAdapter.ts +126 -0
  77. package/src/services/streaming/RenderingProfile.ts +90 -0
  78. package/src/services/streaming/StreamEventParser.ts +181 -0
  79. package/src/services/streaming/ThrottledRenderer.ts +139 -0
  80. package/src/services/streaming/TranscriptBuffer.ts +574 -0
  81. package/src/services/streaming/eventStatusMap.ts +52 -0
  82. package/src/services/streaming/index.ts +46 -0
  83. package/src/services/streaming/renderFormatting.ts +79 -0
  84. package/src/services/streaming/types.ts +234 -0
  85. package/src/skills/SkillLoader.ts +126 -0
  86. package/src/skills/SkillRegistry.ts +366 -0
  87. package/src/skills/index.ts +48 -0
  88. package/src/skills/types.ts +146 -0
  89. package/src/slash-commands/billing.ts +70 -0
  90. package/src/slash-commands/build.ts +413 -0
  91. package/src/slash-commands/builtinCommands.ts +2733 -0
  92. package/src/slash-commands/clone.ts +242 -0
  93. package/src/slash-commands/council.ts +125 -0
  94. package/src/slash-commands/custom/CustomCommandExecutor.ts +143 -0
  95. package/src/slash-commands/custom/CustomCommandLoader.ts +251 -0
  96. package/src/slash-commands/custom/CustomCommandRegistry.ts +144 -0
  97. package/src/slash-commands/custom/index.ts +7 -0
  98. package/src/slash-commands/debate.ts +254 -0
  99. package/src/slash-commands/gmail.ts +105 -0
  100. package/src/slash-commands/index.ts +388 -0
  101. package/src/slash-commands/mcpCommand.ts +205 -0
  102. package/src/slash-commands/types.ts +201 -0
  103. package/src/store/index.ts +76 -0
  104. package/src/store/selectors.ts +246 -0
  105. package/src/store/slices/appSlice.ts +205 -0
  106. package/src/store/slices/commandSlice.ts +115 -0
  107. package/src/store/slices/configSlice.ts +46 -0
  108. package/src/store/slices/focusSlice.ts +64 -0
  109. package/src/store/slices/index.ts +9 -0
  110. package/src/store/slices/sessionSlice.ts +424 -0
  111. package/src/store/streaming-buffer.ts +425 -0
  112. package/src/store/test.ts +296 -0
  113. package/src/store/types.ts +274 -0
  114. package/src/store/vanilla.ts +186 -0
  115. package/src/tools/builtin/bash.ts +236 -0
  116. package/src/tools/builtin/council.ts +105 -0
  117. package/src/tools/builtin/edit.ts +213 -0
  118. package/src/tools/builtin/glob.ts +136 -0
  119. package/src/tools/builtin/grep.ts +263 -0
  120. package/src/tools/builtin/index.ts +61 -0
  121. package/src/tools/builtin/memory.ts +66 -0
  122. package/src/tools/builtin/read.ts +168 -0
  123. package/src/tools/builtin/skill.ts +97 -0
  124. package/src/tools/builtin/snapshot.ts +40 -0
  125. package/src/tools/builtin/task.ts +106 -0
  126. package/src/tools/builtin/write.ts +134 -0
  127. package/src/tools/createTool.ts +221 -0
  128. package/src/tools/execution/ExecutionPipeline.ts +263 -0
  129. package/src/tools/execution/index.ts +40 -0
  130. package/src/tools/execution/stages/CacheStage.ts +131 -0
  131. package/src/tools/execution/stages/ConfirmationStage.ts +203 -0
  132. package/src/tools/execution/stages/DiscoveryStage.ts +46 -0
  133. package/src/tools/execution/stages/ExecutionStage.ts +48 -0
  134. package/src/tools/execution/stages/FormattingStage.ts +44 -0
  135. package/src/tools/execution/stages/HookStage.ts +72 -0
  136. package/src/tools/execution/stages/PermissionStage.ts +287 -0
  137. package/src/tools/execution/stages/PostHookStage.ts +71 -0
  138. package/src/tools/execution/stages/index.ts +12 -0
  139. package/src/tools/execution/test.ts +266 -0
  140. package/src/tools/execution/types.ts +273 -0
  141. package/src/tools/index.ts +81 -0
  142. package/src/tools/registry.ts +304 -0
  143. package/src/tools/schemas.ts +109 -0
  144. package/src/tools/test.ts +220 -0
  145. package/src/tools/types.ts +175 -0
  146. package/src/tools/validation/PermissionChecker.ts +242 -0
  147. package/src/tools/validation/SensitiveFileDetector.ts +210 -0
  148. package/src/tools/validation/index.ts +11 -0
  149. package/src/ui/App.tsx +166 -0
  150. package/src/ui/components/AegisInterface.tsx +484 -0
  151. package/src/ui/components/common/ChatSearch.tsx +150 -0
  152. package/src/ui/components/common/ErrorBoundary.tsx +82 -0
  153. package/src/ui/components/common/ExitMessage.tsx +120 -0
  154. package/src/ui/components/common/LoadingIndicator.tsx +49 -0
  155. package/src/ui/components/common/index.ts +6 -0
  156. package/src/ui/components/dialog/ConfirmationPrompt.tsx +208 -0
  157. package/src/ui/components/dialog/InteractiveSelector.tsx +149 -0
  158. package/src/ui/components/dialog/SetupWizard.tsx +297 -0
  159. package/src/ui/components/dialog/UpdatePrompt.tsx +155 -0
  160. package/src/ui/components/dialog/index.ts +8 -0
  161. package/src/ui/components/index.ts +28 -0
  162. package/src/ui/components/input/CommandSuggestions.tsx +139 -0
  163. package/src/ui/components/input/CustomTextInput.tsx +220 -0
  164. package/src/ui/components/input/InputArea.tsx +361 -0
  165. package/src/ui/components/input/PromptSuggestions.tsx +66 -0
  166. package/src/ui/components/input/index.ts +6 -0
  167. package/src/ui/components/layout/ChatStatusBar.tsx +90 -0
  168. package/src/ui/components/layout/ContextBar.tsx +79 -0
  169. package/src/ui/components/layout/MessageArea.tsx +96 -0
  170. package/src/ui/components/layout/MessageList.tsx +647 -0
  171. package/src/ui/components/layout/MessageSeparator.tsx +26 -0
  172. package/src/ui/components/layout/WelcomeMessage.tsx +93 -0
  173. package/src/ui/components/layout/index.ts +7 -0
  174. package/src/ui/components/markdown/CodeHighlighter.tsx +292 -0
  175. package/src/ui/components/markdown/MessageRenderer.tsx +1211 -0
  176. package/src/ui/components/markdown/index.ts +8 -0
  177. package/src/ui/components/markdown/parser.ts +336 -0
  178. package/src/ui/components/markdown/types.ts +66 -0
  179. package/src/ui/focus/FocusManager.ts +137 -0
  180. package/src/ui/focus/index.ts +13 -0
  181. package/src/ui/focus/types.ts +54 -0
  182. package/src/ui/focus/useFocus.ts +75 -0
  183. package/src/ui/hooks/index.ts +11 -0
  184. package/src/ui/hooks/useAgent.ts +284 -0
  185. package/src/ui/hooks/useCommandHistory.ts +87 -0
  186. package/src/ui/hooks/useCommandProcessor.ts +443 -0
  187. package/src/ui/hooks/useConfirmation.ts +99 -0
  188. package/src/ui/hooks/useCtrlCHandler.ts +100 -0
  189. package/src/ui/hooks/useInputBuffer.ts +122 -0
  190. package/src/ui/hooks/useTerminalSize.ts +68 -0
  191. package/src/ui/hooks/useTerminalWidth.ts +5 -0
  192. package/src/ui/hooks/useWindowedList.ts +118 -0
  193. package/src/ui/render-debugger.ts +621 -0
  194. package/src/ui/test.ts +189 -0
  195. package/src/ui/themes/ThemeManager.ts +332 -0
  196. package/src/ui/themes/aegisTheme.ts +87 -0
  197. package/src/ui/themes/darkTheme.ts +87 -0
  198. package/src/ui/themes/defaultTheme.ts +85 -0
  199. package/src/ui/themes/index.ts +10 -0
  200. package/src/ui/themes/lightTheme.ts +89 -0
  201. package/src/ui/themes/popularThemes.ts +187 -0
  202. package/src/ui/themes/types.ts +130 -0
  203. package/src/utils/clipboard.ts +48 -0
  204. package/src/utils/debug.ts +43 -0
  205. package/src/utils/environment.ts +68 -0
  206. package/src/utils/index.ts +10 -0
package/src/main.tsx ADDED
@@ -0,0 +1,596 @@
1
+ /**
2
+ * AEGIS CLI - 主入口
3
+ *
4
+ *
5
+ * 1. 早期解析 --debug 参数(确保日志可用)
6
+ * 2. 启动版本检查(不等待,与后续流程并行)
7
+ * 3. 创建 yargs CLI 实例
8
+ * 4. 注册全局选项和命令
9
+ * 5. 执行中间件链(validatePermissions → loadConfiguration → validateOutput)
10
+ * 6. 执行默认命令 → 启动 React UI(传递 versionCheckPromise)
11
+ *
12
+ *
13
+ * 1. 默认配置
14
+ * 2. 用户配置 (~/.aegiscode/config.json)
15
+ * 3. 项目配置 (./.aegiscode/config.json)
16
+ * 4. 环境变量 (OPENAI_API_KEY, OPENAI_BASE_URL, OPENAI_MODEL)
17
+ * 5. CLI 参数 (--api-key, --base-url, --model)
18
+ */
19
+
20
+ // RAF polyfill — MUST run before any module that uses requestAnimationFrame.
21
+ // Pass Date.now() as the timestamp so RAF callbacks that throttle by timestamp
22
+ // (e.g. MessageList's RAF_INTERVAL_MS check) work correctly. Without this,
23
+ // `now` is undefined and `undefined - ref < 30` is always false, causing the
24
+ // streaming render loop to fire at the raw setTimeout rate (~16ms) instead of 30ms.
25
+ if (typeof globalThis.requestAnimationFrame === 'undefined') {
26
+ (globalThis as any).requestAnimationFrame = (cb: FrameRequestCallback) => setTimeout(() => cb(Date.now()), 16);
27
+ (globalThis as any).cancelAnimationFrame = (id: number) => clearTimeout(id);
28
+ }
29
+
30
+ import { config as dotenvConfig } from 'dotenv';
31
+ import { resolve } from 'path';
32
+ import { homedir } from 'os';
33
+ // --print's whole point is clean, pipeable stdout (text or JSON) — the
34
+ // dotenvx promo banner would otherwise land on stdout ahead of the result.
35
+ const isPrintMode = process.argv.includes('--print') || process.argv.includes('-p');
36
+ // SetupWizard saves keys to ~/.aegiscode/.env (the only place a globally-installed
37
+ // `aegis` binary can reliably find them — process.cwd() is wherever the user happens
38
+ // to invoke it from). Load that first, then ./.env so a project-local file can override it.
39
+ dotenvConfig({ path: resolve(homedir(), '.aegiscode', '.env'), quiet: isPrintMode });
40
+ dotenvConfig({ path: resolve(process.cwd(), '.env'), quiet: isPrintMode, override: true });
41
+ import React from 'react';
42
+ import yargs from 'yargs';
43
+ import { hideBin } from 'yargs/helpers';
44
+ import { App } from './ui/App.js';
45
+ import { configManager } from './config/index.js';
46
+ import { cliConfig, globalOptions, middlewareChain } from './cli/index.js';
47
+ import { checkVersionOnStartup } from './services/index.js';
48
+ import { runCouncil } from './slash-commands/council.js';
49
+ import { getLatestSessionFile } from './context/index.js';
50
+ import { themeManager } from './ui/themes/index.js';
51
+ import { setGlobalDebug } from './utils/debug.js';
52
+ import type { CliArguments } from './cli/types.js';
53
+ import type { VersionCheckResult } from './services/VersionChecker.js';
54
+ import * as path from 'node:path';
55
+
56
+ // Rendering debugger (auto-starts with --debug-rendering flag)
57
+ const ENABLE_RENDER_DEBUG = process.argv.includes('--debug-rendering');
58
+
59
+ // ========== 全局状
60
+ let isDebugMode = false;
61
+ let versionCheckPromise: Promise<VersionCheckResult | null> | undefined;
62
+
63
+ /**
64
+ *
65
+ *
66
+ *
67
+ * - Logger 在各模块中被创建
68
+ * - 如果等 yargs 解析完再设置 debug,部分初始化日志会丢失
69
+ * - 早期解析确保所有日志都能正确输出
70
+ */
71
+ function parseDebugEarly(): void {
72
+ const rawArgs = hideBin(process.argv)
73
+ const debugIndex = rawArgs.indexOf('--debug')
74
+ const shortDebugIndex = rawArgs.indexOf('-d')
75
+
76
+ if (debugIndex !== -1 || shortDebugIndex !== -1) {
77
+ isDebugMode = true
78
+ setGlobalDebug(true) // 设置全局 debug 状态,供其他模块使
79
+ console.log('[DEBUG] Debug mode enabled via early parsing')
80
+ }
81
+ }
82
+
83
+ /**
84
+ *
85
+ */
86
+ async function main(): Promise<void> {
87
+ // 1. 早期解
88
+ parseDebugEarly();
89
+
90
+ // One-shot memory introspection flags for external callers (e.g. aegiscode-gui's
91
+ // Electron main process) — single source of truth instead of duplicating the
92
+ // SQLite schema/read logic in another codebase. MUST run before anything below
93
+ // imports SharedMemory (its singleton constructor fires init() — TTL eviction +
94
+ // cloud sync — synchronously on first import) or makes network calls (version
95
+ // check, token verification) that a fast read-only call has no business paying for.
96
+ const earlyArgs = process.argv.slice(2);
97
+ // One-shot account re-verification for external callers (aegiscode-gui) — same
98
+ // single-source-of-truth reasoning as the memory flags below.
99
+ if (earlyArgs[0] === '--verify-account-json') {
100
+ const { verifyAccount } = await import('./auth/login.js');
101
+ console.log(JSON.stringify(await verifyAccount()));
102
+ process.exit(0);
103
+ }
104
+
105
+ if (earlyArgs[0] === '--memory-stats-json' || earlyArgs[0] === '--memory-search-json' || earlyArgs[0] === '--memory-clear-json' || earlyArgs[0] === '--memory-upload-json' || earlyArgs[0] === '--memory-download-json') {
106
+ process.env.AEGIS_MEMORY_READONLY = '1';
107
+ const { sharedMemory } = await import('./memory/SharedMemory.js');
108
+ await sharedMemory.whenReady();
109
+
110
+ if (earlyArgs[0] === '--memory-stats-json') {
111
+ console.log(JSON.stringify(sharedMemory.getStats()));
112
+ } else if (earlyArgs[0] === '--memory-search-json') {
113
+ const query = earlyArgs[1] || '';
114
+ const limit = parseInt(earlyArgs[2] || '50', 10);
115
+ const results = query ? await sharedMemory.search(query, limit) : sharedMemory.recent(limit);
116
+ console.log(JSON.stringify(results));
117
+ } else if (earlyArgs[0] === '--memory-upload-json') {
118
+ console.log(JSON.stringify(await sharedMemory.pushAll()));
119
+ } else if (earlyArgs[0] === '--memory-download-json') {
120
+ console.log(JSON.stringify(await sharedMemory.pullAll()));
121
+ } else {
122
+ sharedMemory.clear();
123
+ console.log(JSON.stringify({ ok: true }));
124
+ }
125
+ process.exit(0);
126
+ }
127
+
128
+ // 2. 启动版本检查(不等待,与后续流程并行执
129
+ versionCheckPromise = checkVersionOnStartup();
130
+
131
+ // Memory is available for all logged-in users — no separate token verification needed
132
+
133
+ // Handle --model flag: aegis --model deepseek "question"
134
+ // or aegis --model council "question" for council vote
135
+ const modelArg = process.argv.find(a => a.startsWith('--model='))?.split('=')[1]
136
+ || (() => { const i = process.argv.indexOf('--model'); return i > -1 ? process.argv[i+1] : null; })();
137
+
138
+ if (modelArg) {
139
+ const { readFileSync } = await import('fs');
140
+ const { homedir } = await import('os');
141
+ try {
142
+ const cfg = JSON.parse(readFileSync(`${homedir()}/.aegiscode/config.json`, 'utf8'));
143
+ const found = (cfg.models || []).find((m: any) => m.id === modelArg);
144
+ if (found) {
145
+ process.env.OPENAI_API_KEY = found.apiKey;
146
+ process.env.OPENAI_BASE_URL = found.baseUrl || found.baseURL;
147
+ // Sätt currentModelId så getDefaultModel() väljer rätt
148
+ cfg.currentModelId = found.id;
149
+ const { writeFileSync } = await import('fs');
150
+ writeFileSync(`${homedir()}/.aegiscode/config.json`, JSON.stringify(cfg, null, 2));
151
+ console.log(`\x1b[38;2;0;229;192m[AEGIS] Model: ${found.name || found.id}\x1b[0m`);
152
+ }
153
+ } catch {}
154
+ }
155
+
156
+ // Handle /council command
157
+ const args = process.argv.slice(2);
158
+
159
+ if (args[0] === '/council' || args[0] === 'council') {
160
+ const question = args.slice(1).join(' ');
161
+ if (!question) {
162
+ console.error('Usage: aegis /council <question>');
163
+ process.exit(1);
164
+ }
165
+ runCouncil(question).then(() => process.exit(0));
166
+ return;
167
+ }
168
+ if (isDebugMode) {
169
+ console.log('[DEBUG] Version check started (running in parallel)');
170
+ }
171
+
172
+ // 3. 创建 yargs CLI 实
173
+ const cli = yargs(hideBin(process.argv))
174
+ .scriptName(cliConfig.scriptName)
175
+ .usage(cliConfig.usage)
176
+ .version(cliConfig.version)
177
+ // 注册全局选
178
+ .options(globalOptions)
179
+
180
+ // 注册中间
181
+ .middleware(middlewareChain)
182
+
183
+ // ── auth commands ──────────────────────────────────────────────────────
184
+ .command(
185
+ 'login',
186
+ 'Log in to aegiscloud (Google or username/password), or Claude Code Pro/Max',
187
+ (y) => y
188
+ .option('password', {
189
+ alias: 'p',
190
+ type: 'boolean',
191
+ describe: 'Log in with username and password instead of browser',
192
+ default: false,
193
+ })
194
+ .option('claude-pro', {
195
+ type: 'boolean',
196
+ describe: 'Authenticate with a Claude Code Pro/Max subscription token instead of an API key',
197
+ default: false,
198
+ }),
199
+ async (argv) => {
200
+ const { runLogin, runLoginPassword, runLoginClaudePro } = await import('./auth/login.js');
201
+ try {
202
+ if ((argv as any).claudePro) {
203
+ await runLoginClaudePro();
204
+ process.exit(0);
205
+ } else if ((argv as any).password) {
206
+ await runLoginPassword();
207
+ } else {
208
+ await runLogin();
209
+ }
210
+ console.log('\n\x1b[32m✓ Logged in successfully.\x1b[0m');
211
+ console.log('\nRun \x1b[1maegis\x1b[0m to start coding.\n');
212
+ } catch (err) {
213
+ console.error('\n\x1b[31m✗ Login failed:\x1b[0m', (err as Error).message);
214
+ process.exit(1);
215
+ }
216
+ process.exit(0);
217
+ },
218
+ )
219
+
220
+ .command(
221
+ 'logout',
222
+ 'Log out and remove stored aegiscloud credentials',
223
+ () => {},
224
+ async () => {
225
+ const { runLogout } = await import('./auth/login.js');
226
+ runLogout();
227
+ process.exit(0);
228
+ },
229
+ )
230
+
231
+ // 示
232
+ .example('$0', 'Start interactive mode')
233
+ .example('$0 login', 'Log in via browser')
234
+ .example('$0 continue', 'Continue the most recent conversation')
235
+ .example('$0 resume <session-id>', 'Resume a specific conversation by ID')
236
+ .example('$0 "帮我分析这个项目"', 'Start with an initial message')
237
+ .example('$0 --model gpt-4', 'Use a specific model')
238
+ .example('$0 --router', 'Start with the auto-router on')
239
+ .example('$0 --debug', 'Enable debug mode')
240
+ .example('$0 --init', 'Create default config file')
241
+
242
+ // 帮助
243
+ .help()
244
+ .alias('h', 'help')
245
+
246
+ // 7. 错误处
247
+ .fail((msg, err, yargsInstance) => {
248
+ if (err) {
249
+ console.error('💥 An error occurred:')
250
+ console.error(err.message)
251
+ if (isDebugMode && err.stack) {
252
+ console.error('\nStack trace:')
253
+ console.error(err.stack)
254
+ }
255
+ process.exit(1)
256
+ }
257
+
258
+ if (msg) {
259
+ console.error('❌ Invalid arguments:')
260
+ console.error(msg)
261
+ console.error('')
262
+ yargsInstance.showHelp()
263
+ process.exit(1)
264
+ }
265
+ })
266
+
267
+ // 8. 严格模式(禁止未知选
268
+ .strict()
269
+
270
+ // 9. 默认命
271
+ .command(
272
+ '$0 [message..]',
273
+ 'Start interactive mode',
274
+ (yargs) => {
275
+ return yargs.positional('message', {
276
+ type: 'string',
277
+ describe: 'Initial message to send (can be multiple words)',
278
+ array: true,
279
+ })
280
+ },
281
+ async (argv) => {
282
+ const args = argv as CliArguments
283
+
284
+ // 处理 --init 命
285
+ if (args.init) {
286
+ const configPath = await configManager.createDefaultConfig()
287
+ console.log(`✅ Created default config at: ${configPath}`)
288
+ console.log('')
289
+ console.log('Please edit the file and add your API key:')
290
+ console.log(` vim ${configPath}`)
291
+ process.exit(0)
292
+ }
293
+
294
+ // ── Mandatory login check (Aegiscode Pro) ────────────────────────
295
+ // Aegiscode Pro requires login. No login = no API access.
296
+ const { readFileSync } = await import('node:fs');
297
+ const { homedir } = await import('node:os');
298
+ let hasCredentials = false;
299
+ try {
300
+ const cfg = JSON.parse(readFileSync(`${homedir()}/.aegiscode/config.json`, 'utf8'));
301
+ hasCredentials = !!(cfg?.aegiscloud?.api_key);
302
+ } catch {}
303
+
304
+ const needsLogin = hasCredentials
305
+ ? !(await (await import('./auth/login.js')).ensureAccountValid())
306
+ : true;
307
+
308
+ if (needsLogin) {
309
+ const { runLogin } = await import('./auth/login.js');
310
+ try {
311
+ await runLogin();
312
+ console.log('\n\x1b[32m✓ Logged in.\x1b[0m Starting ÆGIS...\n');
313
+ await configManager.initialize(process.cwd());
314
+ } catch (err) {
315
+ console.error('\n\x1b[31m✗ Login failed:\x1b[0m', (err as Error).message);
316
+ console.error('Run \x1b[1maegis login\x1b[0m to try again.');
317
+ process.exit(1);
318
+ }
319
+ }
320
+
321
+ const modelConfig = configManager.getDefaultModel()
322
+
323
+ // No API key yet — don't exit here. App.tsx already detects this on mount
324
+ // (initializeStoreState) and renders the interactive SetupWizard, which
325
+ // creates ~/.aegiscode/.env for the user. Exiting here with print-only
326
+ // instructions skipped that wizard entirely and left fresh installs
327
+ // stuck telling users to hand-create a hidden folder themselves.
328
+ // --print mode has no UI to fall back on, so it still needs to fail fast.
329
+ if (!modelConfig.apiKey && isPrintMode) {
330
+ process.stderr.write(
331
+ 'No API key configured. Run `aegis` once (without --print) to set one up, ' +
332
+ 'or set an environment variable: export OPENAI_API_KEY=sk-...\n',
333
+ );
334
+ process.exit(1);
335
+ }
336
+
337
+ // 获取初始消息(支持多个单
338
+ const messageArray = argv.message as string[] | undefined
339
+ let initialMessage =
340
+ messageArray && messageArray.length > 0
341
+ ? messageArray.join(' ')
342
+ : undefined
343
+
344
+ if (isDebugMode && initialMessage) {
345
+ console.log('[DEBUG] Initial message:', initialMessage)
346
+ }
347
+
348
+ // Handle `aegis continue` and `aegis resume <id>` without `--` prefix
349
+ // so they behave like `aegis --continue` and `aegis --resume <id>`.
350
+ if (!args.continue && !args.resume && initialMessage && !initialMessage.startsWith('/')) {
351
+ const trimmed = initialMessage.trim();
352
+ if (trimmed === 'continue') {
353
+ args.continue = true;
354
+ initialMessage = undefined;
355
+ } else if (trimmed.startsWith('resume ') || trimmed.startsWith('resume\t')) {
356
+ const parts = trimmed.split(/\s+/);
357
+ if (parts.length >= 2) {
358
+ args.resume = parts[1];
359
+ initialMessage = undefined;
360
+ }
361
+ }
362
+ }
363
+
364
+ // 处理 --continue 和 --resume 参
365
+ let resumeSessionId: string | undefined;
366
+
367
+ if (args.continue) {
368
+ // 获取最近的会话文
369
+ const latestSession = await getLatestSessionFile(process.cwd());
370
+ if (latestSession) {
371
+ // 从文件路径提取 sessionId(去掉 .jsonl 扩展
372
+ resumeSessionId = path.basename(latestSession, '.jsonl');
373
+ if (isDebugMode) {
374
+ console.log('[DEBUG] Continuing session:', resumeSessionId);
375
+ }
376
+ } else {
377
+ console.log('No previous session found. Starting a new conversation.');
378
+ }
379
+ } else if (args.resume && typeof args.resume === 'string') {
380
+ resumeSessionId = args.resume;
381
+ if (isDebugMode) {
382
+ console.log('[DEBUG] Resuming session:', resumeSessionId);
383
+ }
384
+ }
385
+
386
+ // --print: headless mode, no Ink. Runs one turn, writes the result to
387
+ // stdout (text or JSON per --output-format), and exits — no TUI chrome,
388
+ // no spinners, no escape codes, so the output is safe to pipe/script.
389
+ if (args.print) {
390
+ if (!initialMessage) {
391
+ process.stderr.write('Error: --print requires a message, e.g. aegis --print "your question"\n');
392
+ process.exit(1);
393
+ }
394
+
395
+ const { startCostLedger } = await import('./services/CostLedger.js');
396
+ startCostLedger();
397
+ const { startHeartbeat } = await import('./services/Heartbeat.js');
398
+ startHeartbeat();
399
+
400
+ const { ContextManager } = await import('./context/index.js');
401
+ const { Agent } = await import('./agent/Agent.js');
402
+
403
+ const ctxManager = new ContextManager({ compressionThreshold: 100000 });
404
+ let sessionId: string;
405
+ let priorMessages: { role: 'system' | 'user' | 'assistant' | 'tool'; content: string }[] = [];
406
+
407
+ if (resumeSessionId) {
408
+ const loaded = await ctxManager.loadSession(resumeSessionId);
409
+ sessionId = loaded ? resumeSessionId : await ctxManager.createSession();
410
+ if (loaded) {
411
+ priorMessages = ctxManager.getMessages()
412
+ .filter(m => m.role === 'user' || m.role === 'assistant')
413
+ .map(m => ({ role: m.role as 'user' | 'assistant', content: m.content }));
414
+ }
415
+ } else {
416
+ sessionId = await ctxManager.createSession();
417
+ }
418
+
419
+ try {
420
+ const agent = await Agent.create({
421
+ apiKey: modelConfig.apiKey ?? '',
422
+ baseURL: modelConfig.baseURL,
423
+ model: modelConfig.model,
424
+ requireConfirmation: false,
425
+ });
426
+
427
+ const result = await agent.chatWithMetadata(initialMessage, {
428
+ sessionId,
429
+ messages: priorMessages,
430
+ });
431
+
432
+ if (!result.success) {
433
+ throw new Error(result.error?.message || 'Agent execution failed');
434
+ }
435
+ const finalMessage = result.finalMessage || '';
436
+
437
+ await ctxManager.addMessage('user', initialMessage);
438
+ await ctxManager.addMessage('assistant', finalMessage);
439
+ await ctxManager.flush();
440
+
441
+ if (args.outputFormat === 'json') {
442
+ process.stdout.write(JSON.stringify({
443
+ result: finalMessage,
444
+ session_id: sessionId,
445
+ num_turns: result.metadata?.turnsCount,
446
+ num_tool_calls: result.metadata?.toolCallsCount,
447
+ total_tokens: result.metadata?.totalTokens,
448
+ }) + '\n');
449
+ } else {
450
+ process.stdout.write(finalMessage + '\n');
451
+ }
452
+ process.exit(0);
453
+ } catch (error) {
454
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
455
+ process.exit(1);
456
+ }
457
+ }
458
+
459
+ // 初始化主题(从用户配置加载,或自动检测终端颜色模
460
+ themeManager.initializeFromConfig();
461
+
462
+ // CLI 参数覆盖(如果指定
463
+ if (args.theme && themeManager.hasTheme(args.theme)) {
464
+ themeManager.setTheme(args.theme);
465
+ if (isDebugMode) {
466
+ console.log('[DEBUG] Theme overridden by CLI to:', args.theme);
467
+ }
468
+ } else if (isDebugMode) {
469
+ console.log('[DEBUG] Theme:', themeManager.getCurrentThemeName());
470
+ }
471
+
472
+ // Check for --plain flag (plain text mode, no Ink)
473
+ const isPlain = args.plain === true;
474
+ if (isDebugMode) console.log('[DEBUG] Plain mode:', isPlain);
475
+
476
+ if (isPlain) {
477
+ // Plain text mode — just relay input/output
478
+ if (initialMessage) {
479
+ console.log(initialMessage);
480
+ }
481
+ const { createInterface } = await import('node:readline');
482
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
483
+ rl.on('line', (line) => {
484
+ if (line.trim()) console.log(line);
485
+ });
486
+ rl.on('close', () => process.exit(0));
487
+ return;
488
+ }
489
+
490
+ // Ink rendering — with fallback for non-TTY environments
491
+ const isTTY = process.stdin.isTTY === true && process.stdout.isTTY === true;
492
+ if (isDebugMode) console.log('[DEBUG] TTY:', isTTY, '(stdin:', process.stdin.isTTY, 'stdout:', process.stdout.isTTY + ')');
493
+
494
+ // Auto-start rendering debugger if --debug-rendering is set
495
+ if (ENABLE_RENDER_DEBUG) {
496
+ const { startRenderDebugger } = await import('./ui/render-debugger.js');
497
+ startRenderDebugger({ reportInterval: 5000, verbose: false });
498
+ }
499
+
500
+ // Create proper stdin/stdout for Ink (mock if needed to prevent raw mode errors)
501
+ let renderStdin = process.stdin;
502
+ let renderStdout = process.stdout;
503
+
504
+ // Force isTTY on stdout so Ink renders properly in all terminal environments
505
+ if (!process.stdout.isTTY) {
506
+ (process.stdout as any).isTTY = true;
507
+ if (isDebugMode) console.log('[DEBUG] Forcing isTTY=true on stdout');
508
+ }
509
+
510
+ if (!isTTY) {
511
+ const { PassThrough } = await import('node:stream');
512
+ const mockStdin = new PassThrough();
513
+ (mockStdin as any).isTTY = true;
514
+ (mockStdin as any).setRawMode = () => {};
515
+ (mockStdin as any).ref = () => {};
516
+ (mockStdin as any).unref = () => {};
517
+ (mockStdin as any).setEncoding = () => {};
518
+ renderStdin = mockStdin as unknown as typeof process.stdin;
519
+ if (isDebugMode) console.log('[DEBUG] Using mock stdin for Ink render');
520
+ }
521
+
522
+ // Start background services (fire-and-forget, silent on failure)
523
+ const { startCostLedger } = await import('./services/CostLedger.js');
524
+ startCostLedger();
525
+ const { startHeartbeat } = await import('./services/Heartbeat.js');
526
+ startHeartbeat();
527
+
528
+ try {
529
+ const { render } = await import('ink');
530
+ render(
531
+ <App
532
+ apiKey={modelConfig.apiKey ?? ''}
533
+ baseURL={modelConfig.baseURL}
534
+ model={modelConfig.model}
535
+ initialMessage={initialMessage}
536
+ debug={args.debug}
537
+ permissionMode={args.permissionMode}
538
+ versionCheckPromise={versionCheckPromise}
539
+ resumeSessionId={resumeSessionId}
540
+ routerEnabled={args.router}
541
+ />,
542
+ {
543
+ exitOnCtrlC: false,
544
+ patchConsole: true,
545
+ stdin: renderStdin,
546
+ stdout: process.stdout,
547
+ // Disabled: the alt screen buffer blocks normal mouse text selection/copy
548
+ // in most terminal emulators. Real Claude Code renders inline instead.
549
+ alternateScreen: false,
550
+ maxFps: 30,
551
+ // Without this, Ink's default log-update mode erases and rewrites the
552
+ // ENTIRE terminal output on every re-render, even when only a single
553
+ // line changed (e.g. the input cursor blink, every ~530ms, forever).
554
+ // That full erase+rewrite invalidates any in-progress mouse text
555
+ // selection in terminals like Kitty — selecting a message becomes
556
+ // impossible because the screen keeps getting wiped out from under it.
557
+ // incrementalRendering does real line-level diffing and only rewrites
558
+ // lines that actually changed, leaving untouched lines (and any
559
+ // selection on them) alone.
560
+ incrementalRendering: true,
561
+ },
562
+ );
563
+
564
+ // Handle EOF (Ctrl+D) on real stdin to allow normal terminal closing
565
+ if (isTTY && process.stdin.isTTY) {
566
+ process.stdin.on('end', () => {
567
+ if (isDebugMode) console.log('[DEBUG] EOF received on stdin');
568
+ process.exit(0);
569
+ });
570
+ }
571
+ } catch (renderError) {
572
+ // Ink rendering failed, fall back to simple text mode
573
+ if (isDebugMode) console.log('[DEBUG] Ink rendering failed:', (renderError as Error).message, '- falling back to text mode');
574
+ if (initialMessage) {
575
+ console.log(initialMessage);
576
+ }
577
+ const { createInterface } = await import('node:readline');
578
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
579
+ rl.on('line', (line) => {
580
+ if (line.trim()) console.log(line);
581
+ });
582
+ rl.on('close', () => process.exit(0));
583
+ }
584
+ })
585
+ await cli.parse()
586
+ }
587
+
588
+ // 运行主函
589
+ main().catch((error) => {
590
+ console.error('Fatal error:', error.message)
591
+ if (isDebugMode && error.stack) {
592
+ console.error('\nStack trace:')
593
+ console.error(error.stack)
594
+ }
595
+ process.exit(1)
596
+ })