@modelprofile.com/flexharness 10.17.1 → 10.19.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/errors.d.ts +4 -0
- package/dist_ts/errors.js +9 -1
- package/dist_ts/functions.toolpermissions.d.ts +24 -0
- package/dist_ts/functions.toolpermissions.js +38 -0
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/plugins.d.ts +1 -1
- package/dist_ts/plugins.js +1 -1
- package/dist_ts_chat_harness/classes.chatclient.d.ts +95 -0
- package/dist_ts_chat_harness/classes.chatclient.js +263 -0
- package/dist_ts_chat_harness/classes.chatrequesterror.d.ts +4 -0
- package/dist_ts_chat_harness/classes.chatrequesterror.js +8 -0
- package/dist_ts_chat_harness/classes.transcript.d.ts +1 -1
- package/dist_ts_chat_harness/classes.transcript.js +1 -1
- package/dist_ts_chat_harness/functions.intents.d.ts +1 -2
- package/dist_ts_chat_harness/functions.intents.js +1 -1
- package/dist_ts_chat_harness/functions.mapping.d.ts +1 -1
- package/dist_ts_chat_harness/functions.mapping.js +1 -1
- package/dist_ts_chat_harness/index.d.ts +3 -0
- package/dist_ts_chat_harness/index.js +3 -1
- package/dist_ts_chat_harness/interfaces.chat.d.ts +136 -0
- package/dist_ts_chat_harness/interfaces.chat.js +2 -0
- package/dist_ts_chat_harness/plugins.js +1 -1
- package/dist_ts_chat_harness_server/classes.chathub.d.ts +78 -0
- package/dist_ts_chat_harness_server/classes.chathub.js +332 -0
- package/dist_ts_chat_harness_server/classes.chatsubscription.d.ts +55 -0
- package/dist_ts_chat_harness_server/classes.chatsubscription.js +236 -0
- package/dist_ts_chat_harness_server/classes.sessionlists.d.ts +21 -0
- package/dist_ts_chat_harness_server/classes.sessionlists.js +70 -0
- package/dist_ts_chat_harness_server/classes.turngate.d.ts +38 -0
- package/dist_ts_chat_harness_server/classes.turngate.js +207 -0
- package/dist_ts_chat_harness_server/errors.d.ts +5 -0
- package/dist_ts_chat_harness_server/errors.js +9 -0
- package/dist_ts_chat_harness_server/functions.errorprojector.d.ts +11 -0
- package/dist_ts_chat_harness_server/functions.errorprojector.js +32 -0
- package/dist_ts_chat_harness_server/functions.validation.d.ts +18 -0
- package/dist_ts_chat_harness_server/functions.validation.js +88 -0
- package/dist_ts_chat_harness_server/index.d.ts +4 -0
- package/dist_ts_chat_harness_server/index.js +4 -0
- package/dist_ts_chat_harness_server/interfaces.d.ts +67 -0
- package/dist_ts_chat_harness_server/interfaces.js +2 -0
- package/dist_ts_chat_harness_server/plugins.d.ts +7 -0
- package/dist_ts_chat_harness_server/plugins.js +6 -0
- package/dist_ts_chat_harness_typedsocket/classes.clienttransport.d.ts +22 -0
- package/dist_ts_chat_harness_typedsocket/classes.clienttransport.js +55 -0
- package/dist_ts_chat_harness_typedsocket/classes.serverbinding.d.ts +46 -0
- package/dist_ts_chat_harness_typedsocket/classes.serverbinding.js +110 -0
- package/dist_ts_chat_harness_typedsocket/index.d.ts +3 -0
- package/dist_ts_chat_harness_typedsocket/index.js +3 -0
- package/dist_ts_chat_harness_typedsocket/interfaces.requests.d.ts +38 -0
- package/dist_ts_chat_harness_typedsocket/interfaces.requests.js +2 -0
- package/dist_ts_chat_harness_typedsocket/plugins.d.ts +9 -0
- package/dist_ts_chat_harness_typedsocket/plugins.js +8 -0
- package/dist_ts_ocr/ocr.mistral.d.ts +20 -1
- package/dist_ts_ocr/ocr.mistral.js +66 -43
- package/dist_ts_tools/tool.filesystem.d.ts +2 -0
- package/dist_ts_tools/tool.filesystem.js +42 -1
- package/dist_ts_tools_node/classes.workspacereversion.d.ts +48 -0
- package/dist_ts_tools_node/classes.workspacereversion.js +202 -0
- package/dist_ts_tools_node/functions.fileimages.d.ts +22 -0
- package/dist_ts_tools_node/functions.fileimages.js +87 -0
- package/dist_ts_tools_node/index.d.ts +3 -1
- package/dist_ts_tools_node/index.js +2 -1
- package/dist_ts_tools_node/plugins.d.ts +1 -0
- package/dist_ts_tools_node/tool.context.d.ts +20 -0
- package/dist_ts_tools_node/tool.context.js +25 -4
- package/package.json +28 -4
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/errors.ts +10 -0
- package/ts/functions.toolpermissions.ts +60 -0
- package/ts/index.ts +6 -0
- package/ts/plugins.ts +1 -0
- package/ts_chat_harness/classes.chatclient.ts +343 -0
- package/ts_chat_harness/classes.chatrequesterror.ts +7 -0
- package/ts_chat_harness/classes.transcript.ts +1 -1
- package/ts_chat_harness/functions.intents.ts +2 -2
- package/ts_chat_harness/functions.mapping.ts +1 -1
- package/ts_chat_harness/index.ts +20 -0
- package/ts_chat_harness/interfaces.chat.ts +108 -0
- package/ts_chat_harness/readme.md +24 -0
- package/ts_chat_harness_server/classes.chathub.ts +400 -0
- package/ts_chat_harness_server/classes.chatsubscription.ts +257 -0
- package/ts_chat_harness_server/classes.sessionlists.ts +90 -0
- package/ts_chat_harness_server/classes.turngate.ts +247 -0
- package/ts_chat_harness_server/errors.ts +9 -0
- package/ts_chat_harness_server/functions.errorprojector.ts +40 -0
- package/ts_chat_harness_server/functions.validation.ts +104 -0
- package/ts_chat_harness_server/index.ts +19 -0
- package/ts_chat_harness_server/interfaces.ts +92 -0
- package/ts_chat_harness_server/plugins.ts +62 -0
- package/ts_chat_harness_server/readme.md +91 -0
- package/ts_chat_harness_typedsocket/classes.clienttransport.ts +87 -0
- package/ts_chat_harness_typedsocket/classes.serverbinding.ts +172 -0
- package/ts_chat_harness_typedsocket/index.ts +17 -0
- package/ts_chat_harness_typedsocket/interfaces.requests.ts +40 -0
- package/ts_chat_harness_typedsocket/plugins.ts +28 -0
- package/ts_chat_harness_typedsocket/readme.md +57 -0
- package/ts_ocr/ocr.mistral.ts +102 -49
- package/ts_ocr/readme.md +12 -1
- package/ts_tools/readme.md +10 -0
- package/ts_tools/tool.filesystem.ts +55 -0
- package/ts_tools_node/classes.workspacereversion.ts +255 -0
- package/ts_tools_node/functions.fileimages.ts +89 -0
- package/ts_tools_node/index.ts +7 -1
- package/ts_tools_node/plugins.ts +11 -0
- package/ts_tools_node/readme.md +27 -0
- package/ts_tools_node/tool.context.ts +49 -3
package/ts_ocr/ocr.mistral.ts
CHANGED
|
@@ -14,6 +14,18 @@ export interface ISmartAiOcrImageInput {
|
|
|
14
14
|
mimeType: TSmartAiOcrImageMimeType;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/** Mistral OCR reads PDF, PPTX, DOCX and other document formats. */
|
|
18
|
+
export type TSmartAiOcrDocumentMimeType =
|
|
19
|
+
| 'application/pdf'
|
|
20
|
+
| 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
|
|
21
|
+
| 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
|
22
|
+
| string;
|
|
23
|
+
|
|
24
|
+
export interface ISmartAiOcrDocumentInput {
|
|
25
|
+
dataBase64: string;
|
|
26
|
+
mimeType: TSmartAiOcrDocumentMimeType;
|
|
27
|
+
}
|
|
28
|
+
|
|
17
29
|
export interface ISmartAiOcrPageResult {
|
|
18
30
|
index: number;
|
|
19
31
|
text: string;
|
|
@@ -34,6 +46,14 @@ export interface ISmartAiOcrEngine {
|
|
|
34
46
|
) => Promise<ISmartAiOcrResult<IMistralOcrResponse>>;
|
|
35
47
|
}
|
|
36
48
|
|
|
49
|
+
/** The Mistral OCR engine also reads whole documents, such as a PDF with all its pages. */
|
|
50
|
+
export interface ISmartAiMistralOcrEngine extends ISmartAiOcrEngine {
|
|
51
|
+
recognizeDocument: (
|
|
52
|
+
input: ISmartAiOcrDocumentInput,
|
|
53
|
+
options?: ISmartAiMistralOcrRecognizeOptions
|
|
54
|
+
) => Promise<ISmartAiOcrResult<IMistralOcrResponse>>;
|
|
55
|
+
}
|
|
56
|
+
|
|
37
57
|
export interface IMistralOcrPageConfidenceScores {
|
|
38
58
|
average_page_confidence_score?: number;
|
|
39
59
|
averagePageConfidenceScore?: number;
|
|
@@ -70,8 +90,18 @@ export interface IMistralOcrRequest {
|
|
|
70
90
|
confidence_scores_granularity?: TSmartAiMistralOcrConfidenceScoresGranularity;
|
|
71
91
|
}
|
|
72
92
|
|
|
93
|
+
/** A document (PDF, PPTX, DOCX and other formats) as a URL or a base64 data URL; the other fields are those of an image request. */
|
|
94
|
+
export interface IMistralOcrDocumentRequest extends Omit<IMistralOcrRequest, 'document'> {
|
|
95
|
+
document: {
|
|
96
|
+
type: 'document_url';
|
|
97
|
+
document_url: string;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
73
101
|
export interface ISmartAiMistralOcrTransport {
|
|
74
102
|
process: (request: IMistralOcrRequest) => Promise<IMistralOcrResponse>;
|
|
103
|
+
/** Reads documents. A transport without it reads images only, and `recognizeDocument` refuses documents. */
|
|
104
|
+
processDocument?: (request: IMistralOcrDocumentRequest) => Promise<IMistralOcrResponse>;
|
|
75
105
|
}
|
|
76
106
|
|
|
77
107
|
export interface ISmartAiMistralOcrOptions {
|
|
@@ -101,29 +131,28 @@ const createMistralOcrHttpTransport = (options: {
|
|
|
101
131
|
apiKey?: string;
|
|
102
132
|
endpointUrl?: string;
|
|
103
133
|
}): ISmartAiMistralOcrTransport => {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
});
|
|
134
|
+
const post = async (request: IMistralOcrRequest | IMistralOcrDocumentRequest): Promise<IMistralOcrResponse> => {
|
|
135
|
+
if (!options.apiKey) {
|
|
136
|
+
throw new Error('Mistral OCR requires an apiKey when no custom transport is provided.');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const response = await fetch(options.endpointUrl ?? defaultMistralOcrEndpointUrl, {
|
|
140
|
+
method: 'POST',
|
|
141
|
+
headers: {
|
|
142
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
143
|
+
'Content-Type': 'application/json',
|
|
144
|
+
},
|
|
145
|
+
body: JSON.stringify(request),
|
|
146
|
+
});
|
|
118
147
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
148
|
+
if (!response.ok) {
|
|
149
|
+
const errorBody = await response.text();
|
|
150
|
+
throw new Error(`Mistral OCR request failed with status ${response.status}: ${errorBody}`);
|
|
151
|
+
}
|
|
123
152
|
|
|
124
|
-
|
|
125
|
-
},
|
|
153
|
+
return (await response.json()) as IMistralOcrResponse;
|
|
126
154
|
};
|
|
155
|
+
return { process: post, processDocument: post };
|
|
127
156
|
};
|
|
128
157
|
|
|
129
158
|
const getPageConfidence = (page: IMistralOcrPageResponse): number | undefined => {
|
|
@@ -134,9 +163,30 @@ const getPageConfidence = (page: IMistralOcrPageResponse): number | undefined =>
|
|
|
134
163
|
);
|
|
135
164
|
};
|
|
136
165
|
|
|
166
|
+
const toOcrResult = (response: IMistralOcrResponse): ISmartAiOcrResult<IMistralOcrResponse> => {
|
|
167
|
+
const pages = response.pages.map((page) => ({
|
|
168
|
+
index: page.index,
|
|
169
|
+
text: page.markdown,
|
|
170
|
+
confidence: getPageConfidence(page),
|
|
171
|
+
}));
|
|
172
|
+
const pageConfidences = pages
|
|
173
|
+
.map((page) => page.confidence)
|
|
174
|
+
.filter((confidence): confidence is number => typeof confidence === 'number');
|
|
175
|
+
const confidence = pageConfidences.length
|
|
176
|
+
? pageConfidences.reduce((sum, value) => sum + value, 0) / pageConfidences.length
|
|
177
|
+
: undefined;
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
text: pages.map((page) => page.text).join('\n\n').trim(),
|
|
181
|
+
confidence,
|
|
182
|
+
pages,
|
|
183
|
+
raw: response,
|
|
184
|
+
};
|
|
185
|
+
};
|
|
186
|
+
|
|
137
187
|
export const createMistralOcrEngine = (
|
|
138
188
|
options: ISmartAiMistralOcrOptions = {}
|
|
139
|
-
):
|
|
189
|
+
): ISmartAiMistralOcrEngine => {
|
|
140
190
|
const transport =
|
|
141
191
|
options.transport ??
|
|
142
192
|
createMistralOcrHttpTransport({
|
|
@@ -145,6 +195,16 @@ export const createMistralOcrEngine = (
|
|
|
145
195
|
});
|
|
146
196
|
const model = options.model ?? defaultMistralOcrModel;
|
|
147
197
|
|
|
198
|
+
const settingsOf = (recognizeOptions: ISmartAiMistralOcrRecognizeOptions) => ({
|
|
199
|
+
include_image_base64:
|
|
200
|
+
recognizeOptions.includeImageBase64 ?? options.includeImageBase64 ?? false,
|
|
201
|
+
table_format: recognizeOptions.tableFormat ?? options.tableFormat,
|
|
202
|
+
extract_header: recognizeOptions.extractHeader ?? options.extractHeader,
|
|
203
|
+
extract_footer: recognizeOptions.extractFooter ?? options.extractFooter,
|
|
204
|
+
confidence_scores_granularity:
|
|
205
|
+
recognizeOptions.confidenceScoresGranularity ?? options.confidenceScoresGranularity,
|
|
206
|
+
});
|
|
207
|
+
|
|
148
208
|
return {
|
|
149
209
|
recognizeImage: async (input, recognizeOptions = {}) => {
|
|
150
210
|
if (!input.dataBase64) {
|
|
@@ -153,40 +213,33 @@ export const createMistralOcrEngine = (
|
|
|
153
213
|
if (!input.mimeType) {
|
|
154
214
|
throw new Error('Mistral OCR image input requires mimeType.');
|
|
155
215
|
}
|
|
156
|
-
|
|
157
|
-
const response = await transport.process({
|
|
216
|
+
return toOcrResult(await transport.process({
|
|
158
217
|
model,
|
|
159
218
|
document: {
|
|
160
219
|
type: 'image_url',
|
|
161
220
|
image_url: `data:${input.mimeType};base64,${input.dataBase64}`,
|
|
162
221
|
},
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
222
|
+
...settingsOf(recognizeOptions),
|
|
223
|
+
}));
|
|
224
|
+
},
|
|
225
|
+
recognizeDocument: async (input, recognizeOptions = {}) => {
|
|
226
|
+
if (!transport.processDocument) {
|
|
227
|
+
throw new Error('This Mistral OCR transport reads images only: it has no processDocument, so it cannot read documents.');
|
|
228
|
+
}
|
|
229
|
+
if (!input.dataBase64) {
|
|
230
|
+
throw new Error('Mistral OCR document input requires dataBase64.');
|
|
231
|
+
}
|
|
232
|
+
if (!input.mimeType) {
|
|
233
|
+
throw new Error('Mistral OCR document input requires mimeType.');
|
|
234
|
+
}
|
|
235
|
+
return toOcrResult(await transport.processDocument({
|
|
236
|
+
model,
|
|
237
|
+
document: {
|
|
238
|
+
type: 'document_url',
|
|
239
|
+
document_url: `data:${input.mimeType};base64,${input.dataBase64}`,
|
|
240
|
+
},
|
|
241
|
+
...settingsOf(recognizeOptions),
|
|
176
242
|
}));
|
|
177
|
-
const pageConfidences = pages
|
|
178
|
-
.map((page) => page.confidence)
|
|
179
|
-
.filter((confidence): confidence is number => typeof confidence === 'number');
|
|
180
|
-
const confidence = pageConfidences.length
|
|
181
|
-
? pageConfidences.reduce((sum, value) => sum + value, 0) / pageConfidences.length
|
|
182
|
-
: undefined;
|
|
183
|
-
|
|
184
|
-
return {
|
|
185
|
-
text: pages.map((page) => page.text).join('\n\n').trim(),
|
|
186
|
-
confidence,
|
|
187
|
-
pages,
|
|
188
|
-
raw: response,
|
|
189
|
-
};
|
|
190
243
|
},
|
|
191
244
|
};
|
|
192
245
|
};
|
package/ts_ocr/readme.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Image OCR with Mistral or an injected recognition engine. This subpath imports no runtime dependencies and works with Node buffers and browser byte arrays.
|
|
1
|
+
Image OCR with Mistral or an injected recognition engine, and document OCR with Mistral. This subpath imports no runtime dependencies and works with Node buffers and browser byte arrays.
|
|
2
2
|
|
|
3
3
|
Part of the [FlexHarness toolbox](https://code.foss.global/modelprofile.com/flexharness).
|
|
4
4
|
Install the components your project uses. Provider adapters are registered explicitly;
|
|
@@ -20,6 +20,17 @@ console.log(result.text);
|
|
|
20
20
|
|
|
21
21
|
Constructor and call options accept `engine` or `mistralOptions`. A per-call engine takes precedence over the constructor engine. Otherwise Mistral settings merge, with per-call settings taking precedence. `recognizeOptions` controls each request. `recognizeImage` accepts an existing `{ dataBase64, mimeType }` image; `createMistralOcrEngine` remains available for lower-level integrations.
|
|
22
22
|
|
|
23
|
+
`createMistralOcrEngine()` also reads whole documents: `recognizeDocument({ dataBase64, mimeType: 'application/pdf' })` sends the document as a `document_url` data URL, as Mistral's OCR API takes PDF, PPTX, DOCX and other document formats, and returns every page. Mistral accepts documents of up to 50 MB and 1,000 pages.
|
|
24
|
+
|
|
25
|
+
A custom `transport` receives image requests (`IMistralOcrRequest`) through `process`, as before. It reads documents only when it also implements the optional `processDocument`, which receives an `IMistralOcrDocumentRequest`, whose `document` is a `document_url`; for a transport without it, `recognizeDocument` fails with "This Mistral OCR transport reads images only". The built-in transport implements both.
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { createMistralOcrEngine } from '@modelprofile.com/flexharness/ocr';
|
|
29
|
+
|
|
30
|
+
const engine = createMistralOcrEngine({ apiKey: process.env.MISTRAL_API_KEY });
|
|
31
|
+
const { text, pages } = await engine.recognizeDocument({ dataBase64: pdfBase64, mimeType: 'application/pdf' });
|
|
32
|
+
```
|
|
33
|
+
|
|
23
34
|
Model-based PDF text extraction lives in `@modelprofile.com/flexharness/documents`. Native `ocrmypdf` processing remains in `@push.rocks/smartocr`.
|
|
24
35
|
|
|
25
36
|
## Issue Reporting and Security
|
package/ts_tools/readme.md
CHANGED
|
@@ -11,6 +11,16 @@ pnpm add @modelprofile.com/flexharness
|
|
|
11
11
|
See the [toolbox usage and migration guide](https://code.foss.global/modelprofile.com/flexharness#readme)
|
|
12
12
|
for composition examples and the SmartAI/SmartAgent migration map.
|
|
13
13
|
|
|
14
|
+
## Filesystem tools
|
|
15
|
+
|
|
16
|
+
`createFilesystemTools(context, options)` exposes `read_file`, `write_file`, `list_directory` and, unless `includeDelete: false`, `delete_file` over the context's `fs`. `write_file` and `delete_file` ask `context.requestPermission` (`write`, `delete`) before they change anything.
|
|
17
|
+
|
|
18
|
+
`includeEdit: true` adds `edit_file { path, find, replace, replaceAll? }`. It replaces one exact occurrence of `find`, or every occurrence with `replaceAll`, and refuses a `find` that does not occur or, without `replaceAll`, occurs more than once. The replacement text is literal. It asks for `write` permission before it reads the file, so an edit made while the request waits is kept. It is off by default, so the tools an existing agent sees do not change.
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
const tools = createFilesystemTools(context, { includeEdit: true });
|
|
22
|
+
```
|
|
23
|
+
|
|
14
24
|
## Issue Reporting and Security
|
|
15
25
|
|
|
16
26
|
Report issues and security concerns through [community.foss.global](https://community.foss.global).
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
export interface ICreateFilesystemToolsOptions {
|
|
8
8
|
/** Include delete_file. Default: true. */
|
|
9
9
|
includeDelete?: boolean;
|
|
10
|
+
/** Include edit_file, which replaces an exact snippet of a file. Default: false. */
|
|
11
|
+
includeEdit?: boolean;
|
|
10
12
|
/** Maximum output lines before truncating. */
|
|
11
13
|
maxLines?: number;
|
|
12
14
|
/** Maximum output bytes before truncating. */
|
|
@@ -110,6 +112,59 @@ export function createFilesystemTools(context: IToolExecutionContext, options: I
|
|
|
110
112
|
}),
|
|
111
113
|
};
|
|
112
114
|
|
|
115
|
+
if (options.includeEdit === true) {
|
|
116
|
+
tools.edit_file = plugins.tool({
|
|
117
|
+
description:
|
|
118
|
+
'Replace an exact snippet of a UTF-8 file in the active workspace. `find` must occur exactly once unless `replaceAll` is set; include enough surrounding text to make it unique. Requires host permission when configured.',
|
|
119
|
+
inputSchema: plugins.z.object({
|
|
120
|
+
path: plugins.z.string().describe('File path'),
|
|
121
|
+
find: plugins.z.string().min(1).describe('Exact text to replace, copied from the current file'),
|
|
122
|
+
replace: plugins.z.string().describe('The text to put in its place'),
|
|
123
|
+
replaceAll: plugins.z
|
|
124
|
+
.boolean()
|
|
125
|
+
.optional()
|
|
126
|
+
.describe('Replace every occurrence instead of exactly one. Default: false'),
|
|
127
|
+
}),
|
|
128
|
+
execute: async (
|
|
129
|
+
{
|
|
130
|
+
path: filePath,
|
|
131
|
+
find,
|
|
132
|
+
replace,
|
|
133
|
+
replaceAll,
|
|
134
|
+
}: { path: string; find: string; replace: string; replaceAll?: boolean },
|
|
135
|
+
invocationOptions?: plugins.ToolExecutionOptions<unknown>,
|
|
136
|
+
) => {
|
|
137
|
+
if (!context.fs) {
|
|
138
|
+
throw new Error('Filesystem tools are not available in this execution context.');
|
|
139
|
+
}
|
|
140
|
+
const abortSignal = invocationOptions?.abortSignal ?? context.abortSignal;
|
|
141
|
+
const invocation = { toolCallId: invocationOptions?.toolCallId, abortSignal };
|
|
142
|
+
// asked before the file is read: an edit made while the request waits is kept, not overwritten
|
|
143
|
+
await context.requestPermission?.({
|
|
144
|
+
type: 'write',
|
|
145
|
+
title: 'Edit file',
|
|
146
|
+
metadata: { path: filePath },
|
|
147
|
+
toolCallId: invocationOptions?.toolCallId,
|
|
148
|
+
abortSignal,
|
|
149
|
+
});
|
|
150
|
+
const content = await context.fs.readFile(filePath, undefined, invocation);
|
|
151
|
+
const occurrences = content.split(find).length - 1;
|
|
152
|
+
if (occurrences === 0) {
|
|
153
|
+
throw new Error(`The text to replace does not occur in ${filePath}.`);
|
|
154
|
+
}
|
|
155
|
+
if (occurrences > 1 && replaceAll !== true) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`The text to replace occurs ${occurrences} times in ${filePath}; include more surrounding text or set replaceAll.`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
const updated = replaceAll === true ? content.split(find).join(replace) : content.replace(find, () => replace);
|
|
161
|
+
await context.fs.writeFile(filePath, updated, invocation);
|
|
162
|
+
const replaced = replaceAll === true ? occurrences : 1;
|
|
163
|
+
return truncate(`Replaced ${replaced} occurrence${replaced === 1 ? '' : 's'} in ${filePath}`);
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
113
168
|
if (options.includeDelete !== false) {
|
|
114
169
|
tools.delete_file = plugins.tool({
|
|
115
170
|
description: 'Delete a file or empty directory in the active workspace. Requires host permission when configured.',
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import type { IToolFileChange, IToolFileChangeObserver } from './tool.context.js';
|
|
3
|
+
import {
|
|
4
|
+
fileImageBytes,
|
|
5
|
+
fileImagesEqual,
|
|
6
|
+
missingAncestors,
|
|
7
|
+
readFileImage,
|
|
8
|
+
removeEmptyDirectories,
|
|
9
|
+
writeFileImage,
|
|
10
|
+
type TFileImage,
|
|
11
|
+
} from './functions.fileimages.js';
|
|
12
|
+
|
|
13
|
+
export interface IInMemoryWorkspaceReversionOptions {
|
|
14
|
+
/** The workspace a revertible turn names in the session's reversion info. */
|
|
15
|
+
workspace: plugins.IFlexAffectedWorkspace;
|
|
16
|
+
/**
|
|
17
|
+
* The bytes of file images one turn may hold. A turn that changes more is recorded as
|
|
18
|
+
* nonrevertible (`capture-too-large`) and its images are dropped. Default: 64 MiB.
|
|
19
|
+
*/
|
|
20
|
+
maxCaptureBytes?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** One file a turn changed: what it held before the turn's first change and after the turn. */
|
|
24
|
+
interface IFileRecord {
|
|
25
|
+
before: TFileImage;
|
|
26
|
+
after?: TFileImage;
|
|
27
|
+
/** directories the turn's first write created, deepest first */
|
|
28
|
+
createdDirectories: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ICapture {
|
|
32
|
+
captureId: string;
|
|
33
|
+
runId: string;
|
|
34
|
+
state: 'prepared' | 'finalized';
|
|
35
|
+
files: Map<string, IFileRecord>;
|
|
36
|
+
bytes: number;
|
|
37
|
+
nonrevertibleReason?: string;
|
|
38
|
+
outcome?: plugins.TFlexTurnReversionFinalizedOutcome;
|
|
39
|
+
appliedOperations: Set<string>;
|
|
40
|
+
/** apply operations that failed after a partial change could not be rolled back */
|
|
41
|
+
uncertainOperations: Set<string>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface ICaptureReference {
|
|
45
|
+
captureId: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const DEFAULT_MAX_CAPTURE_BYTES = 64 * 1024 * 1024;
|
|
49
|
+
|
|
50
|
+
const isCaptureReference = (valueArg: plugins.TJsonValue): valueArg is plugins.TJsonValue & ICaptureReference =>
|
|
51
|
+
!!valueArg
|
|
52
|
+
&& typeof valueArg === 'object'
|
|
53
|
+
&& !Array.isArray(valueArg)
|
|
54
|
+
&& typeof valueArg.captureId === 'string';
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Turn reversion for the local filesystem tools, held in memory: FlexHarness `/undo` and `/redo`
|
|
58
|
+
* move a turn's file changes together with its transcript.
|
|
59
|
+
*
|
|
60
|
+
* Give each run's `createLocalToolExecutionContext` the observer `observerFor(context.runId)`. The
|
|
61
|
+
* first change of a file in a turn records what the file held before; `finalize` records what it
|
|
62
|
+
* holds after. Undo puts the before images back and redo the after images, each only while every
|
|
63
|
+
* file still holds what the other direction left, so a later edit of the user is never overwritten.
|
|
64
|
+
* A turn that ran a shell command is nonrevertible, since the command may have changed files no
|
|
65
|
+
* observer saw.
|
|
66
|
+
*
|
|
67
|
+
* Captures live as long as the process. A capture FlexHarness asks about after a restart is
|
|
68
|
+
* `missing`, and undoing past it fails without changing anything. Only root-session runs are
|
|
69
|
+
* captured; a change of a run without a prepared capture is refused.
|
|
70
|
+
*/
|
|
71
|
+
export class InMemoryWorkspaceReversion<TScope = unknown> implements plugins.IFlexTurnReversionProviderV2<TScope> {
|
|
72
|
+
public readonly protocolVersion = 2;
|
|
73
|
+
|
|
74
|
+
private readonly workspace: plugins.IFlexAffectedWorkspace;
|
|
75
|
+
private readonly maxCaptureBytes: number;
|
|
76
|
+
private readonly captures = new Map<string, ICapture>();
|
|
77
|
+
private readonly captureIdsByRun = new Map<string, string>();
|
|
78
|
+
|
|
79
|
+
constructor(optionsArg: IInMemoryWorkspaceReversionOptions) {
|
|
80
|
+
this.workspace = { id: optionsArg.workspace.id, label: optionsArg.workspace.label };
|
|
81
|
+
this.maxCaptureBytes = optionsArg.maxCaptureBytes ?? DEFAULT_MAX_CAPTURE_BYTES;
|
|
82
|
+
if (!Number.isSafeInteger(this.maxCaptureBytes) || this.maxCaptureBytes <= 0) {
|
|
83
|
+
throw new Error('maxCaptureBytes must be a positive integer.');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The observer of one run's file changes and shell commands. */
|
|
88
|
+
public observerFor(runIdArg: string): IToolFileChangeObserver {
|
|
89
|
+
return {
|
|
90
|
+
beforeFileChange: (changeArg) => this.recordChange(runIdArg, changeArg),
|
|
91
|
+
beforeShellCommand: () => {
|
|
92
|
+
this.markNonrevertible(this.requirePreparedCapture(runIdArg), 'shell-command');
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public prepare(contextArg: plugins.IFlexTurnReversionBaseContext<TScope>): void {
|
|
98
|
+
contextArg.signal.throwIfAborted();
|
|
99
|
+
if (this.captures.has(contextArg.captureId)) return;
|
|
100
|
+
this.captures.set(contextArg.captureId, {
|
|
101
|
+
captureId: contextArg.captureId,
|
|
102
|
+
runId: contextArg.runId,
|
|
103
|
+
state: 'prepared',
|
|
104
|
+
files: new Map(),
|
|
105
|
+
bytes: 0,
|
|
106
|
+
appliedOperations: new Set(),
|
|
107
|
+
uncertainOperations: new Set(),
|
|
108
|
+
});
|
|
109
|
+
this.captureIdsByRun.set(contextArg.runId, contextArg.captureId);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
public inspectCapture(
|
|
113
|
+
contextArg: plugins.IFlexTurnReversionBaseContext<TScope>,
|
|
114
|
+
): plugins.TFlexTurnReversionCaptureInspectionV2 {
|
|
115
|
+
const capture = this.captures.get(contextArg.captureId);
|
|
116
|
+
if (!capture) return { status: 'missing' };
|
|
117
|
+
return capture.outcome ? { status: 'finalized', outcome: capture.outcome } : { status: 'prepared' };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
public async finalize(
|
|
121
|
+
contextArg: plugins.IFlexTurnReversionBaseContext<TScope>,
|
|
122
|
+
): Promise<plugins.TFlexTurnReversionFinalizedOutcome> {
|
|
123
|
+
contextArg.signal.throwIfAborted();
|
|
124
|
+
const capture = this.requireCapture(contextArg.captureId);
|
|
125
|
+
if (capture.outcome) return capture.outcome;
|
|
126
|
+
const reference: ICaptureReference = { captureId: capture.captureId };
|
|
127
|
+
if (!capture.nonrevertibleReason) {
|
|
128
|
+
for (const [filePath, record] of capture.files) {
|
|
129
|
+
record.after = await readFileImage(filePath);
|
|
130
|
+
if (fileImagesEqual(record.before, record.after)) capture.files.delete(filePath);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
capture.state = 'finalized';
|
|
134
|
+
if (capture.nonrevertibleReason) {
|
|
135
|
+
capture.outcome = {
|
|
136
|
+
disposition: 'nonrevertible',
|
|
137
|
+
reference: { ...reference },
|
|
138
|
+
reasonCode: capture.nonrevertibleReason,
|
|
139
|
+
affectedWorkspaces: [this.workspace],
|
|
140
|
+
};
|
|
141
|
+
} else if (capture.files.size === 0) {
|
|
142
|
+
capture.outcome = { disposition: 'no-change', reference: { ...reference } };
|
|
143
|
+
} else {
|
|
144
|
+
capture.outcome = { disposition: 'revertible', reference: { ...reference }, affectedWorkspaces: [this.workspace] };
|
|
145
|
+
}
|
|
146
|
+
this.captureIdsByRun.delete(capture.runId);
|
|
147
|
+
return capture.outcome;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
public async apply(contextArg: plugins.IFlexTurnReversionApplyContext<TScope>): Promise<void> {
|
|
151
|
+
contextArg.signal.throwIfAborted();
|
|
152
|
+
const capture = this.captureOf(contextArg.reference);
|
|
153
|
+
if (capture.outcome?.disposition !== 'revertible') {
|
|
154
|
+
throw new Error('This turn has no revertible workspace changes.');
|
|
155
|
+
}
|
|
156
|
+
const records = [...capture.files.entries()];
|
|
157
|
+
const expected = (recordArg: IFileRecord) => (contextArg.direction === 'undo' ? recordArg.after! : recordArg.before);
|
|
158
|
+
const target = (recordArg: IFileRecord) => (contextArg.direction === 'undo' ? recordArg.before : recordArg.after!);
|
|
159
|
+
for (const [filePath, record] of records) {
|
|
160
|
+
if (!fileImagesEqual(await readFileImage(filePath), expected(record))) {
|
|
161
|
+
throw new Error(`"${filePath}" changed after the turn; its changes were not ${contextArg.direction === 'undo' ? 'undone' : 'redone'}.`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const written: Array<[string, IFileRecord]> = [];
|
|
165
|
+
try {
|
|
166
|
+
for (const entry of records) {
|
|
167
|
+
await writeFileImage(entry[0], target(entry[1]));
|
|
168
|
+
written.push(entry);
|
|
169
|
+
}
|
|
170
|
+
} catch (error) {
|
|
171
|
+
await this.rollBack(written, expected, contextArg.operationId, capture);
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
if (contextArg.direction === 'undo') {
|
|
175
|
+
for (const [, record] of records) await removeEmptyDirectories(record.createdDirectories);
|
|
176
|
+
}
|
|
177
|
+
capture.appliedOperations.add(contextArg.operationId);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
public inspectApply(
|
|
181
|
+
contextArg: plugins.IFlexTurnReversionApplyContext<TScope>,
|
|
182
|
+
): plugins.IFlexTurnReversionApplyInspection {
|
|
183
|
+
const capture = this.captures.get(isCaptureReference(contextArg.reference) ? contextArg.reference.captureId : '');
|
|
184
|
+
if (!capture) return { status: 'not-applied' };
|
|
185
|
+
if (capture.appliedOperations.has(contextArg.operationId)) return { status: 'applied' };
|
|
186
|
+
if (capture.uncertainOperations.has(contextArg.operationId)) return { status: 'unknown' };
|
|
187
|
+
return { status: 'not-applied' };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
public release(contextArg: plugins.IFlexTurnReversionReleaseContext<TScope>): void {
|
|
191
|
+
const captureId = isCaptureReference(contextArg.reference) ? contextArg.reference.captureId : contextArg.captureId;
|
|
192
|
+
const capture = this.captures.get(captureId);
|
|
193
|
+
if (!capture) return;
|
|
194
|
+
this.captures.delete(captureId);
|
|
195
|
+
if (this.captureIdsByRun.get(capture.runId) === captureId) this.captureIdsByRun.delete(capture.runId);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private async recordChange(runIdArg: string, changeArg: IToolFileChange): Promise<void> {
|
|
199
|
+
const capture = this.requirePreparedCapture(runIdArg);
|
|
200
|
+
if (capture.nonrevertibleReason || capture.files.has(changeArg.path)) return;
|
|
201
|
+
const before = await readFileImage(changeArg.path);
|
|
202
|
+
const createdDirectories = before.kind === 'absent' && changeArg.kind === 'write'
|
|
203
|
+
? await missingAncestors(changeArg.path)
|
|
204
|
+
: [];
|
|
205
|
+
if (capture.files.has(changeArg.path)) return;
|
|
206
|
+
capture.bytes += fileImageBytes(before);
|
|
207
|
+
if (capture.bytes > this.maxCaptureBytes) {
|
|
208
|
+
this.markNonrevertible(capture, 'capture-too-large');
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
capture.files.set(changeArg.path, { before, createdDirectories });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private markNonrevertible(captureArg: ICapture, reasonArg: string): void {
|
|
215
|
+
captureArg.nonrevertibleReason ??= reasonArg;
|
|
216
|
+
captureArg.files.clear();
|
|
217
|
+
captureArg.bytes = 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private async rollBack(
|
|
221
|
+
writtenArg: Array<[string, IFileRecord]>,
|
|
222
|
+
expectedArg: (recordArg: IFileRecord) => TFileImage,
|
|
223
|
+
operationIdArg: string,
|
|
224
|
+
captureArg: ICapture,
|
|
225
|
+
): Promise<void> {
|
|
226
|
+
try {
|
|
227
|
+
for (const [filePath, record] of writtenArg.reverse()) await writeFileImage(filePath, expectedArg(record));
|
|
228
|
+
} catch {
|
|
229
|
+
// the workspace holds part of the change; FlexHarness learns that the outcome is unknown
|
|
230
|
+
captureArg.uncertainOperations.add(operationIdArg);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private requirePreparedCapture(runIdArg: string): ICapture {
|
|
235
|
+
const captureId = this.captureIdsByRun.get(runIdArg);
|
|
236
|
+
const capture = captureId ? this.captures.get(captureId) : undefined;
|
|
237
|
+
if (!capture || capture.state !== 'prepared') {
|
|
238
|
+
throw new Error(`No workspace capture is open for run "${runIdArg}"; the change was refused.`);
|
|
239
|
+
}
|
|
240
|
+
return capture;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private requireCapture(captureIdArg: string): ICapture {
|
|
244
|
+
const capture = this.captures.get(captureIdArg);
|
|
245
|
+
if (!capture) throw new Error(`The workspace capture "${captureIdArg}" is not held.`);
|
|
246
|
+
return capture;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private captureOf(referenceArg: plugins.TJsonValue): ICapture {
|
|
250
|
+
if (!isCaptureReference(referenceArg)) throw new Error('The workspace capture reference is malformed.');
|
|
251
|
+
const capture = this.captures.get(referenceArg.captureId);
|
|
252
|
+
if (!capture) throw new Error('The workspace changes of this turn are no longer held in memory.');
|
|
253
|
+
return capture;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
|
|
3
|
+
/** What one path holds at a moment: nothing, a regular file, or a symbolic link. */
|
|
4
|
+
export type TFileImage =
|
|
5
|
+
| { kind: 'absent' }
|
|
6
|
+
| { kind: 'file'; bytes: Buffer; mode: number }
|
|
7
|
+
| { kind: 'symlink'; target: string };
|
|
8
|
+
|
|
9
|
+
const isMissing = (error: unknown): boolean => {
|
|
10
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
11
|
+
return code === 'ENOENT' || code === 'ENOTDIR';
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Reads what the path holds, without following a final symbolic link. */
|
|
15
|
+
export const readFileImage = async (pathArg: string): Promise<TFileImage> => {
|
|
16
|
+
let stats: plugins.fs.Stats;
|
|
17
|
+
try {
|
|
18
|
+
stats = await plugins.fs.promises.lstat(pathArg);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
if (isMissing(error)) return { kind: 'absent' };
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
if (stats.isSymbolicLink()) return { kind: 'symlink', target: await plugins.fs.promises.readlink(pathArg) };
|
|
24
|
+
if (stats.isFile()) return { kind: 'file', bytes: await plugins.fs.promises.readFile(pathArg), mode: stats.mode & 0o7777 };
|
|
25
|
+
throw new Error(`"${pathArg}" is neither a file nor a symbolic link.`);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** The bytes an image keeps in memory. */
|
|
29
|
+
export const fileImageBytes = (imageArg: TFileImage): number =>
|
|
30
|
+
imageArg.kind === 'file'
|
|
31
|
+
? imageArg.bytes.byteLength
|
|
32
|
+
: imageArg.kind === 'symlink'
|
|
33
|
+
? Buffer.byteLength(imageArg.target)
|
|
34
|
+
: 0;
|
|
35
|
+
|
|
36
|
+
export const fileImagesEqual = (leftArg: TFileImage, rightArg: TFileImage): boolean => {
|
|
37
|
+
if (leftArg.kind === 'absent' || rightArg.kind === 'absent') return leftArg.kind === rightArg.kind;
|
|
38
|
+
if (leftArg.kind === 'symlink' || rightArg.kind === 'symlink') {
|
|
39
|
+
return leftArg.kind === 'symlink' && rightArg.kind === 'symlink' && leftArg.target === rightArg.target;
|
|
40
|
+
}
|
|
41
|
+
return leftArg.bytes.equals(rightArg.bytes);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** Puts the image at the path, creating parent directories as needed. */
|
|
45
|
+
export const writeFileImage = async (pathArg: string, imageArg: TFileImage): Promise<void> => {
|
|
46
|
+
if (imageArg.kind === 'absent') {
|
|
47
|
+
await plugins.fs.promises.rm(pathArg, { force: true });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
await plugins.fs.promises.mkdir(plugins.path.dirname(pathArg), { recursive: true });
|
|
51
|
+
if (imageArg.kind === 'symlink') {
|
|
52
|
+
await plugins.fs.promises.rm(pathArg, { force: true });
|
|
53
|
+
await plugins.fs.promises.symlink(imageArg.target, pathArg);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if ((await readFileImage(pathArg)).kind === 'symlink') await plugins.fs.promises.rm(pathArg);
|
|
57
|
+
await plugins.fs.promises.writeFile(pathArg, imageArg.bytes);
|
|
58
|
+
await plugins.fs.promises.chmod(pathArg, imageArg.mode);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** The ancestors of the path that do not exist yet, deepest first: a write would create them. */
|
|
62
|
+
export const missingAncestors = async (pathArg: string): Promise<string[]> => {
|
|
63
|
+
const missing: string[] = [];
|
|
64
|
+
let current = plugins.path.dirname(pathArg);
|
|
65
|
+
while (current !== plugins.path.dirname(current)) {
|
|
66
|
+
try {
|
|
67
|
+
await plugins.fs.promises.lstat(current);
|
|
68
|
+
break;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (!isMissing(error)) throw error;
|
|
71
|
+
missing.push(current);
|
|
72
|
+
current = plugins.path.dirname(current);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return missing;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** Removes the directories, deepest first, as far as they are empty. */
|
|
79
|
+
export const removeEmptyDirectories = async (directoriesArg: readonly string[]): Promise<void> => {
|
|
80
|
+
for (const directory of directoriesArg) {
|
|
81
|
+
try {
|
|
82
|
+
await plugins.fs.promises.rmdir(directory);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
85
|
+
if (code === 'ENOTEMPTY' || code === 'EEXIST') return;
|
|
86
|
+
if (!isMissing(error)) throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
package/ts_tools_node/index.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
export { createLocalToolExecutionContext } from './tool.context.js';
|
|
2
|
-
export type {
|
|
2
|
+
export type {
|
|
3
|
+
ILocalToolExecutionContextOptions,
|
|
4
|
+
IToolFileChange,
|
|
5
|
+
IToolFileChangeObserver,
|
|
6
|
+
} from './tool.context.js';
|
|
7
|
+
export { InMemoryWorkspaceReversion } from './classes.workspacereversion.js';
|
|
8
|
+
export type { IInMemoryWorkspaceReversionOptions } from './classes.workspacereversion.js';
|
|
3
9
|
export { FileToolJobStore } from './tool.persistence.js';
|
|
4
10
|
export type { IFileToolJobStoreOptions } from './tool.persistence.js';
|
|
5
11
|
export * from './tools.local.js';
|