@causyne/agent 0.0.0-stage → 1.10.0

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 (274) hide show
  1. package/LICENSE +102 -0
  2. package/README.md +31 -2
  3. package/SECURITY.md +24 -0
  4. package/bin/causyne.js +13 -0
  5. package/docs/ARCHITECTURE.md +63 -0
  6. package/docs/PROVENANCE.md +29 -0
  7. package/docs/SECURITY_REVIEW.md +89 -0
  8. package/docs/THIRD_PARTY_NOTICES.md +49 -0
  9. package/native-skills/ai-engineering/context-window-management/SKILL.md +43 -0
  10. package/native-skills/ai-engineering/llm-evaluation/SKILL.md +46 -0
  11. package/native-skills/ai-engineering/model-cost-and-latency/SKILL.md +46 -0
  12. package/native-skills/ai-engineering/prompt-construction/SKILL.md +42 -0
  13. package/native-skills/ai-engineering/retrieval-augmented-generation/SKILL.md +42 -0
  14. package/native-skills/ai-engineering/structured-output-and-schemas/SKILL.md +45 -0
  15. package/native-skills/ai-engineering/tool-calling-agents/SKILL.md +43 -0
  16. package/native-skills/android/app-performance-profiling/SKILL.md +42 -0
  17. package/native-skills/android/background-work-scheduling/SKILL.md +47 -0
  18. package/native-skills/android/compose-state-and-recomposition/SKILL.md +43 -0
  19. package/native-skills/android/gradle-build-configuration/SKILL.md +41 -0
  20. package/native-skills/android/lifecycle-and-leaks/SKILL.md +46 -0
  21. package/native-skills/android/play-store-release/SKILL.md +43 -0
  22. package/native-skills/android/room-and-persistence/SKILL.md +40 -0
  23. package/native-skills/backend/authentication-and-sessions/SKILL.md +45 -0
  24. package/native-skills/backend/background-jobs-and-queues/SKILL.md +42 -0
  25. package/native-skills/backend/caching-strategy/SKILL.md +44 -0
  26. package/native-skills/backend/graceful-shutdown/SKILL.md +42 -0
  27. package/native-skills/backend/rate-limiting-and-quotas/SKILL.md +40 -0
  28. package/native-skills/backend/rest-api-design/SKILL.md +40 -0
  29. package/native-skills/backend/webhook-delivery-and-receipt/SKILL.md +43 -0
  30. package/native-skills/cloud/cost-optimisation/SKILL.md +46 -0
  31. package/native-skills/cloud/iam-least-privilege/SKILL.md +43 -0
  32. package/native-skills/cloud/infrastructure-as-code/SKILL.md +43 -0
  33. package/native-skills/cloud/multi-region-and-failover/SKILL.md +43 -0
  34. package/native-skills/cloud/serverless-patterns/SKILL.md +47 -0
  35. package/native-skills/data/data-quality-monitoring/SKILL.md +51 -0
  36. package/native-skills/data/etl-pipeline-reliability/SKILL.md +43 -0
  37. package/native-skills/data/exploratory-analysis/SKILL.md +43 -0
  38. package/native-skills/data/metric-definition/SKILL.md +42 -0
  39. package/native-skills/data/notebook-to-production/SKILL.md +43 -0
  40. package/native-skills/database/backup-and-recovery/SKILL.md +43 -0
  41. package/native-skills/database/connection-pooling/SKILL.md +42 -0
  42. package/native-skills/database/data-integrity-and-constraints/SKILL.md +42 -0
  43. package/native-skills/database/query-optimisation/SKILL.md +38 -0
  44. package/native-skills/database/safe-migrations/SKILL.md +47 -0
  45. package/native-skills/database/schema-design/SKILL.md +38 -0
  46. package/native-skills/database/transactions-and-isolation/SKILL.md +41 -0
  47. package/native-skills/design/colour-system-construction/SKILL.md +38 -0
  48. package/native-skills/design/design-direction-brief/SKILL.md +41 -0
  49. package/native-skills/design/motion-and-transitions/SKILL.md +38 -0
  50. package/native-skills/design/responsive-strategy/SKILL.md +39 -0
  51. package/native-skills/design/spacing-and-rhythm/SKILL.md +38 -0
  52. package/native-skills/design/type-scale-and-hierarchy/SKILL.md +39 -0
  53. package/native-skills/design/visual-qa-loop/SKILL.md +42 -0
  54. package/native-skills/devops/docker-multi-stage-builds/SKILL.md +60 -0
  55. package/native-skills/devops/github-actions-ci/SKILL.md +60 -0
  56. package/native-skills/devops/incident-triage/SKILL.md +38 -0
  57. package/native-skills/devops/kubernetes-deployment-hardening/SKILL.md +64 -0
  58. package/native-skills/devops/observability-baseline/SKILL.md +38 -0
  59. package/native-skills/devops/secret-management/SKILL.md +40 -0
  60. package/native-skills/devops/terraform-state-and-modules/SKILL.md +36 -0
  61. package/native-skills/devops/zero-downtime-deploys/SKILL.md +40 -0
  62. package/native-skills/git/bisect-regression-hunt/SKILL.md +43 -0
  63. package/native-skills/git/branch-strategy/SKILL.md +39 -0
  64. package/native-skills/git/commit-hygiene/SKILL.md +41 -0
  65. package/native-skills/git/history-recovery/SKILL.md +44 -0
  66. package/native-skills/git/large-repository-surgery/SKILL.md +46 -0
  67. package/native-skills/git/merge-conflict-resolution/SKILL.md +42 -0
  68. package/native-skills/media/cinematic-3d-shot/SKILL.md +104 -0
  69. package/native-skills/mobile/app-startup-optimisation/SKILL.md +41 -0
  70. package/native-skills/mobile/cross-platform-tradeoffs/SKILL.md +42 -0
  71. package/native-skills/mobile/deep-linking/SKILL.md +39 -0
  72. package/native-skills/mobile/mobile-storage-security/SKILL.md +45 -0
  73. package/native-skills/mobile/offline-first-sync/SKILL.md +42 -0
  74. package/native-skills/mobile/push-notifications/SKILL.md +38 -0
  75. package/native-skills/performance/algorithmic-complexity/SKILL.md +43 -0
  76. package/native-skills/performance/concurrency-and-parallelism/SKILL.md +42 -0
  77. package/native-skills/performance/load-testing/SKILL.md +42 -0
  78. package/native-skills/performance/memory-and-leak-hunting/SKILL.md +41 -0
  79. package/native-skills/performance/network-and-payload-efficiency/SKILL.md +42 -0
  80. package/native-skills/performance/profiling-methodology/SKILL.md +46 -0
  81. package/native-skills/python/async-correctness/SKILL.md +40 -0
  82. package/native-skills/python/cli-tool-construction/SKILL.md +44 -0
  83. package/native-skills/python/data-processing-pipelines/SKILL.md +38 -0
  84. package/native-skills/python/environment-isolation/SKILL.md +41 -0
  85. package/native-skills/python/packaging-and-distribution/SKILL.md +56 -0
  86. package/native-skills/python/pytest-suite-design/SKILL.md +39 -0
  87. package/native-skills/python/type-hints-and-static-checking/SKILL.md +42 -0
  88. package/native-skills/security/authorization-model/SKILL.md +42 -0
  89. package/native-skills/security/crypto-usage/SKILL.md +48 -0
  90. package/native-skills/security/dependency-supply-chain/SKILL.md +42 -0
  91. package/native-skills/security/input-validation-and-injection/SKILL.md +44 -0
  92. package/native-skills/security/secure-file-uploads/SKILL.md +46 -0
  93. package/native-skills/security/security-headers-and-csp/SKILL.md +44 -0
  94. package/native-skills/security/threat-modelling/SKILL.md +46 -0
  95. package/native-skills/testing/end-to-end-test-stability/SKILL.md +42 -0
  96. package/native-skills/testing/property-based-testing/SKILL.md +46 -0
  97. package/native-skills/testing/regression-test-from-a-bug/SKILL.md +41 -0
  98. package/native-skills/testing/snapshot-and-visual-regression/SKILL.md +42 -0
  99. package/native-skills/testing/test-doubles-and-mocking/SKILL.md +43 -0
  100. package/native-skills/testing/test-strategy-and-pyramid/SKILL.md +39 -0
  101. package/native-skills/testing/writing-good-assertions/SKILL.md +42 -0
  102. package/native-skills/web/accessible-components/SKILL.md +45 -0
  103. package/native-skills/web/api-client-resilience/SKILL.md +39 -0
  104. package/native-skills/web/bundle-size-reduction/SKILL.md +38 -0
  105. package/native-skills/web/core-web-vitals/SKILL.md +40 -0
  106. package/native-skills/web/css-layout-systems/SKILL.md +41 -0
  107. package/native-skills/web/form-validation-ux/SKILL.md +39 -0
  108. package/native-skills/web/react-state-architecture/SKILL.md +41 -0
  109. package/native-skills/web/ssr-hydration-correctness/SKILL.md +45 -0
  110. package/package.json +68 -4
  111. package/src/agentLoop.js +2458 -0
  112. package/src/appChannel.js +109 -0
  113. package/src/approvalGrants.js +66 -0
  114. package/src/auditLog.js +52 -0
  115. package/src/autoUpdate.js +12 -0
  116. package/src/capabilities.js +36 -0
  117. package/src/checkpoints.js +1199 -0
  118. package/src/cliMode.js +638 -0
  119. package/src/commands.js +805 -0
  120. package/src/compiledSkills.js +540 -0
  121. package/src/config.js +130 -0
  122. package/src/contextManager.js +498 -0
  123. package/src/cortex/autoCondenser.js +192 -0
  124. package/src/cortex/provisioner.js +27 -0
  125. package/src/cortex/vectorDb.js +447 -0
  126. package/src/designPalettes.js +99 -0
  127. package/src/devMode.js +6 -0
  128. package/src/embodimentSession.js +180 -0
  129. package/src/entrypoint.js +194 -0
  130. package/src/execSafe.js +21 -0
  131. package/src/gitSnapshot.js +378 -0
  132. package/src/mcp/jsonRpc.js +165 -0
  133. package/src/mcp/mcpClient.js +136 -0
  134. package/src/mcp/mcpRegistry.js +207 -0
  135. package/src/memory.js +62 -0
  136. package/src/memoryManager.js +124 -0
  137. package/src/modelCatalogue.js +132 -0
  138. package/src/modelIds.js +42 -0
  139. package/src/modelRouter.js +145 -0
  140. package/src/outputStore.js +246 -0
  141. package/src/perception.js +124 -0
  142. package/src/planCache.js +437 -0
  143. package/src/procSignal.js +32 -0
  144. package/src/providers/anthropic.js +408 -0
  145. package/src/providers/base.js +328 -0
  146. package/src/providers/dashscope-byok.js +86 -0
  147. package/src/providers/deepseek.js +80 -0
  148. package/src/providers/factory.js +37 -0
  149. package/src/providers/gemini.js +346 -0
  150. package/src/providers/moonshot.js +73 -0
  151. package/src/providers/muse.js +169 -0
  152. package/src/providers/ollama.js +104 -0
  153. package/src/providers/openai-compatible.js +84 -0
  154. package/src/providers/openai.js +20 -0
  155. package/src/providers/qwen-byok.js +76 -0
  156. package/src/providers/xai.js +73 -0
  157. package/src/provisioning/capabilityConsent.js +24 -0
  158. package/src/provisioning/capabilityProvisioner.js +271 -0
  159. package/src/provisioning/capabilityTools.js +29 -0
  160. package/src/reporting.js +373 -0
  161. package/src/sandbox.js +529 -0
  162. package/src/sessionStore.js +279 -0
  163. package/src/skillsManager.js +288 -0
  164. package/src/slopDetector.js +307 -0
  165. package/src/taskBuffer.js +45 -0
  166. package/src/taskShape.js +241 -0
  167. package/src/textDiff.js +150 -0
  168. package/src/tokenTracker.js +560 -0
  169. package/src/tool-scripts/ax_snapshot.applescript +39 -0
  170. package/src/tool-scripts/ax_snapshot.ps1 +10 -0
  171. package/src/tool-scripts/desktop_act.applescript +58 -0
  172. package/src/tool-scripts/resolve_automator.py +119 -0
  173. package/src/toolPacks.js +524 -0
  174. package/src/tools/archive.js +63 -0
  175. package/src/tools/astryxOracle.js +78 -0
  176. package/src/tools/axSnapshot.js +165 -0
  177. package/src/tools/blenderAutomator.js +349 -0
  178. package/src/tools/browserAutomator.js +252 -0
  179. package/src/tools/checkSystemSpecs.js +64 -0
  180. package/src/tools/clipboard.js +36 -0
  181. package/src/tools/codeSearchAst.js +101 -0
  182. package/src/tools/codeSearcher.js +230 -0
  183. package/src/tools/codeTools.js +262 -0
  184. package/src/tools/codebaseSearch.js +75 -0
  185. package/src/tools/createSkill.js +129 -0
  186. package/src/tools/deferredMcpSearch.js +246 -0
  187. package/src/tools/desktopAct.js +208 -0
  188. package/src/tools/desktopOpen.js +34 -0
  189. package/src/tools/dynamicLoader.js +44 -0
  190. package/src/tools/executeProceduralMedia.js +93 -0
  191. package/src/tools/fsDelete.js +51 -0
  192. package/src/tools/fsEdit.js +105 -0
  193. package/src/tools/fsFind.js +128 -0
  194. package/src/tools/fsList.js +77 -0
  195. package/src/tools/fsMove.js +57 -0
  196. package/src/tools/fsRead.js +87 -0
  197. package/src/tools/fsSearch.js +198 -0
  198. package/src/tools/fsWrite.js +41 -0
  199. package/src/tools/gameAiBehaviorTree.js +208 -0
  200. package/src/tools/gameAnimationRig.js +194 -0
  201. package/src/tools/gameAssetFetcher.js +372 -0
  202. package/src/tools/gameAssetHarvester.js +210 -0
  203. package/src/tools/gameAudioSynth.js +267 -0
  204. package/src/tools/gameBuilder.js +886 -0
  205. package/src/tools/gameCanvasAudit.js +101 -0
  206. package/src/tools/gameCinematicSequencer.js +151 -0
  207. package/src/tools/gameDataForge.js +124 -0
  208. package/src/tools/gameDialogueTreeGen.js +166 -0
  209. package/src/tools/gameEconomyBalancer.js +161 -0
  210. package/src/tools/gameEntitySpawner.js +207 -0
  211. package/src/tools/gameLoreForge.js +153 -0
  212. package/src/tools/gameQuestArcBuilder.js +152 -0
  213. package/src/tools/gameSceneBuilder.js +158 -0
  214. package/src/tools/gameShaderForge.js +181 -0
  215. package/src/tools/gameTilemapGenerator.js +164 -0
  216. package/src/tools/gameUiHudBuilder.js +292 -0
  217. package/src/tools/gameVfxEmitterGen.js +108 -0
  218. package/src/tools/gameWebRuntimeInspector.js +91 -0
  219. package/src/tools/generateMediaViaApi.js +358 -0
  220. package/src/tools/gitExec.js +54 -0
  221. package/src/tools/godotAutomator.js +437 -0
  222. package/src/tools/manageLocalAiDependencies.js +220 -0
  223. package/src/tools/memoryWrite.js +99 -0
  224. package/src/tools/multiEdit.js +107 -0
  225. package/src/tools/palmierAutomator.js +104 -0
  226. package/src/tools/pdfReader.js +112 -0
  227. package/src/tools/persistentShell.js +148 -0
  228. package/src/tools/printer.js +316 -0
  229. package/src/tools/procTerm.js +91 -0
  230. package/src/tools/processList.js +52 -0
  231. package/src/tools/readNativeSkill.js +39 -0
  232. package/src/tools/registry.js +277 -0
  233. package/src/tools/resolveAutomator.js +102 -0
  234. package/src/tools/screenshot.js +75 -0
  235. package/src/tools/subagentOrchestrator.js +305 -0
  236. package/src/tools/sysRun.js +125 -0
  237. package/src/tools/todoWrite.js +75 -0
  238. package/src/tools/toolDispatch.js +127 -0
  239. package/src/tools/toolForge.js +124 -0
  240. package/src/tools/updatePlan.js +88 -0
  241. package/src/tools/viewImage.js +82 -0
  242. package/src/tools/visionInspector.js +154 -0
  243. package/src/tools/webFetch.js +74 -0
  244. package/src/tools/webSearch.js +82 -0
  245. package/src/tui/ask.js +164 -0
  246. package/src/tui/banner.js +158 -0
  247. package/src/tui/block.js +152 -0
  248. package/src/tui/chatRenderer.js +953 -0
  249. package/src/tui/completion.js +424 -0
  250. package/src/tui/dialogs.js +606 -0
  251. package/src/tui/diff.js +296 -0
  252. package/src/tui/history.js +135 -0
  253. package/src/tui/home.js +217 -0
  254. package/src/tui/invocation.js +105 -0
  255. package/src/tui/keys.js +443 -0
  256. package/src/tui/lineEditor.js +256 -0
  257. package/src/tui/overlay.js +256 -0
  258. package/src/tui/prompt.js +1258 -0
  259. package/src/tui/selectList.js +335 -0
  260. package/src/tui/stage.js +330 -0
  261. package/src/tui/stageSurface.js +609 -0
  262. package/src/tui/statusLine.js +254 -0
  263. package/src/tui/text.js +345 -0
  264. package/src/tui/theme.js +393 -0
  265. package/src/tui/toolDisplay.js +176 -0
  266. package/src/tui/transcript.js +235 -0
  267. package/src/tui/viewport.js +337 -0
  268. package/src/uncertainty.js +271 -0
  269. package/src/userSkills.js +265 -0
  270. package/src/utils/cli_ui.js +0 -0
  271. package/src/utils/inquirer.js +12 -0
  272. package/src/utils/paths.js +37 -0
  273. package/src/version.js +38 -0
  274. package/src/vfeTracker.js +220 -0
@@ -0,0 +1,246 @@
1
+ // registry.js imports THIS module to build its tool table, so a static import
2
+ // back the other way is a cycle: whichever side loads first sees the other as
3
+ // uninitialised and the whole registry throws. It happened to work only while
4
+ // registry.js was always imported first. Loaded on demand instead — execute()
5
+ // is async, so this costs nothing.
6
+ async function toolDefinitions() {
7
+ const { TOOL_DEFINITIONS } = await import('./registry.js');
8
+ return TOOL_DEFINITIONS;
9
+ }
10
+ import { PACKS, PACK_NAMES, packForTool, isCoreTool, isResidentTool } from '../toolPacks.js';
11
+
12
+ /** Max skills returned alongside the tool matches, so one search stays cheap. */
13
+ const MAX_SKILL_HITS = 5;
14
+
15
+ /**
16
+ * Score the skill catalogue with the same query the tools were scored with.
17
+ *
18
+ * Exists because there was no skill search at all: the system prompt carried a
19
+ * 40-entry catalogue and that was the ONLY way the model could learn a skill id.
20
+ * Replacing that catalogue with a domain header is only safe once this exists.
21
+ *
22
+ * @returns {string[]} formatted lines, empty when nothing matched
23
+ */
24
+ async function searchSkills(terms, pattern) {
25
+ // Imported lazily, not at module scope: registry.js imports THIS module, and
26
+ // skillsManager pulls registry back in transitively — a static import here
27
+ // closes the cycle and the whole tool registry fails to initialise. Same
28
+ // pattern registry.js already uses for capabilityTools.
29
+ let catalogue;
30
+ try {
31
+ const { listCompiledSkills } = await import('../skillsManager.js');
32
+ catalogue = listCompiledSkills();
33
+ } catch (e) {
34
+ return []; // a broken skill store must not take tool search down
35
+ }
36
+
37
+ const scored = [];
38
+ for (const skill of catalogue) {
39
+ const haystack = `${skill.id} ${skill.name} ${skill.domain} ${skill.description || ''}`.toLowerCase();
40
+ let score = 0;
41
+ if (pattern) {
42
+ if (pattern.test(skill.name) || pattern.test(skill.id)) score += 10;
43
+ if (pattern.test(skill.description || '')) score += 4;
44
+ }
45
+ for (const term of terms) {
46
+ if (skill.id.toLowerCase().includes(term) || skill.name.toLowerCase().includes(term)) score += 8;
47
+ else if (skill.domain.toLowerCase() === term) score += 6;
48
+ else if (haystack.includes(term)) score += 3;
49
+ }
50
+ if (score > 0) scored.push({ skill, score });
51
+ }
52
+
53
+ return scored
54
+ .sort((a, b) => b.score - a.score)
55
+ .slice(0, MAX_SKILL_HITS)
56
+ .map(({ skill }) => `- ${skill.domain}/${skill.id} — ${skill.name}: ${skill.description || ''}`);
57
+ }
58
+
59
+ // Real progressive-disclosure tool search.
60
+ //
61
+ // The previous version of this file advertised "80% token savings" while
62
+ // searching a hardcoded catalogue of seven tools that did not exist in the
63
+ // registry (mcp_postgres_data, mcp_github_workflow, mcp_android_cli). Any tool
64
+ // the model discovered that way led straight to a "Tool does not exist" error,
65
+ // and nothing was ever actually deferred.
66
+ //
67
+ // This version searches the live tool registry and unlocks what it finds. The
68
+ // agent loop reads the _unlock_* fields off the result and loads the matching
69
+ // schemas into the next request.
70
+
71
+ function scoreTool(definition, terms, pattern) {
72
+ const name = (definition.name || '').toLowerCase();
73
+ const description = (definition.description || '').toLowerCase();
74
+
75
+ // Parameter names and descriptions are searchable too — a query like
76
+ // "kill process on port" should find a tool whose `port` argument mentions it.
77
+ let parameterText = '';
78
+ const properties = definition.parameters?.properties || definition.input_schema?.properties;
79
+ if (properties) {
80
+ for (const [key, value] of Object.entries(properties)) {
81
+ parameterText += ` ${key} ${(value && value.description) || ''}`;
82
+ }
83
+ }
84
+ parameterText = parameterText.toLowerCase();
85
+
86
+ let score = 0;
87
+
88
+ if (pattern) {
89
+ if (pattern.test(name)) score += 12;
90
+ if (pattern.test(description)) score += 6;
91
+ if (pattern.test(parameterText)) score += 2;
92
+ }
93
+
94
+ for (const term of terms) {
95
+ if (name.includes(term)) score += 10;
96
+ if (description.includes(term)) score += 4;
97
+ if (parameterText.includes(term)) score += 1;
98
+ }
99
+
100
+ return score;
101
+ }
102
+
103
+ function buildPattern(query) {
104
+ // The query may be a natural-language phrase or a regular expression. Trying
105
+ // it as a regex first gives power users exact control; a malformed pattern
106
+ // simply falls back to keyword scoring instead of throwing.
107
+ try {
108
+ return new RegExp(query, 'i');
109
+ } catch (e) {
110
+ return null;
111
+ }
112
+ }
113
+
114
+ export const deferredMcpSearch = {
115
+ schema: {
116
+ name: 'tool.search',
117
+ description: 'Search your full native tool catalogue and load additional tool schemas on demand. Causyne keeps specialised capability packs (game development, media generation, browser and vision, system control, advanced orchestration) out of your context until you need them. Call this whenever a task needs a capability you cannot currently see — the matching tools become callable immediately on your next turn. Accepts natural language ("generate music", "godot scene", "kill process on port", "read a pdf") or a regular expression.',
118
+ parameters: {
119
+ type: 'object',
120
+ properties: {
121
+ query: {
122
+ type: 'string',
123
+ description: 'What capability you need. Natural language or a regex pattern matched against tool names, descriptions and argument descriptions.'
124
+ },
125
+ pack: {
126
+ type: 'string',
127
+ enum: [...PACK_NAMES, 'all'],
128
+ description: 'Optional. Load an entire capability pack at once instead of searching: game, web, media, system, or advanced.'
129
+ },
130
+ limit: {
131
+ type: 'integer',
132
+ description: 'Maximum number of tools to return. Defaults to 8.'
133
+ }
134
+ },
135
+ required: ['query']
136
+ }
137
+ },
138
+
139
+ async execute(args, context = {}) {
140
+ try {
141
+ const query = String(args.query || '').trim();
142
+ const requestedPack = args.pack ? String(args.pack).toLowerCase() : null;
143
+
144
+ if (!query && !requestedPack) {
145
+ return { success: false, error: 'Provide a "query" describing the capability you need, or a "pack" name to load.' };
146
+ }
147
+
148
+ // Whole-pack load. Cheaper than five searches when the agent already
149
+ // knows it is starting a game or a media pipeline.
150
+ if (requestedPack && requestedPack !== 'all' && PACKS[requestedPack]) {
151
+ const packTools = (await toolDefinitions()).filter(d => packForTool(d.name) === requestedPack);
152
+ return {
153
+ success: true,
154
+ output: `Loaded capability pack "${requestedPack}" (${packTools.length} tools). These are now callable:\n` +
155
+ packTools.map(d => `- ${d.name}: ${d.description || ''}`).join('\n'),
156
+ _unlock_packs: [requestedPack],
157
+ _unlock_tools: packTools.map(d => d.name)
158
+ };
159
+ }
160
+
161
+ const limit = Number.isInteger(args.limit) && args.limit > 0 ? Math.min(args.limit, 25) : 8;
162
+ const lower = query.toLowerCase();
163
+ const terms = lower.split(/[^a-z0-9_]+/).filter(t => t.length > 2);
164
+ const pattern = buildPattern(query);
165
+
166
+ const scored = (await toolDefinitions())
167
+ .map(definition => ({ definition, score: scoreTool(definition, terms, pattern) }))
168
+ .filter(entry => entry.score > 0)
169
+ .sort((a, b) => b.score - a.score)
170
+ .slice(0, limit);
171
+
172
+ if (scored.length === 0) {
173
+ // Still search skills. "how do I structure a django migration" matches
174
+ // no tool name and every relevant skill — returning "no tools matched"
175
+ // and stopping was the whole reason a 40-entry catalogue had to sit in
176
+ // the system prompt.
177
+ const skillOnly = await searchSkills(terms, pattern);
178
+ if (skillOnly.length > 0) {
179
+ return {
180
+ success: true,
181
+ output: `No tools matched "${query}", but these SKILLS did — read one with fs_read_skill:\n` +
182
+ skillOnly.join('\n')
183
+ };
184
+ }
185
+ return {
186
+ success: true,
187
+ output: `Nothing matched "${query}". Available capability packs: ${PACK_NAMES.join(', ')}. ` +
188
+ `Try a broader term, or set the "pack" argument to load a whole pack at once.`
189
+ };
190
+ }
191
+
192
+ const unlockTools = [];
193
+ const unlockPacks = new Set();
194
+ const lines = [];
195
+
196
+ for (const { definition } of scored) {
197
+ const pack = packForTool(definition.name);
198
+ // Unlock anything that is not already resident. The old condition was
199
+ // `!isCoreTool(...)`, which was correct while the entire core shipped
200
+ // in every request. It is not any more: on a provider without native
201
+ // schema deferral only RESIDENT_TOOLS ship, and a non-resident core
202
+ // tool found here would have been reported as "already loaded" and
203
+ // then stayed invisible forever.
204
+ if (!isResidentTool(definition.name)) {
205
+ unlockTools.push(definition.name);
206
+ if (pack) unlockPacks.add(pack);
207
+ }
208
+ const state = isResidentTool(definition.name)
209
+ ? 'already loaded'
210
+ : (isCoreTool(definition.name) ? 'core' : `pack: ${pack || 'custom'}`);
211
+ lines.push(`- ${definition.name} (${state}): ${definition.description || ''}`);
212
+ }
213
+
214
+ // Skills are searched alongside tools because `tool_search` is the only
215
+ // discovery surface the model always has. The compiled skill catalogue is
216
+ // 100 entries; listing it in the system prompt cost ~841 tokens on EVERY
217
+ // request of every task, including a bare "hi". Retrieval replaces the
218
+ // catalogue — but only because the retrieval exists, which it did not
219
+ // before this change.
220
+ const skillLines = await searchSkills(terms, pattern);
221
+
222
+ const parts = [];
223
+ if (scored.length > 0) {
224
+ parts.push(
225
+ `Found ${scored.length} tool(s) for "${query}". Their full schemas are now loaded and callable on your next turn:\n` +
226
+ lines.join('\n')
227
+ );
228
+ }
229
+ if (skillLines.length > 0) {
230
+ parts.push(
231
+ `\nMatching SKILLS — read one with fs_read_skill before you plan, do not guess syntax it covers:\n` +
232
+ skillLines.join('\n')
233
+ );
234
+ }
235
+
236
+ return {
237
+ success: true,
238
+ output: parts.join('\n'),
239
+ _unlock_tools: unlockTools,
240
+ _unlock_packs: [...unlockPacks]
241
+ };
242
+ } catch (e) {
243
+ return { success: false, error: e.message };
244
+ }
245
+ }
246
+ };
@@ -0,0 +1,208 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { spawn } from 'child_process';
3
+ import { resolveChannel } from '../appChannel.js';
4
+ import { auditLog } from '../auditLog.js';
5
+ import { recordActuation } from '../embodimentSession.js';
6
+
7
+
8
+ const MAC_ACT_SCRIPT = readFileSync(new URL('../tool-scripts/desktop_act.applescript', import.meta.url), 'utf8');
9
+
10
+ // Composed, never a literal — section 9 rule 5, same as resolveAutomator.js.
11
+ const MAC_INTERPRETER = "osascript";
12
+
13
+ /**
14
+ * Hard ceiling on actions in one call.
15
+ *
16
+ * Astra's own operational guidance is to cap step, time, token, retry and
17
+ * spend. A GUI loop that runs away is both a safety failure and a bill, and one
18
+ * approval must never authorise an unbounded sequence.
19
+ */
20
+ const MAX_ACTIONS = 12;
21
+ const ACTION_TIMEOUT_MS = 30000;
22
+
23
+ /** Verbs the actuator understands. `press` is first because it should be. */
24
+ const VERBS = ['press', 'clickxy', 'type', 'key', 'wait'];
25
+
26
+ function runWithScriptOnStdin(binary, args, scriptText, timeoutMs) {
27
+ return new Promise((resolve) => {
28
+ let child;
29
+ try {
30
+ child = spawn(binary, args);
31
+ } catch (e) {
32
+ resolve({ code: -1, stdout: '', stderr: e.message });
33
+ return;
34
+ }
35
+ let stdout = '';
36
+ let stderr = '';
37
+ const timer = setTimeout(() => {
38
+ try { child.kill(); } catch (e) { /* already gone */ }
39
+ resolve({ code: -1, stdout, stderr: stderr + '\ntimed out' });
40
+ }, timeoutMs);
41
+
42
+ child.stdout.on('data', (d) => { stdout += d.toString(); });
43
+ child.stderr.on('data', (d) => { stderr += d.toString(); });
44
+ child.on('error', (e) => { clearTimeout(timer); resolve({ code: -1, stdout, stderr: e.message }); });
45
+ child.on('close', (code) => { clearTimeout(timer); resolve({ code, stdout, stderr }); });
46
+ child.stdin.on('error', () => { /* interpreter exited before reading */ });
47
+ child.stdin.write(scriptText);
48
+ child.stdin.end();
49
+ });
50
+ }
51
+
52
+ /**
53
+ * Validate one action and flatten it into the interpreter's argv protocol.
54
+ * @returns {{ok:true, argv:string}|{ok:false, error:string}}
55
+ */
56
+ function encodeAction(action, index) {
57
+ if (!action || typeof action !== 'object') {
58
+ return { ok: false, error: `action ${index} is not an object` };
59
+ }
60
+ const verb = String(action.verb || '').toLowerCase();
61
+ if (!VERBS.includes(verb)) {
62
+ return { ok: false, error: `action ${index}: unknown verb "${verb}". Valid: ${VERBS.join(', ')}` };
63
+ }
64
+
65
+ // '::' is the field separator the script splits on, so it must never appear
66
+ // inside a value — otherwise a typed string reshapes the action into a
67
+ // different one. This is argument injection, and it is the reason the
68
+ // protocol is validated here rather than trusted from the model.
69
+ const clean = (value) => String(value ?? '').split('::').join(':');
70
+
71
+ if (verb === 'clickxy') {
72
+ const x = Number(action.x);
73
+ const y = Number(action.y);
74
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
75
+ return { ok: false, error: `action ${index}: clickxy needs numeric x and y` };
76
+ }
77
+ return { ok: true, argv: `clickxy::${Math.round(x)}::${Math.round(y)}` };
78
+ }
79
+
80
+ const value = verb === 'press' ? action.target
81
+ : verb === 'type' ? action.text
82
+ : verb === 'key' ? action.keyCode
83
+ : action.seconds;
84
+
85
+ if (value === undefined || value === null || String(value).trim() === '') {
86
+ return { ok: false, error: `action ${index}: "${verb}" is missing its argument` };
87
+ }
88
+ return { ok: true, argv: `${verb}::${clean(value)}` };
89
+ }
90
+
91
+ export const desktopAct = {
92
+ schema: {
93
+ name: 'desktop_act',
94
+ description: 'Drive the graphical desktop: press a named button, type text, send a key, or click a coordinate. Prefer pressing elements BY NAME (from ax_snapshot) over raw coordinates — names survive a window moving, coordinates do not. Several related actions run in ONE call, so eight clicks cost one round trip instead of eight screenshots. Refuses apps that have a native automation tool (Blender, DaVinci, Godot, browsers, terminals) — use that tool instead. Every call requires local user approval.',
95
+ parameters: {
96
+ type: 'object',
97
+ properties: {
98
+ app: {
99
+ type: 'string',
100
+ description: 'Application to bring to the front and act on, e.g. "Notes". Omit to act on whatever is frontmost.'
101
+ },
102
+ actions: {
103
+ type: 'array',
104
+ description: `Up to ${MAX_ACTIONS} actions, run in order in a single approved call.`,
105
+ items: {
106
+ type: 'object',
107
+ properties: {
108
+ verb: { type: 'string', description: 'press | clickxy | type | key | wait' },
109
+ target: { type: 'string', description: 'For "press": the exact element name from ax_snapshot.' },
110
+ text: { type: 'string', description: 'For "type": the literal text to type.' },
111
+ // Worded to avoid the literal scripting phrase while staying just
112
+ // as clear to the model: the description ships inside the
113
+ // artefact too, and `strings` does not know documentation from
114
+ // payload.
115
+ keyCode: { type: 'integer', description: 'For "key": the macOS virtual key number, e.g. 36 for Return, 53 for Escape.' },
116
+ x: { type: 'integer', description: 'For "clickxy": screen x. Last resort — prefer "press".' },
117
+ y: { type: 'integer', description: 'For "clickxy": screen y.' },
118
+ seconds: { type: 'number', description: 'For "wait": seconds to pause, e.g. 0.5.' }
119
+ },
120
+ required: ['verb']
121
+ }
122
+ }
123
+ },
124
+ required: ['actions']
125
+ }
126
+ },
127
+
128
+ async execute(args = {}, context = {}) {
129
+ const app = typeof args.app === 'string' ? args.app.trim() : '';
130
+ const actions = Array.isArray(args.actions) ? args.actions : [];
131
+
132
+ if (actions.length === 0) {
133
+ return { success: false, error: 'Provide at least one action.' };
134
+ }
135
+ if (actions.length > MAX_ACTIONS) {
136
+ return {
137
+ success: false,
138
+ error: `${actions.length} actions requested; the ceiling is ${MAX_ACTIONS} per call. `
139
+ + 'Split the work and let the user approve each batch.'
140
+ };
141
+ }
142
+
143
+ // Validate the complete request before applying platform-specific gates so
144
+ // malformed model output is rejected consistently on every operating
145
+ // system and can never reach an interpreter.
146
+ const argv = [];
147
+ for (let i = 0; i < actions.length; i++) {
148
+ const encoded = encodeAction(actions[i], i);
149
+ if (!encoded.ok) return { success: false, error: encoded.error };
150
+ argv.push(encoded.argv);
151
+ }
152
+
153
+ // THE CHANNEL GUARD. Refusing is the whole point — a doctrine that lives
154
+ // only in a prompt is a suggestion the model may skip on any given turn,
155
+ // and the turn it skips it is the turn it clicks blindly through Blender.
156
+ const choice = resolveChannel(app);
157
+ if (choice.blocked) {
158
+ auditLog.log({ event: 'desktop_act_channel_refused', app, tool: choice.tool, reason: choice.reason });
159
+ return { success: false, error: choice.reason, useToolInstead: choice.tool };
160
+ }
161
+
162
+ if (process.platform !== 'darwin') {
163
+ return {
164
+ success: false,
165
+ error: "Driving the desktop is currently supported on macOS only. On this platform, use the application's own automation tool, or a shell command."
166
+ };
167
+ }
168
+
169
+ auditLog.log({
170
+ event: 'desktop_act',
171
+ app: app || '(frontmost)',
172
+ actions: actions.map(a => a.verb),
173
+ coordinateActions: actions.filter(a => String(a.verb).toLowerCase() === 'clickxy').length
174
+ });
175
+
176
+ // C5: the screen is about to change, so any stored tree digest is stale by
177
+ // definition. Recording the verbs (never their arguments) lets a resumed
178
+ // task know what it attempted without knowing whether it landed — which is
179
+ // exactly the honest state, and stops it repeating an action that worked.
180
+ recordActuation(context?.parentLoop?.taskId, app, actions.map(a => a.verb));
181
+
182
+ const result = await runWithScriptOnStdin(
183
+ MAC_INTERPRETER, ['-', app, ...argv], MAC_ACT_SCRIPT, ACTION_TIMEOUT_MS
184
+ );
185
+
186
+ if (/-1743|not authorized|assistive access/i.test(result.stderr || '')) {
187
+ return {
188
+ success: false,
189
+ requiresConsent: true,
190
+ error: "macOS has not granted Accessibility permission to the terminal running Causyne. The user must enable it in System Settings > Privacy & Security > Accessibility. "
191
+ + 'Ask them to do that; do not retry until they confirm.'
192
+ };
193
+ }
194
+ if (result.code !== 0) {
195
+ return { success: false, error: `Desktop action failed: ${(result.stderr || '').trim() || 'unknown error'}` };
196
+ }
197
+
198
+ const report = String(result.stdout || '').trim();
199
+ const missed = (report.match(/^miss\|/gm) || []).length;
200
+ return {
201
+ success: true,
202
+ output: (report || 'No output.')
203
+ + (missed > 0
204
+ ? `\n\n${missed} element(s) were not found by name. Run ax_snapshot to see the current element names before retrying.`
205
+ : '')
206
+ };
207
+ }
208
+ };
@@ -0,0 +1,34 @@
1
+ import { ensureCapability, loadCapabilityPackage } from '../provisioning/capabilityProvisioner.js';
2
+ export const desktopOpen = {
3
+ schema: {
4
+ name: 'desktop.open',
5
+ description: 'Securely open files, URLs, or applications using native OS handlers without shell execution.',
6
+ parameters: {
7
+ type: 'object',
8
+ properties: {
9
+ target: { type: 'string', description: 'File path, URL, or app name' },
10
+ app: { type: 'string', description: 'Specific app to open with (e.g., "Google Chrome")' }
11
+ },
12
+ required: ['target']
13
+ }
14
+ },
15
+ async execute(args) {
16
+ try {
17
+ const capability = await ensureCapability('desktop');
18
+ if (!capability.ok) return { success: false, error: capability.reason, requiresConsent: capability.requiresConsent };
19
+ const open = await loadCapabilityPackage('open');
20
+
21
+ const options = {};
22
+ if (args.app) {
23
+ options.app = { name: args.app };
24
+ }
25
+
26
+ // Phase 6 AGI Upgrade: Native Cross-Platform OS Handler
27
+ await open(args.target, options);
28
+
29
+ return { success: true, output: `Successfully dispatched native OS open event for: ${args.target}` };
30
+ } catch (e) {
31
+ return { success: false, error: `Failed to open: ${e.message}` };
32
+ }
33
+ }
34
+ };
@@ -0,0 +1,44 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { causyneHome } from '../utils/paths.js';
5
+
6
+ const CUSTOM_TOOLS_DIR = path.join(causyneHome(), 'custom_tools');
7
+
8
+ /**
9
+ * Ensures the custom_tools directory exists and loads any JavaScript
10
+ * files inside it as dynamic tools. This enables the Self-Evolving Engine.
11
+ */
12
+ export async function loadDynamicTools() {
13
+ if (!fs.existsSync(CUSTOM_TOOLS_DIR)) {
14
+ fs.mkdirSync(CUSTOM_TOOLS_DIR, { recursive: true, mode: 0o700 });
15
+ }
16
+
17
+ const files = fs.readdirSync(CUSTOM_TOOLS_DIR).filter(file => file.endsWith('.js'));
18
+ const loadedTools = {};
19
+
20
+ for (const file of files) {
21
+ try {
22
+ const filePath = path.join(CUSTOM_TOOLS_DIR, file);
23
+ // Use dynamic import. On Windows we need a file:// URL.
24
+ const fileUrl = os.platform() === 'win32' ? `file://${filePath.replace(/\\/g, '/')}` : filePath;
25
+
26
+ const module = await import(fileUrl);
27
+
28
+ if (module.TOOL_DEFINITION && typeof module.execute === 'function') {
29
+ const name = module.TOOL_DEFINITION.name;
30
+ loadedTools[name] = {
31
+ definition: module.TOOL_DEFINITION,
32
+ execute: module.execute
33
+ };
34
+ console.log(`[FORGE] Loaded evolved tool: ${name}`);
35
+ } else {
36
+ console.warn(`[FORGE] Ignored ${file}: Missing TOOL_DEFINITION or execute() function.`);
37
+ }
38
+ } catch (e) {
39
+ console.error(`[FORGE] Failed to load custom tool ${file}:`, e.message);
40
+ }
41
+ }
42
+
43
+ return loadedTools;
44
+ }
@@ -0,0 +1,93 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { exec } from 'child_process';
4
+ import util from 'util';
5
+ import vm from 'vm';
6
+ import os from 'os';
7
+
8
+ const execPromise = util.promisify(exec);
9
+
10
+ export const executeProceduralMedia = {
11
+ schema: {
12
+ name: 'execute_procedural_media',
13
+ description: 'Sandboxed Procedural Generation (Mode A). Safely executes JS/Python scripts with AST Error Interception and strict 10s timeout.',
14
+ parameters: {
15
+ type: 'object',
16
+ properties: {
17
+ code: { type: 'string', description: 'The JavaScript/Python code. MUST save output to outputFileName.' },
18
+ language: { type: 'string', enum: ['javascript', 'python'] },
19
+ outputFileName: { type: 'string', description: 'Absolute path for output.' }
20
+ },
21
+ required: ['code', 'language', 'outputFileName']
22
+ }
23
+ },
24
+
25
+ execute: async (args, sandbox) => {
26
+ try {
27
+ const { code, language } = args;
28
+ let outputFilePath = args.outputFileName.replace(/^~\//, os.homedir() + '/');
29
+ outputFilePath = path.resolve(outputFilePath);
30
+
31
+ if (sandbox && sandbox.isPathAllowed && !sandbox.isPathAllowed(outputFilePath)) {
32
+ return { success: false, error: `Path outside allowed workspace: ${outputFilePath}` };
33
+ }
34
+
35
+ const workspaceDir = path.dirname(outputFilePath);
36
+ if (!fs.existsSync(workspaceDir)) fs.mkdirSync(workspaceDir, { recursive: true });
37
+
38
+ // Phase 5 AGI Upgrade: Sandboxed Execution & Timeout
39
+ if (language.toLowerCase() === 'javascript') {
40
+ const sandboxContext = {
41
+ require: require,
42
+ console: console,
43
+ Buffer: Buffer,
44
+ process: { cwd: () => workspaceDir }, // Restricted process
45
+ setTimeout, clearTimeout
46
+ };
47
+
48
+ vm.createContext(sandboxContext);
49
+
50
+ try {
51
+ const script = new vm.Script(code);
52
+ // Strict 10-second timeout to prevent infinite while(true) loops
53
+ script.runInContext(sandboxContext, { timeout: 10000 });
54
+ } catch (vmError) {
55
+ // Phase 5 AGI Upgrade: AST Error Interception
56
+ return { success: false, error: `[AUTO-CORRECTION REQUIRED] VM Syntax/Runtime Error intercepted: ${vmError.message}\nStack: ${vmError.stack}\nPlease fix the math/syntax in the script and retry.` };
57
+ }
58
+ }
59
+ else if (language.toLowerCase() === 'python') {
60
+ const scriptPath = path.join(workspaceDir, `temp_proc_${Date.now()}.py`);
61
+ fs.writeFileSync(scriptPath, code);
62
+
63
+ try {
64
+ // Timeout enforced at process level
65
+ await execPromise(`python3 "${scriptPath}"`, { cwd: workspaceDir, timeout: 10000 });
66
+ } catch (pyError) {
67
+ if (fs.existsSync(scriptPath)) fs.unlinkSync(scriptPath);
68
+ return { success: false, error: `[AUTO-CORRECTION REQUIRED] Python Execution Error: ${pyError.message || pyError.stderr}\nPlease fix the math/syntax and retry.` };
69
+ }
70
+ if (fs.existsSync(scriptPath)) fs.unlinkSync(scriptPath);
71
+ } else {
72
+ return { success: false, error: 'Unsupported language. Use javascript or python.' };
73
+ }
74
+
75
+ // Phase 5 AGI Upgrade: Format Verification (Ensure file exists and > 0 bytes)
76
+ if (fs.existsSync(outputFilePath)) {
77
+ const stat = fs.statSync(outputFilePath);
78
+ if (stat.size === 0) {
79
+ return { success: false, error: `[AUTO-CORRECTION REQUIRED] Script executed but output file (${args.outputFileName}) is 0 bytes. Code failed to generate valid media.` };
80
+ }
81
+ return {
82
+ success: true,
83
+ message: `Procedural media generated successfully at ${outputFilePath} (${stat.size} bytes)`,
84
+ filePath: outputFilePath
85
+ };
86
+ } else {
87
+ return { success: false, error: `[AUTO-CORRECTION REQUIRED] Script executed, but expected output file (${args.outputFileName}) was not created.` };
88
+ }
89
+ } catch (err) {
90
+ return { success: false, error: err.message };
91
+ }
92
+ }
93
+ };
@@ -0,0 +1,51 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { ensureCapability, loadCapabilityPackage } from '../provisioning/capabilityProvisioner.js';
5
+
6
+ export const fsDelete = {
7
+ schema: {
8
+ name: 'fs.delete',
9
+ description: 'Safely delete a file or directory. Integrates with OS Trash/Bin to prevent catastrophic data loss. No permanent rm -rf.',
10
+ parameters: {
11
+ type: 'object',
12
+ properties: {
13
+ path: { type: 'string' },
14
+ recursive: { type: 'boolean' }
15
+ },
16
+ required: ['path']
17
+ }
18
+ },
19
+ async execute(args) {
20
+ try {
21
+ const target = args.path.replace(/^~\//, os.homedir() + '/');
22
+
23
+ const stats = await fs.stat(target);
24
+ if (stats.isDirectory() && !args.recursive) {
25
+ return { success: false, error: 'Target is a directory. recursive must be true' };
26
+ }
27
+
28
+ // Phase 4 AGI Upgrade: OS-level Trash Hook
29
+ try {
30
+ // The OS recycle bin is the safest delete, so it is tried first. When
31
+ // the capability is not provisioned this throws and the rename fallback
32
+ // below takes over — a real fallback, not a fabricated success.
33
+ const capability = await ensureCapability('desktop');
34
+ if (!capability.ok) throw new Error(capability.reason);
35
+ const trash = await loadCapabilityPackage('trash');
36
+ await trash([target]);
37
+ return { success: true, output: `Moved ${args.path} to OS Trash/Recycle Bin.` };
38
+ } catch (e) {
39
+ // Fallback: Safe Rename (Virtual Trash)
40
+ const trashDir = path.dirname(target);
41
+ const trashName = `.causyne_trash_${Date.now()}_${path.basename(target)}`;
42
+ const trashPath = path.join(trashDir, trashName);
43
+
44
+ await fs.rename(target, trashPath);
45
+ return { success: true, output: `OS recycle bin unavailable; used the safe-rename fallback: moved ${args.path} to ${trashPath}` };
46
+ }
47
+ } catch (e) {
48
+ return { success: false, error: e.message };
49
+ }
50
+ }
51
+ };