@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,296 @@
1
+ // Structural diff for the approval prompt.
2
+ //
3
+ // The approval gate used to show the user a tool name and a path:
4
+ //
5
+ // [ DANGEROUS ] fs_edit edit
6
+ // site/index.html (3 edits)
7
+ //
8
+ // and then asked "Approve?". That is a procedural gate, not a real one — the
9
+ // human is consenting to a file mutation whose content they have never seen.
10
+ // sandbox.js classifies the risk correctly and makeApprovalHandler asks the
11
+ // question correctly; the missing half was always the payload.
12
+ //
13
+ // This module answers "what will actually change" for the four tools that
14
+ // mutate a file, and it does so under three hard bounds, because the input is
15
+ // attacker-influenced:
16
+ //
17
+ // 1. The diff is computed over at most MAX_SIDE_LINES rows per side. LCS is
18
+ // O(n*m); an unbounded model-supplied payload is a way to hang the terminal
19
+ // at exactly the moment the user is being asked to consent.
20
+ // 2. At most MAX_RENDER_LINES rows reach the screen, so a large rewrite cannot
21
+ // scroll the question itself out of the scrollback.
22
+ // 3. Every content row passes through text.safeDisplay(), because file content
23
+ // can carry escapes — and a cursor-moving escape printed one row above an
24
+ // approval prompt can repaint the prompt.
25
+ //
26
+ // Pure Node over ./theme.js and ./text.js. No dependency, no shell.
27
+
28
+ import fs from 'fs';
29
+ import { paint, dim, GLYPHS } from './theme.js';
30
+ import { truncate, safeDisplay, shortenHome } from './text.js';
31
+
32
+ // Per side, for the LCS table. 600x600 int cells is ~1.4 MB and a few ms.
33
+ const MAX_SIDE_LINES = 600;
34
+
35
+ // Rows that reach the terminal. Deliberately smaller than a screen so the
36
+ // approval question stays visible under the diff.
37
+ const MAX_RENDER_LINES = 40;
38
+
39
+ // Unchanged rows kept either side of a change, so an edit has context.
40
+ const CONTEXT = 2;
41
+
42
+ // A file larger than this is not read to build a diff. At that size the diff is
43
+ // not readable anyway, and the read is on the interactive path.
44
+ const MAX_FILE_BYTES = 512 * 1024;
45
+
46
+ /** Split into lines without inventing a trailing empty row for a final \n. */
47
+ function splitLines(text) {
48
+ const normalised = String(text == null ? '' : text).replace(/\r\n?/g, '\n');
49
+ if (normalised === '') return [];
50
+ const lines = normalised.split('\n');
51
+ if (lines[lines.length - 1] === '') lines.pop();
52
+ return lines;
53
+ }
54
+
55
+ /**
56
+ * Line diff by longest common subsequence.
57
+ *
58
+ * Returns ops in source order: { kind: 'ctx' | 'del' | 'add', text }.
59
+ *
60
+ * A word-level or Myers diff would produce prettier output, but this runs while
61
+ * a human is waiting to answer a security question. Correct and bounded beats
62
+ * minimal.
63
+ */
64
+ export function diffLines(before, after) {
65
+ const a = splitLines(before);
66
+ const b = splitLines(after);
67
+
68
+ // Bound BEFORE building the table, not after.
69
+ const truncated = a.length > MAX_SIDE_LINES || b.length > MAX_SIDE_LINES;
70
+ const left = truncated ? a.slice(0, MAX_SIDE_LINES) : a;
71
+ const right = truncated ? b.slice(0, MAX_SIDE_LINES) : b;
72
+
73
+ const rows = left.length;
74
+ const cols = right.length;
75
+
76
+ // lengths[i][j] = LCS length of left[i..] and right[j..], flattened.
77
+ const width = cols + 1;
78
+ const lengths = new Int32Array((rows + 1) * width);
79
+ for (let i = rows - 1; i >= 0; i--) {
80
+ for (let j = cols - 1; j >= 0; j--) {
81
+ lengths[i * width + j] = left[i] === right[j]
82
+ ? lengths[(i + 1) * width + (j + 1)] + 1
83
+ : Math.max(lengths[(i + 1) * width + j], lengths[i * width + (j + 1)]);
84
+ }
85
+ }
86
+
87
+ const ops = [];
88
+ let i = 0;
89
+ let j = 0;
90
+ while (i < rows && j < cols) {
91
+ if (left[i] === right[j]) {
92
+ ops.push({ kind: 'ctx', text: left[i] });
93
+ i++; j++;
94
+ } else if (lengths[(i + 1) * width + j] >= lengths[i * width + (j + 1)]) {
95
+ ops.push({ kind: 'del', text: left[i] });
96
+ i++;
97
+ } else {
98
+ ops.push({ kind: 'add', text: right[j] });
99
+ j++;
100
+ }
101
+ }
102
+ while (i < rows) ops.push({ kind: 'del', text: left[i++] });
103
+ while (j < cols) ops.push({ kind: 'add', text: right[j++] });
104
+
105
+ return { ops, truncated };
106
+ }
107
+
108
+ /** Added/removed counts, for the one-line summary. */
109
+ export function diffStat(ops) {
110
+ let added = 0;
111
+ let removed = 0;
112
+ for (const op of ops) {
113
+ if (op.kind === 'add') added++;
114
+ else if (op.kind === 'del') removed++;
115
+ }
116
+ return { added, removed };
117
+ }
118
+
119
+ /**
120
+ * Keep only the changed rows plus CONTEXT unchanged rows either side.
121
+ *
122
+ * Without this, editing line 900 of a 1000-line file prints 1000 rows to show
123
+ * one change — which is how a diff scrolls the approval question off screen.
124
+ */
125
+ function collapse(ops) {
126
+ const keep = new Array(ops.length).fill(false);
127
+ for (let index = 0; index < ops.length; index++) {
128
+ if (ops[index].kind === 'ctx') continue;
129
+ const from = Math.max(0, index - CONTEXT);
130
+ const to = Math.min(ops.length - 1, index + CONTEXT);
131
+ for (let k = from; k <= to; k++) keep[k] = true;
132
+ }
133
+
134
+ const out = [];
135
+ let skipping = 0;
136
+ for (let index = 0; index < ops.length; index++) {
137
+ if (keep[index]) {
138
+ if (skipping > 0) {
139
+ out.push({ kind: 'gap', count: skipping });
140
+ skipping = 0;
141
+ }
142
+ out.push(ops[index]);
143
+ } else {
144
+ skipping++;
145
+ }
146
+ }
147
+ // A trailing run of unchanged rows needs no marker — nothing follows it.
148
+ return out;
149
+ }
150
+
151
+ /**
152
+ * Render ops as terminal rows.
153
+ *
154
+ * Colour comes from the semantic roles, so this degrades with NO_COLOR and on a
155
+ * light background exactly like the rest of the TUI. The +/- markers carry the
156
+ * meaning on their own, which is what makes the no-colour output still readable.
157
+ */
158
+ export function renderDiffOps(ops, options = {}) {
159
+ const width = Math.max(20, (options.width || 80) - 8);
160
+ const maxLines = options.maxLines || MAX_RENDER_LINES;
161
+ const collapsed = collapse(ops);
162
+ const rendered = [];
163
+
164
+ for (const op of collapsed) {
165
+ if (rendered.length >= maxLines) break;
166
+
167
+ if (op.kind === 'gap') {
168
+ rendered.push(dim(` ${GLYPHS.ellipsis} ${op.count} unchanged line${op.count === 1 ? '' : 's'}`));
169
+ continue;
170
+ }
171
+
172
+ // Untrusted content: a file can carry escapes, and a credential the model
173
+ // echoed back into an edit must not be printed.
174
+ const body = truncate(safeDisplay(op.text), width, GLYPHS.ellipsis);
175
+
176
+ if (op.kind === 'add') rendered.push(` ${paint('success', '+')} ${paint('success', body)}`);
177
+ else if (op.kind === 'del') rendered.push(` ${paint('danger', '-')} ${paint('danger', body)}`);
178
+ else rendered.push(` ${dim(' ')} ${dim(body)}`);
179
+ }
180
+
181
+ const clipped = collapsed.length > rendered.length;
182
+ return { rows: rendered, clipped };
183
+ }
184
+
185
+ /** Convenience: two texts straight to rows. */
186
+ export function renderDiff(before, after, options = {}) {
187
+ const { ops, truncated } = diffLines(before, after);
188
+ const { rows, clipped } = renderDiffOps(ops, options);
189
+ return { rows, stat: diffStat(ops), clipped: clipped || truncated };
190
+ }
191
+
192
+ /** Read a file for diffing, or null when that is not sensible. */
193
+ function readForDiff(pathish) {
194
+ if (!pathish) return null;
195
+ try {
196
+ const target = String(pathish).replace(/^~(?=\/|\\|$)/, process.env.HOME || '~');
197
+ const stat = fs.statSync(target);
198
+ if (!stat.isFile() || stat.size > MAX_FILE_BYTES) return null;
199
+ return fs.readFileSync(target, 'utf8');
200
+ } catch (e) {
201
+ // Missing file is the normal case for a create; anything else (permission,
202
+ // a directory, a binary) simply means no diff, never a broken transcript.
203
+ return null;
204
+ }
205
+ }
206
+
207
+ function firstString(args, keys) {
208
+ for (const key of keys) {
209
+ const value = args[key];
210
+ if (typeof value === 'string' && value.length > 0) return value;
211
+ }
212
+ return '';
213
+ }
214
+
215
+ /**
216
+ * Concatenate the search/replace sides of an edit list into two texts.
217
+ *
218
+ * Diffing each edit separately would be more precise, but an edit list is
219
+ * applied as one atomic mutation and the user is answering one question about
220
+ * it. One diff, in edit order, matches what they are consenting to.
221
+ */
222
+ function editSides(edits) {
223
+ const before = [];
224
+ const after = [];
225
+ for (const edit of edits) {
226
+ if (!edit || typeof edit !== 'object') continue;
227
+ const from = firstString(edit, ['searchContent', 'old_string', 'oldText', 'search']);
228
+ const to = firstString(edit, ['replaceContent', 'new_string', 'newText', 'replace']);
229
+ if (from) before.push(from);
230
+ if (to) after.push(to);
231
+ }
232
+ return { before: before.join('\n'), after: after.join('\n') };
233
+ }
234
+
235
+
236
+ const DIFF_BUILDERS = new Map([
237
+ ['fs_write', (args) => {
238
+ const path = firstString(args, ['path', 'file_path']);
239
+ const content = typeof args.content === 'string' ? args.content : '';
240
+ const existing = readForDiff(path);
241
+ return {
242
+ path,
243
+ before: existing === null ? '' : existing,
244
+ after: content,
245
+ note: existing === null ? 'new file' : ''
246
+ };
247
+ }],
248
+ ['fs_edit', (args) => {
249
+ const sides = editSides(Array.isArray(args.edits) ? args.edits : []);
250
+ return { path: firstString(args, ['path', 'file_path']), before: sides.before, after: sides.after, note: '' };
251
+ }],
252
+ ['MultiEdit', (args) => {
253
+ const sides = editSides(Array.isArray(args.edits) ? args.edits : []);
254
+ return { path: firstString(args, ['file_path', 'path']), before: sides.before, after: sides.after, note: '' };
255
+ }],
256
+ ['fs_delete', (args) => {
257
+ const path = firstString(args, ['path', 'file_path']);
258
+ const existing = readForDiff(path);
259
+ if (existing === null) return null;
260
+ return { path, before: existing, after: '', note: 'entire file' };
261
+ }]
262
+ ]);
263
+
264
+ /**
265
+ * The diff block for one approval, or null when the tool does not mutate a file.
266
+ *
267
+ * Never throws: a rendering failure here must not be able to suppress the
268
+ * approval question itself.
269
+ */
270
+ export function approvalDiff(toolName, args, options = {}) {
271
+ try {
272
+ const builder = DIFF_BUILDERS.get(toolName);
273
+ if (!builder) return null;
274
+
275
+ const safeArgs = args && typeof args === 'object' ? args : {};
276
+ const plan = builder(safeArgs);
277
+ if (!plan) return null;
278
+ if (plan.before === plan.after) return null;
279
+
280
+ const { rows, stat, clipped } = renderDiff(plan.before, plan.after, options);
281
+ if (rows.length === 0) return null;
282
+
283
+ return {
284
+ path: plan.path ? shortenHome(plan.path) : '',
285
+ note: plan.note,
286
+ rows,
287
+ added: stat.added,
288
+ removed: stat.removed,
289
+ clipped
290
+ };
291
+ } catch (e) {
292
+ return null;
293
+ }
294
+ }
295
+
296
+ export { MAX_SIDE_LINES, MAX_RENDER_LINES, CONTEXT };
@@ -0,0 +1,135 @@
1
+ // Persisted prompt history.
2
+ //
3
+ // Every serious terminal tool remembers what you typed. The chat did not: close
4
+ // it and the last hour of prompts was gone.
5
+ //
6
+ // SECURITY: a shell history file is a classic place for a key to leak, and this
7
+ // one is written by a program whose users paste API keys into it. Any line that
8
+ // redactSecrets() would alter is a line that contains something credential
9
+ // shaped, and it is never written to disk. Losing one recallable prompt is
10
+ // cheaper than persisting a key in plaintext.
11
+ //
12
+ // Pure Node. No dependency, no shell.
13
+
14
+ import fs from 'fs';
15
+ import os from 'os';
16
+ import path from 'path';
17
+ import { redactSecrets } from './text.js';
18
+ import { causyneHome } from '../utils/paths.js';
19
+
20
+ export const MAX_ENTRIES = 500;
21
+
22
+ function historyPath() {
23
+ return path.join(causyneHome(), 'history');
24
+ }
25
+
26
+ /** True when the line carries something that looks like a credential. */
27
+ export function looksSensitive(line) {
28
+ return redactSecrets(line) !== line;
29
+ }
30
+
31
+ export class PromptHistory {
32
+ constructor(options = {}) {
33
+ this.file = options.file || historyPath();
34
+ this.entries = [];
35
+ this.cursor = -1; // -1 means "editing a fresh line, not browsing"
36
+ this.draft = '';
37
+ this.load();
38
+ }
39
+
40
+ load() {
41
+ try {
42
+ const raw = fs.readFileSync(this.file, 'utf8');
43
+ this.entries = raw.split('\n')
44
+ .map((line) => line.trim())
45
+ .filter(Boolean)
46
+ .map((line) => {
47
+ try {
48
+ const parsed = JSON.parse(line);
49
+ return typeof parsed === 'string' ? parsed : null;
50
+ } catch (e) {
51
+ return null;
52
+ }
53
+ })
54
+ .filter((entry) => typeof entry === 'string' && entry.length > 0);
55
+ } catch (e) {
56
+ this.entries = []; // no history yet is the normal first run
57
+ }
58
+ if (this.entries.length > MAX_ENTRIES) {
59
+ this.entries = this.entries.slice(-MAX_ENTRIES);
60
+ }
61
+ this.cursor = -1;
62
+ }
63
+
64
+ /**
65
+ * Record a submitted prompt.
66
+ *
67
+ * Returns false when the line was deliberately not kept, so a caller can say
68
+ * so rather than leaving the user to wonder why it never comes back.
69
+ */
70
+ add(text) {
71
+ const line = String(text == null ? '' : text).trim();
72
+ this.cursor = -1;
73
+ this.draft = '';
74
+
75
+ if (!line) return false;
76
+ if (looksSensitive(line)) return false;
77
+ // Consecutive duplicates are noise; re-running the same prompt is common.
78
+ if (this.entries[this.entries.length - 1] === line) return true;
79
+
80
+ this.entries.push(line);
81
+ if (this.entries.length > MAX_ENTRIES) {
82
+ this.entries = this.entries.slice(-MAX_ENTRIES);
83
+ }
84
+ this.persist();
85
+ return true;
86
+ }
87
+
88
+ persist() {
89
+ try {
90
+ fs.mkdirSync(path.dirname(this.file), { recursive: true });
91
+ const body = this.entries.map((entry) => JSON.stringify(entry)).join('\n') + '\n';
92
+ // Write then rename: a crash mid-write must not truncate the history.
93
+ const temporary = this.file + '.tmp';
94
+ fs.writeFileSync(temporary, body, { mode: 0o600 });
95
+ fs.renameSync(temporary, this.file);
96
+ } catch (e) {
97
+ // History is a convenience. Failing to save it must never break the chat.
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Entries matching the prefix the user has already typed, newest first.
103
+ *
104
+ * Typing `npm ` then pressing Up should walk npm prompts, not everything.
105
+ */
106
+ matching(prefix) {
107
+ if (!prefix) return this.entries.slice().reverse();
108
+ return this.entries.filter((entry) => entry.startsWith(prefix)).reverse();
109
+ }
110
+
111
+ /** Older entry, or null at the end. `draft` is the line being edited. */
112
+ previous(draft) {
113
+ if (this.cursor === -1) this.draft = draft;
114
+ const pool = this.matching(this.draft);
115
+ if (pool.length === 0) return null;
116
+ const next = Math.min(this.cursor + 1, pool.length - 1);
117
+ if (next === this.cursor) return null;
118
+ this.cursor = next;
119
+ return pool[this.cursor];
120
+ }
121
+
122
+ /** Newer entry, or the original draft once the walk returns to the start. */
123
+ next() {
124
+ if (this.cursor <= -1) return null;
125
+ const pool = this.matching(this.draft);
126
+ this.cursor -= 1;
127
+ if (this.cursor === -1) return this.draft;
128
+ return pool[this.cursor] ?? this.draft;
129
+ }
130
+
131
+ reset() {
132
+ this.cursor = -1;
133
+ this.draft = '';
134
+ }
135
+ }
@@ -0,0 +1,217 @@
1
+ // Local desktop home: chat, configuration and workspace reports.
2
+
3
+ import { SelectLayer, InputLayer } from './dialogs.js';
4
+ import { paint, dim, bold, badge, GLYPHS } from './theme.js';
5
+ import { DEFAULT_MODEL_BY_PROVIDER } from '../modelCatalogue.js';
6
+
7
+ /** Local runtime and provider summary. */
8
+ export function homeStatusLine({ config = {} } = {}) {
9
+ const model = config.model ? `${config.provider || 'default'}/${config.model}` : (config.provider || 'no provider');
10
+ const configured = config.apiKey || config.provider === 'ollama';
11
+ return ' ' + [
12
+ paint('success', `${GLYPHS.bullet} local desktop`),
13
+ configured ? dim(model) : paint('warn', `${model} — no key`)
14
+ ].join(dim(` ${GLYPHS.dot} `));
15
+ }
16
+
17
+ /**
18
+ * Actions shown on the front page.
19
+ *
20
+ * `kind` decides what happens after `run`:
21
+ * 'stay' — the result is drawn into the live region, home stays up
22
+ * 'takeover' — the prompt is stopped and something else owns the terminal
23
+ * 'chat' — nothing to run; closing the menu already IS chat
24
+ */
25
+ export const PRIMARY_ACTIONS = [
26
+ { id: 'chat', label: 'chat', hint: 'talk to the agent here', kind: 'chat' },
27
+ {
28
+ id: 'status',
29
+ label: 'status',
30
+ hint: 'local runtime and workspace',
31
+ kind: 'stay',
32
+ run: async () => {
33
+ const { buildStatusText } = await import('../reporting.js');
34
+ return buildStatusText();
35
+ }
36
+ },
37
+ {
38
+ id: 'usage',
39
+ label: 'usage',
40
+ hint: 'tokens and cost today',
41
+ kind: 'stay',
42
+ run: async () => (await import('../reporting.js')).buildUsageText()
43
+ },
44
+ { id: 'config', label: 'config', hint: 'provider, model, API key', kind: 'config' },
45
+ { id: 'more', label: 'more…', hint: 'context · checkpoints · undo · mcp · update', kind: 'more' }
46
+ ];
47
+
48
+ /**
49
+ * The second page.
50
+ *
51
+ * These are real, and several of them are destructive, which is why they are
52
+ * one level down rather than absent: hiding `unpair` would not make it safer,
53
+ * it would make it undiscoverable.
54
+ */
55
+ export const MORE_ACTIONS = [
56
+ {
57
+ id: 'context', label: 'context', category: 'reports', hint: 'what fills the context window', kind: 'stay',
58
+ run: async () => (await import('../reporting.js')).buildContextText()
59
+ },
60
+ {
61
+ id: 'checkpoints', label: 'checkpoints', category: 'reports', hint: 'recent undo points', kind: 'stay',
62
+ run: async () => (await import('../reporting.js')).buildCheckpointsText(15)
63
+ },
64
+ {
65
+ id: 'undo', label: 'undo', category: 'reports', hint: 'roll back the last task', kind: 'command',
66
+ // Routed through the registry so it gets the same confirmation gate the
67
+ // typed command gets. An action that wrote files straight from a menu would
68
+ // be the one path around the gate.
69
+ command: '/undo'
70
+ },
71
+ { id: 'mcp', label: 'mcp', category: 'tools', hint: 'manage MCP servers', kind: 'mcp' },
72
+ {
73
+ id: 'update', label: 'update', category: 'tools', hint: 'update to the latest version', kind: 'takeover',
74
+ run: async () => {
75
+ const { runSelfUpdate } = await import('../autoUpdate.js');
76
+ await runSelfUpdate(true);
77
+ }
78
+ },
79
+
80
+ ];
81
+
82
+ /** Items in the shape SelectList wants, for the `more…` page. */
83
+ export function moreLayer(onPick) {
84
+ return new SelectLayer({
85
+ title: 'more',
86
+ items: MORE_ACTIONS.map((action) => ({
87
+ id: action.id,
88
+ label: action.label,
89
+ description: action.hint,
90
+ category: action.category,
91
+ value: action
92
+ })),
93
+ onPick: (item) => onPick(item && item.value)
94
+ });
95
+ }
96
+
97
+ /** The hint row under the input while the menu is up. */
98
+ export function homeHint() {
99
+ return dim(` ${bold('↑↓')} pick ${GLYPHS.dot} ${bold('enter')} run ${GLYPHS.dot} ${bold('esc')} menu off ${GLYPHS.dot} or just start typing`);
100
+ }
101
+
102
+ export { badge };
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // The two flows that need a keyboard
106
+ //
107
+ // Both of these used inquirer, which takes stdin while ChatPrompt is already
108
+ // holding it in raw mode — the two contend and the question lands on a
109
+ // half-drawn prompt. Inside home there IS a live region and a modal stack, so
110
+ // they can be asked properly. `causyne config` invoked on its own still uses the
111
+ // inquirer wizard, because standalone there is no live region to draw into and
112
+ // building a second one is how the deleted alt-screen shell started.
113
+ // ---------------------------------------------------------------------------
114
+
115
+ /** Provider choices for local CLI configuration. */
116
+ // Suggested models come from src/modelCatalogue.js rather than being repeated
117
+ // here. This list had drifted to `gpt-4o` and `gemini-2.5-pro` — two
118
+
119
+ // stale values again. Labels and hints stay local because they are terminal UI.
120
+ export const PROVIDERS = [
121
+ { id: 'deepseek', label: 'DeepSeek', hint: 'recommended for STEM' },
122
+ { id: 'qwen', label: 'Qwen / Alibaba Cloud', hint: 'fastest' },
123
+ { id: 'dashscope-byok', label: 'Alibaba DashScope', hint: 'multi-model gateway' },
124
+ { id: 'muse', label: 'Meta Model API', hint: 'Muse Spark · 1M context' },
125
+ { id: 'anthropic', label: 'Anthropic', hint: 'Claude' },
126
+ { id: 'openai', label: 'OpenAI', hint: 'GPT' },
127
+ { id: 'gemini', label: 'Gemini', hint: 'Google' },
128
+ { id: 'moonshot', label: 'Moonshot', hint: 'Kimi' },
129
+ { id: 'xai', label: 'xAI', hint: 'Grok' },
130
+ { id: 'openai-compatible', label: 'OpenAI-compatible endpoint', hint: 'custom base URL' },
131
+ { id: 'ollama', label: 'Ollama', hint: 'local / offline' }
132
+ ].map((entry) => ({ ...entry, model: DEFAULT_MODEL_BY_PROVIDER[entry.id] ?? '' }));
133
+
134
+ /**
135
+ * Ask for provider, then model, then key — each in the prompt's own panel.
136
+ *
137
+ * Resolves when the last answer is in, or when any step is dismissed. Dismissing
138
+ * cancels the whole flow rather than saving a half-configuration, because a
139
+ * provider saved without its key leaves the agent in a state that fails on the
140
+ * next turn with an error about credentials rather than about the cancel.
141
+ */
142
+ export function configureInPrompt(prompt, { loadConfig, saveConfig }) {
143
+ return new Promise((resolve) => {
144
+ const config = loadConfig();
145
+
146
+ prompt.openLayer(new SelectLayer({
147
+ title: 'provider',
148
+ items: PROVIDERS.map((p) => ({ id: p.id, label: p.label, description: p.hint, value: p })),
149
+ onPick: (item) => {
150
+ if (!item) { resolve(null); return; }
151
+ const provider = item.value;
152
+ askModel(provider);
153
+ }
154
+ }));
155
+
156
+ function askModel(provider) {
157
+ const suggested = (config.provider === provider.id && config.model) ? config.model : provider.model;
158
+ prompt.openLayer(new InputLayer({
159
+ title: `model for ${provider.label}`,
160
+ lines: [
161
+ 'Model ids change often. Type the one you want, or accept the suggestion.'
162
+ ],
163
+ value: suggested,
164
+ placeholder: 'model id',
165
+ onSubmit: (model) => askKey(provider, (model || '').trim() || suggested),
166
+ onCancel: () => resolve(null)
167
+ }));
168
+ }
169
+
170
+ function askKey(provider, model) {
171
+ prompt.openLayer(new InputLayer({
172
+ title: 'API key',
173
+ lines: [
174
+ `${provider.label} ${GLYPHS.arrow} ${model}`,
175
+ '',
176
+ 'Stored encrypted in ~/.causyne/config.json. It never leaves this machine.'
177
+ ],
178
+ mask: true,
179
+ value: '',
180
+ placeholder: 'paste your key (blank keeps the current one)',
181
+ onSubmit: (apiKey) => {
182
+ const next = loadConfig();
183
+ next.provider = provider.id;
184
+ next.model = model;
185
+ if (apiKey) next.apiKey = apiKey;
186
+ saveConfig(next);
187
+ resolve({ provider: provider.id, model });
188
+ },
189
+ onCancel: () => resolve(null)
190
+ }));
191
+ }
192
+ });
193
+ }
194
+
195
+ /** The MCP server list, as something you can act on rather than read. */
196
+ export async function mcpPicker(prompt, onAction) {
197
+ const { serverStatus } = await import('../mcp/mcpRegistry.js');
198
+ const servers = await serverStatus();
199
+ const rows = Array.isArray(servers) ? servers : (servers && servers.servers) || [];
200
+
201
+ if (rows.length === 0) {
202
+ return { empty: true };
203
+ }
204
+
205
+ prompt.openLayer(new SelectLayer({
206
+ title: 'MCP servers',
207
+ items: rows.map((server) => ({
208
+ id: server.name,
209
+ label: server.name,
210
+ description: `${server.granted ? 'granted' : 'not granted'}${server.command ? ` ${GLYPHS.dot} ${server.command}` : ''}`,
211
+ category: server.granted ? 'granted' : 'available',
212
+ value: server
213
+ })),
214
+ onPick: (item) => { if (item) onAction(item.value); }
215
+ }));
216
+ return { empty: false };
217
+ }