@wangzhizhi/remi 0.0.1-alpha → 0.1.214
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 +3 -3
- package/dist/accentColor.js +28 -0
- package/dist/doctor.js +13 -14
- package/dist/help.js +2 -0
- package/dist/i18n.js +72 -24
- package/dist/initPrompt.js +3 -1
- package/dist/permissions.js +75 -8
- package/dist/repl.js +83 -19
- package/dist/tui/RemiApp.js +355 -45
- package/dist/tui/commands.js +70 -3
- package/dist/tui/hooksPanel.js +164 -0
- package/dist/tui/index.js +27 -2
- package/dist/tui/pastedText.js +82 -0
- package/dist/tui/renderers/Header.js +2 -2
- package/dist/tui/renderers/MessageList.js +304 -57
- package/dist/tui/renderers/PromptBox.js +7 -1
- package/dist/tui/renderers/WorkingIndicator.js +20 -11
- package/dist/tui/statusStats.js +366 -0
- package/dist/tui/theme.js +44 -2
- package/dist/version.js +1 -1
- package/node_modules/@remi/compact/package.json +1 -1
- package/node_modules/@remi/config/dist/index.js +105 -36
- package/node_modules/@remi/config/package.json +1 -1
- package/node_modules/@remi/core/dist/contextBuilder.js +3 -3
- package/node_modules/@remi/core/dist/directoryOverview.js +2 -5
- package/node_modules/@remi/core/dist/index.js +641 -50
- package/node_modules/@remi/core/dist/projectInstructions.js +1 -1
- package/node_modules/@remi/core/dist/promptFiles.js +17 -0
- package/node_modules/@remi/core/dist/responseStyles.js +2 -2
- package/node_modules/@remi/core/package.json +1 -1
- package/node_modules/@remi/hooks/dist/index.js +229 -0
- package/node_modules/@remi/hooks/package.json +8 -0
- package/node_modules/@remi/llm/package.json +1 -1
- package/node_modules/@remi/memory/dist/index.js +10 -3
- package/node_modules/@remi/memory/package.json +1 -1
- package/node_modules/@remi/permissions/package.json +1 -1
- package/node_modules/@remi/sessions/dist/index.js +26 -13
- package/node_modules/@remi/sessions/package.json +1 -1
- package/node_modules/@remi/skills/dist/index.js +32 -10
- package/node_modules/@remi/skills/package.json +1 -1
- package/node_modules/@remi/terminal-markdown/dist/index.js +19 -7
- package/node_modules/@remi/terminal-markdown/package.json +1 -1
- package/node_modules/@remi/tools/dist/index.js +513 -7
- package/node_modules/@remi/tools/package.json +1 -1
- package/package.json +14 -11
- package/prompt/base-system.md +34 -0
- package/prompt/tool-use-system.md +83 -0
package/README.md
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { loadRemiConfig, readProjectRemiConfig, writeProjectRemiConfig } from '@remi/config';
|
|
2
|
+
import { accentColorLabel, accentColorOptions, defaultAccentColorId, resolveAccentColorId, } from './tui/theme.js';
|
|
3
|
+
import { t } from './i18n.js';
|
|
4
|
+
export function accentColorPanelOptions() {
|
|
5
|
+
return accentColorOptions;
|
|
6
|
+
}
|
|
7
|
+
export function parseAccentColorArg(value) {
|
|
8
|
+
if (!value) {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
const normalized = value.trim().toLowerCase();
|
|
12
|
+
return accentColorOptions.find(option => option.id === normalized || option.label.toLowerCase() === normalized)?.id;
|
|
13
|
+
}
|
|
14
|
+
export function resolveConfiguredAccentColor(cwd) {
|
|
15
|
+
try {
|
|
16
|
+
return resolveAccentColorId(loadRemiConfig({ cwd }).config.accentColor);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return defaultAccentColorId;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function saveProjectAccentColor(cwd, colorId) {
|
|
23
|
+
const config = readProjectRemiConfig(cwd);
|
|
24
|
+
writeProjectRemiConfig({ ...config, accentColor: colorId }, cwd);
|
|
25
|
+
}
|
|
26
|
+
export function formatAccentColorChanged(colorId, saved = false, language) {
|
|
27
|
+
return t(language, saved ? 'accentColor.saved' : 'accentColor.selected', { label: accentColorLabel(colorId) });
|
|
28
|
+
}
|
package/dist/doctor.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { loadRemiConfig } from '@remi/config';
|
|
4
|
+
import { loadRemiConfig, projectStateDir } from '@remi/config';
|
|
5
5
|
import { createModelRouter } from '@remi/llm';
|
|
6
6
|
function commandVersion(command, args = ['--version']) {
|
|
7
7
|
return execFileSync(command, args, {
|
|
@@ -38,36 +38,35 @@ export function runDoctor(cwd = process.cwd()) {
|
|
|
38
38
|
detail: cwd,
|
|
39
39
|
});
|
|
40
40
|
try {
|
|
41
|
-
const
|
|
42
|
-
mkdirSync(
|
|
43
|
-
const probePath = join(
|
|
41
|
+
const stateDir = projectStateDir(cwd);
|
|
42
|
+
mkdirSync(stateDir, { recursive: true });
|
|
43
|
+
const probePath = join(stateDir, '.doctor-write-test');
|
|
44
44
|
writeFileSync(probePath, 'ok');
|
|
45
45
|
rmSync(probePath, { force: true });
|
|
46
46
|
checks.push({
|
|
47
|
-
name: '
|
|
47
|
+
name: 'stateDir',
|
|
48
48
|
ok: true,
|
|
49
|
-
detail:
|
|
49
|
+
detail: stateDir,
|
|
50
50
|
});
|
|
51
51
|
}
|
|
52
52
|
catch (error) {
|
|
53
53
|
checks.push({
|
|
54
|
-
name: '
|
|
54
|
+
name: 'stateDir',
|
|
55
55
|
ok: false,
|
|
56
56
|
detail: error instanceof Error ? error.message : String(error),
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
|
-
const keyNames = ['REMI_API_KEY', 'OPENAI_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'DEEPSEEK_API_KEY'];
|
|
60
|
-
const configuredKeys = keyNames.filter(name => Boolean(process.env[name]));
|
|
61
|
-
checks.push({
|
|
62
|
-
name: 'apiKey',
|
|
63
|
-
ok: configuredKeys.length > 0,
|
|
64
|
-
detail: configuredKeys.length > 0 ? configuredKeys.join(', ') : 'not configured',
|
|
65
|
-
});
|
|
66
59
|
try {
|
|
67
60
|
const loaded = loadRemiConfig({ cwd });
|
|
68
61
|
const router = createModelRouter(loaded.config);
|
|
69
62
|
const providers = router.providers.list();
|
|
63
|
+
const configuredProviders = providers.filter(provider => provider.apiKeyStatus === 'configured');
|
|
70
64
|
const missingProviders = providers.filter(provider => provider.apiKeyStatus === 'missing');
|
|
65
|
+
checks.push({
|
|
66
|
+
name: 'apiKey',
|
|
67
|
+
ok: configuredProviders.length > 0,
|
|
68
|
+
detail: configuredProviders.length > 0 ? configuredProviders.map(provider => provider.alias).join(', ') : 'not configured',
|
|
69
|
+
});
|
|
71
70
|
checks.push({
|
|
72
71
|
name: 'profile',
|
|
73
72
|
ok: Boolean(loaded.config.activeProfile),
|
package/dist/help.js
CHANGED
|
@@ -22,6 +22,8 @@ export function formatHelp() {
|
|
|
22
22
|
' /permissions Choose permissions and approval behavior',
|
|
23
23
|
' /statusline Choose status line fields',
|
|
24
24
|
' /style Choose response style',
|
|
25
|
+
' /code-panel Choose code panel colors',
|
|
26
|
+
' /accent-color Choose accent color',
|
|
25
27
|
' /exit Exit',
|
|
26
28
|
].join('\n');
|
|
27
29
|
}
|
package/dist/i18n.js
CHANGED
|
@@ -9,8 +9,11 @@ const dictionaries = {
|
|
|
9
9
|
'slash.init.description': 'Create an AGENTS.md file with repository instructions',
|
|
10
10
|
'slash.model.description': 'Choose what model and reasoning effort to use',
|
|
11
11
|
'slash.permissions.description': 'Choose model permissions and approval behavior',
|
|
12
|
+
'slash.hooks.description': 'Browse configured hooks',
|
|
12
13
|
'slash.style.description': 'Choose how Remi should answer',
|
|
13
|
-
'slash.
|
|
14
|
+
'slash.codePanel.description': 'Choose code panel colors',
|
|
15
|
+
'slash.accentColor.description': 'Choose accent color',
|
|
16
|
+
'slash.stat.description': 'Show global usage stats',
|
|
14
17
|
'slash.statusline.description': 'Choose status line fields',
|
|
15
18
|
'slash.skills.description': 'Manage and load local Remi skills',
|
|
16
19
|
'slash.language.description': 'Choose interface language',
|
|
@@ -30,15 +33,23 @@ const dictionaries = {
|
|
|
30
33
|
'style.description.explain': 'More explanation and context for technical decisions.',
|
|
31
34
|
'style.description.mentor': 'Teaching-oriented answers with reasoning and tradeoffs.',
|
|
32
35
|
'style.description.minimal': 'Minimal replies such as "OK." and "Done."',
|
|
33
|
-
'theme.panel.title': 'Select
|
|
34
|
-
'theme.panel.subtitle': 'Move up/down to live preview code
|
|
36
|
+
'theme.panel.title': 'Select Code Panel',
|
|
37
|
+
'theme.panel.subtitle': 'Move up/down to live preview code colors.',
|
|
35
38
|
'theme.panel.search': 'Type to filter themes',
|
|
36
39
|
'theme.panel.help': 'Press enter to confirm or esc to go back',
|
|
37
40
|
'theme.current': 'current',
|
|
38
|
-
'theme.selected': '
|
|
39
|
-
'theme.saved': '
|
|
40
|
-
'theme.closed': '
|
|
41
|
-
'theme.unknown': 'Unknown
|
|
41
|
+
'theme.selected': 'Code panel selected: {{label}}.',
|
|
42
|
+
'theme.saved': 'Code panel saved: {{label}}.',
|
|
43
|
+
'theme.closed': 'Code panel settings closed without saving.',
|
|
44
|
+
'theme.unknown': 'Unknown code panel: {{theme}}. Run /code-panel to choose.',
|
|
45
|
+
'accentColor.panel.title': 'Select Accent Color',
|
|
46
|
+
'accentColor.panel.subtitle': 'Move up/down to preview interface accent.',
|
|
47
|
+
'accentColor.current': 'current',
|
|
48
|
+
'accentColor.help': 'Press enter to confirm or esc to go back',
|
|
49
|
+
'accentColor.selected': 'Accent color selected: {{label}}.',
|
|
50
|
+
'accentColor.saved': 'Accent color saved: {{label}}.',
|
|
51
|
+
'accentColor.closed': 'Accent color settings closed without saving.',
|
|
52
|
+
'accentColor.unknown': 'Unknown accent color: {{color}}. Run /accent-color to choose.',
|
|
42
53
|
'language.panel.title': 'Select Language',
|
|
43
54
|
'language.panel.subtitle': 'Choose the language for Remi system messages.',
|
|
44
55
|
'language.current': 'current',
|
|
@@ -67,12 +78,22 @@ const dictionaries = {
|
|
|
67
78
|
'permission.request.action.editFileIn': 'Edit files in {{directory}}',
|
|
68
79
|
'permission.request.action.modifyFilesIn': 'Modify files in {{directory}}',
|
|
69
80
|
'permission.request.action.target': 'Target: {{path}}',
|
|
81
|
+
'permission.request.action.webSearch': 'Search the web',
|
|
82
|
+
'permission.request.action.webFetch': 'Fetch web page',
|
|
83
|
+
'permission.request.action.query': 'Query: {{query}}',
|
|
84
|
+
'permission.request.action.url': 'URL: {{url}}',
|
|
70
85
|
'permission.request.path.currentDirectory': 'the current directory',
|
|
71
|
-
'permission.request.persist':
|
|
72
|
-
'permission.request.persistRule':
|
|
73
|
-
'permission.request.sessionRule': '
|
|
74
|
-
'permission.request.once': '
|
|
75
|
-
'permission.request.deny': '
|
|
86
|
+
'permission.request.persist': 'Always allow',
|
|
87
|
+
'permission.request.persistRule': 'Always allow',
|
|
88
|
+
'permission.request.sessionRule': 'Allow for this session',
|
|
89
|
+
'permission.request.once': 'Allow',
|
|
90
|
+
'permission.request.deny': 'Cancel',
|
|
91
|
+
'permission.request.onceDescription': 'Run the tool and continue.',
|
|
92
|
+
'permission.request.sessionDescription': 'Run the tool and remember this choice for this session.',
|
|
93
|
+
'permission.request.sessionRuleDescription': 'Remember this choice for this session for {{rule}}.',
|
|
94
|
+
'permission.request.persistDescription': 'Run the tool and remember this choice for future matching tool calls.',
|
|
95
|
+
'permission.request.persistRuleDescription': 'Remember this choice for future tool calls matching {{rule}}.',
|
|
96
|
+
'permission.request.denyDescription': 'Cancel this tool call.',
|
|
76
97
|
'permission.request.help': 'Press enter to confirm or esc to cancel',
|
|
77
98
|
'permission.rule.prefix': 'commands that start with `{{prefix}}`',
|
|
78
99
|
'permission.rule.scoped': 'commands that start with `{{prefix}}` in `{{cwd}}`',
|
|
@@ -80,6 +101,9 @@ const dictionaries = {
|
|
|
80
101
|
'permission.rule.multipleScoped': 'commands that start with {{prefixes}} in `{{cwd}}`',
|
|
81
102
|
'permission.rule.filesystem': 'file changes under `{{root}}`',
|
|
82
103
|
'permission.rule.filesystemOperations': '{{operations}} files under `{{root}}`',
|
|
104
|
+
'permission.rule.networkTool': '{{tool}}',
|
|
105
|
+
'permission.reason.web_search': 'Queries public web search. Network permission is required before execution.',
|
|
106
|
+
'permission.reason.web_fetch': 'Fetches a public web page. Network permission is required before execution.',
|
|
83
107
|
'permission.savedRule': 'Allowed this session for commands that start with `{{prefix}}`.',
|
|
84
108
|
'permission.savedRules': 'Allowed this session for {{rule}}.',
|
|
85
109
|
'permission.savedPersistentRules': 'Always allowed {{rule}}.',
|
|
@@ -196,8 +220,11 @@ const dictionaries = {
|
|
|
196
220
|
'slash.init.description': '创建包含仓库协作规则的 AGENTS.md 文件',
|
|
197
221
|
'slash.model.description': '选择本次会话使用的模型和推理强度',
|
|
198
222
|
'slash.permissions.description': '选择模型权限和审批行为',
|
|
223
|
+
'slash.hooks.description': '查看已配置 hooks',
|
|
199
224
|
'slash.style.description': '选择 Remi 的回复风格',
|
|
200
|
-
'slash.
|
|
225
|
+
'slash.codePanel.description': '选择代码面板配色',
|
|
226
|
+
'slash.accentColor.description': '选择高亮色',
|
|
227
|
+
'slash.stat.description': '查看全局使用统计',
|
|
201
228
|
'slash.statusline.description': '选择底部状态栏字段',
|
|
202
229
|
'slash.skills.description': '管理和加载本地 Remi skills',
|
|
203
230
|
'slash.language.description': '选择界面语言',
|
|
@@ -217,15 +244,23 @@ const dictionaries = {
|
|
|
217
244
|
'style.description.explain': '提供更多技术背景和决策解释。',
|
|
218
245
|
'style.description.mentor': '偏教学,说明推理过程和取舍。',
|
|
219
246
|
'style.description.minimal': '极简回复,例如“好。”、“搞定。”。',
|
|
220
|
-
'theme.panel.title': '
|
|
221
|
-
'theme.panel.subtitle': '
|
|
247
|
+
'theme.panel.title': '选择代码面板',
|
|
248
|
+
'theme.panel.subtitle': '上下移动以实时预览代码配色。',
|
|
222
249
|
'theme.panel.search': '输入以筛选主题',
|
|
223
250
|
'theme.panel.help': '按回车确认,按 esc 返回',
|
|
224
251
|
'theme.current': '当前',
|
|
225
|
-
'theme.selected': '
|
|
226
|
-
'theme.saved': '
|
|
227
|
-
'theme.closed': '
|
|
228
|
-
'theme.unknown': '
|
|
252
|
+
'theme.selected': '已选择代码面板:{{label}}。',
|
|
253
|
+
'theme.saved': '已保存代码面板:{{label}}。',
|
|
254
|
+
'theme.closed': '已关闭代码面板设置,未保存更改。',
|
|
255
|
+
'theme.unknown': '未知代码面板:{{theme}}。运行 /code-panel 选择。',
|
|
256
|
+
'accentColor.panel.title': '选择高亮色',
|
|
257
|
+
'accentColor.panel.subtitle': '上下移动以预览界面高亮色。',
|
|
258
|
+
'accentColor.current': '当前',
|
|
259
|
+
'accentColor.help': '按回车确认,按 esc 返回',
|
|
260
|
+
'accentColor.selected': '已选择高亮色:{{label}}。',
|
|
261
|
+
'accentColor.saved': '已保存高亮色:{{label}}。',
|
|
262
|
+
'accentColor.closed': '已关闭高亮色设置,未保存更改。',
|
|
263
|
+
'accentColor.unknown': '未知高亮色:{{color}}。运行 /accent-color 选择。',
|
|
229
264
|
'language.panel.title': '选择语言',
|
|
230
265
|
'language.panel.subtitle': '选择 Remi 系统提示语使用的语言。',
|
|
231
266
|
'language.current': '当前',
|
|
@@ -254,12 +289,22 @@ const dictionaries = {
|
|
|
254
289
|
'permission.request.action.editFileIn': '在 {{directory}} 中修改文件',
|
|
255
290
|
'permission.request.action.modifyFilesIn': '在 {{directory}} 中修改文件',
|
|
256
291
|
'permission.request.action.target': '目标:{{path}}',
|
|
292
|
+
'permission.request.action.webSearch': '搜索网页',
|
|
293
|
+
'permission.request.action.webFetch': '抓取网页',
|
|
294
|
+
'permission.request.action.query': '查询:{{query}}',
|
|
295
|
+
'permission.request.action.url': 'URL:{{url}}',
|
|
257
296
|
'permission.request.path.currentDirectory': '当前目录',
|
|
258
|
-
'permission.request.persist': '
|
|
259
|
-
'permission.request.persistRule': '
|
|
260
|
-
'permission.request.sessionRule': '
|
|
261
|
-
'permission.request.once': '
|
|
262
|
-
'permission.request.deny': '
|
|
297
|
+
'permission.request.persist': '始终允许',
|
|
298
|
+
'permission.request.persistRule': '始终允许',
|
|
299
|
+
'permission.request.sessionRule': '本 session 允许',
|
|
300
|
+
'permission.request.once': '允许',
|
|
301
|
+
'permission.request.deny': '取消',
|
|
302
|
+
'permission.request.onceDescription': '运行该工具并继续。',
|
|
303
|
+
'permission.request.sessionDescription': '运行该工具,并在本 session 记住这个选择。',
|
|
304
|
+
'permission.request.sessionRuleDescription': '本 session 对{{rule}}记住这个选择。',
|
|
305
|
+
'permission.request.persistDescription': '运行该工具,并为后续匹配的工具调用记住这个选择。',
|
|
306
|
+
'permission.request.persistRuleDescription': '后续对匹配{{rule}}的工具调用记住这个选择。',
|
|
307
|
+
'permission.request.denyDescription': '取消这次工具调用。',
|
|
263
308
|
'permission.request.help': '按回车确认,按 esc 取消',
|
|
264
309
|
'permission.rule.prefix': '以 `{{prefix}}` 开头的命令',
|
|
265
310
|
'permission.rule.scoped': '`{{cwd}}` 中以 `{{prefix}}` 开头的命令',
|
|
@@ -267,6 +312,9 @@ const dictionaries = {
|
|
|
267
312
|
'permission.rule.multipleScoped': '`{{cwd}}` 中以 {{prefixes}} 开头的命令',
|
|
268
313
|
'permission.rule.filesystem': '`{{root}}` 目录中的文件变更',
|
|
269
314
|
'permission.rule.filesystemOperations': '`{{root}}` 目录中的{{operations}}文件',
|
|
315
|
+
'permission.rule.networkTool': '{{tool}}',
|
|
316
|
+
'permission.reason.web_search': '搜索公开网页,执行前需要网络权限。',
|
|
317
|
+
'permission.reason.web_fetch': '抓取公开网页,执行前需要网络权限。',
|
|
270
318
|
'permission.savedRule': '本 session 已允许以 `{{prefix}}` 开头的命令。',
|
|
271
319
|
'permission.savedRules': '本 session 已允许{{rule}}。',
|
|
272
320
|
'permission.savedPersistentRules': '已始终允许{{rule}}。',
|
package/dist/initPrompt.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export function buildInitPrompt() {
|
|
2
2
|
return `Set up a minimal AGENTS.md for this repository. AGENTS.md is loaded into future Remi sessions, so keep it concise and only include information that would prevent mistakes.
|
|
3
3
|
|
|
4
|
+
Treat this slash-command task as standalone for the current Remi runtime cwd. Do not continue, repair, recreate, or validate prior task artifacts unless current filesystem exploration shows they are part of this repository.
|
|
5
|
+
|
|
4
6
|
Work in this order:
|
|
5
7
|
|
|
6
|
-
1. Explore the repository before writing. Read key files such as README, package manifests, workspace config, build/test config, Makefile, CI config, and any existing AI-agent instruction files: AGENTS.md, AGENT.md,
|
|
8
|
+
1. Explore the repository before writing. Read key files such as README, package manifests, workspace config, build/test config, Makefile, CI config, and any existing AI-agent instruction files: AGENTS.md, AGENT.md, REMI.md, .cursor/rules, .cursorrules, .github/copilot-instructions.md, .windsurfrules, or .clinerules.
|
|
7
9
|
2. Identify only non-obvious guidance: build/test/lint commands, package manager/runtime requirements, architecture boundaries, repo workflow, local setup gotchas, testing quirks, generated files, security rules, or user/team preferences already documented in the repo.
|
|
8
10
|
3. If AGENTS.md already exists, read it first and update it narrowly. Do not silently overwrite existing guidance.
|
|
9
11
|
4. Create or update AGENTS.md at the project root with a short, practical structure. Start it with:
|
package/dist/permissions.js
CHANGED
|
@@ -81,9 +81,24 @@ export function saveProjectPermissionRule(cwd, ruleOrPrefix) {
|
|
|
81
81
|
}, cwd);
|
|
82
82
|
}
|
|
83
83
|
export const saveProjectShellPermissionRule = saveProjectPermissionRule;
|
|
84
|
+
export function canRememberPermissionRequest(request) {
|
|
85
|
+
return !hasHighRiskCommandCapability(request);
|
|
86
|
+
}
|
|
87
|
+
export function hasHighRiskCommandCapability(request) {
|
|
88
|
+
const capabilities = [
|
|
89
|
+
...(request.command?.capability ? [request.command.capability] : []),
|
|
90
|
+
...(request.command?.capabilities ?? []),
|
|
91
|
+
];
|
|
92
|
+
return capabilities.some(capability => {
|
|
93
|
+
if (capability.requiresAdmin) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
return capability.risk === 'network' || capability.risk === 'install' || capability.risk === 'admin' || capability.id === 'download.url' || capability.id === 'tool.install';
|
|
97
|
+
});
|
|
98
|
+
}
|
|
84
99
|
export function addSessionPermissionRules(currentRules, cwd, rules) {
|
|
85
100
|
let nextRules = currentRules;
|
|
86
|
-
for (const rule of rules) {
|
|
101
|
+
for (const rule of dedupePermissionRules(rules)) {
|
|
87
102
|
const normalized = normalizePermissionRule(rule, cwd);
|
|
88
103
|
if (!normalized) {
|
|
89
104
|
continue;
|
|
@@ -92,6 +107,19 @@ export function addSessionPermissionRules(currentRules, cwd, rules) {
|
|
|
92
107
|
}
|
|
93
108
|
return nextRules;
|
|
94
109
|
}
|
|
110
|
+
export function dedupePermissionRules(rules) {
|
|
111
|
+
const seen = new Set();
|
|
112
|
+
const uniqueRules = [];
|
|
113
|
+
for (const rule of rules) {
|
|
114
|
+
const key = permissionRuleDedupKey(rule);
|
|
115
|
+
if (seen.has(key)) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
seen.add(key);
|
|
119
|
+
uniqueRules.push(rule);
|
|
120
|
+
}
|
|
121
|
+
return uniqueRules;
|
|
122
|
+
}
|
|
95
123
|
export function formatPermissionRuleScope(rule, language) {
|
|
96
124
|
if (rule.kind === 'filesystem-write-root') {
|
|
97
125
|
const operationLabel = formatFilesystemOperations(rule.operations, language);
|
|
@@ -100,25 +128,47 @@ export function formatPermissionRuleScope(rule, language) {
|
|
|
100
128
|
}
|
|
101
129
|
return t(language, 'permission.rule.filesystem', { root: rule.root });
|
|
102
130
|
}
|
|
131
|
+
if (rule.kind === 'network-tool') {
|
|
132
|
+
return t(language, 'permission.rule.networkTool', { tool: formatNetworkToolLabel(rule.toolName, language) });
|
|
133
|
+
}
|
|
103
134
|
if (rule.cwd) {
|
|
104
135
|
return t(language, 'permission.rule.scoped', { prefix: rule.prefix, cwd: rule.cwd });
|
|
105
136
|
}
|
|
106
137
|
return t(language, 'permission.rule.prefix', { prefix: rule.prefix });
|
|
107
138
|
}
|
|
139
|
+
function formatNetworkToolLabel(toolName, language) {
|
|
140
|
+
if (toolName === 'web_search') {
|
|
141
|
+
return language === 'zh-Hans' ? '网络搜索' : 'web search';
|
|
142
|
+
}
|
|
143
|
+
if (toolName === 'web_fetch') {
|
|
144
|
+
return language === 'zh-Hans' ? '网页抓取' : 'web page fetch';
|
|
145
|
+
}
|
|
146
|
+
return toolName.replaceAll('_', ' ');
|
|
147
|
+
}
|
|
108
148
|
export function formatPermissionRulesScope(rules, language) {
|
|
109
|
-
|
|
110
|
-
|
|
149
|
+
const uniqueRules = dedupePermissionRules(rules);
|
|
150
|
+
if (uniqueRules.length === 1 && uniqueRules[0]) {
|
|
151
|
+
return formatPermissionRuleScope(uniqueRules[0], language);
|
|
111
152
|
}
|
|
112
|
-
if (
|
|
113
|
-
const firstCwd =
|
|
114
|
-
const sameCwd = firstCwd &&
|
|
115
|
-
const prefixes =
|
|
153
|
+
if (uniqueRules.every(rule => rule.kind === 'shell-prefix')) {
|
|
154
|
+
const firstCwd = uniqueRules[0]?.cwd;
|
|
155
|
+
const sameCwd = firstCwd && uniqueRules.every(rule => rule.kind === 'shell-prefix' && rule.cwd === firstCwd);
|
|
156
|
+
const prefixes = uniqueRules.map(rule => (rule.kind === 'shell-prefix' ? `\`${rule.prefix}\`` : '')).join(', ');
|
|
116
157
|
if (sameCwd) {
|
|
117
158
|
return t(language, 'permission.rule.multipleScoped', { prefixes, cwd: firstCwd });
|
|
118
159
|
}
|
|
119
160
|
return t(language, 'permission.rule.multiple', { prefixes });
|
|
120
161
|
}
|
|
121
|
-
return
|
|
162
|
+
return uniqueRules.map(rule => formatPermissionRuleScope(rule, language)).join(', ');
|
|
163
|
+
}
|
|
164
|
+
function permissionRuleDedupKey(rule) {
|
|
165
|
+
if (rule.kind === 'shell-prefix') {
|
|
166
|
+
return ['shell-prefix', rule.prefix.trim(), rule.cwd?.trim() ?? ''].join('\0');
|
|
167
|
+
}
|
|
168
|
+
if (rule.kind === 'network-tool') {
|
|
169
|
+
return ['network-tool', rule.toolName.trim()].join('\0');
|
|
170
|
+
}
|
|
171
|
+
return ['filesystem-write-root', rule.root.trim(), normalizeFilesystemOperations(rule.operations).join(',')].join('\0');
|
|
122
172
|
}
|
|
123
173
|
function normalizePermissionRule(rule, cwd) {
|
|
124
174
|
if (rule.kind === 'shell-prefix') {
|
|
@@ -134,6 +184,17 @@ function normalizePermissionRule(rule, cwd) {
|
|
|
134
184
|
createdAt: new Date().toISOString(),
|
|
135
185
|
};
|
|
136
186
|
}
|
|
187
|
+
if (rule.kind === 'network-tool') {
|
|
188
|
+
const toolName = rule.toolName.trim();
|
|
189
|
+
if (!toolName) {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
kind: 'network-tool',
|
|
194
|
+
toolName,
|
|
195
|
+
createdAt: new Date().toISOString(),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
137
198
|
const root = rule.root.trim();
|
|
138
199
|
if (!root) {
|
|
139
200
|
return undefined;
|
|
@@ -155,6 +216,12 @@ function mergePermissionRule(currentRules, nextRule) {
|
|
|
155
216
|
}
|
|
156
217
|
return [...currentRules, nextRule];
|
|
157
218
|
}
|
|
219
|
+
if (nextRule.kind === 'network-tool') {
|
|
220
|
+
if (currentRules.some(existing => existing.kind === 'network-tool' && existing.toolName === nextRule.toolName)) {
|
|
221
|
+
return currentRules;
|
|
222
|
+
}
|
|
223
|
+
return [...currentRules, nextRule];
|
|
224
|
+
}
|
|
158
225
|
const existingIndex = currentRules.findIndex(existing => existing.kind === 'filesystem-write-root' && existing.root === nextRule.root);
|
|
159
226
|
if (existingIndex < 0) {
|
|
160
227
|
return [...currentRules, nextRule];
|
package/dist/repl.js
CHANGED
|
@@ -5,9 +5,10 @@ import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
|
5
5
|
import { runChatTurn } from '@remi/core';
|
|
6
6
|
import { createNewSession, readSessionPermissionRules, resolveActiveSessionId, writeSessionPermissionRules } from '@remi/sessions';
|
|
7
7
|
import { buildInitPrompt } from './initPrompt.js';
|
|
8
|
+
import { accentColorPanelOptions, formatAccentColorChanged, parseAccentColorArg, resolveConfiguredAccentColor, saveProjectAccentColor, } from './accentColor.js';
|
|
8
9
|
import { languageLabel, parseLanguageArg, resolveConfiguredLanguage, saveProjectLanguage, t } from './i18n.js';
|
|
9
10
|
import { runModelCommand } from './model.js';
|
|
10
|
-
import { addSessionPermissionRules, formatPermissionProfileChanged, formatPermissionProfileList, formatPermissionRulesScope, parsePermissionProfileArg, resolveConfiguredPermissionProfile, saveProjectPermissionRule, saveProjectPermissionProfile, } from './permissions.js';
|
|
11
|
+
import { addSessionPermissionRules, canRememberPermissionRequest, dedupePermissionRules, formatPermissionProfileChanged, formatPermissionProfileList, formatPermissionRulesScope, parsePermissionProfileArg, resolveConfiguredPermissionProfile, saveProjectPermissionRule, saveProjectPermissionProfile, } from './permissions.js';
|
|
11
12
|
import { formatPermissionFileAction } from './permissionDisplay.js';
|
|
12
13
|
import { formatResumeInstruction } from './resume.js';
|
|
13
14
|
import { formatResponseStyleChanged, formatResponseStyleList, parseResponseStyleArg, resolveConfiguredResponseStyle, saveProjectResponseStyle, } from './style.js';
|
|
@@ -28,6 +29,7 @@ export async function startRepl(options = {}) {
|
|
|
28
29
|
let usageTotals = createEmptyTokenUsage();
|
|
29
30
|
let responseStyle = resolveConfiguredResponseStyle(cwd);
|
|
30
31
|
let syntaxTheme = resolveConfiguredSyntaxTheme(cwd);
|
|
32
|
+
let accentColor = resolveConfiguredAccentColor(cwd);
|
|
31
33
|
let language = resolveConfiguredLanguage(cwd);
|
|
32
34
|
let permissionProfile = resolveConfiguredPermissionProfile(cwd);
|
|
33
35
|
let sessionPermissionRules = readSessionPermissionRules(cwd, sessionId);
|
|
@@ -43,7 +45,7 @@ export async function startRepl(options = {}) {
|
|
|
43
45
|
}
|
|
44
46
|
};
|
|
45
47
|
write(`Remi ${version}`);
|
|
46
|
-
write('Commands: /init, /model, /permissions, /style, /
|
|
48
|
+
write('Commands: /init, /model, /permissions, /style, /code-panel, /accent-color, /language, /new, /exit');
|
|
47
49
|
prompt();
|
|
48
50
|
for await (const rawLine of rl) {
|
|
49
51
|
const line = rawLine.trim();
|
|
@@ -122,8 +124,8 @@ export async function startRepl(options = {}) {
|
|
|
122
124
|
prompt();
|
|
123
125
|
continue;
|
|
124
126
|
}
|
|
125
|
-
if (line === '/
|
|
126
|
-
const args = line.slice('/
|
|
127
|
+
if (line === '/code-panel' || line.startsWith('/code-panel ')) {
|
|
128
|
+
const args = line.slice('/code-panel'.length).trim().split(/\s+/).filter(Boolean);
|
|
127
129
|
const themeArg = args[0];
|
|
128
130
|
if (!themeArg || themeArg === 'list') {
|
|
129
131
|
write(formatSyntaxThemeList(syntaxTheme));
|
|
@@ -142,6 +144,26 @@ export async function startRepl(options = {}) {
|
|
|
142
144
|
prompt();
|
|
143
145
|
continue;
|
|
144
146
|
}
|
|
147
|
+
if (line === '/accent-color' || line.startsWith('/accent-color ')) {
|
|
148
|
+
const args = line.slice('/accent-color'.length).trim().split(/\s+/).filter(Boolean);
|
|
149
|
+
const colorArg = args[0];
|
|
150
|
+
if (!colorArg || colorArg === 'list') {
|
|
151
|
+
write(accentColorPanelOptions().map(option => `${option.id === accentColor ? '*' : ' '} ${option.id.padEnd(8)} ${option.label}`).join('\n'));
|
|
152
|
+
prompt();
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const colorId = parseAccentColorArg(colorArg);
|
|
156
|
+
if (!colorId) {
|
|
157
|
+
write(t(language, 'accentColor.unknown', { color: colorArg }));
|
|
158
|
+
prompt();
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
accentColor = colorId;
|
|
162
|
+
saveProjectAccentColor(cwd, accentColor);
|
|
163
|
+
write(formatAccentColorChanged(accentColor, true, language));
|
|
164
|
+
prompt();
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
145
167
|
const chatInput = line === '/init' ? buildInitPrompt() : line;
|
|
146
168
|
if (line.startsWith('/') && line !== '/init') {
|
|
147
169
|
write(t(language, 'command.unknown', { command: line }));
|
|
@@ -150,12 +172,17 @@ export async function startRepl(options = {}) {
|
|
|
150
172
|
}
|
|
151
173
|
const requestToolPermission = async (request, decision) => {
|
|
152
174
|
const fileAction = formatPermissionFileAction(request, language);
|
|
153
|
-
|
|
154
|
-
write(t(language, 'permission.request.
|
|
175
|
+
const networkAction = formatPermissionNetworkAction(request, language);
|
|
176
|
+
write(t(language, fileAction || networkAction ? 'permission.request.title.action' : 'permission.request.title'));
|
|
177
|
+
write(t(language, 'permission.request.reason', { reason: permissionRequestReason(request, decision.reason, language) }));
|
|
155
178
|
if (fileAction) {
|
|
156
179
|
write(fileAction.action);
|
|
157
180
|
write(fileAction.detail);
|
|
158
181
|
}
|
|
182
|
+
else if (networkAction) {
|
|
183
|
+
write(networkAction.action);
|
|
184
|
+
write(networkAction.detail);
|
|
185
|
+
}
|
|
159
186
|
else {
|
|
160
187
|
write(`$ ${request.command?.commandLine ?? `${request.toolName} ${request.inputSummary}`}`);
|
|
161
188
|
}
|
|
@@ -172,24 +199,24 @@ export async function startRepl(options = {}) {
|
|
|
172
199
|
? [{ kind: 'shell-prefix', prefix: request.command.suggestedPrefix }]
|
|
173
200
|
: [];
|
|
174
201
|
const fallbackSuggestedRules = explicitSuggestedRules.length > 0 ? [] : fallbackFilesystemPermissionRules(request);
|
|
175
|
-
const suggestedRules = explicitSuggestedRules.length > 0 ? explicitSuggestedRules : fallbackSuggestedRules;
|
|
202
|
+
const suggestedRules = dedupePermissionRules(explicitSuggestedRules.length > 0 ? explicitSuggestedRules : fallbackSuggestedRules);
|
|
176
203
|
const ruleLabel = suggestedRules.length > 0 ? formatPermissionRulesScope(suggestedRules, language) : '';
|
|
177
|
-
const canOfferRule = (
|
|
204
|
+
const canOfferRule = canRememberPermissionRequest(request) &&
|
|
205
|
+
((request.canPersist ?? request.command?.canPersist ?? false) || fallbackSuggestedRules.length > 0) &&
|
|
206
|
+
suggestedRules.length > 0;
|
|
178
207
|
const filesystemRulesOnly = suggestedRules.length > 0 && suggestedRules.every(rule => rule.kind === 'filesystem-write-root');
|
|
179
208
|
const shellRulesOnly = suggestedRules.length > 0 && suggestedRules.every(rule => rule.kind === 'shell-prefix');
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const answer = (await rl.question(`Allow? y=yes${persistHint}, n=no: `)).trim().toLowerCase();
|
|
186
|
-
if (answer === 's' && canOfferRule && filesystemRulesOnly) {
|
|
209
|
+
const networkRulesOnly = suggestedRules.length > 0 && suggestedRules.every(rule => rule.kind === 'network-tool');
|
|
210
|
+
const sessionHint = canOfferRule && (filesystemRulesOnly || shellRulesOnly || networkRulesOnly) ? `, s=session ${ruleLabel}` : '';
|
|
211
|
+
const persistHint = canOfferRule && (shellRulesOnly || networkRulesOnly) ? `, p=always ${ruleLabel}` : '';
|
|
212
|
+
const answer = (await rl.question(`Allow? y=allow once${sessionHint}${persistHint}, n=cancel: `)).trim().toLowerCase();
|
|
213
|
+
if (answer === 's' && canOfferRule && (filesystemRulesOnly || shellRulesOnly || networkRulesOnly)) {
|
|
187
214
|
sessionPermissionRules = addSessionPermissionRules(sessionPermissionRules, cwd, suggestedRules);
|
|
188
215
|
writeSessionPermissionRules(cwd, sessionId, sessionPermissionRules);
|
|
189
216
|
write(t(language, 'permission.savedRules', { rule: ruleLabel }));
|
|
190
|
-
return { status: 'allow', reason: 'User approved and
|
|
217
|
+
return { status: 'allow', reason: 'User approved and allowed this rule for the current session.', requirements: decision.requirements };
|
|
191
218
|
}
|
|
192
|
-
if (answer === 'p' && canOfferRule && shellRulesOnly) {
|
|
219
|
+
if (answer === 'p' && canOfferRule && (shellRulesOnly || networkRulesOnly)) {
|
|
193
220
|
for (const rule of suggestedRules) {
|
|
194
221
|
saveProjectPermissionRule(cwd, rule);
|
|
195
222
|
}
|
|
@@ -240,6 +267,14 @@ export async function startRepl(options = {}) {
|
|
|
240
267
|
sawUsageEvent = true;
|
|
241
268
|
usageTotals = addTokenUsage(usageTotals, event.usage);
|
|
242
269
|
}
|
|
270
|
+
else if (event.type === 'hook_start') {
|
|
271
|
+
write(`hook ${event.hookEventName} ${event.command}`);
|
|
272
|
+
}
|
|
273
|
+
else if (event.type === 'hook_result') {
|
|
274
|
+
if (!event.ok || event.blocking) {
|
|
275
|
+
write(`${event.blocking ? 'hook-blocked' : 'hook-error'} ${event.hookEventName} ${event.summary}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
243
278
|
else if (event.type === 'done') {
|
|
244
279
|
if (event.usage && !sawUsageEvent) {
|
|
245
280
|
usageTotals = addTokenUsage(usageTotals, event.usage);
|
|
@@ -277,6 +312,32 @@ function formatCommandCapabilityForDisplay(capability) {
|
|
|
277
312
|
}
|
|
278
313
|
return parts.join(' · ');
|
|
279
314
|
}
|
|
315
|
+
function formatPermissionNetworkAction(request, language) {
|
|
316
|
+
if (request.toolName === 'web_search') {
|
|
317
|
+
const query = parsePermissionRequestField(request.inputSummary, 'query') ?? request.inputSummary;
|
|
318
|
+
return {
|
|
319
|
+
action: t(language, 'permission.request.action.webSearch'),
|
|
320
|
+
detail: t(language, 'permission.request.action.query', { query }),
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
if (request.toolName === 'web_fetch') {
|
|
324
|
+
const url = parsePermissionRequestField(request.inputSummary, 'url') ?? request.inputSummary;
|
|
325
|
+
return {
|
|
326
|
+
action: t(language, 'permission.request.action.webFetch'),
|
|
327
|
+
detail: t(language, 'permission.request.action.url', { url }),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
function permissionRequestReason(request, fallback, language) {
|
|
333
|
+
if (request.toolName === 'web_search') {
|
|
334
|
+
return t(language, 'permission.reason.web_search');
|
|
335
|
+
}
|
|
336
|
+
if (request.toolName === 'web_fetch') {
|
|
337
|
+
return t(language, 'permission.reason.web_fetch');
|
|
338
|
+
}
|
|
339
|
+
return fallback;
|
|
340
|
+
}
|
|
280
341
|
function fallbackFilesystemPermissionRules(request) {
|
|
281
342
|
const operations = permissionFilesystemOperationsForTool(request.toolName);
|
|
282
343
|
const targetPath = request.targetPath ?? parsePermissionRequestPath(request.inputSummary);
|
|
@@ -332,13 +393,16 @@ function isPathInsideOrEqual(target, root) {
|
|
|
332
393
|
return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
|
|
333
394
|
}
|
|
334
395
|
function parsePermissionRequestPath(inputSummary) {
|
|
396
|
+
return parsePermissionRequestField(inputSummary, 'path');
|
|
397
|
+
}
|
|
398
|
+
function parsePermissionRequestField(inputSummary, key) {
|
|
335
399
|
try {
|
|
336
400
|
const parsed = JSON.parse(inputSummary);
|
|
337
401
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
338
402
|
return undefined;
|
|
339
403
|
}
|
|
340
|
-
const
|
|
341
|
-
return typeof
|
|
404
|
+
const value = parsed[key];
|
|
405
|
+
return typeof value === 'string' && value.trim().length > 0 ? value : undefined;
|
|
342
406
|
}
|
|
343
407
|
catch {
|
|
344
408
|
return undefined;
|