@causyne/agent 1.10.18 → 1.10.20

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 (39) hide show
  1. package/docs/ARCHITECTURE.md +20 -0
  2. package/native-skills/media/creative-studio-pipeline/SKILL.md +52 -0
  3. package/native-skills/media/photo-studio-automation/SKILL.md +53 -0
  4. package/native-skills/media/video-timeline-assembly/SKILL.md +52 -0
  5. package/package.json +1 -1
  6. package/src/agentLoop.js +4 -0
  7. package/src/appChannel.js +7 -3
  8. package/src/cliMode.js +33 -67
  9. package/src/commands.js +56 -8
  10. package/src/compiledSkills.js +15 -0
  11. package/src/config.js +80 -11
  12. package/src/creative/apps.js +69 -0
  13. package/src/creative/apps.ts +58 -0
  14. package/src/creative/blenderBatch.js +233 -0
  15. package/src/creative/blenderBatch.ts +217 -0
  16. package/src/creative/pipeline.js +56 -0
  17. package/src/creative/pipeline.ts +82 -0
  18. package/src/creative/studioScripts.js +374 -0
  19. package/src/creative/studioScripts.ts +378 -0
  20. package/src/decisionPolicy.js +2 -0
  21. package/src/decisionPolicy.ts +1 -0
  22. package/src/dynamicCatalogue.js +270 -0
  23. package/src/dynamicCatalogue.ts +283 -0
  24. package/src/entrypoint.js +2 -1
  25. package/src/modelCatalogue.d.ts +5 -0
  26. package/src/modelCatalogue.js +49 -18
  27. package/src/providers/anthropic.js +4 -0
  28. package/src/sandbox.js +4 -0
  29. package/src/toolPacks.js +1 -0
  30. package/src/tools/creativeApps.js +106 -0
  31. package/src/tools/creativeApps.ts +99 -0
  32. package/src/tools/registry.js +2 -1
  33. package/src/tui/banner.js +391 -190
  34. package/src/tui/banner.ts +409 -0
  35. package/src/tui/chatRenderer.js +1 -1
  36. package/src/tui/dialogs.js +10 -0
  37. package/src/tui/home.js +29 -72
  38. package/src/tui/setup.js +104 -0
  39. package/src/tui/setup.ts +98 -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' };
package/src/toolPacks.js CHANGED
@@ -84,6 +84,7 @@ export const PACKS = {
84
84
  'check_system_specs',
85
85
  'manage_local_dependencies',
86
86
  'blender_act',
87
+ 'creative_apps',
87
88
  'resolve_act',
88
89
  'palmier_act'
89
90
  ],
@@ -0,0 +1,106 @@
1
+ import { runStudioEdit } from '../creative/studioScripts.js';
2
+ import { creativeAppStatus } from '../creative/apps.js';
3
+ import { runBlenderBatch } from '../creative/blenderBatch.js';
4
+ import { runStudioPipeline } from '../creative/pipeline.js';
5
+ export const creativeApps = {
6
+ schema: {
7
+ name: 'creative_apps',
8
+ description: 'Inspect installed creative applications, run bounded creative batches, or orchestrate multi-stage creative pipelines. Blender batch creates and edits 3D scenes (primitives, materials, lights, cameras, render) and saves a new .blend. studio_edit uses native scripting: Photo Studio (resize/rotate/layers/flatten -> PSD/PNG/JPG/WEBP), Video Studio (sequences/imports/timeline cuts/markers -> PRPROJ), Motion Studio (compositions/imports/text -> AEP). studio_pipeline executes sequential coordinated multi-host stages. Requires workspace scoping.',
9
+ parameters: {
10
+ type: 'object',
11
+ properties: {
12
+ action: { type: 'string', enum: ['status', 'blender_batch', 'studio_edit', 'studio_pipeline'] },
13
+ host: { type: 'string', enum: ['photoshop', 'premiere', 'after-effects', 'photo-studio', 'video-studio', 'motion-studio'] },
14
+ input: { type: 'string', description: 'Existing .blend, image/document, or project file inside the workspace.' },
15
+ output: { type: 'string', description: 'New .blend, PSD/PNG, PRPROJ, or AEP file inside the workspace; never overwrites.' },
16
+ stages: {
17
+ type: 'array',
18
+ description: 'Stages for studio_pipeline: array of { id, type: "3d_batch"|"studio_batch", config }',
19
+ items: {
20
+ type: 'object',
21
+ properties: {
22
+ id: { type: 'string', description: 'Stage identifier' },
23
+ type: { type: 'string', enum: ['3d_batch', 'studio_batch'], description: 'Stage batch execution type' },
24
+ config: {
25
+ type: 'object',
26
+ description: 'Stage payload configuration',
27
+ properties: {
28
+ host: { type: 'string' },
29
+ input: { type: 'string' },
30
+ output: { type: 'string' },
31
+ actions: {
32
+ type: 'array',
33
+ items: {
34
+ type: 'object',
35
+ properties: {
36
+ type: { type: 'string' }
37
+ }
38
+ }
39
+ }
40
+ }
41
+ }
42
+ },
43
+ required: ['type']
44
+ }
45
+ },
46
+ actions: { type: 'array', maxItems: 64, items: {
47
+ type: 'object', properties: {
48
+ type: { type: 'string', enum: [
49
+ 'add_cube', 'add_sphere', 'add_cylinder', 'add_plane', 'add_camera', 'add_light',
50
+ 'material', 'render', 'transform', 'resize', 'rotate', 'crop', 'layer', 'new_layer', 'flatten',
51
+ 'composition', 'sequence', 'import', 'text', 'marker', 'cut', 'solid', 'adjust_color'
52
+ ] },
53
+ name: { type: 'string' },
54
+ width: { type: 'number' }, height: { type: 'number' },
55
+ angle: { type: 'number' }, index: { type: 'integer' },
56
+ opacity: { type: 'number' }, visible: { type: 'boolean' },
57
+ duration: { type: 'number' }, fps: { type: 'number' },
58
+ path: { type: 'string' }, start: { type: 'number' }, text: { type: 'string' },
59
+ location: { type: 'array', items: { type: 'number' }, minItems: 3, maxItems: 3 },
60
+ scale: { type: 'array', items: { type: 'number' }, minItems: 3, maxItems: 3 },
61
+ rotation: { type: 'array', items: { type: 'number' }, minItems: 3, maxItems: 3 },
62
+ radius: { type: 'number' }, depth: { type: 'number' },
63
+ lightType: { type: 'string', enum: ['POINT', 'SUN', 'SPOT', 'AREA'] },
64
+ energy: { type: 'number' },
65
+ color: { type: 'array', items: { type: 'number' }, minItems: 4, maxItems: 4 },
66
+ outputPath: { type: 'string' },
67
+ resolution: { type: 'array', items: { type: 'number' }, minItems: 2, maxItems: 2 },
68
+ comment: { type: 'string' },
69
+ track: { type: 'integer' },
70
+ time: { type: 'number' },
71
+ left: { type: 'number' },
72
+ top: { type: 'number' },
73
+ blendMode: { type: 'string' },
74
+ size: { type: 'number' },
75
+ font: { type: 'string' },
76
+ x: { type: 'number' },
77
+ y: { type: 'number' },
78
+ brightness: { type: 'number' },
79
+ contrast: { type: 'number' },
80
+ roughness: { type: 'number' },
81
+ metallic: { type: 'number' }
82
+ }, required: ['type'], additionalProperties: false
83
+ } }
84
+ }, required: ['action'], additionalProperties: false
85
+ }
86
+ },
87
+ async execute(value, context = {}) {
88
+ try {
89
+ if (!value || typeof value !== 'object')
90
+ throw new Error('Expected creative app arguments');
91
+ const args = value;
92
+ if (args.action === 'status')
93
+ return { success: true, apps: await creativeAppStatus() };
94
+ if (args.action === 'studio_edit')
95
+ return await runStudioEdit(args, process.cwd(), context.signal);
96
+ if (args.action === 'blender_batch')
97
+ return await runBlenderBatch(args, process.cwd(), context.signal);
98
+ if (args.action === 'studio_pipeline')
99
+ return await runStudioPipeline(args, process.cwd(), context.signal);
100
+ throw new Error('Unknown creative app action');
101
+ }
102
+ catch (error) {
103
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
104
+ }
105
+ }
106
+ };
@@ -0,0 +1,99 @@
1
+ import { runStudioEdit } from '../creative/studioScripts.js';
2
+ import { creativeAppStatus } from '../creative/apps.js';
3
+ import { runBlenderBatch } from '../creative/blenderBatch.js';
4
+ import { runStudioPipeline } from '../creative/pipeline.js';
5
+
6
+ export const creativeApps = {
7
+ schema: {
8
+ name: 'creative_apps',
9
+ description: 'Inspect installed creative applications, run bounded creative batches, or orchestrate multi-stage creative pipelines. Blender batch creates and edits 3D scenes (primitives, materials, lights, cameras, render) and saves a new .blend. studio_edit uses native scripting: Photo Studio (resize/rotate/layers/flatten -> PSD/PNG/JPG/WEBP), Video Studio (sequences/imports/timeline cuts/markers -> PRPROJ), Motion Studio (compositions/imports/text -> AEP). studio_pipeline executes sequential coordinated multi-host stages. Requires workspace scoping.',
10
+ parameters: {
11
+ type: 'object',
12
+ properties: {
13
+ action: { type: 'string', enum: ['status', 'blender_batch', 'studio_edit', 'studio_pipeline'] },
14
+ host: { type: 'string', enum: ['photoshop', 'premiere', 'after-effects', 'photo-studio', 'video-studio', 'motion-studio'] },
15
+ input: { type: 'string', description: 'Existing .blend, image/document, or project file inside the workspace.' },
16
+ output: { type: 'string', description: 'New .blend, PSD/PNG, PRPROJ, or AEP file inside the workspace; never overwrites.' },
17
+ stages: {
18
+ type: 'array',
19
+ description: 'Stages for studio_pipeline: array of { id, type: "3d_batch"|"studio_batch", config }',
20
+ items: {
21
+ type: 'object',
22
+ properties: {
23
+ id: { type: 'string', description: 'Stage identifier' },
24
+ type: { type: 'string', enum: ['3d_batch', 'studio_batch'], description: 'Stage batch execution type' },
25
+ config: {
26
+ type: 'object',
27
+ description: 'Stage payload configuration',
28
+ properties: {
29
+ host: { type: 'string' },
30
+ input: { type: 'string' },
31
+ output: { type: 'string' },
32
+ actions: {
33
+ type: 'array',
34
+ items: {
35
+ type: 'object',
36
+ properties: {
37
+ type: { type: 'string' }
38
+ }
39
+ }
40
+ }
41
+ }
42
+ }
43
+ },
44
+ required: ['type']
45
+ }
46
+ },
47
+ actions: { type: 'array', maxItems: 64, items: {
48
+ type: 'object', properties: {
49
+ type: { type: 'string', enum: [
50
+ 'add_cube', 'add_sphere', 'add_cylinder', 'add_plane', 'add_camera', 'add_light',
51
+ 'material', 'render', 'transform', 'resize', 'rotate', 'crop', 'layer', 'new_layer', 'flatten',
52
+ 'composition', 'sequence', 'import', 'text', 'marker', 'cut', 'solid', 'adjust_color'
53
+ ] },
54
+ name: { type: 'string' },
55
+ width: { type: 'number' }, height: { type: 'number' },
56
+ angle: { type: 'number' }, index: { type: 'integer' },
57
+ opacity: { type: 'number' }, visible: { type: 'boolean' },
58
+ duration: { type: 'number' }, fps: { type: 'number' },
59
+ path: { type: 'string' }, start: { type: 'number' }, text: { type: 'string' },
60
+ location: { type: 'array', items: { type: 'number' }, minItems: 3, maxItems: 3 },
61
+ scale: { type: 'array', items: { type: 'number' }, minItems: 3, maxItems: 3 },
62
+ rotation: { type: 'array', items: { type: 'number' }, minItems: 3, maxItems: 3 },
63
+ radius: { type: 'number' }, depth: { type: 'number' },
64
+ lightType: { type: 'string', enum: ['POINT', 'SUN', 'SPOT', 'AREA'] },
65
+ energy: { type: 'number' },
66
+ color: { type: 'array', items: { type: 'number' }, minItems: 4, maxItems: 4 },
67
+ outputPath: { type: 'string' },
68
+ resolution: { type: 'array', items: { type: 'number' }, minItems: 2, maxItems: 2 },
69
+ comment: { type: 'string' },
70
+ track: { type: 'integer' },
71
+ time: { type: 'number' },
72
+ left: { type: 'number' },
73
+ top: { type: 'number' },
74
+ blendMode: { type: 'string' },
75
+ size: { type: 'number' },
76
+ font: { type: 'string' },
77
+ x: { type: 'number' },
78
+ y: { type: 'number' },
79
+ brightness: { type: 'number' },
80
+ contrast: { type: 'number' },
81
+ roughness: { type: 'number' },
82
+ metallic: { type: 'number' }
83
+ }, required: ['type'], additionalProperties: false
84
+ } }
85
+ }, required: ['action'], additionalProperties: false
86
+ }
87
+ },
88
+ async execute(value: unknown, context: { signal?: AbortSignal } = {}) {
89
+ try {
90
+ if (!value || typeof value !== 'object') throw new Error('Expected creative app arguments');
91
+ const args = value as Record<string, unknown>;
92
+ if (args.action === 'status') return { success: true, apps: await creativeAppStatus() };
93
+ if (args.action === 'studio_edit') return await runStudioEdit(args, process.cwd(), context.signal);
94
+ if (args.action === 'blender_batch') return await runBlenderBatch(args, process.cwd(), context.signal);
95
+ if (args.action === 'studio_pipeline') return await runStudioPipeline(args, process.cwd(), context.signal);
96
+ throw new Error('Unknown creative app action');
97
+ } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) }; }
98
+ }
99
+ };
@@ -1,3 +1,4 @@
1
+ import { creativeApps } from './creativeApps.js';
1
2
  import { sysRun, sysRunBg } from './sysRun.js';
2
3
  import { fsRead } from './fsRead.js';
3
4
  import { fsWrite } from './fsWrite.js';
@@ -119,7 +120,7 @@ const TOOL_MODULES = [
119
120
  gameAssetHarvester, gameAudioSynth, gameVfxEmitterGen,
120
121
  gameShaderForge, gameSceneBuilder, gameEntitySpawner,
121
122
  gameTilemapGenerator, gameUiHudBuilder, gameEconomyBalancer,
122
- gameDataForge, gameWebRuntimeInspector, gameCanvasAudit, readNativeSkill, createSkill
123
+ gameDataForge, gameWebRuntimeInspector, gameCanvasAudit, readNativeSkill, createSkill, creativeApps
123
124
  ];
124
125
 
125
126
  // Base static definitions (backwards compatible). Order is preserved from the