@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.
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/budget-guard.d.ts +75 -0
- package/dist/budget-guard.d.ts.map +1 -0
- package/dist/budget-guard.js +142 -0
- package/dist/budget-guard.js.map +1 -0
- package/dist/compaction/compaction.d.ts +99 -0
- package/dist/compaction/compaction.d.ts.map +1 -0
- package/dist/compaction/compaction.js +302 -0
- package/dist/compaction/compaction.js.map +1 -0
- package/dist/compaction/failsafe.d.ts +57 -0
- package/dist/compaction/failsafe.d.ts.map +1 -0
- package/dist/compaction/failsafe.js +135 -0
- package/dist/compaction/failsafe.js.map +1 -0
- package/dist/compaction/index.d.ts +381 -0
- package/dist/compaction/index.d.ts.map +1 -0
- package/dist/compaction/index.js +979 -0
- package/dist/compaction/index.js.map +1 -0
- package/dist/compaction/microcompaction.d.ts +219 -0
- package/dist/compaction/microcompaction.d.ts.map +1 -0
- package/dist/compaction/microcompaction.js +536 -0
- package/dist/compaction/microcompaction.js.map +1 -0
- package/dist/compaction/observational/buffering.d.ts +225 -0
- package/dist/compaction/observational/buffering.d.ts.map +1 -0
- package/dist/compaction/observational/buffering.js +354 -0
- package/dist/compaction/observational/buffering.js.map +1 -0
- package/dist/compaction/observational/constants.d.ts +70 -0
- package/dist/compaction/observational/constants.d.ts.map +1 -0
- package/dist/compaction/observational/constants.js +507 -0
- package/dist/compaction/observational/constants.js.map +1 -0
- package/dist/compaction/observational/index.d.ts +219 -0
- package/dist/compaction/observational/index.d.ts.map +1 -0
- package/dist/compaction/observational/index.js +641 -0
- package/dist/compaction/observational/index.js.map +1 -0
- package/dist/compaction/observational/observer.d.ts +97 -0
- package/dist/compaction/observational/observer.d.ts.map +1 -0
- package/dist/compaction/observational/observer.js +424 -0
- package/dist/compaction/observational/observer.js.map +1 -0
- package/dist/compaction/observational/recall-tool.d.ts +27 -0
- package/dist/compaction/observational/recall-tool.d.ts.map +1 -0
- package/dist/compaction/observational/recall-tool.js +93 -0
- package/dist/compaction/observational/recall-tool.js.map +1 -0
- package/dist/compaction/observational/reflector.d.ts +94 -0
- package/dist/compaction/observational/reflector.d.ts.map +1 -0
- package/dist/compaction/observational/reflector.js +167 -0
- package/dist/compaction/observational/reflector.js.map +1 -0
- package/dist/compaction/observational/types.d.ts +271 -0
- package/dist/compaction/observational/types.d.ts.map +1 -0
- package/dist/compaction/observational/types.js +15 -0
- package/dist/compaction/observational/types.js.map +1 -0
- package/dist/context-manager.d.ts +134 -0
- package/dist/context-manager.d.ts.map +1 -0
- package/dist/context-manager.js +170 -0
- package/dist/context-manager.js.map +1 -0
- package/dist/cortex-agent.d.ts +1020 -0
- package/dist/cortex-agent.d.ts.map +1 -0
- package/dist/cortex-agent.js +3589 -0
- package/dist/cortex-agent.js.map +1 -0
- package/dist/error-classifier.d.ts +48 -0
- package/dist/error-classifier.d.ts.map +1 -0
- package/dist/error-classifier.js +152 -0
- package/dist/error-classifier.js.map +1 -0
- package/dist/event-bridge.d.ts +166 -0
- package/dist/event-bridge.d.ts.map +1 -0
- package/dist/event-bridge.js +381 -0
- package/dist/event-bridge.js.map +1 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-client.d.ts +119 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +474 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/model-wrapper.d.ts +58 -0
- package/dist/model-wrapper.d.ts.map +1 -0
- package/dist/model-wrapper.js +86 -0
- package/dist/model-wrapper.js.map +1 -0
- package/dist/noop-logger.d.ts +4 -0
- package/dist/noop-logger.d.ts.map +1 -0
- package/dist/noop-logger.js +8 -0
- package/dist/noop-logger.js.map +1 -0
- package/dist/prompt-diagnostics.d.ts +47 -0
- package/dist/prompt-diagnostics.d.ts.map +1 -0
- package/dist/prompt-diagnostics.js +230 -0
- package/dist/prompt-diagnostics.js.map +1 -0
- package/dist/provider-manager.d.ts +224 -0
- package/dist/provider-manager.d.ts.map +1 -0
- package/dist/provider-manager.js +563 -0
- package/dist/provider-manager.js.map +1 -0
- package/dist/provider-registry.d.ts +115 -0
- package/dist/provider-registry.d.ts.map +1 -0
- package/dist/provider-registry.js +305 -0
- package/dist/provider-registry.js.map +1 -0
- package/dist/schema-converter.d.ts +20 -0
- package/dist/schema-converter.d.ts.map +1 -0
- package/dist/schema-converter.js +48 -0
- package/dist/schema-converter.js.map +1 -0
- package/dist/skill-preprocessor.d.ts +46 -0
- package/dist/skill-preprocessor.d.ts.map +1 -0
- package/dist/skill-preprocessor.js +237 -0
- package/dist/skill-preprocessor.js.map +1 -0
- package/dist/skill-registry.d.ts +107 -0
- package/dist/skill-registry.d.ts.map +1 -0
- package/dist/skill-registry.js +330 -0
- package/dist/skill-registry.js.map +1 -0
- package/dist/skill-tool.d.ts +54 -0
- package/dist/skill-tool.d.ts.map +1 -0
- package/dist/skill-tool.js +88 -0
- package/dist/skill-tool.js.map +1 -0
- package/dist/sub-agent-manager.d.ts +90 -0
- package/dist/sub-agent-manager.d.ts.map +1 -0
- package/dist/sub-agent-manager.js +192 -0
- package/dist/sub-agent-manager.js.map +1 -0
- package/dist/token-estimator.d.ts +23 -0
- package/dist/token-estimator.d.ts.map +1 -0
- package/dist/token-estimator.js +27 -0
- package/dist/token-estimator.js.map +1 -0
- package/dist/tool-contract.d.ts +68 -0
- package/dist/tool-contract.d.ts.map +1 -0
- package/dist/tool-contract.js +35 -0
- package/dist/tool-contract.js.map +1 -0
- package/dist/tool-result-persistence.d.ts +89 -0
- package/dist/tool-result-persistence.d.ts.map +1 -0
- package/dist/tool-result-persistence.js +152 -0
- package/dist/tool-result-persistence.js.map +1 -0
- package/dist/tools/bash/index.d.ts +71 -0
- package/dist/tools/bash/index.d.ts.map +1 -0
- package/dist/tools/bash/index.js +485 -0
- package/dist/tools/bash/index.js.map +1 -0
- package/dist/tools/bash/interactive.d.ts +47 -0
- package/dist/tools/bash/interactive.d.ts.map +1 -0
- package/dist/tools/bash/interactive.js +262 -0
- package/dist/tools/bash/interactive.js.map +1 -0
- package/dist/tools/bash/safety.d.ts +149 -0
- package/dist/tools/bash/safety.d.ts.map +1 -0
- package/dist/tools/bash/safety.js +1116 -0
- package/dist/tools/bash/safety.js.map +1 -0
- package/dist/tools/edit.d.ts +57 -0
- package/dist/tools/edit.d.ts.map +1 -0
- package/dist/tools/edit.js +310 -0
- package/dist/tools/edit.js.map +1 -0
- package/dist/tools/glob.d.ts +34 -0
- package/dist/tools/glob.d.ts.map +1 -0
- package/dist/tools/glob.js +268 -0
- package/dist/tools/glob.js.map +1 -0
- package/dist/tools/grep.d.ts +53 -0
- package/dist/tools/grep.d.ts.map +1 -0
- package/dist/tools/grep.js +673 -0
- package/dist/tools/grep.js.map +1 -0
- package/dist/tools/index.d.ts +62 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +52 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/read.d.ts +43 -0
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +459 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/runtime.d.ts +62 -0
- package/dist/tools/runtime.d.ts.map +1 -0
- package/dist/tools/runtime.js +116 -0
- package/dist/tools/runtime.js.map +1 -0
- package/dist/tools/shared/cwd-tracker.d.ts +32 -0
- package/dist/tools/shared/cwd-tracker.d.ts.map +1 -0
- package/dist/tools/shared/cwd-tracker.js +44 -0
- package/dist/tools/shared/cwd-tracker.js.map +1 -0
- package/dist/tools/shared/edit-history.d.ts +55 -0
- package/dist/tools/shared/edit-history.d.ts.map +1 -0
- package/dist/tools/shared/edit-history.js +72 -0
- package/dist/tools/shared/edit-history.js.map +1 -0
- package/dist/tools/shared/edit-matcher.d.ts +83 -0
- package/dist/tools/shared/edit-matcher.d.ts.map +1 -0
- package/dist/tools/shared/edit-matcher.js +359 -0
- package/dist/tools/shared/edit-matcher.js.map +1 -0
- package/dist/tools/shared/file-mutation-lock.d.ts +22 -0
- package/dist/tools/shared/file-mutation-lock.d.ts.map +1 -0
- package/dist/tools/shared/file-mutation-lock.js +35 -0
- package/dist/tools/shared/file-mutation-lock.js.map +1 -0
- package/dist/tools/shared/gitignore.d.ts +17 -0
- package/dist/tools/shared/gitignore.d.ts.map +1 -0
- package/dist/tools/shared/gitignore.js +59 -0
- package/dist/tools/shared/gitignore.js.map +1 -0
- package/dist/tools/shared/pdf-extractor.d.ts +96 -0
- package/dist/tools/shared/pdf-extractor.d.ts.map +1 -0
- package/dist/tools/shared/pdf-extractor.js +196 -0
- package/dist/tools/shared/pdf-extractor.js.map +1 -0
- package/dist/tools/shared/read-registry.d.ts +66 -0
- package/dist/tools/shared/read-registry.d.ts.map +1 -0
- package/dist/tools/shared/read-registry.js +65 -0
- package/dist/tools/shared/read-registry.js.map +1 -0
- package/dist/tools/shared/safe-env.d.ts +18 -0
- package/dist/tools/shared/safe-env.d.ts.map +1 -0
- package/dist/tools/shared/safe-env.js +70 -0
- package/dist/tools/shared/safe-env.js.map +1 -0
- package/dist/tools/sub-agent.d.ts +91 -0
- package/dist/tools/sub-agent.d.ts.map +1 -0
- package/dist/tools/sub-agent.js +89 -0
- package/dist/tools/sub-agent.js.map +1 -0
- package/dist/tools/task-output.d.ts +38 -0
- package/dist/tools/task-output.d.ts.map +1 -0
- package/dist/tools/task-output.js +186 -0
- package/dist/tools/task-output.js.map +1 -0
- package/dist/tools/tool-search/index.d.ts +40 -0
- package/dist/tools/tool-search/index.d.ts.map +1 -0
- package/dist/tools/tool-search/index.js +110 -0
- package/dist/tools/tool-search/index.js.map +1 -0
- package/dist/tools/tool-search/registry.d.ts +82 -0
- package/dist/tools/tool-search/registry.d.ts.map +1 -0
- package/dist/tools/tool-search/registry.js +238 -0
- package/dist/tools/tool-search/registry.js.map +1 -0
- package/dist/tools/undo-edit.d.ts +51 -0
- package/dist/tools/undo-edit.d.ts.map +1 -0
- package/dist/tools/undo-edit.js +231 -0
- package/dist/tools/undo-edit.js.map +1 -0
- package/dist/tools/web-fetch/cache.d.ts +49 -0
- package/dist/tools/web-fetch/cache.d.ts.map +1 -0
- package/dist/tools/web-fetch/cache.js +89 -0
- package/dist/tools/web-fetch/cache.js.map +1 -0
- package/dist/tools/web-fetch/index.d.ts +53 -0
- package/dist/tools/web-fetch/index.d.ts.map +1 -0
- package/dist/tools/web-fetch/index.js +513 -0
- package/dist/tools/web-fetch/index.js.map +1 -0
- package/dist/tools/write.d.ts +59 -0
- package/dist/tools/write.d.ts.map +1 -0
- package/dist/tools/write.js +316 -0
- package/dist/tools/write.js.map +1 -0
- package/dist/types.d.ts +881 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/dist/working-tags.d.ts +44 -0
- package/dist/working-tags.d.ts.map +1 -0
- package/dist/working-tags.js +103 -0
- package/dist/working-tags.js.map +1 -0
- package/package.json +87 -0
- package/src/budget-guard.ts +170 -0
- package/src/compaction/compaction.ts +386 -0
- package/src/compaction/failsafe.ts +185 -0
- package/src/compaction/index.ts +1199 -0
- package/src/compaction/microcompaction.ts +709 -0
- package/src/compaction/observational/buffering.ts +430 -0
- package/src/compaction/observational/constants.ts +532 -0
- package/src/compaction/observational/index.ts +837 -0
- package/src/compaction/observational/observer.ts +510 -0
- package/src/compaction/observational/recall-tool.ts +130 -0
- package/src/compaction/observational/reflector.ts +221 -0
- package/src/compaction/observational/types.ts +343 -0
- package/src/context-manager.ts +237 -0
- package/src/cortex-agent.ts +4297 -0
- package/src/error-classifier.ts +199 -0
- package/src/event-bridge.ts +508 -0
- package/src/index.ts +292 -0
- package/src/mcp-client.ts +582 -0
- package/src/model-wrapper.ts +128 -0
- package/src/noop-logger.ts +9 -0
- package/src/prompt-diagnostics.ts +296 -0
- package/src/provider-manager.ts +823 -0
- package/src/provider-registry.ts +386 -0
- package/src/schema-converter.ts +51 -0
- package/src/skill-preprocessor.ts +314 -0
- package/src/skill-registry.ts +378 -0
- package/src/skill-tool.ts +130 -0
- package/src/sub-agent-manager.ts +236 -0
- package/src/token-estimator.ts +26 -0
- package/src/tool-contract.ts +113 -0
- package/src/tool-result-persistence.ts +197 -0
- package/src/tools/bash/index.ts +633 -0
- package/src/tools/bash/interactive.ts +302 -0
- package/src/tools/bash/safety.ts +1297 -0
- package/src/tools/edit.ts +422 -0
- package/src/tools/glob.ts +330 -0
- package/src/tools/grep.ts +819 -0
- package/src/tools/index.ts +110 -0
- package/src/tools/read.ts +580 -0
- package/src/tools/runtime.ts +173 -0
- package/src/tools/shared/cwd-tracker.ts +50 -0
- package/src/tools/shared/edit-history.ts +96 -0
- package/src/tools/shared/edit-matcher.ts +457 -0
- package/src/tools/shared/file-mutation-lock.ts +40 -0
- package/src/tools/shared/gitignore.ts +61 -0
- package/src/tools/shared/pdf-extractor.ts +290 -0
- package/src/tools/shared/read-registry.ts +93 -0
- package/src/tools/shared/safe-env.ts +82 -0
- package/src/tools/sub-agent.ts +171 -0
- package/src/tools/task-output.ts +236 -0
- package/src/tools/tool-search/index.ts +167 -0
- package/src/tools/tool-search/registry.ts +278 -0
- package/src/tools/undo-edit.ts +314 -0
- package/src/tools/web-fetch/cache.ts +112 -0
- package/src/tools/web-fetch/index.ts +604 -0
- package/src/tools/write.ts +385 -0
- package/src/types.ts +1057 -0
- 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 };
|