@lobehub/chat 1.141.7 → 1.141.9

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 (128) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/apps/desktop/package.json +1 -0
  3. package/apps/desktop/src/main/controllers/LocalFileCtr.ts +279 -52
  4. package/apps/desktop/src/main/controllers/__tests__/LocalFileCtr.test.ts +392 -0
  5. package/changelog/v1.json +18 -0
  6. package/docs/usage/features/{group-chat.mdx → agent-team.mdx} +14 -14
  7. package/docs/usage/features/agent-team.zh-CN.mdx +52 -0
  8. package/locales/ar/chat.json +17 -17
  9. package/locales/ar/setting.json +15 -19
  10. package/locales/ar/welcome.json +1 -1
  11. package/locales/bg-BG/chat.json +17 -17
  12. package/locales/bg-BG/setting.json +15 -19
  13. package/locales/de-DE/chat.json +17 -17
  14. package/locales/de-DE/setting.json +15 -19
  15. package/locales/de-DE/welcome.json +1 -1
  16. package/locales/en-US/chat.json +17 -17
  17. package/locales/en-US/setting.json +15 -19
  18. package/locales/en-US/welcome.json +1 -1
  19. package/locales/es-ES/chat.json +17 -17
  20. package/locales/es-ES/setting.json +15 -19
  21. package/locales/es-ES/welcome.json +1 -1
  22. package/locales/fa-IR/chat.json +17 -17
  23. package/locales/fa-IR/setting.json +15 -19
  24. package/locales/fa-IR/welcome.json +1 -1
  25. package/locales/fr-FR/chat.json +16 -16
  26. package/locales/fr-FR/setting.json +15 -19
  27. package/locales/fr-FR/welcome.json +1 -1
  28. package/locales/it-IT/chat.json +17 -17
  29. package/locales/it-IT/setting.json +15 -19
  30. package/locales/it-IT/welcome.json +1 -1
  31. package/locales/ja-JP/chat.json +17 -17
  32. package/locales/ja-JP/setting.json +15 -19
  33. package/locales/ja-JP/welcome.json +1 -1
  34. package/locales/ko-KR/chat.json +17 -17
  35. package/locales/ko-KR/setting.json +15 -19
  36. package/locales/ko-KR/welcome.json +1 -1
  37. package/locales/nl-NL/chat.json +17 -17
  38. package/locales/nl-NL/setting.json +15 -19
  39. package/locales/nl-NL/welcome.json +1 -1
  40. package/locales/pl-PL/chat.json +17 -17
  41. package/locales/pl-PL/setting.json +15 -19
  42. package/locales/pt-BR/chat.json +17 -17
  43. package/locales/pt-BR/setting.json +15 -19
  44. package/locales/pt-BR/welcome.json +1 -1
  45. package/locales/ru-RU/chat.json +17 -17
  46. package/locales/ru-RU/setting.json +15 -19
  47. package/locales/ru-RU/welcome.json +1 -1
  48. package/locales/tr-TR/chat.json +17 -17
  49. package/locales/tr-TR/setting.json +15 -19
  50. package/locales/vi-VN/chat.json +15 -15
  51. package/locales/vi-VN/setting.json +15 -19
  52. package/locales/zh-CN/chat.json +17 -17
  53. package/locales/zh-CN/setting.json +15 -19
  54. package/locales/zh-CN/welcome.json +1 -1
  55. package/locales/zh-TW/chat.json +17 -17
  56. package/locales/zh-TW/setting.json +15 -19
  57. package/locales/zh-TW/welcome.json +1 -1
  58. package/package.json +1 -1
  59. package/packages/agent-runtime/src/core/InterventionChecker.ts +173 -0
  60. package/packages/agent-runtime/src/core/UsageCounter.ts +248 -0
  61. package/packages/agent-runtime/src/core/__tests__/InterventionChecker.test.ts +334 -0
  62. package/packages/agent-runtime/src/core/__tests__/UsageCounter.test.ts +873 -0
  63. package/packages/agent-runtime/src/core/__tests__/runtime.test.ts +32 -26
  64. package/packages/agent-runtime/src/core/index.ts +2 -0
  65. package/packages/agent-runtime/src/core/runtime.ts +31 -18
  66. package/packages/agent-runtime/src/types/instruction.ts +1 -1
  67. package/packages/agent-runtime/src/types/state.ts +3 -3
  68. package/packages/agent-runtime/src/types/usage.ts +34 -25
  69. package/packages/const/src/settings/systemAgent.ts +0 -1
  70. package/packages/context-engine/src/index.ts +1 -0
  71. package/packages/context-engine/src/tools/ToolNameResolver.ts +2 -2
  72. package/packages/context-engine/src/tools/ToolsEngine.ts +37 -8
  73. package/packages/context-engine/src/tools/__tests__/ToolsEngine.test.ts +149 -5
  74. package/packages/context-engine/src/tools/__tests__/utils.test.ts +2 -2
  75. package/packages/context-engine/src/tools/index.ts +1 -0
  76. package/packages/context-engine/src/tools/types.ts +18 -3
  77. package/packages/context-engine/src/tools/utils.ts +4 -4
  78. package/packages/types/src/tool/builtin.ts +54 -1
  79. package/packages/types/src/tool/index.ts +1 -0
  80. package/packages/types/src/tool/intervention.ts +114 -0
  81. package/packages/types/src/user/settings/systemAgent.ts +0 -1
  82. package/packages/types/src/user/settings/tool.ts +37 -0
  83. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/ChatItem/OrchestratorThinking.tsx +2 -3
  84. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/ChatItem/index.tsx +2 -2
  85. package/src/app/[variants]/(main)/chat/(workspace)/@topic/features/GroupConfig/GroupMember.tsx +34 -2
  86. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/Main.tsx +1 -1
  87. package/src/app/[variants]/(main)/chat/(workspace)/features/{GroupChatSettings → AgentTeamSettings}/index.tsx +4 -5
  88. package/src/app/[variants]/(main)/chat/(workspace)/features/SettingButton.tsx +2 -2
  89. package/src/app/[variants]/(main)/chat/@session/_layout/Desktop/SessionHeader.tsx +2 -0
  90. package/src/app/[variants]/(main)/chat/@session/features/SessionListContent/CollapseGroup/Actions.tsx +18 -1
  91. package/src/components/ChatGroupWizard/ChatGroupWizard.tsx +33 -5
  92. package/src/components/MemberSelectionModal/MemberSelectionModal.tsx +170 -26
  93. package/src/features/Conversation/Messages/Assistant/Actions/index.tsx +7 -2
  94. package/src/features/Conversation/Messages/Assistant/Tool/Render/index.tsx +4 -2
  95. package/src/features/Conversation/Messages/User/Actions.tsx +8 -2
  96. package/src/features/GroupChatSettings/{ChatGroupSettings.tsx → AgentTeamChatSettings.tsx} +6 -5
  97. package/src/features/GroupChatSettings/{GroupMembers.tsx → AgentTeamMembersSettings.tsx} +64 -19
  98. package/src/features/GroupChatSettings/{ChatGroupMeta.tsx → AgentTeamMetaSettings.tsx} +2 -2
  99. package/src/features/GroupChatSettings/AgentTeamSettings.tsx +54 -0
  100. package/src/features/GroupChatSettings/index.ts +4 -5
  101. package/src/locales/default/chat.ts +17 -17
  102. package/src/locales/default/setting.ts +15 -19
  103. package/src/locales/default/welcome.ts +1 -1
  104. package/src/store/chat/slices/aiChat/actions/generateAIGroupChat.ts +2 -1
  105. package/src/store/chat/slices/builtinTool/actions/{dalle.test.ts → __tests__/dalle.test.ts} +2 -5
  106. package/src/store/chat/slices/builtinTool/actions/__tests__/{localFile.test.ts → localSystem.test.ts} +4 -4
  107. package/src/store/chat/slices/builtinTool/actions/index.ts +2 -2
  108. package/src/store/chat/slices/builtinTool/actions/{localFile.ts → localSystem.ts} +183 -69
  109. package/src/store/chatGroup/action.ts +36 -1
  110. package/src/store/electron/selectors/__tests__/desktopState.test.ts +3 -3
  111. package/src/store/electron/selectors/desktopState.ts +11 -2
  112. package/src/store/user/slices/settings/selectors/__snapshots__/settings.test.ts.snap +0 -4
  113. package/src/store/user/slices/settings/selectors/systemAgent.ts +0 -2
  114. package/src/tools/local-system/Placeholder/ListFiles.tsx +10 -8
  115. package/src/tools/local-system/Placeholder/SearchFiles.tsx +12 -10
  116. package/src/tools/local-system/Placeholder/index.tsx +1 -1
  117. package/src/tools/local-system/Render/ReadLocalFile/ReadFileSkeleton.tsx +8 -18
  118. package/src/tools/local-system/Render/ReadLocalFile/ReadFileView.tsx +21 -6
  119. package/src/tools/local-system/Render/SearchFiles/Result.tsx +5 -4
  120. package/src/tools/local-system/Render/SearchFiles/SearchQuery/SearchView.tsx +4 -15
  121. package/src/tools/local-system/Render/SearchFiles/index.tsx +3 -2
  122. package/src/tools/local-system/type.ts +39 -0
  123. package/docs/usage/features/group-chat.zh-CN.mdx +0 -52
  124. package/src/features/GroupChatSettings/GroupSettings.tsx +0 -30
  125. package/src/features/GroupChatSettings/GroupSettingsContent.tsx +0 -24
  126. package/src/tools/local-system/Placeholder/ReadLocalFile.tsx +0 -9
  127. package/src/tools/local-system/Render/ReadLocalFile/style.ts +0 -37
  128. /package/src/store/chat/slices/builtinTool/actions/{search.test.ts → __tests__/search.test.ts} +0 -0
@@ -0,0 +1,248 @@
1
+ import { ModelUsage } from '@lobechat/types';
2
+
3
+ import { Cost, Usage } from '../types/usage';
4
+
5
+ /**
6
+ * UsageCounter - Pure accumulator for usage and cost tracking
7
+ * Focuses only on usage/cost calculations without managing state
8
+ */
9
+ /* eslint-disable unicorn/no-static-only-class */
10
+ export class UsageCounter {
11
+ /**
12
+ * Create default usage statistics
13
+ */
14
+ private static createDefaultUsage(): Usage {
15
+ return {
16
+ humanInteraction: {
17
+ approvalRequests: 0,
18
+ promptRequests: 0,
19
+ selectRequests: 0,
20
+ totalWaitingTimeMs: 0,
21
+ },
22
+ llm: {
23
+ apiCalls: 0,
24
+ processingTimeMs: 0,
25
+ tokens: {
26
+ input: 0,
27
+ output: 0,
28
+ total: 0,
29
+ },
30
+ },
31
+ tools: {
32
+ byTool: [],
33
+ totalCalls: 0,
34
+ totalTimeMs: 0,
35
+ },
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Create default cost statistics
41
+ */
42
+ private static createDefaultCost(): Cost {
43
+ return {
44
+ calculatedAt: new Date().toISOString(),
45
+ currency: 'USD',
46
+ llm: {
47
+ byModel: [],
48
+ currency: 'USD',
49
+ total: 0,
50
+ },
51
+ tools: {
52
+ byTool: [],
53
+ currency: 'USD',
54
+ total: 0,
55
+ },
56
+ total: 0,
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Merge two ModelUsage objects by accumulating token counts
62
+ * @param previous - Previous usage statistics
63
+ * @param current - Current usage statistics to add
64
+ * @returns Merged usage statistics
65
+ */
66
+ private static mergeModelUsage(
67
+ previous: ModelUsage | undefined,
68
+ current: ModelUsage,
69
+ ): ModelUsage {
70
+ if (!previous) return current;
71
+
72
+ const merged: ModelUsage = { ...current };
73
+
74
+ // Accumulate all numeric token fields
75
+ const numericFields: (keyof ModelUsage)[] = [
76
+ 'inputCachedTokens',
77
+ 'inputCacheMissTokens',
78
+ 'inputWriteCacheTokens',
79
+ 'inputTextTokens',
80
+ 'inputImageTokens',
81
+ 'inputAudioTokens',
82
+ 'inputCitationTokens',
83
+ 'outputTextTokens',
84
+ 'outputImageTokens',
85
+ 'outputAudioTokens',
86
+ 'outputReasoningTokens',
87
+ 'acceptedPredictionTokens',
88
+ 'rejectedPredictionTokens',
89
+ 'totalInputTokens',
90
+ 'totalOutputTokens',
91
+ 'totalTokens',
92
+ ];
93
+
94
+ for (const field of numericFields) {
95
+ const prevValue = previous[field] as number | undefined;
96
+ const currValue = current[field] as number | undefined;
97
+
98
+ if (prevValue !== undefined || currValue !== undefined) {
99
+ merged[field] = (prevValue || 0) + (currValue || 0);
100
+ }
101
+ }
102
+
103
+ // Accumulate cost
104
+ if (previous.cost !== undefined || current.cost !== undefined) {
105
+ merged.cost = (previous.cost || 0) + (current.cost || 0);
106
+ }
107
+
108
+ return merged;
109
+ }
110
+
111
+ /**
112
+ * Accumulate LLM usage and cost for a specific model
113
+ * @param params - Accumulation parameters
114
+ * @param params.usage - Current usage statistics (optional, will be created if not provided)
115
+ * @param params.cost - Current cost statistics (optional, will be created if not provided)
116
+ * @param params.provider - Provider name (e.g., "openai")
117
+ * @param params.model - Model name (e.g., "gpt-4")
118
+ * @param params.modelUsage - ModelUsage from model-runtime
119
+ * @returns Updated usage and cost
120
+ */
121
+ static accumulateLLM(params: {
122
+ cost?: Cost;
123
+ model: string;
124
+ modelUsage: ModelUsage;
125
+ provider: string;
126
+ usage?: Usage;
127
+ }): { cost?: Cost; usage: Usage } {
128
+ const { usage, cost, provider, model, modelUsage } = params;
129
+
130
+ // Ensure usage exists
131
+ const newUsage = usage ? structuredClone(usage) : this.createDefaultUsage();
132
+
133
+ // Accumulate token counts to usage.llm
134
+ newUsage.llm.tokens.input += modelUsage.totalInputTokens ?? 0;
135
+ newUsage.llm.tokens.output += modelUsage.totalOutputTokens ?? 0;
136
+ newUsage.llm.tokens.total += modelUsage.totalTokens ?? 0;
137
+ newUsage.llm.apiCalls += 1;
138
+
139
+ // Ensure cost exists if modelUsage has cost
140
+ let newCost = cost
141
+ ? structuredClone(cost)
142
+ : modelUsage.cost
143
+ ? this.createDefaultCost()
144
+ : undefined;
145
+
146
+ if (modelUsage.cost && newCost) {
147
+ const modelId = `${provider}/${model}`;
148
+
149
+ // Find or create byModel entry
150
+ let modelEntry = newCost.llm.byModel.find((entry) => entry.id === modelId);
151
+
152
+ if (!modelEntry) {
153
+ modelEntry = {
154
+ id: modelId,
155
+ model,
156
+ provider,
157
+ totalCost: 0,
158
+ usage: {},
159
+ };
160
+ newCost.llm.byModel.push(modelEntry);
161
+ }
162
+
163
+ // Merge usage breakdown
164
+ modelEntry.usage = UsageCounter.mergeModelUsage(modelEntry.usage, modelUsage);
165
+
166
+ // Accumulate costs
167
+ modelEntry.totalCost += modelUsage.cost;
168
+ newCost.llm.total += modelUsage.cost;
169
+ newCost.total += modelUsage.cost;
170
+ newCost.calculatedAt = new Date().toISOString();
171
+ }
172
+
173
+ return { cost: newCost, usage: newUsage };
174
+ }
175
+
176
+ /**
177
+ * Accumulate tool usage and cost
178
+ * @param params - Accumulation parameters
179
+ * @param params.usage - Current usage statistics (optional, will be created if not provided)
180
+ * @param params.cost - Current cost statistics (optional, will be created if not provided)
181
+ * @param params.toolName - Tool identifier
182
+ * @param params.executionTime - Execution time in milliseconds
183
+ * @param params.success - Whether the execution was successful
184
+ * @param params.toolCost - Optional cost for this tool call
185
+ * @returns Updated usage and cost
186
+ */
187
+ static accumulateTool(params: {
188
+ cost?: Cost;
189
+ executionTime: number;
190
+ success: boolean;
191
+ toolCost?: number;
192
+ toolName: string;
193
+ usage?: Usage;
194
+ }): { cost?: Cost; usage: Usage } {
195
+ const { usage, cost, toolName, executionTime, success, toolCost } = params;
196
+
197
+ // Ensure usage exists
198
+ const newUsage = usage ? structuredClone(usage) : this.createDefaultUsage();
199
+
200
+ // Find or create byTool entry
201
+ let toolEntry = newUsage.tools.byTool.find((entry) => entry.name === toolName);
202
+
203
+ if (!toolEntry) {
204
+ toolEntry = {
205
+ calls: 0,
206
+ errors: 0,
207
+ name: toolName,
208
+ totalTimeMs: 0,
209
+ };
210
+ newUsage.tools.byTool.push(toolEntry);
211
+ }
212
+
213
+ // Accumulate tool usage
214
+ toolEntry.calls += 1;
215
+ toolEntry.totalTimeMs += executionTime;
216
+ if (!success) {
217
+ toolEntry.errors += 1;
218
+ }
219
+
220
+ newUsage.tools.totalCalls += 1;
221
+ newUsage.tools.totalTimeMs += executionTime;
222
+
223
+ // Ensure cost exists if toolCost is provided
224
+ let newCost = cost ? structuredClone(cost) : toolCost ? this.createDefaultCost() : undefined;
225
+
226
+ if (toolCost && newCost) {
227
+ let toolCostEntry = newCost.tools.byTool.find((entry) => entry.name === toolName);
228
+
229
+ if (!toolCostEntry) {
230
+ toolCostEntry = {
231
+ calls: 0,
232
+ currency: 'USD',
233
+ name: toolName,
234
+ totalCost: 0,
235
+ };
236
+ newCost.tools.byTool.push(toolCostEntry);
237
+ }
238
+
239
+ toolCostEntry.calls += 1;
240
+ toolCostEntry.totalCost += toolCost;
241
+ newCost.tools.total += toolCost;
242
+ newCost.total += toolCost;
243
+ newCost.calculatedAt = new Date().toISOString();
244
+ }
245
+
246
+ return { cost: newCost, usage: newUsage };
247
+ }
248
+ }
@@ -0,0 +1,334 @@
1
+ import type { HumanInterventionConfig } from '@lobechat/types';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { InterventionChecker } from '../InterventionChecker';
5
+
6
+ describe('InterventionChecker', () => {
7
+ describe('shouldIntervene', () => {
8
+ it('should return never when config is undefined', () => {
9
+ const result = InterventionChecker.shouldIntervene({ config: undefined, toolArgs: {} });
10
+ expect(result).toBe('never');
11
+ });
12
+
13
+ it('should return the policy when config is a simple string', () => {
14
+ expect(InterventionChecker.shouldIntervene({ config: 'never', toolArgs: {} })).toBe('never');
15
+ expect(InterventionChecker.shouldIntervene({ config: 'always', toolArgs: {} })).toBe(
16
+ 'always',
17
+ );
18
+ expect(InterventionChecker.shouldIntervene({ config: 'first', toolArgs: {} })).toBe('first');
19
+ });
20
+
21
+ it('should handle "first" policy with confirmed history', () => {
22
+ const toolKey = 'web-browsing/crawlSinglePage';
23
+ const confirmedHistory = [toolKey];
24
+
25
+ const result = InterventionChecker.shouldIntervene({
26
+ config: 'first',
27
+ toolArgs: {},
28
+ confirmedHistory,
29
+ toolKey,
30
+ });
31
+ expect(result).toBe('never');
32
+ });
33
+
34
+ it('should require intervention for "first" policy without confirmation', () => {
35
+ const toolKey = 'web-browsing/crawlSinglePage';
36
+ const confirmedHistory: string[] = [];
37
+
38
+ const result = InterventionChecker.shouldIntervene({
39
+ config: 'first',
40
+ toolArgs: {},
41
+ confirmedHistory,
42
+ toolKey,
43
+ });
44
+ expect(result).toBe('first');
45
+ });
46
+
47
+ it('should match rules in order and return first match', () => {
48
+ const config: HumanInterventionConfig = [
49
+ { match: { command: 'ls:*' }, policy: 'never' },
50
+ { match: { command: 'git commit:*' }, policy: 'first' },
51
+ { policy: 'always' }, // Default rule
52
+ ];
53
+
54
+ expect(InterventionChecker.shouldIntervene({ config, toolArgs: { command: 'ls:' } })).toBe(
55
+ 'never',
56
+ );
57
+ expect(
58
+ InterventionChecker.shouldIntervene({ config, toolArgs: { command: 'git commit:' } }),
59
+ ).toBe('first');
60
+ expect(
61
+ InterventionChecker.shouldIntervene({ config, toolArgs: { command: 'rm -rf /' } }),
62
+ ).toBe('always');
63
+ });
64
+
65
+ it('should return always as default when no rule matches', () => {
66
+ const config: HumanInterventionConfig = [{ match: { command: 'ls:*' }, policy: 'never' }];
67
+
68
+ const result = InterventionChecker.shouldIntervene({
69
+ config,
70
+ toolArgs: { command: 'rm -rf /' },
71
+ });
72
+ expect(result).toBe('always');
73
+ });
74
+
75
+ it('should handle multiple parameter matching', () => {
76
+ const config: HumanInterventionConfig = [
77
+ {
78
+ match: {
79
+ command: 'git add:*',
80
+ path: '/Users/project/*',
81
+ },
82
+ policy: 'never',
83
+ },
84
+ { policy: 'always' },
85
+ ];
86
+
87
+ // Both match
88
+ expect(
89
+ InterventionChecker.shouldIntervene({
90
+ config,
91
+ toolArgs: {
92
+ command: 'git add:.',
93
+ path: '/Users/project/file.ts',
94
+ },
95
+ }),
96
+ ).toBe('never');
97
+
98
+ // Only one matches
99
+ expect(
100
+ InterventionChecker.shouldIntervene({
101
+ config,
102
+ toolArgs: {
103
+ command: 'git add:.',
104
+ path: '/tmp/file.ts',
105
+ },
106
+ }),
107
+ ).toBe('always');
108
+ });
109
+
110
+ it('should handle default rule without match', () => {
111
+ const config: HumanInterventionConfig = [
112
+ { match: { command: 'ls:*' }, policy: 'never' },
113
+ { policy: 'first' }, // Default rule
114
+ ];
115
+
116
+ const result = InterventionChecker.shouldIntervene({
117
+ config,
118
+ toolArgs: { command: 'anything' },
119
+ });
120
+ expect(result).toBe('first');
121
+ });
122
+ });
123
+
124
+ describe('matchPattern', () => {
125
+ it('should match exact strings', () => {
126
+ expect(InterventionChecker['matchPattern']('hello', 'hello')).toBe(true);
127
+ expect(InterventionChecker['matchPattern']('hello', 'world')).toBe(false);
128
+ });
129
+
130
+ it('should match wildcard patterns', () => {
131
+ expect(InterventionChecker['matchPattern']('*.ts', 'file.ts')).toBe(true);
132
+ expect(InterventionChecker['matchPattern']('*.ts', 'file.js')).toBe(false);
133
+ expect(InterventionChecker['matchPattern']('test*', 'test123')).toBe(true);
134
+ expect(InterventionChecker['matchPattern']('test*', 'abc123')).toBe(false);
135
+ });
136
+
137
+ it('should match colon-based prefix patterns', () => {
138
+ expect(InterventionChecker['matchPattern']('git add:*', 'git add:')).toBe(true);
139
+ expect(InterventionChecker['matchPattern']('git add:*', 'git add:.')).toBe(true);
140
+ expect(InterventionChecker['matchPattern']('git add:*', 'git add:--all')).toBe(true);
141
+ expect(InterventionChecker['matchPattern']('git add:*', 'git commit')).toBe(false);
142
+ });
143
+
144
+ it('should match path patterns', () => {
145
+ expect(
146
+ InterventionChecker['matchPattern']('/Users/project/*', '/Users/project/file.ts'),
147
+ ).toBe(true);
148
+ expect(InterventionChecker['matchPattern']('/Users/project/*', '/tmp/file.ts')).toBe(false);
149
+ });
150
+ });
151
+
152
+ describe('matchesArgument', () => {
153
+ it('should match exact type', () => {
154
+ const matcher = { pattern: 'git add', type: 'exact' as const };
155
+ expect(InterventionChecker['matchesArgument'](matcher, 'git add')).toBe(true);
156
+ expect(InterventionChecker['matchesArgument'](matcher, 'git add:.')).toBe(false);
157
+ });
158
+
159
+ it('should match prefix type', () => {
160
+ const matcher = { pattern: 'git add', type: 'prefix' as const };
161
+ expect(InterventionChecker['matchesArgument'](matcher, 'git add')).toBe(true);
162
+ expect(InterventionChecker['matchesArgument'](matcher, 'git add:.')).toBe(true);
163
+ expect(InterventionChecker['matchesArgument'](matcher, 'git commit')).toBe(false);
164
+ });
165
+
166
+ it('should match wildcard type', () => {
167
+ const matcher = { pattern: 'git *', type: 'wildcard' as const };
168
+ expect(InterventionChecker['matchesArgument'](matcher, 'git add')).toBe(true);
169
+ expect(InterventionChecker['matchesArgument'](matcher, 'git commit')).toBe(true);
170
+ expect(InterventionChecker['matchesArgument'](matcher, 'npm install')).toBe(false);
171
+ });
172
+
173
+ it('should match regex type', () => {
174
+ const matcher = { pattern: '^git (add|commit)', type: 'regex' as const };
175
+ expect(InterventionChecker['matchesArgument'](matcher, 'git add')).toBe(true);
176
+ expect(InterventionChecker['matchesArgument'](matcher, 'git commit')).toBe(true);
177
+ expect(InterventionChecker['matchesArgument'](matcher, 'git push')).toBe(false);
178
+ });
179
+
180
+ it('should handle simple string matcher', () => {
181
+ expect(InterventionChecker['matchesArgument']('git add:*', 'git add:.')).toBe(true);
182
+ expect(InterventionChecker['matchesArgument']('*.ts', 'file.ts')).toBe(true);
183
+ expect(InterventionChecker['matchesArgument']('exact', 'exact')).toBe(true);
184
+ });
185
+ });
186
+
187
+ describe('generateToolKey', () => {
188
+ it('should generate key without args hash', () => {
189
+ const key = InterventionChecker.generateToolKey('web-browsing', 'crawlSinglePage');
190
+ expect(key).toBe('web-browsing/crawlSinglePage');
191
+ });
192
+
193
+ it('should generate key with args hash', () => {
194
+ const key = InterventionChecker.generateToolKey('bash', 'bash', 'a1b2c3');
195
+ expect(key).toBe('bash/bash#a1b2c3');
196
+ });
197
+ });
198
+
199
+ describe('hashArguments', () => {
200
+ it('should generate consistent hash for same arguments', () => {
201
+ const args1 = { command: 'ls -la', path: '/tmp' };
202
+ const args2 = { command: 'ls -la', path: '/tmp' };
203
+
204
+ const hash1 = InterventionChecker.hashArguments(args1);
205
+ const hash2 = InterventionChecker.hashArguments(args2);
206
+
207
+ expect(hash1).toBe(hash2);
208
+ });
209
+
210
+ it('should generate different hash for different arguments', () => {
211
+ const args1 = { command: 'ls -la' };
212
+ const args2 = { command: 'ls -l' };
213
+
214
+ const hash1 = InterventionChecker.hashArguments(args1);
215
+ const hash2 = InterventionChecker.hashArguments(args2);
216
+
217
+ expect(hash1).not.toBe(hash2);
218
+ });
219
+
220
+ it('should handle key order independence', () => {
221
+ const args1 = { a: 1, b: 2 };
222
+ const args2 = { b: 2, a: 1 };
223
+
224
+ const hash1 = InterventionChecker.hashArguments(args1);
225
+ const hash2 = InterventionChecker.hashArguments(args2);
226
+
227
+ expect(hash1).toBe(hash2);
228
+ });
229
+
230
+ it('should handle empty arguments', () => {
231
+ const hash = InterventionChecker.hashArguments({});
232
+ expect(hash).toBeDefined();
233
+ expect(typeof hash).toBe('string');
234
+ });
235
+
236
+ it('should handle complex nested objects', () => {
237
+ const args = {
238
+ config: { nested: { value: 'test' } },
239
+ array: [1, 2, 3],
240
+ };
241
+
242
+ const hash = InterventionChecker.hashArguments(args);
243
+ expect(hash).toBeDefined();
244
+ expect(typeof hash).toBe('string');
245
+ });
246
+ });
247
+
248
+ describe('Integration scenarios', () => {
249
+ it('should handle Bash tool scenario', () => {
250
+ const config: HumanInterventionConfig = [
251
+ { match: { command: 'ls:*' }, policy: 'never' },
252
+ { match: { command: 'git add:*' }, policy: 'first' },
253
+ { match: { command: 'git commit:*' }, policy: 'first' },
254
+ { match: { command: 'rm:*' }, policy: 'always' },
255
+ { policy: 'always' },
256
+ ];
257
+
258
+ // Safe commands - never
259
+ expect(InterventionChecker.shouldIntervene({ config, toolArgs: { command: 'ls:' } })).toBe(
260
+ 'never',
261
+ );
262
+
263
+ // Git commands - first
264
+ expect(
265
+ InterventionChecker.shouldIntervene({ config, toolArgs: { command: 'git add:.' } }),
266
+ ).toBe('first');
267
+ expect(
268
+ InterventionChecker.shouldIntervene({ config, toolArgs: { command: 'git commit:-m' } }),
269
+ ).toBe('first');
270
+
271
+ // Dangerous commands - always
272
+ expect(InterventionChecker.shouldIntervene({ config, toolArgs: { command: 'rm:-rf' } })).toBe(
273
+ 'always',
274
+ );
275
+ expect(
276
+ InterventionChecker.shouldIntervene({ config, toolArgs: { command: 'npm install' } }),
277
+ ).toBe('always');
278
+ });
279
+
280
+ it('should handle LocalSystem tool scenario', () => {
281
+ const config: HumanInterventionConfig = [
282
+ { match: { path: '/Users/project/*' }, policy: 'never' },
283
+ { policy: 'first' },
284
+ ];
285
+
286
+ // Project directory - never
287
+ expect(
288
+ InterventionChecker.shouldIntervene({
289
+ config,
290
+ toolArgs: { path: '/Users/project/file.ts' },
291
+ }),
292
+ ).toBe('never');
293
+
294
+ // Outside project - first
295
+ expect(
296
+ InterventionChecker.shouldIntervene({ config, toolArgs: { path: '/tmp/file.ts' } }),
297
+ ).toBe('first');
298
+ });
299
+
300
+ it('should handle Web Browsing tool with simple policy', () => {
301
+ const config: HumanInterventionConfig = 'always';
302
+
303
+ expect(
304
+ InterventionChecker.shouldIntervene({ config, toolArgs: { url: 'https://example.com' } }),
305
+ ).toBe('always');
306
+ });
307
+
308
+ it('should handle first policy with confirmation history', () => {
309
+ const config: HumanInterventionConfig = [
310
+ { match: { command: 'git add:*' }, policy: 'first' },
311
+ { policy: 'always' },
312
+ ];
313
+
314
+ const toolKey = 'bash/bash#abc123';
315
+ const args = { command: 'git add:.' };
316
+
317
+ // First time - requires intervention
318
+ expect(
319
+ InterventionChecker.shouldIntervene({
320
+ config,
321
+ toolArgs: args,
322
+ confirmedHistory: [],
323
+ toolKey,
324
+ }),
325
+ ).toBe('first');
326
+
327
+ // After confirmation - never
328
+ const confirmedHistory = [toolKey];
329
+ expect(
330
+ InterventionChecker.shouldIntervene({ config, toolArgs: args, confirmedHistory, toolKey }),
331
+ ).toBe('never');
332
+ });
333
+ });
334
+ });