brainclaw 0.19.14 → 0.21.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.
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Agent capability profiles — describes what integration surfaces each
3
+ * agent supports so brainclaw can adapt its instruction file content,
4
+ * integration depth, and pressure level accordingly.
5
+ *
6
+ * Three profile tiers drive instruction file templates:
7
+ * A (full) — MCP + hooks + auto-approve → lightweight instructions
8
+ * B (standard) — MCP, no hooks → directive instructions with top traps
9
+ * C (limited) — no MCP → rich static content (plans, traps, decisions)
10
+ */
11
+ const PROFILES = {
12
+ 'claude-code': {
13
+ name: 'claude-code',
14
+ hasMcp: true,
15
+ hasHooks: true,
16
+ hasAutoApprove: true,
17
+ hasSkills: true,
18
+ hasRules: true,
19
+ instructionFile: 'CLAUDE.md',
20
+ sharedInstructionFile: true,
21
+ mcpConfigScope: 'both',
22
+ templateTier: 'A',
23
+ },
24
+ cursor: {
25
+ name: 'cursor',
26
+ hasMcp: true,
27
+ hasHooks: false,
28
+ hasAutoApprove: false,
29
+ hasSkills: false,
30
+ hasRules: true,
31
+ instructionFile: '.cursor/rules/brainclaw.md',
32
+ sharedInstructionFile: false,
33
+ mcpConfigScope: 'machine',
34
+ templateTier: 'B',
35
+ },
36
+ windsurf: {
37
+ name: 'windsurf',
38
+ hasMcp: true,
39
+ hasHooks: false,
40
+ hasAutoApprove: false,
41
+ hasSkills: false,
42
+ hasRules: true,
43
+ instructionFile: '.windsurfrules',
44
+ sharedInstructionFile: true,
45
+ mcpConfigScope: 'machine',
46
+ templateTier: 'B',
47
+ },
48
+ cline: {
49
+ name: 'cline',
50
+ hasMcp: true,
51
+ hasHooks: false,
52
+ hasAutoApprove: true,
53
+ hasSkills: false,
54
+ hasRules: true,
55
+ instructionFile: '.clinerules/brainclaw.md',
56
+ sharedInstructionFile: false,
57
+ mcpConfigScope: 'project',
58
+ templateTier: 'B',
59
+ },
60
+ roo: {
61
+ name: 'roo',
62
+ hasMcp: true,
63
+ hasHooks: false,
64
+ hasAutoApprove: true,
65
+ hasSkills: false,
66
+ hasRules: true,
67
+ instructionFile: '.roo/rules/brainclaw.md',
68
+ sharedInstructionFile: false,
69
+ mcpConfigScope: 'project',
70
+ templateTier: 'B',
71
+ },
72
+ continue: {
73
+ name: 'continue',
74
+ hasMcp: true,
75
+ hasHooks: false,
76
+ hasAutoApprove: false,
77
+ hasSkills: false,
78
+ hasRules: true,
79
+ instructionFile: '.continue/rules/brainclaw.md',
80
+ sharedInstructionFile: false,
81
+ mcpConfigScope: 'both',
82
+ templateTier: 'B',
83
+ },
84
+ opencode: {
85
+ name: 'opencode',
86
+ hasMcp: true,
87
+ hasHooks: false,
88
+ hasAutoApprove: false,
89
+ hasSkills: false,
90
+ hasRules: true,
91
+ instructionFile: 'AGENTS.md',
92
+ sharedInstructionFile: true,
93
+ mcpConfigScope: 'project',
94
+ templateTier: 'B',
95
+ },
96
+ codex: {
97
+ name: 'codex',
98
+ hasMcp: true,
99
+ hasHooks: false,
100
+ hasAutoApprove: false,
101
+ hasSkills: false,
102
+ hasRules: true,
103
+ instructionFile: 'AGENTS.md',
104
+ sharedInstructionFile: true,
105
+ mcpConfigScope: 'machine',
106
+ templateTier: 'B',
107
+ },
108
+ antigravity: {
109
+ name: 'antigravity',
110
+ hasMcp: true,
111
+ hasHooks: false,
112
+ hasAutoApprove: false,
113
+ hasSkills: false,
114
+ hasRules: true,
115
+ instructionFile: 'GEMINI.md',
116
+ sharedInstructionFile: true,
117
+ mcpConfigScope: 'machine',
118
+ templateTier: 'B',
119
+ },
120
+ 'github-copilot': {
121
+ name: 'github-copilot',
122
+ hasMcp: false,
123
+ hasHooks: false,
124
+ hasAutoApprove: false,
125
+ hasSkills: true,
126
+ hasRules: true,
127
+ instructionFile: '.github/copilot-instructions.md',
128
+ sharedInstructionFile: true,
129
+ mcpConfigScope: 'none',
130
+ templateTier: 'C',
131
+ },
132
+ openclaw: {
133
+ name: 'openclaw',
134
+ hasMcp: false,
135
+ hasHooks: false,
136
+ hasAutoApprove: false,
137
+ hasSkills: true,
138
+ hasRules: false,
139
+ instructionFile: 'skills/openclaw/SKILL.md',
140
+ sharedInstructionFile: false,
141
+ mcpConfigScope: 'none',
142
+ templateTier: 'C',
143
+ },
144
+ };
145
+ /**
146
+ * Get the capability profile for a known agent.
147
+ * Returns undefined for unknown agent names.
148
+ */
149
+ export function getAgentCapabilityProfile(name) {
150
+ return PROFILES[name];
151
+ }
152
+ /**
153
+ * Get all known agent capability profiles.
154
+ */
155
+ export function getAllAgentCapabilityProfiles() {
156
+ return Object.values(PROFILES);
157
+ }
158
+ /**
159
+ * Get all agent names that match a given template tier.
160
+ */
161
+ export function getAgentsByTier(tier) {
162
+ return Object.values(PROFILES).filter((p) => p.templateTier === tier);
163
+ }
164
+ /**
165
+ * Check if an agent name is a known brainclaw-supported agent.
166
+ */
167
+ export function isKnownAgent(name) {
168
+ return name in PROFILES;
169
+ }
170
+ /**
171
+ * Summarize which integration surfaces are available for a given agent.
172
+ * Useful for setup UI to explain what brainclaw will configure.
173
+ */
174
+ export function describeAgentSurfaces(name) {
175
+ const profile = getAgentCapabilityProfile(name);
176
+ if (!profile)
177
+ return [];
178
+ const surfaces = [];
179
+ if (profile.hasMcp) {
180
+ surfaces.push(`MCP server (${profile.mcpConfigScope})`);
181
+ }
182
+ if (profile.hasRules) {
183
+ surfaces.push(`Instruction file (${profile.instructionFile})`);
184
+ }
185
+ if (profile.hasAutoApprove) {
186
+ surfaces.push('Auto-approve MCP tools');
187
+ }
188
+ if (profile.hasHooks) {
189
+ surfaces.push('Session hooks (pre-prompt + stop)');
190
+ }
191
+ if (profile.hasSkills) {
192
+ surfaces.push('Slash command / skill');
193
+ }
194
+ return surfaces;
195
+ }
196
+ //# sourceMappingURL=agent-capability.js.map
@@ -60,12 +60,30 @@ function readAgentsMarkdown(cwd) {
60
60
  const raw = fs.readFileSync(filepath, 'utf-8');
61
61
  const lines = raw.split(/\r?\n/);
62
62
  const title = lines.find((line) => line.trim().startsWith('#'))?.replace(/^#+\s*/, '').trim();
63
- const rules = lines
64
- .map((line) => line.trim())
65
- .filter((line) => /^([-*]|\d+\.)\s+/.test(line))
66
- .map((line) => line.replace(/^([-*]|\d+\.)\s+/, '').trim())
67
- .filter(Boolean)
68
- .slice(0, MAX_AGENT_RULES);
63
+ // Only extract rules from actionable sections, not from descriptive sections
64
+ // like "why this matters" which contain explanatory bullets, not instructions.
65
+ const SKIP_SECTIONS = /why this matters|what it provides|what brainclaw/i;
66
+ let currentSection = '';
67
+ let skipSection = false;
68
+ const rules = [];
69
+ for (const line of lines) {
70
+ const trimmed = line.trim();
71
+ if (trimmed.startsWith('#')) {
72
+ currentSection = trimmed.replace(/^#+\s*/, '');
73
+ skipSection = SKIP_SECTIONS.test(currentSection);
74
+ continue;
75
+ }
76
+ if (skipSection)
77
+ continue;
78
+ if (/^([-*]|\d+\.)\s+/.test(trimmed)) {
79
+ const text = trimmed.replace(/^([-*]|\d+\.)\s+/, '').trim();
80
+ if (text) {
81
+ rules.push(text);
82
+ if (rules.length >= MAX_AGENT_RULES)
83
+ break;
84
+ }
85
+ }
86
+ }
69
87
  return {
70
88
  present: true,
71
89
  title,
@@ -57,6 +57,9 @@ const DEFAULT_SURFACES = {
57
57
  { kind: 'instructions', location: 'workspace', path: '.roo/rules/brainclaw.md' },
58
58
  { kind: 'mcp', location: 'workspace', path: '.roo/mcp.json' },
59
59
  ],
60
+ 'openclaw': [
61
+ { kind: 'skill', location: 'machine', path: '.openclaw/workspace/skills/brainclaw/SKILL.md' },
62
+ ],
60
63
  };
61
64
  function mergeSurfaces(current, next) {
62
65
  const merged = [...current];
@@ -6,18 +6,20 @@ import os from 'node:os';
6
6
  * environment variables and well-known config paths. Returns the first confident
7
7
  * match, or undefined if no agent is detected.
8
8
  *
9
- * Detection order (highest confidence first):
9
+ * Detection order (highest confidence first — agents with dedicated env vars
10
+ * are tested before agents detected via passive/ambient env vars):
10
11
  * 1. BRAINCLAW_AGENT env var (explicit override)
11
- * 2. GitHub Copilot (VSCODE_GIT_IPC_HANDLE or VSCODE_INJECTION, then GITHUB_COPILOT_*)
12
- * 3. Claude Code (CLAUDE_CODE_VERSION or ANTHROPIC_AI_PRODUCT)
13
- * 4. Cursor (CURSOR_TRACE_ID or CURSOR_*)
14
- * 5. Windsurf (WINDSURF_*)
15
- * 6. Cline (CLINE_*)
12
+ * 2. Claude Code (CLAUDE_CODE_VERSION set by Claude Code itself)
13
+ * 3. Cursor (CURSOR_TRACE_ID set by Cursor itself)
14
+ * 4. Windsurf (WINDSURF_SESSION_ID set by Windsurf itself)
15
+ * 5. Cline (CLINE_AGENT — set by Cline itself)
16
+ * 6. GitHub Copilot (GITHUB_COPILOT_PRODUCT — passive VS Code env, tested after active agents)
16
17
  * 7. Codex CLI (~/.codex/ directory exists)
17
18
  * 8. OpenCode (OPENCODE_* env or ~/.config/opencode/)
18
19
  * 9. Antigravity / Gemini CLI (ANTIGRAVITY_* env or ~/.gemini/antigravity/)
19
20
  * 10. Continue (CONTINUE_*)
20
21
  * 11. Roo Code (ROO_*)
22
+ * 12. OpenClaw (~/.openclaw/ or OPENCLAW_*)
21
23
  */
22
24
  export function detectAiAgent(env = process.env, homeDir = os.homedir()) {
23
25
  // Explicit override
@@ -29,24 +31,14 @@ export function detectAiAgent(env = process.env, homeDir = os.homedir()) {
29
31
  detection_source: 'BRAINCLAW_AGENT env var',
30
32
  };
31
33
  }
32
- // GitHub CopilotVS Code extension or Copilot Chat
33
- if (env.GITHUB_COPILOT_TOKEN ||
34
- env.GITHUB_COPILOT_PRODUCT ||
35
- (env.VSCODE_GIT_IPC_HANDLE && (env.AGENT_NAME?.toLowerCase().includes('copilot') || env.GH_COPILOT_AGENT))) {
36
- return {
37
- name: 'github-copilot',
38
- kind: 'agent',
39
- trust_level: 'trusted',
40
- detection_source: 'GITHUB_COPILOT_* env var',
41
- };
42
- }
43
- // Claude Code (Anthropic's CLI coding agent)
34
+ // Claude Codetested BEFORE Copilot because both can be present in VS Code.
35
+ // CLAUDE_CODE_VERSION is set by Claude Code itself, not by VS Code passively.
44
36
  if (env.CLAUDE_CODE_VERSION || env.ANTHROPIC_AI_PRODUCT === 'claude-code') {
45
37
  return {
46
38
  name: 'claude-code',
47
39
  kind: 'agent',
48
40
  trust_level: 'trusted',
49
- detection_source: 'CLAUDE_CODE_VERSION env var',
41
+ detection_source: env.CLAUDE_CODE_VERSION ? 'CLAUDE_CODE_VERSION env var' : 'ANTHROPIC_AI_PRODUCT env var',
50
42
  };
51
43
  }
52
44
  // Cursor IDE
@@ -76,6 +68,20 @@ export function detectAiAgent(env = process.env, homeDir = os.homedir()) {
76
68
  detection_source: 'CLINE_* env var',
77
69
  };
78
70
  }
71
+ // GitHub Copilot — tested AFTER agents with dedicated env vars.
72
+ // GITHUB_COPILOT_TOKEN and GITHUB_COPILOT_PRODUCT are set passively by VS Code
73
+ // whenever the Copilot extension is installed, even when another agent is active.
74
+ // We only match if no higher-priority agent was detected above.
75
+ if (env.GITHUB_COPILOT_PRODUCT ||
76
+ (env.GITHUB_COPILOT_TOKEN && !env.CLAUDE_CODE_VERSION && !env.CURSOR_TRACE_ID && !env.WINDSURF_SESSION_ID) ||
77
+ (env.VSCODE_GIT_IPC_HANDLE && (env.AGENT_NAME?.toLowerCase().includes('copilot') || env.GH_COPILOT_AGENT))) {
78
+ return {
79
+ name: 'github-copilot',
80
+ kind: 'agent',
81
+ trust_level: 'trusted',
82
+ detection_source: env.GITHUB_COPILOT_PRODUCT ? 'GITHUB_COPILOT_PRODUCT env var' : 'GITHUB_COPILOT_TOKEN env var',
83
+ };
84
+ }
79
85
  // OpenAI Codex CLI (~/.codex/ presence)
80
86
  if (fs.existsSync(path.join(homeDir, '.codex'))) {
81
87
  return {
@@ -121,6 +127,15 @@ export function detectAiAgent(env = process.env, homeDir = os.homedir()) {
121
127
  detection_source: 'ROO_* env var',
122
128
  };
123
129
  }
130
+ // OpenClaw (~/.openclaw/ presence or OPENCLAW_* env)
131
+ if (env.OPENCLAW_SESSION_ID || env.OPENCLAW_AGENT || fs.existsSync(path.join(homeDir, '.openclaw'))) {
132
+ return {
133
+ name: 'openclaw',
134
+ kind: 'agent',
135
+ trust_level: 'trusted',
136
+ detection_source: env.OPENCLAW_SESSION_ID || env.OPENCLAW_AGENT ? 'OPENCLAW_* env var' : '~/.openclaw directory',
137
+ };
138
+ }
124
139
  return undefined;
125
140
  }
126
141
  /**
@@ -267,6 +267,12 @@ function buildBootstrapArtifacts(input) {
267
267
  const repoAnalysis = analyzeRepository(input.cwd);
268
268
  sourcesScanned.push('repo-analysis');
269
269
  seeds.push(...extractRepoAnalysisSeeds(repoAnalysis, input.target));
270
+ // Additional brownfield sources (step 12)
271
+ const additionalSeeds = extractAdditionalBrownfieldSeeds(input.cwd, input.target);
272
+ if (additionalSeeds.seeds.length > 0) {
273
+ sourcesScanned.push(...additionalSeeds.sources);
274
+ seeds.push(...additionalSeeds.seeds);
275
+ }
270
276
  const gitProbe = probeGit(input.cwd, input.target);
271
277
  if (gitProbe.available) {
272
278
  sourcesScanned.push('git');
@@ -703,6 +709,44 @@ function probeGit(cwd, target) {
703
709
  }));
704
710
  }
705
711
  }
712
+ // Step 13: Active branches
713
+ const branchResult = spawnSync('git', ['branch', '--no-merged', 'HEAD', '--format=%(refname:short)'], {
714
+ cwd,
715
+ encoding: 'utf-8',
716
+ timeout: 5000,
717
+ });
718
+ if (branchResult.status === 0) {
719
+ const branches = branchResult.stdout.split(/\r?\n/).map((b) => b.trim()).filter(Boolean).slice(0, 5);
720
+ for (const branch of branches) {
721
+ hotspotSeeds.push(createSeed({
722
+ text: `Active branch: ${branch}`,
723
+ seedKind: 'hotspot',
724
+ sourceKind: 'git',
725
+ sourceRef: `branch:${branch}`,
726
+ confidence: 'low',
727
+ tags: ['bootstrap', 'git', 'branch'],
728
+ }));
729
+ }
730
+ }
731
+ // Step 13: Recent tags
732
+ const tagResult = spawnSync('git', ['tag', '--sort=-creatordate', '-l'], {
733
+ cwd,
734
+ encoding: 'utf-8',
735
+ timeout: 5000,
736
+ });
737
+ if (tagResult.status === 0) {
738
+ const tags = tagResult.stdout.split(/\r?\n/).map((t) => t.trim()).filter(Boolean).slice(0, 3);
739
+ if (tags.length > 0) {
740
+ hotspotSeeds.push(createSeed({
741
+ text: `Version tags: ${tags.join(', ')} (${tags.length} most recent)`,
742
+ seedKind: 'convention',
743
+ sourceKind: 'git',
744
+ sourceRef: 'tags',
745
+ confidence: 'medium',
746
+ tags: ['bootstrap', 'git', 'versioning'],
747
+ }));
748
+ }
749
+ }
706
750
  return {
707
751
  available: true,
708
752
  repoFingerprint,
@@ -1594,4 +1638,137 @@ function normalizeTarget(target) {
1594
1638
  const trimmed = target?.trim();
1595
1639
  return trimmed && trimmed.length > 0 ? trimmed : undefined;
1596
1640
  }
1641
+ // ─── Step 12: Additional brownfield sources ──────────────────────────────────
1642
+ const CI_WORKFLOW_DIRS = ['.github/workflows', '.gitlab'];
1643
+ const CI_FILES = ['.gitlab-ci.yml', 'Jenkinsfile', '.circleci/config.yml'];
1644
+ const CONTRIBUTING_FILES = ['CONTRIBUTING.md', 'CONTRIBUTING'];
1645
+ const CHANGELOG_FILES = ['CHANGELOG.md', 'CHANGELOG', 'HISTORY.md'];
1646
+ const DOCKER_FILES = ['Dockerfile', 'docker-compose.yml', 'docker-compose.yaml', 'compose.yml', 'compose.yaml'];
1647
+ const ENV_EXAMPLE_FILES = ['.env.example', '.env.sample', '.env.template'];
1648
+ const ADR_DIRS = ['doc/adr', 'docs/adr', 'doc/decisions', 'docs/decisions', 'adr'];
1649
+ function extractAdditionalBrownfieldSeeds(cwd, target) {
1650
+ const seeds = [];
1651
+ const sources = [];
1652
+ // CI/CD workflows
1653
+ for (const dir of CI_WORKFLOW_DIRS) {
1654
+ const fullPath = path.join(cwd, dir);
1655
+ if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {
1656
+ sources.push('ci_workflows');
1657
+ try {
1658
+ const files = fs.readdirSync(fullPath).filter((f) => f.endsWith('.yml') || f.endsWith('.yaml'));
1659
+ if (files.length > 0) {
1660
+ seeds.push(createSeed({
1661
+ text: `CI/CD: ${files.length} workflow(s) in ${dir}/`,
1662
+ seedKind: 'convention',
1663
+ sourceKind: 'ci_config',
1664
+ sourceRef: dir,
1665
+ confidence: 'medium',
1666
+ tags: ['bootstrap', 'ci'],
1667
+ relatedPaths: target ? [target] : undefined,
1668
+ }));
1669
+ }
1670
+ }
1671
+ catch { /* skip unreadable */ }
1672
+ break;
1673
+ }
1674
+ }
1675
+ for (const file of CI_FILES) {
1676
+ if (fs.existsSync(path.join(cwd, file))) {
1677
+ if (!sources.includes('ci_workflows'))
1678
+ sources.push('ci_config');
1679
+ seeds.push(createSeed({
1680
+ text: `CI/CD config: ${file}`,
1681
+ seedKind: 'convention',
1682
+ sourceKind: 'ci_config',
1683
+ sourceRef: file,
1684
+ confidence: 'medium',
1685
+ tags: ['bootstrap', 'ci'],
1686
+ }));
1687
+ break;
1688
+ }
1689
+ }
1690
+ // CONTRIBUTING.md
1691
+ const contributingPath = findFirstExisting(cwd, CONTRIBUTING_FILES);
1692
+ if (contributingPath) {
1693
+ sources.push('contributing');
1694
+ seeds.push(createSeed({
1695
+ text: `Contributing guide found: ${path.basename(contributingPath)}`,
1696
+ seedKind: 'convention',
1697
+ sourceKind: 'contributing',
1698
+ sourceRef: path.basename(contributingPath),
1699
+ confidence: 'medium',
1700
+ tags: ['bootstrap', 'contributing'],
1701
+ }));
1702
+ }
1703
+ // CHANGELOG
1704
+ const changelogPath = findFirstExisting(cwd, CHANGELOG_FILES);
1705
+ if (changelogPath) {
1706
+ sources.push('changelog');
1707
+ seeds.push(createSeed({
1708
+ text: `Changelog found: ${path.basename(changelogPath)}`,
1709
+ seedKind: 'convention',
1710
+ sourceKind: 'changelog',
1711
+ sourceRef: path.basename(changelogPath),
1712
+ confidence: 'low',
1713
+ tags: ['bootstrap', 'changelog'],
1714
+ }));
1715
+ }
1716
+ // Docker
1717
+ for (const file of DOCKER_FILES) {
1718
+ if (fs.existsSync(path.join(cwd, file))) {
1719
+ sources.push('docker');
1720
+ seeds.push(createSeed({
1721
+ text: `Docker config: ${file}`,
1722
+ seedKind: 'convention',
1723
+ sourceKind: 'docker',
1724
+ sourceRef: file,
1725
+ confidence: 'medium',
1726
+ tags: ['bootstrap', 'docker', 'infrastructure'],
1727
+ }));
1728
+ break;
1729
+ }
1730
+ }
1731
+ // .env.example
1732
+ const envPath = findFirstExisting(cwd, ENV_EXAMPLE_FILES);
1733
+ if (envPath) {
1734
+ sources.push('env_example');
1735
+ try {
1736
+ const content = fs.readFileSync(envPath, 'utf-8');
1737
+ const varCount = content.split(/\r?\n/).filter((l) => l.includes('=') && !l.startsWith('#')).length;
1738
+ seeds.push(createSeed({
1739
+ text: `Environment template: ${path.basename(envPath)} (${varCount} variables)`,
1740
+ seedKind: 'convention',
1741
+ sourceKind: 'env_example',
1742
+ sourceRef: path.basename(envPath),
1743
+ confidence: 'medium',
1744
+ tags: ['bootstrap', 'env', 'configuration'],
1745
+ }));
1746
+ }
1747
+ catch { /* skip unreadable */ }
1748
+ }
1749
+ // ADR (Architecture Decision Records)
1750
+ for (const dir of ADR_DIRS) {
1751
+ const fullPath = path.join(cwd, dir);
1752
+ if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {
1753
+ sources.push('adr');
1754
+ try {
1755
+ const files = fs.readdirSync(fullPath).filter((f) => f.endsWith('.md'));
1756
+ if (files.length > 0) {
1757
+ seeds.push(createSeed({
1758
+ text: `Architecture Decision Records: ${files.length} ADR(s) in ${dir}/`,
1759
+ seedKind: 'convention',
1760
+ sourceKind: 'adr',
1761
+ sourceRef: dir,
1762
+ confidence: 'high',
1763
+ tags: ['bootstrap', 'adr', 'architecture'],
1764
+ relatedPaths: target ? [target] : undefined,
1765
+ }));
1766
+ }
1767
+ }
1768
+ catch { /* skip unreadable */ }
1769
+ break;
1770
+ }
1771
+ }
1772
+ return { seeds, sources: [...new Set(sources)] };
1773
+ }
1597
1774
  //# sourceMappingURL=bootstrap.js.map