@causyne/agent 0.0.0-stage → 1.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +102 -0
- package/README.md +31 -2
- package/SECURITY.md +24 -0
- package/bin/causyne.js +13 -0
- package/docs/ARCHITECTURE.md +63 -0
- package/docs/PROVENANCE.md +29 -0
- package/docs/SECURITY_REVIEW.md +89 -0
- package/docs/THIRD_PARTY_NOTICES.md +49 -0
- package/native-skills/ai-engineering/context-window-management/SKILL.md +43 -0
- package/native-skills/ai-engineering/llm-evaluation/SKILL.md +46 -0
- package/native-skills/ai-engineering/model-cost-and-latency/SKILL.md +46 -0
- package/native-skills/ai-engineering/prompt-construction/SKILL.md +42 -0
- package/native-skills/ai-engineering/retrieval-augmented-generation/SKILL.md +42 -0
- package/native-skills/ai-engineering/structured-output-and-schemas/SKILL.md +45 -0
- package/native-skills/ai-engineering/tool-calling-agents/SKILL.md +43 -0
- package/native-skills/android/app-performance-profiling/SKILL.md +42 -0
- package/native-skills/android/background-work-scheduling/SKILL.md +47 -0
- package/native-skills/android/compose-state-and-recomposition/SKILL.md +43 -0
- package/native-skills/android/gradle-build-configuration/SKILL.md +41 -0
- package/native-skills/android/lifecycle-and-leaks/SKILL.md +46 -0
- package/native-skills/android/play-store-release/SKILL.md +43 -0
- package/native-skills/android/room-and-persistence/SKILL.md +40 -0
- package/native-skills/backend/authentication-and-sessions/SKILL.md +45 -0
- package/native-skills/backend/background-jobs-and-queues/SKILL.md +42 -0
- package/native-skills/backend/caching-strategy/SKILL.md +44 -0
- package/native-skills/backend/graceful-shutdown/SKILL.md +42 -0
- package/native-skills/backend/rate-limiting-and-quotas/SKILL.md +40 -0
- package/native-skills/backend/rest-api-design/SKILL.md +40 -0
- package/native-skills/backend/webhook-delivery-and-receipt/SKILL.md +43 -0
- package/native-skills/cloud/cost-optimisation/SKILL.md +46 -0
- package/native-skills/cloud/iam-least-privilege/SKILL.md +43 -0
- package/native-skills/cloud/infrastructure-as-code/SKILL.md +43 -0
- package/native-skills/cloud/multi-region-and-failover/SKILL.md +43 -0
- package/native-skills/cloud/serverless-patterns/SKILL.md +47 -0
- package/native-skills/data/data-quality-monitoring/SKILL.md +51 -0
- package/native-skills/data/etl-pipeline-reliability/SKILL.md +43 -0
- package/native-skills/data/exploratory-analysis/SKILL.md +43 -0
- package/native-skills/data/metric-definition/SKILL.md +42 -0
- package/native-skills/data/notebook-to-production/SKILL.md +43 -0
- package/native-skills/database/backup-and-recovery/SKILL.md +43 -0
- package/native-skills/database/connection-pooling/SKILL.md +42 -0
- package/native-skills/database/data-integrity-and-constraints/SKILL.md +42 -0
- package/native-skills/database/query-optimisation/SKILL.md +38 -0
- package/native-skills/database/safe-migrations/SKILL.md +47 -0
- package/native-skills/database/schema-design/SKILL.md +38 -0
- package/native-skills/database/transactions-and-isolation/SKILL.md +41 -0
- package/native-skills/design/colour-system-construction/SKILL.md +38 -0
- package/native-skills/design/design-direction-brief/SKILL.md +41 -0
- package/native-skills/design/motion-and-transitions/SKILL.md +38 -0
- package/native-skills/design/responsive-strategy/SKILL.md +39 -0
- package/native-skills/design/spacing-and-rhythm/SKILL.md +38 -0
- package/native-skills/design/type-scale-and-hierarchy/SKILL.md +39 -0
- package/native-skills/design/visual-qa-loop/SKILL.md +42 -0
- package/native-skills/devops/docker-multi-stage-builds/SKILL.md +60 -0
- package/native-skills/devops/github-actions-ci/SKILL.md +60 -0
- package/native-skills/devops/incident-triage/SKILL.md +38 -0
- package/native-skills/devops/kubernetes-deployment-hardening/SKILL.md +64 -0
- package/native-skills/devops/observability-baseline/SKILL.md +38 -0
- package/native-skills/devops/secret-management/SKILL.md +40 -0
- package/native-skills/devops/terraform-state-and-modules/SKILL.md +36 -0
- package/native-skills/devops/zero-downtime-deploys/SKILL.md +40 -0
- package/native-skills/git/bisect-regression-hunt/SKILL.md +43 -0
- package/native-skills/git/branch-strategy/SKILL.md +39 -0
- package/native-skills/git/commit-hygiene/SKILL.md +41 -0
- package/native-skills/git/history-recovery/SKILL.md +44 -0
- package/native-skills/git/large-repository-surgery/SKILL.md +46 -0
- package/native-skills/git/merge-conflict-resolution/SKILL.md +42 -0
- package/native-skills/media/cinematic-3d-shot/SKILL.md +104 -0
- package/native-skills/mobile/app-startup-optimisation/SKILL.md +41 -0
- package/native-skills/mobile/cross-platform-tradeoffs/SKILL.md +42 -0
- package/native-skills/mobile/deep-linking/SKILL.md +39 -0
- package/native-skills/mobile/mobile-storage-security/SKILL.md +45 -0
- package/native-skills/mobile/offline-first-sync/SKILL.md +42 -0
- package/native-skills/mobile/push-notifications/SKILL.md +38 -0
- package/native-skills/performance/algorithmic-complexity/SKILL.md +43 -0
- package/native-skills/performance/concurrency-and-parallelism/SKILL.md +42 -0
- package/native-skills/performance/load-testing/SKILL.md +42 -0
- package/native-skills/performance/memory-and-leak-hunting/SKILL.md +41 -0
- package/native-skills/performance/network-and-payload-efficiency/SKILL.md +42 -0
- package/native-skills/performance/profiling-methodology/SKILL.md +46 -0
- package/native-skills/python/async-correctness/SKILL.md +40 -0
- package/native-skills/python/cli-tool-construction/SKILL.md +44 -0
- package/native-skills/python/data-processing-pipelines/SKILL.md +38 -0
- package/native-skills/python/environment-isolation/SKILL.md +41 -0
- package/native-skills/python/packaging-and-distribution/SKILL.md +56 -0
- package/native-skills/python/pytest-suite-design/SKILL.md +39 -0
- package/native-skills/python/type-hints-and-static-checking/SKILL.md +42 -0
- package/native-skills/security/authorization-model/SKILL.md +42 -0
- package/native-skills/security/crypto-usage/SKILL.md +48 -0
- package/native-skills/security/dependency-supply-chain/SKILL.md +42 -0
- package/native-skills/security/input-validation-and-injection/SKILL.md +44 -0
- package/native-skills/security/secure-file-uploads/SKILL.md +46 -0
- package/native-skills/security/security-headers-and-csp/SKILL.md +44 -0
- package/native-skills/security/threat-modelling/SKILL.md +46 -0
- package/native-skills/testing/end-to-end-test-stability/SKILL.md +42 -0
- package/native-skills/testing/property-based-testing/SKILL.md +46 -0
- package/native-skills/testing/regression-test-from-a-bug/SKILL.md +41 -0
- package/native-skills/testing/snapshot-and-visual-regression/SKILL.md +42 -0
- package/native-skills/testing/test-doubles-and-mocking/SKILL.md +43 -0
- package/native-skills/testing/test-strategy-and-pyramid/SKILL.md +39 -0
- package/native-skills/testing/writing-good-assertions/SKILL.md +42 -0
- package/native-skills/web/accessible-components/SKILL.md +45 -0
- package/native-skills/web/api-client-resilience/SKILL.md +39 -0
- package/native-skills/web/bundle-size-reduction/SKILL.md +38 -0
- package/native-skills/web/core-web-vitals/SKILL.md +40 -0
- package/native-skills/web/css-layout-systems/SKILL.md +41 -0
- package/native-skills/web/form-validation-ux/SKILL.md +39 -0
- package/native-skills/web/react-state-architecture/SKILL.md +41 -0
- package/native-skills/web/ssr-hydration-correctness/SKILL.md +45 -0
- package/package.json +68 -4
- package/src/agentLoop.js +2458 -0
- package/src/appChannel.js +109 -0
- package/src/approvalGrants.js +66 -0
- package/src/auditLog.js +52 -0
- package/src/autoUpdate.js +12 -0
- package/src/capabilities.js +36 -0
- package/src/checkpoints.js +1199 -0
- package/src/cliMode.js +638 -0
- package/src/commands.js +805 -0
- package/src/compiledSkills.js +540 -0
- package/src/config.js +130 -0
- package/src/contextManager.js +498 -0
- package/src/cortex/autoCondenser.js +192 -0
- package/src/cortex/provisioner.js +27 -0
- package/src/cortex/vectorDb.js +447 -0
- package/src/designPalettes.js +99 -0
- package/src/devMode.js +6 -0
- package/src/embodimentSession.js +180 -0
- package/src/entrypoint.js +194 -0
- package/src/execSafe.js +21 -0
- package/src/gitSnapshot.js +378 -0
- package/src/mcp/jsonRpc.js +165 -0
- package/src/mcp/mcpClient.js +136 -0
- package/src/mcp/mcpRegistry.js +207 -0
- package/src/memory.js +62 -0
- package/src/memoryManager.js +124 -0
- package/src/modelCatalogue.js +132 -0
- package/src/modelIds.js +42 -0
- package/src/modelRouter.js +145 -0
- package/src/outputStore.js +246 -0
- package/src/perception.js +124 -0
- package/src/planCache.js +437 -0
- package/src/procSignal.js +32 -0
- package/src/providers/anthropic.js +408 -0
- package/src/providers/base.js +328 -0
- package/src/providers/dashscope-byok.js +86 -0
- package/src/providers/deepseek.js +80 -0
- package/src/providers/factory.js +37 -0
- package/src/providers/gemini.js +346 -0
- package/src/providers/moonshot.js +73 -0
- package/src/providers/muse.js +169 -0
- package/src/providers/ollama.js +104 -0
- package/src/providers/openai-compatible.js +84 -0
- package/src/providers/openai.js +20 -0
- package/src/providers/qwen-byok.js +76 -0
- package/src/providers/xai.js +73 -0
- package/src/provisioning/capabilityConsent.js +24 -0
- package/src/provisioning/capabilityProvisioner.js +271 -0
- package/src/provisioning/capabilityTools.js +29 -0
- package/src/reporting.js +373 -0
- package/src/sandbox.js +529 -0
- package/src/sessionStore.js +279 -0
- package/src/skillsManager.js +288 -0
- package/src/slopDetector.js +307 -0
- package/src/taskBuffer.js +45 -0
- package/src/taskShape.js +241 -0
- package/src/textDiff.js +150 -0
- package/src/tokenTracker.js +560 -0
- package/src/tool-scripts/ax_snapshot.applescript +39 -0
- package/src/tool-scripts/ax_snapshot.ps1 +10 -0
- package/src/tool-scripts/desktop_act.applescript +58 -0
- package/src/tool-scripts/resolve_automator.py +119 -0
- package/src/toolPacks.js +524 -0
- package/src/tools/archive.js +63 -0
- package/src/tools/astryxOracle.js +78 -0
- package/src/tools/axSnapshot.js +165 -0
- package/src/tools/blenderAutomator.js +349 -0
- package/src/tools/browserAutomator.js +252 -0
- package/src/tools/checkSystemSpecs.js +64 -0
- package/src/tools/clipboard.js +36 -0
- package/src/tools/codeSearchAst.js +101 -0
- package/src/tools/codeSearcher.js +230 -0
- package/src/tools/codeTools.js +262 -0
- package/src/tools/codebaseSearch.js +75 -0
- package/src/tools/createSkill.js +129 -0
- package/src/tools/deferredMcpSearch.js +246 -0
- package/src/tools/desktopAct.js +208 -0
- package/src/tools/desktopOpen.js +34 -0
- package/src/tools/dynamicLoader.js +44 -0
- package/src/tools/executeProceduralMedia.js +93 -0
- package/src/tools/fsDelete.js +51 -0
- package/src/tools/fsEdit.js +105 -0
- package/src/tools/fsFind.js +128 -0
- package/src/tools/fsList.js +77 -0
- package/src/tools/fsMove.js +57 -0
- package/src/tools/fsRead.js +87 -0
- package/src/tools/fsSearch.js +198 -0
- package/src/tools/fsWrite.js +41 -0
- package/src/tools/gameAiBehaviorTree.js +208 -0
- package/src/tools/gameAnimationRig.js +194 -0
- package/src/tools/gameAssetFetcher.js +372 -0
- package/src/tools/gameAssetHarvester.js +210 -0
- package/src/tools/gameAudioSynth.js +267 -0
- package/src/tools/gameBuilder.js +886 -0
- package/src/tools/gameCanvasAudit.js +101 -0
- package/src/tools/gameCinematicSequencer.js +151 -0
- package/src/tools/gameDataForge.js +124 -0
- package/src/tools/gameDialogueTreeGen.js +166 -0
- package/src/tools/gameEconomyBalancer.js +161 -0
- package/src/tools/gameEntitySpawner.js +207 -0
- package/src/tools/gameLoreForge.js +153 -0
- package/src/tools/gameQuestArcBuilder.js +152 -0
- package/src/tools/gameSceneBuilder.js +158 -0
- package/src/tools/gameShaderForge.js +181 -0
- package/src/tools/gameTilemapGenerator.js +164 -0
- package/src/tools/gameUiHudBuilder.js +292 -0
- package/src/tools/gameVfxEmitterGen.js +108 -0
- package/src/tools/gameWebRuntimeInspector.js +91 -0
- package/src/tools/generateMediaViaApi.js +358 -0
- package/src/tools/gitExec.js +54 -0
- package/src/tools/godotAutomator.js +437 -0
- package/src/tools/manageLocalAiDependencies.js +220 -0
- package/src/tools/memoryWrite.js +99 -0
- package/src/tools/multiEdit.js +107 -0
- package/src/tools/palmierAutomator.js +104 -0
- package/src/tools/pdfReader.js +112 -0
- package/src/tools/persistentShell.js +148 -0
- package/src/tools/printer.js +316 -0
- package/src/tools/procTerm.js +91 -0
- package/src/tools/processList.js +52 -0
- package/src/tools/readNativeSkill.js +39 -0
- package/src/tools/registry.js +277 -0
- package/src/tools/resolveAutomator.js +102 -0
- package/src/tools/screenshot.js +75 -0
- package/src/tools/subagentOrchestrator.js +305 -0
- package/src/tools/sysRun.js +125 -0
- package/src/tools/todoWrite.js +75 -0
- package/src/tools/toolDispatch.js +127 -0
- package/src/tools/toolForge.js +124 -0
- package/src/tools/updatePlan.js +88 -0
- package/src/tools/viewImage.js +82 -0
- package/src/tools/visionInspector.js +154 -0
- package/src/tools/webFetch.js +74 -0
- package/src/tools/webSearch.js +82 -0
- package/src/tui/ask.js +164 -0
- package/src/tui/banner.js +158 -0
- package/src/tui/block.js +152 -0
- package/src/tui/chatRenderer.js +953 -0
- package/src/tui/completion.js +424 -0
- package/src/tui/dialogs.js +606 -0
- package/src/tui/diff.js +296 -0
- package/src/tui/history.js +135 -0
- package/src/tui/home.js +217 -0
- package/src/tui/invocation.js +105 -0
- package/src/tui/keys.js +443 -0
- package/src/tui/lineEditor.js +256 -0
- package/src/tui/overlay.js +256 -0
- package/src/tui/prompt.js +1258 -0
- package/src/tui/selectList.js +335 -0
- package/src/tui/stage.js +330 -0
- package/src/tui/stageSurface.js +609 -0
- package/src/tui/statusLine.js +254 -0
- package/src/tui/text.js +345 -0
- package/src/tui/theme.js +393 -0
- package/src/tui/toolDisplay.js +176 -0
- package/src/tui/transcript.js +235 -0
- package/src/tui/viewport.js +337 -0
- package/src/uncertainty.js +271 -0
- package/src/userSkills.js +265 -0
- package/src/utils/cli_ui.js +0 -0
- package/src/utils/inquirer.js +12 -0
- package/src/utils/paths.js +37 -0
- package/src/version.js +38 -0
- package/src/vfeTracker.js +220 -0
|
@@ -0,0 +1,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
|
+
}
|
package/src/tui/keys.js
ADDED
|
@@ -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
|
+
}
|