@causyne/agent 0.0.0-stage → 1.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +102 -0
- package/README.md +31 -2
- package/SECURITY.md +24 -0
- package/bin/causyne.js +13 -0
- package/docs/ARCHITECTURE.md +63 -0
- package/docs/PROVENANCE.md +29 -0
- package/docs/SECURITY_REVIEW.md +89 -0
- package/docs/THIRD_PARTY_NOTICES.md +49 -0
- package/native-skills/ai-engineering/context-window-management/SKILL.md +43 -0
- package/native-skills/ai-engineering/llm-evaluation/SKILL.md +46 -0
- package/native-skills/ai-engineering/model-cost-and-latency/SKILL.md +46 -0
- package/native-skills/ai-engineering/prompt-construction/SKILL.md +42 -0
- package/native-skills/ai-engineering/retrieval-augmented-generation/SKILL.md +42 -0
- package/native-skills/ai-engineering/structured-output-and-schemas/SKILL.md +45 -0
- package/native-skills/ai-engineering/tool-calling-agents/SKILL.md +43 -0
- package/native-skills/android/app-performance-profiling/SKILL.md +42 -0
- package/native-skills/android/background-work-scheduling/SKILL.md +47 -0
- package/native-skills/android/compose-state-and-recomposition/SKILL.md +43 -0
- package/native-skills/android/gradle-build-configuration/SKILL.md +41 -0
- package/native-skills/android/lifecycle-and-leaks/SKILL.md +46 -0
- package/native-skills/android/play-store-release/SKILL.md +43 -0
- package/native-skills/android/room-and-persistence/SKILL.md +40 -0
- package/native-skills/backend/authentication-and-sessions/SKILL.md +45 -0
- package/native-skills/backend/background-jobs-and-queues/SKILL.md +42 -0
- package/native-skills/backend/caching-strategy/SKILL.md +44 -0
- package/native-skills/backend/graceful-shutdown/SKILL.md +42 -0
- package/native-skills/backend/rate-limiting-and-quotas/SKILL.md +40 -0
- package/native-skills/backend/rest-api-design/SKILL.md +40 -0
- package/native-skills/backend/webhook-delivery-and-receipt/SKILL.md +43 -0
- package/native-skills/cloud/cost-optimisation/SKILL.md +46 -0
- package/native-skills/cloud/iam-least-privilege/SKILL.md +43 -0
- package/native-skills/cloud/infrastructure-as-code/SKILL.md +43 -0
- package/native-skills/cloud/multi-region-and-failover/SKILL.md +43 -0
- package/native-skills/cloud/serverless-patterns/SKILL.md +47 -0
- package/native-skills/data/data-quality-monitoring/SKILL.md +51 -0
- package/native-skills/data/etl-pipeline-reliability/SKILL.md +43 -0
- package/native-skills/data/exploratory-analysis/SKILL.md +43 -0
- package/native-skills/data/metric-definition/SKILL.md +42 -0
- package/native-skills/data/notebook-to-production/SKILL.md +43 -0
- package/native-skills/database/backup-and-recovery/SKILL.md +43 -0
- package/native-skills/database/connection-pooling/SKILL.md +42 -0
- package/native-skills/database/data-integrity-and-constraints/SKILL.md +42 -0
- package/native-skills/database/query-optimisation/SKILL.md +38 -0
- package/native-skills/database/safe-migrations/SKILL.md +47 -0
- package/native-skills/database/schema-design/SKILL.md +38 -0
- package/native-skills/database/transactions-and-isolation/SKILL.md +41 -0
- package/native-skills/design/colour-system-construction/SKILL.md +38 -0
- package/native-skills/design/design-direction-brief/SKILL.md +41 -0
- package/native-skills/design/motion-and-transitions/SKILL.md +38 -0
- package/native-skills/design/responsive-strategy/SKILL.md +39 -0
- package/native-skills/design/spacing-and-rhythm/SKILL.md +38 -0
- package/native-skills/design/type-scale-and-hierarchy/SKILL.md +39 -0
- package/native-skills/design/visual-qa-loop/SKILL.md +42 -0
- package/native-skills/devops/docker-multi-stage-builds/SKILL.md +60 -0
- package/native-skills/devops/github-actions-ci/SKILL.md +60 -0
- package/native-skills/devops/incident-triage/SKILL.md +38 -0
- package/native-skills/devops/kubernetes-deployment-hardening/SKILL.md +64 -0
- package/native-skills/devops/observability-baseline/SKILL.md +38 -0
- package/native-skills/devops/secret-management/SKILL.md +40 -0
- package/native-skills/devops/terraform-state-and-modules/SKILL.md +36 -0
- package/native-skills/devops/zero-downtime-deploys/SKILL.md +40 -0
- package/native-skills/git/bisect-regression-hunt/SKILL.md +43 -0
- package/native-skills/git/branch-strategy/SKILL.md +39 -0
- package/native-skills/git/commit-hygiene/SKILL.md +41 -0
- package/native-skills/git/history-recovery/SKILL.md +44 -0
- package/native-skills/git/large-repository-surgery/SKILL.md +46 -0
- package/native-skills/git/merge-conflict-resolution/SKILL.md +42 -0
- package/native-skills/media/cinematic-3d-shot/SKILL.md +104 -0
- package/native-skills/mobile/app-startup-optimisation/SKILL.md +41 -0
- package/native-skills/mobile/cross-platform-tradeoffs/SKILL.md +42 -0
- package/native-skills/mobile/deep-linking/SKILL.md +39 -0
- package/native-skills/mobile/mobile-storage-security/SKILL.md +45 -0
- package/native-skills/mobile/offline-first-sync/SKILL.md +42 -0
- package/native-skills/mobile/push-notifications/SKILL.md +38 -0
- package/native-skills/performance/algorithmic-complexity/SKILL.md +43 -0
- package/native-skills/performance/concurrency-and-parallelism/SKILL.md +42 -0
- package/native-skills/performance/load-testing/SKILL.md +42 -0
- package/native-skills/performance/memory-and-leak-hunting/SKILL.md +41 -0
- package/native-skills/performance/network-and-payload-efficiency/SKILL.md +42 -0
- package/native-skills/performance/profiling-methodology/SKILL.md +46 -0
- package/native-skills/python/async-correctness/SKILL.md +40 -0
- package/native-skills/python/cli-tool-construction/SKILL.md +44 -0
- package/native-skills/python/data-processing-pipelines/SKILL.md +38 -0
- package/native-skills/python/environment-isolation/SKILL.md +41 -0
- package/native-skills/python/packaging-and-distribution/SKILL.md +56 -0
- package/native-skills/python/pytest-suite-design/SKILL.md +39 -0
- package/native-skills/python/type-hints-and-static-checking/SKILL.md +42 -0
- package/native-skills/security/authorization-model/SKILL.md +42 -0
- package/native-skills/security/crypto-usage/SKILL.md +48 -0
- package/native-skills/security/dependency-supply-chain/SKILL.md +42 -0
- package/native-skills/security/input-validation-and-injection/SKILL.md +44 -0
- package/native-skills/security/secure-file-uploads/SKILL.md +46 -0
- package/native-skills/security/security-headers-and-csp/SKILL.md +44 -0
- package/native-skills/security/threat-modelling/SKILL.md +46 -0
- package/native-skills/testing/end-to-end-test-stability/SKILL.md +42 -0
- package/native-skills/testing/property-based-testing/SKILL.md +46 -0
- package/native-skills/testing/regression-test-from-a-bug/SKILL.md +41 -0
- package/native-skills/testing/snapshot-and-visual-regression/SKILL.md +42 -0
- package/native-skills/testing/test-doubles-and-mocking/SKILL.md +43 -0
- package/native-skills/testing/test-strategy-and-pyramid/SKILL.md +39 -0
- package/native-skills/testing/writing-good-assertions/SKILL.md +42 -0
- package/native-skills/web/accessible-components/SKILL.md +45 -0
- package/native-skills/web/api-client-resilience/SKILL.md +39 -0
- package/native-skills/web/bundle-size-reduction/SKILL.md +38 -0
- package/native-skills/web/core-web-vitals/SKILL.md +40 -0
- package/native-skills/web/css-layout-systems/SKILL.md +41 -0
- package/native-skills/web/form-validation-ux/SKILL.md +39 -0
- package/native-skills/web/react-state-architecture/SKILL.md +41 -0
- package/native-skills/web/ssr-hydration-correctness/SKILL.md +45 -0
- package/package.json +68 -4
- package/src/agentLoop.js +2458 -0
- package/src/appChannel.js +109 -0
- package/src/approvalGrants.js +66 -0
- package/src/auditLog.js +52 -0
- package/src/autoUpdate.js +12 -0
- package/src/capabilities.js +36 -0
- package/src/checkpoints.js +1199 -0
- package/src/cliMode.js +638 -0
- package/src/commands.js +805 -0
- package/src/compiledSkills.js +540 -0
- package/src/config.js +130 -0
- package/src/contextManager.js +498 -0
- package/src/cortex/autoCondenser.js +192 -0
- package/src/cortex/provisioner.js +27 -0
- package/src/cortex/vectorDb.js +447 -0
- package/src/designPalettes.js +99 -0
- package/src/devMode.js +6 -0
- package/src/embodimentSession.js +180 -0
- package/src/entrypoint.js +194 -0
- package/src/execSafe.js +21 -0
- package/src/gitSnapshot.js +378 -0
- package/src/mcp/jsonRpc.js +165 -0
- package/src/mcp/mcpClient.js +136 -0
- package/src/mcp/mcpRegistry.js +207 -0
- package/src/memory.js +62 -0
- package/src/memoryManager.js +124 -0
- package/src/modelCatalogue.js +132 -0
- package/src/modelIds.js +42 -0
- package/src/modelRouter.js +145 -0
- package/src/outputStore.js +246 -0
- package/src/perception.js +124 -0
- package/src/planCache.js +437 -0
- package/src/procSignal.js +32 -0
- package/src/providers/anthropic.js +408 -0
- package/src/providers/base.js +328 -0
- package/src/providers/dashscope-byok.js +86 -0
- package/src/providers/deepseek.js +80 -0
- package/src/providers/factory.js +37 -0
- package/src/providers/gemini.js +346 -0
- package/src/providers/moonshot.js +73 -0
- package/src/providers/muse.js +169 -0
- package/src/providers/ollama.js +104 -0
- package/src/providers/openai-compatible.js +84 -0
- package/src/providers/openai.js +20 -0
- package/src/providers/qwen-byok.js +76 -0
- package/src/providers/xai.js +73 -0
- package/src/provisioning/capabilityConsent.js +24 -0
- package/src/provisioning/capabilityProvisioner.js +271 -0
- package/src/provisioning/capabilityTools.js +29 -0
- package/src/reporting.js +373 -0
- package/src/sandbox.js +529 -0
- package/src/sessionStore.js +279 -0
- package/src/skillsManager.js +288 -0
- package/src/slopDetector.js +307 -0
- package/src/taskBuffer.js +45 -0
- package/src/taskShape.js +241 -0
- package/src/textDiff.js +150 -0
- package/src/tokenTracker.js +560 -0
- package/src/tool-scripts/ax_snapshot.applescript +39 -0
- package/src/tool-scripts/ax_snapshot.ps1 +10 -0
- package/src/tool-scripts/desktop_act.applescript +58 -0
- package/src/tool-scripts/resolve_automator.py +119 -0
- package/src/toolPacks.js +524 -0
- package/src/tools/archive.js +63 -0
- package/src/tools/astryxOracle.js +78 -0
- package/src/tools/axSnapshot.js +165 -0
- package/src/tools/blenderAutomator.js +349 -0
- package/src/tools/browserAutomator.js +252 -0
- package/src/tools/checkSystemSpecs.js +64 -0
- package/src/tools/clipboard.js +36 -0
- package/src/tools/codeSearchAst.js +101 -0
- package/src/tools/codeSearcher.js +230 -0
- package/src/tools/codeTools.js +262 -0
- package/src/tools/codebaseSearch.js +75 -0
- package/src/tools/createSkill.js +129 -0
- package/src/tools/deferredMcpSearch.js +246 -0
- package/src/tools/desktopAct.js +208 -0
- package/src/tools/desktopOpen.js +34 -0
- package/src/tools/dynamicLoader.js +44 -0
- package/src/tools/executeProceduralMedia.js +93 -0
- package/src/tools/fsDelete.js +51 -0
- package/src/tools/fsEdit.js +105 -0
- package/src/tools/fsFind.js +128 -0
- package/src/tools/fsList.js +77 -0
- package/src/tools/fsMove.js +57 -0
- package/src/tools/fsRead.js +87 -0
- package/src/tools/fsSearch.js +198 -0
- package/src/tools/fsWrite.js +41 -0
- package/src/tools/gameAiBehaviorTree.js +208 -0
- package/src/tools/gameAnimationRig.js +194 -0
- package/src/tools/gameAssetFetcher.js +372 -0
- package/src/tools/gameAssetHarvester.js +210 -0
- package/src/tools/gameAudioSynth.js +267 -0
- package/src/tools/gameBuilder.js +886 -0
- package/src/tools/gameCanvasAudit.js +101 -0
- package/src/tools/gameCinematicSequencer.js +151 -0
- package/src/tools/gameDataForge.js +124 -0
- package/src/tools/gameDialogueTreeGen.js +166 -0
- package/src/tools/gameEconomyBalancer.js +161 -0
- package/src/tools/gameEntitySpawner.js +207 -0
- package/src/tools/gameLoreForge.js +153 -0
- package/src/tools/gameQuestArcBuilder.js +152 -0
- package/src/tools/gameSceneBuilder.js +158 -0
- package/src/tools/gameShaderForge.js +181 -0
- package/src/tools/gameTilemapGenerator.js +164 -0
- package/src/tools/gameUiHudBuilder.js +292 -0
- package/src/tools/gameVfxEmitterGen.js +108 -0
- package/src/tools/gameWebRuntimeInspector.js +91 -0
- package/src/tools/generateMediaViaApi.js +358 -0
- package/src/tools/gitExec.js +54 -0
- package/src/tools/godotAutomator.js +437 -0
- package/src/tools/manageLocalAiDependencies.js +220 -0
- package/src/tools/memoryWrite.js +99 -0
- package/src/tools/multiEdit.js +107 -0
- package/src/tools/palmierAutomator.js +104 -0
- package/src/tools/pdfReader.js +112 -0
- package/src/tools/persistentShell.js +148 -0
- package/src/tools/printer.js +316 -0
- package/src/tools/procTerm.js +91 -0
- package/src/tools/processList.js +52 -0
- package/src/tools/readNativeSkill.js +39 -0
- package/src/tools/registry.js +277 -0
- package/src/tools/resolveAutomator.js +102 -0
- package/src/tools/screenshot.js +75 -0
- package/src/tools/subagentOrchestrator.js +305 -0
- package/src/tools/sysRun.js +125 -0
- package/src/tools/todoWrite.js +75 -0
- package/src/tools/toolDispatch.js +127 -0
- package/src/tools/toolForge.js +124 -0
- package/src/tools/updatePlan.js +88 -0
- package/src/tools/viewImage.js +82 -0
- package/src/tools/visionInspector.js +154 -0
- package/src/tools/webFetch.js +74 -0
- package/src/tools/webSearch.js +82 -0
- package/src/tui/ask.js +164 -0
- package/src/tui/banner.js +158 -0
- package/src/tui/block.js +152 -0
- package/src/tui/chatRenderer.js +953 -0
- package/src/tui/completion.js +424 -0
- package/src/tui/dialogs.js +606 -0
- package/src/tui/diff.js +296 -0
- package/src/tui/history.js +135 -0
- package/src/tui/home.js +217 -0
- package/src/tui/invocation.js +105 -0
- package/src/tui/keys.js +443 -0
- package/src/tui/lineEditor.js +256 -0
- package/src/tui/overlay.js +256 -0
- package/src/tui/prompt.js +1258 -0
- package/src/tui/selectList.js +335 -0
- package/src/tui/stage.js +330 -0
- package/src/tui/stageSurface.js +609 -0
- package/src/tui/statusLine.js +254 -0
- package/src/tui/text.js +345 -0
- package/src/tui/theme.js +393 -0
- package/src/tui/toolDisplay.js +176 -0
- package/src/tui/transcript.js +235 -0
- package/src/tui/viewport.js +337 -0
- package/src/uncertainty.js +271 -0
- package/src/userSkills.js +265 -0
- package/src/utils/cli_ui.js +0 -0
- package/src/utils/inquirer.js +12 -0
- package/src/utils/paths.js +37 -0
- package/src/version.js +38 -0
- package/src/vfeTracker.js +220 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import { resolveUserPath } from '../utils/paths.js';
|
|
3
|
+
import { createReadStream, createWriteStream } from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { ensureCapability, loadCapabilityPackage } from '../provisioning/capabilityProvisioner.js';
|
|
6
|
+
|
|
7
|
+
export const archive = {
|
|
8
|
+
schema: {
|
|
9
|
+
name: 'archive',
|
|
10
|
+
description: 'Compress or extract archives securely using native Node streams (No Shell Tar).',
|
|
11
|
+
parameters: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
action: { type: 'string', enum: ['compress', 'extract'], description: 'Action to perform' },
|
|
15
|
+
source: { type: 'string', description: 'Absolute path of the file/directory to compress or extract' },
|
|
16
|
+
destination: { type: 'string', description: 'Absolute path for the output' }
|
|
17
|
+
},
|
|
18
|
+
required: ['action', 'source', 'destination']
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
async execute(args) {
|
|
22
|
+
try {
|
|
23
|
+
const source = resolveUserPath(args.source);
|
|
24
|
+
const dest = resolveUserPath(args.destination);
|
|
25
|
+
|
|
26
|
+
if (args.action === 'compress') {
|
|
27
|
+
const capability = await ensureCapability('archive');
|
|
28
|
+
if (!capability.ok) return { success: false, error: capability.reason, requiresConsent: capability.requiresConsent };
|
|
29
|
+
const archiver = await loadCapabilityPackage('archiver');
|
|
30
|
+
|
|
31
|
+
return new Promise(async (resolve) => {
|
|
32
|
+
const output = createWriteStream(dest);
|
|
33
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
34
|
+
|
|
35
|
+
output.on('close', () => resolve({ success: true, output: `Compressed ${archive.pointer()} total bytes to ${dest}` }));
|
|
36
|
+
archive.on('error', (err) => resolve({ success: false, error: err.message }));
|
|
37
|
+
|
|
38
|
+
archive.pipe(output);
|
|
39
|
+
|
|
40
|
+
const stat = await fs.stat(source);
|
|
41
|
+
if (stat.isDirectory()) {
|
|
42
|
+
archive.directory(source, false);
|
|
43
|
+
} else {
|
|
44
|
+
archive.file(source, { name: path.basename(source) });
|
|
45
|
+
}
|
|
46
|
+
await archive.finalize();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
else if (args.action === 'extract') {
|
|
50
|
+
const capability = await ensureCapability('archive');
|
|
51
|
+
if (!capability.ok) return { success: false, error: capability.reason, requiresConsent: capability.requiresConsent };
|
|
52
|
+
const extract = await loadCapabilityPackage('extract-zip');
|
|
53
|
+
|
|
54
|
+
await fs.mkdir(dest, { recursive: true });
|
|
55
|
+
await extract(source, { dir: dest });
|
|
56
|
+
|
|
57
|
+
return { success: true, output: `Extracted successfully to ${dest}` };
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
return { success: false, error: `Archive action failed: ${error.message}` };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
|
|
6
|
+
export const astryxOracle = {
|
|
7
|
+
schema: {
|
|
8
|
+
name: 'astryx_oracle',
|
|
9
|
+
description: 'Query the Astryx Design System (Meta) CLI for UI component documentation, design tokens, and templates. Use this to learn how to build UIs with Astryx.',
|
|
10
|
+
parameters: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
command: {
|
|
14
|
+
type: 'string',
|
|
15
|
+
enum: ['list_components', 'read_component', 'list_docs', 'read_doc', 'list_templates'],
|
|
16
|
+
description: 'The Astryx CLI command to run.',
|
|
17
|
+
},
|
|
18
|
+
target: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
description: 'The specific component or doc topic to read (e.g., "Button", "tokens"). Leave empty for list commands.',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
required: ['command'],
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
execute: async (args, context) => {
|
|
27
|
+
const { command, target = '' } = args;
|
|
28
|
+
|
|
29
|
+
// Sanitize target to prevent command injection
|
|
30
|
+
const sanitizedTarget = target.replace(/[^a-zA-Z0-9_-]/g, '');
|
|
31
|
+
|
|
32
|
+
let cliArgs = [];
|
|
33
|
+
switch (command) {
|
|
34
|
+
case 'list_components':
|
|
35
|
+
cliArgs = ['component'];
|
|
36
|
+
break;
|
|
37
|
+
case 'read_component':
|
|
38
|
+
if (!sanitizedTarget) return { success: false, error: 'target component name is required for read_component' };
|
|
39
|
+
cliArgs = ['component', sanitizedTarget];
|
|
40
|
+
break;
|
|
41
|
+
case 'list_docs':
|
|
42
|
+
cliArgs = ['docs'];
|
|
43
|
+
break;
|
|
44
|
+
case 'read_doc':
|
|
45
|
+
if (!sanitizedTarget) return { success: false, error: 'target doc name is required for read_doc' };
|
|
46
|
+
cliArgs = ['docs', sanitizedTarget];
|
|
47
|
+
break;
|
|
48
|
+
case 'list_templates':
|
|
49
|
+
cliArgs = ['template', '--list'];
|
|
50
|
+
break;
|
|
51
|
+
default:
|
|
52
|
+
return { success: false, error: 'Unknown command' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
// `--no` forbids npm from downloading an absent package. The user must
|
|
57
|
+
// install and review the third-party CLI explicitly before this adapter
|
|
58
|
+
// can call it. execFile keeps model-derived arguments out of a shell.
|
|
59
|
+
const { stdout } = await execFileAsync('npx', ['--no', '@astryxdesign/cli', ...cliArgs], {
|
|
60
|
+
encoding: 'utf8',
|
|
61
|
+
timeout: 30000,
|
|
62
|
+
maxBuffer: 4 * 1024 * 1024
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
success: true,
|
|
67
|
+
output: stdout.trim()
|
|
68
|
+
};
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return {
|
|
71
|
+
success: false,
|
|
72
|
+
error: error.stderr
|
|
73
|
+
? error.stderr.toString()
|
|
74
|
+
: `${error.message}. Install @astryxdesign/cli explicitly before using this optional integration.`
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import { recordObservation } from '../embodimentSession.js';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const MAC_AX_SCRIPT = readFileSync(new URL('../tool-scripts/ax_snapshot.applescript', import.meta.url), 'utf8');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* PowerShell + UIAutomation, the Windows counterpart.
|
|
10
|
+
*
|
|
11
|
+
* NOT VERIFIED ON HARDWARE — this machine is macOS. The macOS path was tested
|
|
12
|
+
* to its permission wall; this one is written from the documented
|
|
13
|
+
* UIAutomationClient surface and is marked so nobody mistakes it for measured.
|
|
14
|
+
*/
|
|
15
|
+
const WINDOWS_AX_SCRIPT = readFileSync(new URL('../tool-scripts/ax_snapshot.ps1', import.meta.url), 'utf8');
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
const MAC_INTERPRETER = "osascript";
|
|
19
|
+
const WIN_INTERPRETER = "powershell";
|
|
20
|
+
|
|
21
|
+
const MAX_NODES_CEILING = 120;
|
|
22
|
+
const DEFAULT_MAX_NODES = 60;
|
|
23
|
+
|
|
24
|
+
/** macOS returns -1743 when Accessibility has not been granted. */
|
|
25
|
+
function isConsentRefusal(text) {
|
|
26
|
+
return /-1743|not authorized|assistive access|accessibility/i.test(text || '');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Run an interpreter with the script on STDIN. Never touches disk.
|
|
31
|
+
*
|
|
32
|
+
* @returns {Promise<{code:number, stdout:string, stderr:string}>}
|
|
33
|
+
*/
|
|
34
|
+
function runWithScriptOnStdin(binary, args, scriptText, timeoutMs = 15000) {
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
let child;
|
|
37
|
+
try {
|
|
38
|
+
child = spawn(binary, args);
|
|
39
|
+
} catch (e) {
|
|
40
|
+
resolve({ code: -1, stdout: '', stderr: e.message });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let stdout = '';
|
|
45
|
+
let stderr = '';
|
|
46
|
+
const timer = setTimeout(() => {
|
|
47
|
+
try { child.kill(); } catch (e) { /* already gone */ }
|
|
48
|
+
resolve({ code: -1, stdout, stderr: stderr + '\ntimed out' });
|
|
49
|
+
}, timeoutMs);
|
|
50
|
+
|
|
51
|
+
child.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
52
|
+
child.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
53
|
+
child.on('error', (e) => { clearTimeout(timer); resolve({ code: -1, stdout, stderr: e.message }); });
|
|
54
|
+
child.on('close', (code) => { clearTimeout(timer); resolve({ code, stdout, stderr }); });
|
|
55
|
+
|
|
56
|
+
child.stdin.on('error', () => { /* interpreter exited before reading */ });
|
|
57
|
+
child.stdin.write(scriptText);
|
|
58
|
+
child.stdin.end();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Collapse the interpreter's flat rows into the compact text the model reads.
|
|
64
|
+
*
|
|
65
|
+
* Deliberately NOT JSON: `AXButton|Export|` is a third the tokens of
|
|
66
|
+
* `{"role":"AXButton","title":"Export","value":""}` and carries the same
|
|
67
|
+
* information. Channel B only beats a screenshot on cost if it stays cheap.
|
|
68
|
+
*/
|
|
69
|
+
function formatRows(raw, maxNodes) {
|
|
70
|
+
const rows = String(raw || '')
|
|
71
|
+
.split('\n')
|
|
72
|
+
.map(line => line.trim())
|
|
73
|
+
.filter(line => line.includes('|'))
|
|
74
|
+
.slice(0, maxNodes);
|
|
75
|
+
|
|
76
|
+
if (rows.length === 0) return null;
|
|
77
|
+
|
|
78
|
+
const lines = [];
|
|
79
|
+
for (const row of rows) {
|
|
80
|
+
const [role = '', title = '', value = ''] = row.split('|');
|
|
81
|
+
const label = [title, value].filter(Boolean).join(' = ');
|
|
82
|
+
lines.push(label ? `${role}: ${label}` : role);
|
|
83
|
+
}
|
|
84
|
+
return lines.join('\n');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const axSnapshot = {
|
|
88
|
+
schema: {
|
|
89
|
+
name: 'ax_snapshot',
|
|
90
|
+
description: 'Read the accessibility tree of an application window as structured text — every button, field, menu and label with its role, title and current value. Use this INSTEAD of a screenshot whenever you need to know what is on screen or verify that an action worked: it is roughly 5x cheaper than an image and gives you element names you can target precisely. Requires the user to have granted Accessibility permission to the terminal running Causyne.',
|
|
91
|
+
parameters: {
|
|
92
|
+
type: 'object',
|
|
93
|
+
properties: {
|
|
94
|
+
app: {
|
|
95
|
+
type: 'string',
|
|
96
|
+
description: 'Application process name, e.g. "Safari" or "Finder". Omit to read whichever application is frontmost.'
|
|
97
|
+
},
|
|
98
|
+
maxNodes: {
|
|
99
|
+
type: 'integer',
|
|
100
|
+
description: `Maximum elements to return. Defaults to ${DEFAULT_MAX_NODES}, hard cap ${MAX_NODES_CEILING}.`
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
async execute(args = {}, context = {}) {
|
|
107
|
+
const app = typeof args.app === 'string' ? args.app.trim() : '';
|
|
108
|
+
const requested = Number.isInteger(args.maxNodes) ? args.maxNodes : DEFAULT_MAX_NODES;
|
|
109
|
+
const maxNodes = Math.max(1, Math.min(requested, MAX_NODES_CEILING));
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
if (process.platform === 'darwin') {
|
|
113
|
+
const result = await runWithScriptOnStdin(MAC_INTERPRETER, ['-', app, String(maxNodes)], MAC_AX_SCRIPT);
|
|
114
|
+
|
|
115
|
+
if (isConsentRefusal(result.stderr)) {
|
|
116
|
+
return {
|
|
117
|
+
success: false,
|
|
118
|
+
requiresConsent: true,
|
|
119
|
+
error: "macOS has not granted Accessibility permission to the terminal running Causyne. The user must enable it in System Settings > Privacy & Security > Accessibility, "
|
|
120
|
+
+ 'then this tool works immediately. Ask them to do that; do not retry until they confirm.'
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (result.code !== 0) {
|
|
124
|
+
return { success: false, error: `Could not read the accessibility tree: ${result.stderr.trim() || 'unknown error'}` };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const output = formatRows(result.stdout, maxNodes);
|
|
128
|
+
if (!output) {
|
|
129
|
+
return { success: true, output: `No labelled UI elements found${app ? ` in "${app}"` : ' in the frontmost application'}.` };
|
|
130
|
+
}
|
|
131
|
+
// C5: remember WHICH app was read and a hash of what it looked like, so
|
|
132
|
+
// a resumed task can tell "the screen is as I left it" from "the screen
|
|
133
|
+
// changed while I was gone". The tree itself is never stored — a record
|
|
134
|
+
// of what the user's screen said would be a surveillance artefact.
|
|
135
|
+
recordObservation(context?.parentLoop?.taskId, app, output);
|
|
136
|
+
return { success: true, output };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (process.platform === 'win32') {
|
|
140
|
+
const result = await runWithScriptOnStdin(
|
|
141
|
+
WIN_INTERPRETER,
|
|
142
|
+
['-NoProfile', '-NonInteractive', '-Command', '-', String(maxNodes)],
|
|
143
|
+
WINDOWS_AX_SCRIPT
|
|
144
|
+
);
|
|
145
|
+
if (result.code !== 0) {
|
|
146
|
+
return { success: false, error: `Could not read the accessibility tree: ${result.stderr.trim() || 'unknown error'}` };
|
|
147
|
+
}
|
|
148
|
+
const output = formatRows(result.stdout, maxNodes);
|
|
149
|
+
return output
|
|
150
|
+
? { success: true, output }
|
|
151
|
+
: { success: true, output: 'No labelled UI elements found.' };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Linux AT-SPI runs over D-Bus and needs a binding this tool deliberately
|
|
155
|
+
// does not carry. Saying so plainly beats returning an empty tree that
|
|
156
|
+
// the model would read as "the window is empty".
|
|
157
|
+
return {
|
|
158
|
+
success: false,
|
|
159
|
+
error: "Reading the accessibility tree is supported on macOS and Windows. On this platform, use screenshot({analyzeText:true}) for OCR bounding boxes instead."
|
|
160
|
+
};
|
|
161
|
+
} catch (e) {
|
|
162
|
+
return { success: false, error: e.message };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
};
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { spawn, execFileSync } from 'child_process';
|
|
5
|
+
|
|
6
|
+
// Time to wait for bridge to become active after launching Blender (ms per poll)
|
|
7
|
+
const BRIDGE_POLL_INTERVAL_MS = 500;
|
|
8
|
+
// Maximum number of polls before giving up
|
|
9
|
+
const BRIDGE_POLL_MAX_ATTEMPTS = 20;
|
|
10
|
+
|
|
11
|
+
function getBlenderExecutablePath() {
|
|
12
|
+
const platform = os.platform();
|
|
13
|
+
const homeDir = os.homedir();
|
|
14
|
+
const causyneBlenderDir = path.join(homeDir, '.causyne', 'engines', 'blender');
|
|
15
|
+
|
|
16
|
+
const candidates = platform === 'darwin' ? [
|
|
17
|
+
path.join(causyneBlenderDir, 'blender'),
|
|
18
|
+
'/Applications/Blender.app/Contents/MacOS/Blender',
|
|
19
|
+
'/usr/local/bin/blender',
|
|
20
|
+
'/opt/homebrew/bin/blender'
|
|
21
|
+
] : platform === 'win32' ? [
|
|
22
|
+
path.join(causyneBlenderDir, 'blender.exe'),
|
|
23
|
+
'C:\\Program Files\\Blender Foundation\\Blender\\blender.exe'
|
|
24
|
+
] : [
|
|
25
|
+
path.join(causyneBlenderDir, 'blender'),
|
|
26
|
+
'/usr/bin/blender',
|
|
27
|
+
'/usr/local/bin/blender'
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// EXISTENCE IS NOT IDENTITY.
|
|
31
|
+
//
|
|
32
|
+
// This used to return the first path that merely EXISTED, and the provisioned
|
|
33
|
+
// `~/.causyne/engines/blender/blender` slot is checked first so a user can
|
|
34
|
+
// supply their own build. On a real machine that slot held a 55-byte shell
|
|
35
|
+
// script — `echo "Sovereign blender stub wrapper v1.3.2"` — left behind by an
|
|
36
|
+
// earlier provisioning experiment. It shadowed a genuine Blender 5.2 LTS in
|
|
37
|
+
// /Applications, so every headless render invoked a stub that printed one
|
|
38
|
+
// line and exited 0. The tool reported success and produced nothing, which is
|
|
39
|
+
// the worst failure mode available: a silent one.
|
|
40
|
+
//
|
|
41
|
+
// Ask each candidate who it is. Only something that answers "Blender" counts.
|
|
42
|
+
for (const cand of candidates) {
|
|
43
|
+
if (!fs.existsSync(cand)) continue;
|
|
44
|
+
try {
|
|
45
|
+
const reply = execFileSync(cand, ['--version'], { timeout: 8000, encoding: 'utf8' });
|
|
46
|
+
if (/^blender\s+\d/i.test(String(reply).trim())) return cand;
|
|
47
|
+
} catch (e) {
|
|
48
|
+
// Unreadable, not executable, or it hung: not a usable Blender.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return candidates[0];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getWorkspaceTempDir() {
|
|
55
|
+
const wsTmp = path.join(process.cwd(), '.tmp_causyne_3d');
|
|
56
|
+
try {
|
|
57
|
+
if (!fs.existsSync(wsTmp)) fs.mkdirSync(wsTmp, { recursive: true });
|
|
58
|
+
return wsTmp;
|
|
59
|
+
} catch (e) {
|
|
60
|
+
return os.tmpdir();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async function runHeadlessCli(scriptPath, customCode, results, simulateMissing = false) {
|
|
71
|
+
const blenderBin = getBlenderExecutablePath();
|
|
72
|
+
if (simulateMissing || !fs.existsSync(blenderBin)) {
|
|
73
|
+
results.push(`[ENGINE MISSING or SIMULATED] Blender executable not found locally (${blenderBin}). Cascading to Sovereign Fallback...`);
|
|
74
|
+
results.push(`[Layer 2 Auto-Install Option] To install Blender (312 MB > 200 MB consent gate), call manage_local_dependencies with action="check_and_setupEng", engine="blender", userConsent: true.`);
|
|
75
|
+
results.push(`[Layer 3 Instant CC0 / WebGL Fallback] Or immediately invoke game.asset_fetch (action: "search_cc0", category: "3d_models") or generate_media_via_api / Three.js WebGL to fulfill user 3D/visual request instantly.`);
|
|
76
|
+
const err = new Error(`[ENGINE MISSING or SIMULATED] Blender binary not found.`);
|
|
77
|
+
err.fallback_required = true;
|
|
78
|
+
err.fallback_guidance = {
|
|
79
|
+
auto_install_option: 'Call manage_local_dependencies (action="check_and_setupEng", engine="blender") - requires explicit userConsent: true because 312 MB > 200 MB gate.',
|
|
80
|
+
instant_execution_option: 'Or invoke game.asset_fetch (search_cc0 3D models) or generate_media_via_api / Three.js WebGL rendering.'
|
|
81
|
+
};
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let targetPythonPath = scriptPath;
|
|
86
|
+
if (!targetPythonPath && customCode) {
|
|
87
|
+
const tempDir = path.join(getWorkspaceTempDir(), 'causyne_blender');
|
|
88
|
+
if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir, { recursive: true });
|
|
89
|
+
targetPythonPath = path.join(tempDir, `causyne_3d_gen_${Date.now()}.py`);
|
|
90
|
+
fs.writeFileSync(targetPythonPath, customCode, 'utf8');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!targetPythonPath || !fs.existsSync(targetPythonPath)) {
|
|
94
|
+
throw new Error('No valid Python script provided for headless Blender execution.');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return new Promise((resolve) => {
|
|
98
|
+
results.push(`[Layer 3 Headless] Launching Blender CLI in background: ${blenderBin} --background --python ${targetPythonPath}`);
|
|
99
|
+
const child = spawn(blenderBin, ['--background', '--python', targetPythonPath], {
|
|
100
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
let stdout = '';
|
|
104
|
+
let stderr = '';
|
|
105
|
+
child.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
106
|
+
child.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
107
|
+
|
|
108
|
+
child.on('close', (code) => {
|
|
109
|
+
if (code === 0) {
|
|
110
|
+
results.push(`[Layer 3 Headless] Execution successful:\n${stdout.trim()}`);
|
|
111
|
+
resolve({ success: true, log: results });
|
|
112
|
+
} else {
|
|
113
|
+
results.push(`[Layer 3 Headless] Execution failed (code ${code}):\n${stderr.trim() || stdout.trim()}`);
|
|
114
|
+
resolve({ success: false, error: `Blender CLI exited with code ${code}`, log: results });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const blenderAct = {
|
|
121
|
+
schema: {
|
|
122
|
+
name: 'blender.act',
|
|
123
|
+
description: 'Automate Blender 3D ("The Cursor for Blender 3D") through its local command-line interface across macOS, Windows, and Linux. Supports 12 stateful AGI actions: scene hierarchy inspection, object X-ray info, real-time selection tracking, non-destructive mutation, auto camera framing, 3-point studio lighting, and full 3D asset export (.blend, .glb, .obj, .fbx).',
|
|
124
|
+
parameters: {
|
|
125
|
+
type: 'object',
|
|
126
|
+
properties: {
|
|
127
|
+
simulateMissing: {
|
|
128
|
+
type: 'boolean',
|
|
129
|
+
description: 'Simulate missing binary to verify sovereign fallback cascade.'
|
|
130
|
+
},
|
|
131
|
+
actions: {
|
|
132
|
+
type: 'array',
|
|
133
|
+
description: 'List of 3D AGI engineering actions to execute in order',
|
|
134
|
+
items: {
|
|
135
|
+
type: 'object',
|
|
136
|
+
properties: {
|
|
137
|
+
type: {
|
|
138
|
+
type: 'string',
|
|
139
|
+
enum: [
|
|
140
|
+
'getSceneGraph',
|
|
141
|
+
'getObjectInfo',
|
|
142
|
+
'getActiveSelection',
|
|
143
|
+
'modifyObject',
|
|
144
|
+
'autoFrameCamera',
|
|
145
|
+
'setupStudioLighting',
|
|
146
|
+
'smartMaterial',
|
|
147
|
+
'smartGeometry',
|
|
148
|
+
'cinematicAtmosphere',
|
|
149
|
+
'smartGeoNodes',
|
|
150
|
+
'exportAsset',
|
|
151
|
+
'executePython',
|
|
152
|
+
'captureViewport',
|
|
153
|
+
'renderFrame',
|
|
154
|
+
'renderAnimation',
|
|
155
|
+
'clearScene',
|
|
156
|
+
'runHeadless'
|
|
157
|
+
]
|
|
158
|
+
},
|
|
159
|
+
code: { type: 'string', description: 'Python (bpy) code snippet to execute in Blender' },
|
|
160
|
+
filepath: { type: 'string', description: 'Target file path for viewport capture, render still, or MP4 video animation (e.g. /Users/example/Desktop/render.png or ./output.png). NOTE: Do NOT use /tmp if you plan to view/read the file with fs_read or view_file due to workspace boundary restrictions.' },
|
|
161
|
+
animation: { type: 'boolean', description: 'Whether to render full video animation sequence across frame_start..frame_end' },
|
|
162
|
+
frame_start: { type: 'number', description: 'Start frame number for video animation render (default 1)' },
|
|
163
|
+
frame_end: { type: 'number', description: 'End frame number for video animation render (default 60)' },
|
|
164
|
+
engine: { type: 'string', description: 'Render engine to use: e.g. "BLENDER_EEVEE_NEXT", "CYCLES"' },
|
|
165
|
+
scriptPath: { type: 'string', description: 'Path to external .py script for complex procedural generation' },
|
|
166
|
+
name: { type: 'string', description: 'Target object name for getObjectInfo, modifyObject, smartMaterial, or smartGeometry' },
|
|
167
|
+
preset: { type: 'string', description: 'Smart PBR Material preset: e.g. "gold_brushed", "sandstone", "velvet", "chrome", "sapphire", "custom"' },
|
|
168
|
+
generator: { type: 'string', description: 'Geometry Nodes generator type: e.g. "masonry_brick_wall", "procedural_staircase", "sci_fi_panels", "scatter_debris"' },
|
|
169
|
+
subdivisions: { type: 'number', description: 'Subdivision level for Geometry Nodes masonry or procedural generators (default 3)' },
|
|
170
|
+
sun_elevation: { type: 'number', description: 'Nishita sky sun elevation angle in degrees for cinematicAtmosphere (default 12.0 for golden hour)' },
|
|
171
|
+
volumetric_fog: { type: 'boolean', description: 'Whether to include volumetric fog in cinematicAtmosphere (default true)' },
|
|
172
|
+
fog_density: { type: 'number', description: 'Density of volumetric fog scatter (default 0.015)' },
|
|
173
|
+
bevel_width: { type: 'number', description: 'Bevel width in Blender units for smartGeometry (default 0.015)' },
|
|
174
|
+
subsurf: { type: 'boolean', description: 'Whether to apply Subdivision Surface modifier in smartGeometry (default true)' },
|
|
175
|
+
focal_length: { type: 'number', description: 'Camera lens focal length in mm for autoFrameCamera (default 65.0mm studio telephoto)' },
|
|
176
|
+
location: { type: 'array', items: { type: 'number' }, description: 'Target [X, Y, Z] coordinates for modifyObject' },
|
|
177
|
+
rotation_euler: { type: 'array', items: { type: 'number' }, description: 'Target [X, Y, Z] rotation in radians for modifyObject' },
|
|
178
|
+
scale: { type: 'array', items: { type: 'number' }, description: 'Target [X, Y, Z] scale factors for modifyObject' },
|
|
179
|
+
visible: { type: 'boolean', description: 'Set object viewport and render visibility' },
|
|
180
|
+
key_energy: { type: 'number', description: 'Key light energy in watts/lumens (default 800)' },
|
|
181
|
+
fill_energy: { type: 'number', description: 'Fill light energy (default 300)' },
|
|
182
|
+
rim_energy: { type: 'number', description: 'Rim light energy (default 500)' },
|
|
183
|
+
directory: { type: 'string', description: 'Directory destination for 3D asset export' },
|
|
184
|
+
filename: { type: 'string', description: 'File name prefix without extension for 3D asset export' },
|
|
185
|
+
formats: { type: 'array', items: { type: 'string' }, description: 'Formats to export: e.g. ["blend", "glb", "obj", "fbx"]' },
|
|
186
|
+
keep_lights_camera: { type: 'boolean', description: 'Whether to preserve lights and camera when clearing scene (default true)' },
|
|
187
|
+
timeoutMs: { type: 'number', description: 'Execution timeout in ms (default 60000ms)' }
|
|
188
|
+
},
|
|
189
|
+
required: ['type']
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
required: ['actions']
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
async execute(args) {
|
|
197
|
+
const simulateMissing = args.simulateMissing || false;
|
|
198
|
+
const blenderBin = getBlenderExecutablePath();
|
|
199
|
+
if (simulateMissing || !fs.existsSync(blenderBin)) {
|
|
200
|
+
return {
|
|
201
|
+
success: false,
|
|
202
|
+
status: 'fallback_required',
|
|
203
|
+
engine: 'blender',
|
|
204
|
+
error: '[ENGINE MISSING or SIMULATED] Blender binary not found.',
|
|
205
|
+
fallback_guidance: {
|
|
206
|
+
auto_install_option: 'Call manage_local_dependencies (action="check_and_setupEng", engine="blender") - requires explicit userConsent: true because 312 MB > 200 MB consent gate.',
|
|
207
|
+
instant_execution_option: 'Or immediately invoke game.asset_fetch (action: "search_cc0", category: "3d_models") or generate_media_via_api / Three.js WebGL to fulfill user 3D/visual request without Blender.'
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let results = [];
|
|
213
|
+
|
|
214
|
+
for (const action of args.actions) {
|
|
215
|
+
if (action.type === 'getSceneGraph') {
|
|
216
|
+
{
|
|
217
|
+
results.push('[Layer 3 Fallback] Bridge not active; cannot query live viewport hierarchy.');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
else if (action.type === 'getObjectInfo') {
|
|
221
|
+
{
|
|
222
|
+
results.push('[Layer 3 Fallback] Bridge not active; cannot inspect object properties.');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
else if (action.type === 'getActiveSelection') {
|
|
226
|
+
{
|
|
227
|
+
results.push('[Layer 3 Fallback] Bridge not active; cannot query selection.');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else if (action.type === 'modifyObject') {
|
|
231
|
+
{
|
|
232
|
+
results.push('[Layer 3 Fallback] Bridge not active; cannot modify live object directly.');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
else if (action.type === 'autoFrameCamera') {
|
|
236
|
+
{
|
|
237
|
+
const camCode = `import bpy, math\ncam = bpy.context.scene.camera\nif not cam:\n cd = bpy.data.cameras.new('Cam')\n cam = bpy.data.objects.new('Cam', cd)\n bpy.context.scene.collection.objects.link(cam)\n bpy.context.scene.camera = cam\ncam.location = (10, -10, 8)\ncam.rotation_euler = (math.radians(50), 0, math.radians(45))\nif hasattr(cam.data, "lens"): cam.data.lens = ${action.focal_length || 65.0}\nprint('Headless 65mm camera framed at studio angle')`;
|
|
238
|
+
await runHeadlessCli(null, camCode, results);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
else if (action.type === 'smartGeometry') {
|
|
242
|
+
{
|
|
243
|
+
const geomCode = `import bpy, math\ntargets = [bpy.data.objects.get(${JSON.stringify(action.name || "")})] if ${JSON.stringify(action.name || "")} else [o for o in bpy.context.scene.objects if o.type == 'MESH' and o.visible_get() and not any(k in o.name.lower() for k in ['floor', 'plane', 'backdrop', 'studio', 'ground', 'cove'])]\ntargets = [t for t in targets if t]\nfor o in targets:\n if hasattr(o.data, "use_auto_smooth"): o.data.use_auto_smooth = True\n for p in o.data.polygons: p.use_smooth = True\n if not any(m.type == 'BEVEL' for m in o.modifiers):\n bev = o.modifiers.new(name="StudioBevel", type='BEVEL'); bev.width = ${action.bevel_width || 0.015}; bev.segments = 3; bev.limit_method = 'ANGLE'; bev.angle_limit = math.radians(35)\n if ${action.subsurf !== false} and not any(m.type == 'SUBSURF' for m in o.modifiers):\n sub = o.modifiers.new(name="StudioSubsurf", type='SUBSURF'); sub.levels = 2; sub.render_levels = 2\nprint("Headless applied Studio Bevel and Subsurf")`;
|
|
244
|
+
await runHeadlessCli(null, geomCode, results);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
else if (action.type === 'smartMaterial') {
|
|
248
|
+
{
|
|
249
|
+
const matCode = `import bpy, time\npreset = ${JSON.stringify(action.preset || "custom")}.lower()\ntargets = [bpy.data.objects.get(${JSON.stringify(action.name || "")})] if ${JSON.stringify(action.name || "")} else [o for o in bpy.context.scene.objects if o.type == 'MESH' and o.visible_get() and not any(k in o.name.lower() for k in ['floor', 'plane', 'backdrop', 'studio', 'ground', 'cove'])]\ntargets = [t for t in targets if t]\nmat = bpy.data.materials.new(name=f"CausynePBR_{preset.capitalize()}_{int(time.time()*1000)%10000}"); mat.use_nodes = True; nodes = mat.node_tree.nodes; links = mat.node_tree.links; nodes.clear()\nbsdf = nodes.new(type="ShaderNodeBsdfPrincipled"); bsdf.location = (0, 0); out = nodes.new(type="ShaderNodeOutputMaterial"); out.location = (300, 0); links.new(bsdf.outputs["BSDF"], out.inputs["Surface"])\nif preset in ["gold", "gold_brushed", "aztec_gold"]:\n bsdf.inputs["Base Color"].default_value = (1.0, 0.766, 0.336, 1.0); bsdf.inputs["Metallic"].default_value = 1.0; bsdf.inputs["Roughness"].default_value = 0.18\nelif preset in ["sandstone", "ancient_stone", "pyramid_stone"]:\n bsdf.inputs["Base Color"].default_value = (0.68, 0.55, 0.40, 1.0); bsdf.inputs["Roughness"].default_value = 0.85\nelif preset in ["velvet", "cushion_velvet", "watch_cushion"]:\n bsdf.inputs["Base Color"].default_value = (0.05, 0.06, 0.08, 1.0); bsdf.inputs["Roughness"].default_value = 0.9\nelif preset in ["chrome", "steel", "watch_case"]:\n bsdf.inputs["Base Color"].default_value = (0.88, 0.89, 0.90, 1.0); bsdf.inputs["Metallic"].default_value = 1.0; bsdf.inputs["Roughness"].default_value = 0.08\nfor o in targets:\n if len(o.data.materials) == 0: o.data.materials.append(mat)\n else: o.data.materials[0] = mat\nprint(f"Headless applied Smart PBR Material ({preset})")`;
|
|
250
|
+
await runHeadlessCli(null, matCode, results);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
else if (action.type === 'cinematicAtmosphere') {
|
|
254
|
+
{
|
|
255
|
+
const atmoCode = `import bpy, math\nw = bpy.context.scene.world or bpy.data.worlds.new("CausyneWorld"); bpy.context.scene.world = w; w.use_nodes = True; nodes = w.node_tree.nodes; links = w.node_tree.links; nodes.clear()\nbg = nodes.new("ShaderNodeBackground"); out = nodes.new("ShaderNodeOutputWorld"); sky = nodes.new("ShaderNodeTexSky"); sky.sky_type = 'MULTIPLE_SCATTERING' if hasattr(sky, 'sky_type') else 'HOSEK_WILKIE'; if hasattr(sky, 'sun_elevation'): sky.sun_elevation = math.radians(${action.sun_elevation || 12.0}); sky.sun_rotation = math.radians(-45.0); links.new(sky.outputs["Color"], bg.inputs["Color"]); links.new(bg.outputs["Background"], out.inputs["Surface"])\nif ${action.volumetric_fog !== false}:\n fm = bpy.data.materials.new("CausyneFog"); fm.use_nodes = True; fn = fm.node_tree.nodes; fl = fm.node_tree.links; fn.clear(); vol = fn.new("ShaderNodeVolumeScatter"); vol.inputs["Density"].default_value = ${action.fog_density || 0.015}; fo = fn.new("ShaderNodeOutputMaterial"); fl.new(vol.outputs["Volume"], fo.inputs["Volume"])\n bpy.ops.mesh.primitive_cube_add(size=40.0, location=(0,0,10)); fb = bpy.context.active_object; fb.name = "CausyneAtmosphereVolume"; fb.display_type = 'WIRE'; fb.data.materials.append(fm)\nprint("Headless applied Cinematic Atmosphere (Nishita Sky + Volumetric Fog)")`;
|
|
256
|
+
await runHeadlessCli(null, atmoCode, results);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
else if (action.type === 'smartGeoNodes') {
|
|
260
|
+
{
|
|
261
|
+
const gnCode = `import bpy\ngen = ${JSON.stringify(action.generator || "masonry_brick_wall")}\ntargets = [o for o in bpy.context.scene.objects if o.type == 'MESH' and "Volume" not in o.name and "Floor" not in o.name]\nfor o in targets:\n mod = o.modifiers.new(name=f"GeoNode_{gen}", type='NODES')\n ng = bpy.data.node_groups.get(f"GN_{gen}")\n if not ng:\n ng = bpy.data.node_groups.new(f"GN_{gen}", 'GeometryNodeTree')\n ng.interface.new_socket("Geometry", in_out='INPUT', socket_type='NodeSocketGeometry')\n ng.interface.new_socket("Geometry", in_out='OUTPUT', socket_type='NodeSocketGeometry')\n gi = ng.nodes.new('NodeGroupInput'); gi.location = (-400,0); go = ng.nodes.new('NodeGroupOutput'); go.location = (400,0)\n if gen == "masonry_brick_wall":\n s = ng.nodes.new('GeometryNodeSubdivideMesh'); s.location = (-150,0); s.inputs["Level"].default_value = ${action.subdivisions || 3}\n ng.links.new(gi.outputs["Geometry"], s.inputs["Mesh"]); ng.links.new(s.outputs["Mesh"], go.inputs["Geometry"])\n else: ng.links.new(gi.outputs["Geometry"], go.inputs["Geometry"])\n mod.node_group = ng\nprint(f"Headless attached Geometry Nodes ({gen})")`;
|
|
262
|
+
await runHeadlessCli(null, gnCode, results);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
else if (action.type === 'setupStudioLighting') {
|
|
266
|
+
{
|
|
267
|
+
const lightCode = `import bpy, math\nfor o in list(bpy.context.scene.objects):\n if o.type == 'LIGHT': bpy.data.objects.remove(o, do_unlink=True)\nkd = bpy.data.lights.new('Key', 'AREA'); kd.energy = ${action.key_energy || 800}; kd.size = 5.0; ko = bpy.data.objects.new('Key', kd); ko.location = (6, -6, 8); ko.rotation_euler = (math.radians(45), 0, math.radians(45)); bpy.context.scene.collection.objects.link(ko)\nprint('Headless studio lighting built')`;
|
|
268
|
+
await runHeadlessCli(null, lightCode, results);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
else if (action.type === 'exportAsset') {
|
|
272
|
+
const destDir = action.directory || path.join(getWorkspaceTempDir(), 'causyne_3d_exports');
|
|
273
|
+
const prefix = action.filename || `causyne_asset_${Date.now()}`;
|
|
274
|
+
const fmts = action.formats || ['blend', 'glb'];
|
|
275
|
+
|
|
276
|
+
{
|
|
277
|
+
const exportCode = `import bpy, os\nd = "${destDir}"\nos.makedirs(d, exist_ok=True)\nif "blend" in ${JSON.stringify(fmts)}:\n bpy.ops.wm.save_as_mainfile(filepath=os.path.join(d, "${prefix}.blend"))\nif "glb" in ${JSON.stringify(fmts)}:\n bpy.ops.export_scene.gltf(filepath=os.path.join(d, "${prefix}.glb"), export_format="GLB")\nif "obj" in ${JSON.stringify(fmts)}:\n if hasattr(bpy.ops.wm, "obj_export"): bpy.ops.wm.obj_export(filepath=os.path.join(d, "${prefix}.obj"))\n else: bpy.ops.export_scene.obj(filepath=os.path.join(d, "${prefix}.obj"))\nprint(f"Headless exported formats to {d}")`;
|
|
278
|
+
await runHeadlessCli(null, exportCode, results);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
else if (action.type === 'clearScene') {
|
|
282
|
+
const keep = action.keep_lights_camera !== false;
|
|
283
|
+
{
|
|
284
|
+
const clearCode = `import bpy\nfor o in list(bpy.context.scene.objects):\n if ${keep} and o.type in ['CAMERA', 'LIGHT']: continue\n bpy.data.objects.remove(o, do_unlink=True)\nprint("Headless cleared scene meshes")`;
|
|
285
|
+
await runHeadlessCli(null, clearCode, results);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
else if (action.type === 'executePython') {
|
|
289
|
+
let pyCode = action.code || '';
|
|
290
|
+
if (action.scriptPath && fs.existsSync(action.scriptPath)) {
|
|
291
|
+
pyCode = fs.readFileSync(action.scriptPath, 'utf8');
|
|
292
|
+
}
|
|
293
|
+
if (!pyCode) {
|
|
294
|
+
results.push(`[Error] No code or valid scriptPath provided for executePython.`);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
{
|
|
298
|
+
results.push('[Layer 3 Fallback] Live bridge not active. Executing via standalone Blender background CLI...');
|
|
299
|
+
try {
|
|
300
|
+
await runHeadlessCli(action.scriptPath, pyCode, results);
|
|
301
|
+
} catch (cliErr) {
|
|
302
|
+
results.push(`[Layer 3 Error] ${cliErr.message}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
else if (action.type === 'captureViewport') {
|
|
307
|
+
const targetPath = action.filepath || path.join(getWorkspaceTempDir(), `causyne_3d_preview_${Date.now()}.png`);
|
|
308
|
+
{
|
|
309
|
+
const captureCode = `import bpy, os\nos.makedirs(os.path.dirname("${targetPath}"), exist_ok=True)\nbpy.ops.render.opengl(write_still=True, filepath="${targetPath}")\nprint("Captured headless viewport to ${targetPath}")`;
|
|
310
|
+
try {
|
|
311
|
+
await runHeadlessCli(null, captureCode, results);
|
|
312
|
+
} catch (cliErr) {
|
|
313
|
+
results.push(`[Layer 3 Capture Error] ${cliErr.message}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
else if (action.type === 'renderFrame' || action.type === 'renderAnimation') {
|
|
318
|
+
const targetPath = action.filepath || path.join(getWorkspaceTempDir(), `causyne_3d_render_${Date.now()}.${action.type === 'renderAnimation' ? 'mp4' : 'png'}`);
|
|
319
|
+
const isAnimation = action.animation || action.type === 'renderAnimation' || targetPath.toLowerCase().endsWith('.mp4') || targetPath.toLowerCase().endsWith('.mov') || targetPath.toLowerCase().endsWith('.mkv');
|
|
320
|
+
const startFrame = action.frame_start || 1;
|
|
321
|
+
const endFrame = action.frame_end || 60;
|
|
322
|
+
|
|
323
|
+
{
|
|
324
|
+
const renderCode = isAnimation ?
|
|
325
|
+
`import bpy, os, time\nos.makedirs(os.path.dirname("${targetPath}"), exist_ok=True)\nbpy.context.scene.frame_start = ${startFrame}\nbpy.context.scene.frame_end = ${endFrame}\nif "${action.engine || ''}": bpy.context.scene.render.engine = "${action.engine || ''}"\ntry:\n bpy.context.scene.render.filepath = "${targetPath}"\n bpy.context.scene.render.image_settings.file_format = 'FFMPEG'\n if hasattr(bpy.context.scene.render, 'ffmpeg'):\n bpy.context.scene.render.ffmpeg.format = 'MPEG4' if "${targetPath}".lower().endswith('.mp4') else 'MKV'\n bpy.context.scene.render.ffmpeg.codec = 'H264'\n bpy.ops.render.render(animation=True)\n print(f"Rendered MP4 animation directly to ${targetPath}")\nexcept Exception:\n import subprocess, shutil as sh_util\n td = os.path.join(os.path.dirname("${targetPath}"), ".causyne_temp_frames_" + str(int(time.time())))\n os.makedirs(td, exist_ok=True)\n bpy.context.scene.render.image_settings.file_format = 'PNG'\n bpy.context.scene.render.filepath = os.path.join(td, "frame_")\n bpy.ops.render.render(animation=True)\n fps = getattr(bpy.context.scene.render, "fps", 30)\n fb = sh_util.which("ffmpeg")\n if not fb:\n for cand in ["/opt/homebrew/bin/ffmpeg", "/usr/local/bin/ffmpeg", "C:\\\\Program Files\\\\ffmpeg\\\\bin\\\\ffmpeg.exe"]:\n if os.path.exists(cand): fb = cand; break\n if not fb: fb = "ffmpeg"\n subprocess.run([fb, "-y", "-framerate", str(fps), "-i", os.path.join(td, "frame_%04d.png"), "-c:v", "libx264", "-pix_fmt", "yuv420p", "${targetPath}"], check=True)\n sh_util.rmtree(td, ignore_errors=True)\n print(f"Rendered MP4 animation via sequence to ${targetPath}")` :
|
|
326
|
+
`import bpy, os\nos.makedirs(os.path.dirname("${targetPath}"), exist_ok=True)\nbpy.context.scene.render.filepath = "${targetPath}"\nif "${action.engine || ''}": bpy.context.scene.render.engine = "${action.engine || ''}"\nbpy.ops.render.render(write_still=True)\nprint(f"Rendered still frame to ${targetPath}")`;
|
|
327
|
+
try {
|
|
328
|
+
await runHeadlessCli(null, renderCode, results);
|
|
329
|
+
} catch (cliErr) {
|
|
330
|
+
results.push(`[Layer 3 Render Error] ${cliErr.message}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
else if (action.type === 'runHeadless') {
|
|
335
|
+
let pyCode = action.code || '';
|
|
336
|
+
if (action.scriptPath && fs.existsSync(action.scriptPath)) {
|
|
337
|
+
pyCode = fs.readFileSync(action.scriptPath, 'utf8');
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
await runHeadlessCli(action.scriptPath, pyCode, results);
|
|
341
|
+
} catch (cliErr) {
|
|
342
|
+
results.push(`[Layer 3 Error] ${cliErr.message}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return { success: true, log: results };
|
|
348
|
+
}
|
|
349
|
+
};
|