chati-dev 3.1.1 → 3.2.1

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,494 @@
1
+ /**
2
+ * @fileoverview Gemini CLI hooks generator.
3
+ *
4
+ * Generates thin wrapper hooks for Gemini CLI that import shared logic
5
+ * from chati.dev/hooks/. Each hook translates between Gemini CLI event
6
+ * format and the existing hook logic, providing governance parity with
7
+ * Claude Code.
8
+ *
9
+ * Constitution Article XIX — hooks are generated at install time, zero runtime overhead.
10
+ *
11
+ * Gemini CLI Hook Events (used by chati.dev):
12
+ * - BeforeModel: runs before model inference (PRISM injection, model advisory)
13
+ * - BeforeTool: runs before tool execution (mode governance, constitution guard, read protection)
14
+ * - PreCompress: runs before context compression (session digest)
15
+ */
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Hook Map — Claude Hook → Gemini Event
19
+ // ---------------------------------------------------------------------------
20
+
21
+ /**
22
+ * Maps each chati.dev hook to its Gemini CLI equivalent event.
23
+ */
24
+ export const HOOK_MAP = {
25
+ 'prism-engine': { event: 'BeforeModel', description: 'Inject PRISM context into model prompt' },
26
+ 'model-governance': { event: 'BeforeModel', description: 'Advisory: recommended model per agent' },
27
+ 'mode-governance': { event: 'BeforeTool', description: 'Block writes outside current mode scope' },
28
+ 'constitution-guard': { event: 'BeforeTool', description: 'Block destructive commands and secret writes' },
29
+ 'read-protection': { event: 'BeforeTool', description: 'Block reads of sensitive files' },
30
+ 'session-digest': { event: 'PreCompress', description: 'Save session state before context compression' },
31
+ };
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Hook Templates
35
+ // ---------------------------------------------------------------------------
36
+
37
+ /**
38
+ * Shared header for all generated hooks.
39
+ */
40
+ const HOOK_HEADER = `#!/usr/bin/env node
41
+ /**
42
+ * Auto-generated by chati.dev — Gemini CLI hook wrapper.
43
+ * Imports shared logic from chati.dev/hooks/ for governance parity.
44
+ * Do not edit manually — regenerate with \`npx chati-dev init\`.
45
+ */
46
+
47
+ import { existsSync, readFileSync } from 'fs';
48
+ import { join } from 'path';
49
+ import { fileURLToPath } from 'url';
50
+
51
+ const __filename = fileURLToPath(import.meta.url);
52
+ const __dirname = join(__filename, '..');
53
+ const projectRoot = join(__dirname, '..', '..');
54
+ `;
55
+
56
+ /**
57
+ * Generate the PRISM engine hook for Gemini CLI.
58
+ * BeforeModel event — injects PRISM context XML into the model prompt.
59
+ */
60
+ function generatePrismEngine() {
61
+ return `${HOOK_HEADER}
62
+ /**
63
+ * PRISM Engine — BeforeModel
64
+ * Reads session state and injects PRISM context block.
65
+ */
66
+ async function main() {
67
+ let input = '';
68
+ for await (const chunk of process.stdin) {
69
+ input += chunk;
70
+ }
71
+
72
+ try {
73
+ const event = JSON.parse(input);
74
+ const cwd = event.cwd || process.cwd();
75
+
76
+ const sessionPath = join(cwd, '.chati', 'session.yaml');
77
+ if (!existsSync(sessionPath)) {
78
+ console.log(JSON.stringify({}));
79
+ return;
80
+ }
81
+
82
+ // Delegate to shared PRISM engine logic
83
+ const hookPath = join(cwd, 'chati.dev', 'hooks', 'prism-engine.js');
84
+ if (existsSync(hookPath)) {
85
+ const mod = await import(hookPath);
86
+ if (typeof mod.buildPrismContext === 'function') {
87
+ const context = await mod.buildPrismContext(cwd);
88
+ if (context) {
89
+ console.log(JSON.stringify({ additionalContext: context }));
90
+ return;
91
+ }
92
+ }
93
+ }
94
+
95
+ console.log(JSON.stringify({}));
96
+ } catch {
97
+ console.log(JSON.stringify({}));
98
+ }
99
+ }
100
+
101
+ main();
102
+ `;
103
+ }
104
+
105
+ /**
106
+ * Generate the model governance hook for Gemini CLI.
107
+ * BeforeModel event — advisory about recommended model per agent.
108
+ */
109
+ function generateModelGovernance() {
110
+ return `${HOOK_HEADER}
111
+ /**
112
+ * Model Governance — BeforeModel
113
+ * Advisory: logs recommended model for current agent.
114
+ */
115
+ async function main() {
116
+ let input = '';
117
+ for await (const chunk of process.stdin) {
118
+ input += chunk;
119
+ }
120
+
121
+ try {
122
+ const event = JSON.parse(input);
123
+ const cwd = event.cwd || process.cwd();
124
+
125
+ const sessionPath = join(cwd, '.chati', 'session.yaml');
126
+ if (!existsSync(sessionPath)) {
127
+ console.log(JSON.stringify({}));
128
+ return;
129
+ }
130
+
131
+ // Read current agent from session
132
+ const raw = readFileSync(sessionPath, 'utf-8');
133
+ const agentMatch = raw.match(/^\\s*current_agent:\\s*(.+)$/m);
134
+ const agent = agentMatch ? agentMatch[1].trim().replace(/^["']|["']$/g, '') : null;
135
+
136
+ if (agent) {
137
+ // Delegate to shared model governance logic
138
+ const hookPath = join(cwd, 'chati.dev', 'hooks', 'model-governance.js');
139
+ if (existsSync(hookPath)) {
140
+ const mod = await import(hookPath);
141
+ if (typeof mod.getRecommendedModel === 'function') {
142
+ const recommendation = mod.getRecommendedModel(cwd, agent);
143
+ if (recommendation) {
144
+ console.log(JSON.stringify({ advisory: \`Recommended model: \${recommendation}\` }));
145
+ return;
146
+ }
147
+ }
148
+ }
149
+ }
150
+
151
+ console.log(JSON.stringify({}));
152
+ } catch {
153
+ console.log(JSON.stringify({}));
154
+ }
155
+ }
156
+
157
+ main();
158
+ `;
159
+ }
160
+
161
+ /**
162
+ * Generate the mode governance hook for Gemini CLI.
163
+ * BeforeTool event — blocks writes outside the scope of current governance mode.
164
+ */
165
+ function generateModeGovernance() {
166
+ return `${HOOK_HEADER}
167
+ /**
168
+ * Mode Governance — BeforeTool
169
+ * Blocks write operations outside current mode scope.
170
+ * Constitution Article XI enforcement.
171
+ */
172
+ const MODE_SCOPES = {
173
+ planning: { allowed: ['chati.dev/', '.chati/', 'chati.dev/artifacts/'] },
174
+ build: { allowed: ['*'] },
175
+ deploy: { allowed: ['*'] },
176
+ };
177
+
178
+ const STATE_TO_MODE = {
179
+ discover: 'planning', plan: 'planning', planning: 'planning',
180
+ build: 'build', validate: 'build',
181
+ deploy: 'deploy', completed: 'deploy',
182
+ };
183
+
184
+ function getCurrentMode(cwd) {
185
+ const sessionPath = join(cwd, '.chati', 'session.yaml');
186
+ if (!existsSync(sessionPath)) return 'planning';
187
+ const raw = readFileSync(sessionPath, 'utf-8');
188
+ const match = raw.match(/^\\s*state:\\s*(.+)$/m);
189
+ const state = match ? match[1].trim().replace(/^["']|["']$/g, '') : 'discover';
190
+ return STATE_TO_MODE[state] || 'planning';
191
+ }
192
+
193
+ function isPathAllowed(filePath, cwd, mode) {
194
+ const scope = MODE_SCOPES[mode];
195
+ if (!scope) return false;
196
+ if (scope.allowed.includes('*')) return true;
197
+
198
+ // Normalize to relative path
199
+ const rel = filePath.startsWith('/') ? filePath.slice(cwd.length + 1) : filePath;
200
+
201
+ // Check path escape
202
+ if (rel.startsWith('..') || rel.startsWith('/')) return false;
203
+
204
+ return scope.allowed.some(prefix => rel.startsWith(prefix));
205
+ }
206
+
207
+ async function main() {
208
+ let input = '';
209
+ for await (const chunk of process.stdin) {
210
+ input += chunk;
211
+ }
212
+
213
+ try {
214
+ const event = JSON.parse(input);
215
+ const cwd = event.cwd || process.cwd();
216
+ const toolName = event.tool_name || '';
217
+ const toolInput = event.tool_input || {};
218
+
219
+ // Only intercept write/edit operations
220
+ if (!['WriteFile', 'EditFile', 'Write', 'Edit'].includes(toolName)) {
221
+ console.log(JSON.stringify({}));
222
+ return;
223
+ }
224
+
225
+ const filePath = toolInput.path || toolInput.file_path || '';
226
+ if (!filePath) {
227
+ console.log(JSON.stringify({}));
228
+ return;
229
+ }
230
+
231
+ const mode = getCurrentMode(cwd);
232
+ if (isPathAllowed(filePath, cwd, mode)) {
233
+ console.log(JSON.stringify({}));
234
+ } else {
235
+ console.log(JSON.stringify({
236
+ error: \`[Article XI] Cannot write "\${filePath}" in \${mode} mode. Allowed: \${MODE_SCOPES[mode].allowed.join(', ')}\`
237
+ }));
238
+ }
239
+ } catch {
240
+ console.log(JSON.stringify({}));
241
+ }
242
+ }
243
+
244
+ main();
245
+ `;
246
+ }
247
+
248
+ /**
249
+ * Generate the constitution guard hook for Gemini CLI.
250
+ * BeforeTool event — blocks destructive commands and secret writes.
251
+ */
252
+ function generateConstitutionGuard() {
253
+ return `${HOOK_HEADER}
254
+ /**
255
+ * Constitution Guard — BeforeTool
256
+ * Blocks destructive commands and secret writes.
257
+ * Constitution Article XI enforcement.
258
+ */
259
+
260
+ const DESTRUCTIVE_PATTERNS = [
261
+ /rm\\s+-rf\\s+\\//,
262
+ /git\\s+reset\\s+--hard/,
263
+ /git\\s+push\\s+--force/,
264
+ /git\\s+push\\s+-f/,
265
+ /git\\s+clean\\s+-fd/,
266
+ /DROP\\s+TABLE/i,
267
+ /DROP\\s+DATABASE/i,
268
+ /TRUNCATE\\s+TABLE/i,
269
+ /chmod\\s+777/,
270
+ ];
271
+
272
+ const SECRET_PATTERNS = [
273
+ /API_KEY\\s*=/,
274
+ /SECRET_KEY\\s*=/,
275
+ /PASSWORD\\s*=/,
276
+ /PRIVATE_KEY\\s*=/,
277
+ /aws_secret_access_key/i,
278
+ /-----BEGIN.*PRIVATE KEY-----/,
279
+ ];
280
+
281
+ const PROTECTED_WRITE_FILES = ['.env', '.env.local', '.env.production', 'credentials.json', 'service-account.json'];
282
+ const SAFE_PATTERNS = ['.env.example', '.env.template', '.env.sample'];
283
+
284
+ async function main() {
285
+ let input = '';
286
+ for await (const chunk of process.stdin) {
287
+ input += chunk;
288
+ }
289
+
290
+ try {
291
+ const event = JSON.parse(input);
292
+ const toolName = event.tool_name || '';
293
+ const toolInput = event.tool_input || {};
294
+
295
+ // Check Bash commands for destructive patterns
296
+ if (['Bash', 'RunCommand'].includes(toolName)) {
297
+ const cmd = toolInput.command || '';
298
+ for (const pattern of DESTRUCTIVE_PATTERNS) {
299
+ if (pattern.test(cmd)) {
300
+ console.log(JSON.stringify({
301
+ error: \`[Constitution Guard] Blocked destructive command: \${cmd.slice(0, 80)}\`
302
+ }));
303
+ return;
304
+ }
305
+ }
306
+ }
307
+
308
+ // Check Write/Edit for secret patterns and protected files
309
+ if (['WriteFile', 'EditFile', 'Write', 'Edit'].includes(toolName)) {
310
+ const filePath = toolInput.path || toolInput.file_path || '';
311
+ const content = toolInput.content || toolInput.new_string || '';
312
+
313
+ // Check if writing to a protected file
314
+ if (PROTECTED_WRITE_FILES.some(p => filePath.endsWith(p)) &&
315
+ !SAFE_PATTERNS.some(s => filePath.endsWith(s))) {
316
+ console.log(JSON.stringify({
317
+ error: \`[Constitution Guard] Cannot write to protected file: \${filePath}\`
318
+ }));
319
+ return;
320
+ }
321
+
322
+ // Check content for secret patterns
323
+ for (const pattern of SECRET_PATTERNS) {
324
+ if (pattern.test(content)) {
325
+ console.log(JSON.stringify({
326
+ error: '[Constitution Guard] Blocked write containing secrets/credentials'
327
+ }));
328
+ return;
329
+ }
330
+ }
331
+ }
332
+
333
+ console.log(JSON.stringify({}));
334
+ } catch {
335
+ console.log(JSON.stringify({}));
336
+ }
337
+ }
338
+
339
+ main();
340
+ `;
341
+ }
342
+
343
+ /**
344
+ * Generate the read protection hook for Gemini CLI.
345
+ * BeforeTool event — blocks reads of sensitive files.
346
+ */
347
+ function generateReadProtection() {
348
+ return `${HOOK_HEADER}
349
+ /**
350
+ * Read Protection — BeforeTool
351
+ * Blocks reading sensitive files (.env, .pem, credentials).
352
+ * Constitution Article XI enforcement.
353
+ */
354
+
355
+ const PROTECTED_FILES = ['.env', '.env.local', '.env.production', '.env.staging'];
356
+ const PROTECTED_EXTENSIONS = ['.pem', '.key', '.p12', '.pfx', '.jks'];
357
+ const PROTECTED_PATHS = ['credentials.json', 'service-account.json', '.git/config', '.aws/credentials', '.npmrc'];
358
+ const SAFE_FILES = ['.env.example', '.env.template', '.env.sample'];
359
+
360
+ async function main() {
361
+ let input = '';
362
+ for await (const chunk of process.stdin) {
363
+ input += chunk;
364
+ }
365
+
366
+ try {
367
+ const event = JSON.parse(input);
368
+ const toolName = event.tool_name || '';
369
+ const toolInput = event.tool_input || {};
370
+
371
+ // Only intercept read operations
372
+ if (!['ReadFile', 'Read'].includes(toolName)) {
373
+ console.log(JSON.stringify({}));
374
+ return;
375
+ }
376
+
377
+ const filePath = toolInput.path || toolInput.file_path || '';
378
+ if (!filePath) {
379
+ console.log(JSON.stringify({}));
380
+ return;
381
+ }
382
+
383
+ // Allow safe patterns first
384
+ if (SAFE_FILES.some(s => filePath.endsWith(s))) {
385
+ console.log(JSON.stringify({}));
386
+ return;
387
+ }
388
+
389
+ // Block protected files
390
+ if (PROTECTED_FILES.some(p => filePath.endsWith(p))) {
391
+ console.log(JSON.stringify({
392
+ error: \`[Read Protection] Cannot read protected file: \${filePath}\`
393
+ }));
394
+ return;
395
+ }
396
+
397
+ // Block protected extensions
398
+ if (PROTECTED_EXTENSIONS.some(ext => filePath.endsWith(ext))) {
399
+ console.log(JSON.stringify({
400
+ error: \`[Read Protection] Cannot read sensitive file type: \${filePath}\`
401
+ }));
402
+ return;
403
+ }
404
+
405
+ // Block protected paths
406
+ if (PROTECTED_PATHS.some(p => filePath.includes(p))) {
407
+ console.log(JSON.stringify({
408
+ error: \`[Read Protection] Cannot read protected path: \${filePath}\`
409
+ }));
410
+ return;
411
+ }
412
+
413
+ console.log(JSON.stringify({}));
414
+ } catch {
415
+ console.log(JSON.stringify({}));
416
+ }
417
+ }
418
+
419
+ main();
420
+ `;
421
+ }
422
+
423
+ /**
424
+ * Generate the session digest hook for Gemini CLI.
425
+ * PreCompress event — saves session state before context compression.
426
+ */
427
+ function generateSessionDigest() {
428
+ return `${HOOK_HEADER}
429
+ /**
430
+ * Session Digest — PreCompress
431
+ * Saves session state before context compression.
432
+ */
433
+ async function main() {
434
+ let input = '';
435
+ for await (const chunk of process.stdin) {
436
+ input += chunk;
437
+ }
438
+
439
+ try {
440
+ const event = JSON.parse(input);
441
+ const cwd = event.cwd || process.cwd();
442
+
443
+ // Delegate to shared session digest logic
444
+ const hookPath = join(cwd, 'chati.dev', 'hooks', 'session-digest.js');
445
+ if (existsSync(hookPath)) {
446
+ const mod = await import(hookPath);
447
+ if (typeof mod.saveDigest === 'function') {
448
+ await mod.saveDigest(cwd);
449
+ }
450
+ }
451
+
452
+ console.log(JSON.stringify({}));
453
+ } catch {
454
+ console.log(JSON.stringify({}));
455
+ }
456
+ }
457
+
458
+ main();
459
+ `;
460
+ }
461
+
462
+ // ---------------------------------------------------------------------------
463
+ // Public API
464
+ // ---------------------------------------------------------------------------
465
+
466
+ /**
467
+ * Generate all 6 Gemini CLI hook files.
468
+ *
469
+ * @returns {Record<string, string>} Map of filename → file content
470
+ */
471
+ export function generateAllGeminiHooks() {
472
+ return {
473
+ 'prism-engine.js': generatePrismEngine(),
474
+ 'model-governance.js': generateModelGovernance(),
475
+ 'mode-governance.js': generateModeGovernance(),
476
+ 'constitution-guard.js': generateConstitutionGuard(),
477
+ 'read-protection.js': generateReadProtection(),
478
+ 'session-digest.js': generateSessionDigest(),
479
+ };
480
+ }
481
+
482
+ /**
483
+ * Generate .gemini/settings.json content with hook configuration.
484
+ *
485
+ * @returns {string} JSON string for .gemini/settings.json
486
+ */
487
+ export function generateGeminiSettings() {
488
+ const hooks = Object.entries(HOOK_MAP).map(([name, config]) => ({
489
+ path: `.gemini/hooks/${name}.js`,
490
+ event: config.event,
491
+ }));
492
+
493
+ return JSON.stringify({ hooks }, null, 2) + '\n';
494
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * IDE Configuration Mapping (6 IDEs)
2
+ * IDE Configuration Mapping (5 IDEs)
3
3
  * Defines where chati.dev agents are deployed per IDE
4
4
  */
5
5
  export const IDE_CONFIGS = {
@@ -47,24 +47,19 @@ export const IDE_CONFIGS = {
47
47
  rulesFile: 'AGENTS.md',
48
48
  mcpConfigFile: null,
49
49
  formatNotes: 'Codex skill format (SKILL.md with YAML frontmatter)',
50
+ rulesPath: '.codex/rules/',
51
+ overrideFile: 'AGENTS.override.md',
50
52
  },
51
53
  'gemini-cli': {
52
54
  name: 'Gemini CLI',
53
55
  description: 'Google AI terminal agent',
54
56
  recommended: false,
55
57
  configPath: '.gemini/commands/',
56
- rulesFile: null,
58
+ rulesFile: 'GEMINI.md',
57
59
  mcpConfigFile: '.gemini/settings.json',
58
60
  formatNotes: 'TOML command format',
59
- },
60
- 'github-copilot': {
61
- name: 'GitHub Copilot',
62
- description: 'GitHub AI pair programmer',
63
- recommended: false,
64
- configPath: '.github/agents/',
65
- rulesFile: '.github/copilot-instructions.md',
66
- mcpConfigFile: null,
67
- formatNotes: 'GitHub Copilot agent format',
61
+ contextPath: '.gemini/context/',
62
+ hooksPath: '.gemini/hooks/',
68
63
  },
69
64
  };
70
65
 
@@ -3,7 +3,7 @@ import { join, dirname } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { IDE_CONFIGS } from '../config/ide-configs.js';
5
5
  import { generateClaudeMCPConfig } from '../config/mcp-configs.js';
6
- import { generateSessionYaml, generateConfigYaml, generateClaudeMd, generateClaudeLocalMd, generateCodexSkill, generateGeminiRouter, generateCopilotAgent } from './templates.js';
6
+ import { generateSessionYaml, generateConfigYaml, generateClaudeMd, generateClaudeLocalMd, generateCodexSkill, generateGeminiRouter, generateGeminiSessionLock, generateAgentsOverrideMd, generateCodexConstitutionGuardRules, generateCodexReadProtectionRules } from './templates.js';
7
7
  import { generateContextFiles } from '../config/context-file-generator.js';
8
8
  import { adaptFrameworkFile, ADAPTABLE_FILES } from '../config/framework-adapter.js';
9
9
  import { verifyManifest } from './manifest.js';
@@ -128,6 +128,9 @@ export async function installFramework(config) {
128
128
  if (hasNonClaude) {
129
129
  generateContextFiles(targetDir, baseContent);
130
130
  }
131
+
132
+ // 7. Update .gitignore with runtime session lock files
133
+ updateGitignore(targetDir, selectedIDEs);
131
134
  }
132
135
 
133
136
  /**
@@ -314,16 +317,42 @@ Pass through all context: session state, handoffs, artifacts, and user input.
314
317
  // Codex CLI: chati skill via .agents/skills/chati/SKILL.md (invoke with $chati)
315
318
  createDir(join(targetDir, '.agents', 'skills', 'chati'));
316
319
  writeFileSync(join(targetDir, '.agents', 'skills', 'chati', 'SKILL.md'), generateCodexSkill(), 'utf-8');
320
+
321
+ // Session lock override file (equivalent to CLAUDE.local.md)
322
+ writeFileSync(join(targetDir, 'AGENTS.override.md'), generateAgentsOverrideMd(), 'utf-8');
323
+
324
+ // Starlark execution policies (equivalent to Claude hooks for constitution guard + read protection)
325
+ createDir(join(targetDir, '.codex', 'rules'));
326
+ writeFileSync(join(targetDir, '.codex', 'rules', 'constitution-guard.rules'), generateCodexConstitutionGuardRules(), 'utf-8');
327
+ writeFileSync(join(targetDir, '.codex', 'rules', 'read-protection.rules'), generateCodexReadProtectionRules(), 'utf-8');
317
328
  } else if (ideKey === 'gemini-cli') {
318
329
  // Gemini CLI: TOML command file (native format for /chati command)
319
330
  writeFileSync(join(targetDir, '.gemini', 'commands', 'chati.toml'), generateGeminiRouter(), 'utf-8');
320
- } else if (ideKey === 'github-copilot') {
321
- // GitHub Copilot: agent file (.github/agents/chati.md) for @chati invocation
322
- writeFileSync(join(targetDir, '.github', 'agents', 'chati.md'), generateCopilotAgent(), 'utf-8');
323
331
 
324
- // Copilot instructions file (auto-loaded by Copilot CLI)
325
- createDir(dirname(join(targetDir, config.rulesFile)));
326
- writeFileSync(join(targetDir, config.rulesFile), generateProviderInstructions(config.name), 'utf-8');
332
+ // Context files via @import (equivalent to .claude/rules/chati/)
333
+ const geminiContextDir = join(targetDir, '.gemini', 'context');
334
+ createDir(geminiContextDir);
335
+ const contextFileNames = ['root.md', 'governance.md', 'protocols.md', 'quality.md'];
336
+ for (const file of contextFileNames) {
337
+ const src = join(FRAMEWORK_SOURCE, 'context', file);
338
+ if (existsSync(src)) {
339
+ const content = readFileSync(src, 'utf-8');
340
+ writeFileSync(join(geminiContextDir, file), adaptFrameworkFile(content, `context/${file}`, 'gemini'), 'utf-8');
341
+ }
342
+ }
343
+
344
+ // Session lock (equivalent to CLAUDE.local.md, imported via @import in GEMINI.md)
345
+ writeFileSync(join(targetDir, '.gemini', 'session-lock.md'), generateGeminiSessionLock(), 'utf-8');
346
+
347
+ // Hooks (6 governance hooks — equivalent to Claude Code hooks)
348
+ const geminiHooksDir = join(targetDir, '.gemini', 'hooks');
349
+ createDir(geminiHooksDir);
350
+ const { generateAllGeminiHooks, generateGeminiSettings } = await import('../config/gemini-hooks-generator.js');
351
+ const hooks = generateAllGeminiHooks();
352
+ for (const [filename, hookContent] of Object.entries(hooks)) {
353
+ writeFileSync(join(geminiHooksDir, filename), hookContent, 'utf-8');
354
+ }
355
+ writeFileSync(join(targetDir, '.gemini', 'settings.json'), generateGeminiSettings(), 'utf-8');
327
356
  } else {
328
357
  // VS Code, Cursor, AntiGravity — generic rules file
329
358
  if (config.rulesFile) {
@@ -335,7 +364,7 @@ Pass through all context: session state, handoffs, artifacts, and user input.
335
364
 
336
365
  /**
337
366
  * Generate provider-agnostic instructions file content.
338
- * Used for non-Claude IDEs (.github/copilot-instructions.md, .vscode/chati/rules.md, etc.)
367
+ * Used for non-Claude IDEs (.vscode/chati/rules.md, .cursorrules, etc.)
339
368
  */
340
369
  function generateProviderInstructions(providerName) {
341
370
  return `# Chati.dev System Rules
@@ -371,6 +400,42 @@ DEPLOY: DevOps
371
400
  `;
372
401
  }
373
402
 
403
+ /**
404
+ * Append chati.dev runtime entries to .gitignore.
405
+ * Session lock files are runtime-only and should never be committed.
406
+ */
407
+ function updateGitignore(targetDir, selectedIDEs) {
408
+ const entries = [
409
+ '',
410
+ '# Chati.dev runtime files (session lock — not committed)',
411
+ '.chati/memories/*/session/',
412
+ ];
413
+
414
+ if (selectedIDEs.includes('claude-code')) {
415
+ entries.push('CLAUDE.local.md');
416
+ }
417
+ if (selectedIDEs.includes('gemini-cli')) {
418
+ entries.push('.gemini/session-lock.md');
419
+ }
420
+ if (selectedIDEs.includes('codex-cli')) {
421
+ entries.push('AGENTS.override.md');
422
+ }
423
+
424
+ entries.push('');
425
+
426
+ const gitignorePath = join(targetDir, '.gitignore');
427
+ const marker = '# Chati.dev runtime files';
428
+
429
+ if (existsSync(gitignorePath)) {
430
+ const existing = readFileSync(gitignorePath, 'utf-8');
431
+ // Don't duplicate if already present
432
+ if (existing.includes(marker)) return;
433
+ writeFileSync(gitignorePath, existing.trimEnd() + '\n' + entries.join('\n'), 'utf-8');
434
+ } else {
435
+ writeFileSync(gitignorePath, entries.join('\n'), 'utf-8');
436
+ }
437
+ }
438
+
374
439
  /**
375
440
  * Recursively create directory if it doesn't exist
376
441
  */