neoagent 2.5.2-beta.9 → 3.0.1-beta.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/README.md +5 -1
- package/docs/integrations.md +11 -7
- package/flutter_app/lib/main_controller.dart +34 -0
- package/flutter_app/lib/main_security.dart +19 -0
- package/lib/schema_migrations.js +224 -0
- package/package.json +2 -2
- package/server/admin/admin.css +29 -0
- package/server/admin/admin.js +81 -1
- package/server/admin/index.html +97 -9
- package/server/db/database.js +57 -1
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +32788 -32752
- package/server/routes/security.js +19 -0
- package/server/routes/settings.js +1 -0
- package/server/routes/skills.js +9 -5
- package/server/services/ai/deliverables/artifact_helpers.js +92 -9
- package/server/services/ai/deliverables/selector.js +17 -0
- package/server/services/ai/hooks.js +3 -2
- package/server/services/ai/learning.js +8 -3
- package/server/services/ai/loop/agent_engine_core.js +67 -0
- package/server/services/ai/loop/blank_recovery.js +37 -0
- package/server/services/ai/loop/conversation_loop.js +391 -252
- package/server/services/ai/loop/error_recovery.js +38 -0
- package/server/services/ai/loop/messaging_delivery.js +52 -6
- package/server/services/ai/loop/progress_classification.js +178 -0
- package/server/services/ai/loop/progress_monitor.js +6 -5
- package/server/services/ai/loop/tool_dispatch.js +1 -0
- package/server/services/ai/loopPolicy.js +15 -6
- package/server/services/ai/messagingFallback.js +23 -1
- package/server/services/ai/preModelCompaction.js +31 -0
- package/server/services/ai/repetitionGuard.js +47 -2
- package/server/services/ai/settings.js +8 -0
- package/server/services/ai/systemPrompt.js +24 -0
- package/server/services/ai/taskAnalysis.js +10 -0
- package/server/services/ai/toolEvidence.js +39 -3
- package/server/services/ai/toolResult.js +29 -0
- package/server/services/ai/toolRunner.js +262 -55
- package/server/services/ai/toolSelector.js +20 -3
- package/server/services/ai/tools.js +201 -31
- package/server/services/cli/shell_worker_pool.js +87 -5
- package/server/services/integrations/github/common.js +2 -2
- package/server/services/integrations/github/repos.js +123 -55
- package/server/services/manager.js +16 -0
- package/server/services/memory/manager.js +39 -24
- package/server/services/runtime/docker-vm-manager.js +21 -1
- package/server/services/runtime/manager.js +6 -2
- package/server/services/security/approval_gate_service.js +108 -5
- package/server/services/security/tool_categories.js +113 -4
- package/server/services/security/tool_security_hook.js +7 -2
- package/server/services/skills/runtime.js +2 -0
- package/server/services/workspace/manager.js +56 -0
|
@@ -5,28 +5,120 @@ const db = require('../../db/database');
|
|
|
5
5
|
const { getCategoryForTool } = require('./tool_categories');
|
|
6
6
|
|
|
7
7
|
const APPROVAL_TIMEOUT_MS = 30_000;
|
|
8
|
+
const SESSION_GRANT_TTL_MS = 12 * 60 * 60 * 1000;
|
|
8
9
|
|
|
9
10
|
class ApprovalGateService {
|
|
10
11
|
constructor({ io }) {
|
|
11
12
|
this._io = io;
|
|
12
|
-
/** @type {Map<string, { resolve: Function, timer: NodeJS.Timeout }>} */
|
|
13
|
+
/** @type {Map<string, { resolve: Function, timer: NodeJS.Timeout, userId: number|string, runId: string|null, toolName: string, toolArgs: object }>} */
|
|
13
14
|
this._pending = new Map();
|
|
14
15
|
/** @type {Set<string>} key = `${userId}:${runId}:${toolName}` */
|
|
15
16
|
this._sessionGrants = new Set();
|
|
17
|
+
this._loadSessionGrants();
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
hasSessionGrant(userId, runId, toolName) {
|
|
19
21
|
return this._sessionGrants.has(`${userId}:${runId}:${toolName}`);
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
_grantKey(userId, runId, toolName) {
|
|
25
|
+
return `${userId}:${runId}:${toolName}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
_loadSessionGrants() {
|
|
29
|
+
try {
|
|
30
|
+
db.prepare(
|
|
31
|
+
`DELETE FROM approval_session_grants
|
|
32
|
+
WHERE expires_at <= datetime('now')`
|
|
33
|
+
).run();
|
|
34
|
+
const rows = db.prepare(
|
|
35
|
+
`SELECT user_id, run_id, tool_name
|
|
36
|
+
FROM approval_session_grants
|
|
37
|
+
WHERE expires_at > datetime('now')`
|
|
38
|
+
).all();
|
|
39
|
+
for (const row of rows) {
|
|
40
|
+
this._sessionGrants.add(this._grantKey(row.user_id, row.run_id, row.tool_name));
|
|
41
|
+
}
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.warn('[ApprovalGate] Failed to load session grants:', err.message);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_persistSessionGrant(userId, runId, toolName) {
|
|
48
|
+
const expiresAt = new Date(Date.now() + SESSION_GRANT_TTL_MS).toISOString();
|
|
49
|
+
db.prepare(
|
|
50
|
+
`INSERT INTO approval_session_grants (
|
|
51
|
+
user_id, run_id, tool_name, expires_at, updated_at
|
|
52
|
+
) VALUES (?, ?, ?, ?, datetime('now'))
|
|
53
|
+
ON CONFLICT(user_id, run_id, tool_name) DO UPDATE SET
|
|
54
|
+
expires_at = excluded.expires_at,
|
|
55
|
+
updated_at = excluded.updated_at`
|
|
56
|
+
).run(userId, runId, toolName, expiresAt);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
_persistPendingApproval(approvalId, userId, runId, toolName, toolArgs, category, expiresAt) {
|
|
60
|
+
db.prepare(
|
|
61
|
+
`INSERT INTO pending_approvals (
|
|
62
|
+
id, user_id, run_id, tool_name, tool_args_json, category, status, expires_at, updated_at
|
|
63
|
+
) VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, datetime('now'))`
|
|
64
|
+
).run(
|
|
65
|
+
approvalId,
|
|
66
|
+
userId,
|
|
67
|
+
runId,
|
|
68
|
+
toolName,
|
|
69
|
+
JSON.stringify(toolArgs ?? {}),
|
|
70
|
+
category,
|
|
71
|
+
expiresAt,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
_updatePendingApprovalStatus(approvalId, status, scope = null) {
|
|
76
|
+
db.prepare(
|
|
77
|
+
`UPDATE pending_approvals
|
|
78
|
+
SET status = ?,
|
|
79
|
+
scope = COALESCE(?, scope),
|
|
80
|
+
decided_at = CASE WHEN ? = 'pending' THEN decided_at ELSE COALESCE(decided_at, datetime('now')) END,
|
|
81
|
+
updated_at = datetime('now')
|
|
82
|
+
WHERE id = ?`
|
|
83
|
+
).run(status, scope, status, approvalId);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getStoredApproval(approvalId, userId) {
|
|
87
|
+
try {
|
|
88
|
+
return db.prepare(
|
|
89
|
+
`SELECT id, run_id, tool_name, status, scope, expires_at, decided_at
|
|
90
|
+
FROM pending_approvals
|
|
91
|
+
WHERE id = ? AND user_id = ?`
|
|
92
|
+
).get(approvalId, userId);
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
shutdown(reason = 'Approval expired because the server restarted.') {
|
|
99
|
+
for (const [approvalId, entry] of this._pending.entries()) {
|
|
100
|
+
clearTimeout(entry.timer);
|
|
101
|
+
this._pending.delete(approvalId);
|
|
102
|
+
this._updatePendingApprovalStatus(approvalId, 'expired', 'once');
|
|
103
|
+
this._io.to(`user:${entry.userId}`).emit('tool:approval_resolved', {
|
|
104
|
+
approvalId,
|
|
105
|
+
decision: 'expired',
|
|
106
|
+
reason,
|
|
107
|
+
});
|
|
108
|
+
this._logDecision(entry.userId, entry.runId, entry.toolName, entry.toolArgs, 'timeout', 'once');
|
|
109
|
+
entry.resolve('expired');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
22
113
|
/**
|
|
23
114
|
* Emits tool:approval_required and waits for a decision.
|
|
24
|
-
* Resolves to 'approved', 'denied', or '
|
|
115
|
+
* Resolves to 'approved', 'denied', 'timeout', or 'expired'.
|
|
25
116
|
*/
|
|
26
117
|
requestApproval(userId, runId, toolName, toolArgs) {
|
|
27
118
|
const approvalId = randomUUID();
|
|
28
119
|
const expiresAt = new Date(Date.now() + APPROVAL_TIMEOUT_MS).toISOString();
|
|
29
120
|
const category = getCategoryForTool(toolName, toolArgs) ?? 'unknown';
|
|
121
|
+
this._persistPendingApproval(approvalId, userId, runId, toolName, toolArgs, category, expiresAt);
|
|
30
122
|
|
|
31
123
|
const payload = { approvalId, runId, toolName, toolArgs, category, expiresAt };
|
|
32
124
|
this._io.to(`user:${userId}`).emit('tool:approval_required', payload);
|
|
@@ -35,6 +127,7 @@ class ApprovalGateService {
|
|
|
35
127
|
const timer = setTimeout(() => {
|
|
36
128
|
if (this._pending.has(approvalId)) {
|
|
37
129
|
this._pending.delete(approvalId);
|
|
130
|
+
this._updatePendingApprovalStatus(approvalId, 'timeout', 'once');
|
|
38
131
|
this._io.to(`user:${userId}`).emit('tool:approval_resolved', {
|
|
39
132
|
approvalId, decision: 'timeout',
|
|
40
133
|
});
|
|
@@ -43,7 +136,14 @@ class ApprovalGateService {
|
|
|
43
136
|
}
|
|
44
137
|
}, APPROVAL_TIMEOUT_MS);
|
|
45
138
|
|
|
46
|
-
this._pending.set(approvalId, {
|
|
139
|
+
this._pending.set(approvalId, {
|
|
140
|
+
resolve,
|
|
141
|
+
timer,
|
|
142
|
+
userId,
|
|
143
|
+
runId,
|
|
144
|
+
toolName,
|
|
145
|
+
toolArgs: toolArgs ?? {},
|
|
146
|
+
});
|
|
47
147
|
});
|
|
48
148
|
}
|
|
49
149
|
|
|
@@ -63,14 +163,17 @@ class ApprovalGateService {
|
|
|
63
163
|
const normalizedScope = ['once', 'session', 'always'].includes(scope) ? scope : 'once';
|
|
64
164
|
|
|
65
165
|
if (normalizedDecision === 'approved' && normalizedScope === 'session') {
|
|
66
|
-
this._sessionGrants.add(
|
|
166
|
+
this._sessionGrants.add(this._grantKey(userId, runId, toolName));
|
|
167
|
+
this._persistSessionGrant(userId, runId, toolName);
|
|
67
168
|
}
|
|
68
169
|
// 'always' scope is handled by the route (sets policy to 'allow') and
|
|
69
170
|
// also acts as a session grant for the current run.
|
|
70
171
|
if (normalizedDecision === 'approved' && normalizedScope === 'always') {
|
|
71
|
-
this._sessionGrants.add(
|
|
172
|
+
this._sessionGrants.add(this._grantKey(userId, runId, toolName));
|
|
173
|
+
this._persistSessionGrant(userId, runId, toolName);
|
|
72
174
|
}
|
|
73
175
|
|
|
176
|
+
this._updatePendingApprovalStatus(approvalId, normalizedDecision, normalizedScope);
|
|
74
177
|
const logScope = normalizedScope === 'always' ? 'session' : normalizedScope;
|
|
75
178
|
this._logDecision(userId, runId, toolName, toolArgs, normalizedDecision, logScope);
|
|
76
179
|
this._io.to(`user:${userId}`).emit('tool:approval_resolved', { approvalId, decision: normalizedDecision });
|
|
@@ -27,6 +27,7 @@ const TOOL_CATEGORIES = {
|
|
|
27
27
|
'update_ai_widget',
|
|
28
28
|
'delete_ai_widget',
|
|
29
29
|
],
|
|
30
|
+
external: [],
|
|
30
31
|
};
|
|
31
32
|
|
|
32
33
|
// Tools that bypass all policy checks — read-only or always safe
|
|
@@ -71,6 +72,98 @@ const SAFE_TOOLS = new Set([
|
|
|
71
72
|
'recordings_get',
|
|
72
73
|
]);
|
|
73
74
|
|
|
75
|
+
const BUILT_IN_TOOLS = new Set([
|
|
76
|
+
'execute_command',
|
|
77
|
+
'browser_navigate',
|
|
78
|
+
'browser_click',
|
|
79
|
+
'browser_type',
|
|
80
|
+
'browser_extract',
|
|
81
|
+
'browser_screenshot',
|
|
82
|
+
'browser_evaluate',
|
|
83
|
+
'desktop_list_devices',
|
|
84
|
+
'desktop_select_device',
|
|
85
|
+
'desktop_observe',
|
|
86
|
+
'desktop_click',
|
|
87
|
+
'desktop_drag',
|
|
88
|
+
'desktop_scroll',
|
|
89
|
+
'desktop_type',
|
|
90
|
+
'desktop_press_key',
|
|
91
|
+
'desktop_launch_app',
|
|
92
|
+
'desktop_get_tree',
|
|
93
|
+
'android_start_emulator',
|
|
94
|
+
'android_stop_emulator',
|
|
95
|
+
'android_list_devices',
|
|
96
|
+
'android_open_app',
|
|
97
|
+
'android_open_intent',
|
|
98
|
+
'android_tap',
|
|
99
|
+
'android_long_press',
|
|
100
|
+
'android_type',
|
|
101
|
+
'android_swipe',
|
|
102
|
+
'android_press_key',
|
|
103
|
+
'android_wait_for',
|
|
104
|
+
'android_observe',
|
|
105
|
+
'android_dump_ui',
|
|
106
|
+
'android_screenshot',
|
|
107
|
+
'android_list_apps',
|
|
108
|
+
'android_install_apk',
|
|
109
|
+
'android_shell',
|
|
110
|
+
'web_search',
|
|
111
|
+
'memory_save',
|
|
112
|
+
'memory_recall',
|
|
113
|
+
'session_search',
|
|
114
|
+
'memory_update_core',
|
|
115
|
+
'memory_write',
|
|
116
|
+
'memory_read',
|
|
117
|
+
'make_call',
|
|
118
|
+
'send_message',
|
|
119
|
+
'read_file',
|
|
120
|
+
'read_files',
|
|
121
|
+
'write_file',
|
|
122
|
+
'edit_file',
|
|
123
|
+
'replace_file_range',
|
|
124
|
+
'list_directory',
|
|
125
|
+
'search_files',
|
|
126
|
+
'code_navigate',
|
|
127
|
+
'query_structured_data',
|
|
128
|
+
'http_request',
|
|
129
|
+
'create_skill',
|
|
130
|
+
'list_skills',
|
|
131
|
+
'update_skill',
|
|
132
|
+
'delete_skill',
|
|
133
|
+
'think',
|
|
134
|
+
'activate_tools',
|
|
135
|
+
'spawn_subagent',
|
|
136
|
+
'delegate_to_agent',
|
|
137
|
+
'list_subagents',
|
|
138
|
+
'wait_subagent',
|
|
139
|
+
'cancel_subagent',
|
|
140
|
+
'notify_user',
|
|
141
|
+
'create_task',
|
|
142
|
+
'list_tasks',
|
|
143
|
+
'delete_task',
|
|
144
|
+
'update_task',
|
|
145
|
+
'create_ai_widget',
|
|
146
|
+
'list_ai_widgets',
|
|
147
|
+
'update_ai_widget',
|
|
148
|
+
'delete_ai_widget',
|
|
149
|
+
'mcp_add_server',
|
|
150
|
+
'mcp_list_servers',
|
|
151
|
+
'mcp_remove_server',
|
|
152
|
+
'generate_image',
|
|
153
|
+
'generate_table',
|
|
154
|
+
'generate_graph',
|
|
155
|
+
'analyze_image',
|
|
156
|
+
'ocr_extract',
|
|
157
|
+
'read_health_data',
|
|
158
|
+
'recordings_list',
|
|
159
|
+
'recordings_get',
|
|
160
|
+
'recordings_search',
|
|
161
|
+
'social_video_extract',
|
|
162
|
+
'task_complete',
|
|
163
|
+
'send_interim_update',
|
|
164
|
+
'save_widget_snapshot',
|
|
165
|
+
]);
|
|
166
|
+
|
|
74
167
|
// category → policy for users with no DB row yet
|
|
75
168
|
const DEFAULT_POLICY = {
|
|
76
169
|
shell: 'require_approval',
|
|
@@ -80,6 +173,7 @@ const DEFAULT_POLICY = {
|
|
|
80
173
|
browser_privileged: 'require_approval',
|
|
81
174
|
network_write: 'require_approval',
|
|
82
175
|
skill_mutation: 'deny',
|
|
176
|
+
external: 'require_approval',
|
|
83
177
|
};
|
|
84
178
|
|
|
85
179
|
// Reverse map: tool → category, built once
|
|
@@ -91,15 +185,30 @@ for (const [category, tools] of Object.entries(TOOL_CATEGORIES)) {
|
|
|
91
185
|
}
|
|
92
186
|
|
|
93
187
|
/**
|
|
94
|
-
* Returns the category for a tool
|
|
95
|
-
*
|
|
188
|
+
* Returns the category for a tool.
|
|
189
|
+
* - Known built-in tools may return null when they are read-only or otherwise
|
|
190
|
+
* intentionally left uncategorized.
|
|
191
|
+
* - Unknown tool names are treated as external capabilities and must never
|
|
192
|
+
* silently bypass policy checks.
|
|
96
193
|
*/
|
|
97
194
|
function getCategoryForTool(toolName, toolArgs = {}) {
|
|
98
195
|
if (toolName === 'http_request') {
|
|
99
196
|
const method = (toolArgs.method || 'GET').toUpperCase();
|
|
100
197
|
return ['GET', 'HEAD', 'OPTIONS'].includes(method) ? null : 'network_write';
|
|
101
198
|
}
|
|
102
|
-
|
|
199
|
+
if (_toolToCategory[toolName]) {
|
|
200
|
+
return _toolToCategory[toolName];
|
|
201
|
+
}
|
|
202
|
+
if (BUILT_IN_TOOLS.has(toolName) || SAFE_TOOLS.has(toolName)) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
return 'external';
|
|
103
206
|
}
|
|
104
207
|
|
|
105
|
-
module.exports = {
|
|
208
|
+
module.exports = {
|
|
209
|
+
TOOL_CATEGORIES,
|
|
210
|
+
SAFE_TOOLS,
|
|
211
|
+
DEFAULT_POLICY,
|
|
212
|
+
BUILT_IN_TOOLS,
|
|
213
|
+
getCategoryForTool,
|
|
214
|
+
};
|
|
@@ -63,10 +63,15 @@ function registerToolSecurityHooks(toolPolicyService, approvalGateService) {
|
|
|
63
63
|
if (decision === 'approved') return;
|
|
64
64
|
|
|
65
65
|
const isTimeout = decision === 'timeout';
|
|
66
|
+
const isExpired = decision === 'expired';
|
|
66
67
|
return {
|
|
67
68
|
block: true,
|
|
68
|
-
blocked_by:
|
|
69
|
-
|
|
69
|
+
blocked_by: isExpired
|
|
70
|
+
? 'approval_expired'
|
|
71
|
+
: (isTimeout ? 'approval_timeout' : 'user_denied'),
|
|
72
|
+
reason: isExpired
|
|
73
|
+
? `Approval for "${toolName}" expired because the server restarted or the run was interrupted. Do not retry unless the user explicitly asks you to try again.`
|
|
74
|
+
: isTimeout
|
|
70
75
|
? `Approval for "${toolName}" timed out — the user did not respond within 30 seconds. ` +
|
|
71
76
|
`Do not retry unless the user explicitly asks you to try again.`
|
|
72
77
|
: `The user denied the use of "${toolName}". Do not retry this tool call in this run.`,
|
|
@@ -21,6 +21,8 @@ function serializeInstalledSkill(skill) {
|
|
|
21
21
|
autoCreated: skill.metadata?.auto_created === true,
|
|
22
22
|
filePath: skill.filePath,
|
|
23
23
|
storeId: skill.metadata?.store_id || '',
|
|
24
|
+
readOnly: skill.readOnly === true,
|
|
25
|
+
ownerType: skill.ownerType || 'global',
|
|
24
26
|
};
|
|
25
27
|
}
|
|
26
28
|
|
|
@@ -294,6 +294,61 @@ class WorkspaceManager {
|
|
|
294
294
|
}
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
+
replaceFileRange(userId, options = {}) {
|
|
298
|
+
let filePath;
|
|
299
|
+
try {
|
|
300
|
+
filePath = this.resolvePath(userId, options.path || '', 'path');
|
|
301
|
+
if (!fs.existsSync(filePath)) {
|
|
302
|
+
return { success: false, error: `File not found: ${filePath}`, path: filePath };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const start = Number(options.start_line ?? options.startLine);
|
|
306
|
+
const end = Number(options.end_line ?? options.endLine ?? start);
|
|
307
|
+
if (!Number.isInteger(start) || start < 1) {
|
|
308
|
+
return { success: false, error: 'start_line must be a positive integer', path: filePath };
|
|
309
|
+
}
|
|
310
|
+
if (!Number.isInteger(end) || end < 1) {
|
|
311
|
+
return { success: false, error: 'end_line must be a positive integer', path: filePath };
|
|
312
|
+
}
|
|
313
|
+
if (start > end) {
|
|
314
|
+
return { success: false, error: 'start_line must be less than or equal to end_line', path: filePath };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const original = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
318
|
+
const hadFinalNewline = original.endsWith('\n');
|
|
319
|
+
const lines = original.split('\n');
|
|
320
|
+
if (hadFinalNewline) lines.pop();
|
|
321
|
+
const totalLines = Math.max(lines.length, 1);
|
|
322
|
+
if (start > totalLines || end > totalLines) {
|
|
323
|
+
return {
|
|
324
|
+
success: false,
|
|
325
|
+
error: `line range ${start}-${end} is outside file with ${totalLines} lines`,
|
|
326
|
+
path: filePath,
|
|
327
|
+
totalLines,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const replacementText = String(options.content ?? '').replace(/\r\n/g, '\n');
|
|
332
|
+
const replacementLines = replacementText === ''
|
|
333
|
+
? []
|
|
334
|
+
: replacementText.replace(/\n$/, '').split('\n');
|
|
335
|
+
lines.splice(start - 1, end - start + 1, ...replacementLines);
|
|
336
|
+
const next = `${lines.join('\n')}${hadFinalNewline ? '\n' : ''}`;
|
|
337
|
+
fs.writeFileSync(filePath, next, 'utf8');
|
|
338
|
+
return {
|
|
339
|
+
success: true,
|
|
340
|
+
path: filePath,
|
|
341
|
+
startLine: start,
|
|
342
|
+
endLine: end,
|
|
343
|
+
replacedLines: end - start + 1,
|
|
344
|
+
insertedLines: replacementLines.length,
|
|
345
|
+
totalLines: lines.length,
|
|
346
|
+
};
|
|
347
|
+
} catch (err) {
|
|
348
|
+
return { success: false, path: filePath || null, error: err.message };
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
297
352
|
listDirectory(userId, options = {}) {
|
|
298
353
|
const dirPath = this.resolvePath(userId, options.path || '.', 'path');
|
|
299
354
|
const depthValue = options.depth != null ? Number(options.depth) : (options.recursive ? 3 : 1);
|
|
@@ -414,4 +469,5 @@ class WorkspaceManager {
|
|
|
414
469
|
|
|
415
470
|
module.exports = {
|
|
416
471
|
WorkspaceManager,
|
|
472
|
+
sanitizeWorkspaceKey,
|
|
417
473
|
};
|