@aiscene/shared 8.0.3 → 8.0.5

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.
Files changed (105) hide show
  1. package/dist/es/agent-tools/agent-behavior-init-args.mjs +44 -0
  2. package/dist/es/agent-tools/base-tools.mjs +163 -0
  3. package/dist/es/agent-tools/chrome-path.mjs +50 -0
  4. package/dist/es/agent-tools/cli-report-session.mjs +78 -0
  5. package/dist/es/agent-tools/error-formatter.mjs +106 -0
  6. package/dist/es/agent-tools/index.mjs +9 -0
  7. package/dist/es/agent-tools/init-arg-utils.mjs +38 -0
  8. package/dist/es/agent-tools/observation-artifact.mjs +5 -0
  9. package/dist/es/agent-tools/observation-record.mjs +248 -0
  10. package/dist/es/agent-tools/tool-defaults.mjs +54 -0
  11. package/dist/es/agent-tools/tool-generator.mjs +529 -0
  12. package/dist/es/agent-tools/types.mjs +3 -0
  13. package/dist/es/agent-tools/user-prompt.mjs +66 -0
  14. package/dist/es/cli/interrupt.mjs +116 -0
  15. package/dist/es/cli/record-command.mjs +130 -0
  16. package/dist/es/cli/screenshot-file.mjs +24 -0
  17. package/dist/es/cli/verbose-ai-act.mjs +230 -0
  18. package/dist/es/cli/verbose-screenshot.mjs +134 -0
  19. package/dist/es/cli/verbose.mjs +404 -0
  20. package/dist/es/env/parse-model-config.mjs +1 -1
  21. package/dist/es/env/types.mjs +18 -3
  22. package/dist/es/img/index.mjs +3 -3
  23. package/dist/es/img/info.mjs +45 -1
  24. package/dist/es/img/photon-loader.mjs +4 -0
  25. package/dist/es/img/transform.mjs +113 -3
  26. package/dist/es/recorder.mjs +249 -0
  27. package/dist/lib/agent-tools/agent-behavior-init-args.js +87 -0
  28. package/dist/lib/agent-tools/base-tools.js +197 -0
  29. package/dist/lib/agent-tools/chrome-path.js +87 -0
  30. package/dist/lib/agent-tools/cli-report-session.js +121 -0
  31. package/dist/lib/agent-tools/error-formatter.js +149 -0
  32. package/dist/lib/agent-tools/index.js +114 -0
  33. package/dist/lib/agent-tools/init-arg-utils.js +78 -0
  34. package/dist/lib/agent-tools/observation-artifact.js +42 -0
  35. package/dist/lib/agent-tools/observation-record.js +297 -0
  36. package/dist/lib/agent-tools/tool-defaults.js +97 -0
  37. package/dist/lib/agent-tools/tool-generator.js +569 -0
  38. package/dist/lib/agent-tools/types.js +40 -0
  39. package/dist/lib/agent-tools/user-prompt.js +103 -0
  40. package/dist/lib/cli/interrupt.js +156 -0
  41. package/dist/lib/cli/record-command.js +164 -0
  42. package/dist/lib/cli/screenshot-file.js +58 -0
  43. package/dist/lib/cli/verbose-ai-act.js +273 -0
  44. package/dist/lib/cli/verbose-screenshot.js +177 -0
  45. package/dist/lib/cli/verbose.js +465 -0
  46. package/dist/lib/env/parse-model-config.js +1 -1
  47. package/dist/lib/env/types.js +32 -2
  48. package/dist/lib/img/index.js +29 -5
  49. package/dist/lib/img/info.js +48 -1
  50. package/dist/lib/img/photon-loader.js +38 -0
  51. package/dist/lib/img/transform.js +135 -4
  52. package/dist/lib/recorder.js +307 -0
  53. package/dist/types/agent-tools/agent-behavior-init-args.d.ts +17 -0
  54. package/dist/types/agent-tools/base-tools.d.ts +158 -0
  55. package/dist/types/agent-tools/chrome-path.d.ts +2 -0
  56. package/dist/types/agent-tools/cli-report-session.d.ts +12 -0
  57. package/dist/types/agent-tools/error-formatter.d.ts +30 -0
  58. package/dist/types/agent-tools/index.d.ts +9 -0
  59. package/dist/types/agent-tools/init-arg-utils.d.ts +13 -0
  60. package/dist/types/agent-tools/observation-artifact.d.ts +10 -0
  61. package/dist/types/agent-tools/observation-record.d.ts +38 -0
  62. package/dist/types/agent-tools/tool-defaults.d.ts +63 -0
  63. package/dist/types/agent-tools/tool-generator.d.ts +13 -0
  64. package/dist/types/agent-tools/types.d.ts +213 -0
  65. package/dist/types/agent-tools/user-prompt.d.ts +13 -0
  66. package/dist/types/cli/interrupt.d.ts +49 -0
  67. package/dist/types/cli/record-command.d.ts +3 -0
  68. package/dist/types/cli/screenshot-file.d.ts +10 -0
  69. package/dist/types/cli/verbose-ai-act.d.ts +44 -0
  70. package/dist/types/cli/verbose-screenshot.d.ts +10 -0
  71. package/dist/types/cli/verbose.d.ts +40 -0
  72. package/dist/types/env/types.d.ts +16 -7
  73. package/dist/types/img/index.d.ts +2 -2
  74. package/dist/types/img/info.d.ts +2 -0
  75. package/dist/types/img/photon-loader.d.ts +2 -0
  76. package/dist/types/img/transform.d.ts +22 -2
  77. package/dist/types/mcp/types.d.ts +1 -0
  78. package/dist/types/recorder.d.ts +113 -0
  79. package/package.json +1 -1
  80. package/src/agent-tools/agent-behavior-init-args.ts +109 -0
  81. package/src/agent-tools/base-tools.ts +399 -0
  82. package/src/agent-tools/chrome-path.ts +74 -0
  83. package/src/agent-tools/cli-report-session.ts +130 -0
  84. package/src/agent-tools/error-formatter.ts +177 -0
  85. package/src/agent-tools/index.ts +9 -0
  86. package/src/agent-tools/init-arg-utils.ts +105 -0
  87. package/src/agent-tools/observation-artifact.ts +29 -0
  88. package/src/agent-tools/observation-record.ts +331 -0
  89. package/src/agent-tools/tool-defaults.ts +119 -0
  90. package/src/agent-tools/tool-generator.ts +866 -0
  91. package/src/agent-tools/types.ts +250 -0
  92. package/src/agent-tools/user-prompt.ts +102 -0
  93. package/src/cli/interrupt.ts +207 -0
  94. package/src/cli/record-command.ts +177 -0
  95. package/src/cli/screenshot-file.ts +61 -0
  96. package/src/cli/verbose-ai-act.ts +387 -0
  97. package/src/cli/verbose-screenshot.ts +269 -0
  98. package/src/cli/verbose.ts +753 -0
  99. package/src/env/types.ts +30 -2
  100. package/src/img/index.ts +12 -0
  101. package/src/img/info.ts +61 -0
  102. package/src/img/photon-loader.ts +5 -0
  103. package/src/img/transform.ts +262 -3
  104. package/src/mcp/types.ts +2 -0
  105. package/src/recorder.ts +625 -0
@@ -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
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Unified, declarative mechanism for "force a default option on every tool
3
+ * call" behaviors exposed by device and Agent Skill CLIs.
4
+ *
5
+ * Adding a new behavior flag (e.g. `--deep-search`) is a one-line change to
6
+ * {@link TOOL_BEHAVIOR_FLAGS}: declare which default-option "bag" it fills.
7
+ * The tool generator, tools managers and CLI parsing are all generic
8
+ * over {@link ToolDefaults} and never need to learn about individual flags.
9
+ *
10
+ * See https://github.com/web-infra-dev/midscene/issues/2446.
11
+ */
12
+
13
+ /**
14
+ * Default options injected into generated tool calls. Each field is an
15
+ * injection point; an explicit per-call value always wins over these defaults.
16
+ */
17
+ export interface ToolDefaults {
18
+ /**
19
+ * Merged into every locate field of action tools (`Tap`, `Input`, ...).
20
+ * e.g. `{ deepLocate: true }`.
21
+ */
22
+ locate?: Record<string, unknown>;
23
+ /**
24
+ * Merged into the `aiAction` options used by the `act` tool.
25
+ * e.g. `{ deepLocate: true, deepThink: true }`.
26
+ */
27
+ act?: Record<string, unknown>;
28
+ }
29
+
30
+ export interface ToolBehaviorFlag {
31
+ /** Kebab-case CLI flag name, e.g. `deep-locate` (exposed as `--deep-locate`). */
32
+ cli: string;
33
+ /** One-line description for help output. */
34
+ description: string;
35
+ /** Default-option bags this flag turns on when present. */
36
+ defaults: ToolDefaults;
37
+ }
38
+
39
+ /**
40
+ * The single source of truth for behavior flags. Add a row to support a new
41
+ * `--flag`; nothing else in the pipeline needs to change.
42
+ */
43
+ export const TOOL_BEHAVIOR_FLAGS: readonly ToolBehaviorFlag[] = [
44
+ {
45
+ cli: 'deep-locate',
46
+ description:
47
+ 'Force deep locate for every locating operation (better precision for small/ambiguous targets, a bit slower).',
48
+ defaults: { locate: { deepLocate: true }, act: { deepLocate: true } },
49
+ },
50
+ {
51
+ cli: 'deep-think',
52
+ description:
53
+ 'Plan the act tool with deep thinking (richer context and sub-goal decomposition, a bit slower).',
54
+ defaults: { act: { deepThink: true } },
55
+ },
56
+ ];
57
+
58
+ /** Merge two {@link ToolDefaults}, with `b` taking precedence over `a`. */
59
+ export function mergeToolDefaults(
60
+ a: ToolDefaults,
61
+ b: ToolDefaults,
62
+ ): ToolDefaults {
63
+ const locate = { ...a.locate, ...b.locate };
64
+ const act = { ...a.act, ...b.act };
65
+ const result: ToolDefaults = {};
66
+ if (Object.keys(locate).length > 0) {
67
+ result.locate = locate;
68
+ }
69
+ if (Object.keys(act).length > 0) {
70
+ result.act = act;
71
+ }
72
+ return result;
73
+ }
74
+
75
+ /**
76
+ * Resolve the active {@link ToolDefaults} from a predicate that says whether a
77
+ * given flag (by its `cli` name) is enabled.
78
+ */
79
+ export function resolveToolDefaults(
80
+ isEnabled: (cli: string) => boolean,
81
+ ): ToolDefaults {
82
+ return TOOL_BEHAVIOR_FLAGS.reduce<ToolDefaults>(
83
+ (acc, flag) =>
84
+ isEnabled(flag.cli) ? mergeToolDefaults(acc, flag.defaults) : acc,
85
+ {},
86
+ );
87
+ }
88
+
89
+ /**
90
+ * Split argv into the resolved {@link ToolDefaults} and the remaining args.
91
+ *
92
+ * Behavior flags (e.g. `--deep-locate`) are global: they may appear anywhere
93
+ * in argv and are not tied to a specific sub-command. They are recognized by
94
+ * exact kebab-case match and removed so a strict per-command parser never sees them. Every other
95
+ * token is returned untouched and in order for that per-command parser.
96
+ *
97
+ * This is the single place that knows how a behavior flag looks on the command
98
+ * line; the device / Agent Skill CLI resolves defaults from
99
+ * {@link TOOL_BEHAVIOR_FLAGS} through here / {@link resolveToolDefaults}.
100
+ */
101
+ export function stripBehaviorFlags(argv: readonly string[]): {
102
+ rawArgs: string[];
103
+ toolDefaults: ToolDefaults;
104
+ } {
105
+ const enabled = new Set<string>();
106
+ const rawArgs: string[] = [];
107
+ for (const arg of argv) {
108
+ const flag = TOOL_BEHAVIOR_FLAGS.find((f) => arg === `--${f.cli}`);
109
+ if (flag) {
110
+ enabled.add(flag.cli);
111
+ } else {
112
+ rawArgs.push(arg);
113
+ }
114
+ }
115
+ return {
116
+ rawArgs,
117
+ toolDefaults: resolveToolDefaults((cli) => enabled.has(cli)),
118
+ };
119
+ }