@modelprofile.com/flexharness 6.1.0 → 7.0.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_chat/classes.chatsession.d.ts +37 -0
- package/dist_ts_chat/classes.chatsession.js +102 -0
- package/dist_ts_chat/index.d.ts +2 -0
- package/dist_ts_chat/index.js +2 -0
- package/dist_ts_chat/interfaces.d.ts +72 -0
- package/dist_ts_chat/interfaces.js +2 -0
- package/dist_ts_chat/plugins.d.ts +4 -0
- package/dist_ts_chat/plugins.js +4 -0
- package/dist_ts_chat_cli/components.chatapp.d.ts +10 -0
- package/dist_ts_chat_cli/components.chatapp.js +75 -0
- package/dist_ts_chat_cli/components.inputarea.d.ts +7 -0
- package/dist_ts_chat_cli/components.inputarea.js +18 -0
- package/dist_ts_chat_cli/components.message.d.ts +12 -0
- package/dist_ts_chat_cli/components.message.js +21 -0
- package/dist_ts_chat_cli/components.messagelist.d.ts +9 -0
- package/dist_ts_chat_cli/components.messagelist.js +19 -0
- package/dist_ts_chat_cli/components.statusbar.d.ts +9 -0
- package/dist_ts_chat_cli/components.statusbar.js +15 -0
- package/dist_ts_chat_cli/index.d.ts +2 -0
- package/dist_ts_chat_cli/index.js +2 -0
- package/dist_ts_chat_cli/plugins.d.ts +12 -0
- package/dist_ts_chat_cli/plugins.js +14 -0
- package/dist_ts_chat_cli/startchat.d.ts +18 -0
- package/dist_ts_chat_cli/startchat.js +17 -0
- package/dist_ts_chat_web/flexchat-input.d.ts +21 -0
- package/dist_ts_chat_web/flexchat-input.js +172 -0
- package/dist_ts_chat_web/flexchat-message.d.ts +26 -0
- package/dist_ts_chat_web/flexchat-message.js +174 -0
- package/dist_ts_chat_web/flexchat-window.d.ts +34 -0
- package/dist_ts_chat_web/flexchat-window.js +394 -0
- package/dist_ts_chat_web/index.d.ts +3 -0
- package/dist_ts_chat_web/index.js +4 -0
- package/dist_ts_chat_web/plugins.d.ts +6 -0
- package/dist_ts_chat_web/plugins.js +4 -0
- package/dist_ts_compaction/index.d.ts +2 -0
- package/dist_ts_compaction/index.js +2 -0
- package/dist_ts_compaction/plugins.d.ts +4 -0
- package/dist_ts_compaction/plugins.js +3 -0
- package/dist_ts_compaction/smartagent.compaction.d.ts +14 -0
- package/dist_ts_compaction/smartagent.compaction.js +49 -0
- package/dist_ts_document/index.d.ts +19 -0
- package/dist_ts_document/index.js +55 -0
- package/dist_ts_document/plugins.d.ts +3 -0
- package/dist_ts_document/plugins.js +4 -0
- package/dist_ts_kvm/classes.browserkvm.d.ts +22 -0
- package/dist_ts_kvm/classes.browserkvm.js +271 -0
- package/dist_ts_kvm/classes.kvmterminal.d.ts +8 -0
- package/dist_ts_kvm/classes.kvmterminal.js +88 -0
- package/dist_ts_kvm/commandwrappers.d.ts +14 -0
- package/dist_ts_kvm/commandwrappers.js +58 -0
- package/dist_ts_kvm/index.d.ts +6 -0
- package/dist_ts_kvm/index.js +7 -0
- package/dist_ts_kvm/interfaces.d.ts +106 -0
- package/dist_ts_kvm/interfaces.js +2 -0
- package/dist_ts_kvm/ocr.d.ts +14 -0
- package/dist_ts_kvm/ocr.js +23 -0
- package/dist_ts_kvm/plugins.d.ts +6 -0
- package/dist_ts_kvm/plugins.js +10 -0
- package/dist_ts_kvm/tools.d.ts +15 -0
- package/dist_ts_kvm/tools.js +40 -0
- package/dist_ts_mcp/index.d.ts +86 -0
- package/dist_ts_mcp/index.js +284 -0
- package/dist_ts_mcp/requestcontext.d.ts +11 -0
- package/dist_ts_mcp/requestcontext.js +42 -0
- package/dist_ts_media/index.d.ts +8 -0
- package/dist_ts_media/index.js +21 -0
- package/dist_ts_media/plugins.d.ts +2 -0
- package/dist_ts_media/plugins.js +3 -0
- package/dist_ts_ocr/classes.imageocr.d.ts +19 -0
- package/dist_ts_ocr/classes.imageocr.js +27 -0
- package/dist_ts_ocr/index.d.ts +2 -0
- package/dist_ts_ocr/index.js +3 -0
- package/dist_ts_ocr/ocr.mistral.d.ts +75 -0
- package/dist_ts_ocr/ocr.mistral.js +77 -0
- package/dist_ts_tools/index.d.ts +17 -0
- package/dist_ts_tools/index.js +10 -0
- package/dist_ts_tools/plugins.d.ts +7 -0
- package/dist_ts_tools/plugins.js +6 -0
- package/dist_ts_tools/tool.browser.d.ts +11 -0
- package/dist_ts_tools/tool.browser.js +128 -0
- package/dist_ts_tools/tool.context.d.ts +4 -0
- package/dist_ts_tools/tool.context.js +23 -0
- package/dist_ts_tools/tool.filesystem.d.ts +11 -0
- package/dist_ts_tools/tool.filesystem.js +106 -0
- package/dist_ts_tools/tool.http.d.ts +2 -0
- package/dist_ts_tools/tool.http.js +65 -0
- package/dist_ts_tools/tool.json.d.ts +2 -0
- package/dist_ts_tools/tool.json.js +47 -0
- package/dist_ts_tools/tool.shell.d.ts +15 -0
- package/dist_ts_tools/tool.shell.js +329 -0
- package/dist_ts_tools/utils.truncation.d.ts +10 -0
- package/dist_ts_tools/utils.truncation.js +26 -0
- package/dist_ts_tools_node/index.d.ts +5 -0
- package/dist_ts_tools_node/index.js +4 -0
- package/dist_ts_tools_node/plugins.d.ts +13 -0
- package/dist_ts_tools_node/plugins.js +13 -0
- package/dist_ts_tools_node/tool.context.d.ts +31 -0
- package/dist_ts_tools_node/tool.context.js +837 -0
- package/dist_ts_tools_node/tool.persistence.d.ts +15 -0
- package/dist_ts_tools_node/tool.persistence.js +129 -0
- package/dist_ts_tools_node/tools.local.d.ts +11 -0
- package/dist_ts_tools_node/tools.local.js +15 -0
- package/package.json +112 -4
- package/readme.md +122 -66
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/readme.md +122 -66
- package/ts_chat/classes.chatsession.ts +116 -0
- package/ts_chat/index.ts +8 -0
- package/ts_chat/interfaces.ts +64 -0
- package/ts_chat/plugins.ts +7 -0
- package/ts_chat/readme.md +49 -0
- package/ts_chat_cli/components.chatapp.ts +103 -0
- package/ts_chat_cli/components.inputarea.ts +29 -0
- package/ts_chat_cli/components.message.ts +48 -0
- package/ts_chat_cli/components.messagelist.ts +45 -0
- package/ts_chat_cli/components.statusbar.ts +31 -0
- package/ts_chat_cli/index.ts +2 -0
- package/ts_chat_cli/plugins.ts +21 -0
- package/ts_chat_cli/readme.md +40 -0
- package/ts_chat_cli/startchat.ts +34 -0
- package/ts_chat_web/flexchat-input.ts +184 -0
- package/ts_chat_web/flexchat-message.ts +185 -0
- package/ts_chat_web/flexchat-window.ts +416 -0
- package/ts_chat_web/index.ts +3 -0
- package/ts_chat_web/plugins.ts +9 -0
- package/ts_chat_web/readme.md +46 -0
- package/ts_compaction/index.ts +2 -0
- package/ts_compaction/plugins.ts +6 -0
- package/ts_compaction/readme.md +37 -0
- package/ts_compaction/smartagent.compaction.ts +59 -0
- package/ts_document/index.ts +82 -0
- package/ts_document/plugins.ts +3 -0
- package/ts_document/readme.md +49 -0
- package/ts_kvm/classes.browserkvm.ts +335 -0
- package/ts_kvm/classes.kvmterminal.ts +103 -0
- package/ts_kvm/commandwrappers.ts +83 -0
- package/ts_kvm/index.ts +6 -0
- package/ts_kvm/interfaces.ts +152 -0
- package/ts_kvm/ocr.ts +41 -0
- package/ts_kvm/plugins.ts +13 -0
- package/ts_kvm/readme.md +482 -0
- package/ts_kvm/tools.ts +58 -0
- package/ts_mcp/index.ts +470 -0
- package/ts_mcp/readme.md +37 -0
- package/ts_mcp/requestcontext.ts +66 -0
- package/ts_media/index.ts +29 -0
- package/ts_media/plugins.ts +2 -0
- package/ts_media/readme.md +37 -0
- package/ts_ocr/classes.imageocr.ts +53 -0
- package/ts_ocr/index.ts +2 -0
- package/ts_ocr/ocr.mistral.ts +192 -0
- package/ts_ocr/readme.md +48 -0
- package/ts_tools/index.ts +49 -0
- package/ts_tools/plugins.ts +7 -0
- package/ts_tools/readme.md +37 -0
- package/ts_tools/tool.browser.ts +159 -0
- package/ts_tools/tool.context.ts +23 -0
- package/ts_tools/tool.filesystem.ts +144 -0
- package/ts_tools/tool.http.ts +78 -0
- package/ts_tools/tool.json.ts +53 -0
- package/ts_tools/tool.shell.ts +418 -0
- package/ts_tools/utils.truncation.ts +39 -0
- package/ts_tools_node/index.ts +5 -0
- package/ts_tools_node/plugins.ts +16 -0
- package/ts_tools_node/readme.md +37 -0
- package/ts_tools_node/tool.context.ts +991 -0
- package/ts_tools_node/tool.persistence.ts +146 -0
- package/ts_tools_node/tools.local.ts +26 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import {
|
|
3
|
+
formatToolOutput,
|
|
4
|
+
type IToolExecutionContext,
|
|
5
|
+
} from './tool.context.js';
|
|
6
|
+
|
|
7
|
+
export interface ICreateFilesystemToolsOptions {
|
|
8
|
+
/** Include delete_file. Default: true. */
|
|
9
|
+
includeDelete?: boolean;
|
|
10
|
+
/** Maximum output lines before truncating. */
|
|
11
|
+
maxLines?: number;
|
|
12
|
+
/** Maximum output bytes before truncating. */
|
|
13
|
+
maxBytes?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createFilesystemTools(context: IToolExecutionContext, options: ICreateFilesystemToolsOptions = {}): plugins.ToolSet {
|
|
17
|
+
const truncate = (output: unknown) => plugins.truncateOutput(formatToolOutput(output), {
|
|
18
|
+
maxLines: options.maxLines,
|
|
19
|
+
maxBytes: options.maxBytes,
|
|
20
|
+
}).content;
|
|
21
|
+
const tools: plugins.ToolSet = {
|
|
22
|
+
read_file: plugins.tool({
|
|
23
|
+
description:
|
|
24
|
+
'Read a UTF-8 file in the active workspace. Paths may be absolute or relative to the workspace root.',
|
|
25
|
+
inputSchema: plugins.z.object({
|
|
26
|
+
path: plugins.z.string().describe('File path'),
|
|
27
|
+
startLine: plugins.z
|
|
28
|
+
.number()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe('First line (1-indexed, inclusive)'),
|
|
31
|
+
endLine: plugins.z
|
|
32
|
+
.number()
|
|
33
|
+
.optional()
|
|
34
|
+
.describe('Last line (1-indexed, inclusive)'),
|
|
35
|
+
}),
|
|
36
|
+
execute: async (
|
|
37
|
+
{
|
|
38
|
+
path: filePath,
|
|
39
|
+
startLine,
|
|
40
|
+
endLine,
|
|
41
|
+
}: {
|
|
42
|
+
path: string;
|
|
43
|
+
startLine?: number;
|
|
44
|
+
endLine?: number;
|
|
45
|
+
},
|
|
46
|
+
invocationOptions?: plugins.ToolExecutionOptions<unknown>,
|
|
47
|
+
) => {
|
|
48
|
+
if (!context.fs) {
|
|
49
|
+
throw new Error('Filesystem tools are not available in this execution context.');
|
|
50
|
+
}
|
|
51
|
+
return truncate(await context.fs.readFile(filePath, { startLine, endLine }, {
|
|
52
|
+
toolCallId: invocationOptions?.toolCallId,
|
|
53
|
+
abortSignal: invocationOptions?.abortSignal ?? context.abortSignal,
|
|
54
|
+
}));
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
|
|
58
|
+
write_file: plugins.tool({
|
|
59
|
+
description:
|
|
60
|
+
'Write UTF-8 content to a file in the active workspace. Creates parent directories and overwrites existing content. Requires host permission when configured.',
|
|
61
|
+
inputSchema: plugins.z.object({
|
|
62
|
+
path: plugins.z.string().describe('File path'),
|
|
63
|
+
content: plugins.z.string().describe('Complete file content to write'),
|
|
64
|
+
}),
|
|
65
|
+
execute: async (
|
|
66
|
+
{ path: filePath, content }: { path: string; content: string },
|
|
67
|
+
invocationOptions?: plugins.ToolExecutionOptions<unknown>,
|
|
68
|
+
) => {
|
|
69
|
+
if (!context.fs) {
|
|
70
|
+
throw new Error('Filesystem tools are not available in this execution context.');
|
|
71
|
+
}
|
|
72
|
+
const abortSignal = invocationOptions?.abortSignal ?? context.abortSignal;
|
|
73
|
+
await context.requestPermission?.({
|
|
74
|
+
type: 'write',
|
|
75
|
+
title: 'Write file',
|
|
76
|
+
metadata: { path: filePath, bytes: Buffer.byteLength(content, 'utf8') },
|
|
77
|
+
toolCallId: invocationOptions?.toolCallId,
|
|
78
|
+
abortSignal,
|
|
79
|
+
});
|
|
80
|
+
const result = await context.fs.writeFile(filePath, content, {
|
|
81
|
+
toolCallId: invocationOptions?.toolCallId,
|
|
82
|
+
abortSignal,
|
|
83
|
+
});
|
|
84
|
+
return truncate(result ?? `Written ${Buffer.byteLength(content, 'utf8')} bytes to ${filePath}`);
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
|
|
88
|
+
list_directory: plugins.tool({
|
|
89
|
+
description: 'List files and directories in the active workspace. Paths may be absolute or relative to the workspace root.',
|
|
90
|
+
inputSchema: plugins.z.object({
|
|
91
|
+
path: plugins.z.string().default('.').describe('Directory path to list'),
|
|
92
|
+
recursive: plugins.z
|
|
93
|
+
.boolean()
|
|
94
|
+
.optional()
|
|
95
|
+
.describe('List recursively. Default: false'),
|
|
96
|
+
}),
|
|
97
|
+
execute: async (
|
|
98
|
+
{ path: directoryPath, recursive }: { path: string; recursive?: boolean },
|
|
99
|
+
invocationOptions?: plugins.ToolExecutionOptions<unknown>,
|
|
100
|
+
) => {
|
|
101
|
+
if (!context.fs) {
|
|
102
|
+
throw new Error('Filesystem tools are not available in this execution context.');
|
|
103
|
+
}
|
|
104
|
+
const result = await context.fs.listDirectory(directoryPath, { recursive }, {
|
|
105
|
+
toolCallId: invocationOptions?.toolCallId,
|
|
106
|
+
abortSignal: invocationOptions?.abortSignal ?? context.abortSignal,
|
|
107
|
+
});
|
|
108
|
+
return truncate(Array.isArray(result) ? result.join('\n') : result);
|
|
109
|
+
},
|
|
110
|
+
}),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
if (options.includeDelete !== false) {
|
|
114
|
+
tools.delete_file = plugins.tool({
|
|
115
|
+
description: 'Delete a file or empty directory in the active workspace. Requires host permission when configured.',
|
|
116
|
+
inputSchema: plugins.z.object({
|
|
117
|
+
path: plugins.z.string().describe('Path to delete'),
|
|
118
|
+
}),
|
|
119
|
+
execute: async (
|
|
120
|
+
{ path: targetPath }: { path: string },
|
|
121
|
+
invocationOptions?: plugins.ToolExecutionOptions<unknown>,
|
|
122
|
+
) => {
|
|
123
|
+
if (!context.fs?.deletePath) {
|
|
124
|
+
throw new Error('Deleting files is not available in this execution context.');
|
|
125
|
+
}
|
|
126
|
+
const abortSignal = invocationOptions?.abortSignal ?? context.abortSignal;
|
|
127
|
+
await context.requestPermission?.({
|
|
128
|
+
type: 'delete',
|
|
129
|
+
title: 'Delete file',
|
|
130
|
+
metadata: { path: targetPath },
|
|
131
|
+
toolCallId: invocationOptions?.toolCallId,
|
|
132
|
+
abortSignal,
|
|
133
|
+
});
|
|
134
|
+
const result = await context.fs.deletePath(targetPath, {
|
|
135
|
+
toolCallId: invocationOptions?.toolCallId,
|
|
136
|
+
abortSignal,
|
|
137
|
+
});
|
|
138
|
+
return truncate(result ?? `Deleted ${targetPath}`);
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return tools;
|
|
144
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
|
|
3
|
+
export function httpTool(): plugins.ToolSet {
|
|
4
|
+
return {
|
|
5
|
+
http_get: plugins.tool({
|
|
6
|
+
description: 'Make an HTTP GET request and return the response.',
|
|
7
|
+
inputSchema: plugins.z.object({
|
|
8
|
+
url: plugins.z.string().describe('URL to request'),
|
|
9
|
+
headers: plugins.z
|
|
10
|
+
.record(plugins.z.string(), plugins.z.string())
|
|
11
|
+
.optional()
|
|
12
|
+
.describe('Request headers'),
|
|
13
|
+
}),
|
|
14
|
+
execute: async ({
|
|
15
|
+
url,
|
|
16
|
+
headers,
|
|
17
|
+
}: {
|
|
18
|
+
url: string;
|
|
19
|
+
headers?: Record<string, string>;
|
|
20
|
+
}) => {
|
|
21
|
+
let req = plugins.smartrequest.default.create().url(url);
|
|
22
|
+
if (headers) {
|
|
23
|
+
req = req.headers(headers);
|
|
24
|
+
}
|
|
25
|
+
const response = await req.get();
|
|
26
|
+
let body: string;
|
|
27
|
+
try {
|
|
28
|
+
const json = await response.json();
|
|
29
|
+
body = JSON.stringify(json, null, 2);
|
|
30
|
+
} catch {
|
|
31
|
+
body = await response.text();
|
|
32
|
+
}
|
|
33
|
+
return plugins.truncateOutput(`HTTP ${response.status}\n${body}`).content;
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
36
|
+
|
|
37
|
+
http_post: plugins.tool({
|
|
38
|
+
description: 'Make an HTTP POST request with a JSON body.',
|
|
39
|
+
inputSchema: plugins.z.object({
|
|
40
|
+
url: plugins.z.string().describe('URL to request'),
|
|
41
|
+
body: plugins.z
|
|
42
|
+
.record(plugins.z.string(), plugins.z.unknown())
|
|
43
|
+
.optional()
|
|
44
|
+
.describe('JSON body to send'),
|
|
45
|
+
headers: plugins.z
|
|
46
|
+
.record(plugins.z.string(), plugins.z.string())
|
|
47
|
+
.optional()
|
|
48
|
+
.describe('Request headers'),
|
|
49
|
+
}),
|
|
50
|
+
execute: async ({
|
|
51
|
+
url,
|
|
52
|
+
body,
|
|
53
|
+
headers,
|
|
54
|
+
}: {
|
|
55
|
+
url: string;
|
|
56
|
+
body?: Record<string, unknown>;
|
|
57
|
+
headers?: Record<string, string>;
|
|
58
|
+
}) => {
|
|
59
|
+
let req = plugins.smartrequest.default.create().url(url);
|
|
60
|
+
if (headers) {
|
|
61
|
+
req = req.headers(headers);
|
|
62
|
+
}
|
|
63
|
+
if (body) {
|
|
64
|
+
req = req.json(body);
|
|
65
|
+
}
|
|
66
|
+
const response = await req.post();
|
|
67
|
+
let responseBody: string;
|
|
68
|
+
try {
|
|
69
|
+
const json = await response.json();
|
|
70
|
+
responseBody = JSON.stringify(json, null, 2);
|
|
71
|
+
} catch {
|
|
72
|
+
responseBody = await response.text();
|
|
73
|
+
}
|
|
74
|
+
return plugins.truncateOutput(`HTTP ${response.status}\n${responseBody}`).content;
|
|
75
|
+
},
|
|
76
|
+
}),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
|
|
3
|
+
export function jsonTool(): plugins.ToolSet {
|
|
4
|
+
return {
|
|
5
|
+
json_validate: plugins.tool({
|
|
6
|
+
description:
|
|
7
|
+
'Validate a JSON string and optionally check for required fields.',
|
|
8
|
+
inputSchema: plugins.z.object({
|
|
9
|
+
jsonString: plugins.z.string().describe('JSON string to validate'),
|
|
10
|
+
requiredFields: plugins.z
|
|
11
|
+
.array(plugins.z.string())
|
|
12
|
+
.optional()
|
|
13
|
+
.describe('Fields that must exist at the top level'),
|
|
14
|
+
}),
|
|
15
|
+
execute: async ({
|
|
16
|
+
jsonString,
|
|
17
|
+
requiredFields,
|
|
18
|
+
}: {
|
|
19
|
+
jsonString: string;
|
|
20
|
+
requiredFields?: string[];
|
|
21
|
+
}) => {
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(jsonString);
|
|
24
|
+
if (requiredFields?.length) {
|
|
25
|
+
const missing = requiredFields.filter((f) => !(f in parsed));
|
|
26
|
+
if (missing.length) {
|
|
27
|
+
return `Invalid: missing required fields: ${missing.join(', ')}`;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const type = Array.isArray(parsed) ? 'array' : typeof parsed;
|
|
31
|
+
return `Valid JSON (${type})`;
|
|
32
|
+
} catch (e) {
|
|
33
|
+
return `Invalid JSON: ${(e as Error).message}`;
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
|
|
38
|
+
json_transform: plugins.tool({
|
|
39
|
+
description: 'Parse a JSON string and return it pretty-printed.',
|
|
40
|
+
inputSchema: plugins.z.object({
|
|
41
|
+
jsonString: plugins.z.string().describe('JSON string to format'),
|
|
42
|
+
}),
|
|
43
|
+
execute: async ({ jsonString }: { jsonString: string }) => {
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(jsonString);
|
|
46
|
+
return JSON.stringify(parsed, null, 2);
|
|
47
|
+
} catch (e) {
|
|
48
|
+
return `Error parsing JSON: ${(e as Error).message}`;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import {
|
|
3
|
+
formatShellResult,
|
|
4
|
+
type IShellCommand,
|
|
5
|
+
type IShellStartedResult,
|
|
6
|
+
type IToolExecutionContext,
|
|
7
|
+
type IToolShellResult,
|
|
8
|
+
} from './tool.context.js';
|
|
9
|
+
|
|
10
|
+
const DEFAULT_FOREGROUND_PREVIEW_BYTES = 50 * 1024;
|
|
11
|
+
const MAX_FOREGROUND_PRELIMINARY_CHUNKS = 64;
|
|
12
|
+
const MAX_FOREGROUND_PRELIMINARY_BYTES = 256 * 1024;
|
|
13
|
+
|
|
14
|
+
interface IForegroundShellStreamOptions {
|
|
15
|
+
abortSignal?: AbortSignal;
|
|
16
|
+
maxLines?: number;
|
|
17
|
+
maxBytes?: number;
|
|
18
|
+
start: (
|
|
19
|
+
abortSignal: AbortSignal,
|
|
20
|
+
onOutput: (stream: 'stdout' | 'stderr', chunk: string) => Promise<void>,
|
|
21
|
+
) => Promise<IToolShellResult | string>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface IQueuedShellOutput {
|
|
25
|
+
kind: 'preliminary' | 'final';
|
|
26
|
+
content: string;
|
|
27
|
+
bytes: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface IShellOutputWaiter {
|
|
31
|
+
resolve: (result: IteratorResult<string>) => void;
|
|
32
|
+
reject: (error: unknown) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface IShellCommandDeniedResult {
|
|
36
|
+
error: string;
|
|
37
|
+
allowedCommands: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const singleShellOutput = async function* <TOutput>(output: TOutput): AsyncGenerator<TOutput> {
|
|
41
|
+
yield output;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const foregroundPreviewByteLimit = (configuredMaxBytes?: number): number => (
|
|
45
|
+
Number.isSafeInteger(configuredMaxBytes) && configuredMaxBytes! > 0
|
|
46
|
+
? Math.min(configuredMaxBytes!, DEFAULT_FOREGROUND_PREVIEW_BYTES)
|
|
47
|
+
: DEFAULT_FOREGROUND_PREVIEW_BYTES
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const formatForegroundShellPreview = (
|
|
51
|
+
stdout: string,
|
|
52
|
+
stderr: string,
|
|
53
|
+
captureTruncated: boolean,
|
|
54
|
+
): string => {
|
|
55
|
+
const output = stderr
|
|
56
|
+
? stdout
|
|
57
|
+
? `stdout:\n${stdout}\nstderr:\n${stderr}`
|
|
58
|
+
: `stderr:\n${stderr}`
|
|
59
|
+
: stdout;
|
|
60
|
+
return captureTruncated
|
|
61
|
+
? `${output}${output ? '\n' : ''}[Live output preview truncated; final output will follow.]`
|
|
62
|
+
: output;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
class ForegroundShellResultStream implements AsyncIterableIterator<string> {
|
|
66
|
+
private readonly abortController = new AbortController();
|
|
67
|
+
private readonly abortSignal: AbortSignal;
|
|
68
|
+
private readonly previewByteLimit: number;
|
|
69
|
+
private readonly queue: IQueuedShellOutput[] = [];
|
|
70
|
+
private readonly waiters: IShellOutputWaiter[] = [];
|
|
71
|
+
private completion?: Promise<IToolShellResult | string>;
|
|
72
|
+
private stdout = '';
|
|
73
|
+
private stderr = '';
|
|
74
|
+
private previewBytes = 0;
|
|
75
|
+
private captureTruncated = false;
|
|
76
|
+
private lastPreview = '';
|
|
77
|
+
private preliminaryChunks = 0;
|
|
78
|
+
private preliminaryBytes = 0;
|
|
79
|
+
private previewsDisabled = false;
|
|
80
|
+
private settled = false;
|
|
81
|
+
private returned = false;
|
|
82
|
+
private terminalError: unknown;
|
|
83
|
+
|
|
84
|
+
constructor(private readonly options: IForegroundShellStreamOptions) {
|
|
85
|
+
this.abortSignal = options.abortSignal
|
|
86
|
+
? AbortSignal.any([options.abortSignal, this.abortController.signal])
|
|
87
|
+
: this.abortController.signal;
|
|
88
|
+
this.previewByteLimit = foregroundPreviewByteLimit(options.maxBytes);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public [Symbol.asyncIterator](): AsyncIterableIterator<string> {
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public next(): Promise<IteratorResult<string>> {
|
|
96
|
+
if (this.returned) return Promise.resolve({ done: true, value: undefined });
|
|
97
|
+
this.ensureStarted();
|
|
98
|
+
const queued = this.queue.shift();
|
|
99
|
+
if (queued) return Promise.resolve(this.deliver(queued));
|
|
100
|
+
if (this.settled) {
|
|
101
|
+
return this.terminalError === undefined
|
|
102
|
+
? Promise.resolve({ done: true, value: undefined })
|
|
103
|
+
: Promise.reject(this.terminalError);
|
|
104
|
+
}
|
|
105
|
+
return new Promise<IteratorResult<string>>((resolve, reject) => {
|
|
106
|
+
this.waiters.push({ resolve, reject });
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public async return(): Promise<IteratorResult<string>> {
|
|
111
|
+
if (!this.returned) {
|
|
112
|
+
this.returned = true;
|
|
113
|
+
this.abortController.abort(new DOMException('Foreground shell iteration stopped.', 'AbortError'));
|
|
114
|
+
this.queue.length = 0;
|
|
115
|
+
this.flushWaiters();
|
|
116
|
+
}
|
|
117
|
+
if (this.completion) {
|
|
118
|
+
try {
|
|
119
|
+
await this.completion;
|
|
120
|
+
} catch {
|
|
121
|
+
// Iterator cancellation owns the abort; the cancelled command result is not a tool failure.
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return { done: true, value: undefined };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
public async throw(error?: unknown): Promise<IteratorResult<string>> {
|
|
128
|
+
if (!this.returned) {
|
|
129
|
+
this.returned = true;
|
|
130
|
+
this.abortController.abort(error);
|
|
131
|
+
this.queue.length = 0;
|
|
132
|
+
while (this.waiters.length > 0) this.waiters.shift()!.reject(error);
|
|
133
|
+
}
|
|
134
|
+
if (this.completion) {
|
|
135
|
+
try {
|
|
136
|
+
await this.completion;
|
|
137
|
+
} catch {
|
|
138
|
+
// The caller-provided iterator error remains authoritative.
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private async handleOutput(stream: 'stdout' | 'stderr', chunk: string): Promise<void> {
|
|
145
|
+
if (this.returned || this.settled || this.previewsDisabled || !chunk) return;
|
|
146
|
+
let accepted = '';
|
|
147
|
+
let acceptedBytes = 0;
|
|
148
|
+
for (const character of chunk) {
|
|
149
|
+
const characterBytes = Buffer.byteLength(character);
|
|
150
|
+
if (this.previewBytes + acceptedBytes + characterBytes > this.previewByteLimit) break;
|
|
151
|
+
accepted += character;
|
|
152
|
+
acceptedBytes += characterBytes;
|
|
153
|
+
}
|
|
154
|
+
if (acceptedBytes < Buffer.byteLength(chunk)) this.captureTruncated = true;
|
|
155
|
+
if (accepted) {
|
|
156
|
+
if (stream === 'stdout') this.stdout += accepted;
|
|
157
|
+
else this.stderr += accepted;
|
|
158
|
+
this.previewBytes += acceptedBytes;
|
|
159
|
+
}
|
|
160
|
+
const preview = plugins.truncateOutput(
|
|
161
|
+
formatForegroundShellPreview(this.stdout, this.stderr, this.captureTruncated),
|
|
162
|
+
{ maxLines: this.options.maxLines, maxBytes: this.options.maxBytes },
|
|
163
|
+
).content;
|
|
164
|
+
if (!preview || preview === this.lastPreview) return;
|
|
165
|
+
const bytes = Buffer.byteLength(JSON.stringify(preview));
|
|
166
|
+
if (
|
|
167
|
+
this.preliminaryChunks + 1 > MAX_FOREGROUND_PRELIMINARY_CHUNKS
|
|
168
|
+
|| this.preliminaryBytes + bytes > MAX_FOREGROUND_PRELIMINARY_BYTES
|
|
169
|
+
) {
|
|
170
|
+
this.previewsDisabled = true;
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
this.lastPreview = preview;
|
|
174
|
+
for (let index = this.queue.length - 1; index >= 0; index--) {
|
|
175
|
+
if (this.queue[index].kind === 'preliminary') this.queue.splice(index, 1);
|
|
176
|
+
}
|
|
177
|
+
this.queue.push({ kind: 'preliminary', content: preview, bytes });
|
|
178
|
+
if (this.captureTruncated) this.previewsDisabled = true;
|
|
179
|
+
this.flushWaiters();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private ensureStarted(): void {
|
|
183
|
+
if (this.completion) return;
|
|
184
|
+
const completion = Promise.resolve().then(() => this.options.start(
|
|
185
|
+
this.abortSignal,
|
|
186
|
+
async (stream, chunk) => this.handleOutput(stream, chunk),
|
|
187
|
+
));
|
|
188
|
+
this.completion = completion;
|
|
189
|
+
void completion.then(
|
|
190
|
+
(result) => this.finish(result),
|
|
191
|
+
(error) => this.fail(error),
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private finish(result: IToolShellResult | string): void {
|
|
196
|
+
this.settled = true;
|
|
197
|
+
if (this.returned) return;
|
|
198
|
+
const content = plugins.truncateOutput(formatShellResult(result), {
|
|
199
|
+
maxLines: this.options.maxLines,
|
|
200
|
+
maxBytes: this.options.maxBytes,
|
|
201
|
+
}).content;
|
|
202
|
+
this.queue.push({ kind: 'final', content, bytes: Buffer.byteLength(content) });
|
|
203
|
+
this.flushWaiters();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private fail(error: unknown): void {
|
|
207
|
+
this.settled = true;
|
|
208
|
+
this.terminalError = error;
|
|
209
|
+
this.flushWaiters();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private deliver(output: IQueuedShellOutput): IteratorResult<string> {
|
|
213
|
+
if (output.kind === 'preliminary') {
|
|
214
|
+
this.preliminaryChunks++;
|
|
215
|
+
this.preliminaryBytes += output.bytes;
|
|
216
|
+
}
|
|
217
|
+
return { done: false, value: output.content };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private flushWaiters(): void {
|
|
221
|
+
while (this.waiters.length > 0 && this.queue.length > 0) {
|
|
222
|
+
this.waiters.shift()!.resolve(this.deliver(this.queue.shift()!));
|
|
223
|
+
}
|
|
224
|
+
if (!this.settled && !this.returned) return;
|
|
225
|
+
while (this.waiters.length > 0) {
|
|
226
|
+
const waiter = this.waiters.shift()!;
|
|
227
|
+
if (!this.returned && this.terminalError !== undefined) waiter.reject(this.terminalError);
|
|
228
|
+
else waiter.resolve({ done: true, value: undefined });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export interface ICreateShellToolsOptions {
|
|
234
|
+
/**
|
|
235
|
+
* Exact executable allowlist for structured execution. Raw command filtering is advisory.
|
|
236
|
+
*/
|
|
237
|
+
allowedCommands?: string[];
|
|
238
|
+
/** Maximum output lines before truncating. */
|
|
239
|
+
maxLines?: number;
|
|
240
|
+
/** Maximum output bytes before truncating. */
|
|
241
|
+
maxBytes?: number;
|
|
242
|
+
/** Expose start_shell when context.jobs exists. Default: true. */
|
|
243
|
+
includeBackground?: boolean;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function createShellTools(context: IToolExecutionContext, options: ICreateShellToolsOptions = {}): plugins.ToolSet {
|
|
247
|
+
const tools: plugins.ToolSet = {
|
|
248
|
+
run_command: plugins.tool({
|
|
249
|
+
description:
|
|
250
|
+
'Execute a raw shell command through bash -lc. Shell composition is supported. Prefer run_shell when direct structured execution is sufficient.',
|
|
251
|
+
inputSchema: plugins.z.object({
|
|
252
|
+
command: plugins.z.string().describe('The shell command to execute'),
|
|
253
|
+
cwd: plugins.z
|
|
254
|
+
.string()
|
|
255
|
+
.optional()
|
|
256
|
+
.describe('Working directory for the command'),
|
|
257
|
+
timeout: plugins.z
|
|
258
|
+
.number()
|
|
259
|
+
.optional()
|
|
260
|
+
.describe('Timeout in milliseconds'),
|
|
261
|
+
timeoutMs: plugins.z
|
|
262
|
+
.number()
|
|
263
|
+
.optional()
|
|
264
|
+
.describe('Timeout in milliseconds'),
|
|
265
|
+
}),
|
|
266
|
+
execute: (
|
|
267
|
+
{
|
|
268
|
+
command,
|
|
269
|
+
cwd,
|
|
270
|
+
timeout,
|
|
271
|
+
timeoutMs,
|
|
272
|
+
}: {
|
|
273
|
+
command: string;
|
|
274
|
+
cwd?: string;
|
|
275
|
+
timeout?: number;
|
|
276
|
+
timeoutMs?: number;
|
|
277
|
+
},
|
|
278
|
+
invocationOptions?: plugins.ToolExecutionOptions<unknown>,
|
|
279
|
+
): AsyncIterable<string> => {
|
|
280
|
+
const shell = context.shell;
|
|
281
|
+
if (!shell) {
|
|
282
|
+
throw new Error('Shell tool is not available in this execution context.');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (options.allowedCommands?.length) {
|
|
286
|
+
const baseCommand = command.trim().split(/\s+/)[0];
|
|
287
|
+
if (!options.allowedCommands.includes(baseCommand)) {
|
|
288
|
+
return singleShellOutput(
|
|
289
|
+
`Raw command "${baseCommand}" is not in the advisory allowed commands list: ${options.allowedCommands.join(', ')}`,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const abortSignal = invocationOptions?.abortSignal ?? context.abortSignal;
|
|
295
|
+
return new ForegroundShellResultStream({
|
|
296
|
+
abortSignal,
|
|
297
|
+
maxLines: options.maxLines,
|
|
298
|
+
maxBytes: options.maxBytes,
|
|
299
|
+
start: async (executionSignal, onOutput) => {
|
|
300
|
+
await context.requestPermission?.({
|
|
301
|
+
type: 'shell',
|
|
302
|
+
title: 'Run shell command',
|
|
303
|
+
metadata: { command, cwd: cwd ?? context.cwd },
|
|
304
|
+
toolCallId: invocationOptions?.toolCallId,
|
|
305
|
+
abortSignal: executionSignal,
|
|
306
|
+
});
|
|
307
|
+
executionSignal.throwIfAborted();
|
|
308
|
+
return shell.run(command, {
|
|
309
|
+
cwd: cwd ?? context.cwd,
|
|
310
|
+
timeoutMs: timeoutMs ?? timeout,
|
|
311
|
+
toolCallId: invocationOptions?.toolCallId,
|
|
312
|
+
abortSignal: executionSignal,
|
|
313
|
+
onOutput,
|
|
314
|
+
});
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
},
|
|
318
|
+
}),
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
if (context.shell?.runStructured) {
|
|
322
|
+
tools.run_shell = plugins.tool({
|
|
323
|
+
description:
|
|
324
|
+
'Execute an executable directly with a literal argument array. No shell parsing or shell composition is performed.',
|
|
325
|
+
inputSchema: plugins.z.object({
|
|
326
|
+
executable: plugins.z.string().describe('Exact executable name or path'),
|
|
327
|
+
args: plugins.z.array(plugins.z.string()).default([]).describe('Literal argument array'),
|
|
328
|
+
cwd: plugins.z.string().optional().describe('Working directory for the command'),
|
|
329
|
+
timeoutMs: plugins.z.number().optional().describe('Timeout in milliseconds'),
|
|
330
|
+
}),
|
|
331
|
+
execute: (
|
|
332
|
+
input: IShellCommand & { cwd?: string; timeoutMs?: number },
|
|
333
|
+
invocationOptions?: plugins.ToolExecutionOptions<unknown>,
|
|
334
|
+
): AsyncIterable<string | IShellCommandDeniedResult> => {
|
|
335
|
+
const runStructured = context.shell?.runStructured;
|
|
336
|
+
if (!runStructured) {
|
|
337
|
+
throw new Error('Structured shell execution is not available in this execution context.');
|
|
338
|
+
}
|
|
339
|
+
if (options.allowedCommands?.length && !options.allowedCommands.includes(input.executable)) {
|
|
340
|
+
return singleShellOutput({
|
|
341
|
+
error: `Executable "${input.executable}" is not in the allowed commands list.`,
|
|
342
|
+
allowedCommands: options.allowedCommands,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
const abortSignal = invocationOptions?.abortSignal ?? context.abortSignal;
|
|
346
|
+
return new ForegroundShellResultStream({
|
|
347
|
+
abortSignal,
|
|
348
|
+
maxLines: options.maxLines,
|
|
349
|
+
maxBytes: options.maxBytes,
|
|
350
|
+
start: async (executionSignal, onOutput) => {
|
|
351
|
+
await context.requestPermission?.({
|
|
352
|
+
type: 'shell',
|
|
353
|
+
title: 'Run structured command',
|
|
354
|
+
metadata: { executable: input.executable, args: input.args, cwd: input.cwd ?? context.cwd },
|
|
355
|
+
toolCallId: invocationOptions?.toolCallId,
|
|
356
|
+
abortSignal: executionSignal,
|
|
357
|
+
});
|
|
358
|
+
executionSignal.throwIfAborted();
|
|
359
|
+
return runStructured(
|
|
360
|
+
{ executable: input.executable, args: input.args },
|
|
361
|
+
{
|
|
362
|
+
cwd: input.cwd ?? context.cwd,
|
|
363
|
+
timeoutMs: input.timeoutMs,
|
|
364
|
+
toolCallId: invocationOptions?.toolCallId,
|
|
365
|
+
abortSignal: executionSignal,
|
|
366
|
+
onOutput,
|
|
367
|
+
},
|
|
368
|
+
);
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (context.jobs && options.includeBackground !== false) {
|
|
376
|
+
tools.start_shell = plugins.tool({
|
|
377
|
+
description:
|
|
378
|
+
'Start a structured shell command as an independent background execution and return its execution ID immediately.',
|
|
379
|
+
inputSchema: plugins.z.object({
|
|
380
|
+
executable: plugins.z.string().describe('Exact executable name or path'),
|
|
381
|
+
args: plugins.z.array(plugins.z.string()).default([]).describe('Literal argument array'),
|
|
382
|
+
cwd: plugins.z.string().optional().describe('Working directory for the command'),
|
|
383
|
+
timeoutMs: plugins.z.number().optional().describe('Background execution timeout in milliseconds'),
|
|
384
|
+
}),
|
|
385
|
+
execute: async (
|
|
386
|
+
input: IShellCommand & { cwd?: string; timeoutMs?: number },
|
|
387
|
+
invocationOptions?: plugins.ToolExecutionOptions<unknown>,
|
|
388
|
+
): Promise<IShellStartedResult> => {
|
|
389
|
+
if (!context.jobs) throw new Error('Background jobs are not available in this execution context.');
|
|
390
|
+
if (options.allowedCommands?.length && !options.allowedCommands.includes(input.executable)) {
|
|
391
|
+
throw new Error(`Executable "${input.executable}" is not in the allowed commands list.`);
|
|
392
|
+
}
|
|
393
|
+
const admissionSignal = invocationOptions?.abortSignal ?? context.abortSignal;
|
|
394
|
+
await context.requestPermission?.({
|
|
395
|
+
type: 'shell-background',
|
|
396
|
+
title: 'Start background shell command',
|
|
397
|
+
metadata: { executable: input.executable, args: input.args, cwd: input.cwd ?? context.cwd },
|
|
398
|
+
toolCallId: invocationOptions?.toolCallId,
|
|
399
|
+
abortSignal: admissionSignal,
|
|
400
|
+
});
|
|
401
|
+
admissionSignal?.throwIfAborted();
|
|
402
|
+
const handle = await context.jobs.start({
|
|
403
|
+
type: 'shell',
|
|
404
|
+
command: { executable: input.executable, args: input.args },
|
|
405
|
+
cwd: input.cwd ?? context.cwd,
|
|
406
|
+
timeoutMs: input.timeoutMs,
|
|
407
|
+
metadata: { toolCallId: invocationOptions?.toolCallId },
|
|
408
|
+
}, {
|
|
409
|
+
toolCallId: invocationOptions?.toolCallId,
|
|
410
|
+
abortSignal: admissionSignal,
|
|
411
|
+
});
|
|
412
|
+
return { executionId: handle.executionId, state: 'running' };
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return tools;
|
|
418
|
+
}
|