salmon-loop 0.2.13 → 0.2.16
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/dist/cli/argv/headless-detection.js +27 -0
- package/dist/cli/chat-flow.js +11 -0
- package/dist/cli/chat.js +160 -24
- package/dist/cli/commands/chat.js +14 -7
- package/dist/cli/commands/flow-mode.js +63 -0
- package/dist/cli/commands/registry.js +2 -0
- package/dist/cli/commands/run/benchmark-artifacts.js +41 -0
- package/dist/cli/commands/run/early-errors.js +23 -0
- package/dist/cli/commands/run/handler.js +115 -27
- package/dist/cli/commands/run/headless-error-writer.js +8 -0
- package/dist/cli/commands/run/loop-params.js +2 -0
- package/dist/cli/commands/run/mode.js +2 -5
- package/dist/cli/commands/run/parse-options.js +16 -0
- package/dist/cli/commands/run/persist-session.js +10 -1
- package/dist/cli/commands/run/preflight.js +10 -0
- package/dist/cli/commands/run/reporter-factory.js +4 -0
- package/dist/cli/commands/run/runtime-llm.js +38 -11
- package/dist/cli/commands/run/runtime-options.js +2 -2
- package/dist/cli/commands/serve.js +91 -71
- package/dist/cli/commands/tool-names.js +78 -78
- package/dist/cli/headless/anthropic-stream-normalized-encoder.js +6 -1
- package/dist/cli/headless/json-protocol.js +37 -0
- package/dist/cli/headless/native-stream-normalized-encoder.js +6 -1
- package/dist/cli/headless/protocol-metadata.js +22 -0
- package/dist/cli/headless/stream-json-protocol.js +34 -1
- package/dist/cli/index.js +6 -4
- package/dist/cli/locales/en.js +30 -6
- package/dist/cli/program-bootstrap.js +8 -3
- package/dist/cli/program-commands.js +5 -1
- package/dist/cli/reporters/anthropic-stream.js +7 -1
- package/dist/cli/reporters/json.js +4 -0
- package/dist/cli/reporters/stream-json.js +17 -2
- package/dist/cli/run-cli.js +5 -3
- package/dist/cli/slash/runtime.js +27 -12
- package/dist/cli/ui/components/CommandInput.js +7 -3
- package/dist/cli/ui/components/CommandSuggestionList.js +1 -1
- package/dist/cli/utils/command-option-source.js +13 -0
- package/dist/cli/utils/verify-resolver.js +8 -4
- package/dist/cli/utils/worktree-prepare-resolver.js +7 -3
- package/dist/core/adapters/fs/file-adapter.js +6 -0
- package/dist/core/adapters/fs/filesystem.js +2 -1
- package/dist/core/adapters/git/git-adapter.js +78 -1
- package/dist/core/benchmark/patch-artifact.js +124 -0
- package/dist/core/benchmark/swe-bench.js +25 -0
- package/dist/core/config/load.js +18 -11
- package/dist/core/config/resolve-llm.js +12 -0
- package/dist/core/config/resolvers/server.js +0 -6
- package/dist/core/config/validate.js +73 -21
- package/dist/core/context/gatherers/metadata-gatherer.js +1 -0
- package/dist/core/context/gatherers/ripgrep-gatherer.js +84 -2
- package/dist/core/context/keywords.js +18 -4
- package/dist/core/context/service-deps.js +2 -2
- package/dist/core/context/service.js +8 -0
- package/dist/core/context/steps/context-gather.js +38 -0
- package/dist/core/context/summarization/summarizer.js +55 -12
- package/dist/core/context/targeting/target-resolver.js +4 -4
- package/dist/core/extensions/index.js +23 -5
- package/dist/core/extensions/paths.js +31 -0
- package/dist/core/extensions/schemas.js +8 -5
- package/dist/core/facades/cli-chat.js +6 -2
- package/dist/core/facades/cli-command-chat.js +1 -0
- package/dist/core/facades/cli-command-tool-names.js +2 -0
- package/dist/core/facades/cli-observability.js +1 -1
- package/dist/core/facades/cli-run-handler.js +4 -2
- package/dist/core/facades/cli-run-persist-session.js +1 -0
- package/dist/core/facades/cli-serve.js +2 -4
- package/dist/core/facades/cli-utils-worktree.js +1 -1
- package/dist/core/failure/diagnostics.js +53 -1
- package/dist/core/grizzco/dsl/llm-strategy.js +4 -1
- package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +67 -9
- package/dist/core/grizzco/engine/pipeline/pipeline.js +6 -2
- package/dist/core/grizzco/engine/transaction/attempt-failure.js +90 -15
- package/dist/core/grizzco/engine/transaction/report-mapper.js +17 -3
- package/dist/core/grizzco/engine/transaction/transaction-runner.js +165 -7
- package/dist/core/grizzco/flows/AutopilotFlow.js +18 -0
- package/dist/core/grizzco/flows/flow-dispatch.js +11 -0
- package/dist/core/grizzco/steps/answer.js +13 -14
- package/dist/core/grizzco/steps/autopilot.js +396 -0
- package/dist/core/grizzco/steps/cache-sharing.js +29 -0
- package/dist/core/grizzco/steps/explore.js +37 -21
- package/dist/core/grizzco/steps/generateReview.js +2 -5
- package/dist/core/grizzco/steps/patch/apply-check.js +10 -0
- package/dist/core/grizzco/steps/patch/diff-normalization.js +70 -0
- package/dist/core/grizzco/steps/patch/diff-salvage.js +46 -0
- package/dist/core/grizzco/steps/patch/prompt-input.js +42 -0
- package/dist/core/grizzco/steps/patch.js +105 -146
- package/dist/core/grizzco/steps/plan.js +101 -25
- package/dist/core/grizzco/steps/preflight.js +5 -6
- package/dist/core/grizzco/steps/request-assembly.js +78 -0
- package/dist/core/grizzco/steps/research.js +39 -36
- package/dist/core/grizzco/steps/tool-runtime.js +47 -0
- package/dist/core/grizzco/steps/verify-shared.js +23 -0
- package/dist/core/grizzco/steps/verify.js +13 -21
- package/dist/core/llm/ai-sdk/chat-executor.js +2 -0
- package/dist/core/llm/ai-sdk/high-level-phase-specs.js +63 -0
- package/dist/core/llm/ai-sdk/message-mapper.js +40 -10
- package/dist/core/llm/ai-sdk/provider-factory.js +14 -0
- package/dist/core/llm/ai-sdk/request-params.js +73 -0
- package/dist/core/llm/ai-sdk/result-mapper.js +16 -0
- package/dist/core/llm/ai-sdk.js +112 -27
- package/dist/core/llm/capabilities.js +12 -0
- package/dist/core/llm/contracts/repair.js +36 -30
- package/dist/core/llm/errors.js +83 -2
- package/dist/core/llm/message-composition.js +7 -22
- package/dist/core/llm/phase-router.js +29 -10
- package/dist/core/llm/redact.js +28 -3
- package/dist/core/llm/registry.js +2 -0
- package/dist/core/llm/request-augmentation.js +55 -0
- package/dist/core/llm/request-envelope.js +334 -0
- package/dist/core/llm/shared-request-assembly.js +35 -0
- package/dist/core/llm/stream-utils.js +13 -4
- package/dist/core/llm/utils.js +18 -29
- package/dist/core/memory/relevant-retrieval.js +144 -0
- package/dist/core/observability/logger.js +11 -2
- package/dist/core/patch/diff.js +1 -0
- package/dist/core/prompts/registry.js +39 -2
- package/dist/core/prompts/runtime.js +50 -12
- package/dist/core/prompts/templates/phases/patch_user.hbs +2 -5
- package/dist/core/prompts/templates/phases/research_user.hbs +11 -0
- package/dist/core/prompts/templates/phases/review_user.hbs +3 -0
- package/dist/core/prompts/templates/system/answer_system.hbs +5 -0
- package/dist/core/prompts/templates/system/autopilot_system.hbs +11 -0
- package/dist/core/prompts/templates/system/explore_system.hbs +14 -23
- package/dist/core/prompts/templates/system/main_system.hbs +4 -16
- package/dist/core/prompts/templates/system/patch_system.hbs +39 -8
- package/dist/core/prompts/templates/system/plan_system.hbs +86 -1
- package/dist/core/prompts/templates/system/research_system.hbs +2 -0
- package/dist/core/protocols/a2a/agent-card.js +3 -2
- package/dist/core/protocols/a2a/sdk/executor.js +2 -1
- package/dist/core/protocols/a2a/sdk/server.js +0 -1
- package/dist/core/protocols/acp/formal-agent.js +74 -51
- package/dist/core/protocols/acp/handlers.js +5 -1
- package/dist/core/protocols/acp/permission-provider.js +1 -1
- package/dist/core/protocols/shared/flow-mode-mapping.js +23 -0
- package/dist/core/public-capabilities/flow-mode-metadata.js +39 -0
- package/dist/core/public-capabilities/projections.js +29 -0
- package/dist/core/public-capabilities/registry.js +26 -0
- package/dist/core/public-capabilities/types.js +2 -0
- package/dist/core/runtime/agent-server-runtime.js +47 -43
- package/dist/core/runtime/execution-profile.js +67 -0
- package/dist/core/session/artifact-state.js +160 -0
- package/dist/core/session/compaction/index.js +183 -0
- package/dist/core/session/compaction/microcompact.js +78 -0
- package/dist/core/session/compaction/tracking.js +48 -0
- package/dist/core/session/compaction/types.js +11 -0
- package/dist/core/session/compression.js +8 -0
- package/dist/core/session/manager.js +244 -8
- package/dist/core/session/pruning-strategy.js +55 -9
- package/dist/core/session/replacement-preview-provider.js +24 -0
- package/dist/core/session/replacement-state.js +131 -0
- package/dist/core/session/resume-repair/pipeline.js +79 -0
- package/dist/core/session/resume-repair/stages/load-raw-archive-state.js +40 -0
- package/dist/core/session/resume-repair/stages/reattach-runtime-state.js +8 -0
- package/dist/core/session/resume-repair/stages/recover-orphaned-branches.js +10 -0
- package/dist/core/session/resume-repair/stages/relink-boundary-and-tail.js +36 -0
- package/dist/core/session/resume-repair/stages/replay-startup-hooks.js +23 -0
- package/dist/core/session/resume-repair/stages/rescue-stale-metadata.js +17 -0
- package/dist/core/session/resume-repair/types.js +2 -0
- package/dist/core/session/summary-sync.js +164 -13
- package/dist/core/session/token-tracker.js +6 -0
- package/dist/core/skills/audit.js +34 -0
- package/dist/core/skills/bridge.js +84 -7
- package/dist/core/skills/discovery.js +94 -0
- package/dist/core/skills/feature-flags.js +52 -0
- package/dist/core/skills/index.js +1 -1
- package/dist/core/skills/loader.js +195 -20
- package/dist/core/skills/parser.js +296 -24
- package/dist/core/skills/permissions.js +117 -0
- package/dist/core/skills/runtime/MicroTaskRunner.js +10 -4
- package/dist/core/skills/runtime/SkillRunner.js +240 -61
- package/dist/core/strata/layers/shadow-driver/shadow-driver.js +37 -7
- package/dist/core/strata/layers/worktree.js +67 -10
- package/dist/core/strata/runtime/synchronizer.js +29 -2
- package/dist/core/streaming/stream-assembler.js +75 -31
- package/dist/core/sub-agent/context-snapshot.js +156 -0
- package/dist/core/sub-agent/core/loop.js +1 -1
- package/dist/core/sub-agent/core/manager.js +119 -20
- package/dist/core/sub-agent/dispatch-policy.js +29 -0
- package/dist/core/sub-agent/prefix-consistency.js +48 -0
- package/dist/core/sub-agent/registry-defaults.js +4 -0
- package/dist/core/sub-agent/tools/task-spawn.js +79 -2
- package/dist/core/sub-agent/types.js +134 -5
- package/dist/core/tools/audit.js +13 -4
- package/dist/core/tools/builtin/ast-grep.js +1 -1
- package/dist/core/tools/builtin/ast.js +1 -1
- package/dist/core/tools/builtin/benchmark.js +360 -0
- package/dist/core/tools/builtin/code-search/backends/rg.js +2 -1
- package/dist/core/tools/builtin/code-search/executor.js +6 -1
- package/dist/core/tools/builtin/code-search/spec.js +26 -2
- package/dist/core/tools/builtin/fs.js +256 -23
- package/dist/core/tools/builtin/git.js +2 -2
- package/dist/core/tools/builtin/index.js +51 -2
- package/dist/core/tools/builtin/interaction.js +8 -1
- package/dist/core/tools/builtin/plan.js +37 -15
- package/dist/core/tools/builtin/shell.js +1 -1
- package/dist/core/tools/loader.js +39 -16
- package/dist/core/tools/mapper.js +17 -3
- package/dist/core/tools/parallel/scheduler.js +35 -4
- package/dist/core/tools/permissions/permission-rules.js +5 -10
- package/dist/core/tools/policy.js +6 -1
- package/dist/core/tools/recoverable-tool-errors.js +10 -0
- package/dist/core/tools/router.js +24 -6
- package/dist/core/tools/session.js +458 -48
- package/dist/core/tools/tool-visibility.js +62 -0
- package/dist/core/tools/types.js +9 -1
- package/dist/core/types/execution.js +4 -0
- package/dist/core/types/flow-mode.js +8 -0
- package/dist/core/utils/path.js +52 -0
- package/dist/core/verification/runner.js +4 -1
- package/dist/languages/typescript/index.js +4 -1
- package/dist/locales/en.js +35 -2
- package/dist/utils/eol.js +1 -1
- package/package.json +13 -6
- package/scripts/fix-es-abstract-compat.js +77 -0
- package/dist/core/runtime/fastify-server-bundle.js +0 -26
- package/dist/core/runtime/sidecar-fastify-plugin.js +0 -35
- package/dist/core/runtime/sidecar-paths.js +0 -47
- package/dist/core/runtime/sidecar-route-catalog.js +0 -103
|
@@ -1,71 +1,246 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { text } from '../../locales/index.js';
|
|
3
4
|
import { syncFs as fs } from '../adapters/fs/node-fs.js';
|
|
4
|
-
import {
|
|
5
|
+
import { tryGetLogger } from '../observability/logger.js';
|
|
5
6
|
import { SkillParser } from './parser.js';
|
|
7
|
+
/**
|
|
8
|
+
* Safe logger accessor that never throws when the logger is not yet initialized.
|
|
9
|
+
*
|
|
10
|
+
* Falls back to a no-op stub so that loader code can run in test environments
|
|
11
|
+
* or early startup paths where the global logger has not been set.
|
|
12
|
+
*/
|
|
13
|
+
function safeLogger() {
|
|
14
|
+
return (tryGetLogger() ?? {
|
|
15
|
+
error: (..._args) => { },
|
|
16
|
+
warn: (..._args) => { },
|
|
17
|
+
info: (..._args) => { },
|
|
18
|
+
debug: (..._args) => { },
|
|
19
|
+
audit: (..._args) => { },
|
|
20
|
+
});
|
|
21
|
+
}
|
|
6
22
|
export class SkillLoader {
|
|
7
23
|
options;
|
|
24
|
+
/**
|
|
25
|
+
* Format the skill catalog into a model-consumable disclosure block.
|
|
26
|
+
*
|
|
27
|
+
* Pure data contract: accepts catalog + optional context paths, returns string.
|
|
28
|
+
* No dependency on session or prompt layer.
|
|
29
|
+
*
|
|
30
|
+
* @param catalog - Array of SkillCatalogEntry from loadCatalog()
|
|
31
|
+
* @returns Formatted string with preamble + skill entries, or empty string if
|
|
32
|
+
* no disclosable skills
|
|
33
|
+
* @see Requirements 4.1, 4.6
|
|
34
|
+
*/
|
|
35
|
+
static formatCatalogDisclosure(catalog) {
|
|
36
|
+
if (catalog.length === 0) {
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
const preamble = `## Available Skills\n\n${text.skills.catalogDisclosurePreamble}`;
|
|
40
|
+
const entries = catalog
|
|
41
|
+
.map((entry) => `- **${entry.name}**: ${entry.description}\n Location: ${entry.location}`)
|
|
42
|
+
.join('\n\n');
|
|
43
|
+
return `${preamble}\n\n${entries}\n`;
|
|
44
|
+
}
|
|
45
|
+
/** Cache of fully activated skills (Tier 2). */
|
|
46
|
+
activated = new Map();
|
|
47
|
+
/** Cached catalog entries from the last loadCatalog() call. */
|
|
48
|
+
catalogCache = null;
|
|
8
49
|
constructor(options) {
|
|
9
50
|
this.options = options;
|
|
10
51
|
}
|
|
11
|
-
|
|
12
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Tier 2: Activate a skill by id — load full SKILL.md content on demand.
|
|
54
|
+
*
|
|
55
|
+
* Looks up the skill in the catalog (loading it first if necessary),
|
|
56
|
+
* reads the full file content, parses it with {@link SkillParser.parse},
|
|
57
|
+
* and caches the result. Subsequent calls for the same id return the
|
|
58
|
+
* cached {@link Skill} without re-reading the file.
|
|
59
|
+
*
|
|
60
|
+
* @param id - The skill identifier (must match a catalog entry)
|
|
61
|
+
* @returns The fully loaded {@link Skill} with instructions
|
|
62
|
+
* @throws Error if the skill id is not found in the catalog
|
|
63
|
+
* @see Requirements 6.2, 6.4
|
|
64
|
+
*/
|
|
65
|
+
async activateSkill(id) {
|
|
66
|
+
// Return cached activation if already loaded
|
|
67
|
+
const cached = this.activated.get(id);
|
|
68
|
+
if (cached) {
|
|
69
|
+
return cached;
|
|
70
|
+
}
|
|
71
|
+
// Ensure catalog is available
|
|
72
|
+
if (!this.catalogCache) {
|
|
73
|
+
this.catalogCache = await this.loadCatalog();
|
|
74
|
+
}
|
|
75
|
+
const entry = this.catalogCache.find((e) => e.id === id);
|
|
76
|
+
if (!entry) {
|
|
77
|
+
throw new Error(text.skills.skillNotFoundInCatalog(id));
|
|
78
|
+
}
|
|
79
|
+
// Read full content and parse with SkillParser
|
|
80
|
+
const content = fs.readFileSync(entry.location, 'utf-8');
|
|
81
|
+
const skill = SkillParser.parse(content, entry.location);
|
|
82
|
+
this.activated.set(id, skill);
|
|
83
|
+
safeLogger().info(text.skills.skillActivated(id));
|
|
84
|
+
return skill;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Tier 1: Load a lightweight catalog of all discoverable skills.
|
|
88
|
+
*
|
|
89
|
+
* Parses only YAML frontmatter (name + description + location) without
|
|
90
|
+
* loading full instruction content. Keeps startup context cost at
|
|
91
|
+
* approximately 50-100 tokens per skill.
|
|
92
|
+
*
|
|
93
|
+
* Uses the same search path priority and conflict resolution as
|
|
94
|
+
* {@link initialize}, but avoids reading instruction bodies.
|
|
95
|
+
*
|
|
96
|
+
* @returns Array of {@link SkillCatalogEntry} in discovery priority order
|
|
97
|
+
* @see Requirements 6.1, 6.3
|
|
98
|
+
*/
|
|
99
|
+
async loadCatalog() {
|
|
100
|
+
const catalog = [];
|
|
13
101
|
const seen = new Map();
|
|
14
102
|
for (const target of this.buildSearchPaths()) {
|
|
15
103
|
if (!fs.existsSync(target.path))
|
|
16
104
|
continue;
|
|
105
|
+
const scope = this.labelToScope(target.label);
|
|
17
106
|
const entries = fs.readdirSync(target.path, { withFileTypes: true });
|
|
18
107
|
for (const entry of entries) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
108
|
+
if (!entry.isDirectory())
|
|
109
|
+
continue;
|
|
110
|
+
const skillFile = path.join(target.path, entry.name, 'SKILL.md');
|
|
111
|
+
if (!skillFile || !fs.existsSync(skillFile))
|
|
112
|
+
continue;
|
|
113
|
+
try {
|
|
114
|
+
const content = fs.readFileSync(skillFile, 'utf-8');
|
|
115
|
+
const catalogEntry = SkillParser.parseFrontmatterOnly(content, skillFile, scope);
|
|
116
|
+
if (seen.has(catalogEntry.id)) {
|
|
117
|
+
const firstSource = seen.get(catalogEntry.id);
|
|
118
|
+
safeLogger().warn(`Duplicate skill ${catalogEntry.id} found in ${skillFile}; already loaded from ${firstSource}`);
|
|
119
|
+
safeLogger().audit('SKILL_DUPLICATE_SKIPPED', {
|
|
120
|
+
skillId: catalogEntry.id,
|
|
121
|
+
skippedPath: skillFile,
|
|
122
|
+
firstSource,
|
|
123
|
+
reason: 'first_win_conflict_resolution',
|
|
124
|
+
}, { source: 'skill-loader', severity: 'low', scope: 'repo' });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
seen.set(catalogEntry.id, `${target.label}:${skillFile}`);
|
|
128
|
+
catalog.push(catalogEntry);
|
|
22
129
|
}
|
|
23
|
-
|
|
24
|
-
skillFile
|
|
130
|
+
catch (err) {
|
|
131
|
+
safeLogger().error(`Failed to load skill catalog entry at ${skillFile}: ${err instanceof Error ? err.message : String(err)}`);
|
|
25
132
|
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
this.catalogCache = catalog;
|
|
136
|
+
return catalog;
|
|
137
|
+
}
|
|
138
|
+
async initialize() {
|
|
139
|
+
return this.loadSkillsFromPaths();
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Synchronous variant of {@link initialize} for callers that cannot await.
|
|
143
|
+
*
|
|
144
|
+
* Safe because all underlying I/O uses `syncFs` (synchronous fs operations).
|
|
145
|
+
* Used by tool-name discovery for tab completion.
|
|
146
|
+
*/
|
|
147
|
+
initializeSync() {
|
|
148
|
+
return this.loadSkillsFromPaths();
|
|
149
|
+
}
|
|
150
|
+
loadSkillsFromPaths() {
|
|
151
|
+
const inventory = [];
|
|
152
|
+
const seen = new Map();
|
|
153
|
+
for (const target of this.buildSearchPaths()) {
|
|
154
|
+
if (!fs.existsSync(target.path))
|
|
155
|
+
continue;
|
|
156
|
+
const entries = fs.readdirSync(target.path, { withFileTypes: true });
|
|
157
|
+
for (const entry of entries) {
|
|
158
|
+
if (!entry.isDirectory())
|
|
159
|
+
continue;
|
|
160
|
+
const skillFile = path.join(target.path, entry.name, 'SKILL.md');
|
|
26
161
|
if (!skillFile || !fs.existsSync(skillFile))
|
|
27
162
|
continue;
|
|
28
163
|
try {
|
|
29
164
|
const content = fs.readFileSync(skillFile, 'utf-8');
|
|
30
165
|
const skill = SkillParser.parse(content, skillFile);
|
|
31
166
|
if (seen.has(skill.id)) {
|
|
32
|
-
|
|
167
|
+
const firstSource = seen.get(skill.id);
|
|
168
|
+
safeLogger().warn(`Duplicate skill ${skill.id} found in ${skillFile}; already loaded from ${firstSource}`);
|
|
169
|
+
safeLogger().audit('SKILL_DUPLICATE_SKIPPED', {
|
|
170
|
+
skillId: skill.id,
|
|
171
|
+
skippedPath: skillFile,
|
|
172
|
+
firstSource,
|
|
173
|
+
reason: 'first_win_conflict_resolution',
|
|
174
|
+
}, { source: 'skill-loader', severity: 'low', scope: 'repo' });
|
|
33
175
|
continue;
|
|
34
176
|
}
|
|
35
177
|
seen.set(skill.id, `${target.label}:${skillFile}`);
|
|
36
178
|
inventory.push(skill);
|
|
37
179
|
}
|
|
38
180
|
catch (err) {
|
|
39
|
-
|
|
181
|
+
safeLogger().error(`Failed to load skill at ${skillFile}: ${err instanceof Error ? err.message : String(err)}`);
|
|
40
182
|
}
|
|
41
183
|
}
|
|
42
184
|
}
|
|
43
185
|
return inventory;
|
|
44
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Derive the catalog scope from a search path label.
|
|
189
|
+
*
|
|
190
|
+
* Labels follow the pattern `prefix:path` where prefix is one of:
|
|
191
|
+
* - `config` → 'config' scope (extra paths from skills.json)
|
|
192
|
+
* - `repo` → 'repo' scope (repo-level directories)
|
|
193
|
+
* - `user` → 'user' scope (user home directories)
|
|
194
|
+
* - `repo` / others → 'repo' scope
|
|
195
|
+
*/
|
|
196
|
+
labelToScope(label) {
|
|
197
|
+
if (label.startsWith('config:'))
|
|
198
|
+
return 'config';
|
|
199
|
+
if (label.startsWith('user:'))
|
|
200
|
+
return 'user';
|
|
201
|
+
return 'repo';
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Build the ordered list of skill search paths.
|
|
205
|
+
*
|
|
206
|
+
* Priority (high → low):
|
|
207
|
+
* 1. Config extra paths (from skills.json discovery.paths)
|
|
208
|
+
* 2. {repoRoot}/.salmonloop/skills
|
|
209
|
+
* 3. {repoRoot}/.agents/skills (cross-client interop)
|
|
210
|
+
* 4. ~/.salmonloop/skills
|
|
211
|
+
* 5. ~/.agents/skills (cross-client interop)
|
|
212
|
+
*
|
|
213
|
+
* First-win conflict resolution: when two skills share the same name,
|
|
214
|
+
* the one from the higher-priority path wins and a warning is logged
|
|
215
|
+
* (handled in initialize()).
|
|
216
|
+
*/
|
|
45
217
|
buildSearchPaths() {
|
|
46
218
|
const paths = [];
|
|
219
|
+
// 1. Config extra paths (highest priority)
|
|
47
220
|
const extra = this.options.extraPaths ?? [];
|
|
48
221
|
for (const extraPath of extra) {
|
|
49
222
|
paths.push({ path: extraPath, label: `config:${extraPath}` });
|
|
50
223
|
}
|
|
224
|
+
// 2. Repo-level .salmonloop/skills
|
|
51
225
|
paths.push({
|
|
52
226
|
path: path.join(this.options.repoRoot, '.salmonloop', 'skills'),
|
|
53
227
|
label: 'repo:.salmonloop/skills',
|
|
54
228
|
});
|
|
229
|
+
// 3. Repo-level .agents/skills (cross-client interop)
|
|
230
|
+
paths.push({
|
|
231
|
+
path: path.join(this.options.repoRoot, '.agents', 'skills'),
|
|
232
|
+
label: 'repo:.agents/skills',
|
|
233
|
+
});
|
|
234
|
+
// 4. User-level ~/.salmonloop/skills
|
|
55
235
|
paths.push({
|
|
56
236
|
path: path.join(os.homedir(), '.salmonloop', 'skills'),
|
|
57
237
|
label: 'user:~/.salmonloop/skills',
|
|
58
238
|
});
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
paths.push({
|
|
65
|
-
path: path.join(this.options.repoRoot, '.claude', 'skills'),
|
|
66
|
-
label: 'compat:.claude/skills',
|
|
67
|
-
});
|
|
68
|
-
}
|
|
239
|
+
// 5. User-level ~/.agents/skills (cross-client interop)
|
|
240
|
+
paths.push({
|
|
241
|
+
path: path.join(os.homedir(), '.agents', 'skills'),
|
|
242
|
+
label: 'user:~/.agents/skills',
|
|
243
|
+
});
|
|
69
244
|
const deduped = [];
|
|
70
245
|
const seen = new Set();
|
|
71
246
|
for (const entry of paths) {
|
|
@@ -1,37 +1,225 @@
|
|
|
1
|
-
import
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { parse as parseYaml } from 'yaml';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { text } from '../../locales/index.js';
|
|
5
|
+
import { tryGetLogger } from '../observability/logger.js';
|
|
6
|
+
/**
|
|
7
|
+
* Safe logger accessor that never throws when the logger is not yet initialized.
|
|
8
|
+
*
|
|
9
|
+
* Falls back to a no-op stub so that parser code can run in test environments
|
|
10
|
+
* or early startup paths where the global logger has not been set.
|
|
11
|
+
*/
|
|
12
|
+
function safeLogger() {
|
|
13
|
+
return (tryGetLogger() ?? {
|
|
14
|
+
error: (..._args) => { },
|
|
15
|
+
warn: (..._args) => { },
|
|
16
|
+
info: (..._args) => { },
|
|
17
|
+
debug: (..._args) => { },
|
|
18
|
+
audit: (..._args) => { },
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Naming convention regex for AgentSkills spec compliance.
|
|
23
|
+
*
|
|
24
|
+
* AgentSkills spec: "unicode lowercase alphanumeric characters (a-z) and
|
|
25
|
+
* hyphens (-)". We accept Unicode lowercase letters (\p{Ll}) and Unicode
|
|
26
|
+
* digits (\p{N}) in addition to ASCII, using the `u` flag for Unicode
|
|
27
|
+
* property escapes.
|
|
28
|
+
*/
|
|
29
|
+
const SKILL_NAME_REGEX = /^[\p{Ll}\p{N}](?:[\p{Ll}\p{N}-]*[\p{Ll}\p{N}])?$/u;
|
|
30
|
+
/** Shared field definitions defined by the Agent Skills specification. */
|
|
31
|
+
const sharedFields = {
|
|
32
|
+
license: z.string().optional(),
|
|
33
|
+
compatibility: z.string().max(500).optional(),
|
|
34
|
+
metadata: z.record(z.string(), z.string()).optional(),
|
|
35
|
+
// AgentSkills spec: "Space-delimited list of pre-approved tools" (Experimental).
|
|
36
|
+
// YAML bare key (`allowed-tools:`) parses as null — normalize to undefined so
|
|
37
|
+
// that Zod's `.optional()` accepts it as "not declared".
|
|
38
|
+
'allowed-tools': z.preprocess((val) => {
|
|
39
|
+
if (val === null || val === undefined)
|
|
40
|
+
return undefined;
|
|
41
|
+
return val;
|
|
42
|
+
}, z.string().optional()),
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Strict Zod schema for SKILL.md frontmatter validation.
|
|
46
|
+
*
|
|
47
|
+
* Enforces AgentSkills naming conventions and type correctness:
|
|
48
|
+
* - `name`: 1-64 chars, lowercase alphanumeric + hyphens, no leading/trailing/consecutive hyphens
|
|
49
|
+
* - `description`: 1-1024 chars, non-empty
|
|
50
|
+
* @see Requirements 5.1, 5.2, 5.4, 5.5
|
|
51
|
+
*/
|
|
52
|
+
export const SkillFrontmatterSchema = z
|
|
53
|
+
.object({
|
|
54
|
+
name: z
|
|
55
|
+
.string()
|
|
56
|
+
.min(1)
|
|
57
|
+
.max(64)
|
|
58
|
+
.regex(SKILL_NAME_REGEX, 'name must be unicode lowercase alphanumeric + hyphens per AgentSkills spec')
|
|
59
|
+
.refine((s) => !s.includes('--'), 'name must not contain consecutive hyphens'),
|
|
60
|
+
description: z.string().min(1).max(1024),
|
|
61
|
+
...sharedFields,
|
|
62
|
+
})
|
|
63
|
+
.strict();
|
|
64
|
+
/**
|
|
65
|
+
* Maximum allowed length for a single extracted command (in characters).
|
|
66
|
+
*
|
|
67
|
+
* This limit mitigates denial-of-service via extremely long command strings and
|
|
68
|
+
* reduces the blast radius of any injection that bypasses pattern checks.
|
|
69
|
+
* The value (4096) aligns with common OS `ARG_MAX` per-argument limits.
|
|
70
|
+
*
|
|
71
|
+
* @security Guard — enforced inside {@link SkillParser.extractCommands}.
|
|
72
|
+
*/
|
|
73
|
+
export const COMMAND_MAX_LENGTH = 4096;
|
|
74
|
+
/** Control character range that must not appear in commands (C0 subset excluding tab, LF, CR). */
|
|
75
|
+
// eslint-disable-next-line no-control-regex
|
|
76
|
+
const CONTROL_CHAR_PATTERN = /[\x00-\x08\x0e-\x1f]/;
|
|
77
|
+
/**
|
|
78
|
+
* Default deny-list of dangerous shell patterns.
|
|
79
|
+
*
|
|
80
|
+
* Each regex targets a well-known destructive or injection-prone idiom:
|
|
81
|
+
*
|
|
82
|
+
* | Pattern | Threat |
|
|
83
|
+
* |-----------------------|-------------------------------------------------|
|
|
84
|
+
* | `rm -rf /` | Recursive root deletion |
|
|
85
|
+
* | `curl … \| sh` | Remote code execution via piped download |
|
|
86
|
+
* | `\beval\b` | Arbitrary code evaluation in shell |
|
|
87
|
+
* | `\bexec\b.*<` | Process replacement with redirected input |
|
|
88
|
+
*
|
|
89
|
+
* The list is intentionally conservative — it catches blatant patterns but does
|
|
90
|
+
* NOT attempt full shell-syntax analysis. Callers may supply their own list via
|
|
91
|
+
* the `dangerousPatterns` parameter of {@link SkillParser.extractCommands}.
|
|
92
|
+
*
|
|
93
|
+
* @security Guard — applied after length and control-character checks.
|
|
94
|
+
*/
|
|
95
|
+
export const DEFAULT_DANGEROUS_PATTERNS = [
|
|
96
|
+
/rm\s+-rf\s+\//,
|
|
97
|
+
/curl\s.*\|\s*sh/,
|
|
98
|
+
/\beval\b/,
|
|
99
|
+
/\bexec\b.*</,
|
|
100
|
+
];
|
|
2
101
|
export class SkillParser {
|
|
102
|
+
/**
|
|
103
|
+
* Parse a SKILL.md file with YAML frontmatter validation.
|
|
104
|
+
*
|
|
105
|
+
* Uses the `yaml` library for robust YAML parsing and Zod schema for
|
|
106
|
+
* field validation and type coercion. Throws on missing or invalid
|
|
107
|
+
* frontmatter instead of silently falling back.
|
|
108
|
+
*
|
|
109
|
+
* @param content - Raw SKILL.md file content
|
|
110
|
+
* @param filePath - Absolute or relative path to the SKILL.md file
|
|
111
|
+
* @throws Error if frontmatter is missing, YAML is malformed, or schema validation fails
|
|
112
|
+
* @see Requirements 1.1, 1.2, 1.3, 1.5, 1.6, 1.8, 1.9, 1.10
|
|
113
|
+
*/
|
|
3
114
|
static parse(content, filePath) {
|
|
4
|
-
//
|
|
5
|
-
|
|
115
|
+
// Accept frontmatter with or without a body after the closing ---.
|
|
116
|
+
// The body capture is optional to avoid rejecting minimal SKILL.md files
|
|
117
|
+
// that contain only frontmatter (no trailing newline or instruction text).
|
|
118
|
+
const yamlRegex = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n([\s\S]*))?$/;
|
|
6
119
|
const match = content.match(yamlRegex);
|
|
7
120
|
if (!match) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
path: filePath,
|
|
12
|
-
metadata: {},
|
|
13
|
-
rawContent: content,
|
|
14
|
-
instructions: content.trim(),
|
|
15
|
-
};
|
|
121
|
+
const msg = text.skills.missingFrontmatter(filePath);
|
|
122
|
+
safeLogger().error(msg);
|
|
123
|
+
throw new Error(msg);
|
|
16
124
|
}
|
|
17
125
|
const yamlRaw = match[1];
|
|
18
|
-
const instructions = match[2];
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
126
|
+
const instructions = match[2] ?? '';
|
|
127
|
+
let parsed;
|
|
128
|
+
try {
|
|
129
|
+
parsed = parseYaml(yamlRaw);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
133
|
+
const msg = text.skills.yamlParseError(filePath, reason);
|
|
134
|
+
safeLogger().error(msg);
|
|
135
|
+
throw new Error(msg);
|
|
136
|
+
}
|
|
137
|
+
if (parsed == null || typeof parsed !== 'object') {
|
|
138
|
+
const msg = text.skills.missingFrontmatter(filePath);
|
|
139
|
+
safeLogger().error(msg);
|
|
140
|
+
throw new Error(msg);
|
|
141
|
+
}
|
|
142
|
+
const result = SkillFrontmatterSchema.safeParse(parsed);
|
|
143
|
+
if (!result.success) {
|
|
144
|
+
const issues = result.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ');
|
|
145
|
+
const msg = text.skills.invalidFrontmatter(filePath, issues);
|
|
146
|
+
safeLogger().error(msg);
|
|
147
|
+
throw new Error(msg);
|
|
148
|
+
}
|
|
149
|
+
const data = result.data;
|
|
150
|
+
const parentDir = path.basename(path.dirname(filePath));
|
|
151
|
+
if (parentDir && parentDir !== '.' && parentDir !== data.name) {
|
|
152
|
+
const msg = text.skills.nameDirMismatch(filePath, parentDir, data.name);
|
|
153
|
+
safeLogger().error(msg);
|
|
154
|
+
throw new Error(msg);
|
|
155
|
+
}
|
|
27
156
|
return {
|
|
28
|
-
id: data.name
|
|
157
|
+
id: data.name,
|
|
29
158
|
path: filePath,
|
|
30
159
|
metadata: data,
|
|
31
160
|
rawContent: content,
|
|
32
161
|
instructions: instructions.trim(),
|
|
33
162
|
};
|
|
34
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Parse only the YAML frontmatter of a SKILL.md file (Tier 1 catalog loading).
|
|
166
|
+
*
|
|
167
|
+
* Extracts name, description, and optional conditional paths without reading
|
|
168
|
+
* the full instruction body. This keeps startup context cost at approximately
|
|
169
|
+
* 50-100 tokens per skill.
|
|
170
|
+
*
|
|
171
|
+
* @param content - Raw SKILL.md file content
|
|
172
|
+
* @param filePath - Absolute or relative path to the SKILL.md file
|
|
173
|
+
* @param scope - Discovery scope for the catalog entry
|
|
174
|
+
* @returns A lightweight {@link SkillCatalogEntry} or throws on invalid frontmatter
|
|
175
|
+
* @see Requirements 1.1, 1.2, 1.3, 1.5, 1.6, 6.1, 6.3
|
|
176
|
+
*/
|
|
177
|
+
static parseFrontmatterOnly(content, filePath, scope) {
|
|
178
|
+
const yamlRegex = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
179
|
+
const match = content.match(yamlRegex);
|
|
180
|
+
if (!match) {
|
|
181
|
+
const msg = text.skills.missingFrontmatter(filePath);
|
|
182
|
+
safeLogger().error(msg);
|
|
183
|
+
throw new Error(msg);
|
|
184
|
+
}
|
|
185
|
+
const yamlRaw = match[1];
|
|
186
|
+
let parsed;
|
|
187
|
+
try {
|
|
188
|
+
parsed = parseYaml(yamlRaw);
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
192
|
+
const msg = text.skills.yamlParseError(filePath, reason);
|
|
193
|
+
safeLogger().error(msg);
|
|
194
|
+
throw new Error(msg);
|
|
195
|
+
}
|
|
196
|
+
if (parsed == null || typeof parsed !== 'object') {
|
|
197
|
+
const msg = text.skills.missingFrontmatter(filePath);
|
|
198
|
+
safeLogger().error(msg);
|
|
199
|
+
throw new Error(msg);
|
|
200
|
+
}
|
|
201
|
+
const result = SkillFrontmatterSchema.safeParse(parsed);
|
|
202
|
+
if (!result.success) {
|
|
203
|
+
const issues = result.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ');
|
|
204
|
+
const msg = text.skills.invalidFrontmatter(filePath, issues);
|
|
205
|
+
safeLogger().error(msg);
|
|
206
|
+
throw new Error(msg);
|
|
207
|
+
}
|
|
208
|
+
const data = result.data;
|
|
209
|
+
const parentDir = path.basename(path.dirname(filePath));
|
|
210
|
+
if (parentDir && parentDir !== '.' && parentDir !== data.name) {
|
|
211
|
+
const msg = text.skills.nameDirMismatch(filePath, parentDir, data.name);
|
|
212
|
+
safeLogger().error(msg);
|
|
213
|
+
throw new Error(msg);
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
id: data.name,
|
|
217
|
+
name: data.name,
|
|
218
|
+
description: data.description,
|
|
219
|
+
location: filePath,
|
|
220
|
+
scope,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
35
223
|
static substituteVariables(template, args) {
|
|
36
224
|
let result = template;
|
|
37
225
|
// Sort keys by length descending to prevent shorter keys from replacing parts of longer keys
|
|
@@ -56,11 +244,95 @@ export class SkillParser {
|
|
|
56
244
|
}
|
|
57
245
|
return result;
|
|
58
246
|
}
|
|
59
|
-
|
|
60
|
-
|
|
247
|
+
/**
|
|
248
|
+
* Extract shell commands from skill instruction markdown.
|
|
249
|
+
*
|
|
250
|
+
* ## Regex: `/^!(?:sh\s+)?(.*)$/gm`
|
|
251
|
+
*
|
|
252
|
+
* ### What it matches
|
|
253
|
+
* Lines that begin with `!` are treated as command directives. Two forms are
|
|
254
|
+
* recognised:
|
|
255
|
+
*
|
|
256
|
+
* - `!sh <command>` — explicit shell prefix (the `sh ` prefix is consumed,
|
|
257
|
+
* only `<command>` is captured).
|
|
258
|
+
* - `!<command>` — shorthand without the `sh` keyword.
|
|
259
|
+
*
|
|
260
|
+
* The `m` (multiline) flag makes `^` / `$` match per-line, so every command
|
|
261
|
+
* line in a multi-line instruction block is extracted independently.
|
|
262
|
+
*
|
|
263
|
+
* ### What it intentionally excludes
|
|
264
|
+
* - Lines that do NOT start with `!` (regular markdown prose).
|
|
265
|
+
* - The `!` prefix itself and the optional `sh ` token — only the payload
|
|
266
|
+
* after them is captured in group 1.
|
|
267
|
+
* - Empty captures are filtered out after extraction (`trim().length > 0`).
|
|
268
|
+
*
|
|
269
|
+
* ### Security implications
|
|
270
|
+
*
|
|
271
|
+
* 1. **Greedy `(.*)` capture** — the capture group accepts any character
|
|
272
|
+
* (except newline) without restriction. This means the regex alone does
|
|
273
|
+
* NOT prevent shell metacharacters, variable expansion, pipes, or
|
|
274
|
+
* subshell invocations from appearing in the captured command.
|
|
275
|
+
*
|
|
276
|
+
* 2. **Multiline mode (`m` flag)** — each line is evaluated independently.
|
|
277
|
+
* An attacker cannot splice two lines into a single command via the regex
|
|
278
|
+
* itself, but embedded newlines within a single logical line (e.g. via
|
|
279
|
+
* `\n` literals in a YAML value) would not be caught by `^…$` anchors.
|
|
280
|
+
* The control-character filter below mitigates this.
|
|
281
|
+
*
|
|
282
|
+
* 3. **No shell-syntax parsing** — the regex performs plain text extraction;
|
|
283
|
+
* it has no awareness of quoting, escaping, or shell grammar. Security
|
|
284
|
+
* therefore relies on the downstream guard chain, NOT on the regex.
|
|
285
|
+
*
|
|
286
|
+
* ### Downstream security guards (defense-in-depth)
|
|
287
|
+
*
|
|
288
|
+
* The following guards are applied sequentially after extraction to mitigate
|
|
289
|
+
* the risks above:
|
|
290
|
+
*
|
|
291
|
+
* | Guard | Constant / Pattern | Purpose |
|
|
292
|
+
* |--------------------------|-----------------------------|--------------------------------------------|
|
|
293
|
+
* | Max length | {@link COMMAND_MAX_LENGTH} | Caps command size to 4096 chars |
|
|
294
|
+
* | Control-char rejection | `CONTROL_CHAR_PATTERN` | Blocks C0 control chars (except tab/LF/CR) |
|
|
295
|
+
* | Dangerous-pattern filter | {@link DEFAULT_DANGEROUS_PATTERNS} | Rejects known destructive idioms |
|
|
296
|
+
* | Audit logging | `SKILL_COMMANDS_EXTRACTED` | Logs all surviving commands for review |
|
|
297
|
+
*
|
|
298
|
+
* @param instructions - Raw skill instruction text (may contain markdown).
|
|
299
|
+
* @param dangerousPatterns - Optional override for the dangerous-pattern
|
|
300
|
+
* deny-list. Defaults to {@link DEFAULT_DANGEROUS_PATTERNS}.
|
|
301
|
+
* @returns Array of sanitised command strings ready for governed execution
|
|
302
|
+
* via ToolRouter.
|
|
303
|
+
*
|
|
304
|
+
* @security Requirement 8.4 — command extraction regex documented with
|
|
305
|
+
* security implications.
|
|
306
|
+
*/
|
|
307
|
+
static extractCommands(instructions, dangerousPatterns = DEFAULT_DANGEROUS_PATTERNS) {
|
|
61
308
|
const commandRegex = /^!(?:sh\s+)?(.*)$/gm;
|
|
62
309
|
const matches = instructions.matchAll(commandRegex);
|
|
63
|
-
|
|
310
|
+
const raw = Array.from(matches, (m) => m[1].trim()).filter((cmd) => cmd.length > 0);
|
|
311
|
+
const logger = tryGetLogger();
|
|
312
|
+
const safe = raw.filter((cmd) => {
|
|
313
|
+
if (cmd.length > COMMAND_MAX_LENGTH) {
|
|
314
|
+
logger?.warn(`Skill command rejected: exceeds max length (${cmd.length} > ${COMMAND_MAX_LENGTH})`);
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
if (CONTROL_CHAR_PATTERN.test(cmd)) {
|
|
318
|
+
logger?.warn('Skill command rejected: contains control characters');
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
const matched = dangerousPatterns.find((p) => p.test(cmd));
|
|
322
|
+
if (matched) {
|
|
323
|
+
logger?.warn(`Skill command rejected: matches dangerous pattern ${matched}`);
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
return true;
|
|
327
|
+
});
|
|
328
|
+
// Audit: log all commands that will be executed
|
|
329
|
+
if (safe.length > 0) {
|
|
330
|
+
logger?.audit('SKILL_COMMANDS_EXTRACTED', {
|
|
331
|
+
commandCount: safe.length,
|
|
332
|
+
commands: safe,
|
|
333
|
+
}, { source: 'skill-parser', severity: 'low', scope: 'session' });
|
|
334
|
+
}
|
|
335
|
+
return safe;
|
|
64
336
|
}
|
|
65
337
|
}
|
|
66
338
|
//# sourceMappingURL=parser.js.map
|