@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,424 @@
1
+ // Completion for the chat prompt: `/` commands and `@` file paths.
2
+ //
3
+ // Two providers, one picker. The command provider reads the SAME registry that
4
+ // executes commands (src/commands.js), so a command can never appear in the
5
+ // picker and then not exist — there is no second list to drift.
6
+ //
7
+ // SECURITY: a filename is untrusted input. `touch $'evil\e[31m.txt'` is legal on
8
+ // every POSIX filesystem, and offering that name would inject an escape both
9
+ // into the picker and into the user's edit buffer. Anything that does not
10
+ // survive sanitize() unchanged is dropped, because a path we cannot print
11
+ // safely is also a path we cannot safely type.
12
+ //
13
+ // Pure Node. No dependency, no shell.
14
+
15
+ import fs from 'fs';
16
+ import fsp from 'fs/promises';
17
+ import path from 'path';
18
+ import { listCommands } from '../commands.js';
19
+ import { listCompiledSkills as listSkillsForCompletion } from '../skillsManager.js';
20
+ import { sanitize, shortenHome } from './text.js';
21
+
22
+ export const MAX_VISIBLE = 8;
23
+
24
+ // Directories that are never worth indexing. Walking node_modules turns a
25
+ // 40 ms index into a 40 s one and buries real files under vendored ones.
26
+ const SKIP_DIRECTORIES = new Set([
27
+ 'node_modules', '.git', 'dist', 'build', 'out', 'target',
28
+ '.next', '.nuxt', '.venv', 'venv', '__pycache__', '.gradle',
29
+ '.idea', '.cache', 'coverage', 'vendor', 'Pods', '.causyne'
30
+ ]);
31
+
32
+ const MAX_INDEX_ENTRIES = 20000;
33
+ const MAX_INDEX_DEPTH = 12;
34
+
35
+ /** A value is offerable only if printing it changes nothing. */
36
+ export function isTerminalSafe(value) {
37
+ const text = String(value == null ? '' : value);
38
+ return text.length > 0 && sanitize(text) === text;
39
+ }
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Matching
43
+ // ---------------------------------------------------------------------------
44
+
45
+ /**
46
+ * Score `candidate` against `query`, or -1 for no match.
47
+ *
48
+ * Prefix beats subsequence, and a match on the basename beats one buried in a
49
+ * parent directory. Substring containment is deliberately NOT the test on its
50
+ * own — rules.json ZM-09: a naive `includes()` both over-matches unrelated
51
+ * paths and under-ranks the file the user meant.
52
+ */
53
+ export function score(candidate, query) {
54
+ if (!query) return 0;
55
+ const haystack = candidate.toLowerCase();
56
+ const needle = query.toLowerCase();
57
+
58
+ if (haystack.startsWith(needle)) return 1000 - candidate.length;
59
+
60
+ const base = haystack.slice(haystack.lastIndexOf('/') + 1);
61
+ if (base.startsWith(needle)) return 800 - candidate.length;
62
+ if (base.includes(needle)) return 600 - candidate.length;
63
+
64
+ // Subsequence: every character of the query in order, gaps allowed. This is
65
+ // what lets "tuich" find "tui/chatRenderer.js".
66
+ let index = 0;
67
+ let gaps = 0;
68
+ for (const character of haystack) {
69
+ if (character === needle[index]) {
70
+ index += 1;
71
+ if (index === needle.length) return 400 - gaps - candidate.length;
72
+ } else if (index > 0) {
73
+ gaps += 1;
74
+ }
75
+ }
76
+ return -1;
77
+ }
78
+
79
+ /**
80
+ * Score, sort and cap — and report how many matched BEFORE the cap.
81
+ *
82
+ * The cap exists for the file index, which can hold tens of thousands of paths.
83
+ * But the picker's "… N more" line was computed from the capped array, so with
84
+ * 115 skill entries it announced "42 more" when 107 were reachable by scrolling.
85
+ * A count that is wrong is worse than no count: it tells the user to stop
86
+ * looking. The total travels with the items so the caller can say the truth.
87
+ */
88
+ function rank(items, query, limit = 50) {
89
+ const scored = [];
90
+ for (const item of items) {
91
+ const value = score(item.match !== undefined ? item.match : item.value, query);
92
+ if (value >= 0) scored.push({ item, value });
93
+ }
94
+ scored.sort((a, b) => b.value - a.value);
95
+ const ranked = scored.slice(0, limit).map((entry) => entry.item);
96
+ ranked.total = scored.length;
97
+ return ranked;
98
+ }
99
+
100
+ // A command's own vocabulary is bounded and small — a few hundred at most — and
101
+ // every entry is reachable by scrolling the picker. Capping it at the file-index
102
+ // limit hid two thirds of the skill library behind a number that denied it was
103
+ // there.
104
+ const ARGUMENT_LIMIT = 500;
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // `/` commands
108
+ // ---------------------------------------------------------------------------
109
+
110
+ function commandItems() {
111
+ const commands = listCommands('terminal')
112
+ .filter((command) => isTerminalSafe(command.name))
113
+ .map((command) => ({
114
+ value: '/' + command.name,
115
+ match: command.name,
116
+ label: '/' + command.name,
117
+ description: sanitize(command.description || '')
118
+ }));
119
+
120
+ // Skills answer to the slash menu directly, the way claude-code surfaces a
121
+ // skill as a command you simply type. `/docker` finds the Docker skill
122
+ // without the user knowing it lives under `devops`, or that `/skills` exists
123
+ // at all — which is the whole difficulty with a hundred-entry library.
124
+ //
125
+ // Ranked against the skill NAME first so the name is what matches, and
126
+ // resolved to the canonical `/skills <domain>/<id>` on accept, so there is
127
+ // still exactly one execution path and no second command namespace to collide
128
+ // with the real commands.
129
+ const skills = listSkillsForCompletion().map((skill) => ({
130
+ value: `/skills ${skill.domain}/${skill.id}`,
131
+ match: `${skill.id} ${skill.domain}`,
132
+ label: `/${skill.id}`,
133
+ description: sanitize(skill.description || '')
134
+ }));
135
+
136
+ // Commands first: on a bare `/` every entry scores 0, and a stable sort then
137
+ // leaves the real commands above the library.
138
+ return commands.concat(skills);
139
+ }
140
+
141
+ /**
142
+ * A command's own argument suggestions.
143
+ *
144
+ * The command owns them: it knows what its argument means and this module does
145
+ * not. Anything a command returns is treated as untrusted display text and
146
+ * sanitised, the same as a filename.
147
+ */
148
+ function argumentItems(name) {
149
+ const command = listCommands('terminal')
150
+ .find((candidate) => candidate.name === name && isTerminalSafe(candidate.name));
151
+ if (!command || typeof command.complete !== 'function') return [];
152
+
153
+ let produced;
154
+ try {
155
+ produced = command.complete();
156
+ } catch (e) {
157
+ return []; // a broken completer must never take the prompt down
158
+ }
159
+ if (!Array.isArray(produced)) return [];
160
+
161
+ return produced
162
+ .filter((entry) => entry && typeof entry.value === 'string' && isTerminalSafe(entry.value))
163
+ .map((entry) => ({
164
+ value: entry.value,
165
+ match: entry.match === undefined ? entry.value : entry.match,
166
+ label: sanitize(entry.label === undefined ? entry.value : entry.label),
167
+ description: sanitize(entry.description || '')
168
+ }));
169
+ }
170
+
171
+ // ---------------------------------------------------------------------------
172
+ // `@` files
173
+ // ---------------------------------------------------------------------------
174
+
175
+ /**
176
+ * A bounded, lazily built list of files under the workspace.
177
+ *
178
+ * openclaw shells out to `fd` and opencode uses `fff`; both are dependencies,
179
+ * and ZM-06 rules those out. ripgrep is not a safe substitute either — Causyne
180
+ * treats it as optional (src/tools/codeSearcher.js probes for a system binary
181
+ * and falls back to Node), so requiring it would make completion work on some
182
+ * machines and not others.
183
+ *
184
+ * So: the directory listing answers instantly and always, and this index
185
+ * upgrades the experience once it is ready. If it is never ready, nothing
186
+ * breaks — the caller must never await it.
187
+ */
188
+ export class FileIndex {
189
+ constructor(root) {
190
+ this.root = root;
191
+ this.entries = null;
192
+ this.building = null;
193
+ this.truncated = false;
194
+ }
195
+
196
+ get isReady() {
197
+ return Array.isArray(this.entries);
198
+ }
199
+
200
+ /** Kick off the walk. Safe to call repeatedly; only the first one runs. */
201
+ ensure() {
202
+ if (this.entries || this.building) return this.building;
203
+ this.building = this.walk()
204
+ .then((entries) => { this.entries = entries; return entries; })
205
+ .catch(() => { this.entries = []; return []; })
206
+ .finally(() => { this.building = null; });
207
+ return this.building;
208
+ }
209
+
210
+ async walk() {
211
+ const found = [];
212
+ const queue = [{ dir: this.root, depth: 0 }];
213
+
214
+ while (queue.length > 0 && found.length < MAX_INDEX_ENTRIES) {
215
+ const { dir, depth } = queue.shift();
216
+ let listing;
217
+ try {
218
+ listing = await fsp.readdir(dir, { withFileTypes: true });
219
+ } catch (e) {
220
+ continue; // unreadable directory is not an error worth surfacing here
221
+ }
222
+
223
+ for (const entry of listing) {
224
+ if (entry.name.startsWith('.') && entry.name !== '.env.example') continue;
225
+ const full = path.join(dir, entry.name);
226
+
227
+ if (entry.isDirectory()) {
228
+ if (SKIP_DIRECTORIES.has(entry.name) || depth >= MAX_INDEX_DEPTH) continue;
229
+ queue.push({ dir: full, depth: depth + 1 });
230
+ continue;
231
+ }
232
+ if (!entry.isFile()) continue;
233
+
234
+ const relative = path.relative(this.root, full);
235
+ if (!isTerminalSafe(relative)) continue;
236
+ found.push(relative);
237
+ if (found.length >= MAX_INDEX_ENTRIES) {
238
+ this.truncated = true;
239
+ break;
240
+ }
241
+ }
242
+ }
243
+ return found;
244
+ }
245
+ }
246
+
247
+ /** Instant, shell-style completion of one directory level. */
248
+ function directoryItems(root, partial) {
249
+ const slash = partial.lastIndexOf('/');
250
+ const dirPart = slash === -1 ? '' : partial.slice(0, slash + 1);
251
+ const target = path.resolve(root, dirPart);
252
+
253
+ // Never offer paths outside the workspace root, whatever the user typed.
254
+ const relativeToRoot = path.relative(root, target);
255
+ if (relativeToRoot.startsWith('..') || path.isAbsolute(relativeToRoot)) return [];
256
+
257
+ let listing;
258
+ try {
259
+ listing = fs.readdirSync(target, { withFileTypes: true });
260
+ } catch (e) {
261
+ return [];
262
+ }
263
+
264
+ const items = [];
265
+ for (const entry of listing) {
266
+ if (entry.name.startsWith('.')) continue;
267
+ if (entry.isDirectory() && SKIP_DIRECTORIES.has(entry.name)) continue;
268
+ const value = dirPart + entry.name + (entry.isDirectory() ? '/' : '');
269
+ if (!isTerminalSafe(value)) continue;
270
+ items.push({
271
+ value,
272
+ match: value,
273
+ label: value,
274
+ description: entry.isDirectory() ? 'directory' : ''
275
+ });
276
+ }
277
+ return items;
278
+ }
279
+
280
+ // ---------------------------------------------------------------------------
281
+ // Trigger detection
282
+ // ---------------------------------------------------------------------------
283
+
284
+ /**
285
+ * What, if anything, should be completed at the cursor.
286
+ *
287
+ * `/` only counts at the very start of the input — that is what keeps a task
288
+ * that merely mentions `/usr/local/bin` from opening a command picker, the same
289
+ * rule parseCommand() enforces when the line is finally submitted.
290
+ */
291
+ export function detectTrigger(text, cursor) {
292
+ const before = text.slice(0, cursor);
293
+
294
+ if (/^\/[a-z0-9_?-]*$/i.test(before)) {
295
+ return { kind: 'command', start: 0, query: before.slice(1) };
296
+ }
297
+
298
+ // `/command <argument>` — the command's OWN suggestions.
299
+ //
300
+ // Completion used to stop dead at the space: `/skills d` offered nothing, so
301
+ // the only way to reach a skill was to submit `/skills` bare and browse. With
302
+ // a hundred of them that is a poor trade for someone who already knows the
303
+ // name they want.
304
+ //
305
+ // openclaw's contract (src/tui/commands.ts): a command declares its argument
306
+ // completions and the prompt asks it for the ones matching what has been typed
307
+ // — `getArgumentCompletions(prefix)`. Same shape here, generic over every
308
+ // command, so `/mcp`, `/model` and the rest can adopt it by adding one
309
+ // function and nothing in this file changes.
310
+ //
311
+ // Anchored, and a single argument token only: a second word ends the
312
+ // completion rather than trying to complete inside a sentence. opencode hides
313
+ // its picker on exactly the same condition.
314
+ const argument = before.match(/^\/([a-z0-9_?-]+)[ \t]+(\S*)$/i);
315
+ if (argument) {
316
+ return {
317
+ kind: 'command-argument',
318
+ command: argument[1],
319
+ start: cursor - argument[2].length,
320
+ query: argument[2]
321
+ };
322
+ }
323
+
324
+ const mention = before.match(/(?:^|\s)@([^\s@]*)$/);
325
+ if (mention) {
326
+ return {
327
+ kind: 'file',
328
+ start: cursor - mention[1].length - 1,
329
+ query: mention[1]
330
+ };
331
+ }
332
+ return null;
333
+ }
334
+
335
+ /**
336
+ * Suggestions for the cursor position, or null when nothing should open.
337
+ *
338
+ * Synchronous by design: a picker that appears a beat after you type feels
339
+ * broken. The file index is consulted only if it has already finished.
340
+ */
341
+ export function suggest(text, cursor, options = {}) {
342
+ const trigger = detectTrigger(text, cursor);
343
+ if (!trigger) return null;
344
+
345
+ if (trigger.kind === 'command') {
346
+ const items = rank(commandItems(), trigger.query);
347
+ if (items.length === 0) return null;
348
+ return { ...trigger, items, total: items.total };
349
+ }
350
+
351
+ if (trigger.kind === 'command-argument') {
352
+ const items = rank(argumentItems(trigger.command), trigger.query, ARGUMENT_LIMIT);
353
+ if (items.length === 0) return null;
354
+ return { ...trigger, items, total: items.total };
355
+ }
356
+
357
+ const root = options.root || process.cwd();
358
+ const index = options.index;
359
+ if (index && !index.isReady) index.ensure();
360
+
361
+ const pool = index && index.isReady && trigger.query.includes('/') === false && trigger.query
362
+ ? index.entries.map((relative) => ({
363
+ value: relative, match: relative, label: relative, description: ''
364
+ }))
365
+ : directoryItems(root, trigger.query);
366
+
367
+ const items = rank(pool, trigger.query.slice(trigger.query.lastIndexOf('/') + 1));
368
+ if (items.length === 0) return null;
369
+ return { ...trigger, items, total: items.total };
370
+ }
371
+
372
+ /** Replace the trigger span with the chosen value. */
373
+ export function applyCompletion(text, cursor, trigger, item) {
374
+ if (trigger.kind === 'command-argument') {
375
+ const head = text.slice(0, trigger.start);
376
+ const tail = text.slice(cursor);
377
+
378
+ // A COMPLETE argument gets a trailing space; a partial one does not.
379
+ //
380
+ // This was written the other way round, on the theory that a space would
381
+ // reopen the picker on a value still being edited. It does not — a
382
+ // completed argument followed by a space matches no trigger at all. What it
383
+ // actually did was run the user's next words straight into the argument:
384
+ // accepting `python/data-processing-pipelines` and typing a question gave
385
+ // `/skills python/data-processing-pipelineswhy my data is like this ?` and
386
+ // a "skill not found" error.
387
+ //
388
+ // The distinction is the same one file completion already makes between a
389
+ // directory and a file: a domain (`devops`) is a place you may want to
390
+ // descend into with `/`, so it gets no space; a full `domain/skill` id is
391
+ // finished, so it gets one and the next thing typed is the instruction.
392
+ const complete = item.value.includes('/');
393
+ const replaced = head + item.value + (complete ? ' ' : '');
394
+ return { text: replaced + tail, cursor: replaced.length };
395
+ }
396
+
397
+ const value = trigger.kind === 'command' ? item.value : '@' + item.value;
398
+ const head = text.slice(0, trigger.start);
399
+ const tail = text.slice(cursor);
400
+ // A directory keeps the picker useful: no trailing space, so the next
401
+ // keystroke drills in rather than starting a new word.
402
+ //
403
+ // Everything else — including a skill picked from the slash menu, whose value
404
+ // is a whole `/skills domain/id` command — gets one, so the next thing typed
405
+ // is a new word rather than more of the last one. The exclusion that used to
406
+ // sit here for values containing a space was the same mistake as above: it
407
+ // made `/docker` complete to `/skills devops/docker-multi-stage-builds` with
408
+ // nothing after it, and the user's instruction fused onto the id.
409
+ const suffix = (trigger.kind === 'file' && item.value.endsWith('/')) ? '' : ' ';
410
+ const replaced = head + value + suffix;
411
+ return { text: replaced + tail, cursor: replaced.length };
412
+ }
413
+
414
+ /** Rows for the picker, already bounded and safe to print. */
415
+ export function renderPicker(items, selected, width) {
416
+ const start = Math.max(0, Math.min(selected - MAX_VISIBLE + 1, items.length - MAX_VISIBLE));
417
+ const window = items.slice(Math.max(0, start), Math.max(0, start) + MAX_VISIBLE);
418
+ return window.map((item, offset) => ({
419
+ item,
420
+ selected: Math.max(0, start) + offset === selected,
421
+ label: shortenHome(item.label),
422
+ description: item.description || ''
423
+ }));
424
+ }