ima-claude 2.10.0 → 2.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -22
- package/dist/cli.js +736 -17
- package/package.json +2 -2
- package/platforms/gemini/adapter.ts +443 -0
- package/platforms/gemini/gemini-extension.json +17 -0
- package/platforms/gemini/hooks-translator.py +66 -0
- package/platforms/gh-copilot/adapter.ts +437 -0
- package/platforms/gh-copilot/hook-translations.md +91 -0
- package/platforms/gh-copilot/hooks-translator.py +66 -0
- package/platforms/shared/detector.ts +7 -1
- package/plugins/ima-claude/.claude-plugin/plugin.json +2 -2
- package/plugins/ima-claude/skills/gh-cli/SKILL.md +286 -0
- package/plugins/ima-claude/skills/mcp-gitea/SKILL.md +358 -0
- package/plugins/ima-claude/skills/mcp-github/SKILL.md +200 -0
- package/plugins/ima-claude/skills/mcp-qdrant/SKILL.md +21 -10
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import { join, dirname } from "path";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { existsSync, readdirSync, statSync, readFileSync, writeFileSync, copyFileSync } from "fs";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
import type { PlatformAdapter, InstallItem, InstallPreview } from "../shared/types";
|
|
7
|
+
import { ensureDir, copyDirRecursive, log, SKILLS_TO_INSTALL, HOOKS_TO_INSTALL, HOOKS_CONFIG, VERSION } from "../../scripts/utils";
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = dirname(__filename);
|
|
11
|
+
|
|
12
|
+
const COPILOT_DIR = join(homedir(), ".copilot");
|
|
13
|
+
const COPILOT_SKILLS_DIR = join(COPILOT_DIR, "skills");
|
|
14
|
+
const COPILOT_AGENTS_DIR = join(COPILOT_DIR, "agents");
|
|
15
|
+
const COPILOT_HOOKS_DIR = join(COPILOT_DIR, "hooks");
|
|
16
|
+
const COPILOT_GUIDELINES_FILE = join(COPILOT_DIR, "copilot-instructions.md");
|
|
17
|
+
|
|
18
|
+
// Claude Code → GitHub Copilot tool name mapping
|
|
19
|
+
const TOOL_MAP: Record<string, string> = {
|
|
20
|
+
Bash: "run_terminal_command",
|
|
21
|
+
Read: "read_file",
|
|
22
|
+
Edit: "edit_file",
|
|
23
|
+
Write: "write_file",
|
|
24
|
+
Glob: "find_files",
|
|
25
|
+
Grep: "search_code",
|
|
26
|
+
LS: "list_directory",
|
|
27
|
+
WebSearch: "web_search",
|
|
28
|
+
WebFetch: "fetch_url",
|
|
29
|
+
ExitPlanMode: "ExitPlanMode",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Claude Code → GitHub Copilot hook event mapping
|
|
33
|
+
const EVENT_MAP: Record<string, string> = {
|
|
34
|
+
PreToolUse: "preToolUse",
|
|
35
|
+
PostToolUse: "postToolUse",
|
|
36
|
+
UserPromptSubmit: "userPromptSubmitted",
|
|
37
|
+
SessionStart: "sessionStart",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Simple single-line YAML parser — same as Gemini/Junie adapters.
|
|
41
|
+
function parseFrontmatter(content: string): { frontmatter: Record<string, string>; body: string } {
|
|
42
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
43
|
+
if (!match) return { frontmatter: {}, body: content };
|
|
44
|
+
|
|
45
|
+
const frontmatter: Record<string, string> = {};
|
|
46
|
+
for (const line of match[1].split("\n")) {
|
|
47
|
+
const colonIdx = line.indexOf(":");
|
|
48
|
+
if (colonIdx === -1) continue;
|
|
49
|
+
const key = line.slice(0, colonIdx).trim();
|
|
50
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
51
|
+
if (key) frontmatter[key] = value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { frontmatter, body: match[2] };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function serializeFrontmatter(frontmatter: Record<string, string>, body: string): string {
|
|
58
|
+
const lines = Object.entries(frontmatter).map(([k, v]) => `${k}: ${v}`);
|
|
59
|
+
return `---\n${lines.join("\n")}\n---\n${body}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function mapToolName(claudeName: string): string {
|
|
63
|
+
return TOOL_MAP[claudeName] ?? claudeName;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function transformAgentForCopilot(content: string): string {
|
|
67
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
68
|
+
|
|
69
|
+
// Drop permissionMode (Copilot has no equivalent)
|
|
70
|
+
// Drop model (Copilot uses its own model selection)
|
|
71
|
+
const { permissionMode: _perm, model: _model, ...kept } = frontmatter;
|
|
72
|
+
|
|
73
|
+
// Map tool names in the tools field if present
|
|
74
|
+
if (kept.tools) {
|
|
75
|
+
const mapped = kept.tools
|
|
76
|
+
.split(",")
|
|
77
|
+
.map((t) => t.trim())
|
|
78
|
+
.map(mapToolName)
|
|
79
|
+
.join(", ");
|
|
80
|
+
kept.tools = mapped;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return serializeFrontmatter(kept, body);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function translateMatcher(matcher: string): string {
|
|
87
|
+
// MCP tool matchers pass through unchanged; only map built-in Claude tool names
|
|
88
|
+
return TOOL_MAP[matcher] ?? matcher;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function translateHookCommand(command: string): string {
|
|
92
|
+
// Rewrite hook commands to route through the translator shim
|
|
93
|
+
// Original: python3 ~/.claude/hooks/some_hook.py
|
|
94
|
+
// Copilot: python3 ~/.copilot/hooks/hooks-translator.py ~/.copilot/hooks/some_hook.py
|
|
95
|
+
const hooksDir = COPILOT_HOOKS_DIR;
|
|
96
|
+
const translatorPath = join(hooksDir, "hooks-translator.py");
|
|
97
|
+
|
|
98
|
+
// Extract the script filename (and any trailing args) from the original command
|
|
99
|
+
const match = command.match(/python3\s+.*\/([^/\s]+\.py)(\s.*)?$/);
|
|
100
|
+
if (!match) return command;
|
|
101
|
+
|
|
102
|
+
const scriptName = match[1];
|
|
103
|
+
const trailingArgs = match[2] ?? "";
|
|
104
|
+
return `python3 ${translatorPath} ${join(hooksDir, scriptName)}${trailingArgs}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function generateCopilotHooksConfig(): Record<string, unknown> {
|
|
108
|
+
const copilotHooks: Record<string, unknown[]> = {};
|
|
109
|
+
|
|
110
|
+
for (const [claudeEvent, hookEntries] of Object.entries(HOOKS_CONFIG.hooks)) {
|
|
111
|
+
const copilotEvent = EVENT_MAP[claudeEvent] ?? claudeEvent;
|
|
112
|
+
|
|
113
|
+
// Flatten: Claude groups multiple hooks under one matcher,
|
|
114
|
+
// Copilot uses one flat entry per hook command
|
|
115
|
+
const flatEntries: Record<string, unknown>[] = [];
|
|
116
|
+
|
|
117
|
+
for (const entry of hookEntries as Array<{ matcher?: string; hooks: Array<{ type: string; command: string }> }>) {
|
|
118
|
+
const translatedMatcher = entry.matcher ? translateMatcher(entry.matcher) : undefined;
|
|
119
|
+
|
|
120
|
+
for (const hook of entry.hooks) {
|
|
121
|
+
const flatEntry: Record<string, unknown> = {};
|
|
122
|
+
if (translatedMatcher) {
|
|
123
|
+
flatEntry.matcher = translatedMatcher;
|
|
124
|
+
}
|
|
125
|
+
flatEntry.type = hook.type;
|
|
126
|
+
flatEntry.bash = translateHookCommand(hook.command);
|
|
127
|
+
flatEntries.push(flatEntry);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
copilotHooks[copilotEvent] = flatEntries;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { version: 1, hooks: copilotHooks };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function generateCopilotInstructionsMd(): string {
|
|
138
|
+
return `# ima-claude: AI Coding Agent Guidelines
|
|
139
|
+
|
|
140
|
+
> Generated by ima-claude v${VERSION} for GitHub Copilot.
|
|
141
|
+
> Source: https://github.com/Soabirw/ima-claude
|
|
142
|
+
|
|
143
|
+
## Default Persona: The Practitioner
|
|
144
|
+
|
|
145
|
+
A 25-year software development veteran. FP-first, composition-minded, anti-over-engineering.
|
|
146
|
+
Uses "we" not "I" — collaborative, humble, light-hearted. "Slow is smooth, smooth is fast."
|
|
147
|
+
|
|
148
|
+
**Philosophy**: Simple > Complex | Evidence > Assumptions | Native > Utilities | MVP > Enterprise
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Memory Routing
|
|
153
|
+
|
|
154
|
+
| Store what | Where | Why |
|
|
155
|
+
|---|---|---|
|
|
156
|
+
| Decisions, preferences, patterns, bugs | Vestige \`smart_ingest\` | Fades naturally if not referenced |
|
|
157
|
+
| Reference material (docs, standards, PRDs) | Qdrant \`qdrant-store\` | Permanent library |
|
|
158
|
+
| Session state, task progress | Serena \`write_memory\` | Project-scoped workbench |
|
|
159
|
+
| Future reminders | Vestige \`intention\` | Surfaces at next session |
|
|
160
|
+
|
|
161
|
+
At session start, check memory before asking questions:
|
|
162
|
+
- Vestige: search for user preferences and project context
|
|
163
|
+
- Vestige: check for pending reminders/intentions
|
|
164
|
+
- Serena: list memories if in a Serena-activated project
|
|
165
|
+
|
|
166
|
+
Auto-store: "I prefer..." → Vestige preference. "Let's go with X because..." → Vestige decision. "The reason this failed..." → Vestige bug.
|
|
167
|
+
|
|
168
|
+
After completing work: store outcome in Vestige, reference material in Qdrant, session state in Serena.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Orchestrator Protocol
|
|
173
|
+
|
|
174
|
+
You are the Orchestrator. Plan and delegate. Do NOT implement directly.
|
|
175
|
+
- Non-trivial work → task-planner (decompose) → task-runner (delegate)
|
|
176
|
+
- Trivial = single file, < 5 lines, no judgment calls
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Available Agents
|
|
181
|
+
|
|
182
|
+
Delegate to named agents — they enforce tools and permissions automatically.
|
|
183
|
+
|
|
184
|
+
| Agent | Use For |
|
|
185
|
+
|---|---|
|
|
186
|
+
| \`explorer\` | File discovery, codebase exploration |
|
|
187
|
+
| \`implementer\` | Feature dev, bug fixes, refactoring |
|
|
188
|
+
| \`reviewer\` | Code review, security audit, FP checks |
|
|
189
|
+
| \`wp-developer\` | WordPress plugins, themes, WP-CLI, forms |
|
|
190
|
+
| \`memory\` | Memory search, storage, consolidation |
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Code Navigation (Serena)
|
|
195
|
+
|
|
196
|
+
When Serena MCP is available, **prefer Serena over read_file/search_code for code investigation.** 40-70% token savings.
|
|
197
|
+
|
|
198
|
+
| Instead of | Use |
|
|
199
|
+
|---|---|
|
|
200
|
+
| Read file to understand structure | Serena get_symbols_overview |
|
|
201
|
+
| search_code for class/function definition | Serena find_symbol |
|
|
202
|
+
| search_code for callers/references | Serena find_referencing_symbols |
|
|
203
|
+
|
|
204
|
+
Use read_file only when you need the actual implementation body of a known, specific symbol.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Complex Reasoning
|
|
209
|
+
|
|
210
|
+
Use sequential thinking before acting on:
|
|
211
|
+
- Debugging / root cause analysis / "why is this failing"
|
|
212
|
+
- Trade-off evaluation / "which approach"
|
|
213
|
+
- Architectural decisions / design choices
|
|
214
|
+
- Multi-step investigations where approach may change
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## MCP Tool Routing
|
|
219
|
+
|
|
220
|
+
| Signal | Preferred Tool |
|
|
221
|
+
|---|---|
|
|
222
|
+
| "latest", "2025/2026", "what's new" | Tavily search |
|
|
223
|
+
| Library/framework API question | Context7 |
|
|
224
|
+
| URL content extraction | Tavily extract (use advanced for complex pages) |
|
|
225
|
+
|
|
226
|
+
Before web tools: check internal knowledge → Context7 → then Tavily.
|
|
227
|
+
Before external lookups: check Vestige memory first.
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Search Preference
|
|
232
|
+
|
|
233
|
+
Always prefer \`rg\` (ripgrep) over grep/find. Faster, respects .gitignore, recursive by default.
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Security
|
|
238
|
+
|
|
239
|
+
- Verify nonce usage and input sanitization in WordPress PHP code
|
|
240
|
+
- Never concatenate user input directly into SQL — use parameterized queries
|
|
241
|
+
- Check for XSS, CSRF, and OWASP top 10 vulnerabilities in written code
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Code Style
|
|
246
|
+
|
|
247
|
+
- Don't create custom FP utility functions (pipe, compose, curry) — use language-native patterns or established libraries
|
|
248
|
+
- In WordPress JavaScript context, use jQuery patterns when jQuery is already loaded
|
|
249
|
+
- Prefer Bootstrap utility classes over custom CSS overrides
|
|
250
|
+
- Run \`composer dump-autoload\` after creating new PHP files
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Documentation
|
|
255
|
+
|
|
256
|
+
Follow the three-tier documentation system:
|
|
257
|
+
- **Active** — Living docs, kept current (README, API docs, architecture)
|
|
258
|
+
- **Archive** — Historical reference, rarely updated (decisions, post-mortems)
|
|
259
|
+
- **Transient** — Ephemeral, git-ignored (session notes, scratch)
|
|
260
|
+
`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export class GhCopilotAdapter implements PlatformAdapter {
|
|
264
|
+
readonly name = "gh-copilot";
|
|
265
|
+
readonly displayName = "GitHub Copilot";
|
|
266
|
+
readonly configDir = COPILOT_DIR;
|
|
267
|
+
|
|
268
|
+
detect(): boolean {
|
|
269
|
+
return existsSync(COPILOT_DIR);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
preview(sourceDir: string): InstallPreview {
|
|
273
|
+
const skillItems: InstallItem[] = SKILLS_TO_INSTALL.map((skill) => ({
|
|
274
|
+
name: skill,
|
|
275
|
+
category: "skill" as const,
|
|
276
|
+
destPath: join(COPILOT_SKILLS_DIR, skill),
|
|
277
|
+
exists: existsSync(join(COPILOT_SKILLS_DIR, skill)),
|
|
278
|
+
})).filter((item) => existsSync(join(sourceDir, "skills", item.name)));
|
|
279
|
+
|
|
280
|
+
const agentsDir = join(sourceDir, "agents");
|
|
281
|
+
const agentItems: InstallItem[] = existsSync(agentsDir)
|
|
282
|
+
? readdirSync(agentsDir)
|
|
283
|
+
.filter((f) => f.endsWith(".md"))
|
|
284
|
+
.map((file) => ({
|
|
285
|
+
name: file.replace(/\.md$/, ""),
|
|
286
|
+
category: "agent" as const,
|
|
287
|
+
destPath: join(COPILOT_AGENTS_DIR, file.replace(/\.md$/, ".agent.md")),
|
|
288
|
+
exists: existsSync(join(COPILOT_AGENTS_DIR, file.replace(/\.md$/, ".agent.md"))),
|
|
289
|
+
}))
|
|
290
|
+
: [];
|
|
291
|
+
|
|
292
|
+
const hookItems: InstallItem[] = HOOKS_TO_INSTALL.map((file) => ({
|
|
293
|
+
name: file,
|
|
294
|
+
category: "hook" as const,
|
|
295
|
+
destPath: join(COPILOT_HOOKS_DIR, file),
|
|
296
|
+
exists: existsSync(join(COPILOT_HOOKS_DIR, file)),
|
|
297
|
+
}));
|
|
298
|
+
|
|
299
|
+
// Include the translator shim and generated hooks.json
|
|
300
|
+
const translatorItem: InstallItem = {
|
|
301
|
+
name: "hooks-translator.py",
|
|
302
|
+
category: "hook",
|
|
303
|
+
destPath: join(COPILOT_HOOKS_DIR, "hooks-translator.py"),
|
|
304
|
+
exists: existsSync(join(COPILOT_HOOKS_DIR, "hooks-translator.py")),
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const guidelineItem: InstallItem = {
|
|
308
|
+
name: "copilot-instructions.md",
|
|
309
|
+
category: "guideline",
|
|
310
|
+
destPath: COPILOT_GUIDELINES_FILE,
|
|
311
|
+
exists: existsSync(COPILOT_GUIDELINES_FILE),
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
platform: this.name,
|
|
316
|
+
targetDir: COPILOT_DIR,
|
|
317
|
+
items: [...skillItems, ...agentItems, ...hookItems, translatorItem, guidelineItem],
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
installSkills(sourceDir: string, exclude?: string[]): void {
|
|
322
|
+
ensureDir(COPILOT_SKILLS_DIR);
|
|
323
|
+
const skills = exclude?.length
|
|
324
|
+
? SKILLS_TO_INSTALL.filter((s) => !exclude.includes(s))
|
|
325
|
+
: SKILLS_TO_INSTALL;
|
|
326
|
+
for (const skill of skills) {
|
|
327
|
+
const src = join(sourceDir, skill);
|
|
328
|
+
if (existsSync(src) && statSync(src).isDirectory()) {
|
|
329
|
+
copyDirRecursive(src, join(COPILOT_SKILLS_DIR, skill));
|
|
330
|
+
log.step(`skill: ${skill}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
installAgents(sourceDir: string, exclude?: string[]): void {
|
|
336
|
+
ensureDir(COPILOT_AGENTS_DIR);
|
|
337
|
+
const entries = readdirSync(sourceDir)
|
|
338
|
+
.filter((f) => f.endsWith(".md"))
|
|
339
|
+
.filter((f) => !exclude?.includes(f.replace(/\.md$/, "")));
|
|
340
|
+
for (const file of entries) {
|
|
341
|
+
const content = readFileSync(join(sourceDir, file), "utf8");
|
|
342
|
+
const transformed = transformAgentForCopilot(content);
|
|
343
|
+
// Copilot uses .agent.md extension
|
|
344
|
+
const destFile = file.replace(/\.md$/, ".agent.md");
|
|
345
|
+
writeFileSync(join(COPILOT_AGENTS_DIR, destFile), transformed);
|
|
346
|
+
log.step(`agent: ${destFile}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
installGuidelines(_pluginRoot: string): void {
|
|
351
|
+
ensureDir(COPILOT_DIR);
|
|
352
|
+
writeFileSync(COPILOT_GUIDELINES_FILE, generateCopilotInstructionsMd());
|
|
353
|
+
log.step(`guidelines: ${COPILOT_GUIDELINES_FILE}`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
installHooks(sourceDir: string, exclude?: string[]): void {
|
|
357
|
+
ensureDir(COPILOT_HOOKS_DIR);
|
|
358
|
+
|
|
359
|
+
// Copy hook scripts
|
|
360
|
+
const hooks = exclude?.length
|
|
361
|
+
? HOOKS_TO_INSTALL.filter((f) => !exclude.includes(f))
|
|
362
|
+
: HOOKS_TO_INSTALL;
|
|
363
|
+
for (const file of hooks) {
|
|
364
|
+
const src = join(sourceDir, file);
|
|
365
|
+
if (existsSync(src)) {
|
|
366
|
+
copyFileSync(src, join(COPILOT_HOOKS_DIR, file));
|
|
367
|
+
log.step(`hook: ${file}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Copy the translator shim from the platform directory
|
|
372
|
+
const shimSrc = join(__dirname, "hooks-translator.py");
|
|
373
|
+
if (!existsSync(shimSrc)) {
|
|
374
|
+
throw new Error(`hooks-translator.py not found at ${shimSrc} — packaging error`);
|
|
375
|
+
}
|
|
376
|
+
copyFileSync(shimSrc, join(COPILOT_HOOKS_DIR, "hooks-translator.py"));
|
|
377
|
+
log.step("hook: hooks-translator.py (shim)");
|
|
378
|
+
|
|
379
|
+
// Generate Copilot-specific hooks.json
|
|
380
|
+
const hooksConfig = generateCopilotHooksConfig();
|
|
381
|
+
const hooksConfigPath = join(COPILOT_HOOKS_DIR, "hooks.json");
|
|
382
|
+
mergeCopilotHooksConfig(hooksConfigPath, hooksConfig);
|
|
383
|
+
log.step("hook: hooks.json (generated for GitHub Copilot)");
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
postInstall(): void {
|
|
387
|
+
log.info("GitHub Copilot install complete. Verify:");
|
|
388
|
+
log.info(` Skills: ${COPILOT_SKILLS_DIR}`);
|
|
389
|
+
log.info(` Agents: ${COPILOT_AGENTS_DIR}`);
|
|
390
|
+
log.info(` Hooks: ${COPILOT_HOOKS_DIR}`);
|
|
391
|
+
log.info(` Guidelines: ${COPILOT_GUIDELINES_FILE}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function mergeCopilotHooksConfig(
|
|
396
|
+
configPath: string,
|
|
397
|
+
newConfig: Record<string, unknown>
|
|
398
|
+
): void {
|
|
399
|
+
let existing: Record<string, unknown> = {};
|
|
400
|
+
|
|
401
|
+
if (existsSync(configPath)) {
|
|
402
|
+
try {
|
|
403
|
+
const content = readFileSync(configPath, "utf8");
|
|
404
|
+
existing = JSON.parse(content);
|
|
405
|
+
} catch {
|
|
406
|
+
existing = {};
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Start with version from new config
|
|
411
|
+
existing.version = (newConfig as Record<string, unknown>).version ?? 1;
|
|
412
|
+
|
|
413
|
+
if (!existing.hooks) {
|
|
414
|
+
existing.hooks = {};
|
|
415
|
+
}
|
|
416
|
+
const existingHooks = existing.hooks as Record<string, unknown[]>;
|
|
417
|
+
const incomingHooks = (newConfig as Record<string, unknown>).hooks as Record<string, Array<{ matcher?: string }>>;
|
|
418
|
+
|
|
419
|
+
// Merge each event type: remove old ima-claude entries, then add new ones.
|
|
420
|
+
// User hooks (without hooks-translator.py in the command) are preserved.
|
|
421
|
+
for (const [event, entries] of Object.entries(incomingHooks)) {
|
|
422
|
+
if (!existingHooks[event]) {
|
|
423
|
+
existingHooks[event] = entries;
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Strip all old ima-claude entries (identified by hooks-translator.py in the bash command)
|
|
428
|
+
const userHooks = (existingHooks[event] as Array<{ bash?: string }>).filter(
|
|
429
|
+
(h) => !h.bash?.includes("hooks-translator.py")
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
// Append new ima-claude entries
|
|
433
|
+
existingHooks[event] = [...userHooks, ...entries];
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
|
|
437
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Hook-to-Copilot Translation Map
|
|
2
|
+
|
|
3
|
+
This document tracks the relationship between Claude Code hooks and their
|
|
4
|
+
GitHub Copilot equivalents. GitHub Copilot has full hook support (preToolUse,
|
|
5
|
+
postToolUse, userPromptSubmitted, sessionStart), so all hooks translate 1:1
|
|
6
|
+
via the hooks-translator.py shim — the same strategy as Gemini CLI.
|
|
7
|
+
|
|
8
|
+
## Translation Strategy
|
|
9
|
+
|
|
10
|
+
Copilot's hook system is functionally equivalent to Claude Code's. The
|
|
11
|
+
`hooks-translator.py` shim sits between Copilot and each hook script,
|
|
12
|
+
translating Copilot tool names to Claude Code equivalents so hook scripts
|
|
13
|
+
work unmodified.
|
|
14
|
+
|
|
15
|
+
Key differences from Claude Code:
|
|
16
|
+
- Hook config uses `{ "version": 1, "hooks": { ... } }` format
|
|
17
|
+
- Event names are camelCase: `preToolUse`, `postToolUse`, `userPromptSubmitted`
|
|
18
|
+
- Each hook entry is flat: `{ matcher, type, bash }` (not grouped)
|
|
19
|
+
- Uses `bash` field (not `command`)
|
|
20
|
+
|
|
21
|
+
## Hook → Copilot Mapping
|
|
22
|
+
|
|
23
|
+
### Tool Redirection Hooks → Translated (via shim)
|
|
24
|
+
|
|
25
|
+
| Hook | Copilot Event | Matcher | Notes |
|
|
26
|
+
|------|--------------|---------|-------|
|
|
27
|
+
| `enforce_rg_over_grep.py` | preToolUse | run_terminal_command | Translates to Bash before hook runs |
|
|
28
|
+
| `webfetch_to_tavily.py` | preToolUse | fetch_url | Translates to WebFetch before hook runs |
|
|
29
|
+
| `websearch_to_tavily.py` | preToolUse | web_search | Translates to WebSearch before hook runs |
|
|
30
|
+
| `tavily_extract_advanced.py` | preToolUse | mcp__tavily__tavily-extract | MCP matcher passthrough |
|
|
31
|
+
|
|
32
|
+
### Memory Hooks → Translated (via shim)
|
|
33
|
+
|
|
34
|
+
| Hook | Copilot Event | Notes |
|
|
35
|
+
|------|--------------|-------|
|
|
36
|
+
| `memory_bootstrap.py` | preToolUse | Runs on multiple tool matchers |
|
|
37
|
+
| `memory_store_reminder.py` | postToolUse | Runs on Edit/Write |
|
|
38
|
+
| `vestige_before_external.py` | preToolUse | Runs on Tavily/Context7 MCP tools |
|
|
39
|
+
|
|
40
|
+
### Serena Hooks → Translated (via shim)
|
|
41
|
+
|
|
42
|
+
| Hook | Copilot Event | Notes |
|
|
43
|
+
|------|--------------|-------|
|
|
44
|
+
| `serena_over_read.py` | preToolUse | read_file → Read translation |
|
|
45
|
+
| `serena_over_grep.py` | preToolUse | search_code → Grep translation |
|
|
46
|
+
| `serena_project_check.py` | preToolUse | MCP matcher passthrough |
|
|
47
|
+
|
|
48
|
+
### Workflow Hooks → Translated (via shim)
|
|
49
|
+
|
|
50
|
+
| Hook | Copilot Event | Notes |
|
|
51
|
+
|------|--------------|-------|
|
|
52
|
+
| `prompt_coach.py` | userPromptSubmitted | Calls Anthropic API — requires ANTHROPIC_API_KEY |
|
|
53
|
+
| `task_master_before_impl.py` | userPromptSubmitted | Works unmodified |
|
|
54
|
+
| `task_master_after_plan.py` | postToolUse | ExitPlanMode matcher passthrough |
|
|
55
|
+
| `jira_issue_fetch.py` | userPromptSubmitted | Works unmodified |
|
|
56
|
+
|
|
57
|
+
### Security Hooks → Translated (via shim)
|
|
58
|
+
|
|
59
|
+
| Hook | Copilot Event | Notes |
|
|
60
|
+
|------|--------------|-------|
|
|
61
|
+
| `wp_security_check.py` | postToolUse | edit_file/write_file → Edit/Write translation |
|
|
62
|
+
| `sql_injection_check.py` | postToolUse | edit_file/write_file → Edit/Write translation |
|
|
63
|
+
|
|
64
|
+
### Code Quality Hooks → Translated (via shim)
|
|
65
|
+
|
|
66
|
+
| Hook | Copilot Event | Notes |
|
|
67
|
+
|------|--------------|-------|
|
|
68
|
+
| `fp_utility_check.py` | postToolUse | edit_file/write_file → Edit/Write translation |
|
|
69
|
+
| `jquery_in_wordpress.py` | postToolUse | edit_file/write_file → Edit/Write translation |
|
|
70
|
+
| `bootstrap_utility_check.py` | postToolUse | edit_file/write_file → Edit/Write translation |
|
|
71
|
+
| `composer_autoload_check.py` | postToolUse | edit_file/write_file → Edit/Write translation |
|
|
72
|
+
| `docs_organization.py` | postToolUse | write_file → Write translation |
|
|
73
|
+
| `block_sed_edits.py` | preToolUse | run_terminal_command → Bash translation |
|
|
74
|
+
|
|
75
|
+
### Sequential Thinking → Translated (via shim)
|
|
76
|
+
|
|
77
|
+
| Hook | Copilot Event | Notes |
|
|
78
|
+
|------|--------------|-------|
|
|
79
|
+
| `sequential_thinking_check.py` | userPromptSubmitted | Works unmodified |
|
|
80
|
+
|
|
81
|
+
### Atlassian Hooks → Translated (via shim)
|
|
82
|
+
|
|
83
|
+
| Hook | Copilot Event | Notes |
|
|
84
|
+
|------|--------------|-------|
|
|
85
|
+
| `atlassian_prereqs.py` | preToolUse | MCP matcher passthrough |
|
|
86
|
+
|
|
87
|
+
### Session Hooks
|
|
88
|
+
|
|
89
|
+
| Hook | Copilot Event | Notes |
|
|
90
|
+
|------|--------------|-------|
|
|
91
|
+
| `bootstrap.sh` | sessionStart | Content becomes copilot-instructions.md guidelines |
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""GitHub Copilot → Claude Code tool name translator shim.
|
|
3
|
+
|
|
4
|
+
Sits between GitHub Copilot and ima-claude hook scripts, translating
|
|
5
|
+
Copilot tool names to their Claude Code equivalents so existing hooks
|
|
6
|
+
work unmodified.
|
|
7
|
+
|
|
8
|
+
Usage (in hooks.json):
|
|
9
|
+
python3 ~/.copilot/hooks/hooks-translator.py ~/.copilot/hooks/some_hook.py
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
# GitHub Copilot → Claude Code tool name mapping (reverse of adapter TOOL_MAP)
|
|
17
|
+
COPILOT_TO_CLAUDE = {
|
|
18
|
+
"run_terminal_command": "Bash",
|
|
19
|
+
"read_file": "Read",
|
|
20
|
+
"edit_file": "Edit",
|
|
21
|
+
"write_file": "Write",
|
|
22
|
+
"find_files": "Glob",
|
|
23
|
+
"search_code": "Grep",
|
|
24
|
+
"list_directory": "LS",
|
|
25
|
+
"web_search": "WebSearch",
|
|
26
|
+
"fetch_url": "WebFetch",
|
|
27
|
+
"ExitPlanMode": "ExitPlanMode",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def main():
|
|
32
|
+
if len(sys.argv) < 2:
|
|
33
|
+
print("Usage: hooks-translator.py <hook-script> [args...]", file=sys.stderr)
|
|
34
|
+
sys.exit(1)
|
|
35
|
+
|
|
36
|
+
target_script = sys.argv[1]
|
|
37
|
+
extra_args = sys.argv[2:]
|
|
38
|
+
|
|
39
|
+
# Read JSON from stdin
|
|
40
|
+
try:
|
|
41
|
+
raw = sys.stdin.read()
|
|
42
|
+
data = json.loads(raw) if raw.strip() else {}
|
|
43
|
+
except json.JSONDecodeError as e:
|
|
44
|
+
print(f"hooks-translator: invalid JSON from stdin: {e}", file=sys.stderr)
|
|
45
|
+
sys.exit(1)
|
|
46
|
+
|
|
47
|
+
# Translate tool_name if present
|
|
48
|
+
tool_name = data.get("tool_name", "")
|
|
49
|
+
if tool_name in COPILOT_TO_CLAUDE:
|
|
50
|
+
data["tool_name"] = COPILOT_TO_CLAUDE[tool_name]
|
|
51
|
+
|
|
52
|
+
# Pipe translated JSON to the actual hook script
|
|
53
|
+
translated = json.dumps(data)
|
|
54
|
+
result = subprocess.run(
|
|
55
|
+
["python3", target_script] + extra_args,
|
|
56
|
+
input=translated,
|
|
57
|
+
stdout=None,
|
|
58
|
+
stderr=None,
|
|
59
|
+
text=True,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
sys.exit(result.returncode)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
if __name__ == "__main__":
|
|
66
|
+
main()
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import type { DetectedPlatform, PlatformAdapter } from "./types";
|
|
2
2
|
import { ClaudeAdapter } from "../claude/adapter";
|
|
3
3
|
import { JunieAdapter } from "../junie/adapter";
|
|
4
|
+
import { GeminiAdapter } from "../gemini/adapter";
|
|
5
|
+
import { GhCopilotAdapter } from "../gh-copilot/adapter";
|
|
4
6
|
|
|
5
7
|
const ADAPTERS: PlatformAdapter[] = [
|
|
6
8
|
new ClaudeAdapter(),
|
|
7
9
|
new JunieAdapter(),
|
|
10
|
+
new GeminiAdapter(),
|
|
11
|
+
new GhCopilotAdapter(),
|
|
8
12
|
];
|
|
9
13
|
|
|
10
14
|
export function detectPlatforms(): DetectedPlatform[] {
|
|
@@ -12,7 +16,9 @@ export function detectPlatforms(): DetectedPlatform[] {
|
|
|
12
16
|
const detected = adapter.detect();
|
|
13
17
|
const note = adapter.name === "claude" && detected
|
|
14
18
|
? "Recommended: install via plugin marketplace instead"
|
|
15
|
-
:
|
|
19
|
+
: adapter.name === "gemini" && detected
|
|
20
|
+
? "Also available as a Gemini extension"
|
|
21
|
+
: undefined;
|
|
16
22
|
|
|
17
23
|
return { adapter, detected, note };
|
|
18
24
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ima-claude",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "IMA's Claude Code skills for functional programming, architecture, and team standards.
|
|
3
|
+
"version": "2.14.0",
|
|
4
|
+
"description": "IMA's Claude Code skills for functional programming, architecture, and team standards. 52 skills, 24 hooks, default persona, 3-tier memory system.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "IMA",
|
|
7
7
|
"url": "https://github.com/Soabirw/ima-claude"
|