@animus-labs/cortex 0.2.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 (293) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -0
  3. package/dist/budget-guard.d.ts +75 -0
  4. package/dist/budget-guard.d.ts.map +1 -0
  5. package/dist/budget-guard.js +142 -0
  6. package/dist/budget-guard.js.map +1 -0
  7. package/dist/compaction/compaction.d.ts +99 -0
  8. package/dist/compaction/compaction.d.ts.map +1 -0
  9. package/dist/compaction/compaction.js +302 -0
  10. package/dist/compaction/compaction.js.map +1 -0
  11. package/dist/compaction/failsafe.d.ts +57 -0
  12. package/dist/compaction/failsafe.d.ts.map +1 -0
  13. package/dist/compaction/failsafe.js +135 -0
  14. package/dist/compaction/failsafe.js.map +1 -0
  15. package/dist/compaction/index.d.ts +381 -0
  16. package/dist/compaction/index.d.ts.map +1 -0
  17. package/dist/compaction/index.js +979 -0
  18. package/dist/compaction/index.js.map +1 -0
  19. package/dist/compaction/microcompaction.d.ts +219 -0
  20. package/dist/compaction/microcompaction.d.ts.map +1 -0
  21. package/dist/compaction/microcompaction.js +536 -0
  22. package/dist/compaction/microcompaction.js.map +1 -0
  23. package/dist/compaction/observational/buffering.d.ts +225 -0
  24. package/dist/compaction/observational/buffering.d.ts.map +1 -0
  25. package/dist/compaction/observational/buffering.js +354 -0
  26. package/dist/compaction/observational/buffering.js.map +1 -0
  27. package/dist/compaction/observational/constants.d.ts +70 -0
  28. package/dist/compaction/observational/constants.d.ts.map +1 -0
  29. package/dist/compaction/observational/constants.js +507 -0
  30. package/dist/compaction/observational/constants.js.map +1 -0
  31. package/dist/compaction/observational/index.d.ts +219 -0
  32. package/dist/compaction/observational/index.d.ts.map +1 -0
  33. package/dist/compaction/observational/index.js +641 -0
  34. package/dist/compaction/observational/index.js.map +1 -0
  35. package/dist/compaction/observational/observer.d.ts +97 -0
  36. package/dist/compaction/observational/observer.d.ts.map +1 -0
  37. package/dist/compaction/observational/observer.js +424 -0
  38. package/dist/compaction/observational/observer.js.map +1 -0
  39. package/dist/compaction/observational/recall-tool.d.ts +27 -0
  40. package/dist/compaction/observational/recall-tool.d.ts.map +1 -0
  41. package/dist/compaction/observational/recall-tool.js +93 -0
  42. package/dist/compaction/observational/recall-tool.js.map +1 -0
  43. package/dist/compaction/observational/reflector.d.ts +94 -0
  44. package/dist/compaction/observational/reflector.d.ts.map +1 -0
  45. package/dist/compaction/observational/reflector.js +167 -0
  46. package/dist/compaction/observational/reflector.js.map +1 -0
  47. package/dist/compaction/observational/types.d.ts +271 -0
  48. package/dist/compaction/observational/types.d.ts.map +1 -0
  49. package/dist/compaction/observational/types.js +15 -0
  50. package/dist/compaction/observational/types.js.map +1 -0
  51. package/dist/context-manager.d.ts +134 -0
  52. package/dist/context-manager.d.ts.map +1 -0
  53. package/dist/context-manager.js +170 -0
  54. package/dist/context-manager.js.map +1 -0
  55. package/dist/cortex-agent.d.ts +1020 -0
  56. package/dist/cortex-agent.d.ts.map +1 -0
  57. package/dist/cortex-agent.js +3589 -0
  58. package/dist/cortex-agent.js.map +1 -0
  59. package/dist/error-classifier.d.ts +48 -0
  60. package/dist/error-classifier.d.ts.map +1 -0
  61. package/dist/error-classifier.js +152 -0
  62. package/dist/error-classifier.js.map +1 -0
  63. package/dist/event-bridge.d.ts +166 -0
  64. package/dist/event-bridge.d.ts.map +1 -0
  65. package/dist/event-bridge.js +381 -0
  66. package/dist/event-bridge.js.map +1 -0
  67. package/dist/index.d.ts +55 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +57 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/mcp-client.d.ts +119 -0
  72. package/dist/mcp-client.d.ts.map +1 -0
  73. package/dist/mcp-client.js +474 -0
  74. package/dist/mcp-client.js.map +1 -0
  75. package/dist/model-wrapper.d.ts +58 -0
  76. package/dist/model-wrapper.d.ts.map +1 -0
  77. package/dist/model-wrapper.js +86 -0
  78. package/dist/model-wrapper.js.map +1 -0
  79. package/dist/noop-logger.d.ts +4 -0
  80. package/dist/noop-logger.d.ts.map +1 -0
  81. package/dist/noop-logger.js +8 -0
  82. package/dist/noop-logger.js.map +1 -0
  83. package/dist/prompt-diagnostics.d.ts +47 -0
  84. package/dist/prompt-diagnostics.d.ts.map +1 -0
  85. package/dist/prompt-diagnostics.js +230 -0
  86. package/dist/prompt-diagnostics.js.map +1 -0
  87. package/dist/provider-manager.d.ts +224 -0
  88. package/dist/provider-manager.d.ts.map +1 -0
  89. package/dist/provider-manager.js +563 -0
  90. package/dist/provider-manager.js.map +1 -0
  91. package/dist/provider-registry.d.ts +115 -0
  92. package/dist/provider-registry.d.ts.map +1 -0
  93. package/dist/provider-registry.js +305 -0
  94. package/dist/provider-registry.js.map +1 -0
  95. package/dist/schema-converter.d.ts +20 -0
  96. package/dist/schema-converter.d.ts.map +1 -0
  97. package/dist/schema-converter.js +48 -0
  98. package/dist/schema-converter.js.map +1 -0
  99. package/dist/skill-preprocessor.d.ts +46 -0
  100. package/dist/skill-preprocessor.d.ts.map +1 -0
  101. package/dist/skill-preprocessor.js +237 -0
  102. package/dist/skill-preprocessor.js.map +1 -0
  103. package/dist/skill-registry.d.ts +107 -0
  104. package/dist/skill-registry.d.ts.map +1 -0
  105. package/dist/skill-registry.js +330 -0
  106. package/dist/skill-registry.js.map +1 -0
  107. package/dist/skill-tool.d.ts +54 -0
  108. package/dist/skill-tool.d.ts.map +1 -0
  109. package/dist/skill-tool.js +88 -0
  110. package/dist/skill-tool.js.map +1 -0
  111. package/dist/sub-agent-manager.d.ts +90 -0
  112. package/dist/sub-agent-manager.d.ts.map +1 -0
  113. package/dist/sub-agent-manager.js +192 -0
  114. package/dist/sub-agent-manager.js.map +1 -0
  115. package/dist/token-estimator.d.ts +23 -0
  116. package/dist/token-estimator.d.ts.map +1 -0
  117. package/dist/token-estimator.js +27 -0
  118. package/dist/token-estimator.js.map +1 -0
  119. package/dist/tool-contract.d.ts +68 -0
  120. package/dist/tool-contract.d.ts.map +1 -0
  121. package/dist/tool-contract.js +35 -0
  122. package/dist/tool-contract.js.map +1 -0
  123. package/dist/tool-result-persistence.d.ts +89 -0
  124. package/dist/tool-result-persistence.d.ts.map +1 -0
  125. package/dist/tool-result-persistence.js +152 -0
  126. package/dist/tool-result-persistence.js.map +1 -0
  127. package/dist/tools/bash/index.d.ts +71 -0
  128. package/dist/tools/bash/index.d.ts.map +1 -0
  129. package/dist/tools/bash/index.js +485 -0
  130. package/dist/tools/bash/index.js.map +1 -0
  131. package/dist/tools/bash/interactive.d.ts +47 -0
  132. package/dist/tools/bash/interactive.d.ts.map +1 -0
  133. package/dist/tools/bash/interactive.js +262 -0
  134. package/dist/tools/bash/interactive.js.map +1 -0
  135. package/dist/tools/bash/safety.d.ts +149 -0
  136. package/dist/tools/bash/safety.d.ts.map +1 -0
  137. package/dist/tools/bash/safety.js +1116 -0
  138. package/dist/tools/bash/safety.js.map +1 -0
  139. package/dist/tools/edit.d.ts +57 -0
  140. package/dist/tools/edit.d.ts.map +1 -0
  141. package/dist/tools/edit.js +310 -0
  142. package/dist/tools/edit.js.map +1 -0
  143. package/dist/tools/glob.d.ts +34 -0
  144. package/dist/tools/glob.d.ts.map +1 -0
  145. package/dist/tools/glob.js +268 -0
  146. package/dist/tools/glob.js.map +1 -0
  147. package/dist/tools/grep.d.ts +53 -0
  148. package/dist/tools/grep.d.ts.map +1 -0
  149. package/dist/tools/grep.js +673 -0
  150. package/dist/tools/grep.js.map +1 -0
  151. package/dist/tools/index.d.ts +62 -0
  152. package/dist/tools/index.d.ts.map +1 -0
  153. package/dist/tools/index.js +52 -0
  154. package/dist/tools/index.js.map +1 -0
  155. package/dist/tools/read.d.ts +43 -0
  156. package/dist/tools/read.d.ts.map +1 -0
  157. package/dist/tools/read.js +459 -0
  158. package/dist/tools/read.js.map +1 -0
  159. package/dist/tools/runtime.d.ts +62 -0
  160. package/dist/tools/runtime.d.ts.map +1 -0
  161. package/dist/tools/runtime.js +116 -0
  162. package/dist/tools/runtime.js.map +1 -0
  163. package/dist/tools/shared/cwd-tracker.d.ts +32 -0
  164. package/dist/tools/shared/cwd-tracker.d.ts.map +1 -0
  165. package/dist/tools/shared/cwd-tracker.js +44 -0
  166. package/dist/tools/shared/cwd-tracker.js.map +1 -0
  167. package/dist/tools/shared/edit-history.d.ts +55 -0
  168. package/dist/tools/shared/edit-history.d.ts.map +1 -0
  169. package/dist/tools/shared/edit-history.js +72 -0
  170. package/dist/tools/shared/edit-history.js.map +1 -0
  171. package/dist/tools/shared/edit-matcher.d.ts +83 -0
  172. package/dist/tools/shared/edit-matcher.d.ts.map +1 -0
  173. package/dist/tools/shared/edit-matcher.js +359 -0
  174. package/dist/tools/shared/edit-matcher.js.map +1 -0
  175. package/dist/tools/shared/file-mutation-lock.d.ts +22 -0
  176. package/dist/tools/shared/file-mutation-lock.d.ts.map +1 -0
  177. package/dist/tools/shared/file-mutation-lock.js +35 -0
  178. package/dist/tools/shared/file-mutation-lock.js.map +1 -0
  179. package/dist/tools/shared/gitignore.d.ts +17 -0
  180. package/dist/tools/shared/gitignore.d.ts.map +1 -0
  181. package/dist/tools/shared/gitignore.js +59 -0
  182. package/dist/tools/shared/gitignore.js.map +1 -0
  183. package/dist/tools/shared/pdf-extractor.d.ts +96 -0
  184. package/dist/tools/shared/pdf-extractor.d.ts.map +1 -0
  185. package/dist/tools/shared/pdf-extractor.js +196 -0
  186. package/dist/tools/shared/pdf-extractor.js.map +1 -0
  187. package/dist/tools/shared/read-registry.d.ts +66 -0
  188. package/dist/tools/shared/read-registry.d.ts.map +1 -0
  189. package/dist/tools/shared/read-registry.js +65 -0
  190. package/dist/tools/shared/read-registry.js.map +1 -0
  191. package/dist/tools/shared/safe-env.d.ts +18 -0
  192. package/dist/tools/shared/safe-env.d.ts.map +1 -0
  193. package/dist/tools/shared/safe-env.js +70 -0
  194. package/dist/tools/shared/safe-env.js.map +1 -0
  195. package/dist/tools/sub-agent.d.ts +91 -0
  196. package/dist/tools/sub-agent.d.ts.map +1 -0
  197. package/dist/tools/sub-agent.js +89 -0
  198. package/dist/tools/sub-agent.js.map +1 -0
  199. package/dist/tools/task-output.d.ts +38 -0
  200. package/dist/tools/task-output.d.ts.map +1 -0
  201. package/dist/tools/task-output.js +186 -0
  202. package/dist/tools/task-output.js.map +1 -0
  203. package/dist/tools/tool-search/index.d.ts +40 -0
  204. package/dist/tools/tool-search/index.d.ts.map +1 -0
  205. package/dist/tools/tool-search/index.js +110 -0
  206. package/dist/tools/tool-search/index.js.map +1 -0
  207. package/dist/tools/tool-search/registry.d.ts +82 -0
  208. package/dist/tools/tool-search/registry.d.ts.map +1 -0
  209. package/dist/tools/tool-search/registry.js +238 -0
  210. package/dist/tools/tool-search/registry.js.map +1 -0
  211. package/dist/tools/undo-edit.d.ts +51 -0
  212. package/dist/tools/undo-edit.d.ts.map +1 -0
  213. package/dist/tools/undo-edit.js +231 -0
  214. package/dist/tools/undo-edit.js.map +1 -0
  215. package/dist/tools/web-fetch/cache.d.ts +49 -0
  216. package/dist/tools/web-fetch/cache.d.ts.map +1 -0
  217. package/dist/tools/web-fetch/cache.js +89 -0
  218. package/dist/tools/web-fetch/cache.js.map +1 -0
  219. package/dist/tools/web-fetch/index.d.ts +53 -0
  220. package/dist/tools/web-fetch/index.d.ts.map +1 -0
  221. package/dist/tools/web-fetch/index.js +513 -0
  222. package/dist/tools/web-fetch/index.js.map +1 -0
  223. package/dist/tools/write.d.ts +59 -0
  224. package/dist/tools/write.d.ts.map +1 -0
  225. package/dist/tools/write.js +316 -0
  226. package/dist/tools/write.js.map +1 -0
  227. package/dist/types.d.ts +881 -0
  228. package/dist/types.d.ts.map +1 -0
  229. package/dist/types.js +16 -0
  230. package/dist/types.js.map +1 -0
  231. package/dist/working-tags.d.ts +44 -0
  232. package/dist/working-tags.d.ts.map +1 -0
  233. package/dist/working-tags.js +103 -0
  234. package/dist/working-tags.js.map +1 -0
  235. package/package.json +87 -0
  236. package/src/budget-guard.ts +170 -0
  237. package/src/compaction/compaction.ts +386 -0
  238. package/src/compaction/failsafe.ts +185 -0
  239. package/src/compaction/index.ts +1199 -0
  240. package/src/compaction/microcompaction.ts +709 -0
  241. package/src/compaction/observational/buffering.ts +430 -0
  242. package/src/compaction/observational/constants.ts +532 -0
  243. package/src/compaction/observational/index.ts +837 -0
  244. package/src/compaction/observational/observer.ts +510 -0
  245. package/src/compaction/observational/recall-tool.ts +130 -0
  246. package/src/compaction/observational/reflector.ts +221 -0
  247. package/src/compaction/observational/types.ts +343 -0
  248. package/src/context-manager.ts +237 -0
  249. package/src/cortex-agent.ts +4297 -0
  250. package/src/error-classifier.ts +199 -0
  251. package/src/event-bridge.ts +508 -0
  252. package/src/index.ts +292 -0
  253. package/src/mcp-client.ts +582 -0
  254. package/src/model-wrapper.ts +128 -0
  255. package/src/noop-logger.ts +9 -0
  256. package/src/prompt-diagnostics.ts +296 -0
  257. package/src/provider-manager.ts +823 -0
  258. package/src/provider-registry.ts +386 -0
  259. package/src/schema-converter.ts +51 -0
  260. package/src/skill-preprocessor.ts +314 -0
  261. package/src/skill-registry.ts +378 -0
  262. package/src/skill-tool.ts +130 -0
  263. package/src/sub-agent-manager.ts +236 -0
  264. package/src/token-estimator.ts +26 -0
  265. package/src/tool-contract.ts +113 -0
  266. package/src/tool-result-persistence.ts +197 -0
  267. package/src/tools/bash/index.ts +633 -0
  268. package/src/tools/bash/interactive.ts +302 -0
  269. package/src/tools/bash/safety.ts +1297 -0
  270. package/src/tools/edit.ts +422 -0
  271. package/src/tools/glob.ts +330 -0
  272. package/src/tools/grep.ts +819 -0
  273. package/src/tools/index.ts +110 -0
  274. package/src/tools/read.ts +580 -0
  275. package/src/tools/runtime.ts +173 -0
  276. package/src/tools/shared/cwd-tracker.ts +50 -0
  277. package/src/tools/shared/edit-history.ts +96 -0
  278. package/src/tools/shared/edit-matcher.ts +457 -0
  279. package/src/tools/shared/file-mutation-lock.ts +40 -0
  280. package/src/tools/shared/gitignore.ts +61 -0
  281. package/src/tools/shared/pdf-extractor.ts +290 -0
  282. package/src/tools/shared/read-registry.ts +93 -0
  283. package/src/tools/shared/safe-env.ts +82 -0
  284. package/src/tools/sub-agent.ts +171 -0
  285. package/src/tools/task-output.ts +236 -0
  286. package/src/tools/tool-search/index.ts +167 -0
  287. package/src/tools/tool-search/registry.ts +278 -0
  288. package/src/tools/undo-edit.ts +314 -0
  289. package/src/tools/web-fetch/cache.ts +112 -0
  290. package/src/tools/web-fetch/index.ts +604 -0
  291. package/src/tools/write.ts +385 -0
  292. package/src/types.ts +1057 -0
  293. package/src/working-tags.ts +118 -0
@@ -0,0 +1,314 @@
1
+ /**
2
+ * Skill Preprocessor: processes SKILL.md body at load time.
3
+ *
4
+ * Three preprocessor types run in order:
5
+ * 1. Variable substitution: ${VAR}, $ARGUMENTS, $N
6
+ * 2. Shell commands: !`command` (parallel execution)
7
+ * 3. Script execution: !{script: path} (parallel execution)
8
+ *
9
+ * Shell commands use the same shell selection logic as the Bash tool
10
+ * (PowerShell on Windows, bash/zsh on Unix).
11
+ *
12
+ * References:
13
+ * - docs/cortex/skill-system.md
14
+ * - docs/cortex/cross-platform-considerations.md
15
+ */
16
+
17
+ import { execFile } from 'node:child_process';
18
+ import * as fs from 'node:fs';
19
+ import * as path from 'node:path';
20
+ import { pathToFileURL } from 'node:url';
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Types
24
+ // ---------------------------------------------------------------------------
25
+
26
+ export interface PreprocessorConfig {
27
+ /** Variables for ${VAR} and $N substitution. */
28
+ variables: Record<string, string>;
29
+ /** Context object passed to script executions. */
30
+ scriptContext: Record<string, unknown>;
31
+ /** Absolute path to the skill directory. */
32
+ skillDir: string;
33
+ }
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Constants
37
+ // ---------------------------------------------------------------------------
38
+
39
+ /** Timeout for each shell command or script execution. */
40
+ const COMMAND_TIMEOUT_MS = 10_000;
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Regex patterns
44
+ // ---------------------------------------------------------------------------
45
+
46
+ /** Match !`command` patterns (shell command markers). */
47
+ const SHELL_COMMAND_PATTERN = /^!`([^`]+)`$/gm;
48
+
49
+ /** Match !{script: path} or !{script: path, key: value, ...} patterns. */
50
+ const SCRIPT_PATTERN = /^!\{script:\s*([^,}]+)(?:,\s*([^}]+))?\}$/gm;
51
+
52
+ /** Match ${VAR} variable references. */
53
+ const VARIABLE_PATTERN = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
54
+
55
+ /** Match $N positional argument references (1-9). */
56
+ const POSITIONAL_PATTERN = /\$([1-9])/g;
57
+
58
+ /** Match $ARGUMENTS reference. */
59
+ const ARGUMENTS_PATTERN = /\$ARGUMENTS/g;
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Shell selection (mirrors bash tool logic)
63
+ // ---------------------------------------------------------------------------
64
+
65
+ interface ShellConfig {
66
+ shell: string;
67
+ args: string[];
68
+ }
69
+
70
+ function getShellConfig(): ShellConfig {
71
+ if (process.platform === 'win32') {
72
+ // PowerShell on Windows
73
+ const psCore = process.env['ProgramFiles'];
74
+ const psCorePath = psCore ? `${psCore}\\PowerShell\\7\\pwsh.exe` : null;
75
+
76
+ // Try pwsh (PowerShell 7+) first, fall back to Windows PowerShell
77
+ try {
78
+ if (psCorePath) {
79
+ fs.accessSync(psCorePath);
80
+ return { shell: psCorePath, args: ['-NoProfile', '-NonInteractive', '-Command'] };
81
+ }
82
+ } catch {
83
+ // Fall through
84
+ }
85
+
86
+ return {
87
+ shell: 'powershell.exe',
88
+ args: ['-NoProfile', '-NonInteractive', '-Command'],
89
+ };
90
+ }
91
+
92
+ // Unix: use $SHELL, falling back to /bin/bash or /bin/sh
93
+ const userShell = process.env['SHELL'];
94
+ if (userShell && !userShell.endsWith('/fish')) {
95
+ return { shell: userShell, args: ['-c'] };
96
+ }
97
+
98
+ // Fall back
99
+ try {
100
+ fs.accessSync('/bin/bash');
101
+ return { shell: '/bin/bash', args: ['-c'] };
102
+ } catch {
103
+ return { shell: '/bin/sh', args: ['-c'] };
104
+ }
105
+ }
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Preprocessor implementation
109
+ // ---------------------------------------------------------------------------
110
+
111
+ /**
112
+ * Preprocess a SKILL.md body. Runs all three stages:
113
+ * 1. Variable substitution
114
+ * 2. Shell commands and scripts (in parallel)
115
+ * 3. Assemble final content
116
+ */
117
+ export async function preprocessSkillBody(
118
+ body: string,
119
+ config: PreprocessorConfig,
120
+ ): Promise<string> {
121
+ // Stage 1: Variable substitution (runs first so vars are available
122
+ // inside shell commands and script arguments)
123
+ let content = substituteVariables(body, config.variables);
124
+
125
+ // Stage 2: Collect shell command and script markers, execute in parallel
126
+ const shellReplacements: Array<{ marker: string; promise: Promise<string> }> = [];
127
+ const scriptReplacements: Array<{ marker: string; promise: Promise<string> }> = [];
128
+
129
+ // Find shell commands
130
+ let match: RegExpExecArray | null;
131
+ const shellRegex = new RegExp(SHELL_COMMAND_PATTERN.source, 'gm');
132
+ while ((match = shellRegex.exec(content)) !== null) {
133
+ const fullMatch = match[0]!;
134
+ const command = match[1]!;
135
+ shellReplacements.push({
136
+ marker: fullMatch,
137
+ promise: executeShellCommand(command, config.skillDir),
138
+ });
139
+ }
140
+
141
+ // Find scripts
142
+ const scriptRegex = new RegExp(SCRIPT_PATTERN.source, 'gm');
143
+ while ((match = scriptRegex.exec(content)) !== null) {
144
+ const fullMatch = match[0]!;
145
+ const scriptPath = match[1]!.trim();
146
+ const extraArgs = match[2]?.trim() ?? '';
147
+ scriptReplacements.push({
148
+ marker: fullMatch,
149
+ promise: executeScript(scriptPath, extraArgs, config),
150
+ });
151
+ }
152
+
153
+ // Execute all in parallel
154
+ const allReplacements = [...shellReplacements, ...scriptReplacements];
155
+ if (allReplacements.length > 0) {
156
+ const results = await Promise.allSettled(
157
+ allReplacements.map(r => r.promise),
158
+ );
159
+
160
+ for (let i = 0; i < allReplacements.length; i++) {
161
+ const replacement = allReplacements[i]!;
162
+ const result = results[i]!;
163
+ const output = result.status === 'fulfilled'
164
+ ? result.value
165
+ : `[Error: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}]`;
166
+ // Use callback to prevent $& and other replacement patterns in output
167
+ content = content.replace(replacement.marker, () => output);
168
+ }
169
+ }
170
+
171
+ return content;
172
+ }
173
+
174
+ /**
175
+ * Substitute ${VAR}, $ARGUMENTS, and $N references with their values.
176
+ */
177
+ export function substituteVariables(
178
+ body: string,
179
+ variables: Record<string, string>,
180
+ ): string {
181
+ let result = body;
182
+
183
+ // Replace $ARGUMENTS first (before ${} to avoid partial matching)
184
+ result = result.replace(ARGUMENTS_PATTERN, () => variables['ARGUMENTS'] ?? '');
185
+
186
+ // Replace positional $1..$9
187
+ result = result.replace(POSITIONAL_PATTERN, (_match, num: string) => {
188
+ return variables[num] ?? '';
189
+ });
190
+
191
+ // Replace ${VAR} references
192
+ result = result.replace(VARIABLE_PATTERN, (_match, varName: string) => {
193
+ return variables[varName] ?? '';
194
+ });
195
+
196
+ return result;
197
+ }
198
+
199
+ /**
200
+ * Execute a shell command and return stdout.
201
+ * Uses the same shell selection as the Bash tool.
202
+ */
203
+ export function executeShellCommand(
204
+ command: string,
205
+ cwd: string,
206
+ ): Promise<string> {
207
+ return new Promise((resolve) => {
208
+ const shellConfig = getShellConfig();
209
+
210
+ // Use execFile to invoke the shell directly with the command as an argument.
211
+ // This avoids double-shell invocation (exec spawns a shell around our shell).
212
+ const args = [...shellConfig.args, command];
213
+
214
+ const child = execFile(
215
+ shellConfig.shell,
216
+ args,
217
+ {
218
+ cwd,
219
+ timeout: COMMAND_TIMEOUT_MS,
220
+ maxBuffer: 1024 * 1024, // 1MB
221
+ env: process.env,
222
+ },
223
+ (error: Error | null, stdout: string, _stderr: string) => {
224
+ if (error) {
225
+ if ('killed' in error && (error as { killed?: boolean }).killed) {
226
+ resolve('[Error: command timed out]');
227
+ } else {
228
+ const exitCode = 'code' in error && (error as Record<string, unknown>)['code'] != null
229
+ ? (error as Record<string, unknown>)['code']
230
+ : 'unknown';
231
+ resolve(`[Error: command failed with exit code ${exitCode}]`);
232
+ }
233
+ return;
234
+ }
235
+ resolve(stdout.trim());
236
+ },
237
+ );
238
+
239
+ child.on('error', () => {
240
+ resolve('[Error: failed to execute command]');
241
+ });
242
+ });
243
+ }
244
+
245
+ /**
246
+ * Execute a JavaScript script and return its output.
247
+ * Scripts are loaded via dynamic import() and must export a default
248
+ * async function.
249
+ */
250
+ export async function executeScript(
251
+ scriptPath: string,
252
+ extraArgsStr: string,
253
+ config: PreprocessorConfig,
254
+ ): Promise<string> {
255
+ const absolutePath = path.isAbsolute(scriptPath)
256
+ ? scriptPath
257
+ : path.resolve(config.skillDir, scriptPath);
258
+
259
+ // Security: reject paths that escape the skill directory
260
+ const relative = path.relative(config.skillDir, absolutePath);
261
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
262
+ return '[Error: script path must be within the skill directory]';
263
+ }
264
+
265
+ // Parse extra args from "key: value, key2: value2" format
266
+ const scriptArgs: Record<string, string> = {};
267
+ if (extraArgsStr) {
268
+ const pairs = extraArgsStr.split(',');
269
+ for (const pair of pairs) {
270
+ const colonIdx = pair.indexOf(':');
271
+ if (colonIdx > 0) {
272
+ const key = pair.substring(0, colonIdx).trim();
273
+ const value = pair.substring(colonIdx + 1).trim();
274
+ scriptArgs[key] = value;
275
+ }
276
+ }
277
+ }
278
+
279
+ // Build context: consumer context spread first, then Cortex built-ins
280
+ // override (skillDir and scriptArgs are Cortex-owned and cannot be
281
+ // overridden by consumer). args/rawArgs come pre-merged in
282
+ // config.scriptContext from the registry with the same precedence.
283
+ const ctx: Record<string, unknown> = {
284
+ ...config.scriptContext,
285
+ skillDir: config.skillDir,
286
+ scriptArgs,
287
+ };
288
+
289
+ // Execute with timeout
290
+ const timeoutPromise = new Promise<string>((resolve) => {
291
+ setTimeout(() => resolve('[Error: script timed out]'), COMMAND_TIMEOUT_MS);
292
+ });
293
+
294
+ const executionPromise = (async (): Promise<string> => {
295
+ try {
296
+ // Dynamic import of the script file
297
+ const fileUrl = pathToFileURL(absolutePath).href;
298
+ const mod = await import(fileUrl);
299
+ const fn = mod.default ?? mod;
300
+
301
+ if (typeof fn !== 'function') {
302
+ return '[Error: script does not export a function]';
303
+ }
304
+
305
+ const result = await fn(ctx);
306
+ return typeof result === 'string' ? result : String(result ?? '');
307
+ } catch (err) {
308
+ const message = err instanceof Error ? err.message : String(err);
309
+ return `[Error: script failed: ${message}]`;
310
+ }
311
+ })();
312
+
313
+ return Promise.race([executionPromise, timeoutPromise]);
314
+ }
@@ -0,0 +1,378 @@
1
+ /**
2
+ * SkillRegistry: manages all known skills for the Cortex agent.
3
+ *
4
+ * Config-driven: the consumer provides paths to SKILL.md files from any
5
+ * source (plugins, user directories, built-ins). The registry does not
6
+ * scan directories.
7
+ *
8
+ * Skills are parsed at registration time (frontmatter extracted, body
9
+ * deferred to load time). The registry produces a compact summary for
10
+ * the load_skill tool description.
11
+ *
12
+ * References:
13
+ * - docs/cortex/skill-system.md
14
+ * - docs/cortex/plans/phase-4-sub-agents-and-skills.md
15
+ */
16
+
17
+ import * as fs from 'node:fs';
18
+ import * as path from 'node:path';
19
+ import type { SkillConfig, SkillEntry } from './types.js';
20
+ import { preprocessSkillBody } from './skill-preprocessor.js';
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Frontmatter parsing
24
+ // ---------------------------------------------------------------------------
25
+
26
+ /**
27
+ * Parse YAML frontmatter from a SKILL.md file.
28
+ * Expects --- delimited frontmatter at the start of the file.
29
+ *
30
+ * This is a lightweight parser that handles the common SKILL.md patterns
31
+ * without requiring a full YAML library. It handles:
32
+ * - Simple key: value pairs
33
+ * - Multi-line strings (using > or |)
34
+ * - Nested metadata maps
35
+ * - Space-delimited lists (for allowed-tools)
36
+ */
37
+ function parseFrontmatter(content: string): { frontmatter: Record<string, unknown>; body: string } {
38
+ const trimmed = content.trimStart();
39
+ if (!trimmed.startsWith('---')) {
40
+ return { frontmatter: {}, body: content };
41
+ }
42
+
43
+ const endIdx = trimmed.indexOf('\n---', 3);
44
+ if (endIdx < 0) {
45
+ return { frontmatter: {}, body: content };
46
+ }
47
+
48
+ const yamlBlock = trimmed.substring(3, endIdx).trim();
49
+ const body = trimmed.substring(endIdx + 4).trimStart();
50
+
51
+ const frontmatter: Record<string, unknown> = {};
52
+ const lines = yamlBlock.split('\n');
53
+ let currentKey = '';
54
+ let multilineValue = '';
55
+ let inMultiline = false;
56
+ let multilineType: '>' | '|' | '' = '';
57
+ let inMetadata = false;
58
+ const metadataMap: Record<string, string> = {};
59
+
60
+ for (let i = 0; i < lines.length; i++) {
61
+ const line = lines[i]!;
62
+
63
+ // Handle metadata block (indented key-value pairs)
64
+ if (inMetadata) {
65
+ const metaMatch = line.match(/^ {2}(\w[\w-]*)\s*:\s*(.*)$/);
66
+ if (metaMatch) {
67
+ metadataMap[metaMatch[1]!] = metaMatch[2]!.trim();
68
+ continue;
69
+ }
70
+ // End of metadata block
71
+ frontmatter['metadata'] = { ...metadataMap };
72
+ inMetadata = false;
73
+ }
74
+
75
+ // Handle multi-line folded/literal values
76
+ if (inMultiline) {
77
+ if (line.startsWith(' ') || line.trim() === '') {
78
+ if (multilineType === '>') {
79
+ multilineValue += (multilineValue ? ' ' : '') + line.trim();
80
+ } else {
81
+ multilineValue += (multilineValue ? '\n' : '') + line.trimStart();
82
+ }
83
+ continue;
84
+ }
85
+ // End of multi-line
86
+ frontmatter[currentKey] = multilineValue.trim();
87
+ inMultiline = false;
88
+ multilineValue = '';
89
+ }
90
+
91
+ // Parse key: value
92
+ const kvMatch = line.match(/^(\w[\w-]*)\s*:\s*(.*)$/);
93
+ if (kvMatch) {
94
+ const key = kvMatch[1]!;
95
+ const rawValue = kvMatch[2]!.trim();
96
+
97
+ if (rawValue === '>' || rawValue === '|') {
98
+ currentKey = key;
99
+ multilineType = rawValue as '>' | '|';
100
+ multilineValue = '';
101
+ inMultiline = true;
102
+ continue;
103
+ }
104
+
105
+ if (key === 'metadata' && rawValue === '') {
106
+ inMetadata = true;
107
+ continue;
108
+ }
109
+
110
+ // Parse boolean values
111
+ if (rawValue === 'true') {
112
+ frontmatter[key] = true;
113
+ } else if (rawValue === 'false') {
114
+ frontmatter[key] = false;
115
+ } else {
116
+ frontmatter[key] = rawValue;
117
+ }
118
+ }
119
+ }
120
+
121
+ // Flush any remaining multi-line or metadata
122
+ if (inMultiline && currentKey) {
123
+ frontmatter[currentKey] = multilineValue.trim();
124
+ }
125
+ if (inMetadata && Object.keys(metadataMap).length > 0) {
126
+ frontmatter['metadata'] = { ...metadataMap };
127
+ }
128
+
129
+ return { frontmatter, body };
130
+ }
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // SkillRegistry
134
+ // ---------------------------------------------------------------------------
135
+
136
+ export class SkillRegistry {
137
+ private readonly entries = new Map<string, SkillEntry>();
138
+
139
+ /** Consumer-provided variables for ${VAR} substitution. */
140
+ private preprocessorVariables: Record<string, string> = {};
141
+
142
+ /** Consumer-provided context for !{script:} executions. */
143
+ private scriptContext: Record<string, unknown> = {};
144
+
145
+ /**
146
+ * Callback fired when skills are added or removed.
147
+ * CortexAgent sets this to rebuild the load_skill tool description.
148
+ */
149
+ onChange: (() => void) | null = null;
150
+
151
+ constructor(configs?: SkillConfig[]) {
152
+ if (configs) {
153
+ for (const config of configs) {
154
+ this.addSkill(config);
155
+ }
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Add a skill from a SKILL.md file path.
161
+ * Reads and parses the frontmatter synchronously at registration time.
162
+ *
163
+ * If a skill with the same name already exists, the new one replaces it
164
+ * (last-registered wins).
165
+ */
166
+ addSkill(config: SkillConfig): void {
167
+ let content: string;
168
+ try {
169
+ content = fs.readFileSync(config.path, 'utf8');
170
+ } catch (err) {
171
+ // Skill file not readable; skip silently
172
+ return;
173
+ }
174
+
175
+ const { frontmatter } = parseFrontmatter(content);
176
+
177
+ const name = typeof frontmatter['name'] === 'string'
178
+ ? frontmatter['name']
179
+ : path.basename(path.dirname(config.path));
180
+
181
+ const description = typeof frontmatter['description'] === 'string'
182
+ ? frontmatter['description']
183
+ : '';
184
+
185
+ const disableModelInvocation = frontmatter['disable-model-invocation'] === true;
186
+
187
+ const entry: SkillEntry = {
188
+ name,
189
+ description,
190
+ path: config.path,
191
+ dir: path.dirname(config.path),
192
+ source: config.source,
193
+ frontmatter,
194
+ modelInvocable: !disableModelInvocation,
195
+ };
196
+ if (config.variables) {
197
+ entry.variables = config.variables;
198
+ }
199
+
200
+ this.entries.set(name, entry);
201
+ this.onChange?.();
202
+ }
203
+
204
+ /**
205
+ * Remove a skill by name.
206
+ */
207
+ removeSkill(name: string): void {
208
+ const existed = this.entries.delete(name);
209
+ if (existed) {
210
+ this.onChange?.();
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Get a skill entry by name.
216
+ */
217
+ getEntry(name: string): SkillEntry | null {
218
+ return this.entries.get(name) ?? null;
219
+ }
220
+
221
+ /**
222
+ * Get all registered skill entries.
223
+ */
224
+ getAll(): SkillEntry[] {
225
+ return [...this.entries.values()];
226
+ }
227
+
228
+ /**
229
+ * Get the number of registered skills.
230
+ */
231
+ get size(): number {
232
+ return this.entries.size;
233
+ }
234
+
235
+ /**
236
+ * Generate the available skills summary for the load_skill tool description.
237
+ *
238
+ * Skills with disable-model-invocation: true (modelInvocable: false) are
239
+ * excluded from the summary. The agent cannot see or auto-load them.
240
+ *
241
+ * Format: XML listing with name, source, and description per skill.
242
+ * Each skill consumes approximately 100 tokens.
243
+ */
244
+ getAvailableSkillsSummary(maxTokens = Number.POSITIVE_INFINITY): string {
245
+ const invocableSkills = [...this.entries.values()]
246
+ .filter(e => e.modelInvocable)
247
+ .sort((a, b) => {
248
+ // Priority: builtin > user > plugin
249
+ const priority = (s: string): number => {
250
+ if (s === 'builtin') return 0;
251
+ if (s === 'user') return 1;
252
+ return 2; // plugin:*
253
+ };
254
+ const pa = priority(a.source);
255
+ const pb = priority(b.source);
256
+ if (pa !== pb) return pa - pb;
257
+ return a.name.localeCompare(b.name);
258
+ });
259
+
260
+ if (invocableSkills.length === 0) {
261
+ return '<available-skills>\n(No skills available)\n</available-skills>';
262
+ }
263
+
264
+ let usedTokens = 0;
265
+ const visibleSkills: SkillEntry[] = [];
266
+
267
+ for (const entry of invocableSkills) {
268
+ const approxTokens = Math.max(
269
+ 32,
270
+ Math.ceil((entry.name.length + entry.description.trim().length) / 4),
271
+ );
272
+
273
+ if (visibleSkills.length > 0 && usedTokens + approxTokens > maxTokens) {
274
+ continue;
275
+ }
276
+
277
+ visibleSkills.push(entry);
278
+ usedTokens += approxTokens;
279
+ }
280
+
281
+ const skillXml = visibleSkills.map(e => {
282
+ const desc = e.description.trim();
283
+ return `<skill name="${e.name}" source="${e.source}">\n${desc}\n</skill>`;
284
+ }).join('\n');
285
+
286
+ return `<available-skills>\n${skillXml}\n</available-skills>`;
287
+ }
288
+
289
+ /**
290
+ * Read and preprocess a skill's full body content.
291
+ * Runs variable substitution, shell commands, and scripts.
292
+ *
293
+ * @param name - The skill name
294
+ * @param callArgs - Arguments from the load_skill tool call
295
+ * @returns The preprocessed skill body
296
+ * @throws Error if the skill is not found
297
+ */
298
+ async getSkillBody(
299
+ name: string,
300
+ callArgs: { args: string[]; rawArgs: string },
301
+ ): Promise<string> {
302
+ const entry = this.entries.get(name);
303
+ if (!entry) {
304
+ throw new Error(`Skill not found: "${name}"`);
305
+ }
306
+
307
+ // Read the file and extract the body (below frontmatter)
308
+ let content: string;
309
+ try {
310
+ content = fs.readFileSync(entry.path, 'utf8');
311
+ } catch (err) {
312
+ throw new Error(`Cannot read skill file: ${entry.path}`);
313
+ }
314
+
315
+ const { body } = parseFrontmatter(content);
316
+
317
+ // Build merged variables (consumer + built-ins, consumer wins on collision)
318
+ const variables: Record<string, string> = {
319
+ SKILL_DIR: entry.dir,
320
+ ARGUMENTS: callArgs.rawArgs,
321
+ };
322
+ // Add positional args
323
+ for (let i = 0; i < 9; i++) {
324
+ variables[String(i + 1)] = callArgs.args[i] ?? '';
325
+ }
326
+ // Merge per-skill variables (e.g., PLUGIN_ROOT for plugin skills)
327
+ if (entry.variables) {
328
+ Object.assign(variables, entry.variables);
329
+ }
330
+ // Merge consumer variables (consumer wins on collision)
331
+ Object.assign(variables, this.preprocessorVariables);
332
+
333
+ // Build merged script context (consumer first, Cortex built-ins last so
334
+ // they cannot be overridden — skillDir, args, rawArgs are Cortex-owned)
335
+ const mergedScriptContext: Record<string, unknown> = {
336
+ ...this.scriptContext,
337
+ skillDir: entry.dir,
338
+ args: callArgs.args,
339
+ rawArgs: callArgs.rawArgs,
340
+ scriptArgs: {},
341
+ };
342
+
343
+ // Run preprocessor
344
+ return preprocessSkillBody(body, {
345
+ variables,
346
+ scriptContext: mergedScriptContext,
347
+ skillDir: entry.dir,
348
+ });
349
+ }
350
+
351
+ /**
352
+ * Set consumer-provided variables for ${VAR} substitution.
353
+ * Called each tick during GATHER to update runtime values.
354
+ */
355
+ setPreprocessorVariables(variables: Record<string, string>): void {
356
+ this.preprocessorVariables = variables;
357
+ }
358
+
359
+ /**
360
+ * Set consumer-provided context for !{script:} executions.
361
+ * Called each tick during GATHER to update runtime values.
362
+ */
363
+ setScriptContext(context: Record<string, unknown>): void {
364
+ this.scriptContext = context;
365
+ }
366
+
367
+ /**
368
+ * Clear all entries. Called during destroy.
369
+ */
370
+ clear(): void {
371
+ this.entries.clear();
372
+ this.preprocessorVariables = {};
373
+ this.scriptContext = {};
374
+ }
375
+ }
376
+
377
+ // Export parseFrontmatter for testing
378
+ export { parseFrontmatter };