@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.
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,84 @@
1
+ import { canonicalModelId } from '../modelIds.js';
2
+ import { DEFAULT_MODEL_BY_PROVIDER } from '../modelCatalogue.js';
3
+ import { LLMProvider } from './base.js';
4
+
5
+ export class OpenAICompatibleProvider extends LLMProvider {
6
+ constructor(config) {
7
+ super(config);
8
+
9
+ // A custom endpoint can serve anything, so guessing from the model id would
10
+ // be wrong as often as right. config.supportsAudio is the deliberate opt-in,
11
+ // exactly as config.supportsVision already is for images.
12
+ this.audioModels = [];
13
+ // 'low' | 'medium' | 'high' | 'off'. Unset means: do not send the parameter.
14
+ this.reasoningEffort = config.reasoningEffort || null;
15
+ this.baseUrl = config.baseUrl || 'https://api.openai.com/v1/chat/completions';
16
+ this.model = canonicalModelId(config.model || DEFAULT_MODEL_BY_PROVIDER['openai']);
17
+ }
18
+
19
+ async chat(messages, options = {}) {
20
+ const preparedMessages = this.prepareMessages(messages);
21
+ const payload = {
22
+ model: this.model,
23
+ messages: preparedMessages,
24
+ temperature: options.temperature || 0.1,
25
+ };
26
+
27
+ // Reasoning models take an effort dial. Sending it to a model that does not
28
+ // support it is rejected, so it only goes out when the user configured it —
29
+ // a custom OpenAI-compatible endpoint may serve anything, and this provider
30
+ // cannot know the model list in advance.
31
+ if (this.reasoningEffort && this.reasoningEffort !== 'off') {
32
+ payload.reasoning_effort = this.reasoningEffort;
33
+ }
34
+
35
+ if (options.jsonMode) {
36
+ payload.response_format = { type: 'json_object' };
37
+ }
38
+
39
+ if (options.tools) {
40
+ payload.tools = options.tools.map(tool => ({
41
+ type: 'function',
42
+ function: tool.function ? tool.function : tool
43
+ }));
44
+ payload.tool_choice = "auto";
45
+ }
46
+
47
+ const response = await fetch(this.baseUrl, {
48
+ method: 'POST',
49
+ headers: {
50
+ 'Content-Type': 'application/json',
51
+ 'Authorization': `Bearer ${this.apiKey}`
52
+ },
53
+ body: JSON.stringify(payload)
54
+ });
55
+
56
+ if (!response.ok) {
57
+ const errorData = await response.text();
58
+ throw new Error(`OpenAI-Compatible API Error (${response.status}): ${errorData}`);
59
+ }
60
+
61
+ const data = await response.json();
62
+ let content = '';
63
+ let toolCalls = [];
64
+
65
+ let reasoningContent = '';
66
+
67
+ if (data.choices && data.choices[0] && data.choices[0].message) {
68
+ const message = data.choices[0].message;
69
+ content = message.content || '';
70
+ toolCalls = message.tool_calls || [];
71
+ // DeepSeek and several compatible endpoints return the chain of thought
72
+ // in a sibling field. It was being discarded, so the reasoning the user
73
+ // paid for never reached them.
74
+ reasoningContent = message.reasoning_content || message.reasoning || '';
75
+ }
76
+
77
+ return {
78
+ content: content,
79
+ toolCalls: (toolCalls && toolCalls.length > 0) ? toolCalls : undefined,
80
+ reasoningContent: reasoningContent || undefined,
81
+ usage: this.normalizeUsage(data.usage || { prompt_tokens: 0, completion_tokens: 0 })
82
+ };
83
+ }
84
+ }
@@ -0,0 +1,20 @@
1
+ import { canonicalModelId } from '../modelIds.js';
2
+ import { DEFAULT_MODEL_BY_PROVIDER } from '../modelCatalogue.js';
3
+ import { OpenAICompatibleProvider } from './openai-compatible.js';
4
+
5
+ export class OpenAIProvider extends OpenAICompatibleProvider {
6
+ constructor(config) {
7
+ super(config);
8
+ this.baseUrl = 'https://api.openai.com/v1/chat/completions';
9
+ this.model = canonicalModelId(config.model || DEFAULT_MODEL_BY_PROVIDER['openai']);
10
+ this.visionModels = ['*'];
11
+ this.imagePayloadFormat = 'openai';
12
+ // Vision is '*' but audio is emphatically NOT. OpenAI accepts input_audio
13
+ // only on the audio-capable families; the text models reject the block
14
+ // outright, so copying visionModels here would 400 on every ordinary chat
15
+ // model. Audio input is also limited to wav and mp3 containers, which is
16
+
17
+ this.audioModels = ['gpt-4o-audio', 'gpt-4o-mini-audio', 'gpt-audio', 'gpt-realtime'];
18
+ this.audioPayloadFormat = 'openai';
19
+ }
20
+ }
@@ -0,0 +1,76 @@
1
+ import { canonicalModelId } from '../modelIds.js';
2
+ import { DEFAULT_MODEL_BY_PROVIDER } from '../modelCatalogue.js';
3
+ import { LLMProvider } from './base.js';
4
+
5
+ // Direct DashScope API connection for BYOK (unlike dashscope.js which is the proxy for Free Demo)
6
+ export class QwenByokProvider extends LLMProvider {
7
+ constructor(config) {
8
+ super(config);
9
+ // Use International/Singapore endpoint by default, allow override via config
10
+ let url = config.baseUrl || 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions';
11
+ if (url.endsWith('/v1')) url += '/chat/completions';
12
+ else if (url.endsWith('/v1/')) url += 'chat/completions';
13
+
14
+ this.baseUrl = url;
15
+ this.model = canonicalModelId(config.model || DEFAULT_MODEL_BY_PROVIDER['qwen']);
16
+ this.apiKey = config.apiKey;
17
+ this.visionModels = ['-vl-', '-vl', 'qwen3.6', 'qwen3.7'];
18
+ // Qwen exposes audio on the omni and dedicated audio families only; the
19
+ // -vl- vision models are image-in, text-out and reject an audio block.
20
+ this.audioModels = ['-omni', 'qwen-audio', 'qwen2-audio'];
21
+ }
22
+
23
+ async chat(messages, options = {}) {
24
+ const sanitizedMessages = this.prepareMessages(messages);
25
+
26
+ const payload = {
27
+ model: this.model,
28
+ messages: sanitizedMessages,
29
+ temperature: options.temperature || 0.1,
30
+ };
31
+
32
+ if (options.jsonMode) {
33
+ payload.response_format = { type: 'json_object' };
34
+ }
35
+
36
+ if (options.tools) {
37
+ payload.tools = options.tools.map(tool => ({
38
+ type: 'function',
39
+ function: tool.function ? tool.function : tool
40
+ }));
41
+ payload.tool_choice = "auto";
42
+ }
43
+
44
+ const response = await fetch(this.baseUrl, {
45
+ method: 'POST',
46
+ headers: {
47
+ 'Content-Type': 'application/json',
48
+ 'Authorization': `Bearer ${this.apiKey}`
49
+ },
50
+ body: JSON.stringify(payload)
51
+ });
52
+
53
+ if (!response.ok) {
54
+ const errorData = await response.text();
55
+
56
+ // Enterprise Error Handling: Detect Workspace-bound API Key configuration errors
57
+ if (response.status === 401 && this.apiKey && this.apiKey.startsWith('sk-ws-')) {
58
+ if (!this.baseUrl.includes('ws-')) {
59
+ throw new Error(`\n❌ QWEN WORKSPACE CONFIGURATION ERROR ❌\n\nYou are using a Workspace-bound API Key (starts with sk-ws-), but you haven't configured your Workspace Base URL!\n\nAlibaba requires a specific URL for these keys.\n\n🛠️ HOW TO FIX THIS:\n1. Open your computer terminal.\n2. Run command: causyne-agent config\n3. When asked for 'Custom Base URL', paste your Workspace URL from Alibaba (e.g. https://{Your-Workspace-ID}.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1/chat/completions).\n\nAlternatively, use a Standard API Key (starts with sk-) which does not require a custom URL.`);
60
+ }
61
+ }
62
+
63
+ throw new Error(`Qwen DashScope API Error (${response.status}): ${errorData}`);
64
+ }
65
+
66
+ const data = await response.json();
67
+ const content = data.choices[0].message.content;
68
+ const toolCalls = data.choices[0].message.tool_calls;
69
+
70
+ return {
71
+ content: content || '',
72
+ toolCalls: (toolCalls && toolCalls.length > 0) ? toolCalls : undefined,
73
+ usage: this.normalizeUsage(data.usage)
74
+ };
75
+ }
76
+ }
@@ -0,0 +1,73 @@
1
+ import { canonicalModelId } from '../modelIds.js';
2
+ import { DEFAULT_MODEL_BY_PROVIDER } from '../modelCatalogue.js';
3
+ import { LLMProvider } from './base.js';
4
+
5
+ export class XAIProvider extends LLMProvider {
6
+ constructor(config) {
7
+ super(config);
8
+ this.baseUrl = 'https://api.x.ai/v1/chat/completions';
9
+ this.model = canonicalModelId(config.model || DEFAULT_MODEL_BY_PROVIDER['xai']);
10
+ this.visionModels = ['grok-4', 'vision'];
11
+ // Grok has no audio input block on the chat completions API.
12
+ this.audioModels = [];
13
+ }
14
+
15
+ async chat(messages, options = {}) {
16
+ const sanitizedMessages = this.prepareMessages(messages);
17
+
18
+ const payload = {
19
+ model: this.model,
20
+ messages: sanitizedMessages,
21
+ temperature: options.temperature || 0.1,
22
+ };
23
+
24
+ if (options.jsonMode) {
25
+ payload.response_format = { type: 'json_object' };
26
+ }
27
+
28
+ let headers = {
29
+ 'Content-Type': 'application/json'
30
+ };
31
+
32
+ if (this.apiKey) {
33
+ headers['Authorization'] = `Bearer ${this.apiKey}`;
34
+ } else {
35
+ // EXPLICIT BYOK CONSTRAINT
36
+ throw new Error(`xAI (Grok) API requires a valid API key (BYOK mode). Please set it in your configuration.`);
37
+ }
38
+
39
+ if (options.tools) {
40
+ payload.tools = options.tools.map(tool => ({
41
+ type: 'function',
42
+ function: tool.function ? tool.function : tool
43
+ }));
44
+ payload.tool_choice = "auto";
45
+ }
46
+
47
+ const response = await fetch(this.baseUrl, {
48
+ method: 'POST',
49
+ headers: headers,
50
+ body: JSON.stringify(payload)
51
+ });
52
+
53
+ if (!response.ok) {
54
+ const errorData = await response.text();
55
+ throw new Error(`xAI API Error (${response.status}): ${errorData}`);
56
+ }
57
+
58
+ const data = await response.json();
59
+ let content = '';
60
+ let toolCalls = [];
61
+
62
+ if (data.choices && data.choices[0] && data.choices[0].message) {
63
+ content = data.choices[0].message.content || '';
64
+ toolCalls = data.choices[0].message.tool_calls || [];
65
+ }
66
+
67
+ return {
68
+ content: content,
69
+ toolCalls: (toolCalls && toolCalls.length > 0) ? toolCalls : undefined,
70
+ usage: this.normalizeUsage(data.usage || { prompt_tokens: 0, completion_tokens: 0 })
71
+ };
72
+ }
73
+ }
@@ -0,0 +1,24 @@
1
+ // Consent state for capability groups, readable without loading the installer.
2
+ //
3
+ // Three states matter and only three: granted (true), declined (false), and
4
+ // never asked (absent). Collapsing "declined" into "not granted" would make the
5
+ // agent re-ask forever, which is how a consent prompt becomes noise the user
6
+ // learns to dismiss.
7
+
8
+ import { loadConfig } from '../config.js';
9
+
10
+ export function consentMap() {
11
+ try {
12
+ return loadConfig().capabilityConsent || {};
13
+ } catch (e) {
14
+ return {};
15
+ }
16
+ }
17
+
18
+ export function isGroupGranted(group) {
19
+ return consentMap()[group] === true;
20
+ }
21
+
22
+ export function isGroupDeclined(group) {
23
+ return consentMap()[group] === false;
24
+ }
@@ -0,0 +1,271 @@
1
+
2
+
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { createRequire } from 'module';
6
+ import { execFileSync } from 'node:child_process';
7
+ import { pathToFileURL } from 'url';
8
+ import { loadConfig, saveConfig } from '../config.js';
9
+ import { isGroupGranted, isGroupDeclined } from './capabilityConsent.js';
10
+ import { causyneHome } from '../utils/paths.js';
11
+
12
+ const CAPABILITY_DIR = path.join(causyneHome(), 'capabilities');
13
+
14
+ // Groups exist so consent is asked once per capability a human recognises
15
+ // ("let it see the screen"), not once per npm package. Names stay in value
16
+ // position — see the D-5 note above.
17
+ const GROUP_PACKAGES = {
18
+ vision: [
19
+ ['puppeteer-core', '25.8.0'],
20
+ ['screenshot-desktop', '1.15.6']
21
+ ],
22
+ ocr: [
23
+ ['tesseract.js', '7.0.0']
24
+ ],
25
+ system: [
26
+ ['systeminformation', '5.33.1']
27
+ ],
28
+ desktop: [
29
+ ['clipboardy', '5.3.2'],
30
+ ['open', '11.0.1'],
31
+ ['trash', '10.1.1']
32
+ ],
33
+ archive: [
34
+ ['archiver', '8.0.0'],
35
+ ['extract-zip', '2.0.1']
36
+ ]
37
+ };
38
+
39
+ // What the user is actually agreeing to. This text reaches the approval prompt
40
+
41
+ // than listing package names.
42
+ export const GROUP_DESCRIPTIONS = {
43
+ vision: 'drive a browser and capture the screen, so the agent can look at what it built',
44
+ ocr: 'read text out of images and screenshots',
45
+ system: 'list running processes and read system load',
46
+ desktop: 'use the clipboard, open files in their default application, and delete to the recycle bin',
47
+ archive: 'create and extract zip archives'
48
+ };
49
+
50
+ export const GROUP_NAMES = Object.keys(GROUP_PACKAGES);
51
+
52
+ /** Which group owns a package, or null when the package is not ours. */
53
+ export function groupForPackage(packageName) {
54
+ for (const [group, packages] of Object.entries(GROUP_PACKAGES)) {
55
+ if (packages.some(([name]) => name === packageName)) return group;
56
+ }
57
+ return null;
58
+ }
59
+
60
+ function manifestFor(groups) {
61
+ const dependencies = [];
62
+ for (const group of groups) {
63
+ for (const entry of GROUP_PACKAGES[group] || []) dependencies.push(entry);
64
+ }
65
+ return {
66
+ name: 'causyne-capabilities-env',
67
+ version: '1.0.0',
68
+ private: true,
69
+ // Keys are produced at runtime; the names above never sit in key position.
70
+ dependencies: Object.fromEntries(dependencies)
71
+ };
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Consent
76
+ // ---------------------------------------------------------------------------
77
+
78
+
79
+ export function grantedGroups() {
80
+ return GROUP_NAMES.filter(isGroupGranted);
81
+ }
82
+
83
+ export function grantGroup(group) {
84
+ if (!GROUP_PACKAGES[group]) return false;
85
+ const config = loadConfig();
86
+ config.capabilityConsent = { ...(config.capabilityConsent || {}), [group]: true };
87
+ saveConfig(config);
88
+ return true;
89
+ }
90
+
91
+ export { isGroupGranted, isGroupDeclined };
92
+
93
+ export function revokeGroup(group) {
94
+ const config = loadConfig();
95
+ config.capabilityConsent = { ...(config.capabilityConsent || {}), [group]: false };
96
+ saveConfig(config);
97
+ return true;
98
+ }
99
+
100
+ // ---------------------------------------------------------------------------
101
+ // Availability
102
+ // ---------------------------------------------------------------------------
103
+
104
+ /** True when the package is already on disk in the sovereign directory. */
105
+ export function isPackageInstalled(packageName) {
106
+ return fs.existsSync(path.join(CAPABILITY_DIR, 'node_modules', packageName));
107
+ }
108
+
109
+ export function isGroupInstalled(group) {
110
+ const packages = GROUP_PACKAGES[group] || [];
111
+ return packages.length > 0 && packages.every(([name]) => isPackageInstalled(name));
112
+ }
113
+
114
+ /**
115
+ * Whether a group can be used right now, and if not, why.
116
+ *
117
+ * Deliberately three states, not two. "Not granted" is a question for the user;
118
+ * "granted but not installed" is work the agent can do; "unavailable" is a
119
+ * dead end the model should route around rather than retry.
120
+ */
121
+ export function groupStatus(group) {
122
+ if (!GROUP_PACKAGES[group]) return { state: 'unknown', group };
123
+ if (isGroupInstalled(group)) return { state: 'ready', group };
124
+ if (isGroupGranted(group)) return { state: 'granted-not-installed', group };
125
+ return { state: 'needs-consent', group, description: GROUP_DESCRIPTIONS[group] };
126
+ }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // Provisioning
130
+ // ---------------------------------------------------------------------------
131
+
132
+ /**
133
+ * Install every granted group into ~/.causyne/capabilities.
134
+ *
135
+ * Runs `npm i` from a written manifest with no explicit package arguments, so a
136
+ * lockfile is produced and the resulting tree can be audited with `npm audit` —
137
+ * the same correction finding D-4 forced on the cortex provisioner, where
138
+ * `--no-save` had left a tree that could not be checked at all.
139
+ *
140
+ * @param {string[]} groups groups to ensure are present, added to those already granted
141
+ */
142
+ export async function provisionCapabilities(groups = []) {
143
+ const target = [...new Set([...grantedGroups(), ...groups])].filter(g => GROUP_PACKAGES[g]);
144
+ if (target.length === 0) return { installed: [], skipped: true };
145
+
146
+ if (!fs.existsSync(CAPABILITY_DIR)) {
147
+ fs.mkdirSync(CAPABILITY_DIR, { recursive: true, mode: 0o700 });
148
+ }
149
+
150
+ const manifestPath = path.join(CAPABILITY_DIR, 'package.json');
151
+ const desired = JSON.stringify(manifestFor(target), null, 2);
152
+
153
+ let needsInstall = true;
154
+ if (fs.existsSync(manifestPath)) {
155
+ try { needsInstall = fs.readFileSync(manifestPath, 'utf8') !== desired; } catch (e) {}
156
+ }
157
+ if (needsInstall) fs.writeFileSync(manifestPath, desired);
158
+
159
+ if (!fs.existsSync(path.join(CAPABILITY_DIR, 'package-lock.json'))) needsInstall = true;
160
+ for (const group of target) {
161
+ if (!isGroupInstalled(group)) { needsInstall = true; break; }
162
+ }
163
+
164
+ if (!needsInstall) return { installed: target, skipped: true };
165
+
166
+ const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
167
+ execFileSync(
168
+ npm,
169
+ ['install', '--ignore-scripts', '--no-fund'],
170
+ { cwd: CAPABILITY_DIR, stdio: 'inherit' }
171
+ );
172
+ return { installed: target, skipped: false };
173
+ }
174
+
175
+ /**
176
+ * The call every capability-backed tool makes first.
177
+ *
178
+ * Returns a plain verdict rather than throwing, because the caller has to turn
179
+ * it into an honest tool result: the model must learn that the capability is
180
+ * unavailable, not that something crashed.
181
+ */
182
+ export async function ensureCapability(group) {
183
+ const status = groupStatus(group);
184
+ if (status.state === 'ready') return { ok: true, group };
185
+ if (status.state === 'unknown') return { ok: false, group, reason: `Unknown capability group "${group}".` };
186
+
187
+ if (status.state === 'needs-consent') {
188
+ return {
189
+ ok: false,
190
+ group,
191
+ requiresConsent: true,
192
+ reason: `This tool needs the "${group}" capability — permission to ${GROUP_DESCRIPTIONS[group]}. ` +
193
+ `It has not been granted yet, so the tool cannot run. To enable it, call ` +
194
+ `manage_local_dependencies with action "grant_capability" and group "${group}"; ` +
195
+ `that request goes to the user for approval.`
196
+ };
197
+ }
198
+
199
+ // Granted but not yet on disk — do the work now.
200
+ try {
201
+ await provisionCapabilities([group]);
202
+ } catch (e) {
203
+ return { ok: false, group, reason: `The "${group}" capability could not be prepared: ${e.message}` };
204
+ }
205
+ return isGroupInstalled(group)
206
+ ? { ok: true, group }
207
+ : { ok: false, group, reason: `The "${group}" capability is unavailable in this environment.` };
208
+ }
209
+
210
+ /**
211
+ * Load a provisioned module.
212
+ *
213
+ * Mirrors requireCortexModule: resolve through Node's own resolver against the
214
+ * sovereign directory so `main`/`exports` are honoured, then import a real file
215
+ * URL. Passing a package DIRECTORY to import() throws ERR_UNSUPPORTED_DIR_IMPORT,
216
+ * which is the bug that silently disabled the entire V5 Cortex once already.
217
+ */
218
+ export async function requireCapabilityModule(packageName) {
219
+ const modulePath = path.join(CAPABILITY_DIR, 'node_modules', packageName);
220
+ if (!fs.existsSync(modulePath)) {
221
+ throw new Error(`Capability module ${packageName} is not provisioned.`);
222
+ }
223
+
224
+ const capabilityRequire = createRequire(path.join(CAPABILITY_DIR, 'package.json'));
225
+ let entryPoint;
226
+ try {
227
+ entryPoint = capabilityRequire.resolve(packageName);
228
+ } catch (resolveError) {
229
+ const manifestPath = path.join(modulePath, 'package.json');
230
+ let mainField = 'index.js';
231
+ if (fs.existsSync(manifestPath)) {
232
+ try {
233
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
234
+ mainField = manifest.main || manifest.module || 'index.js';
235
+ } catch (e) {}
236
+ }
237
+ entryPoint = path.join(modulePath, mainField);
238
+ if (!fs.existsSync(entryPoint)) {
239
+ throw new Error(`Capability module ${packageName} has no resolvable entry point: ${resolveError.message}`);
240
+ }
241
+ }
242
+
243
+ const mod = await import(pathToFileURL(entryPoint).href);
244
+ return mod.default || mod;
245
+ }
246
+
247
+ /**
248
+ * Load a package, preferring one that is already reachable from the agent's own
249
+ * install (a developer running from source may simply have it) and falling back
250
+ * to the sovereign directory. Tools call this instead of a bare dynamic import.
251
+ */
252
+ export async function loadCapabilityPackage(packageName) {
253
+ try {
254
+ const local = await import(packageName);
255
+ return local.default || local;
256
+ } catch (e) {
257
+ try {
258
+ return await requireCapabilityModule(packageName);
259
+ } catch (capabilityError) {
260
+ // Some packages already exist in the Cortex tree as pinned overrides
261
+ // (sharp, for one). Reuse them rather than installing a second copy that
262
+ // could drift to a different version than the one that was audited.
263
+ const { requireCortexModule } = await import('../cortex/provisioner.js');
264
+ return requireCortexModule(packageName);
265
+ }
266
+ }
267
+ }
268
+
269
+ export function capabilityDirectory() {
270
+ return CAPABILITY_DIR;
271
+ }
@@ -0,0 +1,29 @@
1
+ // Which catalogue tools depend on which capability group.
2
+ //
3
+ // Kept separate from capabilityProvisioner.js so that registry.js — which runs
4
+ // on every single LLM call — can consult it without pulling in the install
5
+ // machinery, the config vault, or execSafe.
6
+ //
7
+ // A tool appears here only when the capability is REQUIRED for it to do
8
+ // anything. fs_delete is deliberately absent: it prefers the OS recycle bin but
9
+ // has a real safe-rename fallback, so it works with no capability at all.
10
+
11
+ import { isGroupDeclined } from './capabilityConsent.js';
12
+
13
+ export const TOOLS_BY_GROUP = {
14
+ vision: ['browser_act', 'screenshot', 'vision_inspect_ui', 'game_canvas_audit', 'game_web_runtime_inspector'],
15
+ ocr: [],
16
+ system: ['process_list'],
17
+ desktop: ['clipboard', 'desktop_open'],
18
+ archive: ['archive']
19
+ };
20
+
21
+ /** Catalogue names the user has explicitly declined the capability for. */
22
+ export function declinedToolNames() {
23
+ const declined = new Set();
24
+ for (const [group, tools] of Object.entries(TOOLS_BY_GROUP)) {
25
+ if (!isGroupDeclined(group)) continue;
26
+ for (const name of tools) declined.add(name);
27
+ }
28
+ return declined;
29
+ }