@minicor/mcp-server 3.6.0 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -1
- package/dist/__tests__/builder-discovery.test.d.ts +2 -0
- package/dist/__tests__/builder-discovery.test.d.ts.map +1 -0
- package/dist/__tests__/builder-discovery.test.js +120 -0
- package/dist/__tests__/builder-discovery.test.js.map +1 -0
- package/dist/__tests__/codemode.test.js +35 -0
- package/dist/__tests__/codemode.test.js.map +1 -1
- package/dist/__tests__/config-stores.test.d.ts +2 -0
- package/dist/__tests__/config-stores.test.d.ts.map +1 -0
- package/dist/__tests__/config-stores.test.js +88 -0
- package/dist/__tests__/config-stores.test.js.map +1 -0
- package/dist/__tests__/dev-mode-draft.test.d.ts +2 -0
- package/dist/__tests__/dev-mode-draft.test.d.ts.map +1 -0
- package/dist/__tests__/dev-mode-draft.test.js +260 -0
- package/dist/__tests__/dev-mode-draft.test.js.map +1 -0
- package/dist/__tests__/disk-token-store.test.d.ts +2 -0
- package/dist/__tests__/disk-token-store.test.d.ts.map +1 -0
- package/dist/__tests__/disk-token-store.test.js +231 -0
- package/dist/__tests__/disk-token-store.test.js.map +1 -0
- package/dist/__tests__/laminar-client.test.js +154 -0
- package/dist/__tests__/laminar-client.test.js.map +1 -1
- package/dist/__tests__/lib-dev-mode.test.d.ts +2 -0
- package/dist/__tests__/lib-dev-mode.test.d.ts.map +1 -0
- package/dist/__tests__/lib-dev-mode.test.js +48 -0
- package/dist/__tests__/lib-dev-mode.test.js.map +1 -0
- package/dist/__tests__/paths.test.d.ts +2 -0
- package/dist/__tests__/paths.test.d.ts.map +1 -0
- package/dist/__tests__/paths.test.js +55 -0
- package/dist/__tests__/paths.test.js.map +1 -0
- package/dist/__tests__/recording-frames.test.d.ts +2 -0
- package/dist/__tests__/recording-frames.test.d.ts.map +1 -0
- package/dist/__tests__/recording-frames.test.js +46 -0
- package/dist/__tests__/recording-frames.test.js.map +1 -0
- package/dist/__tests__/server-profile.test.d.ts +2 -0
- package/dist/__tests__/server-profile.test.d.ts.map +1 -0
- package/dist/__tests__/server-profile.test.js +311 -0
- package/dist/__tests__/server-profile.test.js.map +1 -0
- package/dist/builder-index.d.ts +3 -0
- package/dist/builder-index.d.ts.map +1 -0
- package/dist/builder-index.js +5 -0
- package/dist/builder-index.js.map +1 -0
- package/dist/cli.js +4 -3
- package/dist/cli.js.map +1 -1
- package/dist/codemode.d.ts +3 -1
- package/dist/codemode.d.ts.map +1 -1
- package/dist/codemode.js +20 -6
- package/dist/codemode.js.map +1 -1
- package/dist/disk-token-store.d.ts +101 -0
- package/dist/disk-token-store.d.ts.map +1 -0
- package/dist/disk-token-store.js +398 -0
- package/dist/disk-token-store.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +98 -111
- package/dist/index.js.map +1 -1
- package/dist/laminar-client.d.ts +111 -0
- package/dist/laminar-client.d.ts.map +1 -1
- package/dist/laminar-client.js +47 -1
- package/dist/laminar-client.js.map +1 -1
- package/dist/lib.d.ts +4 -0
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +53 -58
- package/dist/lib.js.map +1 -1
- package/dist/paths.d.ts +4 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +33 -0
- package/dist/paths.js.map +1 -1
- package/dist/prompts/debug-execution.d.ts.map +1 -1
- package/dist/prompts/debug-execution.js +3 -2
- package/dist/prompts/debug-execution.js.map +1 -1
- package/dist/recording-frames.d.ts +58 -0
- package/dist/recording-frames.d.ts.map +1 -0
- package/dist/recording-frames.js +244 -0
- package/dist/recording-frames.js.map +1 -0
- package/dist/seed-skills.js +3 -2
- package/dist/seed-skills.js.map +1 -1
- package/dist/server-profile.d.ts +11 -0
- package/dist/server-profile.d.ts.map +1 -0
- package/dist/server-profile.js +104 -0
- package/dist/server-profile.js.map +1 -0
- package/dist/setup.d.ts +1 -0
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +81 -71
- package/dist/setup.js.map +1 -1
- package/dist/tools/builder-discovery.d.ts +17 -0
- package/dist/tools/builder-discovery.d.ts.map +1 -0
- package/dist/tools/builder-discovery.js +131 -0
- package/dist/tools/builder-discovery.js.map +1 -0
- package/dist/tools/core.d.ts.map +1 -1
- package/dist/tools/core.js +117 -43
- package/dist/tools/core.js.map +1 -1
- package/dist/tools/dev-mode-draft.d.ts +3 -0
- package/dist/tools/dev-mode-draft.d.ts.map +1 -0
- package/dist/tools/dev-mode-draft.js +279 -0
- package/dist/tools/dev-mode-draft.js.map +1 -0
- package/dist/tools/vm.d.ts.map +1 -1
- package/dist/tools/vm.js +3 -35
- package/dist/tools/vm.js.map +1 -1
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -1
package/dist/lib.d.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
16
16
|
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
17
17
|
import { type Region } from "./paths.js";
|
|
18
|
+
import { type MinicorServerProfile } from "./server-profile.js";
|
|
18
19
|
export interface TokenCredentials {
|
|
19
20
|
access_token: string;
|
|
20
21
|
refresh_token: string;
|
|
@@ -33,6 +34,8 @@ export interface MinicorServerOptions {
|
|
|
33
34
|
vmServiceBaseUrl?: string;
|
|
34
35
|
/** Expose the experimental single code-mode tool instead of the default one-tool-per-operation surface. Defaults to false (opt in via EXPERIMENTAL_USE_CODE_MODE=1). */
|
|
35
36
|
codeMode?: boolean;
|
|
37
|
+
/** Tool and instruction profile. Defaults to the backwards-compatible full server. */
|
|
38
|
+
profile?: MinicorServerProfile;
|
|
36
39
|
}
|
|
37
40
|
/**
|
|
38
41
|
* Create and connect a Minicor MCP server on the given transport.
|
|
@@ -43,5 +46,6 @@ export declare function createMinicorServer(transport: Transport, opts?: Minicor
|
|
|
43
46
|
close: () => Promise<void>;
|
|
44
47
|
}>;
|
|
45
48
|
export type { LaminarAuth } from "./laminar-client.js";
|
|
49
|
+
export type { MinicorServerProfile } from "./server-profile.js";
|
|
46
50
|
export type { ToolDeps } from "./types.js";
|
|
47
51
|
//# sourceMappingURL=lib.d.ts.map
|
package/dist/lib.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+CAA+C,CAAC;AAE/E,OAAO,
|
|
1
|
+
{"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+CAA+C,CAAC;AAE/E,OAAO,EAIL,KAAK,MAAM,EACZ,MAAM,YAAY,CAAC;AASpB,OAAO,EAKL,KAAK,oBAAoB,EAC1B,MAAM,qBAAqB,CAAC;AAK7B,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,+CAA+C;IAC/C,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6DAA6D;IAC7D,iBAAiB,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,gBAAgB,GAAG;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;IACrF,uEAAuE;IACvE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,wKAAwK;IACxK,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sFAAsF;IACtF,OAAO,CAAC,EAAE,oBAAoB,CAAC;CAChC;AAkDD;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,SAAS,EAAE,SAAS,EACpB,IAAI,GAAE,oBAAyB,GAC9B,OAAO,CAAC;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC,CAiJ5D;AAED,YAAY,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,YAAY,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAChE,YAAY,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/lib.js
CHANGED
|
@@ -14,56 +14,12 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
16
16
|
import { LaminarClient } from "./laminar-client.js";
|
|
17
|
-
import {
|
|
17
|
+
import { apiBaseToRegion, resolveApiBase, validateApiBaseUrl, } from "./paths.js";
|
|
18
18
|
import { TokenProvider } from "./token-provider.js";
|
|
19
19
|
import { CodeModeToolRegistry, asMcpServer, buildCodeModeInstructions, shouldUseCodeMode, } from "./codemode.js";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import { register as registerConfigStores } from "./tools/config-stores.js";
|
|
23
|
-
import { register as registerSyncTools } from "./tools/sync-tools.js";
|
|
24
|
-
import { register as registerVm } from "./tools/vm.js";
|
|
25
|
-
import { register as registerVmRpa } from "./tools/vm-rpa.js";
|
|
26
|
-
import { register as registerTfa } from "./tools/tfa.js";
|
|
27
|
-
import { register as registerIssues } from "./tools/issues.js";
|
|
28
|
-
import { register as registerAgents } from "./tools/agents.js";
|
|
29
|
-
import { register as registerSkills } from "./tools/skills.js";
|
|
30
|
-
import { register as registerSession, autoCheckpoint } from "./tools/session.js";
|
|
31
|
-
import { register as registerWorkflowGuide } from "./prompts/workflow-guide.js";
|
|
32
|
-
import { register as registerDebugExecution } from "./prompts/debug-execution.js";
|
|
33
|
-
import { register as registerBuildRpa } from "./prompts/build-rpa.js";
|
|
34
|
-
import { register as registerTfaGuide } from "./prompts/tfa-guide.js";
|
|
35
|
-
import { register as registerWorkspaceSession } from "./prompts/workspace-session.js";
|
|
20
|
+
import { instructionsForProfile, profileUsesSessions, registerProfile, serverNameForProfile, } from "./server-profile.js";
|
|
21
|
+
import { autoCheckpoint } from "./tools/session.js";
|
|
36
22
|
const DEFAULT_VM_SERVICE_URL = "https://lam-vm-service-ernfuwetfa-uc.a.run.app";
|
|
37
|
-
function registerAllTools(deps) {
|
|
38
|
-
registerCore(deps);
|
|
39
|
-
registerWorkflowOps(deps);
|
|
40
|
-
registerConfigStores(deps);
|
|
41
|
-
registerSyncTools(deps);
|
|
42
|
-
registerVm(deps);
|
|
43
|
-
registerVmRpa(deps);
|
|
44
|
-
registerTfa(deps);
|
|
45
|
-
registerIssues(deps);
|
|
46
|
-
registerAgents(deps);
|
|
47
|
-
registerSkills(deps);
|
|
48
|
-
registerSession(deps);
|
|
49
|
-
registerWorkflowGuide(deps);
|
|
50
|
-
registerDebugExecution(deps);
|
|
51
|
-
registerBuildRpa(deps);
|
|
52
|
-
registerTfaGuide(deps);
|
|
53
|
-
registerWorkspaceSession(deps);
|
|
54
|
-
}
|
|
55
|
-
const DEFAULT_BASE_URL = "https://api.laminar.run";
|
|
56
|
-
const MINICOR_INSTRUCTIONS = `You are connected to the Minicor MCP server for RPA workflow automation.
|
|
57
|
-
|
|
58
|
-
CRITICAL: If the current directory contains a minicor.json file, you are in a workspace clone.
|
|
59
|
-
You MUST call session_start(directory) as your FIRST action. It returns the workspace rules
|
|
60
|
-
(AGENTS.md), prior session state for resuming, and all context you need. Follow the phase loop
|
|
61
|
-
it describes.
|
|
62
|
-
|
|
63
|
-
Before ending your session, call session_end() to persist your progress. Other agents (on other
|
|
64
|
-
machines, scheduled, or human-triggered) depend on this to resume where you left off.
|
|
65
|
-
|
|
66
|
-
If you are NOT in a workspace clone, use the tools freely — list_workspaces, clone_workspace, etc.`;
|
|
67
23
|
async function refreshViaApi(apiBase, refreshToken) {
|
|
68
24
|
try {
|
|
69
25
|
const res = await fetch(`${apiBase}/auth/refresh`, {
|
|
@@ -101,36 +57,48 @@ function buildProvider(creds, apiBase, onUpdate) {
|
|
|
101
57
|
* Returns a handle with a `close()` method for cleanup.
|
|
102
58
|
*/
|
|
103
59
|
export async function createMinicorServer(transport, opts = {}) {
|
|
60
|
+
const profile = opts.profile ?? "full";
|
|
104
61
|
const codeModeEnabled = opts.codeMode ?? shouldUseCodeMode();
|
|
105
|
-
const
|
|
62
|
+
const profileInstructions = instructionsForProfile(profile);
|
|
63
|
+
const server = new McpServer({ name: serverNameForProfile(profile), version: "3.8.0" }, {
|
|
106
64
|
instructions: codeModeEnabled
|
|
107
|
-
? buildCodeModeInstructions(
|
|
108
|
-
|
|
65
|
+
? buildCodeModeInstructions(profileInstructions, {
|
|
66
|
+
sessionLifecycle: profileUsesSessions(profile),
|
|
67
|
+
})
|
|
68
|
+
: profileInstructions,
|
|
109
69
|
});
|
|
110
70
|
const clients = new Map();
|
|
111
71
|
const tokens = new Map();
|
|
112
72
|
const baseUrls = new Map();
|
|
73
|
+
const apiBaseOverrides = new Map();
|
|
74
|
+
const tokenProviders = new Map();
|
|
75
|
+
function rebuildRegionClient(region, baseUrl) {
|
|
76
|
+
clients.set(region, new LaminarClient({ type: "bearer", token: tokens.get(region) ?? "" }, baseUrl, tokenProviders.get(region)));
|
|
77
|
+
baseUrls.set(region, baseUrl);
|
|
78
|
+
}
|
|
113
79
|
if (opts.regionCredentials) {
|
|
114
80
|
for (const [r, creds] of Object.entries(opts.regionCredentials)) {
|
|
115
81
|
if (!creds)
|
|
116
82
|
continue;
|
|
117
|
-
const base = creds.baseUrl ||
|
|
83
|
+
const base = creds.baseUrl || resolveApiBase(r);
|
|
118
84
|
const provider = buildProvider(creds, base, (t) => tokens.set(r, t));
|
|
119
85
|
clients.set(r, new LaminarClient({ type: "bearer", token: creds.access_token }, base, provider));
|
|
120
86
|
tokens.set(r, creds.access_token);
|
|
87
|
+
tokenProviders.set(r, provider);
|
|
121
88
|
baseUrls.set(r, base);
|
|
122
89
|
}
|
|
123
90
|
}
|
|
124
91
|
if (clients.size === 0 && opts.credentials) {
|
|
125
|
-
const baseUrl = opts.baseUrl ||
|
|
126
|
-
const region = baseUrl
|
|
92
|
+
const baseUrl = opts.baseUrl || resolveApiBase("us");
|
|
93
|
+
const region = apiBaseToRegion(baseUrl);
|
|
127
94
|
const provider = buildProvider(opts.credentials, baseUrl, (t) => tokens.set(region, t));
|
|
128
95
|
clients.set(region, new LaminarClient({ type: "bearer", token: opts.credentials.access_token }, baseUrl, provider));
|
|
129
96
|
tokens.set(region, opts.credentials.access_token);
|
|
97
|
+
tokenProviders.set(region, provider);
|
|
130
98
|
baseUrls.set(region, baseUrl);
|
|
131
99
|
}
|
|
132
100
|
if (clients.size === 0) {
|
|
133
|
-
const baseUrl = opts.baseUrl ||
|
|
101
|
+
const baseUrl = opts.baseUrl || resolveApiBase("us");
|
|
134
102
|
clients.set("us", new LaminarClient({ type: "bearer", token: "" }, baseUrl));
|
|
135
103
|
tokens.set("us", "");
|
|
136
104
|
baseUrls.set("us", baseUrl);
|
|
@@ -161,24 +129,51 @@ export async function createMinicorServer(transport, opts = {}) {
|
|
|
161
129
|
},
|
|
162
130
|
getApiBase: (region) => {
|
|
163
131
|
const r = region ?? getDefaultRegion();
|
|
164
|
-
return baseUrls.get(r) ??
|
|
132
|
+
return baseUrls.get(r) ?? resolveApiBase(r);
|
|
133
|
+
},
|
|
134
|
+
getStoredApiBase: (region) => {
|
|
135
|
+
const r = region ?? getDefaultRegion();
|
|
136
|
+
return apiBaseOverrides.get(r);
|
|
137
|
+
},
|
|
138
|
+
setApiBase: async (region, baseUrl) => {
|
|
139
|
+
if (!clients.has(region)) {
|
|
140
|
+
throw new Error(`No client configured for ${region.toUpperCase()} region.`);
|
|
141
|
+
}
|
|
142
|
+
const storedApiBase = validateApiBaseUrl(baseUrl);
|
|
143
|
+
apiBaseOverrides.set(region, storedApiBase);
|
|
144
|
+
const effectiveApiBase = resolveApiBase(region, storedApiBase);
|
|
145
|
+
rebuildRegionClient(region, effectiveApiBase);
|
|
146
|
+
return { storedApiBase, effectiveApiBase };
|
|
147
|
+
},
|
|
148
|
+
unsetApiBase: async (region) => {
|
|
149
|
+
if (!clients.has(region)) {
|
|
150
|
+
throw new Error(`No client configured for ${region.toUpperCase()} region.`);
|
|
151
|
+
}
|
|
152
|
+
apiBaseOverrides.delete(region);
|
|
153
|
+
const effectiveApiBase = resolveApiBase(region);
|
|
154
|
+
rebuildRegionClient(region, effectiveApiBase);
|
|
155
|
+
return { storedApiBase: undefined, effectiveApiBase };
|
|
165
156
|
},
|
|
166
157
|
getVmServiceBase: () => vmServiceBase,
|
|
167
158
|
getAuthToken: (region) => tokens.get(region ?? getDefaultRegion()) ?? "",
|
|
168
159
|
availableRegions: () => [...clients.keys()],
|
|
169
160
|
};
|
|
170
|
-
|
|
161
|
+
registerProfile(deps, profile);
|
|
171
162
|
if (codeModeRegistry) {
|
|
172
163
|
codeModeRegistry.registerCodeModeTool();
|
|
173
164
|
}
|
|
174
165
|
await server.connect(transport);
|
|
175
166
|
transport.onclose = async () => {
|
|
176
|
-
|
|
167
|
+
if (profileUsesSessions(profile)) {
|
|
168
|
+
await autoCheckpoint(deps);
|
|
169
|
+
}
|
|
177
170
|
};
|
|
178
171
|
return {
|
|
179
172
|
server,
|
|
180
173
|
close: async () => {
|
|
181
|
-
|
|
174
|
+
if (profileUsesSessions(profile)) {
|
|
175
|
+
await autoCheckpoint(deps);
|
|
176
|
+
}
|
|
182
177
|
try {
|
|
183
178
|
await server.close();
|
|
184
179
|
}
|
package/dist/lib.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lib.js","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,
|
|
1
|
+
{"version":3,"file":"lib.js","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"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"}
|
package/dist/paths.d.ts
CHANGED
|
@@ -13,5 +13,9 @@ export declare function getConfigPath(): string;
|
|
|
13
13
|
export declare function getWriteConfigPath(): string;
|
|
14
14
|
export type Region = "us" | "ca";
|
|
15
15
|
export declare function regionToApiBase(region: Region): string;
|
|
16
|
+
export declare function normalizeBaseUrl(baseUrl: string): string;
|
|
17
|
+
export declare function validateApiBaseUrl(baseUrl: string): string;
|
|
18
|
+
export declare function resolveDefaultApiBase(region: Region): string;
|
|
19
|
+
export declare function resolveApiBase(region: Region, storedApiBase?: string | null): string;
|
|
16
20
|
export declare function apiBaseToRegion(apiBase: string): Region;
|
|
17
21
|
//# sourceMappingURL=paths.d.ts.map
|
package/dist/paths.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,QAAA,MAAM,WAAW,QAAsC,CAAC;AAGxD,OAAO,EAAE,WAAW,EAAE,CAAC;AAEvB,wBAAgB,YAAY,IAAI,MAAM,CAMrC;AAED,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,wBAAgB,aAAa,IAAI,MAAM,CAMtC;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,MAAM,MAAM,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;AAEjC,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAItD;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEvD"}
|
|
1
|
+
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,QAAA,MAAM,WAAW,QAAsC,CAAC;AAGxD,OAAO,EAAE,WAAW,EAAE,CAAC;AAEvB,wBAAgB,YAAY,IAAI,MAAM,CAMrC;AAED,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,wBAAgB,aAAa,IAAI,MAAM,CAMtC;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,MAAM,MAAM,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;AAEjC,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAItD;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAe1D;AAWD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,EACd,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,GAC5B,MAAM,CAMR;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEvD"}
|
package/dist/paths.js
CHANGED
|
@@ -40,6 +40,39 @@ export function regionToApiBase(region) {
|
|
|
40
40
|
? "https://ca.api.laminar.run"
|
|
41
41
|
: "https://api.laminar.run";
|
|
42
42
|
}
|
|
43
|
+
export function normalizeBaseUrl(baseUrl) {
|
|
44
|
+
return baseUrl.trim().replace(/\/+$/, "");
|
|
45
|
+
}
|
|
46
|
+
export function validateApiBaseUrl(baseUrl) {
|
|
47
|
+
const normalized = normalizeBaseUrl(baseUrl);
|
|
48
|
+
let parsed;
|
|
49
|
+
try {
|
|
50
|
+
parsed = new URL(normalized);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
throw new Error("Backend API URL must be a valid URL.");
|
|
54
|
+
}
|
|
55
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
56
|
+
throw new Error("Backend API URL must start with http:// or https://.");
|
|
57
|
+
}
|
|
58
|
+
return normalized;
|
|
59
|
+
}
|
|
60
|
+
function envApiBase(region) {
|
|
61
|
+
const regional = region === "ca"
|
|
62
|
+
? process.env.MINICOR_API_BASE_URL_CA
|
|
63
|
+
: process.env.MINICOR_API_BASE_URL_US;
|
|
64
|
+
const base = regional ?? process.env.MINICOR_API_BASE_URL;
|
|
65
|
+
return base && base.trim() !== "" ? normalizeBaseUrl(base) : undefined;
|
|
66
|
+
}
|
|
67
|
+
export function resolveDefaultApiBase(region) {
|
|
68
|
+
return envApiBase(region) ?? regionToApiBase(region);
|
|
69
|
+
}
|
|
70
|
+
export function resolveApiBase(region, storedApiBase) {
|
|
71
|
+
const stored = storedApiBase && storedApiBase.trim() !== ""
|
|
72
|
+
? normalizeBaseUrl(storedApiBase)
|
|
73
|
+
: undefined;
|
|
74
|
+
return envApiBase(region) ?? stored ?? regionToApiBase(region);
|
|
75
|
+
}
|
|
43
76
|
export function apiBaseToRegion(apiBase) {
|
|
44
77
|
return apiBase.includes("ca.api") ? "ca" : "us";
|
|
45
78
|
}
|
package/dist/paths.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,UAAU,CAAC,CAAC;AACxD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,UAAU,CAAC,CAAC;AAEvD,OAAO,EAAE,WAAW,EAAE,CAAC;AAEvB,MAAM,UAAU,YAAY;IAC1B,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;IAC1D,IAAI,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC;QAAE,OAAO,WAAW,CAAC;IACnD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;IACxD,IAAI,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC;QAAE,OAAO,UAAU,CAAC;IACjD,OAAO,WAAW,CAAC;AACrB,CAAC;AAED,MAAM,UAAU,iBAAiB;IAC/B,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;IAC1D,IAAI,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC;QAAE,OAAO,WAAW,CAAC;IACnD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;IACxD,IAAI,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC;QAAE,OAAO,UAAU,CAAC;IACjD,OAAO,WAAW,CAAC;AACrB,CAAC;AAED,MAAM,UAAU,kBAAkB;IAChC,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;AAC/C,CAAC;AAID,MAAM,UAAU,eAAe,CAAC,MAAc;IAC5C,OAAO,MAAM,KAAK,IAAI;QACpB,CAAC,CAAC,4BAA4B;QAC9B,CAAC,CAAC,yBAAyB,CAAC;AAChC,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,OAAe;IAC7C,OAAO,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;AAClD,CAAC"}
|
|
1
|
+
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"debug-execution.d.ts","sourceRoot":"","sources":["../../src/prompts/debug-execution.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAI5C,wBAAgB,QAAQ,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,QAAQ,
|
|
1
|
+
{"version":3,"file":"debug-execution.d.ts","sourceRoot":"","sources":["../../src/prompts/debug-execution.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAI5C,wBAAgB,QAAQ,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,QAAQ,QA4EpD"}
|
|
@@ -52,8 +52,9 @@ Analyze the execution, identify failures, explain root causes, and suggest speci
|
|
|
52
52
|
- Look at preceding successful steps for context
|
|
53
53
|
|
|
54
54
|
### 2. Check for video recordings
|
|
55
|
-
-
|
|
56
|
-
-
|
|
55
|
+
- To visually inspect what happened, use \`get_recording_frames\` — it samples full-resolution still frames (as images) from the execution recording, or from a single step when you pass its flow run ID. Great for confirming the on-screen state at the point of failure
|
|
56
|
+
- To hand a full video to a human (or embed it), use \`get_execution_recording\` for a 24h signed URL to the full recording, or \`get_flow_run_recording\` for a single step's recording
|
|
57
|
+
- RPA step executions also include \`recording.blobName\` in their response data (e.g. \`rpa_1_step-name.mp4\`)
|
|
57
58
|
|
|
58
59
|
### 3. If a VM is connected — reproduce on the live VM
|
|
59
60
|
- Use \`replay_execution_step\` with the failed flow run ID to re-run the exact script with before/after screenshots
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"debug-execution.js","sourceRoot":"","sources":["../../src/prompts/debug-execution.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAErC,MAAM,UAAU,QAAQ,CAAC,EAAE,MAAM,EAAE,MAAM,EAAY;IACnD,MAAM,CAAC,MAAM,CACX,0BAA0B,EAC1B,uDAAuD,EACvD;QACE,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,sBAAsB,CAAC;QACvD,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,qBAAqB,CAAC;QACvD,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,mBAAmB,CAAC;KAC5D,EACD,KAAK,EAAE,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE;QAC5C,MAAM,GAAG,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC;QACjC,MAAM,GAAG,GAAG,QAAQ,CAAC,WAAW,CAAC,CAAC;QAClC,MAAM,CAAC,GAAG,MAA4B,CAAC;QACvC,IAAI,aAAkB,CAAC;QACvB,IAAI,SAAc,CAAC;QAEnB,IAAI,CAAC;YACH,aAAa,GAAG,MAAM,MAAM,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;YACvD,SAAS,GAAG,MAAM,MAAM,CAAC,CAAC,CAAC,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC;QACpD,CAAC;QAAC,OAAO,CAAM,EAAE,CAAC;YAChB,OAAO;gBACL,QAAQ,EAAE;oBACR;wBACE,IAAI,EAAE,MAAe;wBACrB,OAAO,EAAE;4BACP,IAAI,EAAE,MAAe;4BACrB,IAAI,EAAE,mCAAmC,CAAC,CAAC,OAAO,EAAE;yBACrD;qBACF;iBACF;aACF,CAAC;QACJ,CAAC;QAED,OAAO;YACL,QAAQ,EAAE;gBACR;oBACE,IAAI,EAAE,MAAe;oBACrB,OAAO,EAAE;wBACP,IAAI,EAAE,MAAe;wBACrB,IAAI,EAAE;;;EAGlB,IAAI,CAAC,SAAS,CAAC;;;EAGf,IAAI,CAAC,aAAa,CAAC
|
|
1
|
+
{"version":3,"file":"debug-execution.js","sourceRoot":"","sources":["../../src/prompts/debug-execution.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAErC,MAAM,UAAU,QAAQ,CAAC,EAAE,MAAM,EAAE,MAAM,EAAY;IACnD,MAAM,CAAC,MAAM,CACX,0BAA0B,EAC1B,uDAAuD,EACvD;QACE,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,sBAAsB,CAAC;QACvD,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,qBAAqB,CAAC;QACvD,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,mBAAmB,CAAC;KAC5D,EACD,KAAK,EAAE,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE;QAC5C,MAAM,GAAG,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC;QACjC,MAAM,GAAG,GAAG,QAAQ,CAAC,WAAW,CAAC,CAAC;QAClC,MAAM,CAAC,GAAG,MAA4B,CAAC;QACvC,IAAI,aAAkB,CAAC;QACvB,IAAI,SAAc,CAAC;QAEnB,IAAI,CAAC;YACH,aAAa,GAAG,MAAM,MAAM,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;YACvD,SAAS,GAAG,MAAM,MAAM,CAAC,CAAC,CAAC,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC;QACpD,CAAC;QAAC,OAAO,CAAM,EAAE,CAAC;YAChB,OAAO;gBACL,QAAQ,EAAE;oBACR;wBACE,IAAI,EAAE,MAAe;wBACrB,OAAO,EAAE;4BACP,IAAI,EAAE,MAAe;4BACrB,IAAI,EAAE,mCAAmC,CAAC,CAAC,OAAO,EAAE;yBACrD;qBACF;iBACF;aACF,CAAC;QACJ,CAAC;QAED,OAAO;YACL,QAAQ,EAAE;gBACR;oBACE,IAAI,EAAE,MAAe;oBACrB,OAAO,EAAE;wBACP,IAAI,EAAE,MAAe;wBACrB,IAAI,EAAE;;;EAGlB,IAAI,CAAC,SAAS,CAAC;;;EAGf,IAAI,CAAC,aAAa,CAAC;;;;;;;;;;;;;;;;;;;;;;;;wEAwBmD;qBAC3D;iBACF;aACF;SACF,CAAC;IACJ,CAAC,CACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts sampled still frames from a recording (MP4) so that a vision-capable
|
|
3
|
+
* model can visually verify or debug an execution.
|
|
4
|
+
*
|
|
5
|
+
* The recording endpoints return a signed URL to an MP4. A model cannot "watch"
|
|
6
|
+
* a video URL, so this module downloads the video and uses ffmpeg to sample a
|
|
7
|
+
* handful of frames, returned as base64 JPEGs. ffmpeg (and ideally ffprobe) must
|
|
8
|
+
* be available on the MCP host; when they are missing we throw a clear error so
|
|
9
|
+
* the caller can fall back to handing the raw URL to a human.
|
|
10
|
+
*/
|
|
11
|
+
export interface ExtractedFrame {
|
|
12
|
+
/** Human-readable label, e.g. "t=12.3s" or "frame 2". */
|
|
13
|
+
label: string;
|
|
14
|
+
/** Frame position in seconds when known, otherwise null. */
|
|
15
|
+
timestampSeconds: number | null;
|
|
16
|
+
/** Base64-encoded JPEG (no data: prefix). */
|
|
17
|
+
base64: string;
|
|
18
|
+
}
|
|
19
|
+
export interface ExtractFramesOptions {
|
|
20
|
+
/** Number of evenly-spaced frames to sample. Default 4. */
|
|
21
|
+
count?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Optional max output width in px. When omitted (the default) frames keep the
|
|
24
|
+
* recording's ORIGINAL resolution — important for reading fine on-screen detail.
|
|
25
|
+
* Only set this if you explicitly want to trade resolution for smaller frames.
|
|
26
|
+
*/
|
|
27
|
+
maxWidth?: number;
|
|
28
|
+
/** Max bytes to download before aborting. Default 200 MB. */
|
|
29
|
+
maxBytes?: number;
|
|
30
|
+
/** Overall download timeout in ms. Default 60s. */
|
|
31
|
+
downloadTimeoutMs?: number;
|
|
32
|
+
/** Per-ffmpeg/ffprobe command timeout in ms. Default 30s. */
|
|
33
|
+
commandTimeoutMs?: number;
|
|
34
|
+
ffmpegPath?: string;
|
|
35
|
+
ffprobePath?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Computes evenly-spaced sample timestamps centered within the duration, so we
|
|
39
|
+
* avoid black/blank frames at the very start and end.
|
|
40
|
+
*/
|
|
41
|
+
export declare function computeSampleTimestamps(durationSeconds: number, count: number): number[];
|
|
42
|
+
/**
|
|
43
|
+
* Builds an ffmpeg scale filter that caps width while preserving aspect ratio.
|
|
44
|
+
* Returns null when no cap is requested, so frames keep their original resolution.
|
|
45
|
+
*/
|
|
46
|
+
export declare function buildScaleFilter(maxWidth?: number): string | null;
|
|
47
|
+
export interface CommandResult {
|
|
48
|
+
code: number | null;
|
|
49
|
+
stdout: string;
|
|
50
|
+
stderr: string;
|
|
51
|
+
}
|
|
52
|
+
export declare function runCommand(command: string, args: string[], timeoutMs: number): Promise<CommandResult>;
|
|
53
|
+
/**
|
|
54
|
+
* Downloads the recording at `url` and returns sampled JPEG frames.
|
|
55
|
+
* Throws with a clear message when ffmpeg is unavailable.
|
|
56
|
+
*/
|
|
57
|
+
export declare function extractFramesFromUrl(url: string, options?: ExtractFramesOptions): Promise<ExtractedFrame[]>;
|
|
58
|
+
//# sourceMappingURL=recording-frames.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recording-frames.d.ts","sourceRoot":"","sources":["../src/recording-frames.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAOH,MAAM,WAAW,cAAc;IAC7B,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,6CAA6C;IAC7C,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,2DAA2D;IAC3D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,6DAA6D;IAC7D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAWD;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,eAAe,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CASxF;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKjE;AAID,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,UAAU,CACxB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,aAAa,CAAC,CA4CxB;AAqHD;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,cAAc,EAAE,CAAC,CAuF3B"}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts sampled still frames from a recording (MP4) so that a vision-capable
|
|
3
|
+
* model can visually verify or debug an execution.
|
|
4
|
+
*
|
|
5
|
+
* The recording endpoints return a signed URL to an MP4. A model cannot "watch"
|
|
6
|
+
* a video URL, so this module downloads the video and uses ffmpeg to sample a
|
|
7
|
+
* handful of frames, returned as base64 JPEGs. ffmpeg (and ideally ffprobe) must
|
|
8
|
+
* be available on the MCP host; when they are missing we throw a clear error so
|
|
9
|
+
* the caller can fall back to handing the raw URL to a human.
|
|
10
|
+
*/
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
import { mkdtemp, open, readFile, readdir, rm } from "node:fs/promises";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
const DEFAULTS = {
|
|
16
|
+
count: 4,
|
|
17
|
+
maxBytes: 200 * 1024 * 1024,
|
|
18
|
+
downloadTimeoutMs: 60_000,
|
|
19
|
+
commandTimeoutMs: 30_000,
|
|
20
|
+
ffmpegPath: "ffmpeg",
|
|
21
|
+
ffprobePath: "ffprobe",
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Computes evenly-spaced sample timestamps centered within the duration, so we
|
|
25
|
+
* avoid black/blank frames at the very start and end.
|
|
26
|
+
*/
|
|
27
|
+
export function computeSampleTimestamps(durationSeconds, count) {
|
|
28
|
+
if (!Number.isFinite(durationSeconds) || durationSeconds <= 0 || count <= 0) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
const n = Math.min(count, Math.max(1, Math.floor(count)));
|
|
32
|
+
return Array.from({ length: n }, (_, i) => {
|
|
33
|
+
const fraction = (i + 0.5) / n;
|
|
34
|
+
return Number((durationSeconds * fraction).toFixed(3));
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Builds an ffmpeg scale filter that caps width while preserving aspect ratio.
|
|
39
|
+
* Returns null when no cap is requested, so frames keep their original resolution.
|
|
40
|
+
*/
|
|
41
|
+
export function buildScaleFilter(maxWidth) {
|
|
42
|
+
if (maxWidth == null || !Number.isFinite(maxWidth) || maxWidth <= 0) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return `scale='min(${maxWidth},iw)':-2`;
|
|
46
|
+
}
|
|
47
|
+
class CommandNotFoundError extends Error {
|
|
48
|
+
}
|
|
49
|
+
export function runCommand(command, args, timeoutMs) {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
52
|
+
let stdout = "";
|
|
53
|
+
let stderr = "";
|
|
54
|
+
let settled = false;
|
|
55
|
+
const timer = setTimeout(() => {
|
|
56
|
+
if (settled)
|
|
57
|
+
return;
|
|
58
|
+
settled = true;
|
|
59
|
+
child.kill("SIGKILL");
|
|
60
|
+
reject(new Error(`${command} timed out after ${timeoutMs}ms`));
|
|
61
|
+
}, timeoutMs);
|
|
62
|
+
child.stdout?.on("data", (d) => (stdout += d.toString()));
|
|
63
|
+
child.stderr?.on("data", (d) => (stderr += d.toString()));
|
|
64
|
+
child.on("error", (err) => {
|
|
65
|
+
if (settled)
|
|
66
|
+
return;
|
|
67
|
+
settled = true;
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
if (err.code === "ENOENT") {
|
|
70
|
+
reject(new CommandNotFoundError(`'${command}' not found on the MCP host`));
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
reject(err);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
child.on("close", (code) => {
|
|
77
|
+
if (settled)
|
|
78
|
+
return;
|
|
79
|
+
settled = true;
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
if (code !== 0) {
|
|
82
|
+
const detail = stderr.trim().split("\n").slice(-3).join("; ");
|
|
83
|
+
reject(new Error(`${command} exited with code ${code}${detail ? `: ${detail}` : ""}`));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
resolve({ code, stdout, stderr });
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
async function downloadToFile(url, dest, maxBytes, timeoutMs) {
|
|
91
|
+
const controller = new AbortController();
|
|
92
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
93
|
+
try {
|
|
94
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
95
|
+
if (!res.ok) {
|
|
96
|
+
throw new Error(`failed to download recording (${res.status})`);
|
|
97
|
+
}
|
|
98
|
+
const contentLength = Number(res.headers.get("content-length") ?? "0");
|
|
99
|
+
if (contentLength && contentLength > maxBytes) {
|
|
100
|
+
throw new Error(`recording too large (${contentLength} bytes > ${maxBytes} limit)`);
|
|
101
|
+
}
|
|
102
|
+
if (!res.body) {
|
|
103
|
+
throw new Error("recording download returned an empty body");
|
|
104
|
+
}
|
|
105
|
+
const handle = await open(dest, "w");
|
|
106
|
+
try {
|
|
107
|
+
let total = 0;
|
|
108
|
+
for await (const chunk of res.body) {
|
|
109
|
+
total += chunk.length;
|
|
110
|
+
if (total > maxBytes) {
|
|
111
|
+
throw new Error(`recording exceeded max size (${maxBytes} bytes)`);
|
|
112
|
+
}
|
|
113
|
+
await handle.write(chunk);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
await handle.close();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function probeDurationSeconds(file, ffprobePath, timeoutMs) {
|
|
125
|
+
try {
|
|
126
|
+
const { stdout } = await runCommand(ffprobePath, ["-v", "error", "-show_entries", "format=duration", "-of", "default=nw=1:nk=1", file], timeoutMs);
|
|
127
|
+
const duration = parseFloat(stdout.trim());
|
|
128
|
+
return Number.isFinite(duration) && duration > 0 ? duration : null;
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
if (e instanceof CommandNotFoundError) {
|
|
132
|
+
return null; // ffprobe missing; caller falls back to thumbnail sampling
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function extractFrameAt(file, timestampSeconds, outPath, scaleFilter, ffmpegPath, timeoutMs) {
|
|
138
|
+
const args = ["-ss", String(timestampSeconds), "-i", file, "-frames:v", "1"];
|
|
139
|
+
if (scaleFilter) {
|
|
140
|
+
args.push("-vf", scaleFilter);
|
|
141
|
+
}
|
|
142
|
+
// -q:v 2 = highest-quality JPEG, so the original resolution is preserved with minimal artifacts.
|
|
143
|
+
args.push("-q:v", "2", "-y", outPath);
|
|
144
|
+
await runCommand(ffmpegPath, args, timeoutMs);
|
|
145
|
+
}
|
|
146
|
+
async function extractThumbnails(file, count, outPattern, scaleFilter, ffmpegPath, timeoutMs) {
|
|
147
|
+
const vf = scaleFilter ? `thumbnail,${scaleFilter}` : "thumbnail";
|
|
148
|
+
await runCommand(ffmpegPath, [
|
|
149
|
+
"-i",
|
|
150
|
+
file,
|
|
151
|
+
"-vf",
|
|
152
|
+
vf,
|
|
153
|
+
"-frames:v",
|
|
154
|
+
String(count),
|
|
155
|
+
"-vsync",
|
|
156
|
+
"vfr",
|
|
157
|
+
"-q:v",
|
|
158
|
+
"2",
|
|
159
|
+
"-y",
|
|
160
|
+
outPattern,
|
|
161
|
+
], timeoutMs);
|
|
162
|
+
}
|
|
163
|
+
async function readJpegBase64(file) {
|
|
164
|
+
try {
|
|
165
|
+
const buf = await readFile(file);
|
|
166
|
+
return buf.length > 0 ? buf.toString("base64") : null;
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Downloads the recording at `url` and returns sampled JPEG frames.
|
|
174
|
+
* Throws with a clear message when ffmpeg is unavailable.
|
|
175
|
+
*/
|
|
176
|
+
export async function extractFramesFromUrl(url, options = {}) {
|
|
177
|
+
const opts = { ...DEFAULTS, ...options };
|
|
178
|
+
const scaleFilter = buildScaleFilter(opts.maxWidth);
|
|
179
|
+
const workDir = await mkdtemp(path.join(tmpdir(), "laminar-frames-"));
|
|
180
|
+
const videoPath = path.join(workDir, "recording.mp4");
|
|
181
|
+
try {
|
|
182
|
+
await downloadToFile(url, videoPath, opts.maxBytes, opts.downloadTimeoutMs);
|
|
183
|
+
const duration = await probeDurationSeconds(videoPath, opts.ffprobePath, opts.commandTimeoutMs);
|
|
184
|
+
const frames = [];
|
|
185
|
+
try {
|
|
186
|
+
if (duration != null) {
|
|
187
|
+
const timestamps = computeSampleTimestamps(duration, opts.count);
|
|
188
|
+
let lastError = null;
|
|
189
|
+
for (let i = 0; i < timestamps.length; i++) {
|
|
190
|
+
const outPath = path.join(workDir, `frame-${i}.jpg`);
|
|
191
|
+
try {
|
|
192
|
+
await extractFrameAt(videoPath, timestamps[i], outPath, scaleFilter, opts.ffmpegPath, opts.commandTimeoutMs);
|
|
193
|
+
}
|
|
194
|
+
catch (e) {
|
|
195
|
+
// ffmpeg missing is fatal; a single bad frame (e.g. a seek that lands
|
|
196
|
+
// past the end) should not abort the whole sample — record it and move on.
|
|
197
|
+
if (e instanceof CommandNotFoundError)
|
|
198
|
+
throw e;
|
|
199
|
+
lastError = e;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const base64 = await readJpegBase64(outPath);
|
|
203
|
+
if (base64) {
|
|
204
|
+
frames.push({
|
|
205
|
+
label: `t=${timestamps[i].toFixed(1)}s`,
|
|
206
|
+
timestampSeconds: timestamps[i],
|
|
207
|
+
base64,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// Surface a genuine failure instead of pretending nothing was recorded.
|
|
212
|
+
if (frames.length === 0 && lastError) {
|
|
213
|
+
throw lastError;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
// No duration (ffprobe missing) — fall back to thumbnail sampling.
|
|
218
|
+
const outPattern = path.join(workDir, "thumb-%03d.jpg");
|
|
219
|
+
await extractThumbnails(videoPath, opts.count, outPattern, scaleFilter, opts.ffmpegPath, opts.commandTimeoutMs);
|
|
220
|
+
const files = (await readdir(workDir))
|
|
221
|
+
.filter((f) => f.startsWith("thumb-") && f.endsWith(".jpg"))
|
|
222
|
+
.sort();
|
|
223
|
+
for (let i = 0; i < files.length; i++) {
|
|
224
|
+
const base64 = await readJpegBase64(path.join(workDir, files[i]));
|
|
225
|
+
if (base64) {
|
|
226
|
+
frames.push({ label: `frame ${i + 1}`, timestampSeconds: null, base64 });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch (e) {
|
|
232
|
+
if (e instanceof CommandNotFoundError) {
|
|
233
|
+
throw new Error("ffmpeg is not installed on the MCP host, so frames cannot be extracted. " +
|
|
234
|
+
"Install ffmpeg, or use get_execution_recording / get_flow_run_recording to get the raw video URL.");
|
|
235
|
+
}
|
|
236
|
+
throw e;
|
|
237
|
+
}
|
|
238
|
+
return frames;
|
|
239
|
+
}
|
|
240
|
+
finally {
|
|
241
|
+
await rm(workDir, { recursive: true, force: true }).catch(() => { });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
//# sourceMappingURL=recording-frames.js.map
|