@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,560 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { loadConfig } from './config.js';
5
+
6
+ import { causyneHome } from './utils/paths.js';
7
+
8
+ const USAGE_DIR = path.join(causyneHome(), 'usage');
9
+
10
+ /**
11
+ * The date key for "today", in the USER'S timezone.
12
+ *
13
+ * This was `new Date().toISOString().split('T')[0]`, which is UTC. A daily
14
+ * budget bucketed in UTC does not reset at the user's midnight: in WIB (UTC+7)
15
+ * it rolls over at 07:00 local, in NZDT (UTC+13) at 13:00, in PST (UTC-8) the
16
+ * previous evening. So a user's morning work was charged to "yesterday" and
17
+ * their daily cap could be exhausted before their day had started — and the
18
+ * reset landed in the middle of it.
19
+ *
20
+ * A limit the user sets as "per day" must mean their day. Anything else is a
21
+ * limit they cannot reason about.
22
+ */
23
+
24
+ let usageQueue = Promise.resolve();
25
+ function withUsageLock(fn) {
26
+ const run = usageQueue.then(fn, fn);
27
+ usageQueue = run.then(() => {}, () => {});
28
+ return run;
29
+ }
30
+
31
+ /**
32
+ * Write a usage file the way the journal does: temp file, then rename.
33
+ *
34
+ * A direct writeFileSync that is interrupted leaves truncated JSON, and
35
+ * getDailyUsage() answers a parse failure with a fresh zeroed record — so a
36
+ * torn write does not surface as an error, it silently ERASES the day's budget.
37
+ * rename() is atomic on the same filesystem, so a reader sees either the whole
38
+ * previous file or the whole new one.
39
+ */
40
+ function writeUsageFile(file, payload) {
41
+ const temp = `${file}.tmp_${process.pid}`;
42
+ fs.writeFileSync(temp, JSON.stringify(payload, null, 2), { encoding: 'utf8', mode: 0o600 });
43
+ fs.renameSync(temp, file);
44
+ }
45
+
46
+ function localDateKey(when = new Date()) {
47
+ const pad = (n) => String(n).padStart(2, '0');
48
+ return `${when.getFullYear()}-${pad(when.getMonth() + 1)}-${pad(when.getDate())}`;
49
+ }
50
+
51
+ // Input-token count above which GPT-6 Astra bills on its second rate card
52
+ // ($20/$75 instead of $10/$50). The higher card applies to the whole request,
53
+ // not to the overage alone.
54
+ const ASTRA_LONG_CONTEXT_THRESHOLD = 272000;
55
+
56
+ export class TokenTracker {
57
+ constructor() {
58
+ this.config = loadConfig();
59
+ this.budget = this.config.tokenBudget || { enabled: false };
60
+ this.taskTokens = 0;
61
+ this.taskCostUsd = 0;
62
+ this.largestStepTokens = 0;
63
+ this.ensureUsageDir();
64
+ }
65
+
66
+
67
+
68
+ // Cost per 1M tokens [prompt, completion] (2026 Pricing)
69
+ //
70
+ // `promptTokens` is optional and only consulted by providers that publish a
71
+ // SECOND rate card above a context threshold. GPT-6 Astra is the first: past
72
+ // 272,000 input tokens the same request bills at $20 in / $75 out instead of
73
+ // $10 / $50. Ignoring that under-reported long-context runs by 2x — and
74
+ // under-reporting is the worse direction, because the user's real invoice
75
+ // then exceeds the cost shown by Causyne.
76
+ getPricing(promptTokens = 0) {
77
+ const provider = this.config.provider?.toLowerCase();
78
+ const model = this.config.model?.toLowerCase() || '';
79
+
80
+ if (provider === 'deepseek') {
81
+ if (model.includes('pro')) return [0.435, 0.87];
82
+ if (model.includes('flash')) return [0.14, 0.28];
83
+ return [0.14, 0.28]; // Default
84
+ }
85
+
86
+ // Meta Model API — Muse. Published rates per 1M tokens, read off the
87
+ // console: Standard $1.25 in / $4.25 out; Contributor $0.10 in / $0.20 out,
88
+ // which is the "up to 95% less than Standard" the tier advertises in
89
+ // exchange for training on your prompts and completions.
90
+ //
91
+ // 1.2 and 1.1 are NOT separately published. They fall back to the 1.3 rate
92
+ // of their own tier, which over-estimates if they are cheaper — the safe
93
+ // direction for something that stops a run when the budget is spent.
94
+ if (provider === 'muse' || provider === 'meta') {
95
+ if (model.includes('-contributor')) return [0.10, 0.20];
96
+ return [1.25, 4.25];
97
+ }
98
+
99
+ if (provider === 'qwen' || provider === 'dashscope') {
100
+ if (model.includes('max')) return [1.25, 3.75];
101
+ if (model.includes('plus')) return [0.32, 1.28];
102
+ if (model.includes('flash')) return [0.19, 1.13];
103
+ return [0.32, 1.28]; // Default
104
+ }
105
+
106
+ if (provider === 'openai') {
107
+ // GPT-6 Astra (2026-09-03). Two rate cards: $10/$50 up to 272K input
108
+ // tokens, $20/$75 for a request that crosses it — the tier applies to
109
+ // the WHOLE request, not just the overage.
110
+ if (model.includes('astra')) {
111
+ return promptTokens > ASTRA_LONG_CONTEXT_THRESHOLD ? [20.00, 75.00] : [10.00, 50.00];
112
+ }
113
+ if (model.includes('sol')) return [5.00, 30.00];
114
+ if (model.includes('terra')) return [2.50, 15.00];
115
+ if (model.includes('luna')) return [1.00, 1.00];
116
+ if (model.includes('mini')) return [0.75, 4.50];
117
+ return [5.00, 30.00]; // Default OpenAI (Sol)
118
+ }
119
+
120
+ if (provider === 'anthropic') {
121
+ // Mythos shares Fable's tier and price. Without its own branch it fell
122
+ // through to the Sonnet default and was billed at a FIFTH of its rate.
123
+ if (model.includes('fable') || model.includes('mythos')) return [10.00, 50.00];
124
+ if (model.includes('opus')) return [5.00, 25.00];
125
+ // Sonnet 4.6 is $3/$15; Sonnet 5 is $2/$10. Checked in that order
126
+ // because 'sonnet-4-6' also matches the generic 'sonnet' test below.
127
+ if (model.includes('sonnet-4-6')) return [3.00, 15.00];
128
+ if (model.includes('sonnet')) return [2.00, 10.00];
129
+ if (model.includes('haiku')) return [1.00, 5.00];
130
+ return [2.00, 10.00]; // Default Anthropic (Sonnet 5)
131
+ }
132
+
133
+ if (provider === 'gemini') {
134
+ if (model.includes('pro')) return [2.00, 12.00];
135
+ if (model.includes('flash-lite')) return [0.25, 1.50];
136
+ // 3.6 Flash holds 3.5's $1.50 input but cuts output from $9.00 to $7.50.
137
+ // Checked before the generic 'flash' test, which it would also match.
138
+ if (model.includes('3.6-flash') || model.includes('3-6-flash')) return [1.50, 7.50];
139
+ if (model.includes('flash')) return [1.50, 9.00];
140
+ return [1.50, 9.00]; // Default Gemini
141
+ }
142
+
143
+ // Local Ollama cost the user $0.00 in BYOK APIs
144
+ if (provider === 'ollama') {
145
+ return [0.00, 0.00];
146
+ }
147
+
148
+ return [5.00, 15.00]; // Unknown BYOK provider fallback
149
+ }
150
+
151
+ // Prompt-cache multipliers, applied against the base input price.
152
+ // Anthropic: cache read 0.1x, cache write (5m) 1.25x.
153
+ // OpenAI / xAI: cached input 0.5x. DeepSeek disk cache: roughly 0.1x.
154
+ cachedInputMultiplier() {
155
+ const provider = this.config.provider?.toLowerCase();
156
+ if (provider === 'anthropic') {
157
+ // Fable 5.1 reprices cache reads to $0.25/MTok against a $10 base — a
158
+ // 0.025x discount, not the family's usual 0.1x. Charging 0.1x here
159
+
160
+ // counter. Same shape as the Muse case below: the discount is a property
161
+ // of the model, not the vendor.
162
+ const model = this.config.model?.toLowerCase() || '';
163
+ if (model.includes('fable-5-1')) return 0.025;
164
+ return 0.1;
165
+ }
166
+ if (provider === 'deepseek') return 0.1;
167
+
168
+ // Muse is the one provider whose cache discount differs by MODEL, not just
169
+ // by vendor: $0.15 against $1.25 on Standard is 0.12x, while $0.002 against
170
+ // $0.10 on Contributor is 0.02x. Collapsing both into one vendor number
171
+ // would over-charge a Contributor run by six times on every cached token.
172
+ if (provider === 'muse' || provider === 'meta') {
173
+ const model = this.config.model?.toLowerCase() || '';
174
+ return model.includes('-contributor') ? 0.02 : 0.12;
175
+ }
176
+
177
+ // GPT-6 Astra publishes cached input at $1.00/MTok against a $10.00 base —
178
+ // a 90% discount, not the 0.5x the rest of this vendor falls back to.
179
+ // Found while verifying the getPricing() rows above; charging 0.5x
180
+ // overstated every cached Astra token FIVEFOLD.
181
+ if (provider === 'openai') {
182
+ const model = this.config.model?.toLowerCase() || '';
183
+ if (model.includes('astra')) return 0.1;
184
+ }
185
+
186
+ // 0.5x is the long-standing OpenAI/xAI cached-input discount. It is NOT
187
+ // verified against the GPT-5.6 family's current rate card; left as-is
188
+ // rather than guessed at, since overstating is the safer error direction
189
+ // for a number the user reads as a bill.
190
+ return 0.5;
191
+ }
192
+
193
+ /**
194
+ * @param {number} promptTokens Total input tokens (cached + uncached).
195
+ * @param {number} completionTokens Output tokens.
196
+ * @param {object} usageDetail Optional { cachedPromptTokens, cacheWriteTokens }.
197
+ */
198
+ calculateCost(promptTokens, completionTokens, usageDetail = null) {
199
+ // promptTokens decides the rate card on providers with a context tier.
200
+ const [promptPrice, completionPrice] = this.getPricing(promptTokens);
201
+
202
+ const cachedTokens = Math.min(usageDetail?.cachedPromptTokens || 0, promptTokens);
203
+ const writeTokens = Math.min(usageDetail?.cacheWriteTokens || 0, promptTokens - cachedTokens);
204
+ const fullPriceTokens = Math.max(0, promptTokens - cachedTokens - writeTokens);
205
+
206
+ const writeMultiplier = usageDetail?.cacheWriteMultiplier || 1.25;
207
+ const promptCost =
208
+ (fullPriceTokens / 1_000_000) * promptPrice +
209
+ (cachedTokens / 1_000_000) * promptPrice * this.cachedInputMultiplier() +
210
+ (writeTokens / 1_000_000) * promptPrice * writeMultiplier;
211
+
212
+ const completionCost = (completionTokens / 1_000_000) * completionPrice;
213
+ return promptCost + completionCost;
214
+ }
215
+
216
+ // Cost per item (image) or per second (video_animation) in USD (2026 Pricing)
217
+ getMediaPricing(provider = '', model = '', mediaType = 'image') {
218
+ const p = provider.toLowerCase();
219
+ const m = model.toLowerCase();
220
+
221
+ // Muse Image is a flat per-image rate; there is no video model in the
222
+ // family, so a video request must not silently bill at the image rate.
223
+ if (p === 'muse' || p === 'meta') {
224
+ if (mediaType === 'video_animation') return 0;
225
+ return 0.01; // $0.01 per image
226
+ }
227
+
228
+ if (p === 'gemini') {
229
+ if (mediaType === 'video_animation' || m.includes('veo')) return 0.05; // $0.05 per second of Veo 2.0 video
230
+ if (m.includes('imagen')) return 0.03; // $0.03 per image
231
+ return 0.03;
232
+ }
233
+ if (p === 'openai') {
234
+ if (mediaType === 'video_animation' || m.includes('sora')) return 0.10; // $0.10 per second
235
+ if (m.includes('dall-e-3')) return 0.08; // $0.08 per HD image
236
+ return 0.04; // $0.04 per standard image
237
+ }
238
+ if (p === 'qwen' || p === 'dashscope') {
239
+ if (mediaType === 'video_animation' || m.includes('wanx') || m.includes('wan2')) return 0.04; // $0.04 per second
240
+ return 0.02; // $0.02 per image
241
+ }
242
+ if (p === 'pollinations' || p === 'local') {
243
+ return 0.00; // Free Mode D / Mode A
244
+ }
245
+ return 0.03; // Default generic media cost
246
+ }
247
+
248
+ async trackMediaUsage(provider, mediaType, model, quantityUnits = 1, onWarning = null) {
249
+ this.config = loadConfig();
250
+ this.budget = this.config.tokenBudget || { enabled: false };
251
+
252
+ const unitPrice = this.getMediaPricing(provider, model, mediaType);
253
+ const cost = unitPrice * (quantityUnits || 1);
254
+
255
+ // Multimedia Equivalent Units (MEU): convert dollar cost to equivalent tokens ($0.000002/token ~ $2/1M tokens)
256
+ const equivalentTokens = Math.round(cost / 0.000002);
257
+
258
+ this.taskTokens += equivalentTokens;
259
+ this.taskCostUsd += cost;
260
+
261
+
262
+
263
+ const daily = await this.getDailyUsage();
264
+ daily.totalTokens += equivalentTokens;
265
+ daily.totalCalls += 1;
266
+ daily.totalCostUsd = (daily.totalCostUsd || 0) + cost;
267
+ await this.saveDailyUsage(daily);
268
+
269
+ const cumulative = await this.getCumulativeUsage();
270
+ cumulative.totalTokens += equivalentTokens;
271
+ cumulative.totalCalls += 1;
272
+ cumulative.totalCostUsd = (cumulative.totalCostUsd || 0) + cost;
273
+ await this.saveCumulativeUsage(cumulative);
274
+
275
+ if (!this.budget.enabled) {
276
+ return { allowed: true, used: daily.totalTokens, costUsd: daily.totalCostUsd, taskCostUsd: this.taskCostUsd, equivalentTokens };
277
+ }
278
+
279
+ // Monetary Kill Switch
280
+ if (this.budget.maxCostUsd && daily.totalCostUsd > this.budget.maxCostUsd) {
281
+ if (this.budget.hardStop) {
282
+ return {
283
+ allowed: false,
284
+ reason: `BYOK Monetary Kill-Switch Activated: Media generation cost ($${cost.toFixed(2)}) pushes total spend to $${daily.totalCostUsd.toFixed(2)}, exceeding your $${this.budget.maxCostUsd.toFixed(2)} limit.`,
285
+ used: daily.totalTokens,
286
+ costUsd: daily.totalCostUsd,
287
+ taskCostUsd: this.taskCostUsd,
288
+ equivalentTokens
289
+ };
290
+ }
291
+ }
292
+
293
+ await this.checkAndTriggerAlerts(daily, onWarning);
294
+ return { allowed: true, used: daily.totalTokens, costUsd: daily.totalCostUsd, taskCostUsd: this.taskCostUsd, equivalentTokens };
295
+ }
296
+
297
+ async checkAndTriggerAlerts(daily, onWarning = null) {
298
+ if (!this.budget.enabled) return;
299
+
300
+ const alertThreshold = this.budget.alertThresholdTokens
301
+ ? Number(this.budget.alertThresholdTokens)
302
+ : (this.budget.maxTokensPerDay && this.budget.warningThreshold ? this.budget.maxTokensPerDay * this.budget.warningThreshold : null);
303
+
304
+ if (alertThreshold && alertThreshold > 0 && daily.totalTokens >= alertThreshold && !daily.alertSent) {
305
+ daily.alertSent = true;
306
+ await this.saveDailyUsage(daily);
307
+
308
+ const msg = `⚠️ Token Budget Alert: Your BYOK usage has reached ${daily.totalTokens.toLocaleString()} tokens (Threshold: ${alertThreshold.toLocaleString()}).`;
309
+ if (onWarning) {
310
+ onWarning(msg);
311
+ }
312
+ }
313
+ }
314
+
315
+ ensureUsageDir() {
316
+ if (!fs.existsSync(USAGE_DIR)) {
317
+ fs.mkdirSync(USAGE_DIR, { recursive: true });
318
+ }
319
+ }
320
+
321
+ getDailyFile() {
322
+ return path.join(USAGE_DIR, `${localDateKey()}.json`);
323
+ }
324
+
325
+ getCumulativeFile() {
326
+ return path.join(USAGE_DIR, 'byok_cumulative.json');
327
+ }
328
+
329
+
330
+ async getDailyUsage() {
331
+ const file = this.getDailyFile();
332
+ if (fs.existsSync(file)) {
333
+ try {
334
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
335
+ } catch(e) {}
336
+ }
337
+ return { date: localDateKey(), totalTokens: 0, totalCalls: 0, totalCostUsd: 0 };
338
+ }
339
+
340
+ async getCumulativeUsage() {
341
+ const file = this.getCumulativeFile();
342
+ if (fs.existsSync(file)) {
343
+ try {
344
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
345
+ } catch(e) {}
346
+ }
347
+
348
+ // Automatic Historical Backfill Migration (v1.1.50):
349
+ // Scan all past `${date}.json` files across USAGE_DIR and sum them up so past 5M+ tokens are never lost!
350
+ let totalTokens = 0;
351
+ let totalCalls = 0;
352
+ let totalCostUsd = 0;
353
+ try {
354
+ if (fs.existsSync(USAGE_DIR)) {
355
+ const files = fs.readdirSync(USAGE_DIR);
356
+ for (const f of files) {
357
+ if (/^\d{4}-\d{2}-\d{2}\.json$/.test(f)) {
358
+ try {
359
+ const data = JSON.parse(fs.readFileSync(path.join(USAGE_DIR, f), 'utf8'));
360
+ totalTokens += (data.totalTokens || 0);
361
+ totalCalls += (data.totalCalls || 0);
362
+ totalCostUsd += (data.totalCostUsd || 0);
363
+ } catch (err) {}
364
+ }
365
+ }
366
+ }
367
+ } catch (err) {}
368
+
369
+ const initialCumulative = { totalTokens, totalCalls, totalCostUsd, backfilled: true, lastUpdated: new Date().toISOString() };
370
+ try {
371
+ writeUsageFile(file, initialCumulative);
372
+ } catch (err) {}
373
+ return initialCumulative;
374
+ }
375
+
376
+
377
+ async saveDailyUsage(usage) {
378
+ writeUsageFile(this.getDailyFile(), usage);
379
+ }
380
+
381
+ async saveCumulativeUsage(usage) {
382
+ writeUsageFile(this.getCumulativeFile(), usage);
383
+ }
384
+
385
+
386
+ async resetDailyUsage() {
387
+ const file = this.getDailyFile();
388
+ if (fs.existsSync(file)) {
389
+ fs.unlinkSync(file);
390
+ }
391
+ // DO NOT reset cumulative usage here, keeping lifetime tracking separate from fresh budget.
392
+ }
393
+
394
+ async resetCumulativeUsage() {
395
+ const file = this.getCumulativeFile();
396
+ if (fs.existsSync(file)) {
397
+ fs.unlinkSync(file);
398
+ }
399
+ }
400
+
401
+ async getReportedUsage() {
402
+ const daily = await this.getDailyUsage();
403
+ const cumulative = await this.getCumulativeUsage();
404
+ return {
405
+ usedTokens: daily.totalTokens, // UI budget tracks against daily "Fresh" setup
406
+ dailyTokens: daily.totalTokens,
407
+ taskTokens: this.taskTokens,
408
+ lifetimeTokens: cumulative.totalTokens,
409
+ costUsd: daily.totalCostUsd, // UI estimated cost tracks against daily "Fresh" setup
410
+ taskCostUsd: this.taskCostUsd,
411
+ lifetimeCostUsd: cumulative.totalCostUsd,
412
+ isDemo: false
413
+ };
414
+ }
415
+
416
+ /**
417
+ * Can the loop afford ANOTHER step without breaching the task ceiling?
418
+ *
419
+ * WHY THIS EXISTS. trackUsage() checks `taskTokens > maxTokensPerTask` AFTER
420
+ * a call has already been paid for, so the run always overshoots by one step.
421
+ * With ~9,000-token steps a 299,000 ceiling lands at ~308,000 — which breaks
422
+ * the only promise a token cap makes. Asked BEFORE the call, using the
423
+ * largest step seen so far, the ceiling actually holds.
424
+ *
425
+ * The largest step rather than the average, deliberately: a cap that holds
426
+ * on average is a cap that fails on the step that matters.
427
+ *
428
+ * @returns {{allowed:boolean, reason?:string, used:number, limit:number, projected:number}}
429
+ */
430
+ canAffordAnotherStep() {
431
+ const limit = this.budget && this.budget.enabled && this.budget.hardStop
432
+ ? Number(this.budget.maxTokensPerTask || 0)
433
+ : 0;
434
+ if (!limit) return { allowed: true, used: this.taskTokens, limit: 0, projected: this.taskTokens };
435
+
436
+ // Before the first step there is nothing to extrapolate from; let it run.
437
+ const step = this.largestStepTokens || 0;
438
+ const projected = this.taskTokens + step;
439
+ if (step > 0 && projected > limit) {
440
+ return {
441
+ allowed: false,
442
+ reason: `Stopping before the next step would breach the task ceiling: ${this.taskTokens.toLocaleString()} used, `
443
+ + `the largest step so far was ${step.toLocaleString()}, and ${projected.toLocaleString()} would exceed the ${limit.toLocaleString()} limit.`,
444
+ used: this.taskTokens, limit, projected
445
+ };
446
+ }
447
+ return { allowed: true, used: this.taskTokens, limit, projected };
448
+ }
449
+
450
+ resetTask() {
451
+ this.taskTokens = 0;
452
+ this.taskCostUsd = 0;
453
+ this.largestStepTokens = 0;
454
+ }
455
+
456
+
457
+ settle(extra, daily, cumulative) {
458
+ return {
459
+ // Daily scope — what the budget bar and the daily limiter both measure.
460
+ usedTokens: daily.totalTokens,
461
+ dailyTokens: daily.totalTokens,
462
+ dailyCostUsd: daily.totalCostUsd || 0,
463
+ dailyLimit: Number(this.budget && this.budget.maxTokensPerDay) || 0,
464
+ // Task scope — what this run has spent.
465
+ taskTokens: this.taskTokens,
466
+ taskCostUsd: this.taskCostUsd,
467
+ taskLimit: Number(this.budget && this.budget.maxTokensPerTask) || 0,
468
+ // Lifetime scope.
469
+ lifetimeTokens: cumulative.totalTokens,
470
+ lifetimeCostUsd: cumulative.totalCostUsd,
471
+ // Retained for clients built against the older frame. It has always been
472
+ // the daily cost on the allowed path, so it keeps that meaning here
473
+ // rather than acquiring a third one.
474
+ costUsd: daily.totalCostUsd || 0,
475
+ used: this.taskTokens,
476
+ ...extra
477
+ };
478
+ }
479
+
480
+ async trackUsage(promptTokens, completionTokens, onWarning = null, usageDetail = null) {
481
+ // Dynamically reload config to apply live budget updates from UI
482
+ this.config = loadConfig();
483
+ this.budget = this.config.tokenBudget || { enabled: false };
484
+
485
+ const total = (promptTokens || 0) + (completionTokens || 0);
486
+ this.taskTokens += total;
487
+ // Feeds canAffordAnotherStep(). The LARGEST step, not the average: a
488
+ // ceiling that holds on average is a ceiling that fails on the step that
489
+ // matters, and every step is bigger than the last as the window grows.
490
+ this.largestStepTokens = Math.max(this.largestStepTokens || 0, total);
491
+
492
+ const cost = this.calculateCost(promptTokens, completionTokens, usageDetail);
493
+ this.taskCostUsd += cost;
494
+
495
+
496
+
497
+ // Normal BYOK Tracking.
498
+ //
499
+ // Read-modify-write, so it runs inside the lock. Outside it, a condenser
500
+ // pass firing concurrently with the loop silently subtracts its own tokens
501
+ // from the day's total.
502
+ const { daily, cumulative } = await withUsageLock(async () => {
503
+ const daily = await this.getDailyUsage();
504
+ daily.totalTokens += total;
505
+ daily.totalCalls += 1;
506
+ daily.totalCostUsd = (daily.totalCostUsd || 0) + cost;
507
+ await this.saveDailyUsage(daily);
508
+
509
+ const cumulative = await this.getCumulativeUsage();
510
+ cumulative.totalTokens += total;
511
+ cumulative.totalCalls += 1;
512
+ cumulative.totalCostUsd = (cumulative.totalCostUsd || 0) + cost;
513
+ await this.saveCumulativeUsage(cumulative);
514
+
515
+ return { daily, cumulative };
516
+ });
517
+
518
+ if (!this.budget.enabled) {
519
+ return this.settle({ allowed: true }, daily, cumulative);
520
+ }
521
+
522
+ // BYOK Monetary Kill Switch
523
+ if (this.budget.maxCostUsd && daily.totalCostUsd > this.budget.maxCostUsd) {
524
+ if (this.budget.hardStop) {
525
+ return this.settle({
526
+ allowed: false,
527
+ reason: `BYOK Monetary Kill-Switch Activated: You have spent $${daily.totalCostUsd.toFixed(2)}, which exceeds your $${this.budget.maxCostUsd.toFixed(2)} limit.`
528
+ }, daily, cumulative);
529
+ }
530
+ }
531
+
532
+ // Check task budget
533
+ if (this.budget.maxTokensPerTask && this.taskTokens > this.budget.maxTokensPerTask) {
534
+ if (this.budget.hardStop) {
535
+ return this.settle({
536
+ allowed: false,
537
+ reason: `Task token budget reached: ${this.taskTokens.toLocaleString()} of ${Number(this.budget.maxTokensPerTask).toLocaleString()} tokens for this task.`,
538
+ limit: this.budget.maxTokensPerTask
539
+ }, daily, cumulative);
540
+ }
541
+ }
542
+
543
+ // Check daily budget. This limit was previously accepted from the UI and
544
+ // used only to derive an alert threshold — it never actually stopped
545
+ // anything, so a runaway task could blow straight through it.
546
+ if (this.budget.maxTokensPerDay && daily.totalTokens > this.budget.maxTokensPerDay) {
547
+ if (this.budget.hardStop) {
548
+ return this.settle({
549
+ allowed: false,
550
+ reason: `Daily token budget reached: ${daily.totalTokens.toLocaleString()} of ${Number(this.budget.maxTokensPerDay).toLocaleString()} tokens used today.`,
551
+ limit: this.budget.maxTokensPerDay
552
+ }, daily, cumulative);
553
+ }
554
+ }
555
+
556
+ await this.checkAndTriggerAlerts(daily, onWarning);
557
+
558
+ return this.settle({ allowed: true }, daily, cumulative);
559
+ }
560
+ }
@@ -0,0 +1,39 @@
1
+ on run argv
2
+ set targetApp to item 1 of argv
3
+ set maxNodes to (item 2 of argv) as integer
4
+ set out to ""
5
+ set n to 0
6
+ tell application "System Events"
7
+ if targetApp is "" then
8
+ set proc to first application process whose frontmost is true
9
+ else
10
+ set proc to first application process whose name is targetApp
11
+ end if
12
+ set out to "app|" & (name of proc) & linefeed
13
+ repeat with w in (windows of proc)
14
+ if n is greater than or equal to maxNodes then exit repeat
15
+ set out to out & "window|" & (name of w) & "||" & linefeed
16
+ set n to n + 1
17
+ repeat with e in (entire contents of w)
18
+ if n is greater than or equal to maxNodes then exit repeat
19
+ set r to ""
20
+ set t to ""
21
+ set v to ""
22
+ try
23
+ set r to (class of e) as string
24
+ end try
25
+ try
26
+ set t to (name of e) as string
27
+ end try
28
+ try
29
+ set v to (value of e) as string
30
+ end try
31
+ if t is not "" or v is not "" then
32
+ set out to out & r & "|" & t & "|" & v & linefeed
33
+ set n to n + 1
34
+ end if
35
+ end repeat
36
+ end repeat
37
+ end tell
38
+ return out
39
+ end run
@@ -0,0 +1,10 @@
1
+ Add-Type -AssemblyName UIAutomationClient,UIAutomationTypes
2
+ $max = [int]$args[0]
3
+ $root = [System.Windows.Automation.AutomationElement]::RootElement
4
+ $cond = [System.Windows.Automation.Condition]::TrueCondition
5
+ $n = 0
6
+ foreach ($e in $root.FindAll([System.Windows.Automation.TreeScope]::Descendants, $cond)) {
7
+ if ($n -ge $max) { break }
8
+ $name = $e.Current.Name
9
+ if ($name) { Write-Output ("" + $e.Current.ControlType.ProgrammaticName + "|" + $name + "|"); $n++ }
10
+ }
@@ -0,0 +1,58 @@
1
+ on run argv
2
+ set appName to item 1 of argv
3
+ set report to ""
4
+ tell application "System Events"
5
+ if appName is not "" then
6
+ set proc to first application process whose name is appName
7
+ set frontmost of proc to true
8
+ delay 0.2
9
+ else
10
+ set proc to first application process whose frontmost is true
11
+ end if
12
+ repeat with i from 2 to (count of argv)
13
+ set step to item i of argv
14
+ set AppleScript's text item delimiters to "::"
15
+ set parts to text items of step
16
+ set AppleScript's text item delimiters to ""
17
+ set verb to item 1 of parts
18
+ if verb is "press" then
19
+ set elemName to item 2 of parts
20
+ set hit to false
21
+ repeat with w in (windows of proc)
22
+ repeat with e in (entire contents of w)
23
+ if hit is false then
24
+ try
25
+ if (name of e) is elemName then
26
+ perform action "AXPress" of e
27
+ set hit to true
28
+ end if
29
+ end try
30
+ end if
31
+ end repeat
32
+ end repeat
33
+ if hit then
34
+ set report to report & "ok|press|" & elemName & linefeed
35
+ else
36
+ set report to report & "miss|press|" & elemName & linefeed
37
+ end if
38
+ else if verb is "clickxy" then
39
+ set px to (item 2 of parts) as integer
40
+ set py to (item 3 of parts) as integer
41
+ click at {px, py}
42
+ set report to report & "ok|clickxy|" & px & "," & py & linefeed
43
+ else if verb is "type" then
44
+ keystroke (item 2 of parts)
45
+ set report to report & "ok|type|" & (length of (item 2 of parts)) & " chars" & linefeed
46
+ else if verb is "key" then
47
+ key code ((item 2 of parts) as integer)
48
+ set report to report & "ok|key|" & (item 2 of parts) & linefeed
49
+ else if verb is "wait" then
50
+ delay ((item 2 of parts) as number)
51
+ set report to report & "ok|wait|" & (item 2 of parts) & linefeed
52
+ else
53
+ set report to report & "err|unknown verb|" & verb & linefeed
54
+ end if
55
+ end repeat
56
+ end tell
57
+ return report
58
+ end run