deepagentsdk 0.9.2
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 +159 -0
- package/package.json +95 -0
- package/src/agent.ts +1230 -0
- package/src/backends/composite.ts +273 -0
- package/src/backends/filesystem.ts +692 -0
- package/src/backends/index.ts +22 -0
- package/src/backends/local-sandbox.ts +175 -0
- package/src/backends/persistent.ts +593 -0
- package/src/backends/sandbox.ts +510 -0
- package/src/backends/state.ts +244 -0
- package/src/backends/utils.ts +287 -0
- package/src/checkpointer/file-saver.ts +98 -0
- package/src/checkpointer/index.ts +5 -0
- package/src/checkpointer/kv-saver.ts +82 -0
- package/src/checkpointer/memory-saver.ts +82 -0
- package/src/checkpointer/types.ts +125 -0
- package/src/cli/components/ApiKeyInput.tsx +300 -0
- package/src/cli/components/FilePreview.tsx +237 -0
- package/src/cli/components/Input.tsx +277 -0
- package/src/cli/components/Message.tsx +93 -0
- package/src/cli/components/ModelSelection.tsx +338 -0
- package/src/cli/components/SlashMenu.tsx +101 -0
- package/src/cli/components/StatusBar.tsx +89 -0
- package/src/cli/components/Subagent.tsx +91 -0
- package/src/cli/components/TodoList.tsx +133 -0
- package/src/cli/components/ToolApproval.tsx +70 -0
- package/src/cli/components/ToolCall.tsx +144 -0
- package/src/cli/components/ToolCallSummary.tsx +175 -0
- package/src/cli/components/Welcome.tsx +75 -0
- package/src/cli/components/index.ts +24 -0
- package/src/cli/hooks/index.ts +12 -0
- package/src/cli/hooks/useAgent.ts +933 -0
- package/src/cli/index.tsx +1066 -0
- package/src/cli/theme.ts +205 -0
- package/src/cli/utils/model-list.ts +365 -0
- package/src/constants/errors.ts +29 -0
- package/src/constants/limits.ts +195 -0
- package/src/index.ts +176 -0
- package/src/middleware/agent-memory.ts +330 -0
- package/src/prompts.ts +196 -0
- package/src/skills/index.ts +2 -0
- package/src/skills/load.ts +191 -0
- package/src/skills/types.ts +53 -0
- package/src/tools/execute.ts +167 -0
- package/src/tools/filesystem.ts +418 -0
- package/src/tools/index.ts +39 -0
- package/src/tools/subagent.ts +443 -0
- package/src/tools/todos.ts +101 -0
- package/src/tools/web.ts +567 -0
- package/src/types/backend.ts +177 -0
- package/src/types/core.ts +220 -0
- package/src/types/events.ts +429 -0
- package/src/types/index.ts +94 -0
- package/src/types/structured-output.ts +43 -0
- package/src/types/subagent.ts +96 -0
- package/src/types.ts +22 -0
- package/src/utils/approval.ts +213 -0
- package/src/utils/events.ts +416 -0
- package/src/utils/eviction.ts +181 -0
- package/src/utils/index.ts +34 -0
- package/src/utils/model-parser.ts +38 -0
- package/src/utils/patch-tool-calls.ts +233 -0
- package/src/utils/project-detection.ts +32 -0
- package/src/utils/summarization.ts +254 -0
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PersistentBackend: Generic persistent storage backend.
|
|
3
|
+
*
|
|
4
|
+
* This backend provides cross-conversation file persistence using a
|
|
5
|
+
* pluggable key-value store interface. It can be used with various
|
|
6
|
+
* storage solutions like Redis, SQLite, or any custom implementation.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
BackendProtocol,
|
|
11
|
+
EditResult,
|
|
12
|
+
FileData,
|
|
13
|
+
FileInfo,
|
|
14
|
+
GrepMatch,
|
|
15
|
+
WriteResult,
|
|
16
|
+
} from "../types.js";
|
|
17
|
+
import {
|
|
18
|
+
createFileData,
|
|
19
|
+
fileDataToString,
|
|
20
|
+
formatReadResponse,
|
|
21
|
+
globSearchFiles,
|
|
22
|
+
grepMatchesFromFiles,
|
|
23
|
+
performStringReplacement,
|
|
24
|
+
updateFileData,
|
|
25
|
+
} from "./utils.js";
|
|
26
|
+
import {
|
|
27
|
+
FILE_NOT_FOUND,
|
|
28
|
+
FILE_ALREADY_EXISTS,
|
|
29
|
+
} from "../constants/errors.js";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Generic key-value store interface for persistent storage.
|
|
33
|
+
*
|
|
34
|
+
* Implement this interface to use any storage backend (Redis, SQLite, cloud storage, etc.)
|
|
35
|
+
* with PersistentBackend. The interface uses hierarchical namespaces for organization.
|
|
36
|
+
*
|
|
37
|
+
* @example Redis implementation
|
|
38
|
+
* ```typescript
|
|
39
|
+
* class RedisStore implements KeyValueStore {
|
|
40
|
+
* constructor(private redis: RedisClient) {}
|
|
41
|
+
*
|
|
42
|
+
* async get(namespace: string[], key: string) {
|
|
43
|
+
* const redisKey = [...namespace, key].join(':');
|
|
44
|
+
* const data = await this.redis.get(redisKey);
|
|
45
|
+
* return data ? JSON.parse(data) : undefined;
|
|
46
|
+
* }
|
|
47
|
+
*
|
|
48
|
+
* async put(namespace: string[], key: string, value: Record<string, unknown>) {
|
|
49
|
+
* const redisKey = [...namespace, key].join(':');
|
|
50
|
+
* await this.redis.set(redisKey, JSON.stringify(value));
|
|
51
|
+
* }
|
|
52
|
+
*
|
|
53
|
+
* async delete(namespace: string[], key: string) {
|
|
54
|
+
* const redisKey = [...namespace, key].join(':');
|
|
55
|
+
* await this.redis.del(redisKey);
|
|
56
|
+
* }
|
|
57
|
+
*
|
|
58
|
+
* async list(namespace: string[]) {
|
|
59
|
+
* const prefix = [...namespace].join(':') + ':';
|
|
60
|
+
* const keys = await this.redis.keys(prefix + '*');
|
|
61
|
+
* const results = [];
|
|
62
|
+
* for (const key of keys) {
|
|
63
|
+
* const data = await this.redis.get(key);
|
|
64
|
+
* if (data) {
|
|
65
|
+
* const relativeKey = key.substring(prefix.length);
|
|
66
|
+
* results.push({ key: relativeKey, value: JSON.parse(data) });
|
|
67
|
+
* }
|
|
68
|
+
* }
|
|
69
|
+
* return results;
|
|
70
|
+
* }
|
|
71
|
+
* }
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export interface KeyValueStore {
|
|
75
|
+
/**
|
|
76
|
+
* Get a value by key from the store.
|
|
77
|
+
* @param namespace - Hierarchical namespace array (e.g., ["project1", "filesystem"])
|
|
78
|
+
* @param key - The key to retrieve (file path in the case of PersistentBackend)
|
|
79
|
+
* @returns The stored value as a record, or undefined if not found
|
|
80
|
+
*/
|
|
81
|
+
get(namespace: string[], key: string): Promise<Record<string, unknown> | undefined>;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Store a value by key in the store.
|
|
85
|
+
* @param namespace - Hierarchical namespace array
|
|
86
|
+
* @param key - The key to store (file path in the case of PersistentBackend)
|
|
87
|
+
* @param value - The value to store (must be serializable to JSON)
|
|
88
|
+
*/
|
|
89
|
+
put(namespace: string[], key: string, value: Record<string, unknown>): Promise<void>;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Delete a value by key from the store.
|
|
93
|
+
* @param namespace - Hierarchical namespace array
|
|
94
|
+
* @param key - The key to delete (file path in the case of PersistentBackend)
|
|
95
|
+
*/
|
|
96
|
+
delete(namespace: string[], key: string): Promise<void>;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* List all keys and values in a namespace.
|
|
100
|
+
* @param namespace - Hierarchical namespace array
|
|
101
|
+
* @returns Array of items with key and value pairs directly in this namespace
|
|
102
|
+
* (not including sub-namespaces)
|
|
103
|
+
*/
|
|
104
|
+
list(namespace: string[]): Promise<Array<{ key: string; value: Record<string, unknown> }>>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Simple in-memory implementation of KeyValueStore.
|
|
109
|
+
*
|
|
110
|
+
* Useful for testing or single-session persistence. Data is stored in a Map
|
|
111
|
+
* and does not persist across application restarts.
|
|
112
|
+
*
|
|
113
|
+
* @example Basic usage
|
|
114
|
+
* ```typescript
|
|
115
|
+
* const store = new InMemoryStore();
|
|
116
|
+
* const backend = new PersistentBackend({ store });
|
|
117
|
+
* ```
|
|
118
|
+
*
|
|
119
|
+
* @example For testing
|
|
120
|
+
* ```typescript
|
|
121
|
+
* const store = new InMemoryStore();
|
|
122
|
+
* // ... run tests ...
|
|
123
|
+
* store.clear(); // Clean up after tests
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export class InMemoryStore implements KeyValueStore {
|
|
127
|
+
private data = new Map<string, Record<string, unknown>>();
|
|
128
|
+
|
|
129
|
+
private makeKey(namespace: string[], key: string): string {
|
|
130
|
+
return [...namespace, key].join(":");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private parseKey(fullKey: string, namespace: string[]): string | null {
|
|
134
|
+
const prefix = namespace.join(":") + ":";
|
|
135
|
+
if (fullKey.startsWith(prefix)) {
|
|
136
|
+
return fullKey.substring(prefix.length);
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async get(namespace: string[], key: string): Promise<Record<string, unknown> | undefined> {
|
|
142
|
+
return this.data.get(this.makeKey(namespace, key));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async put(namespace: string[], key: string, value: Record<string, unknown>): Promise<void> {
|
|
146
|
+
this.data.set(this.makeKey(namespace, key), value);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async delete(namespace: string[], key: string): Promise<void> {
|
|
150
|
+
this.data.delete(this.makeKey(namespace, key));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async list(namespace: string[]): Promise<Array<{ key: string; value: Record<string, unknown> }>> {
|
|
154
|
+
const results: Array<{ key: string; value: Record<string, unknown> }> = [];
|
|
155
|
+
const prefix = namespace.join(":") + ":";
|
|
156
|
+
|
|
157
|
+
for (const [fullKey, value] of this.data.entries()) {
|
|
158
|
+
if (fullKey.startsWith(prefix)) {
|
|
159
|
+
const key = fullKey.substring(prefix.length);
|
|
160
|
+
// Only include items directly in this namespace (no sub-namespaces)
|
|
161
|
+
if (!key.includes(":")) {
|
|
162
|
+
results.push({ key, value });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return results;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Clear all data (useful for testing).
|
|
172
|
+
*/
|
|
173
|
+
clear(): void {
|
|
174
|
+
this.data.clear();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get the number of stored items.
|
|
179
|
+
*/
|
|
180
|
+
size(): number {
|
|
181
|
+
return this.data.size;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Options for creating a PersistentBackend.
|
|
187
|
+
*/
|
|
188
|
+
export interface PersistentBackendOptions {
|
|
189
|
+
/**
|
|
190
|
+
* **Required.** The key-value store implementation to use.
|
|
191
|
+
*
|
|
192
|
+
* You can use the built-in `InMemoryStore` for testing, or implement `KeyValueStore`
|
|
193
|
+
* for custom storage (Redis, SQLite, etc.).
|
|
194
|
+
*
|
|
195
|
+
* @see {@link KeyValueStore} for the interface definition
|
|
196
|
+
* @see {@link InMemoryStore} for a simple in-memory implementation
|
|
197
|
+
*/
|
|
198
|
+
store: KeyValueStore;
|
|
199
|
+
/**
|
|
200
|
+
* Optional namespace prefix for isolation (e.g., project ID, user ID).
|
|
201
|
+
*
|
|
202
|
+
* This allows multiple agents or projects to share the same store without conflicts.
|
|
203
|
+
* Files are stored under `[namespace]/filesystem/` in the key-value store.
|
|
204
|
+
*
|
|
205
|
+
* Default: "default"
|
|
206
|
+
*/
|
|
207
|
+
namespace?: string;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Backend that stores files in a persistent key-value store.
|
|
212
|
+
*
|
|
213
|
+
* This provides cross-conversation file persistence that survives between agent sessions.
|
|
214
|
+
* Files are stored in the provided key-value store, allowing you to use any storage backend
|
|
215
|
+
* (Redis, SQLite, cloud storage, etc.) by implementing the `KeyValueStore` interface.
|
|
216
|
+
*
|
|
217
|
+
* @example Using InMemoryStore (for testing or single-session persistence)
|
|
218
|
+
* ```typescript
|
|
219
|
+
* import { createDeepAgent } from 'deepagentsdk';
|
|
220
|
+
* import { PersistentBackend, InMemoryStore } from 'deepagentsdk';
|
|
221
|
+
* import { anthropic } from '@ai-sdk/anthropic';
|
|
222
|
+
*
|
|
223
|
+
* const store = new InMemoryStore();
|
|
224
|
+
* const backend = new PersistentBackend({ store });
|
|
225
|
+
* const agent = createDeepAgent({
|
|
226
|
+
* model: anthropic('claude-sonnet-4-20250514'),
|
|
227
|
+
* backend,
|
|
228
|
+
* });
|
|
229
|
+
* ```
|
|
230
|
+
*
|
|
231
|
+
* @example With custom namespace for project isolation
|
|
232
|
+
* ```typescript
|
|
233
|
+
* import { createDeepAgent } from 'deepagentsdk';
|
|
234
|
+
* import { PersistentBackend, InMemoryStore } from 'deepagentsdk';
|
|
235
|
+
* import { anthropic } from '@ai-sdk/anthropic';
|
|
236
|
+
*
|
|
237
|
+
* const store = new InMemoryStore();
|
|
238
|
+
* const backend = new PersistentBackend({
|
|
239
|
+
* store,
|
|
240
|
+
* namespace: 'project-123', // Isolate files for this project
|
|
241
|
+
* });
|
|
242
|
+
* const agent = createDeepAgent({
|
|
243
|
+
* model: anthropic('claude-sonnet-4-20250514'),
|
|
244
|
+
* backend,
|
|
245
|
+
* });
|
|
246
|
+
* ```
|
|
247
|
+
*
|
|
248
|
+
* @example Custom KeyValueStore implementation (Redis)
|
|
249
|
+
* ```typescript
|
|
250
|
+
* import { createDeepAgent } from 'deepagentsdk';
|
|
251
|
+
* import { PersistentBackend, type KeyValueStore } from 'deepagentsdk';
|
|
252
|
+
* import { anthropic } from '@ai-sdk/anthropic';
|
|
253
|
+
* import { createClient } from 'redis';
|
|
254
|
+
*
|
|
255
|
+
* class RedisStore implements KeyValueStore {
|
|
256
|
+
* constructor(private redis: ReturnType<typeof createClient>) {}
|
|
257
|
+
*
|
|
258
|
+
* async get(namespace: string[], key: string) {
|
|
259
|
+
* const redisKey = [...namespace, key].join(':');
|
|
260
|
+
* const data = await this.redis.get(redisKey);
|
|
261
|
+
* return data ? JSON.parse(data) : undefined;
|
|
262
|
+
* }
|
|
263
|
+
*
|
|
264
|
+
* async put(namespace: string[], key: string, value: Record<string, unknown>) {
|
|
265
|
+
* const redisKey = [...namespace, key].join(':');
|
|
266
|
+
* await this.redis.set(redisKey, JSON.stringify(value));
|
|
267
|
+
* }
|
|
268
|
+
*
|
|
269
|
+
* async delete(namespace: string[], key: string) {
|
|
270
|
+
* const redisKey = [...namespace, key].join(':');
|
|
271
|
+
* await this.redis.del(redisKey);
|
|
272
|
+
* }
|
|
273
|
+
*
|
|
274
|
+
* async list(namespace: string[]) {
|
|
275
|
+
* const prefix = [...namespace].join(':') + ':';
|
|
276
|
+
* const keys = await this.redis.keys(prefix + '*');
|
|
277
|
+
* const results = [];
|
|
278
|
+
* for (const key of keys) {
|
|
279
|
+
* const data = await this.redis.get(key);
|
|
280
|
+
* if (data) {
|
|
281
|
+
* const relativeKey = key.substring(prefix.length);
|
|
282
|
+
* results.push({ key: relativeKey, value: JSON.parse(data) });
|
|
283
|
+
* }
|
|
284
|
+
* }
|
|
285
|
+
* return results;
|
|
286
|
+
* }
|
|
287
|
+
* }
|
|
288
|
+
*
|
|
289
|
+
* const redis = createClient();
|
|
290
|
+
* await redis.connect();
|
|
291
|
+
*
|
|
292
|
+
* const backend = new PersistentBackend({
|
|
293
|
+
* store: new RedisStore(redis),
|
|
294
|
+
* namespace: 'production'
|
|
295
|
+
* });
|
|
296
|
+
*
|
|
297
|
+
* const agent = createDeepAgent({
|
|
298
|
+
* model: anthropic('claude-sonnet-4-20250514'),
|
|
299
|
+
* backend,
|
|
300
|
+
* });
|
|
301
|
+
* ```
|
|
302
|
+
*/
|
|
303
|
+
export class PersistentBackend implements BackendProtocol {
|
|
304
|
+
private store: KeyValueStore;
|
|
305
|
+
private namespacePrefix: string;
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Create a new PersistentBackend instance.
|
|
309
|
+
*
|
|
310
|
+
* @param options - Configuration options
|
|
311
|
+
* @param options.store - The key-value store implementation to use
|
|
312
|
+
* @param options.namespace - Optional namespace prefix for file isolation
|
|
313
|
+
*/
|
|
314
|
+
constructor(options: PersistentBackendOptions) {
|
|
315
|
+
this.store = options.store;
|
|
316
|
+
this.namespacePrefix = options.namespace || "default";
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Get the namespace for store operations.
|
|
321
|
+
*/
|
|
322
|
+
protected getNamespace(): string[] {
|
|
323
|
+
return [this.namespacePrefix, "filesystem"];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Convert a store value to FileData format.
|
|
328
|
+
*/
|
|
329
|
+
private convertToFileData(value: Record<string, unknown>): FileData {
|
|
330
|
+
if (
|
|
331
|
+
!value.content ||
|
|
332
|
+
!Array.isArray(value.content) ||
|
|
333
|
+
typeof value.created_at !== "string" ||
|
|
334
|
+
typeof value.modified_at !== "string"
|
|
335
|
+
) {
|
|
336
|
+
throw new Error(
|
|
337
|
+
`Store item does not contain valid FileData fields. Got keys: ${Object.keys(value).join(", ")}`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
content: value.content as string[],
|
|
343
|
+
created_at: value.created_at,
|
|
344
|
+
modified_at: value.modified_at,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Convert FileData to a value suitable for store.put().
|
|
350
|
+
*/
|
|
351
|
+
private convertFromFileData(fileData: FileData): Record<string, unknown> {
|
|
352
|
+
return {
|
|
353
|
+
content: fileData.content,
|
|
354
|
+
created_at: fileData.created_at,
|
|
355
|
+
modified_at: fileData.modified_at,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* List files and directories in the specified directory (non-recursive).
|
|
361
|
+
*/
|
|
362
|
+
async lsInfo(path: string): Promise<FileInfo[]> {
|
|
363
|
+
const namespace = this.getNamespace();
|
|
364
|
+
const items = await this.store.list(namespace);
|
|
365
|
+
const infos: FileInfo[] = [];
|
|
366
|
+
const subdirs = new Set<string>();
|
|
367
|
+
|
|
368
|
+
// Normalize path to have trailing slash for proper prefix matching
|
|
369
|
+
const normalizedPath = path.endsWith("/") ? path : path + "/";
|
|
370
|
+
|
|
371
|
+
for (const item of items) {
|
|
372
|
+
const itemKey = item.key;
|
|
373
|
+
|
|
374
|
+
// Check if file is in the specified directory or a subdirectory
|
|
375
|
+
if (!itemKey.startsWith(normalizedPath)) {
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Get the relative path after the directory
|
|
380
|
+
const relative = itemKey.substring(normalizedPath.length);
|
|
381
|
+
|
|
382
|
+
// If relative path contains '/', it's in a subdirectory
|
|
383
|
+
if (relative.includes("/")) {
|
|
384
|
+
// Extract the immediate subdirectory name
|
|
385
|
+
const subdirName = relative.split("/")[0];
|
|
386
|
+
subdirs.add(normalizedPath + subdirName + "/");
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// This is a file directly in the current directory
|
|
391
|
+
try {
|
|
392
|
+
const fd = this.convertToFileData(item.value);
|
|
393
|
+
const size = fd.content.join("\n").length;
|
|
394
|
+
infos.push({
|
|
395
|
+
path: itemKey,
|
|
396
|
+
is_dir: false,
|
|
397
|
+
size: size,
|
|
398
|
+
modified_at: fd.modified_at,
|
|
399
|
+
});
|
|
400
|
+
} catch {
|
|
401
|
+
// Skip invalid items
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Add directories to the results
|
|
407
|
+
for (const subdir of Array.from(subdirs).sort()) {
|
|
408
|
+
infos.push({
|
|
409
|
+
path: subdir,
|
|
410
|
+
is_dir: true,
|
|
411
|
+
size: 0,
|
|
412
|
+
modified_at: "",
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
infos.sort((a, b) => a.path.localeCompare(b.path));
|
|
417
|
+
return infos;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Read file content with line numbers.
|
|
422
|
+
*/
|
|
423
|
+
async read(
|
|
424
|
+
filePath: string,
|
|
425
|
+
offset: number = 0,
|
|
426
|
+
limit: number = 2000
|
|
427
|
+
): Promise<string> {
|
|
428
|
+
try {
|
|
429
|
+
const fileData = await this.readRaw(filePath);
|
|
430
|
+
return formatReadResponse(fileData, offset, limit);
|
|
431
|
+
} catch (e: unknown) {
|
|
432
|
+
const error = e as Error;
|
|
433
|
+
return `Error: ${error.message}`;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Read file content as raw FileData.
|
|
439
|
+
*/
|
|
440
|
+
async readRaw(filePath: string): Promise<FileData> {
|
|
441
|
+
const namespace = this.getNamespace();
|
|
442
|
+
const value = await this.store.get(namespace, filePath);
|
|
443
|
+
|
|
444
|
+
if (!value) {
|
|
445
|
+
throw new Error(`File '${filePath}' not found`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return this.convertToFileData(value);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Create a new file with content.
|
|
453
|
+
*/
|
|
454
|
+
async write(filePath: string, content: string): Promise<WriteResult> {
|
|
455
|
+
const namespace = this.getNamespace();
|
|
456
|
+
|
|
457
|
+
// Check if file exists
|
|
458
|
+
const existing = await this.store.get(namespace, filePath);
|
|
459
|
+
if (existing) {
|
|
460
|
+
return {
|
|
461
|
+
success: false,
|
|
462
|
+
error: FILE_ALREADY_EXISTS(filePath),
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Create new file
|
|
467
|
+
const fileData = createFileData(content);
|
|
468
|
+
const storeValue = this.convertFromFileData(fileData);
|
|
469
|
+
await this.store.put(namespace, filePath, storeValue);
|
|
470
|
+
return { success: true, path: filePath };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Edit a file by replacing string occurrences.
|
|
475
|
+
*/
|
|
476
|
+
async edit(
|
|
477
|
+
filePath: string,
|
|
478
|
+
oldString: string,
|
|
479
|
+
newString: string,
|
|
480
|
+
replaceAll: boolean = false
|
|
481
|
+
): Promise<EditResult> {
|
|
482
|
+
const namespace = this.getNamespace();
|
|
483
|
+
|
|
484
|
+
// Get existing file
|
|
485
|
+
const value = await this.store.get(namespace, filePath);
|
|
486
|
+
if (!value) {
|
|
487
|
+
return { success: false, error: FILE_NOT_FOUND(filePath) };
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
try {
|
|
491
|
+
const fileData = this.convertToFileData(value);
|
|
492
|
+
const content = fileDataToString(fileData);
|
|
493
|
+
const result = performStringReplacement(
|
|
494
|
+
content,
|
|
495
|
+
oldString,
|
|
496
|
+
newString,
|
|
497
|
+
replaceAll
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
if (typeof result === "string") {
|
|
501
|
+
return { success: false, error: result };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const [newContent, occurrences] = result;
|
|
505
|
+
const newFileData = updateFileData(fileData, newContent);
|
|
506
|
+
|
|
507
|
+
// Update file in store
|
|
508
|
+
const storeValue = this.convertFromFileData(newFileData);
|
|
509
|
+
await this.store.put(namespace, filePath, storeValue);
|
|
510
|
+
return { success: true, path: filePath, occurrences };
|
|
511
|
+
} catch (e: unknown) {
|
|
512
|
+
const error = e as Error;
|
|
513
|
+
return { success: false, error: `Error: ${error.message}` };
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Structured search results or error string for invalid input.
|
|
519
|
+
*/
|
|
520
|
+
async grepRaw(
|
|
521
|
+
pattern: string,
|
|
522
|
+
path: string = "/",
|
|
523
|
+
glob: string | null = null
|
|
524
|
+
): Promise<GrepMatch[] | string> {
|
|
525
|
+
const namespace = this.getNamespace();
|
|
526
|
+
const items = await this.store.list(namespace);
|
|
527
|
+
|
|
528
|
+
const files: Record<string, FileData> = {};
|
|
529
|
+
for (const item of items) {
|
|
530
|
+
try {
|
|
531
|
+
files[item.key] = this.convertToFileData(item.value);
|
|
532
|
+
} catch {
|
|
533
|
+
// Skip invalid items
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return grepMatchesFromFiles(files, pattern, path, glob);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Structured glob matching returning FileInfo objects.
|
|
543
|
+
*/
|
|
544
|
+
async globInfo(pattern: string, path: string = "/"): Promise<FileInfo[]> {
|
|
545
|
+
const namespace = this.getNamespace();
|
|
546
|
+
const items = await this.store.list(namespace);
|
|
547
|
+
|
|
548
|
+
const files: Record<string, FileData> = {};
|
|
549
|
+
for (const item of items) {
|
|
550
|
+
try {
|
|
551
|
+
files[item.key] = this.convertToFileData(item.value);
|
|
552
|
+
} catch {
|
|
553
|
+
// Skip invalid items
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const result = globSearchFiles(files, pattern, path);
|
|
559
|
+
if (result === "No files found") {
|
|
560
|
+
return [];
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const paths = result.split("\n");
|
|
564
|
+
const infos: FileInfo[] = [];
|
|
565
|
+
for (const p of paths) {
|
|
566
|
+
const fd = files[p];
|
|
567
|
+
const size = fd ? fd.content.join("\n").length : 0;
|
|
568
|
+
infos.push({
|
|
569
|
+
path: p,
|
|
570
|
+
is_dir: false,
|
|
571
|
+
size: size,
|
|
572
|
+
modified_at: fd?.modified_at || "",
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
return infos;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Delete a file.
|
|
580
|
+
*/
|
|
581
|
+
async deleteFile(filePath: string): Promise<{ error?: string }> {
|
|
582
|
+
const namespace = this.getNamespace();
|
|
583
|
+
const existing = await this.store.get(namespace, filePath);
|
|
584
|
+
|
|
585
|
+
if (!existing) {
|
|
586
|
+
return { error: `File '${filePath}' not found` };
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
await this.store.delete(namespace, filePath);
|
|
590
|
+
return {};
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|