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