@aiscene/shared 8.0.2 → 8.0.4
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/es/agent-tools/agent-behavior-init-args.mjs +44 -0
- package/dist/es/agent-tools/base-tools.mjs +163 -0
- package/dist/es/agent-tools/chrome-path.mjs +50 -0
- package/dist/es/agent-tools/cli-report-session.mjs +78 -0
- package/dist/es/agent-tools/error-formatter.mjs +106 -0
- package/dist/es/agent-tools/index.mjs +9 -0
- package/dist/es/agent-tools/init-arg-utils.mjs +38 -0
- package/dist/es/agent-tools/observation-artifact.mjs +5 -0
- package/dist/es/agent-tools/observation-record.mjs +248 -0
- package/dist/es/agent-tools/tool-defaults.mjs +54 -0
- package/dist/es/agent-tools/tool-generator.mjs +529 -0
- package/dist/es/agent-tools/types.mjs +3 -0
- package/dist/es/agent-tools/user-prompt.mjs +66 -0
- package/dist/es/cli/cli-args.mjs +95 -0
- package/dist/es/cli/cli-error.mjs +24 -0
- package/dist/es/cli/cli-runner.mjs +10 -40
- package/dist/es/cli/index.mjs +4 -2
- package/dist/es/cli/interrupt.mjs +116 -0
- package/dist/es/cli/record-command.mjs +130 -0
- package/dist/es/cli/screenshot-file.mjs +24 -0
- package/dist/es/cli/verbose-ai-act.mjs +230 -0
- package/dist/es/cli/verbose-screenshot.mjs +134 -0
- package/dist/es/cli/verbose.mjs +404 -0
- package/dist/es/constants/example-code.mjs +2 -2
- package/dist/es/constants/index.mjs +6 -1
- package/dist/es/env/model-config-manager.mjs +3 -0
- package/dist/es/env/parse-model-config.mjs +6 -1
- package/dist/es/env/types.mjs +4 -1
- package/dist/es/env/utils.mjs +1 -9
- package/dist/es/extractor/index.mjs +2 -2
- package/dist/es/img/index.mjs +2 -2
- package/dist/es/img/info.mjs +58 -17
- package/dist/es/img/photon-loader.mjs +4 -0
- package/dist/es/key-alias-utils.mjs +19 -0
- package/dist/es/mcp/base-server.mjs +10 -9
- package/dist/es/mcp/base-tools.mjs +72 -4
- package/dist/es/mcp/cli-report-session.mjs +78 -0
- package/dist/es/mcp/error-formatter.mjs +19 -0
- package/dist/es/mcp/index.mjs +2 -0
- package/dist/es/mcp/init-arg-utils.mjs +38 -0
- package/dist/es/mcp/inject-report-html-plugin.mjs +1 -1
- package/dist/es/mcp/tool-generator.mjs +65 -17
- package/dist/es/node/fs.mjs +1 -1
- package/dist/es/recorder.mjs +249 -0
- package/dist/lib/agent-tools/agent-behavior-init-args.js +87 -0
- package/dist/lib/agent-tools/base-tools.js +197 -0
- package/dist/lib/agent-tools/chrome-path.js +87 -0
- package/dist/lib/agent-tools/cli-report-session.js +121 -0
- package/dist/lib/agent-tools/error-formatter.js +149 -0
- package/dist/lib/agent-tools/index.js +114 -0
- package/dist/lib/agent-tools/init-arg-utils.js +78 -0
- package/dist/lib/agent-tools/observation-artifact.js +42 -0
- package/dist/lib/agent-tools/observation-record.js +297 -0
- package/dist/lib/agent-tools/tool-defaults.js +97 -0
- package/dist/lib/agent-tools/tool-generator.js +569 -0
- package/dist/lib/agent-tools/types.js +40 -0
- package/dist/lib/agent-tools/user-prompt.js +103 -0
- package/dist/lib/cli/cli-args.js +138 -0
- package/dist/lib/cli/cli-error.js +61 -0
- package/dist/lib/cli/cli-runner.js +19 -46
- package/dist/lib/cli/index.js +8 -3
- package/dist/lib/cli/interrupt.js +156 -0
- package/dist/lib/cli/record-command.js +164 -0
- package/dist/lib/cli/screenshot-file.js +58 -0
- package/dist/lib/cli/verbose-ai-act.js +273 -0
- package/dist/lib/cli/verbose-screenshot.js +177 -0
- package/dist/lib/cli/verbose.js +465 -0
- package/dist/lib/constants/example-code.js +2 -2
- package/dist/lib/constants/index.js +23 -3
- package/dist/lib/env/model-config-manager.js +3 -0
- package/dist/lib/env/parse-model-config.js +6 -1
- package/dist/lib/env/types.js +9 -0
- package/dist/lib/env/utils.js +0 -11
- package/dist/lib/extractor/index.js +3 -0
- package/dist/lib/img/index.js +3 -0
- package/dist/lib/img/info.js +65 -18
- package/dist/lib/img/photon-loader.js +38 -0
- package/dist/lib/key-alias-utils.js +62 -0
- package/dist/lib/mcp/base-server.js +11 -10
- package/dist/lib/mcp/base-tools.js +74 -6
- package/dist/lib/mcp/cli-report-session.js +121 -0
- package/dist/lib/mcp/error-formatter.js +53 -0
- package/dist/lib/mcp/index.js +24 -10
- package/dist/lib/mcp/init-arg-utils.js +78 -0
- package/dist/lib/mcp/inject-report-html-plugin.js +1 -1
- package/dist/lib/mcp/tool-generator.js +74 -26
- package/dist/lib/node/fs.js +1 -1
- package/dist/lib/recorder.js +307 -0
- package/dist/types/agent-tools/agent-behavior-init-args.d.ts +17 -0
- package/dist/types/agent-tools/base-tools.d.ts +158 -0
- package/dist/types/agent-tools/chrome-path.d.ts +2 -0
- package/dist/types/agent-tools/cli-report-session.d.ts +12 -0
- package/dist/types/agent-tools/error-formatter.d.ts +30 -0
- package/dist/types/agent-tools/index.d.ts +9 -0
- package/dist/types/agent-tools/init-arg-utils.d.ts +13 -0
- package/dist/types/agent-tools/observation-artifact.d.ts +10 -0
- package/dist/types/agent-tools/observation-record.d.ts +38 -0
- package/dist/types/agent-tools/tool-defaults.d.ts +63 -0
- package/dist/types/agent-tools/tool-generator.d.ts +13 -0
- package/dist/types/agent-tools/types.d.ts +213 -0
- package/dist/types/agent-tools/user-prompt.d.ts +13 -0
- package/dist/types/cli/cli-args.d.ts +8 -0
- package/dist/types/cli/cli-error.d.ts +5 -0
- package/dist/types/cli/cli-runner.d.ts +4 -7
- package/dist/types/cli/index.d.ts +3 -1
- package/dist/types/cli/interrupt.d.ts +49 -0
- package/dist/types/cli/record-command.d.ts +3 -0
- package/dist/types/cli/screenshot-file.d.ts +10 -0
- package/dist/types/cli/verbose-ai-act.d.ts +44 -0
- package/dist/types/cli/verbose-screenshot.d.ts +10 -0
- package/dist/types/cli/verbose.d.ts +40 -0
- package/dist/types/constants/example-code.d.ts +1 -1
- package/dist/types/constants/index.d.ts +5 -0
- package/dist/types/env/types.d.ts +14 -5
- package/dist/types/env/utils.d.ts +0 -31
- package/dist/types/extractor/index.d.ts +1 -1
- package/dist/types/img/index.d.ts +1 -1
- package/dist/types/img/info.d.ts +7 -0
- package/dist/types/img/photon-loader.d.ts +2 -0
- package/dist/types/key-alias-utils.d.ts +9 -0
- package/dist/types/mcp/base-tools.d.ts +74 -5
- package/dist/types/mcp/cli-report-session.d.ts +12 -0
- package/dist/types/mcp/error-formatter.d.ts +12 -0
- package/dist/types/mcp/index.d.ts +2 -0
- package/dist/types/mcp/init-arg-utils.d.ts +13 -0
- package/dist/types/mcp/inject-report-html-plugin.d.ts +2 -2
- package/dist/types/mcp/launcher-helper.d.ts +3 -3
- package/dist/types/mcp/tool-generator.d.ts +3 -3
- package/dist/types/mcp/types.d.ts +10 -0
- package/dist/types/recorder.d.ts +113 -0
- package/package.json +1 -1
- package/src/agent-tools/agent-behavior-init-args.ts +109 -0
- package/src/agent-tools/base-tools.ts +399 -0
- package/src/agent-tools/chrome-path.ts +74 -0
- package/src/agent-tools/cli-report-session.ts +130 -0
- package/src/agent-tools/error-formatter.ts +177 -0
- package/src/agent-tools/index.ts +9 -0
- package/src/agent-tools/init-arg-utils.ts +105 -0
- package/src/agent-tools/observation-artifact.ts +29 -0
- package/src/agent-tools/observation-record.ts +331 -0
- package/src/agent-tools/tool-defaults.ts +119 -0
- package/src/agent-tools/tool-generator.ts +866 -0
- package/src/agent-tools/types.ts +250 -0
- package/src/agent-tools/user-prompt.ts +102 -0
- package/src/cli/cli-args.ts +173 -0
- package/src/cli/cli-error.ts +24 -0
- package/src/cli/cli-runner.ts +37 -56
- package/src/cli/index.ts +3 -7
- package/src/cli/interrupt.ts +207 -0
- package/src/cli/record-command.ts +177 -0
- package/src/cli/screenshot-file.ts +61 -0
- package/src/cli/verbose-ai-act.ts +387 -0
- package/src/cli/verbose-screenshot.ts +269 -0
- package/src/cli/verbose.ts +753 -0
- package/src/constants/example-code.ts +2 -2
- package/src/constants/index.ts +10 -0
- package/src/env/model-config-manager.ts +3 -0
- package/src/env/parse-model-config.ts +19 -1
- package/src/env/types.ts +13 -2
- package/src/env/utils.ts +0 -50
- package/src/extractor/index.ts +1 -1
- package/src/img/index.ts +14 -0
- package/src/img/info.ts +97 -45
- package/src/img/photon-loader.ts +5 -0
- package/src/img/transform.ts +261 -2
- package/src/key-alias-utils.ts +23 -0
- package/src/mcp/base-server.ts +10 -12
- package/src/mcp/base-tools.ts +218 -11
- package/src/mcp/cli-report-session.ts +130 -0
- package/src/mcp/error-formatter.ts +52 -0
- package/src/mcp/index.ts +2 -0
- package/src/mcp/init-arg-utils.ts +105 -0
- package/src/mcp/inject-report-html-plugin.ts +3 -3
- package/src/mcp/launcher-helper.ts +3 -3
- package/src/mcp/tool-generator.ts +98 -20
- package/src/mcp/types.ts +18 -1
- package/src/recorder.ts +625 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/** A compact, transport-safe representation of an unknown thrown value. */
|
|
2
|
+
export interface SerializedError {
|
|
3
|
+
name: string;
|
|
4
|
+
message: string;
|
|
5
|
+
stack?: string;
|
|
6
|
+
code?: string | number;
|
|
7
|
+
status?: string | number;
|
|
8
|
+
requestId?: string | number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const maxSerializedStringLength = 4_096;
|
|
12
|
+
const truncatedStringSuffix = '… [truncated]';
|
|
13
|
+
|
|
14
|
+
interface SerializedErrorResult {
|
|
15
|
+
error: SerializedError;
|
|
16
|
+
hasMessage: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isObject(error: unknown): error is object {
|
|
20
|
+
return (
|
|
21
|
+
(typeof error === 'object' && error !== null) || typeof error === 'function'
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function safelyReadProperty(error: object, key: PropertyKey): unknown {
|
|
26
|
+
try {
|
|
27
|
+
return Reflect.get(error, key);
|
|
28
|
+
} catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readBoundedDiagnostic(
|
|
34
|
+
error: object,
|
|
35
|
+
keys: readonly PropertyKey[],
|
|
36
|
+
): string | number | undefined {
|
|
37
|
+
for (const key of keys) {
|
|
38
|
+
const value = safelyReadProperty(error, key);
|
|
39
|
+
if (typeof value === 'number') {
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
if (typeof value === 'string') {
|
|
43
|
+
return truncateSerializedErrorString(value);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Apply the same string bound used by {@link serializeError}. */
|
|
50
|
+
export function truncateSerializedErrorString(value: string): string {
|
|
51
|
+
if (value.length <= maxSerializedStringLength) {
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return `${value.slice(
|
|
56
|
+
0,
|
|
57
|
+
maxSerializedStringLength - truncatedStringSuffix.length,
|
|
58
|
+
)}${truncatedStringSuffix}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readNonEmptyMessage(error: object): string | undefined {
|
|
62
|
+
const message = safelyReadProperty(error, 'message');
|
|
63
|
+
return typeof message === 'string' && message ? message : undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function extractStringMessage(error: object): string | undefined {
|
|
67
|
+
const directMessage = readNonEmptyMessage(error);
|
|
68
|
+
if (directMessage) {
|
|
69
|
+
return directMessage;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const nestedKey of ['error', 'cause'] as const) {
|
|
73
|
+
const nestedError = safelyReadProperty(error, nestedKey);
|
|
74
|
+
if (!isObject(nestedError)) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const nestedMessage = readNonEmptyMessage(nestedError);
|
|
79
|
+
if (nestedMessage) {
|
|
80
|
+
return nestedMessage;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Extract a human-readable message from an unknown thrown value.
|
|
89
|
+
*
|
|
90
|
+
* Many SDK/transport layers reject with structured objects (e.g.
|
|
91
|
+
* `{ code, message }`, `{ error: { message } }`, `{ cause: { message } }`)
|
|
92
|
+
* rather than `Error` instances. This helper returns the bounded message from
|
|
93
|
+
* {@link serializeError}; message-less objects are summarized from the same
|
|
94
|
+
* diagnostic whitelist instead of serializing arbitrary payload fields.
|
|
95
|
+
*/
|
|
96
|
+
export function getErrorMessage(error: unknown): string {
|
|
97
|
+
const result = serializeErrorValue(error);
|
|
98
|
+
const serialized = result.error;
|
|
99
|
+
if (!isObject(error) || result.hasMessage) {
|
|
100
|
+
return serialized.message;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const summary: SerializedError = {
|
|
104
|
+
name: serialized.name,
|
|
105
|
+
message: serialized.message,
|
|
106
|
+
};
|
|
107
|
+
for (const key of ['code', 'status', 'requestId'] as const) {
|
|
108
|
+
const value = serialized[key];
|
|
109
|
+
if (value !== undefined) {
|
|
110
|
+
summary[key] = value;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return truncateSerializedErrorString(JSON.stringify(summary));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Safely read a stack trace from an unknown thrown value when one exists. */
|
|
118
|
+
export function getErrorStack(error: unknown): string | undefined {
|
|
119
|
+
if (!isObject(error)) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const stack = safelyReadProperty(error, 'stack');
|
|
124
|
+
return typeof stack === 'string' ? stack : undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function serializeErrorValue(error: unknown): SerializedErrorResult {
|
|
128
|
+
if (!isObject(error)) {
|
|
129
|
+
const message = truncateSerializedErrorString(String(error));
|
|
130
|
+
return {
|
|
131
|
+
error: {
|
|
132
|
+
name: 'NonError',
|
|
133
|
+
message: message.trim() ? message : 'Empty string thrown',
|
|
134
|
+
},
|
|
135
|
+
hasMessage: Boolean(message.trim()),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const name = safelyReadProperty(error, 'name');
|
|
140
|
+
const serializedName = truncateSerializedErrorString(
|
|
141
|
+
typeof name === 'string' && name ? name : 'Error',
|
|
142
|
+
);
|
|
143
|
+
const message = extractStringMessage(error);
|
|
144
|
+
const serialized: SerializedError = {
|
|
145
|
+
name: serializedName,
|
|
146
|
+
message: truncateSerializedErrorString(
|
|
147
|
+
message ?? `${serializedName} without a message`,
|
|
148
|
+
),
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const stack = getErrorStack(error);
|
|
152
|
+
if (stack) {
|
|
153
|
+
serialized.stack = truncateSerializedErrorString(stack);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const code = readBoundedDiagnostic(error, ['code']);
|
|
157
|
+
const status = readBoundedDiagnostic(error, ['status', 'statusCode']);
|
|
158
|
+
const requestId = readBoundedDiagnostic(error, ['requestId', 'requestID']);
|
|
159
|
+
if (code !== undefined) serialized.code = code;
|
|
160
|
+
if (status !== undefined) serialized.status = status;
|
|
161
|
+
if (requestId !== undefined) serialized.requestId = requestId;
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
error: serialized,
|
|
165
|
+
hasMessage: message !== undefined,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Extract a small, bounded diagnostic object suitable for JSON and test-runner
|
|
171
|
+
* transports. Only message, stack, and a few common diagnostic fields are
|
|
172
|
+
* retained. Arbitrary payloads are never visited, and nested errors are only
|
|
173
|
+
* inspected one level deep when the outer value has no message of its own.
|
|
174
|
+
*/
|
|
175
|
+
export function serializeError(error: unknown): SerializedError {
|
|
176
|
+
return serializeErrorValue(error).error;
|
|
177
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './base-tools';
|
|
2
|
+
export * from './tool-defaults';
|
|
3
|
+
export * from './agent-behavior-init-args';
|
|
4
|
+
export * from './init-arg-utils';
|
|
5
|
+
export * from './error-formatter';
|
|
6
|
+
export * from './tool-generator';
|
|
7
|
+
export * from './types';
|
|
8
|
+
export * from './chrome-path';
|
|
9
|
+
export * from './observation-record';
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import { getKeyAliases, isRecord } from '../key-alias-utils';
|
|
3
|
+
import type { ToolSchema } from './types';
|
|
4
|
+
|
|
5
|
+
function readAliasedValue(
|
|
6
|
+
args: Record<string, unknown>,
|
|
7
|
+
key: string,
|
|
8
|
+
): unknown | undefined {
|
|
9
|
+
for (const alias of getKeyAliases(key)) {
|
|
10
|
+
if (alias in args) {
|
|
11
|
+
return args[alias];
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readNamespacedArg(
|
|
19
|
+
args: Record<string, unknown>,
|
|
20
|
+
namespace: string,
|
|
21
|
+
key: string,
|
|
22
|
+
): unknown | undefined {
|
|
23
|
+
// Lookup order: namespace object first, then flat dotted form, then bare key
|
|
24
|
+
// fallback. Namespace-aware inputs win so multi-platform callers cannot be
|
|
25
|
+
// cross-contaminated by a top-level bare `deviceId` leaking into the wrong
|
|
26
|
+
// platform.
|
|
27
|
+
const namespacedArgs = readAliasedValue(args, namespace);
|
|
28
|
+
if (isRecord(namespacedArgs)) {
|
|
29
|
+
const nestedValue = readAliasedValue(namespacedArgs, key);
|
|
30
|
+
if (nestedValue !== undefined) {
|
|
31
|
+
return nestedValue;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const dottedValue = readAliasedValue(args, `${namespace}.${key}`);
|
|
36
|
+
if (dottedValue !== undefined) {
|
|
37
|
+
return dottedValue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const directValue = readAliasedValue(args, key);
|
|
41
|
+
if (directValue !== undefined) {
|
|
42
|
+
return directValue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function extractNamespacedArgs<
|
|
49
|
+
TFieldName extends string,
|
|
50
|
+
TArgs extends Record<string, unknown> = Record<string, unknown>,
|
|
51
|
+
>(
|
|
52
|
+
args: Record<string, unknown>,
|
|
53
|
+
namespace: string,
|
|
54
|
+
keys: readonly TFieldName[],
|
|
55
|
+
): TArgs | undefined {
|
|
56
|
+
const extracted: Record<string, unknown> = {};
|
|
57
|
+
|
|
58
|
+
for (const key of keys) {
|
|
59
|
+
const value = readNamespacedArg(args, namespace, key);
|
|
60
|
+
if (value !== undefined) {
|
|
61
|
+
extracted[key] = value;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return Object.keys(extracted).length > 0 ? (extracted as TArgs) : undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function sanitizeNamespacedArgs(
|
|
69
|
+
args: Record<string, unknown>,
|
|
70
|
+
namespace: string,
|
|
71
|
+
keys: readonly string[],
|
|
72
|
+
): Record<string, unknown> {
|
|
73
|
+
const excludedKeys = new Set<string>(getKeyAliases(namespace));
|
|
74
|
+
|
|
75
|
+
for (const key of keys) {
|
|
76
|
+
for (const alias of getKeyAliases(key)) {
|
|
77
|
+
excludedKeys.add(alias);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const alias of getKeyAliases(`${namespace}.${key}`)) {
|
|
81
|
+
excludedKeys.add(alias);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return Object.fromEntries(
|
|
86
|
+
Object.entries(args).filter(([key]) => !excludedKeys.has(key)),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Build a flat tool schema whose keys are dotted `"<namespace>.<field>"`.
|
|
92
|
+
*
|
|
93
|
+
* We intentionally stay flat (rather than `{ namespace: z.object({...}) }`) so
|
|
94
|
+
* that CLI (`--android.device-id`) and `--help` output share the same spelling.
|
|
95
|
+
* `readNamespacedArg` understands all three input shapes:
|
|
96
|
+
* nested namespace object, dotted flat key, and bare key fallback.
|
|
97
|
+
*/
|
|
98
|
+
export function createNamespacedInitArgSchema(
|
|
99
|
+
namespace: string,
|
|
100
|
+
shape: Record<string, z.ZodTypeAny>,
|
|
101
|
+
): ToolSchema {
|
|
102
|
+
return Object.fromEntries(
|
|
103
|
+
Object.entries(shape).map(([key, value]) => [`${namespace}.${key}`, value]),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BaseAgent,
|
|
3
|
+
BaseUIObservation,
|
|
4
|
+
UIObservationRecord,
|
|
5
|
+
} from './types';
|
|
6
|
+
|
|
7
|
+
/** CLI-only bridge between an Agent runtime and observation artifacts. */
|
|
8
|
+
export interface ObservationArtifactAdapter {
|
|
9
|
+
exportRecord(observation: BaseUIObservation): Promise<UIObservationRecord>;
|
|
10
|
+
loadRecord(record: UIObservationRecord): BaseUIObservation;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** @internal Property key used by Core and the CLI artifact commands. */
|
|
14
|
+
export const observationArtifactAdapterSymbol = Symbol(
|
|
15
|
+
'midscene.observationArtifactAdapter',
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
type ObservationArtifactAdapterHost = {
|
|
19
|
+
[observationArtifactAdapterSymbol]?: ObservationArtifactAdapter;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Read the CLI artifact capability attached by the Core Agent. */
|
|
23
|
+
export function resolveObservationArtifactAdapter(
|
|
24
|
+
agent: BaseAgent,
|
|
25
|
+
): ObservationArtifactAdapter | undefined {
|
|
26
|
+
return (agent as ObservationArtifactAdapterHost)[
|
|
27
|
+
observationArtifactAdapterSymbol
|
|
28
|
+
];
|
|
29
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
copyFileSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
renameSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
statSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from 'node:fs';
|
|
13
|
+
import {
|
|
14
|
+
basename,
|
|
15
|
+
dirname,
|
|
16
|
+
extname,
|
|
17
|
+
isAbsolute,
|
|
18
|
+
join,
|
|
19
|
+
relative,
|
|
20
|
+
resolve,
|
|
21
|
+
} from 'node:path';
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
import { getMidsceneRunSubDir } from '../common';
|
|
24
|
+
import type { UIObservationFrame, UIObservationRecord } from './types';
|
|
25
|
+
|
|
26
|
+
const observationRecordSchema = z
|
|
27
|
+
.object({
|
|
28
|
+
type: z.literal('midscene_ui_observation'),
|
|
29
|
+
version: z.literal(1),
|
|
30
|
+
startedAt: z.number().finite().nonnegative(),
|
|
31
|
+
endedAt: z.number().finite().nonnegative(),
|
|
32
|
+
frames: z
|
|
33
|
+
.array(
|
|
34
|
+
z
|
|
35
|
+
.object({
|
|
36
|
+
path: z.string().min(1),
|
|
37
|
+
mimeType: z.enum(['image/png', 'image/jpeg']),
|
|
38
|
+
capturedAt: z.number().finite().nonnegative(),
|
|
39
|
+
})
|
|
40
|
+
.strict(),
|
|
41
|
+
)
|
|
42
|
+
.min(1),
|
|
43
|
+
shotSize: z
|
|
44
|
+
.object({
|
|
45
|
+
width: z.number().finite().positive(),
|
|
46
|
+
height: z.number().finite().positive(),
|
|
47
|
+
})
|
|
48
|
+
.strict(),
|
|
49
|
+
shrunkShotToLogicalRatio: z.number().finite().positive(),
|
|
50
|
+
})
|
|
51
|
+
.strict()
|
|
52
|
+
.refine((record) => record.endedAt >= record.startedAt, {
|
|
53
|
+
path: ['endedAt'],
|
|
54
|
+
message: 'must be greater than or equal to startedAt',
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export function defaultObservationRecordPath(): string {
|
|
58
|
+
const suffix = Math.random().toString(36).slice(2, 10);
|
|
59
|
+
return resolve(
|
|
60
|
+
getMidsceneRunSubDir('output'),
|
|
61
|
+
`observation-${Date.now()}-${suffix}.json`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function parseUIObservationRecord(input: unknown): UIObservationRecord {
|
|
66
|
+
const result = observationRecordSchema.safeParse(input);
|
|
67
|
+
if (!result.success) {
|
|
68
|
+
const issue = result.error.issues[0];
|
|
69
|
+
const location = issue.path.length > 0 ? issue.path.join('.') : 'record';
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Invalid UI observation record at ${location}: ${issue.message}`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return result.data;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Return a detached record so callers cannot mutate runtime-owned state. */
|
|
78
|
+
export function cloneUIObservationRecord(
|
|
79
|
+
record: UIObservationRecord,
|
|
80
|
+
): UIObservationRecord {
|
|
81
|
+
return {
|
|
82
|
+
...record,
|
|
83
|
+
frames: record.frames.map((frame) => ({ ...frame })),
|
|
84
|
+
shotSize: { ...record.shotSize },
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function readUIObservationRecord(filePath: string): UIObservationRecord {
|
|
89
|
+
const resolvedPath = resolve(filePath);
|
|
90
|
+
let parsed: unknown;
|
|
91
|
+
try {
|
|
92
|
+
parsed = JSON.parse(readFileSync(resolvedPath, 'utf8'));
|
|
93
|
+
} catch (error) {
|
|
94
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Failed to read UI observation record ${resolvedPath}: ${message}`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
const record = parseUIObservationRecord(parsed);
|
|
100
|
+
const manifestDirectory = dirname(resolvedPath);
|
|
101
|
+
return {
|
|
102
|
+
...record,
|
|
103
|
+
frames: record.frames.map((frame, index) => {
|
|
104
|
+
if (isAbsolute(frame.path)) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Invalid UI observation record at frames.${index}.path: expected a relative image path`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const imagePath = resolve(manifestDirectory, frame.path);
|
|
110
|
+
const relativeToManifest = relative(manifestDirectory, imagePath);
|
|
111
|
+
if (
|
|
112
|
+
relativeToManifest === '..' ||
|
|
113
|
+
relativeToManifest.startsWith(
|
|
114
|
+
`..${process.platform === 'win32' ? '\\' : '/'}`,
|
|
115
|
+
) ||
|
|
116
|
+
isAbsolute(relativeToManifest)
|
|
117
|
+
) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`Invalid UI observation record at frames.${index}.path: image path escapes the manifest directory`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
if (!statSync(imagePath).isFile()) {
|
|
124
|
+
throw new Error('path is not a file');
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Invalid UI observation record at frames.${index}.path: cannot read image ${imagePath}: ${message}`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return { ...frame, path: imagePath };
|
|
133
|
+
}),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface UIObservationRecordMetadata {
|
|
138
|
+
startedAt: number;
|
|
139
|
+
endedAt: number;
|
|
140
|
+
shotSize: UIObservationRecord['shotSize'];
|
|
141
|
+
shrunkShotToLogicalRatio: number;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Incrementally persists observation frames and exports a runtime record whose
|
|
146
|
+
* paths resolve to those image files.
|
|
147
|
+
*/
|
|
148
|
+
export class UIObservationRecordWriter {
|
|
149
|
+
readonly outputPath: string;
|
|
150
|
+
readonly framesDirectory: string;
|
|
151
|
+
private readonly temporaryFramesDirectory: string;
|
|
152
|
+
private finalized = false;
|
|
153
|
+
private disposed = false;
|
|
154
|
+
private finalizedRecord: UIObservationRecord | null = null;
|
|
155
|
+
|
|
156
|
+
constructor(filePath?: string) {
|
|
157
|
+
this.outputPath = filePath
|
|
158
|
+
? resolve(filePath)
|
|
159
|
+
: defaultObservationRecordPath();
|
|
160
|
+
const extension = extname(this.outputPath);
|
|
161
|
+
const stem = basename(this.outputPath, extension);
|
|
162
|
+
this.framesDirectory = join(dirname(this.outputPath), `${stem}.frames`);
|
|
163
|
+
this.temporaryFramesDirectory = `${this.framesDirectory}.tmp-${process.pid}-${Math.random()
|
|
164
|
+
.toString(36)
|
|
165
|
+
.slice(2, 10)}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
persistFrame(dataUrl: string, capturedAt: number): UIObservationFrame {
|
|
169
|
+
if (this.disposed) {
|
|
170
|
+
throw new Error('UI observation record writer has been disposed');
|
|
171
|
+
}
|
|
172
|
+
if (this.finalized) {
|
|
173
|
+
throw new Error('UI observation record has already been finalized');
|
|
174
|
+
}
|
|
175
|
+
const match = /^data:image\/(png|jpe?g);base64,([\s\S]+)$/i.exec(dataUrl);
|
|
176
|
+
if (!match) {
|
|
177
|
+
throw new Error('UI observation frame must be a PNG or JPEG data URL');
|
|
178
|
+
}
|
|
179
|
+
const format = match[1].toLowerCase() === 'png' ? 'png' : 'jpeg';
|
|
180
|
+
const mimeType = format === 'png' ? 'image/png' : 'image/jpeg';
|
|
181
|
+
const bytes = Buffer.from(match[2], 'base64');
|
|
182
|
+
if (bytes.length === 0) {
|
|
183
|
+
throw new Error('UI observation frame contains no image data');
|
|
184
|
+
}
|
|
185
|
+
const digest = createHash('sha256').update(bytes).digest('hex');
|
|
186
|
+
const fileName = `${digest}.${format}`;
|
|
187
|
+
mkdirSync(this.temporaryFramesDirectory, { recursive: true });
|
|
188
|
+
const temporaryPath = join(this.temporaryFramesDirectory, fileName);
|
|
189
|
+
if (!existsSync(temporaryPath)) {
|
|
190
|
+
writeFileSync(temporaryPath, bytes);
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
path: `${basename(this.framesDirectory)}/${fileName}`,
|
|
194
|
+
mimeType,
|
|
195
|
+
capturedAt,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Resolve a persisted frame before or after the writer is finalized. */
|
|
200
|
+
resolveFramePath(frame: UIObservationFrame): string {
|
|
201
|
+
if (this.disposed) {
|
|
202
|
+
throw new Error('UI observation record writer has been disposed');
|
|
203
|
+
}
|
|
204
|
+
const directory = this.finalized
|
|
205
|
+
? this.framesDirectory
|
|
206
|
+
: this.temporaryFramesDirectory;
|
|
207
|
+
return join(directory, basename(frame.path));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Remove persisted images that are no longer referenced by the frame buffer. */
|
|
211
|
+
pruneFrames(frames: UIObservationFrame[]): void {
|
|
212
|
+
if (this.disposed) {
|
|
213
|
+
throw new Error('UI observation record writer has been disposed');
|
|
214
|
+
}
|
|
215
|
+
if (this.finalized || !existsSync(this.temporaryFramesDirectory)) return;
|
|
216
|
+
|
|
217
|
+
const retainedFiles = new Set(frames.map((frame) => basename(frame.path)));
|
|
218
|
+
for (const fileName of readdirSync(this.temporaryFramesDirectory)) {
|
|
219
|
+
if (!retainedFiles.has(fileName)) {
|
|
220
|
+
rmSync(join(this.temporaryFramesDirectory, fileName), { force: true });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
finalize(
|
|
226
|
+
frames: UIObservationFrame[],
|
|
227
|
+
metadata: UIObservationRecordMetadata,
|
|
228
|
+
): UIObservationRecord {
|
|
229
|
+
if (this.disposed) {
|
|
230
|
+
throw new Error('UI observation record writer has been disposed');
|
|
231
|
+
}
|
|
232
|
+
if (this.finalizedRecord) return this.finalizedRecord;
|
|
233
|
+
const record = parseUIObservationRecord({
|
|
234
|
+
type: 'midscene_ui_observation',
|
|
235
|
+
version: 1,
|
|
236
|
+
frames,
|
|
237
|
+
...metadata,
|
|
238
|
+
});
|
|
239
|
+
this.pruneFrames(record.frames);
|
|
240
|
+
mkdirSync(dirname(this.outputPath), { recursive: true });
|
|
241
|
+
mkdirSync(this.temporaryFramesDirectory, { recursive: true });
|
|
242
|
+
rmSync(this.framesDirectory, { recursive: true, force: true });
|
|
243
|
+
renameSync(this.temporaryFramesDirectory, this.framesDirectory);
|
|
244
|
+
this.finalized = true;
|
|
245
|
+
this.finalizedRecord = {
|
|
246
|
+
...record,
|
|
247
|
+
frames: record.frames.map((frame) => ({
|
|
248
|
+
...frame,
|
|
249
|
+
path: resolve(dirname(this.outputPath), frame.path),
|
|
250
|
+
})),
|
|
251
|
+
};
|
|
252
|
+
return this.finalizedRecord;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Delete writer-owned temporary or finalized frame files. */
|
|
256
|
+
dispose(): void {
|
|
257
|
+
if (this.disposed) return;
|
|
258
|
+
rmSync(this.temporaryFramesDirectory, { recursive: true, force: true });
|
|
259
|
+
rmSync(this.framesDirectory, { recursive: true, force: true });
|
|
260
|
+
this.disposed = true;
|
|
261
|
+
this.finalizedRecord = null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Persist a resolved observation record as a portable JSON manifest plus an
|
|
267
|
+
* adjacent image directory. The input record remains usable after writing.
|
|
268
|
+
*/
|
|
269
|
+
export function writeUIObservationRecord(
|
|
270
|
+
record: UIObservationRecord,
|
|
271
|
+
filePath?: string,
|
|
272
|
+
): string {
|
|
273
|
+
const validated = parseUIObservationRecord(record);
|
|
274
|
+
const outputPath = filePath
|
|
275
|
+
? resolve(filePath)
|
|
276
|
+
: defaultObservationRecordPath();
|
|
277
|
+
const extension = extname(outputPath);
|
|
278
|
+
const stem = basename(outputPath, extension);
|
|
279
|
+
const framesDirectory = join(dirname(outputPath), `${stem}.frames`);
|
|
280
|
+
const temporaryFramesDirectory = `${framesDirectory}.tmp-${process.pid}-${Math.random()
|
|
281
|
+
.toString(36)
|
|
282
|
+
.slice(2, 10)}`;
|
|
283
|
+
const temporaryManifest = `${outputPath}.tmp-${process.pid}-${Math.random()
|
|
284
|
+
.toString(36)
|
|
285
|
+
.slice(2, 10)}`;
|
|
286
|
+
const copiedPaths = new Map<string, string>();
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
mkdirSync(temporaryFramesDirectory, { recursive: true });
|
|
290
|
+
const frames = validated.frames.map((frame, index) => {
|
|
291
|
+
const sourcePath = resolve(frame.path);
|
|
292
|
+
try {
|
|
293
|
+
if (!statSync(sourcePath).isFile()) {
|
|
294
|
+
throw new Error('path is not a file');
|
|
295
|
+
}
|
|
296
|
+
} catch (error) {
|
|
297
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
298
|
+
throw new Error(
|
|
299
|
+
`Invalid UI observation record at frames.${index}.path: cannot read image ${sourcePath}: ${message}`,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
let relativePath = copiedPaths.get(sourcePath);
|
|
304
|
+
if (!relativePath) {
|
|
305
|
+
const imageExtension = frame.mimeType === 'image/png' ? 'png' : 'jpeg';
|
|
306
|
+
const fileName = `${String(copiedPaths.size).padStart(4, '0')}.${imageExtension}`;
|
|
307
|
+
relativePath = `${basename(framesDirectory)}/${fileName}`;
|
|
308
|
+
copyFileSync(sourcePath, join(temporaryFramesDirectory, fileName));
|
|
309
|
+
copiedPaths.set(sourcePath, relativePath);
|
|
310
|
+
}
|
|
311
|
+
return { ...frame, path: relativePath };
|
|
312
|
+
});
|
|
313
|
+
const serializedRecord = parseUIObservationRecord({
|
|
314
|
+
...validated,
|
|
315
|
+
frames,
|
|
316
|
+
});
|
|
317
|
+
mkdirSync(dirname(outputPath), { recursive: true });
|
|
318
|
+
writeFileSync(
|
|
319
|
+
temporaryManifest,
|
|
320
|
+
JSON.stringify(serializedRecord, null, 2),
|
|
321
|
+
'utf8',
|
|
322
|
+
);
|
|
323
|
+
rmSync(framesDirectory, { recursive: true, force: true });
|
|
324
|
+
renameSync(temporaryFramesDirectory, framesDirectory);
|
|
325
|
+
renameSync(temporaryManifest, outputPath);
|
|
326
|
+
return outputPath;
|
|
327
|
+
} finally {
|
|
328
|
+
rmSync(temporaryFramesDirectory, { recursive: true, force: true });
|
|
329
|
+
rmSync(temporaryManifest, { force: true });
|
|
330
|
+
}
|
|
331
|
+
}
|