@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
package/src/tui/ask.js ADDED
@@ -0,0 +1,164 @@
1
+ // One question, asked properly, outside the chat surface.
2
+ //
3
+ // `causyne config` and `causyne pair` run with no chat session behind them, so
4
+ // there is no live region for a dialog to draw into. That is why they were the
5
+ // last two surfaces still on inquirer — and inquirer is a different design
6
+ // system: its own colours, its own glyphs, no NO_COLOR handling, and a list
7
+ // widget that looks nothing like the picker the same user sees inside `causyne`.
8
+ //
9
+ // The fix is NOT a second app shell. That was tried once: screen.js, layout.js,
10
+ // transcript.js and appShell.js existed, took over the terminal, and were
11
+ // deleted under UX-01 when it turned out there is no way to detect a terminal
12
+ // that ignores the alternate buffer. Rebuilding a host would walk straight back
13
+ // into that.
14
+ //
15
+ // Instead this borrows the host that already works. ChatPrompt owns raw mode,
16
+ // key decoding, the bounded live region and the layer stack, and `attach()` —
17
+ // the part that wires it to a chat renderer — is optional. So a standalone
18
+ // question is just a ChatPrompt started for the lifetime of one dialog and
19
+ // stopped again: same keys, same panel, same width clamping, same normal-buffer
20
+ // discipline, no second implementation to drift.
21
+ //
22
+ // Every helper degrades. If stdin is not a terminal, or raw mode cannot be
23
+ // taken, they fall back to a plain readline question — a better editor must
24
+ // never be the reason a command refuses to run.
25
+
26
+ import readline from 'readline';
27
+ import { ChatPrompt, isInteractive } from './prompt.js';
28
+ import { SelectLayer, InputLayer, ConfirmLayer } from './dialogs.js';
29
+
30
+ /**
31
+ * Run one layer on a temporary prompt host.
32
+ *
33
+ * `build(resolve)` returns the Layer to show and calls `resolve` with the
34
+ * answer. The host is torn down before this settles, so whatever the caller
35
+ * prints next lands in a clean terminal.
36
+ */
37
+ function hostOneLayer(build) {
38
+ return new Promise((resolve) => {
39
+ let prompt;
40
+ let settled = false;
41
+
42
+ const finish = (value) => {
43
+ if (settled) return;
44
+ settled = true;
45
+ try { if (prompt) prompt.stop(); } catch (e) { /* already stopped */ }
46
+ // Let the terminal settle before the caller writes over the same rows.
47
+ setImmediate(() => resolve(value));
48
+ };
49
+
50
+ try {
51
+ prompt = new ChatPrompt({
52
+ commands: [],
53
+ // ctrl+c during a question is a cancel, not an exit: the caller decides
54
+ // what "no answer" means for it. An exit here would kill the process
55
+ // mid-configuration and leave a half-written config.json.
56
+ onInterrupt: () => finish(null),
57
+ onExit: () => finish(null)
58
+ });
59
+ prompt.start();
60
+ } catch (e) {
61
+ resolve(undefined); // signals "no TTY host" — caller falls back
62
+ return;
63
+ }
64
+
65
+ prompt.openLayer(build(finish));
66
+ });
67
+ }
68
+
69
+ /** True when a panel-based question can be asked at all. */
70
+ export function canAskInPanel() {
71
+ return isInteractive();
72
+ }
73
+
74
+ /** Plain readline, for pipes, CI and any terminal that refuses raw mode. */
75
+ function fallbackQuestion(question, { mask = false } = {}) {
76
+ return new Promise((resolve) => {
77
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
78
+ rl.question(`${question} `, (answer) => { rl.close(); resolve(answer); });
79
+ if (mask) {
80
+ // Suppress the echo of what is typed. `_writeToOutput` is readline's own
81
+ // hook for exactly this; the prompt itself still has to be drawn or the
82
+ // user sees nothing at all.
83
+ rl._writeToOutput = function (text) {
84
+ if (text.includes(question)) rl.output.write(text);
85
+ };
86
+ }
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Pick one of a list.
92
+ *
93
+ * @param {object} options
94
+ * @param {string} options.title
95
+ * @param {Array} options.items `{ id, label, description }`
96
+ * @returns {Promise<object|null>} the chosen item, or null if dismissed
97
+ */
98
+ export async function askSelect({ title = '', items = [] } = {}) {
99
+ if (items.length === 0) return null;
100
+
101
+ if (canAskInPanel()) {
102
+ const answer = await hostOneLayer((resolve) => new SelectLayer({
103
+ title,
104
+ items: items.map((item) => ({ ...item, value: item })),
105
+ onPick: (picked) => resolve(picked ? picked.value : null)
106
+ }));
107
+ if (answer !== undefined) return answer;
108
+ }
109
+
110
+ // Numbered list is the honest fallback: it needs no cursor control at all.
111
+ console.log(`\n${title}`);
112
+ items.forEach((item, index) => {
113
+ console.log(` ${String(index + 1).padStart(2)}. ${item.label}${item.description ? ` — ${item.description}` : ''}`);
114
+ });
115
+ const raw = await fallbackQuestion(`Choose 1-${items.length}:`);
116
+ const choice = Number(String(raw).trim());
117
+ return Number.isInteger(choice) && choice >= 1 && choice <= items.length ? items[choice - 1] : null;
118
+ }
119
+
120
+ /**
121
+ * Ask for a line of text.
122
+ *
123
+ * @returns {Promise<string|null>} the answer, or null if dismissed
124
+ */
125
+ export async function askText({ title = '', lines = [], value = '', placeholder = '', mask = false, validate = null } = {}) {
126
+ while (true) {
127
+ let answer;
128
+
129
+ if (canAskInPanel()) {
130
+ answer = await hostOneLayer((resolve) => new InputLayer({
131
+ title, lines, value, placeholder, mask,
132
+ onSubmit: (text) => resolve(text),
133
+ onCancel: () => resolve(null)
134
+ }));
135
+ if (answer === undefined) answer = await fallbackQuestion(`${title}:`, { mask });
136
+ } else {
137
+ if (lines.length) console.log('\n' + lines.join('\n'));
138
+ answer = await fallbackQuestion(`${title}:`, { mask });
139
+ }
140
+
141
+ if (answer === null) return null;
142
+
143
+ // Re-ask rather than accept something the caller has said is wrong. This is
144
+
145
+
146
+ const problem = validate ? validate(answer) : null;
147
+ if (!problem) return answer;
148
+ console.log(` ${problem}`);
149
+ value = answer;
150
+ }
151
+ }
152
+
153
+ /** A yes/no, defaulting to no on dismissal. */
154
+ export async function askConfirm(message) {
155
+ if (canAskInPanel()) {
156
+ const answer = await hostOneLayer((resolve) => new ConfirmLayer({
157
+ message,
158
+ onAnswer: (yes) => resolve(Boolean(yes))
159
+ }));
160
+ if (answer !== undefined) return Boolean(answer);
161
+ }
162
+ const raw = await fallbackQuestion(`${message} [y/N]`);
163
+ return /^y(es)?$/i.test(String(raw).trim());
164
+ }
@@ -0,0 +1,158 @@
1
+ // The banner: the wordmark, and the first thing a new user is invited to ask.
2
+ //
3
+ // This lives in its own module because it is written ONCE, straight into the
4
+ // user's real terminal history, before anything else runs — the same shape
5
+ // openclaw uses (src/cli/banner.ts emitCliBanner: art first, metadata line
6
+ // underneath) and opencode's entry splash, whose own comment calls these
7
+ // "scrollback snapshots ... immutable terminal history once committed".
8
+ //
9
+ // Nothing here repaints. Nothing here clears. See UX-01 in .agents/rules.json.
10
+ //
11
+ // Pure Node over theme.js and text.js. No dependency, no shell.
12
+
13
+ import { paint, bold, dim, GLYPHS, HAS_UNICODE } from './theme.js';
14
+ import { visibleWidth, truncate, padEnd, shortenHome } from './text.js';
15
+
16
+ // Painted through the theme rather than with a hardcoded \x1b[1m\x1b[36m, so it
17
+ // still obeys NO_COLOR and still degrades on a terminal without box-drawing.
18
+ const WORDMARK = [
19
+ ' ████ ███ █ █ ████ █ █ █ █ █████',
20
+ '█ █ █ █ █ █ █ █ ██ █ █ ',
21
+ '█ █████ █ █ ███ █ █ █ █ ████ ',
22
+ '█ █ █ █ █ █ █ █ ██ █ ',
23
+ ' ████ █ █ ███ ████ █ █ █ █████'
24
+ ];
25
+
26
+ // A terminal without box-drawing support would render the art as mojibake, so
27
+ // it gets the wordmark spelled out instead of a field of question marks.
28
+ const WORDMARK_PLAIN = ['C A U S Y N E'];
29
+
30
+ /**
31
+ * The banner rows.
32
+ *
33
+ * `unicode` is overridable so both branches can actually be tested — under
34
+ * `node --test` there is no TTY, so HAS_UNICODE is false and the block art would
35
+ * never be exercised at all.
36
+ */
37
+ export function wordmark(width = 80, { unicode = HAS_UNICODE } = {}) {
38
+ const art = (unicode && width >= 54) ? WORDMARK : WORDMARK_PLAIN;
39
+ const rows = art.map((line) => paint('accent', line));
40
+ rows.push('');
41
+ rows.push(dim('Causyne AI Agent'));
42
+ return rows;
43
+ }
44
+
45
+ /**
46
+ * The session header: the wordmark and the session facts, SIDE BY SIDE.
47
+ *
48
+ * This used to be three stacked blocks — six rows of art, then a blank, then a
49
+ * bordered panel of four label/value rows, then two separate hint lines. Nineteen
50
+ * rows of chrome before the user could type a character, on a surface whose whole
51
+ * job is to get out of the way.
52
+ *
53
+ * Neither reference does that. claude-code sets its art in the left column with
54
+ * the version, model, plan and directory stacked beside it — three rows, one
55
+ * block. opencode puts the logo alone and the facts in the prompt box. The
56
+ * common property is that the identity and the facts are ONE object, not a
57
+ * sequence of them.
58
+ *
59
+ * Falls back to stacking when the terminal cannot hold both columns, because a
60
+ * header that wraps is worse than a header that is tall.
61
+ */
62
+ export function sessionBanner(info = {}, width = 80, { unicode = HAS_UNICODE } = {}) {
63
+ const art = (unicode && width >= 54) ? WORDMARK : WORDMARK_PLAIN;
64
+ const artWidth = Math.max(...art.map(visibleWidth));
65
+
66
+ const facts = [];
67
+ if (info.version) facts.push([bold(paint('accent', 'Causyne')), dim(info.version)]);
68
+ const model = info.model || 'no model configured';
69
+ facts.push([paint('ink', model), info.provider ? dim(providerLabel(info.provider)) : '']);
70
+ const mode = [info.autonomyMode ? `mode ${info.autonomyMode}` : '', info.tier ? `tier ${info.tier}` : '']
71
+ .filter(Boolean).join(` ${GLYPHS.dot} `);
72
+ if (mode) facts.push([dim(mode), '']);
73
+ if (info.workspace) facts.push([dim(shortenHome(info.workspace)), '']);
74
+
75
+ const GAP = 4;
76
+ // The spare column is the same one the stacked branch below already reserves
77
+ // (`width - 1`). Without it a full-width fact row lands in the terminal's
78
+ // deferred-wrap state: the cursor sits past the last column, and the next
79
+ // write decides whether that becomes a wrap. Measured on an 80-column pty,
80
+ // the workspace row came back at exactly 80 while every other row was 79.
81
+ const room = width - artWidth - GAP - 1;
82
+
83
+ // Not enough width for two columns: stack, rather than wrap either one.
84
+ if (room < 24) {
85
+ return art.map((line) => paint('accent', line))
86
+ .concat([''], facts.map(([left, right]) =>
87
+ truncate(right ? `${left} ${right}` : left, width - 1, GLYPHS.ellipsis)));
88
+ }
89
+
90
+ const rows = [];
91
+ const height = Math.max(art.length, facts.length);
92
+ for (let index = 0; index < height; index++) {
93
+ const artLine = art[index] === undefined ? '' : paint('accent', art[index]);
94
+ const fact = facts[index];
95
+ const left = padEnd(artLine, artWidth) + ' '.repeat(GAP);
96
+ if (!fact) { rows.push(artLine); continue; }
97
+ const [primary, secondary] = fact;
98
+ const text = secondary ? `${primary} ${secondary}` : primary;
99
+ rows.push(left + truncate(text, room, GLYPHS.ellipsis));
100
+ }
101
+ return rows;
102
+ }
103
+
104
+ /**
105
+ * Display the provider the way the user wrote it, normalised for casing only.
106
+ *
107
+ * Causyne is BYOK: the provider is whatever the user configured, and nothing here
108
+ * may assume or default to one. A config saying `Gemini` and one saying `gemini`
109
+ * are the same provider and must read the same.
110
+ */
111
+ export function providerLabel(provider) {
112
+ const raw = String(provider || '').trim();
113
+ if (!raw) return '';
114
+ const KNOWN = {
115
+ openai: 'OpenAI', anthropic: 'Anthropic', gemini: 'Gemini', ollama: 'Ollama',
116
+ deepseek: 'DeepSeek', dashscope: 'DashScope', 'dashscope-byok': 'DashScope',
117
+ qwen: 'Qwen', xai: 'xAI', grok: 'xAI', moonshot: 'Moonshot', kimi: 'Moonshot',
118
+ muse: 'Meta', meta: 'Meta',
119
+ 'openai-compatible': 'OpenAI-compatible'
120
+ };
121
+ return KNOWN[raw.toLowerCase()] || raw;
122
+ }
123
+
124
+ // What to ask, for the person who has just opened this for the first time.
125
+ //
126
+ // `ask anything` is an invitation, not an example — it tells a new user nothing
127
+ // about what this agent can actually be asked to do. Both references show a
128
+ // concrete prompt instead (opencode seeds a random one per launch from
129
+ // routes/home.tsx). One is picked per session so the hint does not churn while
130
+ // the user is reading it.
131
+ export const EXAMPLES = [
132
+ 'summarise what changed in this repo today',
133
+ 'find every TODO left in src/ and group them by file',
134
+ 'explain what this project does and how it is wired',
135
+ 'run the tests and tell me what broke',
136
+ 'review my last commit for anything risky'
137
+ ];
138
+
139
+ /** One example, chosen per session rather than per keystroke. */
140
+ export function pickExample() {
141
+ return EXAMPLES[Math.floor(Math.random() * EXAMPLES.length)];
142
+ }
143
+
144
+ /**
145
+ * Join hint fragments, dropping the least important until the line fits.
146
+ *
147
+ * Same policy as the footer: a hint clipped mid-word reads as a bug, so drop
148
+ * whole fragments rather than truncate one, and fall back entirely when even a
149
+ * single fragment will not fit.
150
+ */
151
+ export function fitHints(parts, width, fallback) {
152
+ const separator = ` ${GLYPHS.dot} `;
153
+ const kept = parts.slice();
154
+ while (kept.length > 1 && visibleWidth(kept.join(separator)) > width) kept.pop();
155
+ const line = kept.join(separator);
156
+ // A narrow terminal must not get `try "review my last commit for anything r`.
157
+ return visibleWidth(line) <= width ? line : fallback;
158
+ }
@@ -0,0 +1,152 @@
1
+ // Blocks: a left accent bar and a filled surface, the shape that makes a run of
2
+ // terminal rows read as ONE thing.
3
+ //
4
+ // Causyne's chat printed every kind of row the same way — the user's message, the
5
+ // agent's reply, an aside from the loop and the prompt itself were all just
6
+ // lines in the scrollback, distinguishable only by a leading glyph. On a busy
7
+ // turn there was no visual seam anywhere, which is the difference the reference
8
+ // tools actually make. opencode gives each of those a left border in the agent's
9
+ // colour and a background one step off the terminal's own
10
+ // (component/prompt/index.tsx:1353 — `border={["left"]}` + `borderColor` +
11
+ // `backgroundColor={theme.backgroundElement}` + `paddingLeft={2}`).
12
+ //
13
+ // Two properties make it work, and both are easy to get wrong:
14
+ //
15
+ // * EVERY ROW IS PADDED TO THE SAME WIDTH. A background only reads as a surface
16
+ // if it runs past the last character; ragged rows read as highlighted text.
17
+ // * THE PADDING IS MEASURED, NOT COUNTED. padEnd() on a coloured string pads by
18
+ // byte length, so the escape sequences count toward the column and the right
19
+ // edge of the block frays. text.js padEnd() measures visible width.
20
+ //
21
+ // It degrades in two steps rather than one: without truecolor/256 there is no
22
+ // fill and the bar alone carries the block; without colour at all the bar is an
23
+ // ASCII `|`. A transcript piped to a file stays clean either way.
24
+ //
25
+ // Pure Node over ./theme.js and ./text.js. No dependency, no shell.
26
+
27
+ import { paint, fill, dim, GLYPHS, HAS_COLOR } from './theme.js';
28
+ import { visibleWidth, padEnd, truncate, wrap } from './text.js';
29
+
30
+ // Columns between the bar and the text. opencode uses paddingLeft={2} inside its
31
+ // bordered box; one column belongs to the bar, so the gutter is one more.
32
+ const GUTTER = 1;
33
+
34
+ /**
35
+ * Render `lines` as one block with a left accent bar.
36
+ *
37
+ * @param {string[]} lines already-styled rows; NOT re-wrapped (see blockOf)
38
+ * @param {object} options
39
+ * @param {number} options.width total columns the block may occupy
40
+ * @param {string} options.role accent role for the bar (user, agent, accent…)
41
+ * @param {boolean} options.filled paint the surface behind the text
42
+ * @param {string} options.surface which surface role to fill with
43
+ * @returns {string[]} rows, each exactly `width` columns wide
44
+ */
45
+ export function barBlock(lines, {
46
+ width = 80,
47
+ role = 'accent',
48
+ filled = true,
49
+ surface = 'surface',
50
+ punctuate = false
51
+ } = {}) {
52
+ // The input box asks for a continuous heavy rule with a dot at its middle —
53
+ // long line, dot, long line. Everything else keeps the half-block bar, because
54
+ // a message block is content and the input box is chrome you type INTO; the
55
+ // reference draws that distinction too, giving its prompt box custom border
56
+ // characters no message uses (component/prompt/index.tsx:1354-1358).
57
+ //
58
+ // Punctuation needs three rows to read as "middle": below that it would land
59
+ // on the only row there is, and a dot alone is not a rule.
60
+ const glyph = punctuate ? GLYPHS.barHeavy : GLYPHS.bar;
61
+ const middle = punctuate && lines.length >= 3 ? Math.floor(lines.length / 2) : -1;
62
+
63
+ const barWidth = visibleWidth(glyph);
64
+ const body = Math.max(1, width - barWidth - GUTTER);
65
+ const bar = paint(role, glyph);
66
+ const marked = paint(role, GLYPHS.bullet);
67
+ const gutter = ' '.repeat(GUTTER);
68
+
69
+ return lines.map((line, index) => {
70
+ // Clip first, then pad: a line longer than the block would otherwise push
71
+ // the fill past the right edge and wrap, which is the failure ChatPrompt
72
+ // clampRows() exists to prevent one layer up.
73
+ const clipped = visibleWidth(line) > body ? truncate(line, body, GLYPHS.ellipsis) : line;
74
+ const padded = padEnd(clipped, body);
75
+ const inner = filled ? fill(surface, gutter + padded) : gutter + padded;
76
+ return (index === middle ? marked : bar) + inner;
77
+ });
78
+ }
79
+
80
+ /**
81
+ * Wrap `text` to the block's inner width, then render it as a block.
82
+ *
83
+ * The convenience form for plain prose. Callers that have already styled their
84
+ * rows individually (the prompt editor, a status row) use barBlock directly.
85
+ */
86
+ export function blockOf(text, options = {}) {
87
+ const width = options.width || 80;
88
+ const inner = Math.max(1, width - visibleWidth(GLYPHS.bar) - GUTTER);
89
+ const lines = wrap(String(text == null ? '' : text), inner);
90
+ return barBlock(lines.length ? lines : [''], options);
91
+ }
92
+
93
+ /**
94
+ * A bordered panel with its title inset into the top border.
95
+ *
96
+ * The session header used to be two horizontal rules with label/value rows
97
+ * between them, which reads as a table of settings rather than as a greeting.
98
+ * Both references open with a bordered panel instead — claude-code insets the
99
+ * product name and version into the top rule exactly this way.
100
+ *
101
+ * `rows` are already-styled strings. Returns rows of exactly `width` columns.
102
+ */
103
+ export function panel(rows, { width = 80, title = '', role = 'rule' } = {}) {
104
+ const inner = Math.max(4, width - 2);
105
+ const h = GLYPHS.hLine;
106
+ const label = title ? ` ${title} ` : '';
107
+ const clippedLabel = visibleWidth(label) > inner - 2 ? '' : label;
108
+ const lead = clippedLabel ? h : '';
109
+ const trail = h.repeat(Math.max(0, inner - visibleWidth(clippedLabel) - visibleWidth(lead)));
110
+
111
+ const top = paint(role, GLYPHS.topLeft + lead)
112
+ + (clippedLabel ? paint('accent', clippedLabel) : '')
113
+ + paint(role, trail + GLYPHS.topRight);
114
+ const bottom = paint(role, GLYPHS.bottomLeft + h.repeat(inner) + GLYPHS.bottomRight);
115
+
116
+ const side = paint(role, GLYPHS.vLine);
117
+ const middle = rows.map((row) => {
118
+ const clipped = visibleWidth(row) > inner ? truncate(row, inner, GLYPHS.ellipsis) : row;
119
+ return side + padEnd(clipped, inner) + side;
120
+ });
121
+
122
+ return [top, ...middle, bottom];
123
+ }
124
+
125
+ /**
126
+ * `9.2K (5%)` — what the context window has left, at a glance.
127
+ *
128
+ * Returns '' rather than a misleading 0% when no limit is known: an invented
129
+ * denominator is worse than a missing number.
130
+ *
131
+ * BOTH ARGUMENTS MUST BE IN THE SAME UNIT. The caller was passing a cumulative
132
+ * TOKEN count and a CHARACTER pool, which is how a quarter-full window came to
133
+ * report 99%.
134
+ */
135
+ export function contextMeter(usedTokens, limitTokens) {
136
+ const used = Number(usedTokens) || 0;
137
+ const limit = Number(limitTokens) || 0;
138
+ if (used <= 0) return '';
139
+ const compact = used < 1000
140
+ ? String(Math.round(used))
141
+ : used < 1000000
142
+ ? `${(used / 1000).toFixed(used < 10000 ? 1 : 0)}K`
143
+ : `${(used / 1000000).toFixed(1)}M`;
144
+ if (limit <= 0) return compact;
145
+ // NOT clamped to 99. The clamp was there to keep the meter from looking like
146
+ // a hard stop, and its real effect was to hide a 242% reading behind a
147
+ // plausible-looking one for a whole session. A meter that cannot report
148
+ // "over" is a meter that cannot report the only thing worth acting on.
149
+ return `${compact} (${Math.round((used / limit) * 100)}%)`;
150
+ }
151
+
152
+ export { GUTTER };