claude-code-templates 1.29.2 ā 1.29.4
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/README.md +7 -3
- package/cli-tool/bin/create-claude-config.js +3 -0
- package/cli-tool/package.json +1 -1
- package/cli-tool/src/error-reporting.js +158 -0
- package/cli-tool/src/index.js +142 -5
- package/cli-tool/src/sandbox-server.js +57 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
[](https://z.ai/subscribe?ic=8JVLJQFSKB&utm_source=github&utm_medium=badge&utm_campaign=readme)
|
|
6
6
|
[](https://claude.com/contact-sales/claude-for-oss)
|
|
7
7
|
[](https://get.neon.com/4eCjZDz)
|
|
8
|
+
[](https://github.com/sponsors/davila7)
|
|
8
9
|
[](https://buymeacoffee.com/daniavila)
|
|
9
10
|
[](https://github.com/davila7/claude-code-templates)
|
|
10
11
|
|
|
@@ -39,7 +40,8 @@
|
|
|
39
40
|
|
|
40
41
|
**[Browse All Templates](https://aitmpl.com)** - Interactive web interface to explore and install 100+ agents, commands, settings, hooks, and MCPs.
|
|
41
42
|
|
|
42
|
-
<img width="
|
|
43
|
+
<img width="1787" height="958" alt="image" src="https://github.com/user-attachments/assets/d84feaa4-f871-4843-bbee-42d8f51b2f21" />
|
|
44
|
+
|
|
43
45
|
|
|
44
46
|
## š Quick Installation
|
|
45
47
|
|
|
@@ -153,10 +155,12 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
|
153
155
|
- **š Issues**: [GitHub Issues](https://github.com/davila7/claude-code-templates/issues)
|
|
154
156
|
|
|
155
157
|
## Stargazers over time
|
|
156
|
-
[](https://www.aitmpl.com/component/skill/git/star-history-chart)
|
|
157
159
|
|
|
158
160
|
---
|
|
159
161
|
|
|
160
162
|
**ā Found this useful? Give us a star to support the project!**
|
|
161
163
|
|
|
162
|
-
[](https://github.com/sponsors/davila7)
|
|
165
|
+
|
|
166
|
+
[](https://buymeacoffee.com/daniavila)
|
|
@@ -4,6 +4,7 @@ const { program } = require('commander');
|
|
|
4
4
|
const chalk = require('chalk');
|
|
5
5
|
const { createClaudeConfig } = require('../src/index');
|
|
6
6
|
const { showBanner } = require('../src/tui');
|
|
7
|
+
const { captureCliError } = require('../src/error-reporting');
|
|
7
8
|
|
|
8
9
|
const pkg = require('../package.json');
|
|
9
10
|
|
|
@@ -37,6 +38,7 @@ program
|
|
|
37
38
|
.option('--setting <setting>', 'install specific setting component (supports comma-separated values)')
|
|
38
39
|
.option('--hook <hook>', 'install specific hook component (supports comma-separated values)')
|
|
39
40
|
.option('--skill <skill>', 'install specific skill component (supports comma-separated values)')
|
|
41
|
+
.option('--loop <loop>', 'install specific loop component and its referenced components (supports comma-separated values)')
|
|
40
42
|
.option('--workflow <workflow>', 'install workflow from hash (#hash) OR workflow YAML (base64 encoded) when used with --agent/--command/--mcp')
|
|
41
43
|
.option('--prompt <prompt>', 'execute the provided prompt in Claude Code after installation or in sandbox')
|
|
42
44
|
.option('--create-agent <agent>', 'create a global agent accessible from anywhere (e.g., customer-support)')
|
|
@@ -62,6 +64,7 @@ program
|
|
|
62
64
|
await createClaudeConfig(options);
|
|
63
65
|
} catch (error) {
|
|
64
66
|
console.error(chalk.red('Error:'), error.message);
|
|
67
|
+
await captureCliError(error, { command: 'createClaudeConfig' });
|
|
65
68
|
process.exit(1);
|
|
66
69
|
}
|
|
67
70
|
});
|
package/cli-tool/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-templates",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.29.4",
|
|
4
4
|
"description": "CLI tool to setup Claude Code configurations with framework-specific commands, automation hooks and MCP Servers for your projects",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ErrorReporting - Opt-in crash reporting to Sentry
|
|
3
|
+
*
|
|
4
|
+
* Unlike tracking-service.js (anonymous usage analytics, on by default),
|
|
5
|
+
* this NEVER sends anything unless the user explicitly opts in via
|
|
6
|
+
* CCT_ERROR_REPORTING=true. It always respects the existing opt-out flags
|
|
7
|
+
* (CCT_NO_TRACKING, CCT_NO_ANALYTICS, CI) so opt-out still wins over opt-in.
|
|
8
|
+
*
|
|
9
|
+
* No SDK dependency ā sends directly to the Sentry envelope API via fetch(),
|
|
10
|
+
* matching the zero-dependency style used elsewhere in this project
|
|
11
|
+
* (cloudflare-workers/*\/sentry.js, dashboard/src/lib/api/error-tracking.ts).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// Public DSN for the "aitmpl-cli" Sentry project. Sentry DSNs are not
|
|
15
|
+
// secrets (they only allow sending events, not reading them) ā this is the
|
|
16
|
+
// same pattern most public telemetry SDKs use. Override with
|
|
17
|
+
// CCT_SENTRY_DSN for local testing against a different project.
|
|
18
|
+
const DEFAULT_SENTRY_DSN = 'https://1c1637a93ea6fb81e3edae3d039bdae0@o42738.ingest.us.sentry.io/4511679145312256';
|
|
19
|
+
const SENTRY_DSN = process.env.CCT_SENTRY_DSN || DEFAULT_SENTRY_DSN;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Whether error reporting is allowed to run at all.
|
|
23
|
+
* Opt-in required (CCT_ERROR_REPORTING=true), and opt-out always wins.
|
|
24
|
+
*/
|
|
25
|
+
function shouldReportErrors() {
|
|
26
|
+
if (process.env.CCT_ERROR_REPORTING !== 'true') {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (process.env.CCT_NO_TRACKING === 'true' ||
|
|
31
|
+
process.env.CCT_NO_ANALYTICS === 'true' ||
|
|
32
|
+
process.env.CI === 'true') {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!SENTRY_DSN) {
|
|
37
|
+
if (process.env.CCT_DEBUG === 'true') {
|
|
38
|
+
console.debug('š CCT_ERROR_REPORTING is set but no DSN is configured; skipping.');
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseDsn(dsn) {
|
|
47
|
+
try {
|
|
48
|
+
const url = new URL(dsn);
|
|
49
|
+
return {
|
|
50
|
+
publicKey: url.username,
|
|
51
|
+
host: url.host,
|
|
52
|
+
projectId: url.pathname.replace(/^\//, ''),
|
|
53
|
+
};
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Report a CLI error to Sentry. Opt-in only ā see shouldReportErrors().
|
|
61
|
+
* Never throws; safe to call from any catch block.
|
|
62
|
+
*
|
|
63
|
+
* @param {Error} error
|
|
64
|
+
* @param {object} [context] - e.g. { command: 'install', componentType: 'agent' }
|
|
65
|
+
*/
|
|
66
|
+
async function captureCliError(error, context = {}) {
|
|
67
|
+
if (!shouldReportErrors()) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const parsed = parseDsn(SENTRY_DSN);
|
|
73
|
+
if (!parsed) return;
|
|
74
|
+
|
|
75
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
76
|
+
const stack = error instanceof Error ? error.stack : undefined;
|
|
77
|
+
const eventId = require('crypto').randomUUID().replace(/-/g, '');
|
|
78
|
+
const timestamp = new Date().toISOString();
|
|
79
|
+
|
|
80
|
+
const event = {
|
|
81
|
+
event_id: eventId,
|
|
82
|
+
timestamp,
|
|
83
|
+
platform: 'node',
|
|
84
|
+
logger: 'cli',
|
|
85
|
+
environment: 'production',
|
|
86
|
+
tags: {
|
|
87
|
+
cli_version: getCliVersion(),
|
|
88
|
+
node_version: process.version,
|
|
89
|
+
platform: process.platform,
|
|
90
|
+
...context,
|
|
91
|
+
},
|
|
92
|
+
extra: context,
|
|
93
|
+
exception: {
|
|
94
|
+
values: [{
|
|
95
|
+
type: error instanceof Error ? (error.name || 'Error') : 'Error',
|
|
96
|
+
value: message,
|
|
97
|
+
stacktrace: stack
|
|
98
|
+
? { frames: [{ filename: 'cli', function: 'anonymous', context_line: stack }] }
|
|
99
|
+
: undefined,
|
|
100
|
+
}],
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const envelopeHeader = JSON.stringify({ event_id: eventId, sent_at: timestamp });
|
|
105
|
+
const itemHeader = JSON.stringify({ type: 'event' });
|
|
106
|
+
const body = `${envelopeHeader}\n${itemHeader}\n${JSON.stringify(event)}\n`;
|
|
107
|
+
|
|
108
|
+
const endpoint = `https://${parsed.host}/api/${parsed.projectId}/envelope/`;
|
|
109
|
+
const authHeader = `Sentry sentry_version=7, sentry_client=aitmpl-cli/1.0, sentry_key=${parsed.publicKey}`;
|
|
110
|
+
|
|
111
|
+
if (typeof fetch !== 'function') {
|
|
112
|
+
// Node < 18 without global fetch ā skip rather than pull in a dependency.
|
|
113
|
+
if (process.env.CCT_DEBUG === 'true') {
|
|
114
|
+
console.debug('š Error reporting requires Node 18+ (global fetch); skipping.');
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const controller = new AbortController();
|
|
120
|
+
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
await fetch(endpoint, {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
headers: {
|
|
126
|
+
'Content-Type': 'application/x-sentry-envelope',
|
|
127
|
+
'X-Sentry-Auth': authHeader,
|
|
128
|
+
},
|
|
129
|
+
body,
|
|
130
|
+
signal: controller.signal,
|
|
131
|
+
});
|
|
132
|
+
if (process.env.CCT_DEBUG === 'true') {
|
|
133
|
+
console.debug('š Error reported to Sentry:', eventId);
|
|
134
|
+
}
|
|
135
|
+
} finally {
|
|
136
|
+
clearTimeout(timeoutId);
|
|
137
|
+
}
|
|
138
|
+
} catch (reportingError) {
|
|
139
|
+
// Error reporting must never crash the CLI or mask the original error.
|
|
140
|
+
if (process.env.CCT_DEBUG === 'true') {
|
|
141
|
+
console.debug('š Error reporting failed (non-critical):', reportingError.message);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function getCliVersion() {
|
|
147
|
+
try {
|
|
148
|
+
const path = require('path');
|
|
149
|
+
const fs = require('fs');
|
|
150
|
+
const packagePath = path.join(__dirname, '..', 'package.json');
|
|
151
|
+
const packageData = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
152
|
+
return packageData.version || 'unknown';
|
|
153
|
+
} catch {
|
|
154
|
+
return 'unknown';
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = { captureCliError, shouldReportErrors };
|
package/cli-tool/src/index.js
CHANGED
|
@@ -143,7 +143,7 @@ async function createClaudeConfig(options = {}) {
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
// Handle multiple components installation (new approach)
|
|
146
|
-
if (options.agent || options.command || options.mcp || options.setting || options.hook || options.skill) {
|
|
146
|
+
if (options.agent || options.command || options.mcp || options.setting || options.hook || options.skill || options.loop) {
|
|
147
147
|
// If --workflow is used with components, treat it as YAML
|
|
148
148
|
if (options.workflow) {
|
|
149
149
|
options.yaml = options.workflow;
|
|
@@ -1652,6 +1652,123 @@ async function installIndividualSkill(skillName, targetDir, options) {
|
|
|
1652
1652
|
}
|
|
1653
1653
|
}
|
|
1654
1654
|
|
|
1655
|
+
/**
|
|
1656
|
+
* Parse the `components:` frontmatter field of a loop into {type, path} refs.
|
|
1657
|
+
* The field is a flat bracketed list of `type:path` tokens, e.g.
|
|
1658
|
+
* components: [agent:documentation/docs-architect, skill:git/create-pr]
|
|
1659
|
+
*/
|
|
1660
|
+
function parseLoopReferencedComponents(loopContent) {
|
|
1661
|
+
const refs = [];
|
|
1662
|
+
if (!loopContent || !loopContent.startsWith('---')) return refs;
|
|
1663
|
+
const fmEnd = loopContent.indexOf('---', 3);
|
|
1664
|
+
if (fmEnd === -1) return refs;
|
|
1665
|
+
const frontmatter = loopContent.slice(3, fmEnd);
|
|
1666
|
+
for (const line of frontmatter.split('\n')) {
|
|
1667
|
+
if (!line.startsWith('components:')) continue;
|
|
1668
|
+
const value = line.slice(line.indexOf(':') + 1).trim().replace(/^\[|\]$/g, '');
|
|
1669
|
+
for (const token of value.split(',')) {
|
|
1670
|
+
const trimmed = token.trim().replace(/^["']|["']$/g, '');
|
|
1671
|
+
if (!trimmed) continue;
|
|
1672
|
+
const sep = trimmed.indexOf(':');
|
|
1673
|
+
if (sep === -1) continue;
|
|
1674
|
+
const type = trimmed.slice(0, sep).trim();
|
|
1675
|
+
const compPath = trimmed.slice(sep + 1).trim().replace(/\.(md|json)$/, '');
|
|
1676
|
+
if (type && compPath) refs.push({ type, path: compPath });
|
|
1677
|
+
}
|
|
1678
|
+
break;
|
|
1679
|
+
}
|
|
1680
|
+
return refs;
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
/**
|
|
1684
|
+
* Install a loop component (markdown) into .claude/loops/, then auto-install
|
|
1685
|
+
* every component it references (agents, skills, hooks, commands, settings, mcps).
|
|
1686
|
+
*/
|
|
1687
|
+
async function installIndividualLoop(loopName, targetDir, options = {}) {
|
|
1688
|
+
console.log(chalk.blue(`š Installing loop: ${loopName}`));
|
|
1689
|
+
const startTime = Date.now();
|
|
1690
|
+
|
|
1691
|
+
try {
|
|
1692
|
+
const githubUrl = `https://raw.githubusercontent.com/davila7/claude-code-templates/main/cli-tool/components/loops/${loopName}.md`;
|
|
1693
|
+
console.log(chalk.gray(`š„ Downloading from GitHub (main branch)...`));
|
|
1694
|
+
|
|
1695
|
+
const response = await fetch(githubUrl);
|
|
1696
|
+
if (!response.ok) {
|
|
1697
|
+
if (response.status === 404) {
|
|
1698
|
+
console.log(chalk.red(`ā Loop "${loopName}" not found`));
|
|
1699
|
+
trackingService.trackInstallationOutcome('loop', loopName, 'failure', { errorType: 'not_found', durationMs: Date.now() - startTime, batchId: options.batchId });
|
|
1700
|
+
return false;
|
|
1701
|
+
}
|
|
1702
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
const loopContent = await response.text();
|
|
1706
|
+
|
|
1707
|
+
// Write the loop file to .claude/loops/ (flat, like agents)
|
|
1708
|
+
const loopsDir = path.join(targetDir, '.claude', 'loops');
|
|
1709
|
+
await fs.ensureDir(loopsDir);
|
|
1710
|
+
const fileName = loopName.includes('/') ? loopName.split('/').pop() : loopName;
|
|
1711
|
+
const targetFile = path.join(loopsDir, `${fileName}.md`);
|
|
1712
|
+
await fs.writeFile(targetFile, loopContent, 'utf8');
|
|
1713
|
+
|
|
1714
|
+
if (!options.silent) {
|
|
1715
|
+
console.log(chalk.green(`ā
Loop "${loopName}" installed successfully!`));
|
|
1716
|
+
console.log(chalk.cyan(`š Installed to: ${path.relative(targetDir, targetFile)}`));
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
trackingService.trackDownload('loop', loopName, {
|
|
1720
|
+
installation_type: 'individual_loop',
|
|
1721
|
+
target_directory: path.relative(process.cwd(), targetDir),
|
|
1722
|
+
source: 'github_main'
|
|
1723
|
+
});
|
|
1724
|
+
|
|
1725
|
+
// Auto-install referenced components
|
|
1726
|
+
const refs = parseLoopReferencedComponents(loopContent);
|
|
1727
|
+
if (refs.length > 0) {
|
|
1728
|
+
console.log(chalk.blue(`\nš§© Installing ${refs.length} referenced component(s) for this loop...`));
|
|
1729
|
+
const refOptions = { ...options, silent: true };
|
|
1730
|
+
for (const ref of refs) {
|
|
1731
|
+
try {
|
|
1732
|
+
switch (ref.type) {
|
|
1733
|
+
case 'agent':
|
|
1734
|
+
await installIndividualAgent(ref.path, targetDir, refOptions);
|
|
1735
|
+
break;
|
|
1736
|
+
case 'command':
|
|
1737
|
+
await installIndividualCommand(ref.path, targetDir, refOptions);
|
|
1738
|
+
break;
|
|
1739
|
+
case 'skill':
|
|
1740
|
+
await installIndividualSkill(ref.path, targetDir, refOptions);
|
|
1741
|
+
break;
|
|
1742
|
+
case 'hook':
|
|
1743
|
+
await installIndividualHook(ref.path, targetDir, refOptions);
|
|
1744
|
+
break;
|
|
1745
|
+
case 'setting':
|
|
1746
|
+
await installIndividualSetting(ref.path, targetDir, refOptions);
|
|
1747
|
+
break;
|
|
1748
|
+
case 'mcp':
|
|
1749
|
+
await installIndividualMCP(ref.path, targetDir, refOptions);
|
|
1750
|
+
break;
|
|
1751
|
+
default:
|
|
1752
|
+
console.log(chalk.yellow(` ā ļø Unknown referenced component type: ${ref.type}:${ref.path}`));
|
|
1753
|
+
continue;
|
|
1754
|
+
}
|
|
1755
|
+
console.log(chalk.green(` ā ${ref.type}: ${ref.path}`));
|
|
1756
|
+
} catch (refError) {
|
|
1757
|
+
console.log(chalk.yellow(` ā ļø Could not install ${ref.type}:${ref.path} (${refError.message})`));
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
trackingService.trackInstallationOutcome('loop', loopName, 'success', { durationMs: Date.now() - startTime, batchId: options.batchId });
|
|
1763
|
+
return true;
|
|
1764
|
+
|
|
1765
|
+
} catch (error) {
|
|
1766
|
+
console.log(chalk.red(`ā Error installing loop: ${error.message}`));
|
|
1767
|
+
trackingService.trackInstallationOutcome('loop', loopName, 'failure', { errorType: 'network_error', errorMessage: error.message, durationMs: Date.now() - startTime, batchId: options.batchId });
|
|
1768
|
+
return false;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1655
1772
|
/**
|
|
1656
1773
|
* Install multiple components with optional YAML workflow
|
|
1657
1774
|
*/
|
|
@@ -1666,7 +1783,8 @@ async function installMultipleComponents(options, targetDir) {
|
|
|
1666
1783
|
mcps: [],
|
|
1667
1784
|
settings: [],
|
|
1668
1785
|
hooks: [],
|
|
1669
|
-
skills: []
|
|
1786
|
+
skills: [],
|
|
1787
|
+
loops: []
|
|
1670
1788
|
};
|
|
1671
1789
|
|
|
1672
1790
|
// Parse comma-separated values for each component type
|
|
@@ -1700,7 +1818,12 @@ async function installMultipleComponents(options, targetDir) {
|
|
|
1700
1818
|
components.skills = skillsInput.split(',').map(s => s.trim()).filter(s => s);
|
|
1701
1819
|
}
|
|
1702
1820
|
|
|
1703
|
-
|
|
1821
|
+
if (options.loop) {
|
|
1822
|
+
const loopsInput = Array.isArray(options.loop) ? options.loop.join(',') : options.loop;
|
|
1823
|
+
components.loops = loopsInput.split(',').map(l => l.trim()).filter(l => l);
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
const totalComponents = components.agents.length + components.commands.length + components.mcps.length + components.settings.length + components.hooks.length + components.skills.length + components.loops.length;
|
|
1704
1827
|
|
|
1705
1828
|
if (totalComponents === 0) {
|
|
1706
1829
|
console.log(chalk.yellow('ā ļø No components specified to install.'));
|
|
@@ -1714,13 +1837,15 @@ async function installMultipleComponents(options, targetDir) {
|
|
|
1714
1837
|
console.log(chalk.gray(` Settings: ${components.settings.length}`));
|
|
1715
1838
|
console.log(chalk.gray(` Hooks: ${components.hooks.length}`));
|
|
1716
1839
|
console.log(chalk.gray(` Skills: ${components.skills.length}`));
|
|
1717
|
-
|
|
1840
|
+
console.log(chalk.gray(` Loops: ${components.loops.length}`));
|
|
1841
|
+
|
|
1718
1842
|
// Counter for successfully installed components
|
|
1719
1843
|
let successfullyInstalled = 0;
|
|
1720
1844
|
|
|
1721
1845
|
// Ask for installation locations once for configuration components (if any exist and not in silent mode)
|
|
1722
1846
|
let sharedInstallLocations = ['local']; // default
|
|
1723
|
-
|
|
1847
|
+
// Loops can pull in settings/hooks via their referenced components, so prompt for a location when loops are present too.
|
|
1848
|
+
const hasSettingsOrHooks = components.settings.length > 0 || components.hooks.length > 0 || components.loops.length > 0;
|
|
1724
1849
|
|
|
1725
1850
|
if (hasSettingsOrHooks && !options.yes) {
|
|
1726
1851
|
console.log(chalk.blue('\nš Choose installation locations for configuration components:'));
|
|
@@ -1812,6 +1937,18 @@ async function installMultipleComponents(options, targetDir) {
|
|
|
1812
1937
|
if (skillSuccess) successfullyInstalled++;
|
|
1813
1938
|
}
|
|
1814
1939
|
|
|
1940
|
+
// Install loops (auto-installs their referenced components)
|
|
1941
|
+
for (const loop of components.loops) {
|
|
1942
|
+
console.log(chalk.gray(` Installing loop: ${loop}`));
|
|
1943
|
+
const loopSuccess = await installIndividualLoop(loop, targetDir, {
|
|
1944
|
+
...options,
|
|
1945
|
+
silent: true,
|
|
1946
|
+
sharedInstallLocations: sharedInstallLocations,
|
|
1947
|
+
batchId
|
|
1948
|
+
});
|
|
1949
|
+
if (loopSuccess) successfullyInstalled++;
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1815
1952
|
// Handle YAML workflow if provided
|
|
1816
1953
|
if (options.yaml) {
|
|
1817
1954
|
console.log(chalk.blue('\nš Processing workflow YAML...'));
|
|
@@ -9,6 +9,12 @@ const fs = require('fs');
|
|
|
9
9
|
const app = express();
|
|
10
10
|
const PORT = process.env.PORT || 3444;
|
|
11
11
|
|
|
12
|
+
// Platform-aware command names. Now that spawns run without `shell: true`,
|
|
13
|
+
// Windows needs the `.cmd` shim to resolve npm-installed executables (bare
|
|
14
|
+
// 'npx'/'claude' would fail with ENOENT on win32).
|
|
15
|
+
const NPX_CMD = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
16
|
+
const CLAUDE_CMD = process.platform === 'win32' ? 'claude.cmd' : 'claude';
|
|
17
|
+
|
|
12
18
|
// Load .env file from current working directory (where user runs the command)
|
|
13
19
|
function loadEnvFile() {
|
|
14
20
|
const envPath = path.join(process.cwd(), '.env');
|
|
@@ -47,11 +53,26 @@ function loadEnvFile() {
|
|
|
47
53
|
// Load environment variables on startup
|
|
48
54
|
const hasApiKeys = loadEnvFile();
|
|
49
55
|
|
|
50
|
-
//
|
|
56
|
+
// CORS middleware ā restrict to the local Studio UI origin only.
|
|
57
|
+
// A wildcard (`*`) origin combined with the command-executing endpoints below
|
|
58
|
+
// lets any web page the developer visits drive requests into this server
|
|
59
|
+
// (drive-by RCE). Only allow the same-origin UI served from localhost:PORT.
|
|
60
|
+
const ALLOWED_ORIGINS = new Set([
|
|
61
|
+
`http://localhost:${PORT}`,
|
|
62
|
+
`http://127.0.0.1:${PORT}`,
|
|
63
|
+
]);
|
|
51
64
|
app.use((req, res, next) => {
|
|
52
|
-
|
|
65
|
+
const origin = req.headers.origin;
|
|
66
|
+
if (origin && ALLOWED_ORIGINS.has(origin)) {
|
|
67
|
+
res.header('Access-Control-Allow-Origin', origin);
|
|
68
|
+
}
|
|
69
|
+
res.header('Vary', 'Origin');
|
|
53
70
|
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
54
71
|
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
|
|
72
|
+
// Reject cross-origin requests outright for the state-changing endpoints.
|
|
73
|
+
if (origin && !ALLOWED_ORIGINS.has(origin)) {
|
|
74
|
+
return res.status(403).json({ success: false, error: 'Cross-origin request rejected' });
|
|
75
|
+
}
|
|
55
76
|
if (req.method === 'OPTIONS') {
|
|
56
77
|
res.sendStatus(200);
|
|
57
78
|
} else {
|
|
@@ -199,22 +220,31 @@ app.get('/api/tasks', (req, res) => {
|
|
|
199
220
|
// API endpoint to install agent
|
|
200
221
|
app.post('/api/install-agent', async (req, res) => {
|
|
201
222
|
const { agentName } = req.body;
|
|
202
|
-
|
|
223
|
+
|
|
203
224
|
if (!agentName) {
|
|
204
225
|
return res.status(400).json({
|
|
205
226
|
success: false,
|
|
206
227
|
error: 'Agent name is required'
|
|
207
228
|
});
|
|
208
229
|
}
|
|
209
|
-
|
|
230
|
+
|
|
231
|
+
// SECURITY: agent names are `category/name` slugs. Reject anything else so a
|
|
232
|
+
// value like "x; rm -rf ~" can never reach the child process.
|
|
233
|
+
if (!/^[A-Za-z0-9._/-]+$/.test(agentName)) {
|
|
234
|
+
return res.status(400).json({
|
|
235
|
+
success: false,
|
|
236
|
+
error: 'Invalid agent name'
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
210
240
|
try {
|
|
211
241
|
console.log(chalk.blue('š§ Installing agent:'), chalk.cyan(agentName));
|
|
212
|
-
|
|
213
|
-
//
|
|
214
|
-
|
|
242
|
+
|
|
243
|
+
// SECURITY: shell:false (default) keeps agentName as a single argv entry ā
|
|
244
|
+
// no shell parses it, so metacharacters cannot inject commands.
|
|
245
|
+
const child = spawn(NPX_CMD, ['claude-code-templates@latest', '--agent', agentName, '--yes'], {
|
|
215
246
|
cwd: process.cwd(),
|
|
216
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
217
|
-
shell: true
|
|
247
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
218
248
|
});
|
|
219
249
|
|
|
220
250
|
let output = [];
|
|
@@ -276,11 +306,17 @@ async function checkAndInstallAgent(agentName, task) {
|
|
|
276
306
|
|
|
277
307
|
task.output.push(`š§ Agent ${agentName} not found locally. Installing...`);
|
|
278
308
|
|
|
309
|
+
// SECURITY: reject anything that is not a plain `category/name` slug.
|
|
310
|
+
if (!/^[A-Za-z0-9._/-]+$/.test(agentName)) {
|
|
311
|
+
task.output.push(`ā Invalid agent name: ${agentName}`);
|
|
312
|
+
return Promise.resolve(false);
|
|
313
|
+
}
|
|
314
|
+
|
|
279
315
|
return new Promise((resolve, reject) => {
|
|
280
|
-
|
|
316
|
+
// SECURITY: shell:false (default) ā agentName stays a single argv entry.
|
|
317
|
+
const child = spawn(NPX_CMD, ['claude-code-templates@latest', '--agent', agentName, '--yes'], {
|
|
281
318
|
cwd: process.cwd(),
|
|
282
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
283
|
-
shell: true
|
|
319
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
284
320
|
});
|
|
285
321
|
|
|
286
322
|
child.stdout.on('data', (data) => {
|
|
@@ -460,15 +496,18 @@ async function executeLocalTask(task) {
|
|
|
460
496
|
finalPrompt = `As a ${task.agent.replace('-', ' ')}, ${task.prompt}`;
|
|
461
497
|
}
|
|
462
498
|
|
|
463
|
-
// Execute Claude Code locally with just the prompt
|
|
464
|
-
|
|
499
|
+
// Execute Claude Code locally with just the prompt.
|
|
500
|
+
// SECURITY: never run through a shell. With shell:true, Node joins argv into
|
|
501
|
+
// a single `sh -c` string and the attacker-controlled prompt is parsed by the
|
|
502
|
+
// shell (command injection). shell:false keeps finalPrompt as a single argv[1].
|
|
503
|
+
// Windows: resolve claude.cmd explicitly (CLAUDE_CMD) instead of re-enabling shell:true.
|
|
504
|
+
const child = spawn(CLAUDE_CMD, [finalPrompt], {
|
|
465
505
|
cwd: process.cwd(),
|
|
466
506
|
stdio: ['ignore', 'pipe', 'pipe'], // Ignore stdin to prevent hanging
|
|
467
507
|
env: {
|
|
468
508
|
...process.env,
|
|
469
509
|
PATH: process.env.PATH
|
|
470
510
|
},
|
|
471
|
-
shell: true, // Use shell to find claude command
|
|
472
511
|
timeout: 300000 // 5 minute timeout
|
|
473
512
|
});
|
|
474
513
|
|
|
@@ -558,8 +597,9 @@ async function executeLocalTask(task) {
|
|
|
558
597
|
}
|
|
559
598
|
}
|
|
560
599
|
|
|
561
|
-
// Start server
|
|
562
|
-
|
|
600
|
+
// Start server ā bind to loopback only. This is a local developer tool that
|
|
601
|
+
// executes commands; binding to 0.0.0.0 exposes RCE to the whole LAN.
|
|
602
|
+
app.listen(PORT, '127.0.0.1', () => {
|
|
563
603
|
console.log(chalk.blue('\\nšØ Claude Code Studio Server'));
|
|
564
604
|
console.log(chalk.cyan('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
565
605
|
console.log(chalk.green(`š Server running on http://localhost:${PORT}`));
|