@code4bug/jarvis-agent 1.0.2
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/LICENSE +21 -0
- package/README.md +227 -0
- package/dist/agents/code-reviewer.md +69 -0
- package/dist/agents/dba.md +68 -0
- package/dist/agents/finance-advisor.md +81 -0
- package/dist/agents/index.d.ts +31 -0
- package/dist/agents/index.js +86 -0
- package/dist/agents/jarvis.md +95 -0
- package/dist/agents/stock-trader.md +81 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +9 -0
- package/dist/commands/index.d.ts +19 -0
- package/dist/commands/index.js +79 -0
- package/dist/commands/init.d.ts +15 -0
- package/dist/commands/init.js +283 -0
- package/dist/components/DangerConfirm.d.ts +17 -0
- package/dist/components/DangerConfirm.js +50 -0
- package/dist/components/MarkdownText.d.ts +12 -0
- package/dist/components/MarkdownText.js +166 -0
- package/dist/components/MessageItem.d.ts +8 -0
- package/dist/components/MessageItem.js +78 -0
- package/dist/components/MultilineInput.d.ts +34 -0
- package/dist/components/MultilineInput.js +437 -0
- package/dist/components/SlashCommandMenu.d.ts +16 -0
- package/dist/components/SlashCommandMenu.js +43 -0
- package/dist/components/StatusBar.d.ts +8 -0
- package/dist/components/StatusBar.js +26 -0
- package/dist/components/StreamingText.d.ts +6 -0
- package/dist/components/StreamingText.js +10 -0
- package/dist/components/WelcomeHeader.d.ts +6 -0
- package/dist/components/WelcomeHeader.js +25 -0
- package/dist/config/agentState.d.ts +16 -0
- package/dist/config/agentState.js +65 -0
- package/dist/config/constants.d.ts +25 -0
- package/dist/config/constants.js +67 -0
- package/dist/config/loader.d.ts +30 -0
- package/dist/config/loader.js +64 -0
- package/dist/config/systemInfo.d.ts +12 -0
- package/dist/config/systemInfo.js +95 -0
- package/dist/core/QueryEngine.d.ts +52 -0
- package/dist/core/QueryEngine.js +246 -0
- package/dist/core/hint.d.ts +14 -0
- package/dist/core/hint.js +279 -0
- package/dist/core/query.d.ts +24 -0
- package/dist/core/query.js +245 -0
- package/dist/core/safeguard.d.ts +96 -0
- package/dist/core/safeguard.js +236 -0
- package/dist/hooks/useFocus.d.ts +12 -0
- package/dist/hooks/useFocus.js +35 -0
- package/dist/hooks/useInputHistory.d.ts +14 -0
- package/dist/hooks/useInputHistory.js +102 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -0
- package/dist/screens/repl.d.ts +1 -0
- package/dist/screens/repl.js +842 -0
- package/dist/services/api/llm.d.ts +27 -0
- package/dist/services/api/llm.js +314 -0
- package/dist/services/api/mock.d.ts +9 -0
- package/dist/services/api/mock.js +102 -0
- package/dist/skills/index.d.ts +23 -0
- package/dist/skills/index.js +232 -0
- package/dist/skills/loader.d.ts +45 -0
- package/dist/skills/loader.js +108 -0
- package/dist/tools/createSkill.d.ts +8 -0
- package/dist/tools/createSkill.js +255 -0
- package/dist/tools/index.d.ts +16 -0
- package/dist/tools/index.js +23 -0
- package/dist/tools/listDirectory.d.ts +2 -0
- package/dist/tools/listDirectory.js +20 -0
- package/dist/tools/readFile.d.ts +2 -0
- package/dist/tools/readFile.js +17 -0
- package/dist/tools/runCommand.d.ts +2 -0
- package/dist/tools/runCommand.js +69 -0
- package/dist/tools/searchFiles.d.ts +2 -0
- package/dist/tools/searchFiles.js +45 -0
- package/dist/tools/writeFile.d.ts +2 -0
- package/dist/tools/writeFile.js +42 -0
- package/dist/types/index.d.ts +86 -0
- package/dist/types/index.js +2 -0
- package/package.json +55 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 统一安全围栏 — 危险命令拦截 + 敏感信息保护 + 双模式授权
|
|
3
|
+
*
|
|
4
|
+
* 授权模式:
|
|
5
|
+
* 1. 临时授权(once) — 仅当前会话有效,会话重置后失效
|
|
6
|
+
* 2. 持久授权(always)— 写入 ~/.jarvis/.permissions.json,跨会话永久生效
|
|
7
|
+
*
|
|
8
|
+
* critical 级别命令不可授权,high 级别支持两种授权模式。
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import os from 'os';
|
|
13
|
+
// ===== 持久化路径 =====
|
|
14
|
+
const JARVIS_DIR = path.join(os.homedir(), '.jarvis');
|
|
15
|
+
const PERMISSIONS_FILE = path.join(JARVIS_DIR, '.permissions.json');
|
|
16
|
+
/** 危险命令规则表 */
|
|
17
|
+
export const DANGER_RULES = [
|
|
18
|
+
// === critical: 直接禁止,不可授权 ===
|
|
19
|
+
{ name: 'fork-bomb', pattern: /:\(\)\{.*\|.*\};:/, level: 'critical', reason: 'Fork bomb,会导致系统崩溃' },
|
|
20
|
+
{ name: 'dev-null-disk', pattern: />\s*\/dev\/sda/, level: 'critical', reason: '直接写入磁盘设备,会破坏文件系统' },
|
|
21
|
+
{ name: 'mkfs', pattern: /\bmkfs\b/, level: 'critical', reason: '格式化磁盘,数据不可恢复' },
|
|
22
|
+
{ name: 'dd-disk', pattern: /\bdd\b.*\bof=\/dev\//, level: 'critical', reason: '直接写入磁盘设备,数据不可恢复' },
|
|
23
|
+
// === high: 需要用户确认 ===
|
|
24
|
+
{ name: 'rm-rf-root', pattern: /\brm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+(-[a-zA-Z]*r[a-zA-Z]*\s+)?|(-[a-zA-Z]*r[a-zA-Z]*\s+(-[a-zA-Z]*f[a-zA-Z]*\s+)?))\s*\/(\s|$)/, level: 'high', reason: '递归强制删除根目录,会摧毁整个系统' },
|
|
25
|
+
{ name: 'rm-rf-wildcard', pattern: /\brm\s+-[a-zA-Z]*r[a-zA-Z]*f?\s+.*\*/, level: 'high', reason: '递归删除通配符匹配的文件,范围不可控' },
|
|
26
|
+
{ name: 'rm-recursive', pattern: /\brm\s+(-[a-zA-Z]*r[a-zA-Z]*)\b/, level: 'high', reason: '递归删除目录,操作不可逆,请确认目标路径' },
|
|
27
|
+
{ name: 'chmod-777', pattern: /\bchmod\s+(-R\s+)?777\b/, level: 'high', reason: '开放所有权限,存在严重安全隐患' },
|
|
28
|
+
{ name: 'curl-pipe-sh', pattern: /\bcurl\b.*\|\s*(ba)?sh/, level: 'high', reason: '从网络下载并直接执行脚本,存在供应链攻击风险' },
|
|
29
|
+
{ name: 'wget-pipe-sh', pattern: /\bwget\b.*\|\s*(ba)?sh/, level: 'high', reason: '从网络下载并直接执行脚本,存在供应链攻击风险' },
|
|
30
|
+
{ name: 'shutdown', pattern: /\b(shutdown|reboot|halt|poweroff)\b/, level: 'high', reason: '关机/重启命令,会中断所有服务' },
|
|
31
|
+
{ name: 'kill-all', pattern: /\bkill\s+-9\s+-1\b/, level: 'high', reason: '杀死所有用户进程' },
|
|
32
|
+
{ name: 'iptables-flush', pattern: /\biptables\s+-F\b/, level: 'high', reason: '清空防火墙规则,可能暴露系统' },
|
|
33
|
+
{ name: 'passwd-change', pattern: /\b(passwd|chpasswd|usermod)\b/, level: 'high', reason: '修改用户密码/账户,影响系统安全' },
|
|
34
|
+
{ name: 'sudoers-edit', pattern: /\/etc\/sudoers/, level: 'high', reason: '修改 sudoers 文件,影响权限体系' },
|
|
35
|
+
{ name: 'ssh-key-ops', pattern: /\.ssh\/(authorized_keys|id_rsa|id_ed25519)/, level: 'high', reason: '操作 SSH 密钥,影响远程访问安全' },
|
|
36
|
+
{ name: 'env-export', pattern: /\benv\b|\bprintenv\b|\bexport\b.*=/, level: 'high', reason: '操作环境变量,可能泄露敏感信息' },
|
|
37
|
+
{ name: 'history-access', pattern: /\bhistory\b|\.bash_history|\.zsh_history/, level: 'high', reason: '访问命令历史,可能包含敏感信息' },
|
|
38
|
+
{ name: 'network-sniff', pattern: /\b(tcpdump|wireshark|nmap|netcat|nc)\b/, level: 'high', reason: '网络嗅探/扫描工具,可能用于攻击' },
|
|
39
|
+
{ name: 'crontab-edit', pattern: /\bcrontab\s+-[er]\b/, level: 'high', reason: '修改定时任务,可能植入持久化后门' },
|
|
40
|
+
{ name: 'systemctl', pattern: /\bsystemctl\s+(stop|disable|mask|restart)\b/, level: 'high', reason: '操作系统服务,可能中断关键服务' },
|
|
41
|
+
];
|
|
42
|
+
/** 敏感信息匹配规则 — 用于输出脱敏 */
|
|
43
|
+
export const SENSITIVE_PATTERNS = [
|
|
44
|
+
{ name: 'AWS Access Key', pattern: /\b(AKIA[0-9A-Z]{16})\b/g, replacement: '[AWS_ACCESS_KEY]' },
|
|
45
|
+
{ name: 'AWS Secret Key', pattern: /\b([A-Za-z0-9/+=]{40})\b/g, replacement: '[AWS_SECRET_KEY]' },
|
|
46
|
+
{ name: 'Generic API Key', pattern: /\b(api[_-]?key|apikey)\s*[:=]\s*['"]?([^\s'"]+)/gi, replacement: '$1=[REDACTED]' },
|
|
47
|
+
{ name: 'Generic Secret', pattern: /\b(secret|token|password|passwd|pwd)\s*[:=]\s*['"]?([^\s'"]+)/gi, replacement: '$1=[REDACTED]' },
|
|
48
|
+
{ name: 'Bearer Token', pattern: /Bearer\s+[A-Za-z0-9\-._~+/]+=*/g, replacement: 'Bearer [REDACTED]' },
|
|
49
|
+
{ name: 'Private Key Block', pattern: /-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----[\s\S]*?-----END\s+(RSA\s+)?PRIVATE\s+KEY-----/g, replacement: '[PRIVATE_KEY_REDACTED]' },
|
|
50
|
+
{ name: 'Connection String', pattern: /\b(mongodb|postgres|mysql|redis):\/\/[^\s]+/gi, replacement: '[CONNECTION_STRING_REDACTED]' },
|
|
51
|
+
{ name: 'Env Var Inline', pattern: /\$\{?[A-Z_]*(KEY|SECRET|TOKEN|PASSWORD|PASSWD)[A-Z_]*\}?/gi, replacement: '[ENV_VAR_REDACTED]' },
|
|
52
|
+
];
|
|
53
|
+
// ===== 持久化授权:文件读写 =====
|
|
54
|
+
/** 读取持久化授权文件 */
|
|
55
|
+
function loadPermissions() {
|
|
56
|
+
const defaults = {
|
|
57
|
+
_comment: '安全围栏持久化授权配置 — 由 /authorize always 写入,可手动编辑',
|
|
58
|
+
rules: [],
|
|
59
|
+
commands: [],
|
|
60
|
+
};
|
|
61
|
+
try {
|
|
62
|
+
if (!fs.existsSync(PERMISSIONS_FILE))
|
|
63
|
+
return defaults;
|
|
64
|
+
const raw = fs.readFileSync(PERMISSIONS_FILE, 'utf-8');
|
|
65
|
+
const parsed = JSON.parse(raw);
|
|
66
|
+
return {
|
|
67
|
+
_comment: parsed._comment ?? defaults._comment,
|
|
68
|
+
rules: Array.isArray(parsed.rules) ? parsed.rules : [],
|
|
69
|
+
commands: Array.isArray(parsed.commands) ? parsed.commands : [],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return defaults;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** 写入持久化授权文件 */
|
|
77
|
+
function savePermissions(perms) {
|
|
78
|
+
try {
|
|
79
|
+
if (!fs.existsSync(JARVIS_DIR)) {
|
|
80
|
+
fs.mkdirSync(JARVIS_DIR, { recursive: true });
|
|
81
|
+
}
|
|
82
|
+
fs.writeFileSync(PERMISSIONS_FILE, JSON.stringify(perms, null, 2), 'utf-8');
|
|
83
|
+
}
|
|
84
|
+
catch { /* 静默失败,不影响主流程 */ }
|
|
85
|
+
}
|
|
86
|
+
/** 检查命令是否已持久化授权 */
|
|
87
|
+
function isPermanentlyAuthorized(command, ruleName) {
|
|
88
|
+
const perms = loadPermissions();
|
|
89
|
+
// 1. 按规则名称授权(整类放行)
|
|
90
|
+
if (ruleName && perms.rules.includes(ruleName))
|
|
91
|
+
return true;
|
|
92
|
+
// 2. 按具体命令授权
|
|
93
|
+
const trimmed = command.trim();
|
|
94
|
+
return perms.commands.some((e) => e.command === trimmed);
|
|
95
|
+
}
|
|
96
|
+
// ===== 临时授权:会话级内存 =====
|
|
97
|
+
/** 会话级临时授权集合 */
|
|
98
|
+
const sessionAuthorizedCommands = new Set();
|
|
99
|
+
const sessionAuthorizedRules = new Set();
|
|
100
|
+
// ===== 对外授权 API =====
|
|
101
|
+
/**
|
|
102
|
+
* 授权命令
|
|
103
|
+
* @param command 具体命令字符串
|
|
104
|
+
* @param mode 'once' 仅本次会话 | 'always' 持久化到文件
|
|
105
|
+
* @param ruleName 可选,关联的规则名称
|
|
106
|
+
*/
|
|
107
|
+
export function authorizeCommand(command, mode = 'once', ruleName) {
|
|
108
|
+
const trimmed = command.trim();
|
|
109
|
+
if (mode === 'once') {
|
|
110
|
+
sessionAuthorizedCommands.add(trimmed);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
// 持久化写入
|
|
114
|
+
const perms = loadPermissions();
|
|
115
|
+
if (!perms.commands.some((e) => e.command === trimmed)) {
|
|
116
|
+
perms.commands.push({
|
|
117
|
+
ruleName: ruleName ?? 'unknown',
|
|
118
|
+
command: trimmed,
|
|
119
|
+
grantedAt: new Date().toISOString(),
|
|
120
|
+
});
|
|
121
|
+
savePermissions(perms);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* 按规则名称授权(整类命令放行)
|
|
127
|
+
* @param ruleName 规则名称(对应 DangerRule.name)
|
|
128
|
+
* @param mode 'once' 仅本次会话 | 'always' 持久化到文件
|
|
129
|
+
*/
|
|
130
|
+
export function authorizeRule(ruleName, mode = 'once') {
|
|
131
|
+
if (mode === 'once') {
|
|
132
|
+
sessionAuthorizedRules.add(ruleName);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
const perms = loadPermissions();
|
|
136
|
+
if (!perms.rules.includes(ruleName)) {
|
|
137
|
+
perms.rules.push(ruleName);
|
|
138
|
+
savePermissions(perms);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* 撤销持久化授权
|
|
144
|
+
* @param target 规则名称或具体命令
|
|
145
|
+
*/
|
|
146
|
+
export function revokeAuthorization(target) {
|
|
147
|
+
const perms = loadPermissions();
|
|
148
|
+
let changed = false;
|
|
149
|
+
// 尝试从 rules 中移除
|
|
150
|
+
const ruleIdx = perms.rules.indexOf(target);
|
|
151
|
+
if (ruleIdx !== -1) {
|
|
152
|
+
perms.rules.splice(ruleIdx, 1);
|
|
153
|
+
changed = true;
|
|
154
|
+
}
|
|
155
|
+
// 尝试从 commands 中移除
|
|
156
|
+
const cmdIdx = perms.commands.findIndex((e) => e.command === target || e.ruleName === target);
|
|
157
|
+
if (cmdIdx !== -1) {
|
|
158
|
+
perms.commands.splice(cmdIdx, 1);
|
|
159
|
+
changed = true;
|
|
160
|
+
}
|
|
161
|
+
if (changed)
|
|
162
|
+
savePermissions(perms);
|
|
163
|
+
return changed;
|
|
164
|
+
}
|
|
165
|
+
/** 列出所有持久化授权 */
|
|
166
|
+
export function listPermanentAuthorizations() {
|
|
167
|
+
return loadPermissions();
|
|
168
|
+
}
|
|
169
|
+
/** 检查命令是否已授权(临时 + 持久化) */
|
|
170
|
+
export function isAuthorized(command, ruleName) {
|
|
171
|
+
const trimmed = command.trim();
|
|
172
|
+
// 1. 会话级临时授权
|
|
173
|
+
if (sessionAuthorizedCommands.has(trimmed))
|
|
174
|
+
return true;
|
|
175
|
+
if (ruleName && sessionAuthorizedRules.has(ruleName))
|
|
176
|
+
return true;
|
|
177
|
+
// 2. 持久化授权
|
|
178
|
+
return isPermanentlyAuthorized(trimmed, ruleName);
|
|
179
|
+
}
|
|
180
|
+
/** 清空会话级临时授权(会话重置时调用,不影响持久化授权) */
|
|
181
|
+
export function clearAuthorizations() {
|
|
182
|
+
sessionAuthorizedCommands.clear();
|
|
183
|
+
sessionAuthorizedRules.clear();
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* 校验命令是否安全
|
|
187
|
+
*/
|
|
188
|
+
export function validateCommand(command) {
|
|
189
|
+
for (const rule of DANGER_RULES) {
|
|
190
|
+
if (rule.pattern.test(command)) {
|
|
191
|
+
if (rule.level === 'critical') {
|
|
192
|
+
return {
|
|
193
|
+
allowed: false,
|
|
194
|
+
reason: `🚫 [禁止执行] ${rule.reason}`,
|
|
195
|
+
rule,
|
|
196
|
+
canOverride: false,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
// high 级别:先检查是否已授权
|
|
200
|
+
if (isAuthorized(command, rule.name)) {
|
|
201
|
+
return { allowed: true };
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
allowed: false,
|
|
205
|
+
reason: `[危险命令] ${rule.reason}`,
|
|
206
|
+
rule,
|
|
207
|
+
canOverride: true,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return { allowed: true };
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* 对输出文本进行敏感信息脱敏
|
|
215
|
+
*/
|
|
216
|
+
export function sanitizeOutput(text) {
|
|
217
|
+
let result = text;
|
|
218
|
+
for (const sp of SENSITIVE_PATTERNS) {
|
|
219
|
+
result = result.replace(sp.pattern, sp.replacement);
|
|
220
|
+
}
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* 检查文件写入内容是否包含硬编码敏感信息
|
|
225
|
+
*/
|
|
226
|
+
export function detectSensitiveContent(content) {
|
|
227
|
+
const findings = [];
|
|
228
|
+
for (const sp of SENSITIVE_PATTERNS) {
|
|
229
|
+
sp.pattern.lastIndex = 0;
|
|
230
|
+
if (sp.pattern.test(content)) {
|
|
231
|
+
findings.push(sp.name);
|
|
232
|
+
}
|
|
233
|
+
sp.pattern.lastIndex = 0;
|
|
234
|
+
}
|
|
235
|
+
return findings;
|
|
236
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 检测终端窗口是否处于激活(聚焦)状态。
|
|
3
|
+
*
|
|
4
|
+
* 利用终端 focus reporting 功能:
|
|
5
|
+
* - 发送 \x1B[?1004h 开启 focus 事件上报
|
|
6
|
+
* - 终端聚焦时发送 \x1B[I
|
|
7
|
+
* - 终端失焦时发送 \x1B[O
|
|
8
|
+
* - 退出时发送 \x1B[?1004l 关闭上报
|
|
9
|
+
*
|
|
10
|
+
* 支持的终端:iTerm2, kitty, WezTerm, Windows Terminal, xterm 等
|
|
11
|
+
*/
|
|
12
|
+
export declare function useWindowFocus(): boolean;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* 检测终端窗口是否处于激活(聚焦)状态。
|
|
4
|
+
*
|
|
5
|
+
* 利用终端 focus reporting 功能:
|
|
6
|
+
* - 发送 \x1B[?1004h 开启 focus 事件上报
|
|
7
|
+
* - 终端聚焦时发送 \x1B[I
|
|
8
|
+
* - 终端失焦时发送 \x1B[O
|
|
9
|
+
* - 退出时发送 \x1B[?1004l 关闭上报
|
|
10
|
+
*
|
|
11
|
+
* 支持的终端:iTerm2, kitty, WezTerm, Windows Terminal, xterm 等
|
|
12
|
+
*/
|
|
13
|
+
export function useWindowFocus() {
|
|
14
|
+
const [focused, setFocused] = useState(true);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
// 开启 focus reporting
|
|
17
|
+
process.stdout.write('\x1B[?1004h');
|
|
18
|
+
const onData = (data) => {
|
|
19
|
+
const str = data.toString('utf-8');
|
|
20
|
+
if (str.includes('\x1B[I')) {
|
|
21
|
+
setFocused(true);
|
|
22
|
+
}
|
|
23
|
+
else if (str.includes('\x1B[O')) {
|
|
24
|
+
setFocused(false);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
process.stdin.on('data', onData);
|
|
28
|
+
return () => {
|
|
29
|
+
process.stdin.off('data', onData);
|
|
30
|
+
// 关闭 focus reporting
|
|
31
|
+
process.stdout.write('\x1B[?1004l');
|
|
32
|
+
};
|
|
33
|
+
}, []);
|
|
34
|
+
return focused;
|
|
35
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 输入历史记录 hook
|
|
3
|
+
*
|
|
4
|
+
* - 上箭头:浏览更早的历史
|
|
5
|
+
* - 下箭头:浏览更近的历史,到底回到当前输入
|
|
6
|
+
* - 提交时自动追加到历史
|
|
7
|
+
*/
|
|
8
|
+
export declare function useInputHistory(): {
|
|
9
|
+
pushHistory: (input: string) => void;
|
|
10
|
+
navigateUp: (currentInput: string) => string | null;
|
|
11
|
+
navigateDown: () => string | null;
|
|
12
|
+
resetNavigation: () => void;
|
|
13
|
+
historyIndex: number;
|
|
14
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { useState, useCallback, useRef } from 'react';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
const HISTORY_DIR = path.join(os.homedir(), '.jarvis');
|
|
6
|
+
const HISTORY_FILE = path.join(HISTORY_DIR, '.input_history');
|
|
7
|
+
const MAX_HISTORY = 20;
|
|
8
|
+
/** 从文件加载历史记录 */
|
|
9
|
+
function loadHistory() {
|
|
10
|
+
try {
|
|
11
|
+
if (!fs.existsSync(HISTORY_FILE))
|
|
12
|
+
return [];
|
|
13
|
+
const content = fs.readFileSync(HISTORY_FILE, 'utf-8').trim();
|
|
14
|
+
if (!content)
|
|
15
|
+
return [];
|
|
16
|
+
return content.split('\n').slice(-MAX_HISTORY);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** 保存历史记录到文件 */
|
|
23
|
+
function saveHistory(history) {
|
|
24
|
+
try {
|
|
25
|
+
if (!fs.existsSync(HISTORY_DIR)) {
|
|
26
|
+
fs.mkdirSync(HISTORY_DIR, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
fs.writeFileSync(HISTORY_FILE, history.slice(-MAX_HISTORY).join('\n') + '\n', 'utf-8');
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// 静默失败
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 输入历史记录 hook
|
|
36
|
+
*
|
|
37
|
+
* - 上箭头:浏览更早的历史
|
|
38
|
+
* - 下箭头:浏览更近的历史,到底回到当前输入
|
|
39
|
+
* - 提交时自动追加到历史
|
|
40
|
+
*/
|
|
41
|
+
export function useInputHistory() {
|
|
42
|
+
const [history, setHistory] = useState(() => loadHistory());
|
|
43
|
+
// -1 表示当前输入(非历史浏览状态)
|
|
44
|
+
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
45
|
+
// 保存用户正在编辑的内容,以便从历史返回时恢复
|
|
46
|
+
const draftRef = useRef('');
|
|
47
|
+
/** 添加一条历史记录 */
|
|
48
|
+
const pushHistory = useCallback((input) => {
|
|
49
|
+
const trimmed = input.trim();
|
|
50
|
+
if (!trimmed)
|
|
51
|
+
return;
|
|
52
|
+
setHistory((prev) => {
|
|
53
|
+
// 去重:移除已有的相同记录,再追加到末尾
|
|
54
|
+
const filtered = prev.filter((h) => h !== trimmed);
|
|
55
|
+
const next = [...filtered, trimmed].slice(-MAX_HISTORY);
|
|
56
|
+
saveHistory(next);
|
|
57
|
+
return next;
|
|
58
|
+
});
|
|
59
|
+
setHistoryIndex(-1);
|
|
60
|
+
draftRef.current = '';
|
|
61
|
+
}, []);
|
|
62
|
+
/** 上箭头:向更早的历史移动,返回应显示的文本 */
|
|
63
|
+
const navigateUp = useCallback((currentInput) => {
|
|
64
|
+
if (history.length === 0)
|
|
65
|
+
return null;
|
|
66
|
+
if (historyIndex === -1) {
|
|
67
|
+
// 首次进入历史浏览,保存当前输入
|
|
68
|
+
draftRef.current = currentInput;
|
|
69
|
+
const newIndex = history.length - 1;
|
|
70
|
+
setHistoryIndex(newIndex);
|
|
71
|
+
return history[newIndex];
|
|
72
|
+
}
|
|
73
|
+
if (historyIndex > 0) {
|
|
74
|
+
const newIndex = historyIndex - 1;
|
|
75
|
+
setHistoryIndex(newIndex);
|
|
76
|
+
return history[newIndex];
|
|
77
|
+
}
|
|
78
|
+
// 已经到最早的记录
|
|
79
|
+
return null;
|
|
80
|
+
}, [history, historyIndex]);
|
|
81
|
+
/** 下箭头:向更近的历史移动,返回应显示的文本 */
|
|
82
|
+
const navigateDown = useCallback(() => {
|
|
83
|
+
if (historyIndex === -1)
|
|
84
|
+
return null;
|
|
85
|
+
if (historyIndex < history.length - 1) {
|
|
86
|
+
const newIndex = historyIndex + 1;
|
|
87
|
+
setHistoryIndex(newIndex);
|
|
88
|
+
return history[newIndex];
|
|
89
|
+
}
|
|
90
|
+
// 回到当前输入
|
|
91
|
+
setHistoryIndex(-1);
|
|
92
|
+
return draftRef.current;
|
|
93
|
+
}, [history, historyIndex]);
|
|
94
|
+
/** 重置浏览状态(用户开始编辑时调用) */
|
|
95
|
+
const resetNavigation = useCallback(() => {
|
|
96
|
+
if (historyIndex !== -1) {
|
|
97
|
+
setHistoryIndex(-1);
|
|
98
|
+
draftRef.current = '';
|
|
99
|
+
}
|
|
100
|
+
}, [historyIndex]);
|
|
101
|
+
return { pushHistory, navigateUp, navigateDown, resetNavigation, historyIndex };
|
|
102
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function startJarvis(): void;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function REPL(): import("react/jsx-runtime").JSX.Element;
|