@code4bug/jarvis-agent 1.1.7 → 1.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,4 +12,4 @@ export interface InitResult {
12
12
  /** JARVIS.md 是否为新建(false 表示覆盖) */
13
13
  isNew: boolean;
14
14
  }
15
- export declare function executeInit(): InitResult;
15
+ export declare function executeInit(): Promise<InitResult>;
@@ -8,6 +8,9 @@ import fs from 'fs';
8
8
  import path from 'path';
9
9
  import { execSync } from 'child_process';
10
10
  import { APP_NAME, APP_VERSION } from '../config/constants.js';
11
+ import { LLMServiceImpl, getDefaultConfig } from '../services/api/llm.js';
12
+ import { allTools } from '../tools/index.js';
13
+ import { loadAllAgents } from '../agents/index.js';
11
14
  // ===== 辅助函数 =====
12
15
  /** 安全执行命令 */
13
16
  function safeExec(cmd) {
@@ -123,7 +126,393 @@ function countSourceFiles(dir) {
123
126
  walk(dir);
124
127
  return { total, byExt };
125
128
  }
126
- export function executeInit() {
129
+ /** 读取文本文件,超长时截断 */
130
+ function readTextFile(filePath, maxChars = 6000) {
131
+ try {
132
+ const content = fs.readFileSync(filePath, 'utf-8');
133
+ if (content.length <= maxChars)
134
+ return content;
135
+ return `${content.slice(0, maxChars)}\n\n[...已截断,共 ${content.length} 字符]`;
136
+ }
137
+ catch {
138
+ return '';
139
+ }
140
+ }
141
+ function shouldIgnoreEntry(name) {
142
+ return name.startsWith('.')
143
+ || name === 'node_modules'
144
+ || name === 'dist'
145
+ || name === 'build'
146
+ || name === 'target'
147
+ || name === '__pycache__'
148
+ || name === '.git';
149
+ }
150
+ function normalizePath(relativePath) {
151
+ return relativePath.split(path.sep).join('/');
152
+ }
153
+ function isTextLikeFile(fileName) {
154
+ const ext = path.extname(fileName).toLowerCase();
155
+ return [
156
+ '.md', '.txt', '.json', '.jsonc', '.yaml', '.yml', '.toml', '.ini', '.conf',
157
+ '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
158
+ '.py', '.java', '.go', '.rs', '.kt', '.kts',
159
+ '.c', '.cc', '.cpp', '.h', '.hpp',
160
+ '.sh', '.bash', '.zsh', '.sql', '.xml', '.gradle', '.properties',
161
+ ].includes(ext) || fileName === 'Dockerfile' || fileName === 'Makefile';
162
+ }
163
+ function collectRootContextFiles(cwd) {
164
+ const preferredPatterns = [
165
+ /^README(\..+)?$/i,
166
+ /^package\.json$/i,
167
+ /^pnpm-lock\.ya?ml$/i,
168
+ /^package-lock\.json$/i,
169
+ /^yarn\.lock$/i,
170
+ /^tsconfig.*\.json$/i,
171
+ /^vite\.config\./i,
172
+ /^webpack\.config\./i,
173
+ /^next\.config\./i,
174
+ /^nuxt\.config\./i,
175
+ /^pom\.xml$/i,
176
+ /^build\.gradle(\.kts)?$/i,
177
+ /^settings\.gradle(\.kts)?$/i,
178
+ /^pyproject\.toml$/i,
179
+ /^requirements.*\.txt$/i,
180
+ /^go\.mod$/i,
181
+ /^Cargo\.toml$/i,
182
+ /^composer\.json$/i,
183
+ /^Gemfile$/i,
184
+ /^Dockerfile$/i,
185
+ /^docker-compose\.ya?ml$/i,
186
+ /^Makefile$/i,
187
+ /^AGENT.*\.md$/i,
188
+ /^SKILL.*\.md$/i,
189
+ ];
190
+ try {
191
+ const entries = fs.readdirSync(cwd, { withFileTypes: true });
192
+ return entries
193
+ .filter((entry) => entry.isFile() && !shouldIgnoreEntry(entry.name))
194
+ .map((entry) => entry.name)
195
+ .filter((name) => preferredPatterns.some((pattern) => pattern.test(name)))
196
+ .sort((a, b) => a.localeCompare(b))
197
+ .slice(0, 12);
198
+ }
199
+ catch {
200
+ return [];
201
+ }
202
+ }
203
+ function chooseRepresentativeFiles(dirPath, relativeDir, maxFiles = 3) {
204
+ const preferredNames = [
205
+ 'index', 'main', 'app', 'cli', 'server', 'client', 'api',
206
+ 'router', 'routes', 'controller', 'service', 'model',
207
+ 'commands', 'tools', 'query', 'engine',
208
+ ];
209
+ try {
210
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true })
211
+ .filter((entry) => entry.isFile() && !shouldIgnoreEntry(entry.name) && isTextLikeFile(entry.name))
212
+ .sort((a, b) => {
213
+ const aBase = path.parse(a.name).name.toLowerCase();
214
+ const bBase = path.parse(b.name).name.toLowerCase();
215
+ const aScore = preferredNames.findIndex((name) => name === aBase);
216
+ const bScore = preferredNames.findIndex((name) => name === bBase);
217
+ const normalizedAScore = aScore === -1 ? preferredNames.length : aScore;
218
+ const normalizedBScore = bScore === -1 ? preferredNames.length : bScore;
219
+ if (normalizedAScore !== normalizedBScore)
220
+ return normalizedAScore - normalizedBScore;
221
+ return a.name.localeCompare(b.name);
222
+ });
223
+ return entries
224
+ .slice(0, maxFiles)
225
+ .map((entry) => normalizePath(path.join(relativeDir, entry.name)));
226
+ }
227
+ catch {
228
+ return [];
229
+ }
230
+ }
231
+ function collectSourceContextFiles(cwd) {
232
+ const candidateDirs = ['src', 'app', 'lib', 'cmd', 'internal', 'pkg', 'server', 'client', 'backend', 'frontend'];
233
+ const result = [];
234
+ for (const dirName of candidateDirs) {
235
+ const dirPath = path.join(cwd, dirName);
236
+ if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory())
237
+ continue;
238
+ result.push(...chooseRepresentativeFiles(dirPath, dirName, 3));
239
+ try {
240
+ const subDirs = fs.readdirSync(dirPath, { withFileTypes: true })
241
+ .filter((entry) => entry.isDirectory() && !shouldIgnoreEntry(entry.name))
242
+ .sort((a, b) => a.name.localeCompare(b.name))
243
+ .slice(0, 4);
244
+ for (const subDir of subDirs) {
245
+ result.push(...chooseRepresentativeFiles(path.join(dirPath, subDir.name), path.join(dirName, subDir.name), 2));
246
+ }
247
+ }
248
+ catch {
249
+ // ignore
250
+ }
251
+ }
252
+ return Array.from(new Set(result)).slice(0, 18);
253
+ }
254
+ /** 采样项目关键文件,提供给大模型做总结 */
255
+ function collectProjectContext(cwd) {
256
+ const candidates = [
257
+ ...collectRootContextFiles(cwd),
258
+ ...collectSourceContextFiles(cwd),
259
+ ];
260
+ const parts = [];
261
+ for (const relativePath of candidates) {
262
+ const fullPath = path.join(cwd, relativePath);
263
+ if (!fs.existsSync(fullPath))
264
+ continue;
265
+ const content = readTextFile(fullPath);
266
+ if (!content)
267
+ continue;
268
+ parts.push(`## 文件: ${normalizePath(relativePath)}\n\n\`\`\`\n${content}\n\`\`\``);
269
+ }
270
+ return parts.join('\n\n');
271
+ }
272
+ function stripMarkdownCodeFence(text) {
273
+ const trimmed = text.trim();
274
+ const match = trimmed.match(/^```(?:md|markdown)?\s*([\s\S]*?)\s*```$/i);
275
+ return match ? match[1].trim() : trimmed;
276
+ }
277
+ function stripJsonCodeFence(text) {
278
+ const trimmed = text.trim();
279
+ const match = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
280
+ return match ? match[1].trim() : trimmed;
281
+ }
282
+ function listTopLevelDirectories(cwd) {
283
+ try {
284
+ return fs.readdirSync(cwd, { withFileTypes: true })
285
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules' && entry.name !== 'dist')
286
+ .map((entry) => entry.name)
287
+ .sort((a, b) => a.localeCompare(b));
288
+ }
289
+ catch {
290
+ return [];
291
+ }
292
+ }
293
+ function buildRealCommandSuggestions(cwd, pkg) {
294
+ const commands = [];
295
+ if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) {
296
+ commands.push('pnpm install');
297
+ }
298
+ else if (fs.existsSync(path.join(cwd, 'package-lock.json'))) {
299
+ commands.push('npm install');
300
+ }
301
+ if (pkg?.scripts?.dev)
302
+ commands.push('npm run dev');
303
+ if (pkg?.scripts?.start)
304
+ commands.push('npm run start');
305
+ if (pkg?.scripts?.build)
306
+ commands.push('npm run build');
307
+ if (pkg?.scripts?.test)
308
+ commands.push('npm test');
309
+ return commands;
310
+ }
311
+ function renderJarvisMd(input) {
312
+ const { projectName, packageJson: pkg, projectTypes, gitInfo, dirTree, devCommands, summary } = input;
313
+ const md = [];
314
+ md.push(`# ${pkg?.name || projectName}`);
315
+ md.push('');
316
+ md.push('## 项目概览');
317
+ md.push('');
318
+ md.push(summary.overview || '待补充');
319
+ md.push('');
320
+ md.push('## 技术栈');
321
+ md.push('');
322
+ md.push(`- 项目类型:${projectTypes.join(', ')}`);
323
+ md.push(`- 运行时:${pkg?.type || '未发现'}`);
324
+ md.push(`- 版本:${pkg?.version || '未发现'}`);
325
+ md.push(`- Git 分支:${gitInfo?.branch || '未发现'}`);
326
+ md.push('');
327
+ md.push('## 目录与模块');
328
+ md.push('');
329
+ for (const line of summary.architecture) {
330
+ md.push(`- ${line}`);
331
+ }
332
+ md.push('');
333
+ md.push('```');
334
+ md.push(`${projectName}/`);
335
+ md.push(...dirTree);
336
+ md.push('```');
337
+ md.push('');
338
+ md.push('## 当前能力');
339
+ md.push('');
340
+ for (const line of summary.capabilities) {
341
+ md.push(`- ${line}`);
342
+ }
343
+ md.push('');
344
+ md.push('## 开发与构建');
345
+ md.push('');
346
+ if (devCommands.length > 0) {
347
+ md.push('```bash');
348
+ md.push(...devCommands);
349
+ md.push('```');
350
+ }
351
+ else {
352
+ md.push('待补充');
353
+ }
354
+ md.push('');
355
+ md.push('## 协作约定');
356
+ md.push('');
357
+ for (const line of summary.collaborationNotes) {
358
+ md.push(`- ${line}`);
359
+ }
360
+ md.push('');
361
+ md.push(`> 由 ${APP_NAME} /init 自动生成`);
362
+ md.push('');
363
+ return md.join('\n');
364
+ }
365
+ async function generateJarvisMdWithLLM(input) {
366
+ const service = new LLMServiceImpl({
367
+ ...getDefaultConfig(),
368
+ });
369
+ const projectContext = collectProjectContext(input.cwd);
370
+ const topLevelDirectories = listTopLevelDirectories(input.cwd);
371
+ const devCommands = buildRealCommandSuggestions(input.cwd, input.packageJson);
372
+ const toolNames = allTools.map((tool) => tool.name);
373
+ const agentNames = Array.from(loadAllAgents().values()).map((agent) => agent.meta.name);
374
+ const extStats = Object.entries(input.fileStats.byExt)
375
+ .sort((a, b) => b[1] - a[1])
376
+ .map(([ext, count]) => `${ext}: ${count}`)
377
+ .join(', ');
378
+ const prompt = [
379
+ '你是项目初始化助手。请基于提供的真实项目信息,为 JARVIS.md 先生成结构化摘要。',
380
+ '',
381
+ '要求:',
382
+ '1. 只能依据给定信息总结,不要编造不存在的模块、流程、命令、目录或能力',
383
+ '2. 全文使用中文',
384
+ '3. 必须只输出合法 JSON,不要附加解释,不要输出 Markdown',
385
+ '4. 内容要有总结性,但必须可被事实支撑',
386
+ '5. 如果信息不足,明确写“待补充”或“未发现”,不要猜测',
387
+ '6. 禁止使用 emoji、营销文案、夸张措辞',
388
+ '',
389
+ 'JSON 结构如下:',
390
+ '{',
391
+ ' "overview": "1 段中文概述,80-160 字",',
392
+ ' "architecture": ["3 到 6 条,每条一句,描述目录或模块职责"],',
393
+ ' "capabilities": ["4 到 8 条,每条一句,描述当前已实现能力"],',
394
+ ' "collaborationNotes": ["3 到 6 条,每条一句,描述开发协作约定或注意事项"]',
395
+ '}',
396
+ '',
397
+ '以下是项目事实:',
398
+ `- 项目名称: ${input.packageJson?.name || input.projectName}`,
399
+ `- 项目目录名: ${input.projectName}`,
400
+ `- 项目版本: ${input.packageJson?.version || '未发现'}`,
401
+ `- 项目描述: ${input.packageJson?.description || '未发现'}`,
402
+ `- 项目类型: ${input.projectTypes.join(', ')}`,
403
+ `- Git 分支: ${input.gitInfo?.branch || '未发现'}`,
404
+ `- Git 远程: ${input.gitInfo?.remote || '未发现'}`,
405
+ `- 最近提交: ${input.gitInfo?.lastCommit || '未发现'}`,
406
+ `- 源文件总数: ${input.fileStats.total}`,
407
+ `- 扩展名统计: ${extStats || '未发现'}`,
408
+ `- 顶层目录: ${topLevelDirectories.join(', ') || '未发现'}`,
409
+ `- 内置工具: ${toolNames.join(', ') || '未发现'}`,
410
+ `- 内置智能体: ${agentNames.join(', ') || '未发现'}`,
411
+ `- 可确认开发命令: ${devCommands.join(' | ') || '未发现'}`,
412
+ '',
413
+ '目录树(浅层):',
414
+ `${input.projectName}/`,
415
+ ...input.dirTree,
416
+ '',
417
+ '关键文件内容:',
418
+ projectContext || '(未读取到关键文件)',
419
+ ].join('\n');
420
+ let result = '';
421
+ const transcript = [
422
+ { role: 'user', content: prompt },
423
+ ];
424
+ await new Promise((resolve, reject) => {
425
+ service.streamMessage(transcript, [], {
426
+ onText: (text) => { result += text; },
427
+ onToolUse: () => { },
428
+ onComplete: () => resolve(),
429
+ onError: (error) => reject(error),
430
+ }, undefined, { includeUserProfile: false }).catch(reject);
431
+ });
432
+ const cleaned = stripJsonCodeFence(stripMarkdownCodeFence(result));
433
+ if (!cleaned) {
434
+ throw new Error('大模型未返回有效内容');
435
+ }
436
+ let summary;
437
+ try {
438
+ const parsed = JSON.parse(cleaned);
439
+ summary = {
440
+ overview: String(parsed.overview || '').trim() || '待补充',
441
+ architecture: Array.isArray(parsed.architecture) ? parsed.architecture.map((item) => String(item).trim()).filter(Boolean) : [],
442
+ capabilities: Array.isArray(parsed.capabilities) ? parsed.capabilities.map((item) => String(item).trim()).filter(Boolean) : [],
443
+ collaborationNotes: Array.isArray(parsed.collaborationNotes) ? parsed.collaborationNotes.map((item) => String(item).trim()).filter(Boolean) : [],
444
+ };
445
+ }
446
+ catch (error) {
447
+ throw new Error(`解析大模型总结失败: ${error.message}`);
448
+ }
449
+ if (summary.architecture.length === 0)
450
+ summary.architecture = ['待补充'];
451
+ if (summary.capabilities.length === 0)
452
+ summary.capabilities = ['待补充'];
453
+ if (summary.collaborationNotes.length === 0)
454
+ summary.collaborationNotes = ['待补充'];
455
+ return renderJarvisMd({
456
+ projectName: input.projectName,
457
+ packageJson: input.packageJson,
458
+ projectTypes: input.projectTypes,
459
+ gitInfo: input.gitInfo,
460
+ dirTree: input.dirTree,
461
+ devCommands,
462
+ summary,
463
+ });
464
+ }
465
+ function generateBasicJarvisMd(input) {
466
+ const { cwd, projectName, packageJson: pkg, projectTypes, gitInfo, dirTree } = input;
467
+ const md = [];
468
+ md.push(`# ${pkg?.name || projectName}`);
469
+ md.push('');
470
+ if (pkg?.description) {
471
+ md.push(pkg.description);
472
+ md.push('');
473
+ }
474
+ md.push('## 项目概览');
475
+ md.push('');
476
+ md.push(`- 项目类型:${projectTypes.join(', ')}`);
477
+ md.push(`- 当前目录:\`${cwd}\``);
478
+ md.push(`- 版本:${pkg?.version || '未发现'}`);
479
+ md.push(`- Git 分支:${gitInfo?.branch || '未发现'}`);
480
+ md.push('');
481
+ md.push('## 目录结构');
482
+ md.push('');
483
+ md.push('```');
484
+ md.push(`${projectName}/`);
485
+ md.push(...dirTree);
486
+ md.push('```');
487
+ md.push('');
488
+ if (pkg?.scripts) {
489
+ md.push('## 开发命令');
490
+ md.push('');
491
+ md.push('```bash');
492
+ if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) {
493
+ md.push('pnpm install');
494
+ }
495
+ else if (fs.existsSync(path.join(cwd, 'package-lock.json'))) {
496
+ md.push('npm install');
497
+ }
498
+ if (pkg.scripts.dev)
499
+ md.push('npm run dev');
500
+ if (pkg.scripts.build)
501
+ md.push('npm run build');
502
+ if (pkg.scripts.test)
503
+ md.push('npm test');
504
+ md.push('```');
505
+ md.push('');
506
+ }
507
+ md.push('## 协作约定');
508
+ md.push('');
509
+ md.push('- 本文件为自动生成结果;若需更准确的业务背景,请补充 README 或项目文档。');
510
+ md.push('');
511
+ md.push(`> 由 ${APP_NAME} /init 自动生成`);
512
+ md.push('');
513
+ return md.join('\n');
514
+ }
515
+ export async function executeInit() {
127
516
  const cwd = process.cwd();
128
517
  const projectName = path.basename(cwd);
129
518
  const pkg = readPackageJson();
@@ -207,73 +596,36 @@ export function executeInit() {
207
596
  display.push('');
208
597
  }
209
598
  }
210
- // ===== 生成 JARVIS.md =====
211
- const md = [];
212
- md.push(`# ${pkg?.name || projectName}`);
213
- md.push('');
214
- if (pkg?.description) {
215
- md.push(pkg.description);
216
- md.push('');
217
- }
218
- md.push('---');
219
- md.push('');
220
- md.push('## 项目信息');
221
- md.push('');
222
- md.push(`| 项目 | 值 |`);
223
- md.push(`|------|-----|`);
224
- md.push(`| 名称 | ${pkg?.name || projectName} |`);
225
- if (pkg?.version)
226
- md.push(`| 版本 | ${pkg.version} |`);
227
- md.push(`| 类型 | ${projectTypes.join(', ')} |`);
228
- if (gitInfo?.branch)
229
- md.push(`| Git 分支 | ${gitInfo.branch} |`);
230
- if (gitInfo?.remote)
231
- md.push(`| Git 远程 | ${gitInfo.remote} |`);
232
- md.push('');
233
- md.push('## 目录结构');
234
- md.push('');
235
- md.push('```');
236
- md.push(`${projectName}/`);
237
- for (const line of dirTree) {
238
- md.push(line);
599
+ let jarvisMdContent = '';
600
+ let generationMode = '基础扫描';
601
+ try {
602
+ jarvisMdContent = await generateJarvisMdWithLLM({
603
+ projectName,
604
+ cwd,
605
+ packageJson: pkg,
606
+ projectTypes,
607
+ gitInfo,
608
+ fileStats,
609
+ dirTree,
610
+ });
611
+ generationMode = '大模型总结';
239
612
  }
240
- md.push('```');
241
- md.push('');
242
- // 快速开始
243
- if (pkg?.scripts) {
244
- md.push('## 快速开始');
245
- md.push('');
246
- md.push('```bash');
247
- if (pkg.scripts.install || fs.existsSync(path.join(cwd, 'package-lock.json'))) {
248
- md.push('# 安装依赖');
249
- md.push('npm install');
250
- md.push('');
251
- }
252
- if (pkg.scripts.dev) {
253
- md.push('# 开发模式');
254
- md.push(`npm run dev`);
255
- }
256
- else if (pkg.scripts.start) {
257
- md.push('# 启动');
258
- md.push(`npm run start`);
259
- }
260
- if (pkg.scripts.build) {
261
- md.push('');
262
- md.push('# 构建');
263
- md.push('npm run build');
264
- }
265
- md.push('```');
266
- md.push('');
613
+ catch {
614
+ jarvisMdContent = generateBasicJarvisMd({
615
+ cwd,
616
+ projectName,
617
+ packageJson: pkg,
618
+ projectTypes,
619
+ gitInfo,
620
+ dirTree,
621
+ });
267
622
  }
268
- md.push('---');
269
- md.push('');
270
- md.push(`> 由 ${APP_NAME} /init 自动生成`);
271
- md.push('');
272
- const jarvisMdContent = md.join('\n');
273
623
  const jarvisMdPath = path.join(cwd, 'JARVIS.md');
274
624
  const isNew = !fs.existsSync(jarvisMdPath);
275
625
  // 写入文件
276
626
  fs.writeFileSync(jarvisMdPath, jarvisMdContent, 'utf-8');
627
+ display.push(`[ 输出 ]`);
628
+ display.push(` 生成方式: ${generationMode}`);
277
629
  display.push(isNew ? '已生成 JARVIS.md' : '已更新 JARVIS.md');
278
630
  return {
279
631
  displayText: display.join('\n'),
@@ -20,6 +20,6 @@ const LOGO_COLORS = ['cyan', 'cyan', 'blueBright', 'blueBright', 'magenta', 'mag
20
20
  function WelcomeHeader({ width }) {
21
21
  const maxPath = Math.max(width - 10, 20);
22
22
  const showLogo = width >= 52;
23
- return (_jsxs(Box, { flexDirection: "column", paddingX: 1, width: width, children: [showLogo && (_jsx(Box, { flexDirection: "column", children: LOGO_LINES.map((line, i) => (_jsx(Text, { color: LOGO_COLORS[i], children: line }, i))) })), _jsxs(Box, { marginTop: 0, children: [_jsx(Text, { color: "gray" }), _jsx(Text, { color: "white", bold: true, children: "Your AI-Powered Dev Companion" }), _jsx(Text, { color: "gray" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "green", children: "\u6B22\u8FCE\u4F7F\u7528 Jarvis\uFF0C\u8BF7\u76F4\u63A5\u8F93\u5165\u4F60\u7684\u95EE\u9898\u6216\u9700\u6C42\u3002" }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: '─'.repeat(Math.min(width - 4, 48)) }) }), _jsxs(Box, { marginTop: 0, children: [_jsx(Text, { color: "gray", children: "model " }), _jsx(Text, { color: "cyan", children: MODEL_NAME }), _jsxs(Text, { color: "gray", children: [" ", APP_NAME, " "] }), _jsx(Text, { color: "magenta", children: APP_VERSION })] }), _jsx(Box, { children: _jsx(Text, { color: "gray", children: truncatePath(process.cwd(), maxPath) }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: '─'.repeat(Math.min(width - 4, 48)) }) }), _jsxs(Box, { children: [_jsx(Text, { color: "gray", children: "/" }), _jsx(Text, { color: "cyan", children: "init" }), _jsx(Text, { color: "gray", children: " \u521D\u59CB\u5316 " }), _jsx(Text, { color: "gray", children: "/" }), _jsx(Text, { color: "cyan", children: "help" }), _jsx(Text, { color: "gray", children: " \u5E2E\u52A9 " }), _jsx(Text, { color: "gray", children: "/" }), _jsx(Text, { color: "cyan", children: "new" }), _jsx(Text, { color: "gray", children: " \u65B0\u4F1A\u8BDD " }), _jsx(Text, { color: "gray", children: "/" }), _jsx(Text, { color: "cyan", children: "agent" }), _jsx(Text, { color: "gray", children: " \u5207\u6362" })] }), _jsx(Text, { children: ' ' })] }));
23
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, width: width, children: [showLogo && (_jsx(Box, { flexDirection: "column", children: LOGO_LINES.map((line, i) => (_jsx(Text, { color: LOGO_COLORS[i], children: line }, i))) })), _jsxs(Box, { marginTop: 0, children: [_jsx(Text, { color: "gray" }), _jsx(Text, { color: "white", bold: true, children: "Your AI-Powered Dev Companion" }), _jsx(Text, { color: "gray" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: '─'.repeat(Math.min(width - 4, 48)) }) }), _jsxs(Box, { marginTop: 0, children: [_jsx(Text, { color: "gray", children: "model " }), _jsx(Text, { color: "cyan", children: MODEL_NAME }), _jsxs(Text, { color: "gray", children: [" ", APP_NAME, " "] }), _jsx(Text, { color: "magenta", children: APP_VERSION })] }), _jsx(Box, { children: _jsx(Text, { color: "gray", children: truncatePath(process.cwd(), maxPath) }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: '─'.repeat(Math.min(width - 4, 48)) }) }), _jsxs(Box, { children: [_jsx(Text, { color: "gray", children: "/" }), _jsx(Text, { color: "cyan", children: "init" }), _jsx(Text, { color: "gray", children: " \u521D\u59CB\u5316 " }), _jsx(Text, { color: "gray", children: "/" }), _jsx(Text, { color: "cyan", children: "help" }), _jsx(Text, { color: "gray", children: " \u5E2E\u52A9 " }), _jsx(Text, { color: "gray", children: "/" }), _jsx(Text, { color: "cyan", children: "new" }), _jsx(Text, { color: "gray", children: " \u65B0\u4F1A\u8BDD " }), _jsx(Text, { color: "gray", children: "/" }), _jsx(Text, { color: "cyan", children: "agent" }), _jsx(Text, { color: "gray", children: " \u5207\u6362" })] }), _jsx(Text, { children: ' ' })] }));
24
24
  }
25
25
  export default React.memo(WelcomeHeader);
@@ -3,32 +3,49 @@ import { useState, useCallback, useRef, useEffect } from 'react';
3
3
  export function useDoubleCtrlCExit(exit) {
4
4
  const [countdown, setCountdown] = useState(null);
5
5
  const timerRef = useRef(null);
6
+ const armedRef = useRef(false);
7
+ const deadlineRef = useRef(null);
6
8
  const clearTimer = useCallback(() => {
7
9
  if (timerRef.current) {
8
- clearInterval(timerRef.current);
10
+ clearTimeout(timerRef.current);
9
11
  timerRef.current = null;
10
12
  }
13
+ armedRef.current = false;
14
+ deadlineRef.current = null;
11
15
  setCountdown(null);
12
16
  }, []);
13
17
  const handleCtrlC = useCallback(() => {
14
- if (countdown !== null) {
18
+ if (armedRef.current) {
15
19
  clearTimer();
16
20
  exit();
17
21
  return;
18
22
  }
19
- setCountdown(1);
20
- timerRef.current = setInterval(() => {
21
- setCountdown((prev) => {
22
- if (prev === null || prev <= 1) {
23
- clearTimer();
24
- return null;
25
- }
26
- return prev - 1;
27
- });
28
- }, 1000);
29
- }, [countdown, clearTimer, exit]);
23
+ armedRef.current = true;
24
+ deadlineRef.current = Date.now() + 3000;
25
+ setCountdown(3);
26
+ timerRef.current = setTimeout(() => {
27
+ clearTimer();
28
+ }, 3000);
29
+ }, [clearTimer, exit]);
30
+ useEffect(() => {
31
+ if (!armedRef.current || deadlineRef.current === null)
32
+ return;
33
+ const interval = setInterval(() => {
34
+ if (deadlineRef.current === null)
35
+ return;
36
+ const remainMs = deadlineRef.current - Date.now();
37
+ if (remainMs <= 0) {
38
+ clearTimer();
39
+ return;
40
+ }
41
+ setCountdown(Math.ceil(remainMs / 1000));
42
+ }, 100);
43
+ return () => clearInterval(interval);
44
+ }, [countdown, clearTimer]);
30
45
  // 组件卸载时清理
31
- useEffect(() => () => { if (timerRef.current)
32
- clearInterval(timerRef.current); }, []);
46
+ useEffect(() => () => {
47
+ if (timerRef.current)
48
+ clearTimeout(timerRef.current);
49
+ }, []);
33
50
  return { countdown, handleCtrlC };
34
51
  }
@@ -28,7 +28,11 @@ export default function REPL() {
28
28
  const { exit } = useApp();
29
29
  const width = useTerminalWidth();
30
30
  const windowFocused = useWindowFocus();
31
- const { countdown, handleCtrlC } = useDoubleCtrlCExit(exit);
31
+ const handleExit = useCallback(() => {
32
+ exit();
33
+ setTimeout(() => process.exit(0), 50);
34
+ }, [exit]);
35
+ const { countdown, handleCtrlC } = useDoubleCtrlCExit(handleExit);
32
36
  const { pushHistory, navigateUp, navigateDown, resetNavigation } = useInputHistory();
33
37
  const [messages, setMessages] = useState([]);
34
38
  const [input, setInput] = useState('');
@@ -181,7 +185,7 @@ export default function REPL() {
181
185
  }
182
186
  }
183
187
  else {
184
- const msg = executeSlashCommand(cmdName);
188
+ const msg = await executeSlashCommand(cmdName);
185
189
  if (msg)
186
190
  setMessages((prev) => [...prev, msg]);
187
191
  }
@@ -4,4 +4,4 @@ import { Message } from '../types/index.js';
4
4
  *
5
5
  * 纯函数,返回要追加的系统消息。不涉及 React 状态。
6
6
  */
7
- export declare function executeSlashCommand(cmdName: string): Message | null;
7
+ export declare function executeSlashCommand(cmdName: string): Promise<Message | null>;
@@ -9,10 +9,10 @@ import { listPermanentAuthorizations, DANGER_RULES, } from '../core/safeguard.js
9
9
  *
10
10
  * 纯函数,返回要追加的系统消息。不涉及 React 状态。
11
11
  */
12
- export function executeSlashCommand(cmdName) {
12
+ export async function executeSlashCommand(cmdName) {
13
13
  switch (cmdName) {
14
14
  case 'init': {
15
- const result = executeInit();
15
+ const result = await executeInit();
16
16
  return {
17
17
  id: `init-${Date.now()}`,
18
18
  type: 'system',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code4bug/jarvis-agent",
3
- "version": "1.1.7",
3
+ "version": "1.1.8",
4
4
  "description": "基于 React + TypeScript + Ink 构建的命令行智能体交互界面",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",