@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,283 @@
1
+ // Sovereign Dynamic Model Architecture & Live Discovery
2
+ //
3
+ // WHY THIS EXISTS:
4
+ // AI providers release and update models continuously. Hardcoded static
5
+ // catalogues freeze models at build time. This module gives Causyne a dual-layer
6
+ // model architecture:
7
+ // 1. Curated baseline: Instant offline availability with zero latency and verified pricing/capability tiers.
8
+ // 2. Dynamic discovery: Live discovery via provider APIs (Ollama, OpenAI, Gemini, Anthropic, DeepSeek, etc.)
9
+ // with resilient local caching in ~/.causyne/models_cache.json.
10
+ //
11
+ // ZERO-MALWARE: Pure TypeScript/ESM. Strictly respects timeouts, no secret leakage, no unprompted background calls.
12
+
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+ import { causyneHome } from './utils/paths.js';
16
+ import { MODEL_CATALOGUE, DEFAULT_MODEL_BY_PROVIDER } from './modelCatalogue.js';
17
+
18
+ export interface DynamicCatalogueCache {
19
+ version: 1;
20
+ updatedAt: string;
21
+ providers: Record<string, {
22
+ timestamp: number;
23
+ models: string[];
24
+ }>;
25
+ }
26
+
27
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
28
+ const REQUEST_TIMEOUT_MS = 3500; // Fast timeout to never block user interaction
29
+
30
+ export function cacheFilePath(): string {
31
+ return path.join(causyneHome(), 'models_cache.json');
32
+ }
33
+
34
+ export function normalizeProviderKey(provider: string): string {
35
+ const p = (provider || '').toLowerCase().trim();
36
+ if (p === 'dashscope' || p === 'dashscope-byok' || p === 'alibaba') return 'dashscope-byok';
37
+ if (p === 'ollama') return 'ollama';
38
+ if (p === 'openai') return 'openai';
39
+ if (p === 'anthropic' || p === 'claude') return 'anthropic';
40
+ if (p === 'gemini' || p === 'google') return 'gemini';
41
+ if (p === 'deepseek') return 'deepseek';
42
+ if (p === 'xai' || p === 'grok') return 'xai';
43
+ if (p === 'moonshot' || p === 'kimi') return 'moonshot';
44
+ if (p === 'muse' || p === 'meta') return 'muse';
45
+ if (p === 'openai-compatible' || p === 'custom') return 'openai-compatible';
46
+ return p;
47
+ }
48
+
49
+ /** Get the static curated models for a provider id. */
50
+ export function getCuratedModels(provider: string): string[] {
51
+ const norm = normalizeProviderKey(provider);
52
+ const mapping: Record<string, keyof typeof MODEL_CATALOGUE> = {
53
+ 'deepseek': 'DeepSeek',
54
+ 'qwen': 'Qwen',
55
+ 'dashscope-byok': 'Alibaba (DashScope)',
56
+ 'dashscope': 'Alibaba (DashScope)',
57
+ 'anthropic': 'Anthropic',
58
+ 'openai': 'OpenAI',
59
+ 'gemini': 'Gemini',
60
+ 'muse': 'Meta (Muse)',
61
+ 'ollama': 'Ollama (Local)',
62
+ 'moonshot': 'Moonshot (Kimi)',
63
+ 'xai': 'xAI (Grok)'
64
+ };
65
+ const key = mapping[norm];
66
+ if (key && MODEL_CATALOGUE[key]) {
67
+ return [...MODEL_CATALOGUE[key]];
68
+ }
69
+ return [];
70
+ }
71
+
72
+ /** Read cache safely from ~/.causyne/models_cache.json */
73
+ export function readModelCache(): DynamicCatalogueCache | null {
74
+ try {
75
+ const file = cacheFilePath();
76
+ if (!fs.existsSync(file)) return null;
77
+ const content = fs.readFileSync(file, 'utf8');
78
+ const parsed = JSON.parse(content) as DynamicCatalogueCache;
79
+ if (parsed && parsed.version === 1 && typeof parsed.providers === 'object') {
80
+ return parsed;
81
+ }
82
+ } catch {
83
+ // Graceful fallback on corrupt or inaccessible cache
84
+ }
85
+ return null;
86
+ }
87
+
88
+ /** Save cache safely to ~/.causyne/models_cache.json */
89
+ export function writeModelCache(cache: DynamicCatalogueCache): void {
90
+ try {
91
+ const dir = causyneHome();
92
+ if (!fs.existsSync(dir)) {
93
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
94
+ }
95
+ const file = cacheFilePath();
96
+ fs.writeFileSync(file, JSON.stringify(cache, null, 2), { mode: 0o600 });
97
+ } catch {
98
+ // Cache failures must never crash the CLI
99
+ }
100
+ }
101
+
102
+ /** Get cached models for a provider if not expired */
103
+ export function getCachedModels(provider: string): string[] | null {
104
+ const cache = readModelCache();
105
+ if (!cache) return null;
106
+ const norm = normalizeProviderKey(provider);
107
+ const entry = cache.providers[norm];
108
+ if (!entry) return null;
109
+ if (Date.now() - entry.timestamp > CACHE_TTL_MS) return null;
110
+ return entry.models;
111
+ }
112
+
113
+ /** Save models for a provider into the local cache */
114
+ export function saveCachedModels(provider: string, models: string[]): void {
115
+ const norm = normalizeProviderKey(provider);
116
+ const cache: DynamicCatalogueCache = readModelCache() || {
117
+ version: 1,
118
+ updatedAt: new Date().toISOString(),
119
+ providers: {}
120
+ };
121
+ cache.providers[norm] = {
122
+ timestamp: Date.now(),
123
+ models: [...new Set(models)]
124
+ };
125
+ cache.updatedAt = new Date().toISOString();
126
+ writeModelCache(cache);
127
+ }
128
+
129
+ /** Live query provider API to discover available models */
130
+ export async function fetchLiveModels(
131
+ provider: string,
132
+ config: { apiKey?: string; baseUrl?: string } = {}
133
+ ): Promise<string[]> {
134
+ const norm = normalizeProviderKey(provider);
135
+ const { apiKey, baseUrl } = config;
136
+
137
+ try {
138
+ if (norm === 'ollama') {
139
+ const host = (baseUrl || 'http://127.0.0.1:11434').replace(/\/+$/, '');
140
+ const res = await fetch(`${host}/api/tags`, {
141
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
142
+ });
143
+ if (!res.ok) return [];
144
+ const data = await res.json() as { models?: Array<{ name?: string }> };
145
+ if (Array.isArray(data.models)) {
146
+ return data.models
147
+ .map((m) => (m.name || '').replace(/:latest$/, ''))
148
+ .filter(Boolean);
149
+ }
150
+ }
151
+
152
+ if (norm === 'openai') {
153
+ if (!apiKey) return [];
154
+ const host = (baseUrl || 'https://api.openai.com/v1').replace(/\/+$/, '');
155
+ const res = await fetch(`${host}/models`, {
156
+ headers: { Authorization: `Bearer ${apiKey}` },
157
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
158
+ });
159
+ if (!res.ok) return [];
160
+ const data = await res.json() as { data?: Array<{ id?: string }> };
161
+ if (Array.isArray(data.data)) {
162
+ return data.data
163
+ .map((m) => m.id || '')
164
+ .filter((id) => /^(gpt|o1|o3|chatgpt)/i.test(id))
165
+ .sort();
166
+ }
167
+ }
168
+
169
+ if (norm === 'gemini') {
170
+ if (!apiKey) return [];
171
+ const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(apiKey)}`, {
172
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
173
+ });
174
+ if (!res.ok) return [];
175
+ const data = await res.json() as { models?: Array<{ name?: string; supportedGenerationMethods?: string[] }> };
176
+ if (Array.isArray(data.models)) {
177
+ return data.models
178
+ .filter((m) => m.supportedGenerationMethods?.includes('generateContent'))
179
+ .map((m) => (m.name || '').replace(/^models\//, ''))
180
+ .filter(Boolean)
181
+ .sort();
182
+ }
183
+ }
184
+
185
+ if (norm === 'anthropic') {
186
+ if (!apiKey) return [];
187
+ const res = await fetch('https://api.anthropic.com/v1/models', {
188
+ headers: {
189
+ 'x-api-key': apiKey,
190
+ 'anthropic-version': '2023-06-01'
191
+ },
192
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
193
+ });
194
+ if (!res.ok) return [];
195
+ const data = await res.json() as { data?: Array<{ id?: string }> };
196
+ if (Array.isArray(data.data)) {
197
+ return data.data
198
+ .map((m) => m.id || '')
199
+ .filter(Boolean)
200
+ .sort();
201
+ }
202
+ }
203
+
204
+ if (norm === 'deepseek') {
205
+ if (!apiKey) return [];
206
+ const res = await fetch('https://api.deepseek.com/models', {
207
+ headers: { Authorization: `Bearer ${apiKey}` },
208
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
209
+ });
210
+ if (!res.ok) return [];
211
+ const data = await res.json() as { data?: Array<{ id?: string }> };
212
+ if (Array.isArray(data.data)) {
213
+ return data.data.map((m) => m.id || '').filter(Boolean).sort();
214
+ }
215
+ }
216
+
217
+ if (norm === 'openai-compatible') {
218
+ if (!baseUrl) return [];
219
+ const host = baseUrl.replace(/\/+$/, '');
220
+ const headers: Record<string, string> = {};
221
+ if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
222
+ const res = await fetch(`${host}/models`, {
223
+ headers,
224
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
225
+ });
226
+ if (!res.ok) return [];
227
+ const data = await res.json() as { data?: Array<{ id?: string }> };
228
+ if (Array.isArray(data.data)) {
229
+ return data.data.map((m) => m.id || '').filter(Boolean).sort();
230
+ }
231
+ }
232
+ } catch {
233
+ // Network errors or timeouts silently fall back
234
+ }
235
+ return [];
236
+ }
237
+
238
+ export interface ModelResolutionResult {
239
+ models: string[];
240
+ source: 'live' | 'cache' | 'catalogue';
241
+ }
242
+
243
+ /**
244
+ * Resolve available models using dual-layer architecture:
245
+ * 1. Checks live query if forceRefresh is true.
246
+ * 2. Checks local cache if available and fresh.
247
+ * 3. Falls back to static curated catalogue merged with any cached items.
248
+ */
249
+ export async function resolveAvailableModels(
250
+ provider: string,
251
+ config: { apiKey?: string; baseUrl?: string } = {},
252
+ forceRefresh = false
253
+ ): Promise<ModelResolutionResult> {
254
+ const norm = normalizeProviderKey(provider);
255
+ const curated = getCuratedModels(norm);
256
+
257
+ if (forceRefresh) {
258
+ const live = await fetchLiveModels(norm, config);
259
+ if (live.length > 0) {
260
+ const combined = [...new Set([...live, ...curated])];
261
+ saveCachedModels(norm, combined);
262
+ return { models: combined, source: 'live' };
263
+ }
264
+ }
265
+
266
+ const cached = getCachedModels(norm);
267
+ if (cached && cached.length > 0) {
268
+ const combined = [...new Set([...cached, ...curated])];
269
+ return { models: combined, source: 'cache' };
270
+ }
271
+
272
+ // Attempt lazy background discovery if credentials exist
273
+ if (config.apiKey || norm === 'ollama') {
274
+ const live = await fetchLiveModels(norm, config);
275
+ if (live.length > 0) {
276
+ const combined = [...new Set([...live, ...curated])];
277
+ saveCachedModels(norm, combined);
278
+ return { models: combined, source: 'live' };
279
+ }
280
+ }
281
+
282
+ return { models: curated, source: 'catalogue' };
283
+ }
package/src/entrypoint.js CHANGED
@@ -90,7 +90,8 @@ program
90
90
 
91
91
  program
92
92
  .command('config')
93
- .description('Configure Direct API Provider (Interactive)')
93
+ .alias('setup')
94
+ .description('Guided provider, endpoint, model and autonomy setup')
94
95
  .option('-p, --provider <name>', 'Set LLM provider (openai, anthropic, gemini, muse, ollama, dashscope, deepseek)')
95
96
  .option('-k, --apikey <key>', 'Set API key')
96
97
  .option('-m, --model <name>', 'Set model name')
@@ -0,0 +1,5 @@
1
+ export const MODEL_CATALOGUE: Record<string, string[]>;
2
+ export const PROVIDER_LABELS: string[];
3
+ export const DEFAULT_MODEL_BY_PROVIDER: Record<string, string>;
4
+ export const UTILITY_MODEL_BY_PROVIDER: Record<string, string>;
5
+ export function allCatalogueModels(): string[];
@@ -34,22 +34,52 @@
34
34
  * The label keys are a stable configuration format, not display sugar.
35
35
  */
36
36
  export const MODEL_CATALOGUE = {
37
- 'DeepSeek': ['deepseek-v4-pro', 'deepseek-v4-flash'],
38
- 'Qwen': ['qwen3.7-max', 'qwen3.7-plus', 'qwen3.6-flash'],
37
+ 'DeepSeek': ['deepseek-chat', 'deepseek-reasoner', 'deepseek-v4-pro', 'deepseek-v4-flash'],
38
+ 'Qwen': [
39
+ 'qwen2.5-coder-32b-instruct',
40
+ 'qwen2.5-72b-instruct',
41
+ 'qwen-max',
42
+ 'qwen-plus',
43
+ 'qwen-turbo',
44
+ 'qwen3.7-max',
45
+ 'qwen3.7-plus',
46
+ 'qwen3.6-flash'
47
+ ],
39
48
  // Anthropic ids use DASHES and carry no date suffix. A dotted id here is a
40
49
  // bug, not a variant — see src/modelIds.js.
41
50
  'Anthropic': [
51
+ 'claude-3-7-sonnet',
52
+ 'claude-3-5-sonnet',
53
+ 'claude-3-5-haiku',
54
+ 'claude-3-opus',
42
55
  'claude-fable-5-1',
43
56
  'claude-opus-5',
44
57
  'claude-opus-4-8',
45
58
  'claude-sonnet-5',
46
59
  'claude-haiku-4-5'
47
60
  ],
48
- // GPT-6 Astra leads: it is the current flagship and the only model in the
49
- // line-up trained for computer use. It is also the priciest ($10/$50 against
50
- // Sol's $5/$30), which is why it heads the list but is not the default.
51
- 'OpenAI': ['gpt-6-astra', 'gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna', 'gpt-5.4-mini'],
52
- 'Gemini': ['gemini-3.6-flash', 'gemini-3.5-flash', 'gemini-3.1-pro', 'gemini-3.1-flash-lite'],
61
+ 'OpenAI': [
62
+ 'gpt-4o',
63
+ 'gpt-4o-mini',
64
+ 'o3-mini',
65
+ 'o1',
66
+ 'gpt-6-astra',
67
+ 'gpt-5.6-sol',
68
+ 'gpt-5.6-terra',
69
+ 'gpt-5.6-luna',
70
+ 'gpt-5.4-mini'
71
+ ],
72
+ 'Gemini': [
73
+ 'gemini-2.5-pro',
74
+ 'gemini-2.5-flash',
75
+ 'gemini-2.0-flash',
76
+ 'gemini-1.5-pro',
77
+ 'gemini-1.5-flash',
78
+ 'gemini-3.6-flash',
79
+ 'gemini-3.5-flash',
80
+ 'gemini-3.1-pro',
81
+ 'gemini-3.1-flash-lite'
82
+ ],
53
83
  // Standard first. The -contributor variants are cheaper because Meta trains
54
84
  // on the user's prompts and completions, so they must be a choice made
55
85
  // deliberately, never the one at the top of the list.
@@ -60,14 +90,15 @@ export const MODEL_CATALOGUE = {
60
90
  'muse-spark-1.3-contributor',
61
91
  'muse-spark-1.2-contributor'
62
92
  ],
63
- 'Ollama (Local)': ['llama3', 'mistral', 'gemma2', 'custom'],
93
+ 'Ollama (Local)': ['llama3.3', 'qwen2.5-coder', 'deepseek-r1', 'llama3', 'mistral', 'gemma2', 'custom'],
64
94
  'Alibaba (DashScope)': [
95
+ 'qwen-max', 'qwen-plus', 'qwen-turbo', 'qwen2.5-coder-32b-instruct',
65
96
  'qwen3.7-max', 'qwen3.7-plus', 'kimi-k2.7-code', 'kimi-k2.6',
66
97
  'kimi-k2.5', 'kimi-k2-thinking', 'Moonshot-Kimi-K2-Instruct',
67
98
  'deepseek-v4-flash', 'deepseek-v4-pro'
68
99
  ],
69
100
  'Moonshot (Kimi)': ['kimi-k3', 'kimi-k2.7-code', 'kimi-k2.6'],
70
- 'xAI (Grok)': ['grok-4.5', 'grok-latest']
101
+ 'xAI (Grok)': ['grok-3', 'grok-3-mini', 'grok-2-1212', 'grok-4.5', 'grok-latest']
71
102
  };
72
103
 
73
104
  /** Provider labels in picker order. */
@@ -75,17 +106,17 @@ export const PROVIDER_LABELS = Object.keys(MODEL_CATALOGUE);
75
106
 
76
107
 
77
108
  export const DEFAULT_MODEL_BY_PROVIDER = {
78
- deepseek: 'deepseek-v4-pro',
79
- qwen: 'qwen3.7-plus',
80
- 'dashscope-byok': 'qwen3.7-plus',
81
- dashscope: 'qwen3.7-plus',
109
+ deepseek: 'deepseek-chat',
110
+ qwen: 'qwen2.5-coder-32b-instruct',
111
+ 'dashscope-byok': 'qwen-plus',
112
+ dashscope: 'qwen-plus',
82
113
  muse: 'muse-spark-1.3',
83
- anthropic: 'claude-opus-5',
84
- openai: 'gpt-5.6-sol',
85
- gemini: 'gemini-3.5-flash',
114
+ anthropic: 'claude-3-7-sonnet',
115
+ openai: 'gpt-4o',
116
+ gemini: 'gemini-2.5-pro',
86
117
  moonshot: 'kimi-k3',
87
- xai: 'grok-4.5',
88
- ollama: 'llama3',
118
+ xai: 'grok-3',
119
+ ollama: 'llama3.3',
89
120
  'openai-compatible': ''
90
121
  };
91
122
 
@@ -8,6 +8,10 @@ import { LLMProvider } from './base.js';
8
8
  // full capability, none of the token cost. Older models fall back to the
9
9
  // client-side progressive disclosure in src/toolPacks.js.
10
10
  const TOOL_SEARCH_MODELS = [
11
+ 'claude-3-7-sonnet',
12
+ 'claude-3-5-sonnet',
13
+ 'claude-3-5-haiku',
14
+ 'claude-3-opus',
11
15
  'claude-fable-5',
12
16
  'claude-mythos-5',
13
17
  'claude-opus-5',
package/src/sandbox.js CHANGED
@@ -298,6 +298,10 @@ export class Sandbox {
298
298
  return { allowed: true, risk: 'safe' };
299
299
  }
300
300
 
301
+ if (toolName === 'creative_apps') {
302
+ return { allowed: true, risk: args.action === 'status' ? 'safe' : 'dangerous' };
303
+ }
304
+
301
305
  if (toolName === 'game_asset_fetch' && args.action === 'download_asset') {
302
306
  if (!args.url || this.isInternalUrl(args.url)) {
303
307
  return { allowed: false, reason: 'Cannot download from an internal or invalid URL', risk: 'forbidden' };
@@ -353,9 +357,12 @@ export class Sandbox {
353
357
 
354
358
  // Mode C. Section 4 says gated, not walled — the capability is real, the
355
359
  // human just says yes first.
356
- if (toolName === 'desktop_act') {
360
+ if (toolName === 'desktop_act' || toolName === 'computer_use') {
357
361
  return { allowed: true, risk: 'extreme' };
358
362
  }
363
+ if (toolName === 'ui_forge') {
364
+ return { allowed: true, risk: 'safe' };
365
+ }
359
366
  if (toolName === 'fs_write' || toolName === 'fs_move' || toolName === 'fs_edit' || toolName === 'MultiEdit') {
360
367
  return { allowed: true, risk: 'moderate' };
361
368
  }
package/src/toolPacks.js CHANGED
@@ -76,7 +76,8 @@ export const PACKS = {
76
76
  'screenshot',
77
77
  'vision_inspect_ui',
78
78
  'view_image',
79
- 'astryx_oracle'
79
+ 'astryx_oracle',
80
+ 'ui_forge'
80
81
  ],
81
82
  media: [
82
83
  'generate_media_via_api',
@@ -84,6 +85,7 @@ export const PACKS = {
84
85
  'check_system_specs',
85
86
  'manage_local_dependencies',
86
87
  'blender_act',
88
+ 'creative_apps',
87
89
  'resolve_act',
88
90
  'palmier_act'
89
91
  ],
@@ -95,6 +97,7 @@ export const PACKS = {
95
97
  // Channel C. Ships beside its observer on purpose: acting on a screen you
96
98
  // have not read is how an agent clicks the wrong button confidently.
97
99
  'desktop_act',
100
+ 'computer_use',
98
101
  'process_list',
99
102
  'proc_term',
100
103
  'archive',
@@ -361,11 +364,11 @@ When the user asks you to build, create, test, expand, or develop ANY game, you
361
364
 
362
365
  const GAME_INTENT = /\b(game|games|gamedev|permainan|godot|unity|unreal|rpg|platformer|roguelike|arcade|sprite|spritesheet|tilemap|voxel|minecraft|aetheria|voxellia|nes|snes|gameplay|player character|level design|boss fight)\b|\.(tscn|gd)\b/i;
363
366
 
364
- const WEB_INTENT = /\b(web|webpage|website|situs|frontend|front-end|landing page|homepage|dashboard|portfolio|html|css|scss|tailwind|bootstrap|react|vue|svelte|angular|next\.?js|nuxt|astro|ui|ux|user interface|web app|webapp|spa|dom|responsive)\b/i;
367
+ const WEB_INTENT = /\b(web|webpage|website|situs|frontend|front-end|landing page|homepage|dashboard|portfolio|html|css|scss|tailwind|bootstrap|react|vue|svelte|angular|next\.?js|nuxt|astro|ui|ux|user interface|web app|webapp|spa|dom|responsive|ui forge|component forge|component generation|ui component)\b/i;
365
368
 
366
369
  const MEDIA_INTENT = /\b(image|images|picture|photo|gambar|render|3d model|blender|animation|animate|video|movie|film|klip|audio|sound|sfx|music|musik|song|lagu|voice|suara|tts|text.to.speech|narration|podcast|thumbnail|poster|illustration|logo)\b/i;
367
370
 
368
- const SYSTEM_INTENT = /\b(process|processes|pid|kill|port|zip|unzip|tar|archive|compress|extract|clipboard|copy to clipboard|print|printer|cetak|open in browser|launch app)\b/i;
371
+ const SYSTEM_INTENT = /\b(process|processes|pid|kill|port|zip|unzip|tar|archive|compress|extract|clipboard|copy to clipboard|print|printer|cetak|open in browser|launch app|computer use|vision grounding|desktop vision|gui agent|coordinate click|desktop control)\b/i;
369
372
 
370
373
  const ADVANCED_INTENT = /\b(pdf|forge a tool|custom tool|new tool|plugin|plugins|custom plugin|subagent|sub-agent|parallel agent|swarm|ast|symbols|refactor across)\b/i;
371
374
 
@@ -0,0 +1,139 @@
1
+ /*
2
+ * Copyright (c) 2025 Bytedance, Inc. and its affiliates.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ *
5
+ * Causyne Vision Desktop & Coordinate Grounding Tool Wrapper.
6
+ */
7
+ import { parseVisionGroundingPrediction, visionGroundingToDesktopAct, groundBoxCenter } from '../computerUse/visionGrounding.js';
8
+ import { desktopAct } from './desktopAct.js';
9
+ import { auditLog } from '../auditLog.js';
10
+ export const computerUse = {
11
+ schema: {
12
+ name: 'computer_use',
13
+ description: 'Ground and execute graphical computer-use actions (click coordinates, type text, send key presses, drag, wait) derived from vision-language models and coordinate grounding predictions. Automatically translates normalized bounding boxes and coordinates against current screen dimensions with safety boundary checks. Requires explicit approval.',
14
+ parameters: {
15
+ type: 'object',
16
+ properties: {
17
+ action: {
18
+ type: 'string',
19
+ enum: ['execute_prediction', 'click', 'type', 'key', 'drag', 'wait'],
20
+ description: 'Type of computer-use action to perform.'
21
+ },
22
+ prediction: {
23
+ type: 'string',
24
+ description: 'Raw prediction text from a vision-language model or coordinate grounding engine, e.g. "Thought: Click search bar. Action: click(start_box=\'(500, 200)\')".'
25
+ },
26
+ coordinates: {
27
+ type: 'array',
28
+ items: { type: 'number' },
29
+ description: 'Coordinates as [x, y] or bounding box [x1, y1, x2, y2] normalized (0-1000) or in screen pixels.'
30
+ },
31
+ text: {
32
+ type: 'string',
33
+ description: 'Text to type into the active or targeted element.'
34
+ },
35
+ key: {
36
+ type: 'string',
37
+ description: 'Key or hotkey to press, e.g. "return", "tab", "escape".'
38
+ },
39
+ seconds: {
40
+ type: 'number',
41
+ description: 'Seconds to wait (0.1 to 10).'
42
+ },
43
+ app: {
44
+ type: 'string',
45
+ description: 'Application to focus before executing actions, e.g. "Safari".'
46
+ },
47
+ screenContext: {
48
+ type: 'object',
49
+ properties: {
50
+ width: { type: 'number' },
51
+ height: { type: 'number' },
52
+ scaleFactor: { type: 'number' }
53
+ },
54
+ description: 'Current display dimensions and scale factor for coordinate grounding.'
55
+ }
56
+ }
57
+ }
58
+ },
59
+ async execute(args = {}, context = {}) {
60
+ const screen = args.screenContext || { width: 1920, height: 1080, scaleFactor: 1 };
61
+ const app = typeof args.app === 'string' ? args.app.trim() : '';
62
+ let desktopActions = [];
63
+ // Mode 1: Parse full model prediction from vision grounding engine
64
+ if (args.prediction || args.action === 'execute_prediction') {
65
+ const pred = args.prediction || '';
66
+ if (!pred.trim()) {
67
+ return { success: false, error: 'No prediction string provided for execute_prediction.' };
68
+ }
69
+ const parsed = parseVisionGroundingPrediction(pred, screen);
70
+ if (parsed.actions.length === 0) {
71
+ return { success: false, error: 'Could not parse any executable GUI actions from prediction string.' };
72
+ }
73
+ desktopActions = visionGroundingToDesktopAct(parsed.actions);
74
+ }
75
+ else {
76
+ // Mode 2: Direct action request
77
+ const actionType = args.action || 'click';
78
+ if (actionType === 'click') {
79
+ const coords = args.coordinates || [];
80
+ const point = groundBoxCenter(coords, screen);
81
+ if (!point)
82
+ return { success: false, error: 'Click requires valid coordinates [x, y].' };
83
+ desktopActions.push({ verb: 'clickxy', x: point.x, y: point.y });
84
+ }
85
+ else if (actionType === 'type') {
86
+ if (args.coordinates && args.coordinates.length >= 2) {
87
+ const point = groundBoxCenter(args.coordinates, screen);
88
+ if (point)
89
+ desktopActions.push({ verb: 'clickxy', x: point.x, y: point.y });
90
+ }
91
+ if (!args.text)
92
+ return { success: false, error: 'Type action requires "text" parameter.' };
93
+ desktopActions.push({ verb: 'type', text: args.text });
94
+ }
95
+ else if (actionType === 'key') {
96
+ if (!args.key)
97
+ return { success: false, error: 'Key action requires "key" parameter.' };
98
+ const keyMap = {
99
+ return: 36, enter: 36, tab: 48, space: 49, backspace: 51, escape: 53, esc: 53
100
+ };
101
+ const keyCode = keyMap[args.key.toLowerCase()];
102
+ if (keyCode !== undefined) {
103
+ desktopActions.push({ verb: 'key', keyCode });
104
+ }
105
+ else {
106
+ desktopActions.push({ verb: 'type', text: args.key });
107
+ }
108
+ }
109
+ else if (actionType === 'wait') {
110
+ desktopActions.push({ verb: 'wait', seconds: Math.min(10, Math.max(0.1, args.seconds || 1)) });
111
+ }
112
+ else {
113
+ return { success: false, error: `Unsupported computer_use action: ${actionType}` };
114
+ }
115
+ }
116
+ if (desktopActions.length === 0) {
117
+ return { success: false, error: 'No actions to execute.' };
118
+ }
119
+ if (desktopActions.length > 12) {
120
+ return { success: false, error: `Maximum 12 actions allowed per call; got ${desktopActions.length}.` };
121
+ }
122
+ auditLog.log({
123
+ event: 'computer_use_actuation',
124
+ app,
125
+ actionCount: desktopActions.length,
126
+ verbs: desktopActions.map((a) => a.verb)
127
+ });
128
+ const execResult = await desktopAct.execute({ app, actions: desktopActions }, context);
129
+ const finalRes = {
130
+ success: Boolean(execResult.success),
131
+ actions: desktopActions
132
+ };
133
+ if (execResult.output)
134
+ finalRes.output = execResult.output;
135
+ if (execResult.error)
136
+ finalRes.error = execResult.error;
137
+ return finalRes;
138
+ }
139
+ };