@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,105 @@
1
+ // Deciding whether an argv is "the bare command" or "a subcommand".
2
+ //
3
+ // This existed as `process.argv.length <= 2` in entrypoint.js, which is right
4
+ // only for the exact string `causyne`. The moment a root option is added — and
5
+ // `--dev-emulator` is already one on five subcommands — `causyne --dev-emulator`
6
+ // stops being bare and falls through to Commander, which reports it as an
7
+ // invalid command. The user typed the product's own flag and was told the
8
+ // product does not exist.
9
+ //
10
+ // So the classification walks argv properly: consume declared root options
11
+ // together with their values, then look at what bare words are left. Zero words
12
+ // and no help/version request means the user asked for the application itself.
13
+ //
14
+ // Two rules keep this conservative, because getting it wrong in the other
15
+ // direction is worse — swallowing a real subcommand into the home screen would
16
+ // make the CLI unscriptable:
17
+ //
18
+ // 1. An option this table does not know about is NOT bare. `causyne --nonsense`
19
+ // goes to Commander so Commander can report it, exactly as it does today.
20
+ // 2. `--` ends the scan. Anything after it is operands, not a command path.
21
+ //
22
+ // Modelled on openclaw's getRootOptionAwareCommandPath, which exists for the
23
+ // same reason: `openclaw --profile foo` must still launch the TUI.
24
+
25
+ /**
26
+ * Root options, and how many argv slots each consumes AFTER itself.
27
+ *
28
+ * Only options accepted BEFORE a subcommand belong here. A flag declared on a
29
+ * subcommand (`causyne start --dev-emulator`) is Commander's business — it is
30
+ * never seen by this walker, because the bare word `start` stops the scan.
31
+ */
32
+ export const ROOT_OPTIONS = new Map([
33
+ ['-h', 0],
34
+ ['--help', 0],
35
+ ['-V', 0],
36
+ ['--version', 0],
37
+ ['--dev-emulator', 0]
38
+ ]);
39
+
40
+ /** Options that mean "print something and exit", never "launch the app". */
41
+ const HELP_OR_VERSION = new Set(['-h', '--help', '-V', '--version']);
42
+
43
+ /**
44
+ * Classify a full process.argv.
45
+ *
46
+ * @returns {{commandPath: string[], primary: string|null, hasHelpOrVersion: boolean,
47
+ * rootOptions: string[], unknownOption: string|null, isBareRoot: boolean}}
48
+ */
49
+ export function resolveInvocation(argv = []) {
50
+ const tokens = Array.isArray(argv) ? argv.slice(2) : [];
51
+ const commandPath = [];
52
+ const rootOptions = [];
53
+ let hasHelpOrVersion = false;
54
+ let unknownOption = null;
55
+
56
+ for (let i = 0; i < tokens.length; i++) {
57
+ const token = String(tokens[i]);
58
+
59
+ // Everything past `--` is an operand by POSIX convention. Stop looking for
60
+ // a command name; whatever we have collected is the answer.
61
+ if (token === '--') break;
62
+
63
+ if (token.startsWith('-') && token !== '-') {
64
+ // `--opt=value` carries its value inline, so it consumes no extra slot.
65
+ const eq = token.indexOf('=');
66
+ const name = eq === -1 ? token : token.slice(0, eq);
67
+
68
+ if (HELP_OR_VERSION.has(name)) {
69
+ hasHelpOrVersion = true;
70
+ rootOptions.push(name);
71
+ continue;
72
+ }
73
+
74
+ if (ROOT_OPTIONS.has(name)) {
75
+ rootOptions.push(name);
76
+ // Skip the value only when it was not written inline.
77
+ if (eq === -1) i += ROOT_OPTIONS.get(name);
78
+ continue;
79
+ }
80
+
81
+ // Rule 1: an option we do not recognise is Commander's to reject. Record
82
+ // the first one so the caller can explain itself if it wants to.
83
+ if (unknownOption === null) unknownOption = name;
84
+ continue;
85
+ }
86
+
87
+ commandPath.push(token);
88
+ // One bare word is all that is needed to know this is not the bare command.
89
+ break;
90
+ }
91
+
92
+ return {
93
+ commandPath,
94
+ primary: commandPath.length > 0 ? commandPath[0] : null,
95
+ hasHelpOrVersion,
96
+ rootOptions,
97
+ unknownOption,
98
+ isBareRoot: commandPath.length === 0 && !hasHelpOrVersion && unknownOption === null
99
+ };
100
+ }
101
+
102
+ /** The one question entrypoint.js asks: should this argv open the home screen? */
103
+ export function isBareRoot(argv = []) {
104
+ return resolveInvocation(argv).isBareRoot;
105
+ }
@@ -0,0 +1,443 @@
1
+ // Keypress decoding — raw stdin bytes to named key events.
2
+ //
3
+ // `inquirer` gave the chat one blocking line of input and nothing else: no
4
+ // history, no completion, no way to type while the agent was working. Replacing
5
+ // it means owning the keyboard, and owning the keyboard starts here.
6
+ //
7
+ // This module is deliberately pure: bytes in, events out, no I/O and no state
8
+ // beyond the partial sequence still in flight. That is what makes it testable
9
+ // by feeding it the exact bytes a terminal sends.
10
+ //
11
+ // Pure Node. No dependency, no shell.
12
+
13
+ // Bracketed paste. Terminals wrap pasted text in these when we ask them to, and
14
+ // that is the only reliable way to tell "the user pressed Enter" apart from
15
+ // "the user pasted a block that happens to contain newlines".
16
+ export const PASTE_START = '\x1b[200~';
17
+ export const PASTE_END = '\x1b[201~';
18
+
19
+ // CSI parameter-1 modifier bitmask, minus one: 1=shift, 2=alt, 4=ctrl.
20
+ const MOD_SHIFT = 1;
21
+ const MOD_ALT = 2;
22
+ const MOD_CTRL = 4;
23
+
24
+ const CSI_FINAL_NAMES = {
25
+ A: 'up', B: 'down', C: 'right', D: 'left',
26
+ H: 'home', F: 'end', E: 'clear', Z: 'tab'
27
+ };
28
+
29
+ const CSI_TILDE_NAMES = {
30
+ 1: 'home', 2: 'insert', 3: 'delete', 4: 'end',
31
+ 5: 'pageup', 6: 'pagedown', 7: 'home', 8: 'end',
32
+ 11: 'f1', 12: 'f2', 13: 'f3', 14: 'f4', 15: 'f5',
33
+ 17: 'f6', 18: 'f7', 19: 'f8', 20: 'f9', 21: 'f10',
34
+ 23: 'f11', 24: 'f12'
35
+ };
36
+
37
+ function makeKey(name, extra = {}) {
38
+ return {
39
+ name,
40
+ ctrl: false,
41
+ alt: false,
42
+ shift: false,
43
+ sequence: '',
44
+ text: '',
45
+ ...extra
46
+ };
47
+ }
48
+
49
+ function applyModifier(key, modifier) {
50
+ const bits = Number.isFinite(modifier) ? modifier - 1 : 0;
51
+ if (bits & MOD_SHIFT) key.shift = true;
52
+ if (bits & MOD_ALT) key.alt = true;
53
+ if (bits & MOD_CTRL) key.ctrl = true;
54
+ return key;
55
+ }
56
+
57
+ /**
58
+ * How many bytes of a UTF-8 character start at `offset`, or 0 when the
59
+ * character is cut short by the end of the buffer.
60
+ *
61
+ * A multi-byte character can straddle two stdin chunks. Decoding the buffer
62
+ * eagerly would turn one emoji into two replacement characters, so the tail is
63
+ * held back until the rest arrives.
64
+ */
65
+ function utf8Length(buffer, offset) {
66
+ const byte = buffer[offset];
67
+ let needed = 1;
68
+ if (byte >= 0xf0) needed = 4;
69
+ else if (byte >= 0xe0) needed = 3;
70
+ else if (byte >= 0xc0) needed = 2;
71
+ return offset + needed <= buffer.length ? needed : 0;
72
+ }
73
+
74
+ /**
75
+ * Decoder for one input stream.
76
+ *
77
+ * Holds only the bytes of a sequence that has not finished arriving. Everything
78
+ * else is derived, so a decoder can be thrown away and remade at any time.
79
+ */
80
+ export class KeyDecoder {
81
+ constructor() {
82
+ this.pending = Buffer.alloc(0);
83
+ this.inPaste = false;
84
+ this.pasteBuffer = '';
85
+ }
86
+
87
+ /** Feed a chunk; get back every complete key event it contained. */
88
+ decode(chunk) {
89
+ const incoming = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), 'utf8');
90
+ this.pending = this.pending.length ? Buffer.concat([this.pending, incoming]) : incoming;
91
+
92
+ const events = [];
93
+ let offset = 0;
94
+
95
+ while (offset < this.pending.length) {
96
+ const consumed = this.step(this.pending, offset, events);
97
+ // 0 means "the rest of this sequence has not arrived yet".
98
+ if (consumed === 0) break;
99
+ offset += consumed;
100
+ }
101
+
102
+ this.pending = this.pending.subarray(offset);
103
+ return events;
104
+ }
105
+
106
+ /** Decode one event at `offset`. Returns bytes consumed, or 0 if incomplete. */
107
+ step(buffer, offset, events) {
108
+ // Inside a paste, everything is literal text until the closing marker. The
109
+ // terminal guarantees the marker cannot appear in pasted content.
110
+ if (this.inPaste) return this.stepPaste(buffer, offset, events);
111
+
112
+ const byte = buffer[offset];
113
+
114
+ if (byte === 0x1b) return this.stepEscape(buffer, offset, events);
115
+
116
+ if (byte === 0x0d || byte === 0x0a) {
117
+ events.push(makeKey('enter', { sequence: String.fromCharCode(byte) }));
118
+ return 1;
119
+ }
120
+ if (byte === 0x7f || byte === 0x08) {
121
+ events.push(makeKey('backspace', { sequence: String.fromCharCode(byte) }));
122
+ return 1;
123
+ }
124
+ if (byte === 0x09) {
125
+ events.push(makeKey('tab', { sequence: '\t' }));
126
+ return 1;
127
+ }
128
+
129
+ // C0 controls are Ctrl+letter, with the handful above already claimed.
130
+ if (byte < 0x20) {
131
+ events.push(makeKey(String.fromCharCode(byte + 96), {
132
+ ctrl: true,
133
+ sequence: String.fromCharCode(byte)
134
+ }));
135
+ return 1;
136
+ }
137
+
138
+ const length = utf8Length(buffer, offset);
139
+ if (length === 0) return 0;
140
+
141
+ const text = buffer.toString('utf8', offset, offset + length);
142
+ events.push(makeKey('char', { sequence: text, text }));
143
+ return length;
144
+ }
145
+
146
+ stepPaste(buffer, offset, events) {
147
+ const text = buffer.toString('utf8', offset);
148
+ const end = text.indexOf(PASTE_END);
149
+
150
+ if (end === -1) {
151
+ // No terminator yet. Keep whole UTF-8 characters, hold back a split tail.
152
+ let safe = buffer.length;
153
+ while (safe > offset && utf8Length(buffer, safe - 1) === 0) safe--;
154
+ // A partial terminator at the tail must not be swallowed as content.
155
+ const chunk = buffer.toString('utf8', offset, safe);
156
+ const keep = partialSuffixLength(chunk, PASTE_END);
157
+ const usable = chunk.slice(0, chunk.length - keep);
158
+ if (!usable) return 0;
159
+ this.pasteBuffer += usable;
160
+ return Buffer.byteLength(usable, 'utf8');
161
+ }
162
+
163
+ this.pasteBuffer += text.slice(0, end);
164
+ this.inPaste = false;
165
+ const pasted = this.pasteBuffer;
166
+ this.pasteBuffer = '';
167
+ events.push(makeKey('paste', { text: pasted, sequence: pasted }));
168
+ return Buffer.byteLength(text.slice(0, end), 'utf8') + PASTE_END.length;
169
+ }
170
+
171
+ stepEscape(buffer, offset, events) {
172
+ const remaining = buffer.length - offset;
173
+
174
+ // A lone ESC at the end of a chunk is the Escape key. Terminals deliver
175
+ // escape sequences atomically in practice, so waiting for more would make
176
+ // Escape feel broken rather than making anything more correct.
177
+ if (remaining === 1) {
178
+ events.push(makeKey('escape', { sequence: '\x1b' }));
179
+ return 1;
180
+ }
181
+
182
+ const next = buffer[offset + 1];
183
+
184
+ if (next === 0x5b) return this.stepCSI(buffer, offset, events); // ESC [
185
+ if (next === 0x4f) return this.stepSS3(buffer, offset, events); // ESC O
186
+
187
+ // ESC + key is Alt+key. ESC ESC is Escape pressed twice.
188
+ if (next === 0x1b) {
189
+ events.push(makeKey('escape', { sequence: '\x1b' }));
190
+ return 1;
191
+ }
192
+ if (next === 0x0d || next === 0x0a) {
193
+ events.push(makeKey('enter', { alt: true, sequence: '\x1b\r' }));
194
+ return 2;
195
+ }
196
+ if (next === 0x7f || next === 0x08) {
197
+ events.push(makeKey('backspace', { alt: true, sequence: '\x1b\x7f' }));
198
+ return 2;
199
+ }
200
+
201
+ // ESC + a control byte is Alt+Ctrl+letter.
202
+ //
203
+ // Without this the pair came out as a key literally NAMED \x15, which no
204
+ // binding can match — so every ctrl+alt chord worked only under the kitty
205
+ // keyboard protocol (`ESC [ 117 ; 7 u`) and silently did nothing in every
206
+ // ordinary terminal. That is how the half-page scroll bindings shipped
207
+ // firing on almost nobody.
208
+ //
209
+ // 0x00 is excluded: ctrl+space and ctrl+@ both send NUL and there is no way
210
+ // to tell them apart, so naming it would be a guess.
211
+ if (next > 0x00 && next < 0x20) {
212
+ events.push(makeKey(String.fromCharCode(next + 96), {
213
+ ctrl: true, alt: true, sequence: '\x1b' + String.fromCharCode(next)
214
+ }));
215
+ return 2;
216
+ }
217
+
218
+ const length = utf8Length(buffer, offset + 1);
219
+ if (length === 0) return 0;
220
+ const text = buffer.toString('utf8', offset + 1, offset + 1 + length);
221
+ events.push(makeKey(text.toLowerCase(), { alt: true, sequence: '\x1b' + text }));
222
+ return 1 + length;
223
+ }
224
+
225
+ stepCSI(buffer, offset, events) {
226
+ // X10 mouse first, and from the RAW buffer.
227
+ //
228
+ // `ESC [ M Cb Cx Cy` carries three bytes that are offset by 32 and are
229
+ // routinely >127 — i.e. not valid UTF-8. Decoding the buffer as UTF-8 first
230
+ // would mangle them into replacement characters, and the generic CSI scan
231
+ // below would stop at the `M`, consume three bytes and leave the other three
232
+ // to be read as text the user never typed.
233
+ //
234
+ // We never ask for this encoding: mouseTracking() sends ?1006 alongside
235
+ // ?1000. But a terminal that does not understand ?1006 silently ignores it
236
+ // and answers in X10 anyway, and "silently ignores" is exactly the failure
237
+ // mode UX-01 exists because of.
238
+ if (buffer[offset + 2] === 0x4d && !this.inPaste) {
239
+ return this.stepX10Mouse(buffer, offset, events);
240
+ }
241
+
242
+ const text = buffer.toString('utf8', offset);
243
+
244
+ if (text.startsWith(PASTE_START)) {
245
+ this.inPaste = true;
246
+ this.pasteBuffer = '';
247
+ return PASTE_START.length;
248
+ }
249
+
250
+ // A CSI sequence is ESC [ , parameters, then one final byte in @..~.
251
+ let index = 2;
252
+ while (index < text.length && !/[@-~]/.test(text[index])) index++;
253
+ if (index >= text.length) return 0; // still arriving
254
+
255
+ const final = text[index];
256
+ const params = text.slice(2, index);
257
+ const sequence = text.slice(0, index + 1);
258
+ const numbers = params.split(/[;:]/).map((part) => parseInt(part, 10));
259
+
260
+ // SGR mouse: ESC [ < b ; x ; y M|m. `<` is 0x3C, outside the @..~ final
261
+ // range, so the scan above already found the right final byte.
262
+ if ((final === 'M' || final === 'm') && params.startsWith('<')) {
263
+ return this.emitMouse(params.slice(1).split(';').map((n) => parseInt(n, 10)),
264
+ final === 'M', sequence, events);
265
+ }
266
+
267
+ if (final === '~') {
268
+ const name = CSI_TILDE_NAMES[numbers[0]];
269
+ if (name) {
270
+ events.push(applyModifier(makeKey(name, { sequence }), numbers[1]));
271
+ return sequence.length;
272
+ }
273
+ } else if (final === 'u') {
274
+ // Kitty keyboard protocol: ESC [ codepoint ; modifiers u
275
+ const codepoint = numbers[0];
276
+ if (Number.isFinite(codepoint) && codepoint >= 32) {
277
+ const character = String.fromCodePoint(codepoint);
278
+ const key = applyModifier(
279
+ makeKey('char', { sequence, text: character }),
280
+ numbers[1]
281
+ );
282
+ // Ctrl+char is a command, not text — the caller must not insert it.
283
+ if (key.ctrl || key.alt) key.text = '';
284
+ if (key.name === 'char' && !key.text) key.name = character.toLowerCase();
285
+ events.push(key);
286
+ return sequence.length;
287
+ }
288
+ } else if (CSI_FINAL_NAMES[final]) {
289
+ const key = applyModifier(
290
+ makeKey(CSI_FINAL_NAMES[final], { sequence }),
291
+ numbers.length > 1 ? numbers[1] : undefined
292
+ );
293
+ // ESC [ Z is Shift+Tab and carries no modifier parameter of its own.
294
+ if (final === 'Z') key.shift = true;
295
+ events.push(key);
296
+ return sequence.length;
297
+ }
298
+
299
+ // Unknown but well-formed: consume it so it can never reach the buffer as
300
+ // literal text and corrupt the line the user is editing.
301
+ events.push(makeKey('unknown', { sequence }));
302
+ return sequence.length;
303
+ }
304
+
305
+ stepX10Mouse(buffer, offset, events) {
306
+ // ESC [ M plus exactly three bytes.
307
+ if (buffer.length - offset < 6) return 0;
308
+ const code = buffer[offset + 3] - 32;
309
+ const column = buffer[offset + 4] - 32;
310
+ const row = buffer[offset + 5] - 32;
311
+ const sequence = buffer.toString('latin1', offset, offset + 6);
312
+ this.emitMouse([code, column, row], true, sequence, events);
313
+ return 6;
314
+ }
315
+
316
+ /**
317
+ * One mouse report, in either encoding.
318
+ *
319
+ * Bit 6 of the button code marks a wheel event; the low two bits then give
320
+ * the direction. Everything else is a click, which is emitted as an inert
321
+ * `mouse` event rather than dropped: consuming it is what stops the bytes
322
+ * reaching the line the user is editing, and naming it lets a caller act on
323
+ * it later without another decoder change.
324
+ */
325
+ emitMouse(numbers, pressed, sequence, events) {
326
+ const [code, column, row] = numbers;
327
+ if (!Number.isFinite(code) || !Number.isFinite(column) || !Number.isFinite(row)) {
328
+ events.push(makeKey('unknown', { sequence }));
329
+ return sequence.length;
330
+ }
331
+
332
+ // Modifier bits live in the button code for mouse reports, not in a CSI
333
+ // parameter, so applyModifier() does not apply here.
334
+ const modifiers = {
335
+ shift: (code & 4) !== 0,
336
+ alt: (code & 8) !== 0,
337
+ ctrl: (code & 16) !== 0
338
+ };
339
+ // Wire coordinates are 1-based.
340
+ const at = { x: column - 1, y: row - 1 };
341
+
342
+ if ((code & 64) !== 0) {
343
+ const direction = ['up', 'down', 'left', 'right'][code & 3];
344
+ // A wheel report arrives as a press; there is no release to pair it with.
345
+ if (pressed) {
346
+ events.push(makeKey('wheel', { sequence, ...modifiers, wheel: { direction, ...at } }));
347
+ return sequence.length;
348
+ }
349
+ }
350
+
351
+ events.push(makeKey('mouse', {
352
+ sequence, ...modifiers,
353
+ mouse: { button: code & 3, pressed, ...at }
354
+ }));
355
+ return sequence.length;
356
+ }
357
+
358
+ stepSS3(buffer, offset, events) {
359
+ if (buffer.length - offset < 3) return 0;
360
+ const final = buffer.toString('utf8', offset + 2, offset + 3);
361
+ const name = CSI_FINAL_NAMES[final] || { P: 'f1', Q: 'f2', R: 'f3', S: 'f4' }[final];
362
+ events.push(makeKey(name || 'unknown', { sequence: '\x1bO' + final }));
363
+ return 3;
364
+ }
365
+ }
366
+
367
+ /**
368
+ * Length of the longest suffix of `text` that is a prefix of `marker`.
369
+ *
370
+ * A paste terminator can be split across two chunks; without this the leading
371
+ * bytes of the terminator would be appended to the pasted text as content and
372
+ * the terminator would then never match.
373
+ */
374
+ function partialSuffixLength(text, marker) {
375
+ const max = Math.min(text.length, marker.length - 1);
376
+ for (let length = max; length > 0; length--) {
377
+ if (marker.startsWith(text.slice(text.length - length))) return length;
378
+ }
379
+ return 0;
380
+ }
381
+
382
+ /**
383
+ * Turn mouse reporting on/off.
384
+ *
385
+ * ?1000 is button press/release reporting, which is what carries the wheel.
386
+ * ?1006 asks for SGR encoding so the coordinates are decimal parameters rather
387
+ * than three offset bytes that break past column 223.
388
+ *
389
+ * DELIBERATELY NOT ?1002 (button-drag) or ?1003 (any motion). opencode enables
390
+ * both and needs OPENCODE_DISABLE_MOUSE as an escape hatch because of it. Causyne
391
+ * offers no in-app selection or hover, so it has no reason to ask for drag
392
+ * reports, and not asking is the difference between a terminal that can still
393
+ * mostly be dragged over and one that cannot be at all.
394
+ *
395
+ * Be precise about what this does NOT buy: ?1000 still reports the click
396
+ * itself, and several terminals respond to any mouse reporting by putting their
397
+ * own selection behind a modifier (Option in iTerm2 and Terminal.app, Shift in
398
+ * most X11 terminals). This narrows the damage; it does not remove it. That is
399
+ * what CAUSYNE_MOUSE=0 is for, and why it is documented rather than buried.
400
+ *
401
+ * There is no mode that reports the wheel alone. UX-02.
402
+ */
403
+ export function mouseTracking(enabled) {
404
+ return enabled ? '\x1b[?1000h\x1b[?1006h' : '\x1b[?1006l\x1b[?1000l';
405
+ }
406
+
407
+ /** Turn bracketed paste on/off. Paired with the editor's lifecycle. */
408
+ export function bracketedPaste(enabled) {
409
+ return enabled ? '\x1b[?2004h' : '\x1b[?2004l';
410
+ }
411
+
412
+ /**
413
+ * True when `event` is the plain printable character `letter`.
414
+ *
415
+ * THE BUG THIS EXISTS TO PREVENT — and it shipped:
416
+ *
417
+ * A printable key does NOT decode to its own name. `y` produces
418
+ * `{ name: 'char', text: 'y' }`, because the decoder reserves `name` for keys
419
+ * that have no character (escape, up, enter) and puts the character in `text`.
420
+ * Only MODIFIED letters get their letter as a name: ctrl+c is
421
+ * `{ name: 'c', ctrl: true }`, alt+b is `{ name: 'b', alt: true }`.
422
+ *
423
+ * That asymmetry is easy to get wrong from the outside, and the approval
424
+ * dialog got it wrong: it tested `event.name === 'y'`, which is true for
425
+ * ctrl+y and false for y. So the y/n keys on the one modal that gates every
426
+ * dangerous action in the product did nothing at all, silently — every
427
+ * keypress fell through to the layer's `default:` branch and was swallowed.
428
+ * Escape still worked, because escape genuinely is named 'escape', which made
429
+ * the dialog look responsive while being impossible to approve.
430
+ *
431
+ * Anything comparing a bare letter must come through here.
432
+ *
433
+ * @param {object} event A decoded key event.
434
+ * @param {string} letter Single character to test, case-insensitive.
435
+ * @returns {boolean}
436
+ */
437
+ export function isChar(event, letter) {
438
+ if (!event || event.ctrl || event.alt || event.meta) return false;
439
+ const typed = typeof event.text === 'string' && event.text.length > 0
440
+ ? event.text
441
+ : (event.name && event.name.length === 1 ? event.name : '');
442
+ return typed.toLowerCase() === String(letter).toLowerCase();
443
+ }