@causyne/agent 1.10.19 → 1.10.21

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 (48) hide show
  1. package/docs/ARCHITECTURE.md +20 -0
  2. package/docs/THIRD_PARTY_NOTICES.md +44 -0
  3. package/native-skills/media/creative-studio-pipeline/SKILL.md +52 -0
  4. package/native-skills/media/photo-studio-automation/SKILL.md +53 -0
  5. package/native-skills/media/video-timeline-assembly/SKILL.md +52 -0
  6. package/package.json +1 -1
  7. package/src/agentLoop.js +4 -0
  8. package/src/appChannel.js +7 -3
  9. package/src/cliMode.js +33 -67
  10. package/src/commands.js +56 -8
  11. package/src/compiledSkills.js +15 -0
  12. package/src/computerUse/visionGrounding.js +249 -0
  13. package/src/computerUse/visionGrounding.ts +331 -0
  14. package/src/config.js +80 -11
  15. package/src/creative/apps.js +69 -0
  16. package/src/creative/apps.ts +58 -0
  17. package/src/creative/blenderBatch.js +233 -0
  18. package/src/creative/blenderBatch.ts +217 -0
  19. package/src/creative/pipeline.js +56 -0
  20. package/src/creative/pipeline.ts +82 -0
  21. package/src/creative/studioScripts.js +374 -0
  22. package/src/creative/studioScripts.ts +378 -0
  23. package/src/decisionPolicy.js +2 -0
  24. package/src/decisionPolicy.ts +1 -0
  25. package/src/dynamicCatalogue.js +270 -0
  26. package/src/dynamicCatalogue.ts +283 -0
  27. package/src/entrypoint.js +2 -1
  28. package/src/modelCatalogue.d.ts +5 -0
  29. package/src/modelCatalogue.js +49 -18
  30. package/src/providers/anthropic.js +4 -0
  31. package/src/sandbox.js +8 -1
  32. package/src/toolPacks.js +6 -3
  33. package/src/tools/computerUse.js +139 -0
  34. package/src/tools/computerUse.ts +147 -0
  35. package/src/tools/creativeApps.js +106 -0
  36. package/src/tools/creativeApps.ts +99 -0
  37. package/src/tools/desktopAct.d.ts +10 -0
  38. package/src/tools/registry.js +5 -1
  39. package/src/tools/uiForge.js +132 -0
  40. package/src/tools/uiForge.ts +145 -0
  41. package/src/tui/banner.js +3 -1
  42. package/src/tui/banner.ts +3 -1
  43. package/src/tui/dialogs.js +10 -0
  44. package/src/tui/home.js +29 -72
  45. package/src/tui/setup.js +104 -0
  46. package/src/tui/setup.ts +98 -0
  47. package/src/uiEngine/uiForge.js +211 -0
  48. package/src/uiEngine/uiForge.ts +258 -0
@@ -0,0 +1,249 @@
1
+ /*
2
+ * Copyright (c) 2025 Bytedance, Inc. and its affiliates.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ *
5
+ * Adapted for Causyne Vision Desktop & Coordinate Grounding Engine.
6
+ */
7
+ const DEFAULT_FACTOR = [1000, 1000];
8
+ /**
9
+ * Extract numbers from a coordinate string like "(250, 400)" or "[100, 200, 300, 400]"
10
+ */
11
+ export function extractCoordinateNumbers(raw) {
12
+ const clean = String(raw || '').replace(/[()[\]]/g, ' ').trim();
13
+ if (!clean)
14
+ return [];
15
+ return clean
16
+ .split(/[\s,]+/)
17
+ .map((s) => Number.parseFloat(s))
18
+ .filter((n) => Number.isFinite(n));
19
+ }
20
+ /**
21
+ * Ground normalized coordinates (0-1000) to actual screen pixels.
22
+ */
23
+ export function groundPoint(nums, screen = { width: 1920, height: 1080, scaleFactor: 1 }, factor = DEFAULT_FACTOR) {
24
+ if (nums.length < 2)
25
+ return null;
26
+ const rawX = nums[0] ?? 0;
27
+ const rawY = nums[1] ?? 0;
28
+ const scale = screen.scaleFactor || 1;
29
+ const normX = Math.max(0, Math.min(1, rawX / factor[0]));
30
+ const normY = Math.max(0, Math.min(1, rawY / factor[1]));
31
+ return {
32
+ x: Math.round(normX * screen.width * scale),
33
+ y: Math.round(normY * screen.height * scale)
34
+ };
35
+ }
36
+ /**
37
+ * Ground a bounding box (x1, y1, x2, y2) into its midpoint (center click).
38
+ */
39
+ export function groundBoxCenter(nums, screen = { width: 1920, height: 1080, scaleFactor: 1 }, factor = DEFAULT_FACTOR) {
40
+ if (nums.length < 2)
41
+ return null;
42
+ if (nums.length === 2) {
43
+ return groundPoint(nums, screen, factor);
44
+ }
45
+ const x1 = nums[0] ?? 0;
46
+ const y1 = nums[1] ?? 0;
47
+ const x2 = nums[2] ?? x1;
48
+ const y2 = nums[3] ?? y1;
49
+ const midX = (x1 + x2) / 2;
50
+ const midY = (y1 + y2) / 2;
51
+ return groundPoint([midX, midY], screen, factor);
52
+ }
53
+ /**
54
+ * Parse vision grounding action format:
55
+ * E.g. Action: click(start_box='(240, 500)')
56
+ */
57
+ export function parseSingleAction(rawAction, screenContext = { width: 1920, height: 1080, scaleFactor: 1 }) {
58
+ const line = rawAction.trim();
59
+ if (!line)
60
+ return null;
61
+ const callMatch = line.match(/^([a-zA-Z_0-9]+)\s*\((.*)\)$/);
62
+ if (!callMatch) {
63
+ if (/finished/i.test(line)) {
64
+ return { action: 'finished', rawStr: line };
65
+ }
66
+ return null;
67
+ }
68
+ const funcName = callMatch[1]?.toLowerCase();
69
+ const argsRaw = callMatch[2] || '';
70
+ const argsMap = {};
71
+ const kvRegex = /([a-zA-Z_0-9]+)\s*=\s*['"]([^'"]+)['"]/g;
72
+ let match;
73
+ while ((match = kvRegex.exec(argsRaw)) !== null) {
74
+ const key = match[1];
75
+ const val = match[2];
76
+ if (key && val) {
77
+ argsMap[key] = val;
78
+ }
79
+ }
80
+ if (Object.keys(argsMap).length === 0) {
81
+ const boxMatch = argsRaw.match(/['"]?(\([0-9\s,.]+\)|\[[0-9\s,.]+\])['"]?/);
82
+ if (boxMatch && boxMatch[1]) {
83
+ argsMap['start_box'] = boxMatch[1];
84
+ }
85
+ }
86
+ const actionObj = {
87
+ action: funcName,
88
+ rawStr: line
89
+ };
90
+ const startBox = argsMap['start_box'] || argsMap['point'] || argsMap['box'];
91
+ if (startBox) {
92
+ const nums = extractCoordinateNumbers(startBox);
93
+ const center = groundBoxCenter(nums, screenContext);
94
+ if (center) {
95
+ actionObj.coordinates = center;
96
+ actionObj.startCoordinates = center;
97
+ }
98
+ }
99
+ const endBox = argsMap['end_box'];
100
+ if (endBox) {
101
+ const nums = extractCoordinateNumbers(endBox);
102
+ const center = groundBoxCenter(nums, screenContext);
103
+ if (center) {
104
+ actionObj.endCoordinates = center;
105
+ }
106
+ }
107
+ if (argsMap['content']) {
108
+ actionObj.text = argsMap['content'];
109
+ }
110
+ if (argsMap['text']) {
111
+ actionObj.text = argsMap['text'];
112
+ }
113
+ if (argsMap['key']) {
114
+ actionObj.key = argsMap['key'];
115
+ }
116
+ if (argsMap['direction']) {
117
+ const dir = argsMap['direction'].toLowerCase();
118
+ if (dir === 'up' || dir === 'down' || dir === 'left' || dir === 'right') {
119
+ actionObj.direction = dir;
120
+ }
121
+ }
122
+ if (argsMap['time'] || argsMap['seconds']) {
123
+ const sec = Number.parseFloat(argsMap['time'] || argsMap['seconds'] || '1');
124
+ actionObj.seconds = Number.isFinite(sec) ? sec : 1;
125
+ }
126
+ return actionObj;
127
+ }
128
+ /**
129
+ * Parse complete vision grounding prediction text with Thought and Action blocks.
130
+ */
131
+ export function parseVisionGroundingPrediction(predictionText, screenContext = { width: 1920, height: 1080, scaleFactor: 1 }) {
132
+ let thought;
133
+ let reflection;
134
+ const text = (predictionText || '').trim();
135
+ if (text.includes('Thought:')) {
136
+ const tMatch = text.match(/Thought:\s*([\s\S]+?)(?=\s*Action[::]|$)/i);
137
+ if (tMatch && tMatch[1]) {
138
+ thought = tMatch[1].trim();
139
+ }
140
+ }
141
+ if (text.includes('Reflection:')) {
142
+ const rMatch = text.match(/Reflection:\s*([\s\S]+?)(?=\s*(?:Action_Summary|Action)[::]|$)/i);
143
+ if (rMatch && rMatch[1]) {
144
+ reflection = rMatch[1].trim();
145
+ }
146
+ }
147
+ const actionLines = [];
148
+ if (/Action[::]/i.test(text)) {
149
+ const parts = text.split(/Action[::]/i);
150
+ for (let i = 1; i < parts.length; i++) {
151
+ const seg = parts[i] || '';
152
+ const lines = seg.split(/\n+/).map((l) => l.trim()).filter(Boolean);
153
+ actionLines.push(...lines);
154
+ }
155
+ }
156
+ else {
157
+ actionLines.push(...text.split(/\n+/).map((l) => l.trim()).filter(Boolean));
158
+ }
159
+ const actions = [];
160
+ for (const line of actionLines) {
161
+ const parsed = parseSingleAction(line, screenContext);
162
+ if (parsed) {
163
+ if (thought && !parsed.thought)
164
+ parsed.thought = thought;
165
+ if (reflection && !parsed.reflection)
166
+ parsed.reflection = reflection;
167
+ actions.push(parsed);
168
+ }
169
+ }
170
+ return { thought, reflection, actions };
171
+ }
172
+ const MAC_KEY_CODES = {
173
+ return: 36,
174
+ enter: 36,
175
+ tab: 48,
176
+ space: 49,
177
+ backspace: 51,
178
+ escape: 53,
179
+ esc: 53,
180
+ up: 126,
181
+ down: 125,
182
+ left: 123,
183
+ right: 124
184
+ };
185
+ /**
186
+ * Convert parsed vision grounding actions into Causyne desktop_act equivalent instructions.
187
+ */
188
+ export function visionGroundingToDesktopAct(actions) {
189
+ const result = [];
190
+ for (const act of actions) {
191
+ switch (act.action) {
192
+ case 'click':
193
+ case 'left_click':
194
+ case 'double_click':
195
+ case 'triple_click': {
196
+ const point = act.coordinates;
197
+ if (point) {
198
+ result.push({
199
+ verb: 'clickxy',
200
+ x: point.x,
201
+ y: point.y
202
+ });
203
+ if (act.action === 'double_click') {
204
+ result.push({ verb: 'clickxy', x: point.x, y: point.y });
205
+ }
206
+ }
207
+ break;
208
+ }
209
+ case 'type': {
210
+ if (act.coordinates) {
211
+ result.push({ verb: 'clickxy', x: act.coordinates.x, y: act.coordinates.y });
212
+ }
213
+ if (act.text) {
214
+ result.push({ verb: 'type', text: act.text });
215
+ }
216
+ break;
217
+ }
218
+ case 'press_key':
219
+ case 'hotkey': {
220
+ const keyName = (act.key || '').toLowerCase().trim();
221
+ const code = MAC_KEY_CODES[keyName];
222
+ if (code !== undefined) {
223
+ result.push({ verb: 'key', keyCode: code });
224
+ }
225
+ else if (keyName) {
226
+ result.push({ verb: 'type', text: keyName });
227
+ }
228
+ break;
229
+ }
230
+ case 'wait': {
231
+ result.push({ verb: 'wait', seconds: act.seconds || 1 });
232
+ break;
233
+ }
234
+ case 'drag': {
235
+ if (act.startCoordinates) {
236
+ result.push({ verb: 'clickxy', x: act.startCoordinates.x, y: act.startCoordinates.y });
237
+ }
238
+ if (act.endCoordinates) {
239
+ result.push({ verb: 'clickxy', x: act.endCoordinates.x, y: act.endCoordinates.y });
240
+ }
241
+ break;
242
+ }
243
+ case 'finished':
244
+ default:
245
+ break;
246
+ }
247
+ }
248
+ return result;
249
+ }
@@ -0,0 +1,331 @@
1
+ /*
2
+ * Copyright (c) 2025 Bytedance, Inc. and its affiliates.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ *
5
+ * Adapted for Causyne Vision Desktop & Coordinate Grounding Engine.
6
+ */
7
+
8
+ export interface ScreenContext {
9
+ width: number;
10
+ height: number;
11
+ scaleFactor?: number;
12
+ }
13
+
14
+ export type ActionVerb =
15
+ | 'click'
16
+ | 'left_click'
17
+ | 'right_click'
18
+ | 'double_click'
19
+ | 'triple_click'
20
+ | 'middle_click'
21
+ | 'mouse_down'
22
+ | 'mouse_up'
23
+ | 'drag'
24
+ | 'move'
25
+ | 'type'
26
+ | 'press_key'
27
+ | 'hotkey'
28
+ | 'scroll'
29
+ | 'wait'
30
+ | 'finished';
31
+
32
+ export interface VisionAction {
33
+ action: ActionVerb;
34
+ thought?: string;
35
+ reflection?: string;
36
+ coordinates?: { x: number; y: number };
37
+ startCoordinates?: { x: number; y: number };
38
+ endCoordinates?: { x: number; y: number };
39
+ text?: string;
40
+ key?: string;
41
+ direction?: 'up' | 'down' | 'left' | 'right';
42
+ seconds?: number;
43
+ rawStr?: string;
44
+ }
45
+
46
+ export interface DesktopActEquivalent {
47
+ verb: 'press' | 'clickxy' | 'type' | 'key' | 'wait';
48
+ x?: number;
49
+ y?: number;
50
+ text?: string;
51
+ keyCode?: number;
52
+ target?: string;
53
+ seconds?: number;
54
+ }
55
+
56
+ const DEFAULT_FACTOR: [number, number] = [1000, 1000];
57
+
58
+ /**
59
+ * Extract numbers from a coordinate string like "(250, 400)" or "[100, 200, 300, 400]"
60
+ */
61
+ export function extractCoordinateNumbers(raw: string): number[] {
62
+ const clean = String(raw || '').replace(/[()[\]]/g, ' ').trim();
63
+ if (!clean) return [];
64
+ return clean
65
+ .split(/[\s,]+/)
66
+ .map((s) => Number.parseFloat(s))
67
+ .filter((n) => Number.isFinite(n));
68
+ }
69
+
70
+ /**
71
+ * Ground normalized coordinates (0-1000) to actual screen pixels.
72
+ */
73
+ export function groundPoint(
74
+ nums: number[],
75
+ screen: ScreenContext = { width: 1920, height: 1080, scaleFactor: 1 },
76
+ factor: [number, number] = DEFAULT_FACTOR
77
+ ): { x: number; y: number } | null {
78
+ if (nums.length < 2) return null;
79
+ const rawX = nums[0] ?? 0;
80
+ const rawY = nums[1] ?? 0;
81
+ const scale = screen.scaleFactor || 1;
82
+
83
+ const normX = Math.max(0, Math.min(1, rawX / factor[0]));
84
+ const normY = Math.max(0, Math.min(1, rawY / factor[1]));
85
+
86
+ return {
87
+ x: Math.round(normX * screen.width * scale),
88
+ y: Math.round(normY * screen.height * scale)
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Ground a bounding box (x1, y1, x2, y2) into its midpoint (center click).
94
+ */
95
+ export function groundBoxCenter(
96
+ nums: number[],
97
+ screen: ScreenContext = { width: 1920, height: 1080, scaleFactor: 1 },
98
+ factor: [number, number] = DEFAULT_FACTOR
99
+ ): { x: number; y: number } | null {
100
+ if (nums.length < 2) return null;
101
+ if (nums.length === 2) {
102
+ return groundPoint(nums, screen, factor);
103
+ }
104
+ const x1 = nums[0] ?? 0;
105
+ const y1 = nums[1] ?? 0;
106
+ const x2 = nums[2] ?? x1;
107
+ const y2 = nums[3] ?? y1;
108
+
109
+ const midX = (x1 + x2) / 2;
110
+ const midY = (y1 + y2) / 2;
111
+ return groundPoint([midX, midY], screen, factor);
112
+ }
113
+
114
+ /**
115
+ * Parse vision grounding action format:
116
+ * E.g. Action: click(start_box='(240, 500)')
117
+ */
118
+ export function parseSingleAction(
119
+ rawAction: string,
120
+ screenContext: ScreenContext = { width: 1920, height: 1080, scaleFactor: 1 }
121
+ ): VisionAction | null {
122
+ const line = rawAction.trim();
123
+ if (!line) return null;
124
+
125
+ const callMatch = line.match(/^([a-zA-Z_0-9]+)\s*\((.*)\)$/);
126
+ if (!callMatch) {
127
+ if (/finished/i.test(line)) {
128
+ return { action: 'finished', rawStr: line };
129
+ }
130
+ return null;
131
+ }
132
+
133
+ const funcName = callMatch[1]?.toLowerCase() as ActionVerb;
134
+ const argsRaw = callMatch[2] || '';
135
+
136
+ const argsMap: Record<string, string> = {};
137
+ const kvRegex = /([a-zA-Z_0-9]+)\s*=\s*['"]([^'"]+)['"]/g;
138
+ let match: RegExpExecArray | null;
139
+ while ((match = kvRegex.exec(argsRaw)) !== null) {
140
+ const key = match[1];
141
+ const val = match[2];
142
+ if (key && val) {
143
+ argsMap[key] = val;
144
+ }
145
+ }
146
+
147
+ if (Object.keys(argsMap).length === 0) {
148
+ const boxMatch = argsRaw.match(/['"]?(\([0-9\s,.]+\)|\[[0-9\s,.]+\])['"]?/);
149
+ if (boxMatch && boxMatch[1]) {
150
+ argsMap['start_box'] = boxMatch[1];
151
+ }
152
+ }
153
+
154
+ const actionObj: VisionAction = {
155
+ action: funcName,
156
+ rawStr: line
157
+ };
158
+
159
+ const startBox = argsMap['start_box'] || argsMap['point'] || argsMap['box'];
160
+ if (startBox) {
161
+ const nums = extractCoordinateNumbers(startBox);
162
+ const center = groundBoxCenter(nums, screenContext);
163
+ if (center) {
164
+ actionObj.coordinates = center;
165
+ actionObj.startCoordinates = center;
166
+ }
167
+ }
168
+
169
+ const endBox = argsMap['end_box'];
170
+ if (endBox) {
171
+ const nums = extractCoordinateNumbers(endBox);
172
+ const center = groundBoxCenter(nums, screenContext);
173
+ if (center) {
174
+ actionObj.endCoordinates = center;
175
+ }
176
+ }
177
+
178
+ if (argsMap['content']) {
179
+ actionObj.text = argsMap['content'];
180
+ }
181
+ if (argsMap['text']) {
182
+ actionObj.text = argsMap['text'];
183
+ }
184
+ if (argsMap['key']) {
185
+ actionObj.key = argsMap['key'];
186
+ }
187
+ if (argsMap['direction']) {
188
+ const dir = argsMap['direction'].toLowerCase();
189
+ if (dir === 'up' || dir === 'down' || dir === 'left' || dir === 'right') {
190
+ actionObj.direction = dir;
191
+ }
192
+ }
193
+ if (argsMap['time'] || argsMap['seconds']) {
194
+ const sec = Number.parseFloat(argsMap['time'] || argsMap['seconds'] || '1');
195
+ actionObj.seconds = Number.isFinite(sec) ? sec : 1;
196
+ }
197
+
198
+ return actionObj;
199
+ }
200
+
201
+ /**
202
+ * Parse complete vision grounding prediction text with Thought and Action blocks.
203
+ */
204
+ export function parseVisionGroundingPrediction(
205
+ predictionText: string,
206
+ screenContext: ScreenContext = { width: 1920, height: 1080, scaleFactor: 1 }
207
+ ): { thought?: string | undefined; reflection?: string | undefined; actions: VisionAction[] } {
208
+ let thought: string | undefined;
209
+ let reflection: string | undefined;
210
+
211
+ const text = (predictionText || '').trim();
212
+
213
+ if (text.includes('Thought:')) {
214
+ const tMatch = text.match(/Thought:\s*([\s\S]+?)(?=\s*Action[::]|$)/i);
215
+ if (tMatch && tMatch[1]) {
216
+ thought = tMatch[1].trim();
217
+ }
218
+ }
219
+ if (text.includes('Reflection:')) {
220
+ const rMatch = text.match(/Reflection:\s*([\s\S]+?)(?=\s*(?:Action_Summary|Action)[::]|$)/i);
221
+ if (rMatch && rMatch[1]) {
222
+ reflection = rMatch[1].trim();
223
+ }
224
+ }
225
+
226
+ const actionLines: string[] = [];
227
+ if (/Action[::]/i.test(text)) {
228
+ const parts = text.split(/Action[::]/i);
229
+ for (let i = 1; i < parts.length; i++) {
230
+ const seg = parts[i] || '';
231
+ const lines = seg.split(/\n+/).map((l) => l.trim()).filter(Boolean);
232
+ actionLines.push(...lines);
233
+ }
234
+ } else {
235
+ actionLines.push(...text.split(/\n+/).map((l) => l.trim()).filter(Boolean));
236
+ }
237
+
238
+ const actions: VisionAction[] = [];
239
+
240
+ for (const line of actionLines) {
241
+ const parsed = parseSingleAction(line, screenContext);
242
+ if (parsed) {
243
+ if (thought && !parsed.thought) parsed.thought = thought;
244
+ if (reflection && !parsed.reflection) parsed.reflection = reflection;
245
+ actions.push(parsed);
246
+ }
247
+ }
248
+
249
+ return { thought, reflection, actions };
250
+ }
251
+
252
+ const MAC_KEY_CODES: Record<string, number> = {
253
+ return: 36,
254
+ enter: 36,
255
+ tab: 48,
256
+ space: 49,
257
+ backspace: 51,
258
+ escape: 53,
259
+ esc: 53,
260
+ up: 126,
261
+ down: 125,
262
+ left: 123,
263
+ right: 124
264
+ };
265
+
266
+ /**
267
+ * Convert parsed vision grounding actions into Causyne desktop_act equivalent instructions.
268
+ */
269
+ export function visionGroundingToDesktopAct(actions: VisionAction[]): DesktopActEquivalent[] {
270
+ const result: DesktopActEquivalent[] = [];
271
+
272
+ for (const act of actions) {
273
+ switch (act.action) {
274
+ case 'click':
275
+ case 'left_click':
276
+ case 'double_click':
277
+ case 'triple_click': {
278
+ const point = act.coordinates;
279
+ if (point) {
280
+ result.push({
281
+ verb: 'clickxy',
282
+ x: point.x,
283
+ y: point.y
284
+ });
285
+ if (act.action === 'double_click') {
286
+ result.push({ verb: 'clickxy', x: point.x, y: point.y });
287
+ }
288
+ }
289
+ break;
290
+ }
291
+ case 'type': {
292
+ if (act.coordinates) {
293
+ result.push({ verb: 'clickxy', x: act.coordinates.x, y: act.coordinates.y });
294
+ }
295
+ if (act.text) {
296
+ result.push({ verb: 'type', text: act.text });
297
+ }
298
+ break;
299
+ }
300
+ case 'press_key':
301
+ case 'hotkey': {
302
+ const keyName = (act.key || '').toLowerCase().trim();
303
+ const code = MAC_KEY_CODES[keyName];
304
+ if (code !== undefined) {
305
+ result.push({ verb: 'key', keyCode: code });
306
+ } else if (keyName) {
307
+ result.push({ verb: 'type', text: keyName });
308
+ }
309
+ break;
310
+ }
311
+ case 'wait': {
312
+ result.push({ verb: 'wait', seconds: act.seconds || 1 });
313
+ break;
314
+ }
315
+ case 'drag': {
316
+ if (act.startCoordinates) {
317
+ result.push({ verb: 'clickxy', x: act.startCoordinates.x, y: act.startCoordinates.y });
318
+ }
319
+ if (act.endCoordinates) {
320
+ result.push({ verb: 'clickxy', x: act.endCoordinates.x, y: act.endCoordinates.y });
321
+ }
322
+ break;
323
+ }
324
+ case 'finished':
325
+ default:
326
+ break;
327
+ }
328
+ }
329
+
330
+ return result;
331
+ }
package/src/config.js CHANGED
@@ -78,30 +78,41 @@ export function loadConfig() {
78
78
  }
79
79
  }
80
80
 
81
- // Self-heal a model id written before src/modelIds.js existed. The
82
- // provider normalises too, but tokenTracker.getPricing() and the /model
83
- // display both read config.model directly —
84
- // they must agree with what is actually sent, not with what a stale
85
- // Android build once wrote here.
81
+ // Self-heal a model id written before src/modelIds.js existed.
86
82
  const canonicalModel = canonicalModelId(parsed.model);
87
83
  if (canonicalModel !== parsed.model) {
88
84
  parsed.model = canonicalModel;
89
85
  saveConfig(parsed);
90
86
  }
91
87
 
88
+ // Self-heal legacy unconfigured DashScope bug:
89
+ // If config was written with provider 'dashscope' or 'dashscope-byok' but without an apiKey,
90
+ // it was an unintended default from older builds. Heal to an environment key or unconfigured.
91
+ if ((parsed.provider === 'dashscope' || parsed.provider === 'dashscope-byok') && !parsed.apiKey) {
92
+ const envDetected = detectEnvironmentProvider();
93
+ if (envDetected) {
94
+ parsed.provider = envDetected.provider;
95
+ parsed.model = envDetected.model;
96
+ parsed.apiKey = envDetected.apiKey;
97
+ } else {
98
+ parsed.provider = '';
99
+ parsed.model = '';
100
+ }
101
+ saveConfig(parsed);
102
+ }
103
+
92
104
  return parsed;
93
105
  } catch (e) {
94
106
  console.error('Error reading config.json:', e);
95
107
  }
96
108
  }
97
109
 
98
- // Default config
110
+ // Default config: auto-detect from environment if available, otherwise unconfigured
111
+ const envDetected = detectEnvironmentProvider();
99
112
  const defaultConfig = {
100
- provider: 'dashscope',
101
- // From the catalogue, not repeated here — this had been left at the
102
- // long-retired `qwen-plus` while the picker had moved to qwen3.7.
103
- model: DEFAULT_MODEL_BY_PROVIDER.dashscope,
104
- apiKey: '',
113
+ provider: envDetected ? envDetected.provider : '',
114
+ model: envDetected ? envDetected.model : '',
115
+ apiKey: envDetected ? envDetected.apiKey : '',
105
116
  autonomyMode: 'B',
106
117
  tokenBudget: {
107
118
  enabled: false,
@@ -116,6 +127,64 @@ export function loadConfig() {
116
127
  return defaultConfig;
117
128
  }
118
129
 
130
+ /**
131
+ * Auto-detect available API keys from environment variables.
132
+ * Precedence matches frontier capability: Gemini, Anthropic, OpenAI, DeepSeek, xAI, Moonshot, Qwen/DashScope.
133
+ */
134
+ export function detectEnvironmentProvider() {
135
+ const env = process.env;
136
+ if (env.GEMINI_API_KEY || env.GOOGLE_API_KEY) {
137
+ return {
138
+ provider: 'gemini',
139
+ apiKey: env.GEMINI_API_KEY || env.GOOGLE_API_KEY || '',
140
+ model: DEFAULT_MODEL_BY_PROVIDER.gemini
141
+ };
142
+ }
143
+ if (env.ANTHROPIC_API_KEY) {
144
+ return {
145
+ provider: 'anthropic',
146
+ apiKey: env.ANTHROPIC_API_KEY,
147
+ model: DEFAULT_MODEL_BY_PROVIDER.anthropic
148
+ };
149
+ }
150
+ if (env.OPENAI_API_KEY) {
151
+ return {
152
+ provider: 'openai',
153
+ apiKey: env.OPENAI_API_KEY,
154
+ model: DEFAULT_MODEL_BY_PROVIDER.openai
155
+ };
156
+ }
157
+ if (env.DEEPSEEK_API_KEY) {
158
+ return {
159
+ provider: 'deepseek',
160
+ apiKey: env.DEEPSEEK_API_KEY,
161
+ model: DEFAULT_MODEL_BY_PROVIDER.deepseek
162
+ };
163
+ }
164
+ if (env.XAI_API_KEY || env.GROK_API_KEY) {
165
+ return {
166
+ provider: 'xai',
167
+ apiKey: env.XAI_API_KEY || env.GROK_API_KEY || '',
168
+ model: DEFAULT_MODEL_BY_PROVIDER.xai
169
+ };
170
+ }
171
+ if (env.MOONSHOT_API_KEY) {
172
+ return {
173
+ provider: 'moonshot',
174
+ apiKey: env.MOONSHOT_API_KEY,
175
+ model: DEFAULT_MODEL_BY_PROVIDER.moonshot
176
+ };
177
+ }
178
+ if (env.DASHSCOPE_API_KEY || env.QWEN_API_KEY) {
179
+ return {
180
+ provider: 'dashscope-byok',
181
+ apiKey: env.DASHSCOPE_API_KEY || env.QWEN_API_KEY || '',
182
+ model: DEFAULT_MODEL_BY_PROVIDER['dashscope-byok']
183
+ };
184
+ }
185
+ return null;
186
+ }
187
+
119
188
  export function saveConfig(config) {
120
189
  ensureConfigDir();
121
190
  // SECURITY: Deep copy to avoid mutating the in-memory config