@code4bug/jarvis-agent 1.3.4 → 1.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -98,7 +98,21 @@ Jarvis 会按下面顺序加载配置,后者覆盖前者:
98
98
 
99
99
  - `system.model` 对应 `models` 里的 key
100
100
  - `extra_body` 会直接合并进请求体,方便兼容不同服务商
101
- - 如果没有可用模型配置,程序会回退到 Mock Service,适合本地界面联调
101
+
102
+ ### 首次启动配置
103
+
104
+ 如果未检测到可用的全局配置,Jarvis 启动时会先进入首次配置向导,而不是直接进入主界面。
105
+
106
+ 当前配置向导支持两种接入方式:
107
+
108
+ - OpenAI 兼容接口
109
+ - Ollama / 本地模型
110
+
111
+ 配置写入位置:
112
+
113
+ - `~/.jarvis/config.json`
114
+
115
+ 项目目录下的 `./.jarvis/config.json` 仍可作为本地覆盖配置使用。
102
116
 
103
117
  ## 常用命令
104
118
 
@@ -116,21 +130,44 @@ jarvis --version
116
130
  | `/init` | 扫描当前项目并生成 `JARVIS.md` |
117
131
  | `/new` | 开启新会话 |
118
132
  | `/resume` | 恢复历史会话 |
119
- | `/agent` | 切换智能体 |
133
+ | `/resume <ID>` | 直接恢复指定历史会话 |
134
+ | `/agent` | 打开智能体切换列表 |
135
+ | `/agent <名称>` | 切换智能体,重启后生效 |
120
136
  | `/permissions` | 查看持久化授权 |
121
137
  | `/skills` | 查看当前工具与外部 Skill |
122
138
  | `/session_clear` | 清理非当前会话历史 |
123
139
  | `/version` | 显示版本 |
124
140
  | `/help` | 显示帮助 |
141
+ | `/create_skill <描述>` | 根据描述创建新的 Skill |
142
+ | `/exit` | 退出应用 |
143
+ | `/quit` | 退出应用 |
144
+ | `/bye` | 退出应用 |
145
+ | `/read <路径>` | 以工具方式读取文件 |
146
+ | `/write <路径>` | 以工具方式写入文件 |
147
+ | `/bash <命令>` | 以工具方式执行命令 |
148
+ | `/ls <路径>` | 以工具方式列出目录 |
149
+ | `/search <关键词>` | 以工具方式搜索文件内容 |
125
150
 
126
151
  ### 快捷键
127
152
 
128
153
  | 快捷键 | 说明 |
129
154
  | --- | --- |
130
155
  | `Ctrl + L` | 清屏并开始新会话 |
131
- | `Ctrl + C` | 退出 |
132
- | `Esc` | 中断当前任务或清空输入 |
156
+ | `Ctrl + C` | 3 秒内连续按两次退出 |
157
+ | `Ctrl + O` | 切换详情视图 |
158
+ | `Esc` | 中断当前任务;空闲时双击清空输入 |
133
159
  | `Alt/Option + Enter` | 输入换行 |
160
+ | `Tab` | 输入为空时填入提示词;在斜杠菜单中补全当前项 |
161
+ | `? + Enter` | 输出快捷键帮助信息 |
162
+
163
+ ### 输入与交互
164
+
165
+ - 输入 `/` 可打开斜杠菜单
166
+ - 斜杠菜单中可使用 `↑ / ↓` 切换命令
167
+ - 在斜杠菜单中按 `Tab` 可补全当前命令
168
+ - 在斜杠菜单中按 `Enter` 可提交当前命令
169
+ - 对于多行输入,`↑ / ↓` 用于在输入框内移动光标
170
+ - 对于单行输入,`↑ / ↓` 用于切换历史输入
134
171
 
135
172
  ## 内置工具
136
173
 
@@ -209,6 +246,12 @@ Agent 通过 Markdown 文件定义元信息与系统提示词,切换结果会
209
246
 
210
247
  每次会话会保存消息、摘要、更新时间和累计 Token。
211
248
 
249
+ 支持能力:
250
+
251
+ - `/resume` 从列表恢复历史会话
252
+ - `/resume <ID>` 直接恢复指定会话
253
+ - `/session_clear` 清理除当前会话外的历史记录
254
+
212
255
  ### 长期记忆
213
256
 
214
257
  - 记忆文件:`~/.jarvis/MEMORY.md`
@@ -91,6 +91,7 @@ vibe: 你的全能助手,有问必答,随时待命。
91
91
  - 涉及代码时,代码必须可直接运行,无语法错误
92
92
  - 多方案时先推荐最优解,再列备选
93
93
  - 根据问题类型灵活调整输出格式:代码用代码块、信息用结构化列表、分析用表格等
94
+ - 用户消息里的文件名、路径、命令、参数、扩展名必须逐字保留,不得擅自纠错或改写;若目标不存在,先检查目录或搜索再判断
94
95
 
95
96
  ### 沟通风格
96
97
  - 默认中文回答,代码注释优先中文
@@ -1,11 +1,13 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import React, { useMemo } from 'react';
3
- import { Box, Text } from 'ink';
3
+ import { Box, Text, useStdout } from 'ink';
4
4
  import { Marked } from 'marked';
5
5
  // @ts-ignore — marked-terminal 无内置类型声明
6
6
  import { markedTerminal } from 'marked-terminal';
7
7
  // @ts-ignore
8
8
  import Table from 'cli-table3';
9
+ // @ts-ignore — wrap-ansi 无显式类型声明
10
+ import wrapAnsi from 'wrap-ansi';
9
11
  // ===== ANSI 颜色常量 =====
10
12
  const RESET = '\x1b[0m';
11
13
  const BG_CODE = '\x1b[48;5;236m'; // 深灰背景
@@ -154,6 +156,29 @@ function renderMarkdown(text) {
154
156
  return text;
155
157
  }
156
158
  }
159
+ function wrapRenderedLine(line, width) {
160
+ if (!line)
161
+ return [''];
162
+ const safeWidth = Math.max(width, 12);
163
+ const plainLine = stripAnsi(line);
164
+ const listPrefixMatch = plainLine.match(/^(\s*(?:[*+-]|\d+\.)\s+)/);
165
+ if (!listPrefixMatch) {
166
+ return wrapAnsi(line, safeWidth, {
167
+ hard: true,
168
+ trim: false,
169
+ wordWrap: false,
170
+ }).split('\n');
171
+ }
172
+ const prefix = listPrefixMatch[1];
173
+ const content = line.slice(prefix.length);
174
+ const contentWidth = Math.max(safeWidth - prefix.length, 8);
175
+ const wrappedContent = wrapAnsi(content, contentWidth, {
176
+ hard: true,
177
+ trim: false,
178
+ wordWrap: false,
179
+ }).split('\n');
180
+ return wrappedContent.map((segment, index) => (`${index === 0 ? prefix : ' '.repeat(prefix.length)}${segment}`));
181
+ }
157
182
  /**
158
183
  * Markdown 终端渲染组件
159
184
  * 支持表格(动态列宽+自动换行)、代码块(深色背景+边框)、加粗、列表等
@@ -164,10 +189,14 @@ function renderMarkdown(text) {
164
189
  * 与 ANSI 转义序列冲突,出现光标错位和输出错乱。
165
190
  */
166
191
  function MarkdownText({ text, color }) {
192
+ const { stdout } = useStdout();
167
193
  const lines = useMemo(() => {
168
194
  const rendered = renderMarkdown(text);
169
- return rendered.split('\n');
170
- }, [text]);
171
- return (_jsx(Box, { flexDirection: "column", children: lines.map((line, i) => (_jsx(Text, { wrap: "wrap", color: color, children: line }, i))) }));
195
+ const availableWidth = Math.max(Math.min(stdout?.columns ?? 80, 100) - 6, 20);
196
+ return rendered
197
+ .split('\n')
198
+ .flatMap((line) => wrapRenderedLine(line, availableWidth));
199
+ }, [text, stdout]);
200
+ return (_jsx(Box, { flexDirection: "column", children: lines.map((line, i) => (_jsx(Text, { color: color, children: line || ' ' }, i))) }));
172
201
  }
173
202
  export default React.memo(MarkdownText);
@@ -3,6 +3,7 @@ import React from 'react';
3
3
  import { Box, Text } from 'ink';
4
4
  import Spinner from 'ink-spinner';
5
5
  import MarkdownText from './MarkdownText.js';
6
+ import ShortcutHelpMessage from './ShortcutHelpMessage.js';
6
7
  // 状态圆点 icon,根据消息类型 + 状态决定颜色
7
8
  // reasoning 完成 → 白色 / tool_exec 成功 → 绿色 / error → 红色 / aborted → 黄色 / pending → 黄色
8
9
  function statusDot(status, type) {
@@ -80,7 +81,7 @@ function MessageItem({ msg, showDetails = false }) {
80
81
  _jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsxs(Text, { color: dotColor, children: [dot, " "] }), _jsxs(Text, { color: "blue", dimColor: true, children: [msg.subAgentId, " \u203A "] })] }), _jsx(Box, { marginLeft: 2, flexDirection: "column", children: _jsx(MarkdownText, { text: msg.content }) })] })) : (_jsxs(Box, { flexDirection: "row", alignItems: "flex-start", children: [_jsxs(Text, { color: dotColor, children: [dot, " "] }), _jsx(Box, { flexDirection: "column", flexShrink: 1, children: _jsx(MarkdownText, { text: msg.content }) })] })), _jsx(MessageStats, { msg: msg, show: showDetails })] }));
81
82
  }
82
83
  if (msg.type === 'system' && msg.content) {
83
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { flexDirection: "row", alignItems: "flex-start", children: [_jsxs(Text, { color: dotColor, children: [dot, " "] }), _jsx(Box, { flexDirection: "column", flexShrink: 1, children: _jsx(MarkdownText, { text: msg.content, color: "gray" }) })] }), _jsx(MessageStats, { msg: msg, show: showDetails })] }));
84
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { flexDirection: "row", alignItems: "flex-start", children: [_jsxs(Text, { color: dotColor, children: [dot, " "] }), _jsx(Box, { flexDirection: "column", flexShrink: 1, children: msg.systemKind === 'shortcut_help' ? (_jsx(ShortcutHelpMessage, {})) : (_jsx(MarkdownText, { text: msg.content, color: "gray" })) })] }), _jsx(MessageStats, { msg: msg, show: showDetails })] }));
84
85
  }
85
86
  if (msg.status !== 'pending' && msg.content) {
86
87
  return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { flexDirection: "row", alignItems: "flex-start", children: [_jsxs(Text, { color: dotColor, children: [dot, " "] }), _jsx(Box, { flexDirection: "column", flexShrink: 1, children: _jsx(MarkdownText, { text: msg.content, color: "gray" }) })] }), _jsx(MessageStats, { msg: msg, show: showDetails })] }));
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+ declare function ShortcutHelpMessage(): import("react/jsx-runtime").JSX.Element;
3
+ declare const _default: React.MemoExoticComponent<typeof ShortcutHelpMessage>;
4
+ export default _default;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { Box, Text } from 'ink';
4
+ import { getShortcutPlatformNote, getShortcutSections } from '../config/shortcuts.js';
5
+ function ShortcutHelpMessage() {
6
+ const sections = getShortcutSections();
7
+ const note = getShortcutPlatformNote();
8
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "\u5FEB\u6377\u952E\u5E2E\u52A9" }), _jsx(Text, { color: "gray", children: note }), sections.map((section) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "yellow", bold: true, children: section.title }), section.items.map((item, index) => (_jsxs(Box, { children: [_jsx(Box, { width: 24, flexShrink: 0, children: _jsx(Text, { color: "green", bold: true, children: item.key }) }), _jsx(Text, { color: "white", children: item.description })] }, `${section.title}-${item.key}-${index}`)))] }, section.title)))] }));
9
+ }
10
+ export default React.memo(ShortcutHelpMessage);
@@ -22,6 +22,6 @@ function WelcomeHeader({ width }) {
22
22
  const showLogo = width >= 52;
23
23
  const appName = getAppName();
24
24
  const modelName = getModelName();
25
- 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: modelName }), _jsxs(Text, { color: "gray", children: [" ", appName, " "] }), _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: ' ' })] }));
25
+ 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: modelName }), _jsxs(Text, { color: "gray", children: [" ", appName, " "] }), _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, { color: "yellow", bold: true, children: "?" }), _jsx(Text, { color: "gray", children: " \u5FEB\u6377\u952E\u63D0\u793A" })] }), _jsx(Text, { children: ' ' })] }));
26
26
  }
27
27
  export default React.memo(WelcomeHeader);
@@ -0,0 +1,13 @@
1
+ export interface ShortcutItem {
2
+ key: string;
3
+ description: string;
4
+ }
5
+ export interface ShortcutSection {
6
+ title: string;
7
+ items: ShortcutItem[];
8
+ }
9
+ export declare function getShortcutPlatformLabel(): string;
10
+ export declare function getShortcutAltKeyLabel(): string;
11
+ export declare function getShortcutPlatformNote(): string;
12
+ export declare function getShortcutSections(): ShortcutSection[];
13
+ export declare function buildShortcutHelpText(): string;
@@ -0,0 +1,67 @@
1
+ const isMac = process.platform === 'darwin';
2
+ const isWindows = process.platform === 'win32';
3
+ export function getShortcutPlatformLabel() {
4
+ if (isMac)
5
+ return 'macOS';
6
+ if (isWindows)
7
+ return 'Windows';
8
+ return 'Linux';
9
+ }
10
+ export function getShortcutAltKeyLabel() {
11
+ return isMac ? 'Option' : 'Alt';
12
+ }
13
+ export function getShortcutPlatformNote() {
14
+ if (isMac)
15
+ return '当前平台:macOS(终端内快捷键使用 Ctrl / Option,通常不是 Cmd)';
16
+ if (isWindows)
17
+ return '当前平台:Windows(快捷键使用 Ctrl / Alt)';
18
+ return '当前平台:Linux(快捷键使用 Ctrl / Alt)';
19
+ }
20
+ export function getShortcutSections() {
21
+ const altKey = getShortcutAltKeyLabel();
22
+ const platform = getShortcutPlatformLabel();
23
+ return [
24
+ {
25
+ title: '通用快捷键',
26
+ items: [
27
+ { key: '? + Enter', description: '查看全部快捷键说明' },
28
+ { key: 'Ctrl + C', description: '退出 Jarvis(需连续按两次)' },
29
+ { key: 'Ctrl + L', description: '清屏并开始新会话' },
30
+ { key: 'Ctrl + O', description: '切换详情视图' },
31
+ { key: 'Esc', description: '中断当前任务;空闲时双击清空输入' },
32
+ { key: `${altKey} + Enter`, description: '插入换行' },
33
+ { key: 'Tab', description: '输入为空时填入提示词;斜杠菜单中补全选中项' },
34
+ { key: 'Enter', description: '发送消息;斜杠菜单中提交当前选中命令' },
35
+ ],
36
+ },
37
+ {
38
+ title: `输入编辑(${platform})`,
39
+ items: [
40
+ { key: '← / →', description: '左右移动光标' },
41
+ { key: '↑ / ↓', description: '多行输入时上下移动光标' },
42
+ { key: '↑ / ↓', description: '单行输入时切换历史记录' },
43
+ ],
44
+ },
45
+ {
46
+ title: '斜杠菜单',
47
+ items: [
48
+ { key: '/', description: '打开命令菜单' },
49
+ { key: '↑ / ↓', description: '切换命令' },
50
+ { key: 'Tab / Enter', description: '补全或提交当前命令' },
51
+ { key: 'Esc', description: '关闭命令菜单' },
52
+ ],
53
+ },
54
+ ];
55
+ }
56
+ export function buildShortcutHelpText() {
57
+ return [
58
+ '快捷键帮助',
59
+ getShortcutPlatformNote(),
60
+ '',
61
+ ...getShortcutSections().flatMap((section) => [
62
+ `${section.title}:`,
63
+ ...section.items.map((item) => ` ${item.key} ${item.description}`),
64
+ '',
65
+ ]),
66
+ ].join('\n').trim();
67
+ }
@@ -19,6 +19,7 @@ import { subscribeAgentCount, getActiveAgentCount } from '../core/spawnRegistry.
19
19
  import { logError, logInfo, logWarn } from '../core/logger.js';
20
20
  import { getAgentSubCommands } from '../commands/index.js';
21
21
  import { setActiveAgent } from '../config/agentState.js';
22
+ import { buildShortcutHelpText } from '../config/shortcuts.js';
22
23
  import { hideTerminalCursor, showTerminalCursor } from '../terminal/cursor.js';
23
24
  export default function REPL() {
24
25
  const { exit } = useApp();
@@ -200,6 +201,22 @@ export default function REPL() {
200
201
  const trimmed = value.trim();
201
202
  if (!trimmed || isProcessing || !engineRef.current)
202
203
  return;
204
+ if (trimmed === '?') {
205
+ setInput('');
206
+ slashMenu.setSlashMenuVisible(false);
207
+ if (HIDE_WELCOME_AFTER_INPUT)
208
+ setShowWelcome(false);
209
+ setMessages((prev) => [...prev, {
210
+ id: `shortcut-help-${Date.now()}`,
211
+ type: 'system',
212
+ status: 'success',
213
+ systemKind: 'shortcut_help',
214
+ content: buildShortcutHelpText(),
215
+ timestamp: Date.now(),
216
+ }]);
217
+ logInfo('ui.shortcut_help.open');
218
+ return;
219
+ }
203
220
  logInfo('ui.submit', {
204
221
  inputLength: trimmed.length,
205
222
  isSlashCommand: trimmed.startsWith('/'),
@@ -186,6 +186,9 @@ export class LLMServiceImpl {
186
186
  '\n\n[安全围栏] 系统已内置安全围栏(Safeguard),会自动拦截危险命令并弹出交互式确认菜单。' +
187
187
  '当你需要执行任何命令时,直接调用 Bash 工具即可,不要自行判断命令是否危险,不要用文字询问用户"是否确认执行"。' +
188
188
  '安全围栏会自动处理拦截和用户确认流程。' +
189
+ '\n\n[文件与命令保真] 用户消息中出现的文件名、目录名、路径、命令、参数、扩展名,必须按原文逐字使用,不得自行纠错、改写、补全或翻译。' +
190
+ '即使你怀疑用户有拼写错误,也必须先按原文检查;若原文目标不存在,再基于目录扫描结果给出候选项。' +
191
+ '尤其不要把相似文件名擅自改成你认为“更正确”的拼写。' +
189
192
  '\n\n[文件修改约束] 修改已有文件时,先读取目标文件,再优先使用局部修改方式。' +
190
193
  '如果只改少量内容,优先使用 write_file 的 replace 模式;需要结构化补丁时使用 diff 模式;只有新建文件或确实需要大范围重写时才使用 overwrite。';
191
194
  // 追加系统环境信息,帮助 LLM 感知用户运行环境
@@ -6,6 +6,7 @@ export interface Message {
6
6
  status: MessageStatus;
7
7
  content: string;
8
8
  timestamp: number;
9
+ systemKind?: 'shortcut_help';
9
10
  /** 耗时 ms */
10
11
  duration?: number;
11
12
  /** token 统计 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code4bug/jarvis-agent",
3
- "version": "1.3.4",
3
+ "version": "1.3.6",
4
4
  "description": "基于 React + TypeScript + Ink 构建的命令行智能体交互界面",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",