@zds-ai/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +497 -0
- package/dist/agent/grok-agent.d.ts +250 -0
- package/dist/agent/grok-agent.js +2480 -0
- package/dist/agent/grok-agent.js.map +1 -0
- package/dist/agent/index.d.ts +14 -0
- package/dist/agent/index.js +136 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/commands/mcp.d.ts +2 -0
- package/dist/commands/mcp.js +239 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/grok/client.d.ts +55 -0
- package/dist/grok/client.js +276 -0
- package/dist/grok/client.js.map +1 -0
- package/dist/grok/tools.d.ts +8 -0
- package/dist/grok/tools.js +878 -0
- package/dist/grok/tools.js.map +1 -0
- package/dist/hooks/use-enhanced-input.d.ts +38 -0
- package/dist/hooks/use-enhanced-input.js +228 -0
- package/dist/hooks/use-enhanced-input.js.map +1 -0
- package/dist/hooks/use-input-handler.d.ts +36 -0
- package/dist/hooks/use-input-handler.js +1099 -0
- package/dist/hooks/use-input-handler.js.map +1 -0
- package/dist/hooks/use-input-history.d.ts +9 -0
- package/dist/hooks/use-input-history.js +61 -0
- package/dist/hooks/use-input-history.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +869 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/client.d.ts +41 -0
- package/dist/mcp/client.js +224 -0
- package/dist/mcp/client.js.map +1 -0
- package/dist/mcp/config.d.ts +13 -0
- package/dist/mcp/config.js +56 -0
- package/dist/mcp/config.js.map +1 -0
- package/dist/mcp/transports.d.ts +53 -0
- package/dist/mcp/transports.js +256 -0
- package/dist/mcp/transports.js.map +1 -0
- package/dist/tools/character-tool.d.ts +27 -0
- package/dist/tools/character-tool.js +194 -0
- package/dist/tools/character-tool.js.map +1 -0
- package/dist/tools/clear-cache-tool.d.ts +14 -0
- package/dist/tools/clear-cache-tool.js +82 -0
- package/dist/tools/clear-cache-tool.js.map +1 -0
- package/dist/tools/confirmation-tool.d.ts +16 -0
- package/dist/tools/confirmation-tool.js +72 -0
- package/dist/tools/confirmation-tool.js.map +1 -0
- package/dist/tools/env-tool.d.ts +17 -0
- package/dist/tools/env-tool.js +89 -0
- package/dist/tools/env-tool.js.map +1 -0
- package/dist/tools/file-conversion-tool.d.ts +16 -0
- package/dist/tools/file-conversion-tool.js +181 -0
- package/dist/tools/file-conversion-tool.js.map +1 -0
- package/dist/tools/image-tool.d.ts +22 -0
- package/dist/tools/image-tool.js +268 -0
- package/dist/tools/image-tool.js.map +1 -0
- package/dist/tools/index.d.ts +14 -0
- package/dist/tools/index.js +15 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/internet-tool.d.ts +11 -0
- package/dist/tools/internet-tool.js +108 -0
- package/dist/tools/internet-tool.js.map +1 -0
- package/dist/tools/introspect-tool.d.ts +11 -0
- package/dist/tools/introspect-tool.js +243 -0
- package/dist/tools/introspect-tool.js.map +1 -0
- package/dist/tools/morph-editor.d.ts +38 -0
- package/dist/tools/morph-editor.js +318 -0
- package/dist/tools/morph-editor.js.map +1 -0
- package/dist/tools/restart-tool.d.ts +7 -0
- package/dist/tools/restart-tool.js +24 -0
- package/dist/tools/restart-tool.js.map +1 -0
- package/dist/tools/search.d.ts +71 -0
- package/dist/tools/search.js +340 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/task-tool.d.ts +19 -0
- package/dist/tools/task-tool.js +115 -0
- package/dist/tools/task-tool.js.map +1 -0
- package/dist/tools/text-editor.d.ts +35 -0
- package/dist/tools/text-editor.js +669 -0
- package/dist/tools/text-editor.js.map +1 -0
- package/dist/tools/tool-discovery.d.ts +20 -0
- package/dist/tools/tool-discovery.js +45 -0
- package/dist/tools/tool-discovery.js.map +1 -0
- package/dist/tools/zsh.d.ts +13 -0
- package/dist/tools/zsh.js +168 -0
- package/dist/tools/zsh.js.map +1 -0
- package/dist/types/index.d.ts +31 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/ui/app.d.ts +7 -0
- package/dist/ui/app.js +99 -0
- package/dist/ui/app.js.map +1 -0
- package/dist/ui/components/active-task-status.d.ts +7 -0
- package/dist/ui/components/active-task-status.js +37 -0
- package/dist/ui/components/active-task-status.js.map +1 -0
- package/dist/ui/components/api-key-input.d.ts +7 -0
- package/dist/ui/components/api-key-input.js +80 -0
- package/dist/ui/components/api-key-input.js.map +1 -0
- package/dist/ui/components/backend-status.d.ts +7 -0
- package/dist/ui/components/backend-status.js +85 -0
- package/dist/ui/components/backend-status.js.map +1 -0
- package/dist/ui/components/chat-history.d.ts +8 -0
- package/dist/ui/components/chat-history.js +187 -0
- package/dist/ui/components/chat-history.js.map +1 -0
- package/dist/ui/components/chat-input.d.ts +9 -0
- package/dist/ui/components/chat-input.js +63 -0
- package/dist/ui/components/chat-input.js.map +1 -0
- package/dist/ui/components/chat-interface.d.ts +9 -0
- package/dist/ui/components/chat-interface.js +389 -0
- package/dist/ui/components/chat-interface.js.map +1 -0
- package/dist/ui/components/command-suggestions.d.ts +17 -0
- package/dist/ui/components/command-suggestions.js +22 -0
- package/dist/ui/components/command-suggestions.js.map +1 -0
- package/dist/ui/components/confirmation-dialog.d.ts +11 -0
- package/dist/ui/components/confirmation-dialog.js +105 -0
- package/dist/ui/components/confirmation-dialog.js.map +1 -0
- package/dist/ui/components/context-status.d.ts +7 -0
- package/dist/ui/components/context-status.js +36 -0
- package/dist/ui/components/context-status.js.map +1 -0
- package/dist/ui/components/diff-renderer.d.ts +13 -0
- package/dist/ui/components/diff-renderer.js +206 -0
- package/dist/ui/components/diff-renderer.js.map +1 -0
- package/dist/ui/components/loading-spinner.d.ts +8 -0
- package/dist/ui/components/loading-spinner.js +64 -0
- package/dist/ui/components/loading-spinner.js.map +1 -0
- package/dist/ui/components/mcp-status.d.ts +5 -0
- package/dist/ui/components/mcp-status.js +57 -0
- package/dist/ui/components/mcp-status.js.map +1 -0
- package/dist/ui/components/model-selection.d.ts +12 -0
- package/dist/ui/components/model-selection.js +17 -0
- package/dist/ui/components/model-selection.js.map +1 -0
- package/dist/ui/components/mood-status.d.ts +7 -0
- package/dist/ui/components/mood-status.js +34 -0
- package/dist/ui/components/mood-status.js.map +1 -0
- package/dist/ui/components/persona-status.d.ts +7 -0
- package/dist/ui/components/persona-status.js +34 -0
- package/dist/ui/components/persona-status.js.map +1 -0
- package/dist/ui/shared/max-sized-box.d.ts +8 -0
- package/dist/ui/shared/max-sized-box.js +6 -0
- package/dist/ui/shared/max-sized-box.js.map +1 -0
- package/dist/ui/utils/code-colorizer.d.ts +2 -0
- package/dist/ui/utils/code-colorizer.js +7 -0
- package/dist/ui/utils/code-colorizer.js.map +1 -0
- package/dist/ui/utils/colors.d.ts +14 -0
- package/dist/ui/utils/colors.js +15 -0
- package/dist/ui/utils/colors.js.map +1 -0
- package/dist/ui/utils/markdown-renderer.d.ts +4 -0
- package/dist/ui/utils/markdown-renderer.js +40 -0
- package/dist/ui/utils/markdown-renderer.js.map +1 -0
- package/dist/utils/auth-helper.d.ts +63 -0
- package/dist/utils/auth-helper.js +129 -0
- package/dist/utils/auth-helper.js.map +1 -0
- package/dist/utils/chat-history-manager-sqlite.d.ts +92 -0
- package/dist/utils/chat-history-manager-sqlite.js +334 -0
- package/dist/utils/chat-history-manager-sqlite.js.map +1 -0
- package/dist/utils/chat-history-manager.d.ts +87 -0
- package/dist/utils/chat-history-manager.js +273 -0
- package/dist/utils/chat-history-manager.js.map +1 -0
- package/dist/utils/chat-history-manager.json-backup.d.ts +69 -0
- package/dist/utils/chat-history-manager.json-backup.js +215 -0
- package/dist/utils/chat-history-manager.json-backup.js.map +1 -0
- package/dist/utils/confirmation-service.d.ts +46 -0
- package/dist/utils/confirmation-service.js +165 -0
- package/dist/utils/confirmation-service.js.map +1 -0
- package/dist/utils/custom-instructions.d.ts +1 -0
- package/dist/utils/custom-instructions.js +30 -0
- package/dist/utils/custom-instructions.js.map +1 -0
- package/dist/utils/database-connection.d.ts +27 -0
- package/dist/utils/database-connection.js +81 -0
- package/dist/utils/database-connection.js.map +1 -0
- package/dist/utils/database-schema.d.ts +17 -0
- package/dist/utils/database-schema.js +93 -0
- package/dist/utils/database-schema.js.map +1 -0
- package/dist/utils/error-logger.d.ts +13 -0
- package/dist/utils/error-logger.js +56 -0
- package/dist/utils/error-logger.js.map +1 -0
- package/dist/utils/hook-executor.d.ts +59 -0
- package/dist/utils/hook-executor.js +351 -0
- package/dist/utils/hook-executor.js.map +1 -0
- package/dist/utils/model-config.d.ts +28 -0
- package/dist/utils/model-config.js +42 -0
- package/dist/utils/model-config.js.map +1 -0
- package/dist/utils/path-utils.d.ts +4 -0
- package/dist/utils/path-utils.js +12 -0
- package/dist/utils/path-utils.js.map +1 -0
- package/dist/utils/settings-manager.d.ts +169 -0
- package/dist/utils/settings-manager.js +403 -0
- package/dist/utils/settings-manager.js.map +1 -0
- package/dist/utils/settings.d.ts +1 -0
- package/dist/utils/settings.js +4 -0
- package/dist/utils/settings.js.map +1 -0
- package/dist/utils/slash-commands.d.ts +25 -0
- package/dist/utils/slash-commands.js +454 -0
- package/dist/utils/slash-commands.js.map +1 -0
- package/dist/utils/startup-hook.d.ts +13 -0
- package/dist/utils/startup-hook.js +44 -0
- package/dist/utils/startup-hook.js.map +1 -0
- package/dist/utils/text-utils.d.ts +80 -0
- package/dist/utils/text-utils.js +182 -0
- package/dist/utils/text-utils.js.map +1 -0
- package/dist/utils/token-counter.d.ts +33 -0
- package/dist/utils/token-counter.js +78 -0
- package/dist/utils/token-counter.js.map +1 -0
- package/package.json +102 -0
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { ConfirmationService } from "../utils/confirmation-service.js";
|
|
4
|
+
import { expandHomeDir } from "../utils/path-utils.js";
|
|
5
|
+
import { getHandledToolNames } from "./tool-discovery.js";
|
|
6
|
+
export class TextEditorTool {
|
|
7
|
+
editHistory = [];
|
|
8
|
+
confirmationService = ConfirmationService.getInstance();
|
|
9
|
+
agent; // Reference to GrokAgent for context awareness
|
|
10
|
+
setAgent(agent) {
|
|
11
|
+
this.agent = agent;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Get the maximum characters allowed based on current context usage
|
|
15
|
+
* Returns null if viewFile should be disabled
|
|
16
|
+
*/
|
|
17
|
+
getMaxCharsAllowed() {
|
|
18
|
+
if (!this.agent) {
|
|
19
|
+
return 80000; // Default: ~20k tokens if no agent
|
|
20
|
+
}
|
|
21
|
+
const contextPercent = this.agent.getContextUsagePercent();
|
|
22
|
+
// Disable viewFile at 95%+
|
|
23
|
+
if (contextPercent >= 95) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
// 2k tokens max at 90%+
|
|
27
|
+
if (contextPercent >= 90) {
|
|
28
|
+
return 8000; // ~2k tokens
|
|
29
|
+
}
|
|
30
|
+
// 10k tokens max at 80%+
|
|
31
|
+
if (contextPercent >= 80) {
|
|
32
|
+
return 40000; // ~10k tokens
|
|
33
|
+
}
|
|
34
|
+
// 20k tokens max normally
|
|
35
|
+
return 80000; // ~20k tokens
|
|
36
|
+
}
|
|
37
|
+
async viewFile(filePath, viewRange) {
|
|
38
|
+
try {
|
|
39
|
+
// Check if viewFile is disabled due to high context usage
|
|
40
|
+
const maxChars = this.getMaxCharsAllowed();
|
|
41
|
+
if (maxChars === null) {
|
|
42
|
+
const contextPercent = this.agent?.getContextUsagePercent() || 0;
|
|
43
|
+
return {
|
|
44
|
+
success: false,
|
|
45
|
+
error: `viewFile is disabled at ${Math.round(contextPercent)}% context usage. Please save notes and clear cache first.`,
|
|
46
|
+
output: `viewFile is disabled at ${Math.round(contextPercent)}% context usage. Please save notes and clear cache first.`
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const expandedPath = expandHomeDir(filePath);
|
|
50
|
+
const resolvedPath = path.resolve(expandedPath);
|
|
51
|
+
if (await fs.pathExists(resolvedPath)) {
|
|
52
|
+
const stats = await fs.stat(resolvedPath);
|
|
53
|
+
if (stats.isDirectory()) {
|
|
54
|
+
const files = await fs.readdir(resolvedPath);
|
|
55
|
+
return {
|
|
56
|
+
success: true,
|
|
57
|
+
output: `Directory contents of ${filePath}:\n${files.join("\n")}`,
|
|
58
|
+
displayOutput: `Listed directory ${filePath} (${files.length} items)`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const content = await fs.readFile(resolvedPath, "utf-8");
|
|
62
|
+
const lines = content.split("\n");
|
|
63
|
+
if (viewRange) {
|
|
64
|
+
const [start, end] = viewRange;
|
|
65
|
+
const selectedLines = lines.slice(start - 1, end);
|
|
66
|
+
let numberedLines = selectedLines
|
|
67
|
+
.map((line, idx) => `${start + idx}: ${line}`)
|
|
68
|
+
.join("\n");
|
|
69
|
+
// Apply character limit even to ranges
|
|
70
|
+
if (numberedLines.length > maxChars) {
|
|
71
|
+
numberedLines = numberedLines.substring(0, maxChars);
|
|
72
|
+
const contextPercent = this.agent?.getContextUsagePercent() || 0;
|
|
73
|
+
return {
|
|
74
|
+
success: true,
|
|
75
|
+
output: `Lines ${start}-${end} of ${filePath} (truncated due to ${Math.round(contextPercent)}% context usage):\n${numberedLines}\n\n[Content truncated to ~${Math.round(maxChars / 4000)}k tokens. Use smaller ranges.]`,
|
|
76
|
+
displayOutput: `Read ${filePath} (lines ${start}-${end}, truncated)`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
success: true,
|
|
81
|
+
output: `Lines ${start}-${end} of ${filePath}:\n${numberedLines}`,
|
|
82
|
+
displayOutput: `Read ${filePath} (lines ${start}-${end})`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// Full file view - apply limits
|
|
86
|
+
const totalLines = lines.length;
|
|
87
|
+
let numberedLines = lines
|
|
88
|
+
.map((line, idx) => `${idx + 1}: ${line}`)
|
|
89
|
+
.join("\n");
|
|
90
|
+
let truncated = false;
|
|
91
|
+
if (numberedLines.length > maxChars) {
|
|
92
|
+
numberedLines = numberedLines.substring(0, maxChars);
|
|
93
|
+
truncated = true;
|
|
94
|
+
}
|
|
95
|
+
const contextPercent = this.agent?.getContextUsagePercent() || 0;
|
|
96
|
+
const truncationMessage = truncated
|
|
97
|
+
? `\n\n[Content truncated to ~${Math.round(maxChars / 4000)}k tokens due to ${Math.round(contextPercent)}% context usage. Use viewFile with viewRange parameter for specific sections.]`
|
|
98
|
+
: "";
|
|
99
|
+
return {
|
|
100
|
+
success: true,
|
|
101
|
+
output: `Contents of ${filePath}:\n${numberedLines}${truncationMessage}`,
|
|
102
|
+
displayOutput: truncated
|
|
103
|
+
? `Read ${filePath} (${totalLines} lines, truncated due to context limits)`
|
|
104
|
+
: `Read ${filePath} (${totalLines} lines)`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
return {
|
|
109
|
+
success: false,
|
|
110
|
+
error: `File or directory not found: ${filePath}`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
return {
|
|
116
|
+
success: false,
|
|
117
|
+
error: `Error viewing ${filePath}: ${error.message}`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async strReplace(filePath, oldStr, newStr, replaceAll = false) {
|
|
122
|
+
try {
|
|
123
|
+
const expandedPath = expandHomeDir(filePath);
|
|
124
|
+
const resolvedPath = path.resolve(expandedPath);
|
|
125
|
+
if (!(await fs.pathExists(resolvedPath))) {
|
|
126
|
+
return {
|
|
127
|
+
success: false,
|
|
128
|
+
error: `File not found: ${filePath}`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const content = await fs.readFile(resolvedPath, "utf-8");
|
|
132
|
+
if (!content.includes(oldStr)) {
|
|
133
|
+
if (oldStr.includes('\n')) {
|
|
134
|
+
const fuzzyResult = this.findFuzzyMatch(content, oldStr);
|
|
135
|
+
if (fuzzyResult) {
|
|
136
|
+
oldStr = fuzzyResult;
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
return {
|
|
140
|
+
success: false,
|
|
141
|
+
error: `String not found in file. For multi-line replacements, consider using line-based editing.`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
return {
|
|
147
|
+
success: false,
|
|
148
|
+
error: `String not found in file: "${oldStr}"`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const occurrences = (content.match(new RegExp(oldStr.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length;
|
|
153
|
+
// If tool approval hook is configured, skip ConfirmationService (hook handles authorization)
|
|
154
|
+
const { getSettingsManager } = await import('../utils/settings-manager.js');
|
|
155
|
+
const settings = getSettingsManager();
|
|
156
|
+
const hasToolApprovalHook = !!settings.getToolApprovalHook();
|
|
157
|
+
const sessionFlags = this.confirmationService.getSessionFlags();
|
|
158
|
+
if (!hasToolApprovalHook && !sessionFlags.fileOperations && !sessionFlags.allOperations) {
|
|
159
|
+
const previewContent = replaceAll
|
|
160
|
+
? content.split(oldStr).join(newStr)
|
|
161
|
+
: content.replace(oldStr, newStr);
|
|
162
|
+
const oldLines = content.split("\n");
|
|
163
|
+
const newLines = previewContent.split("\n");
|
|
164
|
+
const diffContent = this.generateDiff(oldLines, newLines, filePath);
|
|
165
|
+
const confirmationResult = await this.confirmationService.requestConfirmation({
|
|
166
|
+
operation: `Edit file${replaceAll && occurrences > 1 ? ` (${occurrences} occurrences)` : ''}`,
|
|
167
|
+
filename: filePath,
|
|
168
|
+
showVSCodeOpen: false,
|
|
169
|
+
content: diffContent,
|
|
170
|
+
}, "file");
|
|
171
|
+
if (!confirmationResult.confirmed) {
|
|
172
|
+
return {
|
|
173
|
+
success: false,
|
|
174
|
+
error: confirmationResult.feedback
|
|
175
|
+
? `File edit canceled by user: ${confirmationResult.feedback}`
|
|
176
|
+
: "File edit canceled by user",
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const newContent = replaceAll
|
|
181
|
+
? content.split(oldStr).join(newStr)
|
|
182
|
+
: content.replace(oldStr, newStr);
|
|
183
|
+
await fs.writeFile(resolvedPath, newContent, "utf-8");
|
|
184
|
+
this.editHistory.push({
|
|
185
|
+
command: "str_replace",
|
|
186
|
+
path: filePath,
|
|
187
|
+
old_str: oldStr,
|
|
188
|
+
new_str: newStr,
|
|
189
|
+
});
|
|
190
|
+
const oldLines = content.split("\n");
|
|
191
|
+
const newLines = newContent.split("\n");
|
|
192
|
+
const diff = this.generateDiff(oldLines, newLines, filePath);
|
|
193
|
+
// Extract filename from path for display
|
|
194
|
+
const filename = path.basename(filePath);
|
|
195
|
+
const changeCount = replaceAll && occurrences > 1 ? occurrences : 1;
|
|
196
|
+
return {
|
|
197
|
+
success: true,
|
|
198
|
+
output: diff,
|
|
199
|
+
displayOutput: `Updated ${filename} (${changeCount} ${changeCount === 1 ? 'change' : 'changes'})`,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
return {
|
|
204
|
+
success: false,
|
|
205
|
+
error: `Error replacing text in ${filePath}: ${error.message}`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async createNewFile(filePath, content) {
|
|
210
|
+
try {
|
|
211
|
+
// Validate required parameters
|
|
212
|
+
if (content === undefined || content === null) {
|
|
213
|
+
return {
|
|
214
|
+
success: false,
|
|
215
|
+
error: "Content parameter is required for createNewFile",
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const expandedPath = expandHomeDir(filePath);
|
|
219
|
+
const resolvedPath = path.resolve(expandedPath);
|
|
220
|
+
// If tool approval hook is configured, skip ConfirmationService (hook handles authorization)
|
|
221
|
+
const { getSettingsManager } = await import('../utils/settings-manager.js');
|
|
222
|
+
const settings = getSettingsManager();
|
|
223
|
+
const hasToolApprovalHook = !!settings.getToolApprovalHook();
|
|
224
|
+
// Check if user has already accepted file operations for this session
|
|
225
|
+
const sessionFlags = this.confirmationService.getSessionFlags();
|
|
226
|
+
if (!hasToolApprovalHook && !sessionFlags.fileOperations && !sessionFlags.allOperations) {
|
|
227
|
+
// Create a diff-style preview for file creation
|
|
228
|
+
const contentLines = content.split("\n");
|
|
229
|
+
const diffContent = [
|
|
230
|
+
`Created ${filePath}`,
|
|
231
|
+
`--- /dev/null`,
|
|
232
|
+
`+++ b/${filePath}`,
|
|
233
|
+
`@@ -0,0 +1,${contentLines.length} @@`,
|
|
234
|
+
...contentLines.map((line) => `+${line}`),
|
|
235
|
+
].join("\n");
|
|
236
|
+
const confirmationResult = await this.confirmationService.requestConfirmation({
|
|
237
|
+
operation: "Write",
|
|
238
|
+
filename: filePath,
|
|
239
|
+
showVSCodeOpen: false,
|
|
240
|
+
content: diffContent,
|
|
241
|
+
}, "file");
|
|
242
|
+
if (!confirmationResult.confirmed) {
|
|
243
|
+
return {
|
|
244
|
+
success: false,
|
|
245
|
+
error: confirmationResult.feedback
|
|
246
|
+
? `File creation canceled by user: ${confirmationResult.feedback}`
|
|
247
|
+
: "File creation canceled by user",
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const dir = path.dirname(resolvedPath);
|
|
252
|
+
await fs.ensureDir(dir);
|
|
253
|
+
await fs.writeFile(resolvedPath, content, "utf-8");
|
|
254
|
+
this.editHistory.push({
|
|
255
|
+
command: "create",
|
|
256
|
+
path: filePath,
|
|
257
|
+
content,
|
|
258
|
+
});
|
|
259
|
+
// Generate diff output using the same method as str_replace
|
|
260
|
+
const oldLines = []; // Empty for new files
|
|
261
|
+
const newLines = content.split("\n");
|
|
262
|
+
const diff = this.generateDiff(oldLines, newLines, filePath);
|
|
263
|
+
// Extract filename from path for display
|
|
264
|
+
const filename = path.basename(filePath);
|
|
265
|
+
const lineCount = newLines.length;
|
|
266
|
+
return {
|
|
267
|
+
success: true,
|
|
268
|
+
output: diff,
|
|
269
|
+
displayOutput: `Created ${filename} (${lineCount} ${lineCount === 1 ? 'line' : 'lines'})`,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
return {
|
|
274
|
+
success: false,
|
|
275
|
+
error: `Error creating ${filePath}: ${error.message}`,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async replaceLines(filePath, startLine, endLine, newContent) {
|
|
280
|
+
try {
|
|
281
|
+
const resolvedPath = path.resolve(filePath);
|
|
282
|
+
if (!(await fs.pathExists(resolvedPath))) {
|
|
283
|
+
return {
|
|
284
|
+
success: false,
|
|
285
|
+
error: `File not found: ${filePath}`,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
const fileContent = await fs.readFile(resolvedPath, "utf-8");
|
|
289
|
+
const lines = fileContent.split("\n");
|
|
290
|
+
if (startLine < 1 || startLine > lines.length) {
|
|
291
|
+
return {
|
|
292
|
+
success: false,
|
|
293
|
+
error: `Invalid start line: ${startLine}. File has ${lines.length} lines.`,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
if (endLine < startLine || endLine > lines.length) {
|
|
297
|
+
return {
|
|
298
|
+
success: false,
|
|
299
|
+
error: `Invalid end line: ${endLine}. Must be between ${startLine} and ${lines.length}.`,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
// If tool approval hook is configured, skip ConfirmationService (hook handles authorization)
|
|
303
|
+
const { getSettingsManager } = await import('../utils/settings-manager.js');
|
|
304
|
+
const settings = getSettingsManager();
|
|
305
|
+
const hasToolApprovalHook = !!settings.getToolApprovalHook();
|
|
306
|
+
const sessionFlags = this.confirmationService.getSessionFlags();
|
|
307
|
+
if (!hasToolApprovalHook && !sessionFlags.fileOperations && !sessionFlags.allOperations) {
|
|
308
|
+
const newLines = [...lines];
|
|
309
|
+
const replacementLines = newContent.split("\n");
|
|
310
|
+
newLines.splice(startLine - 1, endLine - startLine + 1, ...replacementLines);
|
|
311
|
+
const diffContent = this.generateDiff(lines, newLines, filePath);
|
|
312
|
+
const confirmationResult = await this.confirmationService.requestConfirmation({
|
|
313
|
+
operation: `Replace lines ${startLine}-${endLine}`,
|
|
314
|
+
filename: filePath,
|
|
315
|
+
showVSCodeOpen: false,
|
|
316
|
+
content: diffContent,
|
|
317
|
+
}, "file");
|
|
318
|
+
if (!confirmationResult.confirmed) {
|
|
319
|
+
return {
|
|
320
|
+
success: false,
|
|
321
|
+
error: confirmationResult.feedback
|
|
322
|
+
? `Line replacement canceled by user: ${confirmationResult.feedback}`
|
|
323
|
+
: "Line replacement canceled by user",
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const replacementLines = newContent.split("\n");
|
|
328
|
+
lines.splice(startLine - 1, endLine - startLine + 1, ...replacementLines);
|
|
329
|
+
const newFileContent = lines.join("\n");
|
|
330
|
+
await fs.writeFile(resolvedPath, newFileContent, "utf-8");
|
|
331
|
+
this.editHistory.push({
|
|
332
|
+
command: "str_replace",
|
|
333
|
+
path: filePath,
|
|
334
|
+
old_str: `lines ${startLine}-${endLine}`,
|
|
335
|
+
new_str: newContent,
|
|
336
|
+
});
|
|
337
|
+
const oldLines = fileContent.split("\n");
|
|
338
|
+
const diff = this.generateDiff(oldLines, lines, filePath);
|
|
339
|
+
// Extract filename from path for display
|
|
340
|
+
const filename = path.basename(filePath);
|
|
341
|
+
const lineRange = startLine === endLine ? `line ${startLine}` : `lines ${startLine}-${endLine}`;
|
|
342
|
+
return {
|
|
343
|
+
success: true,
|
|
344
|
+
output: diff,
|
|
345
|
+
displayOutput: `Replaced ${lineRange} in ${filename}`,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
return {
|
|
350
|
+
success: false,
|
|
351
|
+
error: `Error replacing lines in ${filePath}: ${error.message}`,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
async insertLines(filePath, insertLine, content) {
|
|
356
|
+
try {
|
|
357
|
+
const resolvedPath = path.resolve(filePath);
|
|
358
|
+
if (!(await fs.pathExists(resolvedPath))) {
|
|
359
|
+
return {
|
|
360
|
+
success: false,
|
|
361
|
+
error: `File not found: ${filePath}`,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
const fileContent = await fs.readFile(resolvedPath, "utf-8");
|
|
365
|
+
const lines = fileContent.split("\n");
|
|
366
|
+
lines.splice(insertLine - 1, 0, content);
|
|
367
|
+
const newContent = lines.join("\n");
|
|
368
|
+
await fs.writeFile(resolvedPath, newContent, "utf-8");
|
|
369
|
+
this.editHistory.push({
|
|
370
|
+
command: "insert",
|
|
371
|
+
path: filePath,
|
|
372
|
+
insert_line: insertLine,
|
|
373
|
+
content,
|
|
374
|
+
});
|
|
375
|
+
const filename = path.basename(filePath);
|
|
376
|
+
return {
|
|
377
|
+
success: true,
|
|
378
|
+
output: `Successfully inserted content at line ${insertLine} in ${filePath}`,
|
|
379
|
+
displayOutput: `Inserted at line ${insertLine} in ${filename}`,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
return {
|
|
384
|
+
success: false,
|
|
385
|
+
error: `Error inserting content in ${filePath}: ${error.message}`,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
async undoEdit() {
|
|
390
|
+
if (this.editHistory.length === 0) {
|
|
391
|
+
return {
|
|
392
|
+
success: false,
|
|
393
|
+
error: "No edits to undo",
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
const lastEdit = this.editHistory.pop();
|
|
397
|
+
try {
|
|
398
|
+
switch (lastEdit.command) {
|
|
399
|
+
case "str_replace":
|
|
400
|
+
if (lastEdit.path && lastEdit.old_str && lastEdit.new_str) {
|
|
401
|
+
const content = await fs.readFile(lastEdit.path, "utf-8");
|
|
402
|
+
const revertedContent = content.replace(lastEdit.new_str, lastEdit.old_str);
|
|
403
|
+
await fs.writeFile(lastEdit.path, revertedContent, "utf-8");
|
|
404
|
+
}
|
|
405
|
+
break;
|
|
406
|
+
case "create":
|
|
407
|
+
if (lastEdit.path) {
|
|
408
|
+
await fs.remove(lastEdit.path);
|
|
409
|
+
}
|
|
410
|
+
break;
|
|
411
|
+
case "insert":
|
|
412
|
+
if (lastEdit.path && lastEdit.insert_line) {
|
|
413
|
+
const content = await fs.readFile(lastEdit.path, "utf-8");
|
|
414
|
+
const lines = content.split("\n");
|
|
415
|
+
lines.splice(lastEdit.insert_line - 1, 1);
|
|
416
|
+
await fs.writeFile(lastEdit.path, lines.join("\n"), "utf-8");
|
|
417
|
+
}
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
success: true,
|
|
422
|
+
output: `Successfully undid ${lastEdit.command} operation`,
|
|
423
|
+
displayOutput: `Undid ${lastEdit.command} operation`,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
return {
|
|
428
|
+
success: false,
|
|
429
|
+
error: `Error undoing edit: ${error.message}`,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
findFuzzyMatch(content, searchStr) {
|
|
434
|
+
const functionMatch = searchStr.match(/function\s+(\w+)/);
|
|
435
|
+
if (!functionMatch)
|
|
436
|
+
return null;
|
|
437
|
+
const functionName = functionMatch[1];
|
|
438
|
+
const contentLines = content.split('\n');
|
|
439
|
+
let functionStart = -1;
|
|
440
|
+
for (let i = 0; i < contentLines.length; i++) {
|
|
441
|
+
if (contentLines[i].includes(`function ${functionName}`) && contentLines[i].includes('{')) {
|
|
442
|
+
functionStart = i;
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (functionStart === -1)
|
|
447
|
+
return null;
|
|
448
|
+
let braceCount = 0;
|
|
449
|
+
let functionEnd = functionStart;
|
|
450
|
+
for (let i = functionStart; i < contentLines.length; i++) {
|
|
451
|
+
const line = contentLines[i];
|
|
452
|
+
for (const char of line) {
|
|
453
|
+
if (char === '{')
|
|
454
|
+
braceCount++;
|
|
455
|
+
if (char === '}')
|
|
456
|
+
braceCount--;
|
|
457
|
+
}
|
|
458
|
+
if (braceCount === 0 && i > functionStart) {
|
|
459
|
+
functionEnd = i;
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const actualFunction = contentLines.slice(functionStart, functionEnd + 1).join('\n');
|
|
464
|
+
const searchNormalized = this.normalizeForComparison(searchStr);
|
|
465
|
+
const actualNormalized = this.normalizeForComparison(actualFunction);
|
|
466
|
+
if (this.isSimilarStructure(searchNormalized, actualNormalized)) {
|
|
467
|
+
return actualFunction;
|
|
468
|
+
}
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
normalizeForComparison(str) {
|
|
472
|
+
return str
|
|
473
|
+
.replace(/["'`]/g, '"')
|
|
474
|
+
.replace(/\s+/g, ' ')
|
|
475
|
+
.replace(/{\s+/g, '{ ')
|
|
476
|
+
.replace(/\s+}/g, ' }')
|
|
477
|
+
.replace(/;\s*/g, ';')
|
|
478
|
+
.trim();
|
|
479
|
+
}
|
|
480
|
+
isSimilarStructure(search, actual) {
|
|
481
|
+
const extractTokens = (str) => {
|
|
482
|
+
const tokens = str.match(/\b(function|console\.log|return|if|else|for|while)\b/g) || [];
|
|
483
|
+
return tokens;
|
|
484
|
+
};
|
|
485
|
+
const searchTokens = extractTokens(search);
|
|
486
|
+
const actualTokens = extractTokens(actual);
|
|
487
|
+
if (searchTokens.length !== actualTokens.length)
|
|
488
|
+
return false;
|
|
489
|
+
for (let i = 0; i < searchTokens.length; i++) {
|
|
490
|
+
if (searchTokens[i] !== actualTokens[i])
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Compute Longest Common Subsequence using dynamic programming
|
|
497
|
+
* Returns array of indices in oldLines that are part of LCS
|
|
498
|
+
*/
|
|
499
|
+
computeLCS(oldLines, newLines) {
|
|
500
|
+
const m = oldLines.length;
|
|
501
|
+
const n = newLines.length;
|
|
502
|
+
const dp = Array(m + 1).fill(0).map(() => Array(n + 1).fill(0));
|
|
503
|
+
// Build LCS length table
|
|
504
|
+
for (let i = 1; i <= m; i++) {
|
|
505
|
+
for (let j = 1; j <= n; j++) {
|
|
506
|
+
if (oldLines[i - 1] === newLines[j - 1]) {
|
|
507
|
+
dp[i][j] = dp[i - 1][j - 1] + 1;
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return dp;
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Extract changes from LCS table
|
|
518
|
+
* Returns array of change regions
|
|
519
|
+
*/
|
|
520
|
+
extractChanges(oldLines, newLines, lcs) {
|
|
521
|
+
const changes = [];
|
|
522
|
+
let i = oldLines.length;
|
|
523
|
+
let j = newLines.length;
|
|
524
|
+
let oldEnd = i;
|
|
525
|
+
let newEnd = j;
|
|
526
|
+
let inChange = false;
|
|
527
|
+
while (i > 0 || j > 0) {
|
|
528
|
+
if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
|
|
529
|
+
// Lines match - if we were in a change, close it
|
|
530
|
+
if (inChange) {
|
|
531
|
+
changes.unshift({
|
|
532
|
+
oldStart: i,
|
|
533
|
+
oldEnd: oldEnd,
|
|
534
|
+
newStart: j,
|
|
535
|
+
newEnd: newEnd
|
|
536
|
+
});
|
|
537
|
+
inChange = false;
|
|
538
|
+
}
|
|
539
|
+
i--;
|
|
540
|
+
j--;
|
|
541
|
+
}
|
|
542
|
+
else if (j > 0 && (i === 0 || lcs[i][j - 1] >= lcs[i - 1][j])) {
|
|
543
|
+
// Insertion in new file
|
|
544
|
+
if (!inChange) {
|
|
545
|
+
oldEnd = i;
|
|
546
|
+
newEnd = j;
|
|
547
|
+
inChange = true;
|
|
548
|
+
}
|
|
549
|
+
j--;
|
|
550
|
+
}
|
|
551
|
+
else if (i > 0) {
|
|
552
|
+
// Deletion from old file
|
|
553
|
+
if (!inChange) {
|
|
554
|
+
oldEnd = i;
|
|
555
|
+
newEnd = j;
|
|
556
|
+
inChange = true;
|
|
557
|
+
}
|
|
558
|
+
i--;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
// Close any remaining change
|
|
562
|
+
if (inChange) {
|
|
563
|
+
changes.unshift({
|
|
564
|
+
oldStart: 0,
|
|
565
|
+
oldEnd: oldEnd,
|
|
566
|
+
newStart: 0,
|
|
567
|
+
newEnd: newEnd
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
return changes;
|
|
571
|
+
}
|
|
572
|
+
generateDiff(oldLines, newLines, filePath) {
|
|
573
|
+
const CONTEXT_LINES = 3;
|
|
574
|
+
// Use LCS-based diff algorithm to find actual changes
|
|
575
|
+
const lcs = this.computeLCS(oldLines, newLines);
|
|
576
|
+
const changes = this.extractChanges(oldLines, newLines, lcs);
|
|
577
|
+
const hunks = [];
|
|
578
|
+
let accumulatedOffset = 0;
|
|
579
|
+
for (let changeIdx = 0; changeIdx < changes.length; changeIdx++) {
|
|
580
|
+
const change = changes[changeIdx];
|
|
581
|
+
let contextStart = Math.max(0, change.oldStart - CONTEXT_LINES);
|
|
582
|
+
let contextEnd = Math.min(oldLines.length, change.oldEnd + CONTEXT_LINES);
|
|
583
|
+
if (hunks.length > 0) {
|
|
584
|
+
const lastHunk = hunks[hunks.length - 1];
|
|
585
|
+
const lastHunkEnd = lastHunk.oldStart + lastHunk.oldCount;
|
|
586
|
+
if (lastHunkEnd >= contextStart) {
|
|
587
|
+
const oldHunkEnd = lastHunk.oldStart + lastHunk.oldCount;
|
|
588
|
+
const newContextEnd = Math.min(oldLines.length, change.oldEnd + CONTEXT_LINES);
|
|
589
|
+
for (let idx = oldHunkEnd; idx < change.oldStart; idx++) {
|
|
590
|
+
lastHunk.lines.push({ type: ' ', content: oldLines[idx] });
|
|
591
|
+
}
|
|
592
|
+
for (let idx = change.oldStart; idx < change.oldEnd; idx++) {
|
|
593
|
+
lastHunk.lines.push({ type: '-', content: oldLines[idx] });
|
|
594
|
+
}
|
|
595
|
+
for (let idx = change.newStart; idx < change.newEnd; idx++) {
|
|
596
|
+
lastHunk.lines.push({ type: '+', content: newLines[idx] });
|
|
597
|
+
}
|
|
598
|
+
for (let idx = change.oldEnd; idx < newContextEnd && idx < oldLines.length; idx++) {
|
|
599
|
+
lastHunk.lines.push({ type: ' ', content: oldLines[idx] });
|
|
600
|
+
}
|
|
601
|
+
lastHunk.oldCount = newContextEnd - lastHunk.oldStart;
|
|
602
|
+
lastHunk.newCount = lastHunk.oldCount + (change.newEnd - change.newStart) - (change.oldEnd - change.oldStart);
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
const hunk = {
|
|
607
|
+
oldStart: contextStart + 1,
|
|
608
|
+
oldCount: contextEnd - contextStart,
|
|
609
|
+
newStart: contextStart + 1 + accumulatedOffset,
|
|
610
|
+
newCount: contextEnd - contextStart + (change.newEnd - change.newStart) - (change.oldEnd - change.oldStart),
|
|
611
|
+
lines: []
|
|
612
|
+
};
|
|
613
|
+
for (let idx = contextStart; idx < change.oldStart; idx++) {
|
|
614
|
+
hunk.lines.push({ type: ' ', content: oldLines[idx] });
|
|
615
|
+
}
|
|
616
|
+
for (let idx = change.oldStart; idx < change.oldEnd; idx++) {
|
|
617
|
+
hunk.lines.push({ type: '-', content: oldLines[idx] });
|
|
618
|
+
}
|
|
619
|
+
for (let idx = change.newStart; idx < change.newEnd; idx++) {
|
|
620
|
+
hunk.lines.push({ type: '+', content: newLines[idx] });
|
|
621
|
+
}
|
|
622
|
+
for (let idx = change.oldEnd; idx < contextEnd && idx < oldLines.length; idx++) {
|
|
623
|
+
hunk.lines.push({ type: ' ', content: oldLines[idx] });
|
|
624
|
+
}
|
|
625
|
+
hunks.push(hunk);
|
|
626
|
+
accumulatedOffset += (change.newEnd - change.newStart) - (change.oldEnd - change.oldStart);
|
|
627
|
+
}
|
|
628
|
+
let addedLines = 0;
|
|
629
|
+
let removedLines = 0;
|
|
630
|
+
for (const hunk of hunks) {
|
|
631
|
+
for (const line of hunk.lines) {
|
|
632
|
+
if (line.type === '+')
|
|
633
|
+
addedLines++;
|
|
634
|
+
if (line.type === '-')
|
|
635
|
+
removedLines++;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
let summary = `Updated ${filePath}`;
|
|
639
|
+
if (addedLines > 0 && removedLines > 0) {
|
|
640
|
+
summary += ` with ${addedLines} addition${addedLines !== 1 ? "s" : ""} and ${removedLines} removal${removedLines !== 1 ? "s" : ""}`;
|
|
641
|
+
}
|
|
642
|
+
else if (addedLines > 0) {
|
|
643
|
+
summary += ` with ${addedLines} addition${addedLines !== 1 ? "s" : ""}`;
|
|
644
|
+
}
|
|
645
|
+
else if (removedLines > 0) {
|
|
646
|
+
summary += ` with ${removedLines} removal${removedLines !== 1 ? "s" : ""}`;
|
|
647
|
+
}
|
|
648
|
+
else if (changes.length === 0) {
|
|
649
|
+
return `No changes in ${filePath}`;
|
|
650
|
+
}
|
|
651
|
+
let diff = summary + "\n";
|
|
652
|
+
diff += `--- a/${filePath}\n`;
|
|
653
|
+
diff += `+++ b/${filePath}\n`;
|
|
654
|
+
for (const hunk of hunks) {
|
|
655
|
+
diff += `@@ -${hunk.oldStart},${hunk.oldCount} +${hunk.newStart},${hunk.newCount} @@\n`;
|
|
656
|
+
for (const line of hunk.lines) {
|
|
657
|
+
diff += `${line.type}${line.content}\n`;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return diff.trim();
|
|
661
|
+
}
|
|
662
|
+
getEditHistory() {
|
|
663
|
+
return [...this.editHistory];
|
|
664
|
+
}
|
|
665
|
+
getHandledToolNames() {
|
|
666
|
+
return getHandledToolNames(this);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
//# sourceMappingURL=text-editor.js.map
|