@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
@@ -0,0 +1,805 @@
1
+ // Sovereign Slash-Command Registry — one table for the local terminal.
2
+ //
3
+ // WHY ONE TABLE
4
+ // Commands are declared once and rendered by the terminal chat and completion
5
+ // UI, so help, parsing and execution cannot drift apart.
6
+ //
7
+ // WHAT A COMMAND IS
8
+ // A thin, synchronous-ish surface over capability that already exists in the
9
+ // local harness. /clear is memoryManager.clearHistory(), /stop is the abort controller,
10
+ // /undo is src/checkpoints.js. Nothing here reimplements behaviour; it exposes it.
11
+ //
12
+ // ZERO-MALWARE: pure JS over existing modules. No shell, no dependency, no eval.
13
+
14
+ import { COMPILED_SKILLS } from './compiledSkills.js';
15
+ import {
16
+ readCompiledSkill, listCompiledSkills, invalidateUserSkills, userSkillProblems
17
+ } from './skillsManager.js';
18
+ import {
19
+ userSkillsRoot, parseSkillId, writeUserSkill, deleteUserSkill, skillTemplate
20
+ } from './userSkills.js';
21
+ import { buildContextText, buildUsageText, buildCheckpointsText, buildUndoText, buildStatusText } from './reporting.js';
22
+ import { loadConfig, saveConfig } from './config.js';
23
+ import { serverStatus, grantServer, revokeServer, addServer, removeServer } from './mcp/mcpRegistry.js';
24
+ import { analyzeFiles, formatFindings } from './slopDetector.js';
25
+ import { suggestMovements, formatMovementBrief, DESIGN_MOVEMENTS } from './designPalettes.js';
26
+ import fs from 'fs';
27
+ import os from 'os';
28
+ import path from 'path';
29
+
30
+ export const SCOPES = ['local'];
31
+
32
+ function ok(output, extra = {}) {
33
+ return { ok: true, output, ...extra };
34
+ }
35
+ function fail(output) {
36
+ return { ok: false, output };
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // The table
41
+ // ---------------------------------------------------------------------------
42
+
43
+ export const COMMANDS = [
44
+ {
45
+ name: 'help',
46
+ aliases: ['?', 'commands'],
47
+ scope: 'local',
48
+ usage: '/help [command]',
49
+ description: 'List the available commands',
50
+ run: (argv, ctx) => {
51
+ const [wanted] = argv;
52
+ if (wanted) {
53
+ const command = resolveCommand(wanted);
54
+ if (!command) return fail(`No command named "${wanted}". Try /help.`);
55
+ const aliases = command.aliases?.length ? `\n aliases : ${command.aliases.map(a => '/' + a).join(', ')}` : '';
56
+ return ok(`${command.usage}\n ${command.description}${aliases}`);
57
+ }
58
+ const visible = listCommands(ctx.surface);
59
+ const width = Math.max(...visible.map(c => c.usage.length));
60
+ const lines = ['Commands', ''];
61
+ for (const command of visible) {
62
+ lines.push(` ${command.usage.padEnd(width)} ${command.description}`);
63
+ }
64
+ lines.push('');
65
+ lines.push('Anything not starting with / is sent to the agent as a task.');
66
+ return ok(lines.join('\n'));
67
+ }
68
+ },
69
+
70
+ {
71
+ name: 'skill',
72
+ scope: 'local',
73
+ usage: '/skill new <domain>/<name> | /skill edit <domain>/<name> | /skill rm <domain>/<name> | /skill mine',
74
+ description: 'Create, edit or remove your own skills',
75
+
76
+ // Only the ACTIONS. This used to append the user's skill ids too, but a
77
+ // skill id is the second argument (`/skill rm team/x`) and completion only
78
+ // reaches the first — so accepting one produced `/skill team/x` and an
79
+ // "Unknown action" error. Offering something in a position where it cannot
80
+ // work is worse than offering nothing.
81
+ complete: () => {
82
+ const mine = listCompiledSkills().filter((skill) => skill.source === 'user');
83
+ return [
84
+ { value: 'new', label: 'new', description: 'scaffold a new skill you own' },
85
+ { value: 'mine', label: 'mine', description: `list your own skills (${mine.length})` },
86
+ { value: 'edit', label: 'edit', description: 'show where one of your skills lives' },
87
+ { value: 'rm', label: 'rm', description: 'remove one of your skills' }
88
+ ];
89
+ },
90
+
91
+ run: (argv) => {
92
+ const [action, target, ...rest] = argv;
93
+ const mine = () => listCompiledSkills().filter((skill) => skill.source === 'user');
94
+ if (rest.length > 0) {
95
+ return fail(`/skill takes at most one target. Ignored: ${JSON.stringify(rest.join(' '))}\n`
96
+ + 'A skill name is one word: lowercase letters, digits and hyphens.');
97
+ }
98
+
99
+ if (!action || action === 'mine') {
100
+ const own = mine();
101
+ const problems = userSkillProblems();
102
+ const lines = [`Your own skills (${own.length})`, ''];
103
+ if (own.length === 0) {
104
+ lines.push(` none yet — ${userSkillsRoot()}`);
105
+ lines.push('');
106
+ lines.push(' /skill new <domain>/<name> scaffold one');
107
+ lines.push(' or ask Causyne to write one for you: "make me a skill for our release process"');
108
+ } else {
109
+ for (const skill of own) lines.push(` ${skill.domain}/${skill.id} — ${skill.description}`);
110
+ lines.push('');
111
+ lines.push(` stored in ${userSkillsRoot()}`);
112
+ lines.push(' yours shadow a compiled skill of the same id, so you can correct any of them');
113
+ }
114
+ if (problems.length > 0) {
115
+ lines.push('');
116
+ lines.push('Problems:');
117
+ for (const problem of problems) lines.push(` ${problem}`);
118
+ }
119
+ return ok(lines.join('\n'));
120
+ }
121
+
122
+ if (action === 'new') {
123
+ const parsed = parseSkillId(target);
124
+ if (parsed.error) return fail(parsed.error);
125
+ const written = writeUserSkill({
126
+ domain: parsed.domain,
127
+ name: parsed.name,
128
+ content: skillTemplate(parsed.domain, parsed.name)
129
+ });
130
+ if (!written.ok) return fail(written.error);
131
+ invalidateUserSkills();
132
+ return ok([
133
+ `Created ${parsed.domain}/${parsed.name}`,
134
+ '',
135
+ ` ${written.path}`,
136
+ '',
137
+ 'Open it and fill in the workflow. Every step ends with a "Done when:" line you can',
138
+ 'actually check, and the file closes with a Verification section — that is what makes',
139
+ 'a skill a procedure rather than a note. The description decides when the agent reaches',
140
+ 'for it, so write it as "what this does. Use when …".'
141
+ ].join('\n'));
142
+ }
143
+
144
+ if (action === 'edit') {
145
+ const parsed = parseSkillId(target);
146
+ if (parsed.error) return fail(parsed.error);
147
+ const found = mine().find((s) => s.domain === parsed.domain && s.id === parsed.name);
148
+ if (!found) {
149
+ return fail(`${target} is not one of your skills. /skill mine lists them, `
150
+ + `/skill new ${target} creates it.`);
151
+ }
152
+ return ok(`${target}\n\n ${path.join(userSkillsRoot(), parsed.domain, parsed.name, 'SKILL.md')}`);
153
+ }
154
+
155
+ if (action === 'rm') {
156
+ const parsed = parseSkillId(target);
157
+ if (parsed.error) return fail(parsed.error);
158
+ const removed = deleteUserSkill(parsed.domain, parsed.name);
159
+ if (!removed.ok) return fail(removed.error);
160
+ invalidateUserSkills();
161
+ return ok(`Removed ${target}`);
162
+ }
163
+
164
+ return fail(`Unknown action "${action}". Use new, edit, rm or mine.`);
165
+ }
166
+ },
167
+ {
168
+ name: 'skills',
169
+ scope: 'local',
170
+ usage: '/skills [domain | domain/skill [instruction] | domain/skill --show]',
171
+ description: 'Run a skill on your task, browse the library, or read one with --show',
172
+
173
+ /**
174
+ * Argument suggestions, offered as the user types `/skills <prefix>`.
175
+ *
176
+ * openclaw's contract (src/tui/commands.ts getArgumentCompletions): the
177
+ * command owns its argument vocabulary and the prompt asks for it. Ranking
178
+ * and filtering are the prompt's job — this returns the whole vocabulary and
179
+ * completion.js scores it with the same function it uses for commands and
180
+ * filenames, so `/skills doc` reaches devops/docker-multi-stage-builds by a
181
+ * basename-prefix match rather than needing the domain typed first.
182
+ *
183
+ * Domains are offered alongside skills because `/skills devops` is a valid
184
+ * argument in its own right.
185
+ */
186
+ complete: () => {
187
+ const skills = listCompiledSkills();
188
+ const domains = [...new Set(skills.map((skill) => skill.domain))]
189
+ .map((domain) => ({
190
+ value: domain,
191
+ label: domain,
192
+ description: `${skills.filter((s) => s.domain === domain).length} skills`
193
+ }));
194
+ return domains.concat(skills.map((skill) => ({
195
+ value: `${skill.domain}/${skill.id}`,
196
+ // Rank against the skill id as well as the full path, so typing the
197
+ // skill name alone finds it without knowing which domain it lives in.
198
+ match: `${skill.domain}/${skill.id}`,
199
+ label: `${skill.domain}/${skill.id}`,
200
+ description: skill.description
201
+ })));
202
+ },
203
+ run: (argv) => {
204
+ const [target] = argv;
205
+
206
+ if (!target) {
207
+ const catalogue = listCompiledSkills();
208
+ if (catalogue.length === 0) return ok('No skills are compiled into this build.');
209
+
210
+ // An interactive terminal browses; everything else reads. The picker is
211
+ // fuzzy-searchable and truncates to its own pane, so a 100-skill library
212
+ // stays usable and cannot overflow the terminal — the failure the flat
213
+ // listing had at five skills, let alone a hundred.
214
+ const summary = ['Compiled skills', ''];
215
+ const byDomain = new Map();
216
+ for (const skill of catalogue) {
217
+ if (!byDomain.has(skill.domain)) byDomain.set(skill.domain, []);
218
+ byDomain.get(skill.domain).push(skill);
219
+ }
220
+ for (const [domain, skills] of byDomain) {
221
+ summary.push(` ${domain} (${skills.length})`);
222
+ for (const skill of skills) {
223
+ summary.push(` ${domain}/${skill.id} — ${skill.description || skill.name || ''}`);
224
+ }
225
+ }
226
+ summary.push('');
227
+ summary.push(`${catalogue.length} skills across ${byDomain.size} domains.`);
228
+ summary.push('Read one with /skills <domain>/<skill>.');
229
+
230
+ return { ok: true, output: summary.join('\n'), effect: 'pick-skill', items: catalogue };
231
+ }
232
+
233
+ if (target.includes('/')) {
234
+ const separator = target.indexOf('/');
235
+ const domain = target.slice(0, separator);
236
+ const id = target.slice(separator + 1);
237
+ const content = readCompiledSkill(domain, id);
238
+ if (typeof content === 'string' && content.startsWith('Error:')) return fail(content);
239
+
240
+ // Everything after the skill id is the TASK.
241
+ //
242
+ // `/skills` used to take argv[0] and discard the rest, so
243
+ // `/skills data/notebook-to-production please produksi ini` silently
244
+ // threw away "please produksi ini" and printed the document instead.
245
+ const instruction = argv.slice(1).join(' ').trim();
246
+
247
+ // `--show` reads it as a document. Anything else RUNS it.
248
+ if (instruction === '--show' || instruction === '-s') {
249
+ return ok(stripFrontmatter(content));
250
+ }
251
+
252
+ return {
253
+ ok: true,
254
+ effect: 'invoke-skill',
255
+ skill: { id: `${domain}/${id}`, content },
256
+ instruction,
257
+ output: `${domain}/${id}`
258
+ };
259
+ }
260
+
261
+ const skills = COMPILED_SKILLS[target];
262
+ if (!skills) {
263
+ return fail(`No skill domain "${target}". Available: ${Object.keys(COMPILED_SKILLS).join(', ')}`);
264
+ }
265
+ const names = Object.keys(skills);
266
+ if (names.length === 0) return ok(`Domain "${target}" has no compiled skills.`);
267
+
268
+ const listing = [`${target} skills (${names.length})`, '',
269
+ ...names.map(id => ` ${target}/${id} — ${skills[id].description || ''}`)].join('\n');
270
+
271
+ // Trailing text after a DOMAIN means the user tried to invoke something.
272
+ // A domain is not a skill, so it cannot be run — but silently listing and
273
+ // discarding what they typed is the same failure the skill branch had.
274
+ const extra = argv.slice(1).join(' ').trim();
275
+ if (extra && extra !== '--show' && extra !== '-s') {
276
+ return fail(`"${target}" is a domain, not a skill — there is nothing to run.\n`
277
+ + `Pick one of its skills and try again, for example:\n`
278
+ + ` /skills ${target}/${names[0]} ${extra}\n\n${listing}`);
279
+ }
280
+ return ok(listing);
281
+ }
282
+ },
283
+
284
+ {
285
+ name: 'undo',
286
+ scope: 'local',
287
+ usage: '/undo [taskId] [--step] [--full] [--apply]',
288
+ description: "Preview rolling back the last task's file changes (--apply to write)",
289
+ run: (argv) => {
290
+ const flags = new Set(argv.filter(a => a.startsWith('--')));
291
+ const taskId = argv.find(a => !a.startsWith('--'));
292
+
293
+ // PREVIEW by default. Typing /undo used to restore files immediately —
294
+ // no preview, no confirmation, and none of the running-task guard the
295
+ // structured rollback flow has. A chat box is not a place where one word
296
+ // should be able to overwrite a day's work, so the destructive form has
297
+ // to be asked for by name.
298
+ const apply = flags.has('--apply');
299
+
300
+ const { ok: succeeded, text } = buildUndoText(taskId, {
301
+ step: flags.has('--step'),
302
+ full: flags.has('--full'),
303
+ dryRun: !apply
304
+ });
305
+
306
+ if (!succeeded) return fail(text);
307
+ if (apply) return ok(text);
308
+ return ok(`${text}\n\nNothing was written. Run the same command with --apply to restore these files.`);
309
+ }
310
+ },
311
+
312
+ {
313
+ name: 'print',
314
+ scope: 'local',
315
+ usage: '/print',
316
+ description: 'Flush this session into the terminal scrollback and start the stage clean',
317
+ run: (argv, ctx) => {
318
+ // The Stage keeps the transcript in memory and paints a window of it, so
319
+ // until you exit, nothing is in your terminal's own scrollback to grep,
320
+ // copy or pipe. This is the way out without ending the session — and it
321
+ // prints BEFORE clearing, because ctrl+L in a terminal leaves your history
322
+ // behind and so must this.
323
+ const surface = ctx && ctx.prompt && ctx.prompt.surface;
324
+ if (!surface) {
325
+ return ok('Nothing to flush: this session already prints straight into your terminal.');
326
+ }
327
+ const rows = surface.flushAndClear();
328
+ return ok(`Printed ${rows} row${rows === 1 ? '' : 's'} into your terminal scrollback.`);
329
+ }
330
+ },
331
+
332
+ {
333
+ name: 'checkpoints',
334
+ aliases: ['ck'],
335
+ scope: 'local',
336
+ usage: '/checkpoints [count]',
337
+ description: 'List recent undo checkpoints',
338
+ run: (argv) => ok(buildCheckpointsText(parseInt(argv[0], 10) || 15))
339
+ },
340
+
341
+ {
342
+ name: 'context',
343
+ aliases: ['ctx'],
344
+ scope: 'local',
345
+ usage: '/context',
346
+ description: 'Show what fills the context window',
347
+ run: () => ok(buildContextText())
348
+ },
349
+
350
+ {
351
+ name: 'usage',
352
+ aliases: ['cost'],
353
+ scope: 'local',
354
+ usage: '/usage',
355
+ description: 'Show token usage and estimated cost',
356
+ run: async () => ok(await buildUsageText())
357
+ },
358
+
359
+ {
360
+ name: 'cortex',
361
+ aliases: ['brain'],
362
+ scope: 'local',
363
+ usage: '/cortex [list | forget <id> | forget-here]',
364
+ description: 'Inspect what the agent has learned, and forget any of it',
365
+ // A memory store you cannot read is not a memory, it is a black box that
366
+ // occasionally changes the agent's behaviour. Section 4 asks for intelligent
367
+ // safety rather than opacity, and section 10 layer 5 asks for a trail a
368
+ // human can review. This is that surface.
369
+ run: async (args = []) => {
370
+ const { cortexDb } = await import('./cortex/vectorDb.js');
371
+ const verb = String(args[0] || '').toLowerCase();
372
+
373
+ try {
374
+ if (verb === 'forget' && args[1]) {
375
+ const removed = await cortexDb.forget({ id: args[1] });
376
+ return removed ? ok('Forgotten.') : fail(`No memory "${args[1]}".`);
377
+ }
378
+
379
+ if (verb === 'forget-here') {
380
+ const removed = await cortexDb.forget({ workspace: process.cwd() });
381
+ return ok(removed === 0
382
+ ? 'Nothing was learned in this project yet.'
383
+ : `Forgot ${removed} memor${removed === 1 ? 'y' : 'ies'} from this project. Global lessons are untouched.`);
384
+ }
385
+
386
+ const stats = await cortexDb.stats();
387
+ if (stats.total === 0) {
388
+ return ok('Nothing learned yet. Cortex records a lesson when a task recovers from a real error.');
389
+ }
390
+
391
+ const kinds = stats.byKind.map((k) => `${k.c} ${k.kind}`).join(', ');
392
+ const age = stats.newest ? new Date(stats.newest).toISOString().slice(0, 10) : 'never';
393
+ const header = `${stats.total} memories (${kinds}) across ${stats.workspaces} project(s). Newest: ${age}.`;
394
+
395
+ if (verb !== 'list') {
396
+ return ok(header + '\n\nSee them with /cortex list. Forget this project\'s with /cortex forget-here.');
397
+ }
398
+
399
+ const rows = await cortexDb.inventory({ workspace: process.cwd() });
400
+ if (rows.length === 0) {
401
+ return ok(header + '\n\nNothing applies to this project.');
402
+ }
403
+
404
+ const home = process.env.HOME || '';
405
+ const lines = rows.slice(0, 15).map((r) => {
406
+ const when = new Date(r.timestamp).toISOString().slice(0, 10);
407
+ const where = r.workspace ? (home ? r.workspace.replace(home, '~') : r.workspace) : 'global';
408
+ const first = String(r.text).split('\n').filter(Boolean).slice(0, 2).join(' — ').slice(0, 96);
409
+ return ` ${r.id} ${when} ${String(r.hits).padStart(3)} hits [${r.kind}] ${where}\n ${first}`;
410
+ });
411
+
412
+ return ok(header + '\n\nApplying to this project:\n' + lines.join('\n')
413
+ + '\n\nForget one with /cortex forget <id>.');
414
+ } catch (e) {
415
+ // The neural engine is provisioned at runtime and can be absent. Saying
416
+ // so is the point: a silent Cortex is indistinguishable from an empty
417
+ // one, and the user is entitled to know which they have.
418
+ return fail(`Cortex is unavailable: ${e.message}. Memories are not being recorded or retrieved.`);
419
+ }
420
+ }
421
+ },
422
+
423
+ {
424
+ name: 'sessions',
425
+ aliases: ['session'],
426
+ scope: 'local',
427
+ usage: '/sessions [switch <id> | delete <id>]',
428
+ description: 'List chat sessions, or switch to one',
429
+ // Sessions replaced ONE global chat_history.json that every directory read.
430
+ // A session you cannot list or switch to is not a session, so this ships
431
+ // with the store rather than after it.
432
+ run: async (args = [], context = {}) => {
433
+ const store = await import('./sessionStore.js');
434
+ const verb = String(args[0] || '').toLowerCase();
435
+ const memory = context.loop && context.loop.memoryManager;
436
+
437
+ if (verb === 'switch' && args[1]) {
438
+ const next = memory ? memory.useSession(args[1]) : store.switchSession(args[1]);
439
+ if (!next) return fail(`No session "${args[1]}".`);
440
+ if (context.loop) context.loop.messages = next.messages.filter(m => m.role !== 'system');
441
+ return ok(`Switched to "${next.title}" (${next.messages.length} messages).`);
442
+ }
443
+
444
+ if (verb === 'delete' && args[1]) {
445
+ return store.deleteSession(args[1]) ? ok('Session deleted.') : fail(`No session "${args[1]}".`);
446
+ }
447
+
448
+ const rows = store.listSessions();
449
+ if (rows.length === 0) {
450
+ return ok('No sessions yet. This conversation becomes one as soon as you send something.');
451
+ }
452
+
453
+ const active = store.getActiveSessionId();
454
+ const home = process.env.HOME || '';
455
+ const lines = rows.slice(0, 20).map((s) => {
456
+ const when = new Date(s.updatedAt).toISOString().slice(0, 16).replace('T', ' ');
457
+ const where = s.workspace ? (home ? s.workspace.replace(home, '~') : s.workspace) : '(no workspace)';
458
+ const mark = s.id === active ? '*' : ' ';
459
+ return ` ${mark} ${s.id} ${when} ${String(s.messageCount).padStart(4)} msg ${where}\n ${s.title}`;
460
+ });
461
+
462
+ return ok(
463
+ `${rows.length} session${rows.length === 1 ? '' : 's'} (* = active):\n`
464
+ + lines.join('\n')
465
+ + '\n\nSwitch with /sessions switch <id>. A new directory always starts a new session.'
466
+ );
467
+ }
468
+ },
469
+
470
+ {
471
+ name: 'approvals',
472
+ scope: 'local',
473
+ usage: '/approvals [clear]',
474
+ description: 'List tools granted "Allow always", or revoke them',
475
+ // A standing grant with no way to see or revoke it is a trap, not a
476
+ // convenience. This is the way out, and the confirm dialog that grants one
477
+ // names this command so the user is told about it at the moment it matters.
478
+ run: async (args = []) => {
479
+ const { listAlwaysAllowed, revokeAllAlways } = await import('./approvalGrants.js');
480
+
481
+ if (String(args[0] || '').toLowerCase() === 'clear') {
482
+ const count = revokeAllAlways();
483
+ return ok(count === 0
484
+ ? 'No standing approvals to revoke.'
485
+ : `Revoked ${count} standing approval${count === 1 ? '' : 's'}. Causyne will ask again.`);
486
+ }
487
+
488
+ const granted = listAlwaysAllowed();
489
+ if (granted.length === 0) {
490
+ return ok('No standing approvals. Every gated action still asks.');
491
+ }
492
+ return ok(
493
+ 'Running without asking, until Causyne restarts:\n'
494
+ + granted.map((tool) => ` - ${tool}`).join('\n')
495
+ + '\n\nRevoke with /approvals clear.'
496
+ );
497
+ }
498
+ },
499
+
500
+ {
501
+ name: 'clear',
502
+ aliases: ['reset', 'new'],
503
+ scope: 'local',
504
+ usage: '/clear',
505
+ description: 'Clear the conversation and start from a fresh context',
506
+ run: (argv, ctx) => {
507
+ const loop = ctx.loop;
508
+ if (loop) {
509
+ loop.messages = [];
510
+ loop.unlockedPacks = new Set();
511
+ loop.unlockedTools = new Set();
512
+ loop.unlockOrder = [];
513
+ loop.staticContext = null;
514
+ loop.isInitialized = false;
515
+ if (typeof loop.rearmAbortController === 'function') loop.rearmAbortController();
516
+ if (loop.memoryManager) loop.memoryManager.clearHistory();
517
+ return ok('Conversation cleared. The next task starts from a clean context.');
518
+ }
519
+ return fail('No active session to clear.');
520
+ }
521
+ },
522
+
523
+ {
524
+ name: 'stop',
525
+ aliases: ['abort'],
526
+ scope: 'local',
527
+ usage: '/stop',
528
+ description: 'Stop the task that is currently running',
529
+ run: (argv, ctx) => {
530
+ const loop = ctx.loop;
531
+ if (loop && typeof loop.abort === 'function') {
532
+ loop.abort();
533
+ return ok('Stop requested.');
534
+ }
535
+ return fail('Nothing is running.');
536
+ }
537
+ },
538
+
539
+ {
540
+ name: 'model',
541
+ scope: 'local',
542
+ usage: '/model [provider/model | model]',
543
+ description: 'Show or change the model in use',
544
+ run: (argv) => {
545
+ const config = loadConfig();
546
+ if (argv.length === 0) {
547
+ return ok(`Provider : ${config.provider || 'default'}\nModel : ${config.model || 'default'}`);
548
+ }
549
+ const requested = argv.join(' ').trim();
550
+ const separator = requested.indexOf('/');
551
+ // A bare name changes only the model; "provider/model" changes both. Any
552
+ // string is accepted — model ids move faster than any hardcoded list, and
553
+ // guessing from memory is exactly what .agents/AGENTS.md §3 forbids.
554
+ if (separator > 0) {
555
+ config.provider = requested.slice(0, separator);
556
+ config.model = requested.slice(separator + 1);
557
+ } else {
558
+ config.model = requested;
559
+ }
560
+ saveConfig(config);
561
+ return ok(`Now using ${config.provider} / ${config.model}.`);
562
+ }
563
+ },
564
+
565
+ {
566
+ name: 'mcp',
567
+ scope: 'local',
568
+ usage: '/mcp [add <name> <cmd…> | add <name> --url <url> | remove|grant|revoke <name>]',
569
+ description: 'Manage MCP servers: list, add, remove, grant, revoke',
570
+ run: (argv) => {
571
+ const [action, serverName, ...rest] = argv;
572
+
573
+ if (action === 'add') {
574
+ if (!serverName) {
575
+ return fail('Usage: mcp add <name> <command> [args…]\n or: mcp add <name> --url <https://…>');
576
+ }
577
+ if (rest.length === 0) return fail(`Give "${serverName}" a command to run, or --url for an HTTP server.`);
578
+
579
+ let definition;
580
+ if (rest[0] === '--url') {
581
+ if (!rest[1]) return fail('--url needs a URL.');
582
+ definition = { name: serverName, url: rest[1] };
583
+ } else {
584
+ definition = { name: serverName, command: rest[0], args: rest.slice(1) };
585
+ }
586
+
587
+ const result = addServer(definition);
588
+ if (!result.ok) return fail(`Could not add "${serverName}": ${result.error}`);
589
+
590
+ // Registering is not consenting — say so, because a stdio server is a
591
+ // process Causyne will spawn on this machine.
592
+ return ok(
593
+ `Added "${serverName}".\n\n`
594
+ + `It is NOT enabled yet. A stdio MCP server is a program Causyne runs on\n`
595
+ + `this computer, so enabling it is a separate, deliberate step:\n`
596
+ + ` mcp grant ${serverName}`
597
+ );
598
+ }
599
+
600
+ if (action === 'remove') {
601
+ if (!serverName) return fail('Usage: /mcp remove <server>');
602
+ const result = removeServer(serverName);
603
+ if (!result.ok) return fail(result.error);
604
+ return ok(`Removed "${serverName}" and dropped its consent.`);
605
+ }
606
+
607
+ if (action === 'grant' || action === 'revoke') {
608
+ if (!serverName) return fail(`Usage: /mcp ${action} <server>`);
609
+ const known = serverStatus().some(s => s.name === serverName);
610
+ if (!known) return fail(`No MCP server named "${serverName}" is configured.`);
611
+ if (action === 'grant') {
612
+ grantServer(serverName);
613
+ return ok(`Granted "${serverName}". Its tools enter the catalogue on the next task.`);
614
+ }
615
+ revokeServer(serverName);
616
+ return ok(`Revoked "${serverName}". Its tools are no longer callable.`);
617
+ }
618
+
619
+ if (action) {
620
+ return fail(`Unknown mcp action "${action}". Try: add, remove, grant, revoke, or mcp on its own to list.`);
621
+ }
622
+
623
+ const servers = serverStatus();
624
+ if (servers.length === 0) {
625
+ return ok('No MCP servers are configured.\n\nAdd one with:\n mcp add <name> <command> [args…]\n mcp add <name> --url <https://…>\n\n(prefix with / in chat, or `causyne-agent mcp …` in a terminal)');
626
+ }
627
+ const lines = ['MCP servers', ''];
628
+ for (const server of servers) {
629
+ lines.push(` ${server.name} [${server.transport}] ${server.granted ? 'granted' : 'NOT granted'}${server.connected ? ', connected' : ''}`);
630
+ lines.push(` ${server.target}`);
631
+ if (server.tools.length > 0) lines.push(` tools: ${server.tools.join(', ')}`);
632
+ }
633
+ lines.push('');
634
+ lines.push(' mcp grant <name> to enable, mcp remove <name> to delete.');
635
+ return ok(lines.join('\n'));
636
+ }
637
+ },
638
+
639
+ {
640
+ name: 'slop',
641
+ scope: 'local',
642
+ usage: '/slop <file… | directory> [--movements]',
643
+ description: 'Score HTML/CSS against the anti-AI-slop rules (Vision §7)',
644
+ run: (argv) => {
645
+ const wantsMovements = argv.includes('--movements');
646
+ const targets = argv.filter(a => !a.startsWith('--'));
647
+
648
+ if (wantsMovements && targets.length === 0) {
649
+ return ok('Design movements to commit to:\n\n' + formatMovementBrief(DESIGN_MOVEMENTS));
650
+ }
651
+ if (targets.length === 0) {
652
+ return fail('Usage: slop <file… | directory> (or: slop --movements to see the catalogue)');
653
+ }
654
+
655
+ const files = [];
656
+ const collect = (target) => {
657
+ const resolved = path.resolve(String(target).replace(/^~\//, os.homedir() + '/'));
658
+ let stats;
659
+ try { stats = fs.statSync(resolved); } catch (e) { return `not found: ${target}`; }
660
+
661
+ if (stats.isDirectory()) {
662
+ const stack = [resolved];
663
+ while (stack.length > 0 && files.length < 200) {
664
+ const dir = stack.pop();
665
+ let entries;
666
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (e) { continue; }
667
+ for (const entry of entries) {
668
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
669
+ const full = path.join(dir, entry.name);
670
+ if (entry.isDirectory()) stack.push(full);
671
+ else if (entry.isFile()) {
672
+ try { files.push({ path: full, content: fs.readFileSync(full, 'utf8') }); } catch (e) {}
673
+ }
674
+ }
675
+ }
676
+ return null;
677
+ }
678
+ try { files.push({ path: resolved, content: fs.readFileSync(resolved, 'utf8') }); } catch (e) {
679
+ return `could not read: ${target}`;
680
+ }
681
+ return null;
682
+ };
683
+
684
+ const problems = targets.map(collect).filter(Boolean);
685
+ if (files.length === 0) {
686
+ return fail(problems.length > 0 ? problems.join('\n') : 'Nothing to scan.');
687
+ }
688
+
689
+ const result = analyzeFiles(files);
690
+ if (result.scanned.length === 0) {
691
+ return ok('No HTML/CSS/component files among the targets — nothing to score.');
692
+ }
693
+ if (result.findings.length === 0) {
694
+ return ok(`Scanned ${result.scanned.length} file(s). Score 0 — clean. No slop signatures found.`);
695
+ }
696
+ const brief = result.verdict === 'slop' ? formatMovementBrief(suggestMovements([], 3)) : '';
697
+ return ok(`Scanned ${result.scanned.length} file(s).\n\n` + formatFindings(result, brief));
698
+ }
699
+ },
700
+
701
+ {
702
+ name: 'status',
703
+ scope: 'local',
704
+ usage: '/status',
705
+ description: 'Show the current provider, autonomy mode and workspace scope',
706
+ // One builder, shared with `causyne status`, so status output cannot drift.
707
+ run: (argv, ctx) => ok(buildStatusText({ loop: ctx.loop || null }))
708
+ },
709
+
710
+ {
711
+ name: 'exit',
712
+ aliases: ['quit'],
713
+ scope: 'local',
714
+ usage: '/exit',
715
+ description: 'Leave the chat',
716
+ run: () => ok('Goodbye.', { effect: 'exit' })
717
+ }
718
+ ];
719
+
720
+ // ---------------------------------------------------------------------------
721
+ // Lookup and parsing
722
+ // ---------------------------------------------------------------------------
723
+
724
+ export function resolveCommand(name) {
725
+ const normalized = String(name || '').replace(/^\//, '').trim().toLowerCase();
726
+ if (!normalized) return null;
727
+ return COMMANDS.find(c => c.name === normalized || (c.aliases || []).includes(normalized)) || null;
728
+ }
729
+
730
+ /** Commands visible on the local terminal surface. */
731
+ /**
732
+ * Drop the YAML frontmatter before showing a skill to a human.
733
+ *
734
+ * The frontmatter is routing metadata for the agent — `name:` and
735
+ * `description:` — and printing it verbatim is why an invoked skill looked like
736
+ * a file dump rather than a procedure.
737
+ */
738
+ function stripFrontmatter(content) {
739
+ return String(content == null ? '' : content).replace(/^---\n[\s\S]*?\n---\n*/, '');
740
+ }
741
+
742
+ export function listCommands(surface = 'terminal') {
743
+ return surface === 'terminal' ? COMMANDS.slice() : [];
744
+ }
745
+
746
+ /**
747
+ * Split a raw input line into a command and its arguments.
748
+ *
749
+ * Returns null for anything that is not a slash command, which is the signal to
750
+ * the caller that this line is a task for the agent. A bare "/" is not a
751
+ * command, and neither is a path like "/usr/local/bin" — requiring a letter
752
+ * immediately after the slash keeps absolute paths out of the command namespace.
753
+ */
754
+ export function parseCommand(input) {
755
+ if (typeof input !== 'string') return null;
756
+ const trimmed = input.trim();
757
+ if (!trimmed.startsWith('/')) return null;
758
+
759
+ const parts = trimmed.slice(1).split(/\s+/).filter(Boolean);
760
+ const name = (parts.shift() || '').toLowerCase();
761
+
762
+ // The name must be a single bare word. Requiring that is what keeps absolute
763
+ // paths out of the command namespace: "/usr/local/bin" has a slash in its
764
+ // first token, so it is a task for the agent, not a command named
765
+ // "usr/local/bin". Anchored, not a loose prefix test — see rules.json ZM-09
766
+ // on why guards must be context-aware.
767
+ if (!/^[a-z?][a-z0-9_-]*$/.test(name)) return null;
768
+
769
+ return { name, argv: parts, raw: trimmed };
770
+ }
771
+
772
+ export function isCommandInput(input) {
773
+ return parseCommand(input) !== null;
774
+ }
775
+
776
+ /**
777
+ * Execute a parsed command.
778
+ *
779
+ * Never throws: a command that fails reports it as output, because both surfaces
780
+ * render the result into a conversation rather than a stack trace.
781
+ */
782
+ export async function runCommand(name, argv = [], ctx = {}) {
783
+ const surface = ctx.surface || 'terminal';
784
+ const command = resolveCommand(name);
785
+
786
+ if (!command) {
787
+ return fail(`Unknown command "/${name}". Try /help.`);
788
+ }
789
+ if (surface !== 'terminal') {
790
+ return fail(`"/${command.name}" is not available on this surface.`);
791
+ }
792
+
793
+ try {
794
+ return await command.run(argv, { ...ctx, surface });
795
+ } catch (e) {
796
+ return fail(`/${command.name} failed: ${e.message}`);
797
+ }
798
+ }
799
+
800
+ /** Convenience for a surface holding a raw line. Returns null if not a command. */
801
+ export async function handleInput(input, ctx = {}) {
802
+ const parsed = parseCommand(input);
803
+ if (!parsed) return null;
804
+ return runCommand(parsed.name, parsed.argv, ctx);
805
+ }