@modelprofile.com/flexharness 6.1.0 → 7.0.1
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,49 @@
|
|
|
1
|
+
PDF processing and language-model document recipes.
|
|
2
|
+
|
|
3
|
+
Part of the [FlexHarness toolbox](https://code.foss.global/modelprofile.com/flexharness).
|
|
4
|
+
Install the components your project uses. Provider adapters are registered explicitly;
|
|
5
|
+
the agent and harness do not install or register vendor SDKs.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add @modelprofile.com/flexharness @push.rocks/smartpdf
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { extractTextFromPdf, stopSmartpdf } from '@modelprofile.com/flexharness/documents';
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const text = await extractTextFromPdf(pdfBytes, { model });
|
|
16
|
+
console.log(text);
|
|
17
|
+
} finally {
|
|
18
|
+
await stopSmartpdf();
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Supply a model from your selected FlexHarness provider. `extractTextFromPdf` replaces SmartOCR's `extractTextFromPdfBufferWithSmartAi`: it renders PDF pages through SmartPDF and asks the model to preserve text, page order, headings and table structure. Override `systemMessage` and `userMessage` or supply `messageHistory` when needed.
|
|
23
|
+
|
|
24
|
+
`analyzeDocuments({ model, userMessage, pdfDocuments, systemMessage?, messageHistory? })` supports general analysis across multiple PDFs. This component owns a shared SmartPDF instance; call `stopSmartpdf()` when all document work has finished. Image-only OCR is available independently in `flexharness/ocr`.
|
|
25
|
+
|
|
26
|
+
## Issue Reporting and Security
|
|
27
|
+
|
|
28
|
+
Report issues and security concerns through [community.foss.global](https://community.foss.global).
|
|
29
|
+
|
|
30
|
+
## License and Legal Information
|
|
31
|
+
|
|
32
|
+
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the repository license file.
|
|
33
|
+
|
|
34
|
+
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
|
35
|
+
|
|
36
|
+
### Trademarks
|
|
37
|
+
|
|
38
|
+
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
|
39
|
+
|
|
40
|
+
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
|
41
|
+
|
|
42
|
+
### Company Information
|
|
43
|
+
|
|
44
|
+
Task Venture Capital GmbH<br>
|
|
45
|
+
Registered at District Court Bremen HRB 35230 HB, Germany
|
|
46
|
+
|
|
47
|
+
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
|
48
|
+
|
|
49
|
+
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import type {
|
|
3
|
+
IBrowserKvmOptions,
|
|
4
|
+
IKvmDriver,
|
|
5
|
+
IKvmFrame,
|
|
6
|
+
IKvmTypeTextOptions,
|
|
7
|
+
TKvmKey,
|
|
8
|
+
TKvmKind,
|
|
9
|
+
} from './interfaces.js';
|
|
10
|
+
|
|
11
|
+
const defaultViewerSelector = 'video, canvas';
|
|
12
|
+
|
|
13
|
+
interface ICaptureMediaResult {
|
|
14
|
+
captured: boolean;
|
|
15
|
+
width?: number;
|
|
16
|
+
height?: number;
|
|
17
|
+
dataBase64?: string;
|
|
18
|
+
error?: string;
|
|
19
|
+
hasMediaElement?: boolean;
|
|
20
|
+
mediaHasFrame?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class BrowserKvm implements IKvmDriver {
|
|
24
|
+
public readonly kind: TKvmKind;
|
|
25
|
+
|
|
26
|
+
private options: IBrowserKvmOptions;
|
|
27
|
+
private browser?: plugins.puppeteer.Browser;
|
|
28
|
+
private page?: plugins.puppeteer.Page;
|
|
29
|
+
|
|
30
|
+
constructor(options: IBrowserKvmOptions) {
|
|
31
|
+
this.options = options;
|
|
32
|
+
this.kind = options.kind ?? 'generic';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public async connect(): Promise<void> {
|
|
36
|
+
if (this.browser && this.page) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const timeoutMs = this.options.timeoutMs ?? 30000;
|
|
41
|
+
const args: string[] = [];
|
|
42
|
+
if (process.env.CI || process.getuid?.() === 0) {
|
|
43
|
+
args.push('--no-sandbox', '--disable-setuid-sandbox');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this.browser = await plugins.puppeteer.launch({
|
|
47
|
+
args,
|
|
48
|
+
acceptInsecureCerts: this.options.ignoreHttpsErrors ?? false,
|
|
49
|
+
defaultViewport: null,
|
|
50
|
+
executablePath: this.options.executablePath,
|
|
51
|
+
headless: this.options.headless ?? true,
|
|
52
|
+
timeout: timeoutMs,
|
|
53
|
+
userDataDir: this.options.userDataDir,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
this.page = await this.browser.newPage();
|
|
57
|
+
this.page.setDefaultTimeout(timeoutMs);
|
|
58
|
+
this.page.setDefaultNavigationTimeout(timeoutMs);
|
|
59
|
+
|
|
60
|
+
await this.page.goto(this.options.url, {
|
|
61
|
+
waitUntil: 'domcontentloaded',
|
|
62
|
+
timeout: timeoutMs,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await this.tryGenericLogin();
|
|
66
|
+
await this.waitForViewerReady();
|
|
67
|
+
await this.focusViewer();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public async disconnect(): Promise<void> {
|
|
71
|
+
const browser = this.browser;
|
|
72
|
+
this.page = undefined;
|
|
73
|
+
this.browser = undefined;
|
|
74
|
+
if (browser) {
|
|
75
|
+
await browser.close();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public async focusViewer(): Promise<void> {
|
|
80
|
+
const page = this.requirePage();
|
|
81
|
+
const viewerSelector = this.getViewerSelector();
|
|
82
|
+
const viewerElement = await page.$(viewerSelector);
|
|
83
|
+
if (!viewerElement) {
|
|
84
|
+
throw new Error(`KVM viewer selector missing: ${viewerSelector}`);
|
|
85
|
+
}
|
|
86
|
+
await viewerElement.click();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public async captureFrame(): Promise<IKvmFrame> {
|
|
90
|
+
const page = this.requirePage();
|
|
91
|
+
const captureSelector = this.getCaptureSelector();
|
|
92
|
+
const captureElement = await page.$(captureSelector);
|
|
93
|
+
if (!captureElement) {
|
|
94
|
+
throw new Error(`KVM capture selector missing: ${captureSelector}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const mediaResult = await page.evaluate((selector: string): ICaptureMediaResult => {
|
|
98
|
+
const rootElement = document.querySelector(selector);
|
|
99
|
+
if (!rootElement) {
|
|
100
|
+
return {
|
|
101
|
+
captured: false,
|
|
102
|
+
error: `KVM capture selector missing: ${selector}`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const mediaElement = (rootElement.matches('video, canvas')
|
|
107
|
+
? rootElement
|
|
108
|
+
: rootElement.querySelector('video, canvas')) as HTMLVideoElement | HTMLCanvasElement | null;
|
|
109
|
+
|
|
110
|
+
if (!mediaElement) {
|
|
111
|
+
return {
|
|
112
|
+
captured: false,
|
|
113
|
+
hasMediaElement: false,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let width = 0;
|
|
118
|
+
let height = 0;
|
|
119
|
+
if (mediaElement instanceof HTMLVideoElement) {
|
|
120
|
+
width = mediaElement.videoWidth;
|
|
121
|
+
height = mediaElement.videoHeight;
|
|
122
|
+
} else if (mediaElement instanceof HTMLCanvasElement) {
|
|
123
|
+
width = mediaElement.width;
|
|
124
|
+
height = mediaElement.height;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (width <= 0 || height <= 0) {
|
|
128
|
+
return {
|
|
129
|
+
captured: false,
|
|
130
|
+
hasMediaElement: true,
|
|
131
|
+
mediaHasFrame: false,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const canvasElement = document.createElement('canvas');
|
|
137
|
+
canvasElement.width = width;
|
|
138
|
+
canvasElement.height = height;
|
|
139
|
+
const context = canvasElement.getContext('2d');
|
|
140
|
+
if (!context) {
|
|
141
|
+
return {
|
|
142
|
+
captured: false,
|
|
143
|
+
hasMediaElement: true,
|
|
144
|
+
mediaHasFrame: true,
|
|
145
|
+
error: 'Could not create canvas 2D context for KVM frame capture.',
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
context.drawImage(mediaElement, 0, 0, width, height);
|
|
149
|
+
const dataUrl = canvasElement.toDataURL('image/png');
|
|
150
|
+
return {
|
|
151
|
+
captured: true,
|
|
152
|
+
width,
|
|
153
|
+
height,
|
|
154
|
+
dataBase64: dataUrl.slice(dataUrl.indexOf(',') + 1),
|
|
155
|
+
};
|
|
156
|
+
} catch (error) {
|
|
157
|
+
return {
|
|
158
|
+
captured: false,
|
|
159
|
+
hasMediaElement: true,
|
|
160
|
+
mediaHasFrame: true,
|
|
161
|
+
error: error instanceof Error ? error.message : String(error),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}, captureSelector);
|
|
165
|
+
|
|
166
|
+
if (mediaResult.captured) {
|
|
167
|
+
return {
|
|
168
|
+
timestamp: Date.now(),
|
|
169
|
+
width: mediaResult.width ?? 0,
|
|
170
|
+
height: mediaResult.height ?? 0,
|
|
171
|
+
mimeType: 'image/png',
|
|
172
|
+
dataBase64: mediaResult.dataBase64 ?? '',
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (mediaResult.error?.startsWith('KVM capture selector missing')) {
|
|
177
|
+
throw new Error(mediaResult.error);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (mediaResult.hasMediaElement && mediaResult.mediaHasFrame === false) {
|
|
181
|
+
throw new Error(`KVM media element has no frame: ${captureSelector}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const boundingBox = await captureElement.boundingBox();
|
|
185
|
+
if (!boundingBox || boundingBox.width <= 0 || boundingBox.height <= 0) {
|
|
186
|
+
throw new Error(`KVM capture selector has no visible frame: ${captureSelector}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const screenshot = await captureElement.screenshot({
|
|
190
|
+
type: 'png',
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
timestamp: Date.now(),
|
|
195
|
+
width: Math.round(boundingBox.width),
|
|
196
|
+
height: Math.round(boundingBox.height),
|
|
197
|
+
mimeType: 'image/png',
|
|
198
|
+
dataBase64: Buffer.from(screenshot).toString('base64'),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
public async typeText(text: string, options?: IKvmTypeTextOptions): Promise<void> {
|
|
203
|
+
const page = this.requirePage();
|
|
204
|
+
await this.focusViewer();
|
|
205
|
+
await page.keyboard.type(text, {
|
|
206
|
+
delay: options?.delayMs,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
public async pressKey(key: TKvmKey): Promise<void> {
|
|
211
|
+
const page = this.requirePage();
|
|
212
|
+
await this.focusViewer();
|
|
213
|
+
await page.keyboard.press(key as plugins.puppeteer.KeyInput);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
public async pressShortcut(keys: TKvmKey[]): Promise<void> {
|
|
217
|
+
const page = this.requirePage();
|
|
218
|
+
await this.focusViewer();
|
|
219
|
+
|
|
220
|
+
const pressedKeys: TKvmKey[] = [];
|
|
221
|
+
try {
|
|
222
|
+
for (const key of keys) {
|
|
223
|
+
await page.keyboard.down(key as plugins.puppeteer.KeyInput);
|
|
224
|
+
pressedKeys.push(key);
|
|
225
|
+
}
|
|
226
|
+
} finally {
|
|
227
|
+
for (const key of pressedKeys.reverse()) {
|
|
228
|
+
await page.keyboard.up(key as plugins.puppeteer.KeyInput);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
public async wait(milliseconds: number): Promise<void> {
|
|
234
|
+
await new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private async tryGenericLogin(): Promise<void> {
|
|
238
|
+
if (!this.options.username || !this.options.password) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const page = this.requirePage();
|
|
243
|
+
const usernameElement = await this.findFirstElement([
|
|
244
|
+
'input[name="username"]',
|
|
245
|
+
'input[autocomplete="username"]',
|
|
246
|
+
'input[type="email"]',
|
|
247
|
+
'input[type="text"]',
|
|
248
|
+
]);
|
|
249
|
+
const passwordElement = await this.findFirstElement([
|
|
250
|
+
'input[name="password"]',
|
|
251
|
+
'input[autocomplete="current-password"]',
|
|
252
|
+
'input[type="password"]',
|
|
253
|
+
]);
|
|
254
|
+
|
|
255
|
+
if (!usernameElement || !passwordElement) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
await usernameElement.click({ clickCount: 3 });
|
|
260
|
+
await usernameElement.type(this.options.username);
|
|
261
|
+
await passwordElement.click({ clickCount: 3 });
|
|
262
|
+
await passwordElement.type(this.options.password);
|
|
263
|
+
await passwordElement.press('Enter');
|
|
264
|
+
|
|
265
|
+
await Promise.race([
|
|
266
|
+
page
|
|
267
|
+
.waitForNavigation({
|
|
268
|
+
waitUntil: 'domcontentloaded',
|
|
269
|
+
timeout: Math.min(this.options.timeoutMs ?? 30000, 10000),
|
|
270
|
+
})
|
|
271
|
+
.catch(() => undefined),
|
|
272
|
+
this.wait(1000),
|
|
273
|
+
]);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private async waitForViewerReady(): Promise<void> {
|
|
277
|
+
const page = this.requirePage();
|
|
278
|
+
const viewerSelector = this.getViewerSelector();
|
|
279
|
+
await page.waitForFunction(
|
|
280
|
+
(selector: string) => {
|
|
281
|
+
const rootElement = document.querySelector(selector);
|
|
282
|
+
if (!rootElement) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const mediaElement = (rootElement.matches('video, canvas')
|
|
287
|
+
? rootElement
|
|
288
|
+
: rootElement.querySelector('video, canvas')) as HTMLVideoElement | HTMLCanvasElement | null;
|
|
289
|
+
|
|
290
|
+
if (mediaElement instanceof HTMLVideoElement) {
|
|
291
|
+
return mediaElement.videoWidth > 0 && mediaElement.videoHeight > 0;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (mediaElement instanceof HTMLCanvasElement) {
|
|
295
|
+
return mediaElement.width > 0 && mediaElement.height > 0;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const boundingRect = rootElement.getBoundingClientRect();
|
|
299
|
+
return boundingRect.width > 0 && boundingRect.height > 0;
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
timeout: this.options.timeoutMs ?? 30000,
|
|
303
|
+
},
|
|
304
|
+
viewerSelector
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private async findFirstElement(
|
|
309
|
+
selectors: string[]
|
|
310
|
+
): Promise<plugins.puppeteer.ElementHandle<Element> | null> {
|
|
311
|
+
const page = this.requirePage();
|
|
312
|
+
for (const selector of selectors) {
|
|
313
|
+
const element = await page.$(selector);
|
|
314
|
+
if (element) {
|
|
315
|
+
return element;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private getViewerSelector(): string {
|
|
322
|
+
return this.options.viewerSelector ?? defaultViewerSelector;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private getCaptureSelector(): string {
|
|
326
|
+
return this.options.captureSelector ?? this.options.viewerSelector ?? defaultViewerSelector;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private requirePage(): plugins.puppeteer.Page {
|
|
330
|
+
if (!this.page) {
|
|
331
|
+
throw new Error('BrowserKvm is not connected. Call connect() first.');
|
|
332
|
+
}
|
|
333
|
+
return this.page;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { createWrappedKvmCommand, parseWrappedKvmCommandOutput } from './commandwrappers.js';
|
|
2
|
+
import type {
|
|
3
|
+
IKvmTerminalCommandResult,
|
|
4
|
+
IKvmTerminalOptions,
|
|
5
|
+
} from './interfaces.js';
|
|
6
|
+
|
|
7
|
+
export class KvmTerminal {
|
|
8
|
+
private options: IKvmTerminalOptions;
|
|
9
|
+
|
|
10
|
+
constructor(options: IKvmTerminalOptions) {
|
|
11
|
+
this.options = options;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public async bootstrap(): Promise<void> {
|
|
15
|
+
const osHint = this.options.osHint ?? 'unknown';
|
|
16
|
+
|
|
17
|
+
switch (osHint) {
|
|
18
|
+
case 'windows':
|
|
19
|
+
await this.options.kvm.pressShortcut(['Meta', 'R']);
|
|
20
|
+
await this.options.kvm.wait(500);
|
|
21
|
+
await this.options.kvm.typeText('powershell -NoLogo');
|
|
22
|
+
await this.options.kvm.pressKey('Enter');
|
|
23
|
+
await this.options.kvm.wait(1500);
|
|
24
|
+
break;
|
|
25
|
+
case 'macos':
|
|
26
|
+
await this.options.kvm.pressShortcut(['Meta', 'Space']);
|
|
27
|
+
await this.options.kvm.wait(500);
|
|
28
|
+
await this.options.kvm.typeText('Terminal');
|
|
29
|
+
await this.options.kvm.pressKey('Enter');
|
|
30
|
+
await this.options.kvm.wait(1500);
|
|
31
|
+
break;
|
|
32
|
+
case 'linux':
|
|
33
|
+
await this.options.kvm.pressShortcut(['Control', 'Alt', 'T']);
|
|
34
|
+
await this.options.kvm.wait(1500);
|
|
35
|
+
break;
|
|
36
|
+
case 'unknown':
|
|
37
|
+
default:
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public async runCommand(command: string): Promise<IKvmTerminalCommandResult> {
|
|
43
|
+
const wrappedCommand = createWrappedKvmCommand(command, this.options.shellHint ?? 'unknown');
|
|
44
|
+
const commandTimeoutMs = this.options.commandTimeoutMs ?? 30000;
|
|
45
|
+
const ocrPollIntervalMs = this.options.ocrPollIntervalMs ?? 500;
|
|
46
|
+
const startedAt = Date.now();
|
|
47
|
+
let rawOcrText = '';
|
|
48
|
+
let attempts = 0;
|
|
49
|
+
|
|
50
|
+
await this.options.kvm.focusViewer();
|
|
51
|
+
await this.options.kvm.typeText(wrappedCommand.textToType);
|
|
52
|
+
await this.options.kvm.pressKey('Enter');
|
|
53
|
+
|
|
54
|
+
while (true) {
|
|
55
|
+
attempts++;
|
|
56
|
+
rawOcrText = await this.observeText();
|
|
57
|
+
const parsedResult = parseWrappedKvmCommandOutput({
|
|
58
|
+
commandId: wrappedCommand.commandId,
|
|
59
|
+
startMarker: wrappedCommand.startMarker,
|
|
60
|
+
endMarkerPrefix: wrappedCommand.endMarkerPrefix,
|
|
61
|
+
rawText: rawOcrText,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (parsedResult.completed) {
|
|
65
|
+
return {
|
|
66
|
+
commandId: wrappedCommand.commandId,
|
|
67
|
+
command,
|
|
68
|
+
completed: true,
|
|
69
|
+
timedOut: false,
|
|
70
|
+
exitCode: parsedResult.exitCode,
|
|
71
|
+
combinedText: parsedResult.combinedText,
|
|
72
|
+
rawOcrText,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const reachedMaxAttempts =
|
|
77
|
+
typeof this.options.ocrMaxAttempts === 'number' && attempts >= this.options.ocrMaxAttempts;
|
|
78
|
+
const reachedTimeout = Date.now() - startedAt >= commandTimeoutMs;
|
|
79
|
+
if (reachedMaxAttempts || reachedTimeout) {
|
|
80
|
+
return {
|
|
81
|
+
commandId: wrappedCommand.commandId,
|
|
82
|
+
command,
|
|
83
|
+
completed: false,
|
|
84
|
+
timedOut: true,
|
|
85
|
+
combinedText: rawOcrText,
|
|
86
|
+
rawOcrText,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const remainingTimeMs = commandTimeoutMs - (Date.now() - startedAt);
|
|
91
|
+
await this.options.kvm.wait(Math.min(ocrPollIntervalMs, Math.max(remainingTimeMs, 0)));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public async observeText(): Promise<string> {
|
|
96
|
+
const frame = await this.options.kvm.captureFrame();
|
|
97
|
+
const result = await this.options.ocrEngine.recognize(frame, {
|
|
98
|
+
crop: this.options.ocrCrop,
|
|
99
|
+
language: 'eng',
|
|
100
|
+
});
|
|
101
|
+
return result.text;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import type { IWrappedKvmCommand, TKvmShellHint } from './interfaces.js';
|
|
3
|
+
|
|
4
|
+
export interface IParseWrappedKvmCommandOutputOptions {
|
|
5
|
+
commandId: string;
|
|
6
|
+
startMarker: string;
|
|
7
|
+
endMarkerPrefix: string;
|
|
8
|
+
rawText: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface IParseWrappedKvmCommandOutputResult {
|
|
12
|
+
completed: boolean;
|
|
13
|
+
exitCode?: number;
|
|
14
|
+
combinedText: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const createWrappedKvmCommand = (
|
|
18
|
+
command: string,
|
|
19
|
+
shellHint: TKvmShellHint
|
|
20
|
+
): IWrappedKvmCommand => {
|
|
21
|
+
const commandId = plugins.crypto.randomUUID().replace(/-/g, '');
|
|
22
|
+
const startMarker = `SMARTKVM_START_${commandId}`;
|
|
23
|
+
const endMarkerPrefix = `SMARTKVM_END_${commandId}_`;
|
|
24
|
+
|
|
25
|
+
let textToType: string;
|
|
26
|
+
switch (shellHint) {
|
|
27
|
+
case 'powershell':
|
|
28
|
+
textToType = `$__smartkvm_id = "${commandId}"; Write-Output "${startMarker}"; $__smartkvm_status = 0; try { ${command}; if ($null -ne $LASTEXITCODE) { $__smartkvm_status = $LASTEXITCODE } } catch { Write-Output $_; $__smartkvm_status = 1 }; Write-Output "${endMarkerPrefix}$__smartkvm_status"`;
|
|
29
|
+
break;
|
|
30
|
+
case 'cmd':
|
|
31
|
+
textToType = `echo ${startMarker} & ${command} & echo ${endMarkerPrefix}%ERRORLEVEL%`;
|
|
32
|
+
break;
|
|
33
|
+
case 'bash':
|
|
34
|
+
case 'zsh':
|
|
35
|
+
case 'sh':
|
|
36
|
+
case 'unknown':
|
|
37
|
+
default:
|
|
38
|
+
textToType = `printf '\n${startMarker}\n'; ${command}; __smartkvm_status=$?; printf '\n${endMarkerPrefix}%s\n' "$__smartkvm_status"`;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
commandId,
|
|
44
|
+
shellHint,
|
|
45
|
+
command,
|
|
46
|
+
textToType,
|
|
47
|
+
startMarker,
|
|
48
|
+
endMarkerPrefix,
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const parseWrappedKvmCommandOutput = (
|
|
53
|
+
options: IParseWrappedKvmCommandOutputOptions
|
|
54
|
+
): IParseWrappedKvmCommandOutputResult => {
|
|
55
|
+
const normalizedRawText = options.rawText.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
56
|
+
const startIndex = normalizedRawText.indexOf(options.startMarker);
|
|
57
|
+
if (startIndex === -1) {
|
|
58
|
+
return {
|
|
59
|
+
completed: false,
|
|
60
|
+
combinedText: normalizedRawText,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const contentStartIndex = startIndex + options.startMarker.length;
|
|
65
|
+
const endIndex = normalizedRawText.indexOf(options.endMarkerPrefix, contentStartIndex);
|
|
66
|
+
if (endIndex === -1) {
|
|
67
|
+
return {
|
|
68
|
+
completed: false,
|
|
69
|
+
combinedText: normalizedRawText,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const endMarkerValueStartIndex = endIndex + options.endMarkerPrefix.length;
|
|
74
|
+
const endMarkerLine = normalizedRawText.slice(endMarkerValueStartIndex).split('\n')[0].trim();
|
|
75
|
+
const exitCodeMatch = endMarkerLine.match(/^-?\d+/);
|
|
76
|
+
const exitCode = exitCodeMatch ? Number.parseInt(exitCodeMatch[0], 10) : undefined;
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
completed: true,
|
|
80
|
+
exitCode,
|
|
81
|
+
combinedText: normalizedRawText.slice(contentStartIndex, endIndex).trim(),
|
|
82
|
+
};
|
|
83
|
+
};
|