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.
@@ -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
- : undefined;
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.9.0",
4
- "description": "IMA's Claude Code skills for functional programming, architecture, and team standards. 47 skills, 24 hooks, default persona, 3-tier memory system.",
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"