@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,23 @@
|
|
|
1
|
+
import { readFile } from './readFile.js';
|
|
2
|
+
import { writeFile } from './writeFile.js';
|
|
3
|
+
import { runCommand } from './runCommand.js';
|
|
4
|
+
import { listDirectory } from './listDirectory.js';
|
|
5
|
+
import { searchFiles } from './searchFiles.js';
|
|
6
|
+
import { createSkill } from './createSkill.js';
|
|
7
|
+
export { readFile, writeFile, runCommand, listDirectory, searchFiles, createSkill };
|
|
8
|
+
/** 所有内置工具 */
|
|
9
|
+
export const allTools = [readFile, writeFile, runCommand, listDirectory, searchFiles, createSkill];
|
|
10
|
+
/** 按名称查找内置工具 */
|
|
11
|
+
export function findTool(name) {
|
|
12
|
+
return allTools.find((t) => t.name === name);
|
|
13
|
+
}
|
|
14
|
+
// ===== 合并工具(内置 + 外部 Skills)=====
|
|
15
|
+
import { getMergedTools, findMergedTool } from '../skills/index.js';
|
|
16
|
+
/** 获取所有工具(内置 + 外部 skills),供 QueryEngine 使用 */
|
|
17
|
+
export function getAllTools() {
|
|
18
|
+
return getMergedTools();
|
|
19
|
+
}
|
|
20
|
+
/** 按名称查找工具(内置 + 外部 skills) */
|
|
21
|
+
export function findToolMerged(name) {
|
|
22
|
+
return findMergedTool(name);
|
|
23
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
export const listDirectory = {
|
|
3
|
+
name: 'list_directory',
|
|
4
|
+
description: '列出指定目录的文件和文件夹',
|
|
5
|
+
parameters: {
|
|
6
|
+
path: { type: 'string', description: '目录路径', required: true },
|
|
7
|
+
},
|
|
8
|
+
execute: async (args) => {
|
|
9
|
+
const dirPath = args.path || '.';
|
|
10
|
+
try {
|
|
11
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
12
|
+
return entries
|
|
13
|
+
.map((e) => (e.isDirectory() ? `[DIR] ${e.name}` : ` ${e.name}`))
|
|
14
|
+
.join('\n');
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
throw new Error(`列出目录失败: ${e.message}`);
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
export const readFile = {
|
|
3
|
+
name: 'read_file',
|
|
4
|
+
description: '读取指定路径的文件内容',
|
|
5
|
+
parameters: {
|
|
6
|
+
path: { type: 'string', description: '文件路径', required: true },
|
|
7
|
+
},
|
|
8
|
+
execute: async (args) => {
|
|
9
|
+
const filePath = args.path;
|
|
10
|
+
try {
|
|
11
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
12
|
+
}
|
|
13
|
+
catch (e) {
|
|
14
|
+
throw new Error(`读取文件失败: ${e.message}`);
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { sanitizeOutput } from '../core/safeguard.js';
|
|
3
|
+
/**
|
|
4
|
+
* 异步执行命令,不阻塞事件循环,保证 TUI 渲染正常
|
|
5
|
+
*/
|
|
6
|
+
function execAsync(command, options) {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
exec(command, options, (error, stdout, stderr) => {
|
|
9
|
+
if (error) {
|
|
10
|
+
const parts = [];
|
|
11
|
+
if (stderr)
|
|
12
|
+
parts.push(`[stderr] ${sanitizeOutput(String(stderr).trim())}`);
|
|
13
|
+
if (stdout)
|
|
14
|
+
parts.push(`[stdout] ${sanitizeOutput(String(stdout).trim())}`);
|
|
15
|
+
if (error.code != null)
|
|
16
|
+
parts.push(`[exit code] ${error.code}`);
|
|
17
|
+
if (parts.length === 0)
|
|
18
|
+
parts.push(error.message);
|
|
19
|
+
reject(new Error(`命令执行失败:\n${parts.join('\n')}`));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
resolve(sanitizeOutput(String(stdout).trim()) || '(命令执行完成,无输出)');
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export const runCommand = {
|
|
27
|
+
name: 'Bash',
|
|
28
|
+
description: '执行 Bash 命令并返回输出。调用格式: Bash(command),例如 Bash(git status)、Bash(ls -la)',
|
|
29
|
+
parameters: {
|
|
30
|
+
command: { type: 'string', description: '要执行的 Bash 命令', required: true },
|
|
31
|
+
},
|
|
32
|
+
execute: async (args) => {
|
|
33
|
+
const command = args.command;
|
|
34
|
+
// 安全围栏拦截已在 query 层(executeTool)统一处理,此处仅负责执行 + 脱敏
|
|
35
|
+
return execAsync(command, {
|
|
36
|
+
encoding: 'utf-8',
|
|
37
|
+
timeout: 30000,
|
|
38
|
+
maxBuffer: 1024 * 1024,
|
|
39
|
+
env: sanitizeEnv(process.env),
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* 构造安全的子进程环境变量:
|
|
45
|
+
* 继承当前 env 但移除高危敏感变量,避免泄露到命令输出
|
|
46
|
+
*/
|
|
47
|
+
function sanitizeEnv(env) {
|
|
48
|
+
const SENSITIVE_KEYS = [
|
|
49
|
+
'AWS_SECRET_ACCESS_KEY',
|
|
50
|
+
'AWS_SESSION_TOKEN',
|
|
51
|
+
'GITHUB_TOKEN',
|
|
52
|
+
'GH_TOKEN',
|
|
53
|
+
'NPM_TOKEN',
|
|
54
|
+
'OPENAI_API_KEY',
|
|
55
|
+
'ANTHROPIC_API_KEY',
|
|
56
|
+
'DATABASE_URL',
|
|
57
|
+
'DB_PASSWORD',
|
|
58
|
+
'PRIVATE_KEY',
|
|
59
|
+
'SECRET_KEY',
|
|
60
|
+
'ENCRYPTION_KEY',
|
|
61
|
+
];
|
|
62
|
+
const cleaned = { ...env };
|
|
63
|
+
for (const key of SENSITIVE_KEYS) {
|
|
64
|
+
if (cleaned[key]) {
|
|
65
|
+
cleaned[key] = '[REDACTED]';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return cleaned;
|
|
69
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
export const searchFiles = {
|
|
4
|
+
name: 'search_files',
|
|
5
|
+
description: '在指定目录中搜索包含关键词的文件',
|
|
6
|
+
parameters: {
|
|
7
|
+
path: { type: 'string', description: '搜索目录', required: true },
|
|
8
|
+
pattern: { type: 'string', description: '搜索关键词', required: true },
|
|
9
|
+
},
|
|
10
|
+
execute: async (args) => {
|
|
11
|
+
const dirPath = args.path || '.';
|
|
12
|
+
const pattern = args.pattern;
|
|
13
|
+
const results = [];
|
|
14
|
+
function walk(dir) {
|
|
15
|
+
try {
|
|
16
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
const full = path.join(dir, entry.name);
|
|
19
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules')
|
|
20
|
+
continue;
|
|
21
|
+
if (entry.isDirectory()) {
|
|
22
|
+
walk(full);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
try {
|
|
26
|
+
const content = fs.readFileSync(full, 'utf-8');
|
|
27
|
+
const lines = content.split('\n');
|
|
28
|
+
for (let i = 0; i < lines.length; i++) {
|
|
29
|
+
if (lines[i].includes(pattern)) {
|
|
30
|
+
results.push(`${full}:${i + 1}: ${lines[i].trim()}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch { /* skip binary */ }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch { /* skip inaccessible */ }
|
|
39
|
+
}
|
|
40
|
+
walk(dirPath);
|
|
41
|
+
return results.length > 0
|
|
42
|
+
? results.slice(0, 50).join('\n')
|
|
43
|
+
: `未找到包含 "${pattern}" 的文件`;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { detectSensitiveContent } from '../core/safeguard.js';
|
|
4
|
+
export const writeFile = {
|
|
5
|
+
name: 'write_file',
|
|
6
|
+
description: '写入内容到指定文件',
|
|
7
|
+
parameters: {
|
|
8
|
+
path: { type: 'string', description: '文件路径', required: true },
|
|
9
|
+
content: { type: 'string', description: '文件内容', required: true },
|
|
10
|
+
},
|
|
11
|
+
execute: async (args) => {
|
|
12
|
+
const filePath = args.path;
|
|
13
|
+
const content = args.content;
|
|
14
|
+
// ===== 安全围栏:检测硬编码敏感信息 =====
|
|
15
|
+
const findings = detectSensitiveContent(content);
|
|
16
|
+
if (findings.length > 0) {
|
|
17
|
+
const warning = `检测到文件中包含疑似敏感信息: ${findings.join(', ')}\n` +
|
|
18
|
+
'建议使用环境变量替代硬编码。文件仍将写入,但请注意安全风险。';
|
|
19
|
+
// 仍然写入,但在结果中附带警告
|
|
20
|
+
try {
|
|
21
|
+
const dir = path.dirname(filePath);
|
|
22
|
+
if (!fs.existsSync(dir))
|
|
23
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
24
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
25
|
+
return `文件已写入: ${filePath}\n${warning}`;
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
throw new Error(`写入文件失败: ${e.message}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const dir = path.dirname(filePath);
|
|
33
|
+
if (!fs.existsSync(dir))
|
|
34
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
35
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
36
|
+
return `文件已写入: ${filePath}`;
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
throw new Error(`写入文件失败: ${e.message}`);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export type MessageStatus = 'pending' | 'success' | 'error' | 'aborted';
|
|
2
|
+
export type MessageType = 'user' | 'reasoning' | 'tool_exec' | 'file_op' | 'thinking' | 'system' | 'network' | 'error';
|
|
3
|
+
export interface Message {
|
|
4
|
+
id: string;
|
|
5
|
+
type: MessageType;
|
|
6
|
+
status: MessageStatus;
|
|
7
|
+
content: string;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
/** 耗时 ms */
|
|
10
|
+
duration?: number;
|
|
11
|
+
/** token 统计 */
|
|
12
|
+
tokenCount?: number;
|
|
13
|
+
/** 首 token 延时 ms */
|
|
14
|
+
firstTokenLatency?: number;
|
|
15
|
+
/** 平均每秒 token 数 */
|
|
16
|
+
tokensPerSecond?: number;
|
|
17
|
+
/** 工具调用相关 */
|
|
18
|
+
toolName?: string;
|
|
19
|
+
toolArgs?: Record<string, unknown>;
|
|
20
|
+
toolResult?: string;
|
|
21
|
+
/** 大模型思考过程,仅本地展示,不带入上下文 */
|
|
22
|
+
think?: string;
|
|
23
|
+
/** 中断提示文案,仅 aborted 状态时使用 */
|
|
24
|
+
abortHint?: string;
|
|
25
|
+
}
|
|
26
|
+
export type ContentBlock = {
|
|
27
|
+
type: 'text';
|
|
28
|
+
text: string;
|
|
29
|
+
} | {
|
|
30
|
+
type: 'tool_use';
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
input: Record<string, unknown>;
|
|
34
|
+
};
|
|
35
|
+
export interface LoopState {
|
|
36
|
+
iteration: number;
|
|
37
|
+
maxIterations: number;
|
|
38
|
+
isRunning: boolean;
|
|
39
|
+
aborted: boolean;
|
|
40
|
+
}
|
|
41
|
+
export interface ToolParameter {
|
|
42
|
+
type: string;
|
|
43
|
+
description: string;
|
|
44
|
+
required?: boolean;
|
|
45
|
+
}
|
|
46
|
+
export interface Tool {
|
|
47
|
+
name: string;
|
|
48
|
+
description: string;
|
|
49
|
+
parameters: Record<string, ToolParameter>;
|
|
50
|
+
execute: (args: Record<string, unknown>) => Promise<string>;
|
|
51
|
+
}
|
|
52
|
+
export interface Session {
|
|
53
|
+
id: string;
|
|
54
|
+
messages: Message[];
|
|
55
|
+
createdAt: number;
|
|
56
|
+
updatedAt: number;
|
|
57
|
+
totalTokens: number;
|
|
58
|
+
totalCost: number;
|
|
59
|
+
/** 会话摘要(首条用户消息截取),用于 /resume 列表展示 */
|
|
60
|
+
summary?: string;
|
|
61
|
+
}
|
|
62
|
+
export interface LLMServiceConfig {
|
|
63
|
+
apiKey?: string;
|
|
64
|
+
model?: string;
|
|
65
|
+
maxTokens?: number;
|
|
66
|
+
}
|
|
67
|
+
export interface StreamCallbacks {
|
|
68
|
+
onText: (text: string) => void;
|
|
69
|
+
/** 大模型思考过程(reasoning_content),仅本地展示 */
|
|
70
|
+
onThinking?: (text: string) => void;
|
|
71
|
+
onToolUse: (id: string, name: string, input: Record<string, unknown>) => void;
|
|
72
|
+
onComplete: () => void;
|
|
73
|
+
onError: (error: Error) => void;
|
|
74
|
+
}
|
|
75
|
+
/** 可中断信号,用于取消正在进行的 LLM 流式请求 */
|
|
76
|
+
export interface AbortSignal {
|
|
77
|
+
aborted: boolean;
|
|
78
|
+
}
|
|
79
|
+
export interface TranscriptMessage {
|
|
80
|
+
role: 'user' | 'assistant' | 'tool_result';
|
|
81
|
+
content: string | ContentBlock[];
|
|
82
|
+
toolUseId?: string;
|
|
83
|
+
}
|
|
84
|
+
export interface LLMService {
|
|
85
|
+
streamMessage: (transcript: TranscriptMessage[], tools: Tool[], callbacks: StreamCallbacks, abortSignal?: AbortSignal) => Promise<void>;
|
|
86
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@code4bug/jarvis-agent",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "基于 React + TypeScript + Ink 构建的命令行智能体交互界面",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"jarvis": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc && cp src/agents/*.md dist/agents/",
|
|
15
|
+
"prepack": "npm run build",
|
|
16
|
+
"start": "tsx src/cli.ts",
|
|
17
|
+
"dev": "tsx src/cli.ts",
|
|
18
|
+
"test": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"ink": "^5.1.0",
|
|
22
|
+
"ink-spinner": "^5.0.0",
|
|
23
|
+
"marked": "^15.0.12",
|
|
24
|
+
"marked-terminal": "^7.3.0",
|
|
25
|
+
"react": "^18.3.1",
|
|
26
|
+
"uuid": "^11.1.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^22.0.0",
|
|
30
|
+
"@types/react": "^18.3.18",
|
|
31
|
+
"@types/uuid": "^10.0.0",
|
|
32
|
+
"tsx": "^4.19.0",
|
|
33
|
+
"typescript": "^5.7.0"
|
|
34
|
+
},
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"author": "Code4Bug",
|
|
37
|
+
"homepage": "https://github.com/Code4Bug/jarvis#readme",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/Code4Bug/jarvis.git"
|
|
41
|
+
},
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/Code4Bug/jarvis/issues"
|
|
44
|
+
},
|
|
45
|
+
"keywords": [
|
|
46
|
+
"cli",
|
|
47
|
+
"agent",
|
|
48
|
+
"ai",
|
|
49
|
+
"chatbot",
|
|
50
|
+
"ink",
|
|
51
|
+
"react",
|
|
52
|
+
"typescript",
|
|
53
|
+
"terminal"
|
|
54
|
+
]
|
|
55
|
+
}
|