@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.
Files changed (47) hide show
  1. package/README.md +3 -3
  2. package/dist/accentColor.js +28 -0
  3. package/dist/doctor.js +13 -14
  4. package/dist/help.js +2 -0
  5. package/dist/i18n.js +72 -24
  6. package/dist/initPrompt.js +3 -1
  7. package/dist/permissions.js +75 -8
  8. package/dist/repl.js +83 -19
  9. package/dist/tui/RemiApp.js +355 -45
  10. package/dist/tui/commands.js +70 -3
  11. package/dist/tui/hooksPanel.js +164 -0
  12. package/dist/tui/index.js +27 -2
  13. package/dist/tui/pastedText.js +82 -0
  14. package/dist/tui/renderers/Header.js +2 -2
  15. package/dist/tui/renderers/MessageList.js +304 -57
  16. package/dist/tui/renderers/PromptBox.js +7 -1
  17. package/dist/tui/renderers/WorkingIndicator.js +20 -11
  18. package/dist/tui/statusStats.js +366 -0
  19. package/dist/tui/theme.js +44 -2
  20. package/dist/version.js +1 -1
  21. package/node_modules/@remi/compact/package.json +1 -1
  22. package/node_modules/@remi/config/dist/index.js +105 -36
  23. package/node_modules/@remi/config/package.json +1 -1
  24. package/node_modules/@remi/core/dist/contextBuilder.js +3 -3
  25. package/node_modules/@remi/core/dist/directoryOverview.js +2 -5
  26. package/node_modules/@remi/core/dist/index.js +641 -50
  27. package/node_modules/@remi/core/dist/projectInstructions.js +1 -1
  28. package/node_modules/@remi/core/dist/promptFiles.js +17 -0
  29. package/node_modules/@remi/core/dist/responseStyles.js +2 -2
  30. package/node_modules/@remi/core/package.json +1 -1
  31. package/node_modules/@remi/hooks/dist/index.js +229 -0
  32. package/node_modules/@remi/hooks/package.json +8 -0
  33. package/node_modules/@remi/llm/package.json +1 -1
  34. package/node_modules/@remi/memory/dist/index.js +10 -3
  35. package/node_modules/@remi/memory/package.json +1 -1
  36. package/node_modules/@remi/permissions/package.json +1 -1
  37. package/node_modules/@remi/sessions/dist/index.js +26 -13
  38. package/node_modules/@remi/sessions/package.json +1 -1
  39. package/node_modules/@remi/skills/dist/index.js +32 -10
  40. package/node_modules/@remi/skills/package.json +1 -1
  41. package/node_modules/@remi/terminal-markdown/dist/index.js +19 -7
  42. package/node_modules/@remi/terminal-markdown/package.json +1 -1
  43. package/node_modules/@remi/tools/dist/index.js +513 -7
  44. package/node_modules/@remi/tools/package.json +1 -1
  45. package/package.json +14 -11
  46. package/prompt/base-system.md +34 -0
  47. package/prompt/tool-use-system.md +83 -0
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # Remi
2
2
 
3
- Personal alpha build of the Remi AI coding agent CLI.
3
+ Remi AI coding agent CLI.
4
4
 
5
5
  ```bash
6
- npm install -g @wangzhizhi/remi@alpha
6
+ npm install -g @wangzhizhi/remi@0.1.214
7
7
  remi setup
8
- remi doctor
8
+ remi
9
9
  ```
@@ -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 agentDir = join(cwd, '.agent');
42
- mkdirSync(agentDir, { recursive: true });
43
- const probePath = join(agentDir, '.doctor-write-test');
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: 'agentDir',
47
+ name: 'stateDir',
48
48
  ok: true,
49
- detail: agentDir,
49
+ detail: stateDir,
50
50
  });
51
51
  }
52
52
  catch (error) {
53
53
  checks.push({
54
- name: 'agentDir',
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.theme.description': 'Choose syntax theme for code and diffs',
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 Syntax Theme',
34
- 'theme.panel.subtitle': 'Move up/down to live preview code themes.',
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': 'Syntax theme selected: {{label}}.',
39
- 'theme.saved': 'Syntax theme saved: {{label}}.',
40
- 'theme.closed': 'Syntax theme settings closed without saving.',
41
- 'theme.unknown': 'Unknown syntax theme: {{theme}}. Run /theme to choose.',
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': "Yes, and don't ask again for commands that start with `{{prefix}}`",
72
- 'permission.request.persistRule': "Yes, and don't ask again for {{rule}}",
73
- 'permission.request.sessionRule': 'Yes, during this session for {{rule}}',
74
- 'permission.request.once': 'Yes, proceed',
75
- 'permission.request.deny': 'No, tell Remi what to do differently',
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.theme.description': '选择代码和 diff 的语法主题',
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': '已选择语法主题:{{label}}。',
226
- 'theme.saved': '已保存语法主题:{{label}}。',
227
- 'theme.closed': '已关闭语法主题设置,未保存更改。',
228
- 'theme.unknown': '未知语法主题:{{theme}}。运行 /theme 选择。',
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': '是,并且不再询问以 `{{prefix}}` 开头的命令',
259
- 'permission.request.persistRule': '是,并且不再询问{{rule}}',
260
- 'permission.request.sessionRule': '是,本 session 允许{{rule}}',
261
- 'permission.request.once': '是,继续',
262
- 'permission.request.deny': '否,告诉 Remi 换个方式',
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}}。',
@@ -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, CLAUDE.md, REMI.md, .cursor/rules, .cursorrules, .github/copilot-instructions.md, .windsurfrules, or .clinerules.
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:
@@ -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
- if (rules.length === 1 && rules[0]) {
110
- return formatPermissionRuleScope(rules[0], language);
149
+ const uniqueRules = dedupePermissionRules(rules);
150
+ if (uniqueRules.length === 1 && uniqueRules[0]) {
151
+ return formatPermissionRuleScope(uniqueRules[0], language);
111
152
  }
112
- if (rules.every(rule => rule.kind === 'shell-prefix')) {
113
- const firstCwd = rules[0]?.cwd;
114
- const sameCwd = firstCwd && rules.every(rule => rule.kind === 'shell-prefix' && rule.cwd === firstCwd);
115
- const prefixes = rules.map(rule => (rule.kind === 'shell-prefix' ? `\`${rule.prefix}\`` : '')).join(', ');
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 rules.map(rule => formatPermissionRuleScope(rule, language)).join(', ');
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, /theme, /language, /new, /exit');
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 === '/theme' || line.startsWith('/theme ')) {
126
- const args = line.slice('/theme'.length).trim().split(/\s+/).filter(Boolean);
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
- write(t(language, fileAction ? 'permission.request.title.action' : 'permission.request.title'));
154
- write(t(language, 'permission.request.reason', { reason: decision.reason }));
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 = ((request.canPersist ?? request.command?.canPersist ?? false) || fallbackSuggestedRules.length > 0) && suggestedRules.length > 0;
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 persistHint = canOfferRule && filesystemRulesOnly
181
- ? `, s=session ${ruleLabel}`
182
- : canOfferRule && shellRulesOnly
183
- ? `, p=don't-ask-again ${ruleLabel}`
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 saved a permission rule.', requirements: decision.requirements };
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 path = parsed['path'];
341
- return typeof path === 'string' && path.trim().length > 0 ? path : undefined;
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;