Skip to content

Commit eb2dcc3

Browse files
committed
feat: add plugin caching with environment variable expansion
- Add cache system that copies plugins per construct instance - Support ${VAR} and ${VAR:-default} syntax in configs - Expand env vars in .mcp.json, agent frontmatter, skill frontmatter - Add CLAUDE_PLUGIN_ROOT expansion to cached plugin path - Add --clear-cache CLI flag for cleanup after crashes - Support HTTP MCP servers in translation - Auto-cleanup cache on process exit (including SIGINT/SIGTERM)
1 parent 053b4b6 commit eb2dcc3

File tree

12 files changed

+1580
-62
lines changed

12 files changed

+1580
-62
lines changed

CLAUDE.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ bun run index.ts --load tmux@scaryrawr-plugins -- --continue
1717

1818
# Run with saved config
1919
bun run index.ts
20+
21+
# Clear plugin caches (after crashes/interrupts)
22+
bun run index.ts --clear-cache
2023
```
2124

2225
### Project Structure
@@ -158,6 +161,20 @@ const mergedEnv = {
158161
};
159162
```
160163

164+
### Variable Expansion
165+
Environment variables are automatically expanded during plugin loading:
166+
- **Syntax**: `${VAR}` or `${VAR:-default}`
167+
- **Scope**: MCP configs, agent frontmatter, and skill frontmatter
168+
- **Special Variable**: `${CLAUDE_PLUGIN_ROOT}` expands to the cached plugin directory path
169+
- See `variable-expansion.md` for complete syntax and behavior details
170+
171+
### Plugin Cache System
172+
Plugins are cached per construct instance for reliable environment variable expansion:
173+
- **Cache Location**: `~/.cache/construct/plugins/<instance-id>/<marketplace>/<plugin>/`
174+
- **Lifecycle**: Auto-created on startup, auto-deleted on exit (normal or signal-based)
175+
- **Purpose**: Enables environment variable expansion in plugin files without modifying originals
176+
- **Isolation**: Each construct instance has isolated cache, enabling parallel runs
177+
161178
## Testing Strategies
162179

163180
### Unit Testing

README.md

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,21 +58,69 @@ construct -- "fix the failing tests"
5858
construct operator
5959
construct operator -- --continue
6060

61+
# Clear plugin caches (removes all cached instances, useful after crashes)
62+
construct --clear-cache
63+
6164
# Type-check the codebase
6265
bun run typecheck
6366
```
6467

6568
`construct operator` launches an interactive `fzf` multi-select for plugins, saves your selection to `.construct.json`, and then runs Copilot with those plugins enabled.
6669

70+
## Environment Variables
71+
72+
Construct supports automatic environment variable expansion in plugin configurations:
73+
74+
### Supported Syntax
75+
76+
- `${VAR}` - Basic expansion (uses value if set, otherwise unchanged)
77+
- `${VAR:-default}` - Expansion with default value (uses default if VAR is unset or empty)
78+
79+
### Examples
80+
81+
```json
82+
{
83+
"chrome-devtools": {
84+
"command": "npx",
85+
"args": [
86+
"chrome-devtools-mcp@latest",
87+
"--browser-url=http://${DEVTOOLS_BASE_URL:-127.0.0.1}:${DEVTOOLS_PORT:-9222}"
88+
]
89+
},
90+
"greptile": {
91+
"type": "http",
92+
"url": "https://api.greptile.com/mcp",
93+
"headers": {
94+
"Authorization": "Bearer ${GREPTILE_API_KEY}"
95+
}
96+
}
97+
}
98+
```
99+
100+
### Special Variables
101+
102+
**`${CLAUDE_PLUGIN_ROOT}`** - Automatically expands to the cached plugin directory path. Useful for referencing plugin-relative paths in configurations.
103+
104+
### Expansion Scope
105+
106+
Environment variable expansion applies to:
107+
- MCP server configurations (`.mcp.json`)
108+
- Agent file frontmatter (YAML headers in `agents/*.md`)
109+
- Skill file frontmatter (YAML headers in `skills/*/SKILL.md`)
110+
111+
Note: Plugin file bodies (markdown content) are not expanded, only metadata/frontmatter.
112+
67113
## How It Works
68114

69115
1. **Scans** `~/.claude/plugins/installed_plugins.json` for installed Claude Code plugins
70116
2. **Discovers** skills, MCP servers, and agents in each plugin
71-
3. **Translates** Claude Code formats to Copilot CLI equivalents:
117+
3. **Caches** plugins per construct instance for reliable environment variable expansion
118+
4. **Translates** Claude Code formats to Copilot CLI equivalents:
72119
- Skills → `COPILOT_SKILLS_DIRS` environment variable
73-
- MCP configs → `--additional-mcp-config` JSON argument
120+
- MCP configs → `--additional-mcp-config` JSON argument (supports both local and HTTP servers)
74121
- Agents → `.github/agents/<plugin>-<agent>.md` files with translated tool references
75-
4. **Spawns** `copilot` with the translated configuration
122+
- Environment variables → Automatically expanded in configurations
123+
5. **Spawns** `copilot` with the translated configuration
76124

77125
### Agent Translation
78126

index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { loadConfig, saveConfig, mergeCliWithConfig } from "./src/config";
55
import { translatePlugins } from "./src/translator";
66
import { executeCopilot } from "./src/executor";
77
import { runOperator } from "./src/operator";
8+
import { clearAllCaches } from "./src/cache";
89

910
async function main(): Promise<void> {
1011
const args = parseCliArgs(process.argv);
@@ -14,6 +15,13 @@ async function main(): Promise<void> {
1415
process.exit(exitCode);
1516
}
1617

18+
// Handle --clear-cache
19+
if (args.clearCache) {
20+
await clearAllCaches();
21+
console.log("Cache cleared.");
22+
process.exit(0);
23+
}
24+
1725
// Handle --list
1826
if (args.listAvailablePlugins) {
1927
const plugins = await listAvailablePlugins();

src/agent-translator.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -183,16 +183,22 @@ export async function writeAgentFile(
183183
* @param component - Agent component to translate
184184
* @param pluginInfo - Plugin information
185185
* @param mcpServers - List of available MCP server names
186+
* @param cachedPath - Path to cached plugin directory with expanded frontmatter
186187
* @returns TranslatedAgent or null if translation fails
187188
*/
188189
export async function translateSingleAgent(
189190
component: { type: string; path: string; name: string },
190191
pluginInfo: PluginInfo,
191-
mcpServers: string[]
192+
mcpServers: string[],
193+
cachedPath: string
192194
): Promise<TranslatedAgent | null> {
193195
try {
194-
// Read agent file
195-
const file = Bun.file(component.path);
196+
// Compute relative path from plugin's install path and read from cached path
197+
const relativePath = component.path.replace(pluginInfo.installPath, '').replace(/^\//, '');
198+
const cachedFilePath = join(cachedPath, relativePath);
199+
200+
// Read agent file from cached path
201+
const file = Bun.file(cachedFilePath);
196202
const content = await file.text();
197203

198204
// Parse frontmatter
@@ -269,23 +275,33 @@ export async function translateSingleAgent(
269275
*
270276
* @param plugins - Array of enabled plugins
271277
* @param mcpServers - List of available MCP server names
278+
* @param cachedPaths - Map of plugin name to cached path
272279
* @returns Array of translated agents for cleanup tracking
273280
*/
274281
export async function translateAgents(
275282
plugins: PluginInfo[],
276-
mcpServers: string[]
283+
mcpServers: string[],
284+
cachedPaths: Map<string, string>
277285
): Promise<TranslatedAgent[]> {
278286
const translatedAgents: TranslatedAgent[] = [];
279287

280288
for (const plugin of plugins) {
289+
// Look up cached path for this plugin
290+
const cachedPath = cachedPaths.get(plugin.name);
291+
if (!cachedPath) {
292+
console.warn(`Skipping plugin ${plugin.name}: no cached path found`);
293+
continue;
294+
}
295+
281296
// Find agent components
282297
const agentComponents = plugin.components.filter(c => c.type === 'agent');
283298

284299
for (const component of agentComponents) {
285300
const translated = await translateSingleAgent(
286301
component,
287302
plugin,
288-
mcpServers
303+
mcpServers,
304+
cachedPath
289305
);
290306

291307
if (translated) {

0 commit comments

Comments
 (0)