delimit-cli 4.7.3 โ†’ 4.7.4

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.
@@ -263,62 +263,95 @@ def _render_pr_comment(ctx: Dict) -> str:
263
263
  total = ctx["counts"]["total"]
264
264
  additive_count = ctx["counts"]["additive"]
265
265
 
266
- # Header with semver badge
267
- badge = {"major": "๐Ÿ”ด MAJOR", "minor": "๐ŸŸก MINOR", "patch": "๐ŸŸข PATCH", "none": "โšช NONE"}
268
- badge_text = badge.get(bump, bump.upper())
266
+ if bc == 0:
267
+ # โ”€โ”€ GREEN PATH โ”€โ”€
268
+ semver_label = bump.upper() if bump != "none" else "NONE"
269
+ lines.append("## \U0001f6e1\ufe0f Governance Passed")
270
+ lines.append("")
271
+ if total > 0:
272
+ lines.append(
273
+ f"> **No breaking API changes detected.** "
274
+ f"{additive_count} additive change{'s' if additive_count != 1 else ''} "
275
+ f"found \u2014 Semver: **{semver_label}**"
276
+ )
277
+ else:
278
+ lines.append("> **No breaking API changes detected.**")
279
+ lines.append("")
269
280
 
270
- if bc > 0:
271
- lines.append(f"## {badge_text} โ€” Breaking Changes Detected")
281
+ # Additive changes (collapsed)
282
+ additive = ctx["additive_changes"]
283
+ if additive:
284
+ lines.append("<details>")
285
+ lines.append(f"<summary>\u2705 New additions ({len(additive)})</summary>")
286
+ lines.append("")
287
+ for c in additive:
288
+ lines.append(f"- `{c['path']}` \u2014 {c['message']}")
289
+ lines.append("")
290
+ lines.append("</details>")
291
+ lines.append("")
272
292
  else:
273
- lines.append(f"## {badge_text} โ€” API Changes Look Good")
274
- lines.append("")
293
+ # โ”€โ”€ RED PATH โ”€โ”€
294
+ lines.append("## \U0001f6e1\ufe0f Breaking API Changes Detected")
295
+ lines.append("")
275
296
 
276
- # Summary line
277
- parts = [f"**{total}** change{'s' if total != 1 else ''}"]
278
- if bc > 0:
279
- parts.append(f"**{bc}** breaking")
280
- if additive_count > 0:
281
- parts.append(f"**{additive_count}** additive")
282
- lines.append(" ยท ".join(parts))
283
- lines.append("")
297
+ # Summary card
298
+ parts = [f"\U0001f534 **{bc} breaking change{'s' if bc != 1 else ''}**"]
299
+ parts.append("Semver: **MAJOR**")
300
+ separator = " \u00b7 "
301
+ lines.append(f"> {separator.join(parts)}")
302
+ lines.append("")
284
303
 
285
- # Breaking changes table
286
- if bc > 0:
304
+ # Stats table
305
+ lines.append("| | Count |")
306
+ lines.append("|---|---|")
307
+ lines.append(f"| Total changes | {total} |")
308
+ lines.append(f"| Breaking | {bc} |")
309
+ lines.append(f"| Additive | {additive_count} |")
310
+ lines.append("")
311
+
312
+ # Breaking changes table
287
313
  lines.append("### Breaking Changes")
288
314
  lines.append("")
289
- lines.append("| Change | Location | Severity |")
290
- lines.append("|--------|----------|----------|")
315
+ lines.append("| Severity | Change | Location |")
316
+ lines.append("|----------|--------|----------|")
291
317
  for c in ctx["breaking_changes"]:
292
318
  change_type = c.get("type", "breaking")
293
319
  severity = _pr_severity(change_type)
294
- lines.append(f"| {c['message']} | `{c['path']}` | {severity} |")
320
+ lines.append(f"| {severity} | {c['message']} | `{c['path']}` |")
295
321
  lines.append("")
296
322
 
297
323
  # Migration guidance
298
324
  lines.append("<details>")
299
- lines.append("<summary>๐Ÿ“‹ Migration guide</summary>")
325
+ lines.append("<summary>\U0001f4cb Migration guide</summary>")
300
326
  lines.append("")
301
327
  for i, c in enumerate(ctx["breaking_changes"], 1):
302
- lines.append(f"**{i}. {c['path']}**")
303
- lines.append(f"- {_pr_migration_hint(c)}")
328
+ lines.append(f"**{i}. `{c['path']}`**")
329
+ lines.append(f"{_pr_migration_hint(c)}")
304
330
  lines.append("")
305
331
  lines.append("</details>")
306
332
  lines.append("")
307
333
 
308
- # Additive changes
309
- additive = ctx["additive_changes"]
310
- if additive:
311
- lines.append("<details>")
312
- lines.append(f"<summary>โœ… New additions ({len(additive)})</summary>")
313
- lines.append("")
314
- for c in additive:
315
- lines.append(f"- `{c['path']}` โ€” {c['message']}")
316
- lines.append("")
317
- lines.append("</details>")
334
+ # Additive changes
335
+ additive = ctx["additive_changes"]
336
+ if additive:
337
+ lines.append("<details>")
338
+ lines.append(f"<summary>\u2705 New additions ({len(additive)})</summary>")
339
+ lines.append("")
340
+ for c in additive:
341
+ lines.append(f"- `{c['path']}` \u2014 {c['message']}")
342
+ lines.append("")
343
+ lines.append("</details>")
344
+ lines.append("")
345
+
346
+ lines.append("> **Fix locally:** `npx delimit-cli lint`")
318
347
  lines.append("")
319
348
 
320
349
  lines.append("---")
321
- lines.append("*[Delimit](https://github.com/delimit-ai/delimit) ยท API governance for CI/CD*")
350
+ lines.append(
351
+ "Powered by [Delimit](https://delimit.ai) \u00b7 "
352
+ "[Docs](https://delimit.ai/docs) \u00b7 "
353
+ "[Install](https://github.com/marketplace/actions/delimit-api-governance)"
354
+ )
322
355
  return "\n".join(lines)
323
356
 
324
357
 
@@ -17,6 +17,7 @@ const KNOWN_MODEL_PROVIDERS = [
17
17
  { pattern: /claude|anthropic/i, vendor: 'anthropic', family: 'claude' },
18
18
  { pattern: /openai|gpt-\d|o1|o3/i, vendor: 'openai', family: 'gpt' },
19
19
  { pattern: /gemini|vertex/i, vendor: 'google', family: 'gemini' },
20
+ { pattern: /antigravity|agy/i, vendor: 'google', family: 'antigravity' },
20
21
  { pattern: /codex/i, vendor: 'openai', family: 'codex' },
21
22
  { pattern: /grok|xai/i, vendor: 'xai', family: 'grok' },
22
23
  { pattern: /llama|mistral/i, vendor: 'meta-or-mistral', family: 'open-weight' },
package/lib/auth-setup.js CHANGED
@@ -55,6 +55,7 @@ class DelimitAuthSetup {
55
55
  'claude': 'Anthropic Claude',
56
56
  'openai': 'OpenAI GPT',
57
57
  'gemini': 'Google Gemini',
58
+ 'antigravity': 'Google Antigravity',
58
59
  'codex': 'GitHub Copilot'
59
60
  };
60
61
 
@@ -223,6 +224,11 @@ class DelimitAuthSetup {
223
224
  config.projectId = await this.prompt(` GCP Project ID (optional): `, '');
224
225
  break;
225
226
 
227
+ case 'antigravity':
228
+ config.apiKey = await this.promptSecret(` Google AI API key (optional): `);
229
+ config.projectId = await this.prompt(` GCP Project ID (optional): `, '');
230
+ break;
231
+
226
232
  case 'codex':
227
233
  config.token = await this.promptSecret(` GitHub Copilot token: `);
228
234
  break;
@@ -685,6 +691,9 @@ class DelimitAuthSetup {
685
691
  if (credentials.gemini?.apiKey) {
686
692
  envContent += `export GOOGLE_AI_API_KEY="${credentials.gemini.apiKey}"\n`;
687
693
  }
694
+ if (credentials.antigravity?.apiKey) {
695
+ envContent += `export ANTIGRAVITY_API_KEY="${credentials.antigravity.apiKey}"\n`;
696
+ }
688
697
 
689
698
  // Organization
690
699
  if (credentials.organization?.policyUrl) {
@@ -860,7 +869,7 @@ class DelimitAuthSetup {
860
869
  if (credentials.github.ghToken) console.log(chalk.gray(' - GitHub CLI configured'));
861
870
  }
862
871
 
863
- for (const tool of ['claude', 'openai', 'gemini', 'codex']) {
872
+ for (const tool of ['claude', 'openai', 'gemini', 'antigravity', 'codex']) {
864
873
  if (credentials[tool]) {
865
874
  console.log(chalk.white(` โ€ข ${tool}: Configured`));
866
875
  }
@@ -0,0 +1,244 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const chalk = require('chalk');
5
+ const { spawnSync, execSync } = require('child_process');
6
+
7
+ class DelimitChatREPL {
8
+ constructor(options = {}) {
9
+ this.apiFallbackEnabled = options.apiFallback !== undefined ?
10
+ !!options.apiFallback :
11
+ (process.env.DELIMIT_API_FALLBACK === 'true' || false);
12
+ this.failedModels = new Set();
13
+ this.modelsConfig = this.loadModels();
14
+ this.routesConfig = this.loadRoutes();
15
+ this.agentName = 'orchestrator'; // Default agent
16
+ }
17
+
18
+ loadModels() {
19
+ const modelsPath = path.join(os.homedir(), '.delimit', 'models.json');
20
+ if (fs.existsSync(modelsPath)) {
21
+ try {
22
+ return JSON.parse(fs.readFileSync(modelsPath, 'utf-8'));
23
+ } catch (e) {
24
+ console.error(chalk.red('Failed to parse models.json'));
25
+ }
26
+ }
27
+ return { fallbacks: { default: [] } };
28
+ }
29
+
30
+ loadRoutes() {
31
+ const routesPath = path.join(os.homedir(), '.delimit', 'routes.json');
32
+ if (fs.existsSync(routesPath)) {
33
+ try {
34
+ return JSON.parse(fs.readFileSync(routesPath, 'utf-8'));
35
+ } catch (e) {}
36
+ }
37
+ return {};
38
+ }
39
+
40
+ getActiveChain() {
41
+ const chain = this.modelsConfig.fallbacks?.['default'] || [];
42
+ const activeModels = [];
43
+
44
+ for (const provider of chain) {
45
+ if (this.failedModels.has(provider)) continue;
46
+ const p = this.modelsConfig[provider];
47
+ if (!p) continue;
48
+
49
+ if (p.auth_mode === 'chat_login' || p.auth_mode === 'adc') {
50
+ activeModels.push({ id: provider, type: 'subscription' });
51
+ } else if (p.api_key && this.apiFallbackEnabled) {
52
+ activeModels.push({ id: provider, type: 'api' });
53
+ }
54
+ }
55
+ return activeModels;
56
+ }
57
+
58
+ start() {
59
+ console.log(chalk.magenta.bold(`
60
+ ____ ________ ______ _____________
61
+ / __ \\/ ____/ / / _/ |/ / _/_ __/
62
+ / / / / __/ / / / // /|_/ // / / /
63
+ / /_/ / /___/ /____/ // / / // / / /
64
+ /_____/_____/_____/___/_/ /_/___/ /_/
65
+ v4.7.3`));
66
+
67
+ while (true) {
68
+ const chain = this.getActiveChain();
69
+ if (chain.length === 0) {
70
+ console.log(chalk.red('\n Error: No active models available.'));
71
+ if (!this.apiFallbackEnabled) {
72
+ console.log(chalk.yellow(' Auto-Phoenix stalled: All flat-rate subscription models have degraded.'));
73
+ // Ask user to enable API fallback synchronously
74
+ try {
75
+ execSync('read -p " Enable API Fallback to continue using paid tokens? (y/N) " yn && [ "$yn" = "y" ] || [ "$yn" = "Y" ]', {stdio: 'inherit'});
76
+ this.apiFallbackEnabled = true;
77
+ console.log(chalk.green(' API Fallback enabled. Resuming...\n'));
78
+ continue;
79
+ } catch (e) {
80
+ console.log(chalk.gray(' API Fallback declined. Exiting.\n'));
81
+ process.exit(1);
82
+ }
83
+ } else {
84
+ console.log(chalk.red(' Auto-Phoenix stalled: No remaining models in fallback chain.\n'));
85
+ process.exit(1);
86
+ }
87
+ }
88
+
89
+ const activeModel = chain[0];
90
+ const chainStr = chain.map(m => m.type === 'subscription' ? chalk.green(m.id) : chalk.yellow(m.id)).join(' -> ');
91
+ console.log(`\n [Agent: ${chalk.white(this.agentName)}] [API Fallback: ${this.apiFallbackEnabled ? chalk.green('ON') : chalk.gray('OFF')}]`);
92
+ console.log(` Active Routing: ${chainStr}`);
93
+ console.log(chalk.magenta.bold(` [Delimit] `) + chalk.magenta(`โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•`));
94
+ console.log(chalk.magenta.bold(` [Delimit] `) + chalk.magenta(`<`) + chalk.yellow(`/`) + chalk.magenta(`> `) + chalk.bold(`GOVERNANCE ACTIVE: ${activeModel.id.toUpperCase()}`));
95
+ console.log(chalk.magenta.bold(` [Delimit] `) + chalk.magenta(`โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•\n`));
96
+
97
+ const shimPath = path.join(os.homedir(), '.delimit', 'shims', activeModel.id);
98
+ if (!fs.existsSync(shimPath)) {
99
+ console.log(chalk.red(` Error: Shim not found for ${activeModel.id} at ${shimPath}`));
100
+ this.failedModels.add(activeModel.id);
101
+ continue;
102
+ }
103
+
104
+ // Probe model quota/health before entering interactive session
105
+ let isHealthy = true;
106
+ if (activeModel.id === 'claude' || activeModel.id === 'gemini' || activeModel.id === 'gemini_consumer' || activeModel.id === 'antigravity') {
107
+ process.stdout.write(` Probing ${chalk.bold(activeModel.id)} quota... `);
108
+
109
+ // Configure environment for the probe (identical to the run environment)
110
+ const probeEnv = { ...process.env, DELIMIT_QUIET: 'true' };
111
+ const p = this.modelsConfig[activeModel.id];
112
+ if (activeModel.id === 'claude') {
113
+ if (p && p.auth_mode === 'chat_login') {
114
+ delete probeEnv.ANTHROPIC_API_KEY;
115
+ }
116
+ } else if (activeModel.id === 'gemini' || activeModel.id === 'gemini_consumer' || activeModel.id === 'antigravity') {
117
+ if (!p || p.auth_mode === 'chat_login') {
118
+ delete probeEnv.GOOGLE_CLOUD_PROJECT;
119
+ delete probeEnv.GEMINI_USER_GCP_PROJECT;
120
+ delete probeEnv.GEMINI_CLI_USE_COMPUTE_ADC;
121
+ delete probeEnv.GOOGLE_APPLICATION_CREDENTIALS;
122
+ }
123
+ }
124
+
125
+ // Spawn the shim silently with -p "space" (timeout 6000ms)
126
+ const probeResult = spawnSync(shimPath, ['-p', 'space'], { env: probeEnv, timeout: 6000 });
127
+
128
+ const isTimeout = probeResult.error && probeResult.error.code === 'ETIMEDOUT';
129
+ if ((probeResult.error && !isTimeout) || (probeResult.status !== null && probeResult.status !== 0)) {
130
+ console.log(chalk.red('failed (out of quota/limit)'));
131
+ isHealthy = false;
132
+ } else {
133
+ console.log(chalk.green('verified'));
134
+ }
135
+ }
136
+
137
+ if (!isHealthy) {
138
+ this.failedModels.add(activeModel.id);
139
+ continue;
140
+ }
141
+
142
+ const args = [];
143
+ if (activeModel.id.startsWith('gemini')) {
144
+ const p = this.modelsConfig[activeModel.id];
145
+ if (p && p.model) {
146
+ let modelName = p.model;
147
+ if (modelName.endsWith('-latest') && this.routesConfig[activeModel.id]) {
148
+ const basePrefix = modelName.replace('-latest', '');
149
+ const concrete = this.routesConfig[activeModel.id].find(m => m.startsWith(basePrefix) && m !== modelName);
150
+ if (concrete) modelName = concrete;
151
+ }
152
+ args.unshift('-m', modelName);
153
+ }
154
+ }
155
+
156
+ const env = { ...process.env };
157
+ // Suppress the shim banner so it feels like a native session
158
+ env.DELIMIT_QUIET = 'true';
159
+
160
+ const p = this.modelsConfig[activeModel.id];
161
+
162
+ // Fix 403 Google Cloud API Error by preventing accidental enterprise routing
163
+ // If the model is using chat_login (consumer Google One plan), we MUST strip
164
+ // any global GCP env vars that would accidentally trigger Code Assist Enterprise mode.
165
+ if (activeModel.id === 'gemini' || activeModel.id === 'gemini_consumer' || activeModel.id === 'antigravity') {
166
+ if (!p || p.auth_mode === 'chat_login') {
167
+ delete env.GOOGLE_CLOUD_PROJECT;
168
+ delete env.GEMINI_USER_GCP_PROJECT;
169
+ delete env.GEMINI_CLI_USE_COMPUTE_ADC;
170
+ delete env.GOOGLE_APPLICATION_CREDENTIALS;
171
+ } else if (p && p.auth_mode === 'adc') {
172
+ if (p.project) {
173
+ env.GOOGLE_CLOUD_PROJECT = p.project;
174
+ env.GEMINI_USER_GCP_PROJECT = p.project;
175
+ }
176
+ if (p.credentials_path) {
177
+ env.GOOGLE_APPLICATION_CREDENTIALS = p.credentials_path;
178
+ }
179
+ env.GEMINI_CLI_USE_COMPUTE_ADC = 'true';
180
+ }
181
+ }
182
+
183
+ // Force Claude Code to use chat login rather than API keys when auth_mode is chat_login
184
+ if (activeModel.id === 'claude') {
185
+ if (p && p.auth_mode === 'chat_login') {
186
+ delete env.ANTHROPIC_API_KEY;
187
+ }
188
+ }
189
+
190
+ // Execute the model interactively. Ignore SIGINT in parent while child runs
191
+ // to prevent Ctrl+C from killing the parent Node process.
192
+ const sigintHandler = () => {};
193
+ process.on('SIGINT', sigintHandler);
194
+ const result = spawnSync(shimPath, args, { stdio: 'inherit', env });
195
+ process.removeListener('SIGINT', sigintHandler);
196
+
197
+ if (result.status === 0) {
198
+ // Clean exit (user typed /exit)
199
+ console.log(chalk.gray('\n Session saved. Exiting Delimit OS.'));
200
+ process.exit(0);
201
+ } else if (result.signal === 'SIGINT') {
202
+ console.log(chalk.yellow('\n Session interrupted (Ctrl+C).'));
203
+ try {
204
+ execSync('read -p " Migrate to the next fallback model? (Y/n) " yn && [ "$yn" = "n" ] || [ "$yn" = "N" ]', {stdio: 'inherit'});
205
+ console.log(chalk.yellow(` โš  ${activeModel.id} interrupted. Auto-Phoenix initiating seamless migration...`));
206
+ this.failedModels.add(activeModel.id);
207
+
208
+ // Capture soul to preserve context before switching
209
+ try {
210
+ const pyCmd = `import sys; sys.path.insert(0, '/home/delimit/delimit-gateway'); from ai.session_phoenix import capture_soul; capture_soul(active_task='Auto-Phoenix migration from ${activeModel.id}')`;
211
+ execSync(`python3 -c "${pyCmd}"`, { stdio: 'ignore' });
212
+ } catch (e) {}
213
+
214
+ if (chain.length > 1) {
215
+ const nextModel = chain[1];
216
+ console.log(chalk.green(` โœ“ Soul captured. Rehydrating into ${chalk.bold(nextModel.id)}...`));
217
+ }
218
+ } catch (e) {
219
+ console.log(chalk.gray(' Exiting Delimit OS.\n'));
220
+ process.exit(0);
221
+ }
222
+ } else {
223
+ // The CLI crashed (e.g. 429 Quota Error or exit code != 0)
224
+ console.log(chalk.red(`\n Execution failed: Model CLI exited with status ${result.status}`));
225
+ console.log(chalk.yellow(` โš  ${activeModel.id} degraded. Auto-Phoenix initiating seamless migration...`));
226
+ this.failedModels.add(activeModel.id);
227
+
228
+ // Capture soul to preserve context before switching
229
+ try {
230
+ const pyCmd = `import sys; sys.path.insert(0, '/home/delimit/delimit-gateway'); from ai.session_phoenix import capture_soul; capture_soul(active_task='Auto-Phoenix migration from ${activeModel.id}')`;
231
+ execSync(`python3 -c "${pyCmd}"`, { stdio: 'ignore' });
232
+ } catch (e) {}
233
+
234
+ if (chain.length > 1) {
235
+ const nextModel = chain[1];
236
+ console.log(chalk.green(` โœ“ Soul captured. Rehydrating into ${chalk.bold(nextModel.id)}...`));
237
+ }
238
+ // Loop continues to spawn the next model
239
+ }
240
+ }
241
+ }
242
+ }
243
+
244
+ module.exports = { DelimitChatREPL };
@@ -227,6 +227,29 @@ function detectAITools() {
227
227
  });
228
228
  }
229
229
 
230
+ // Antigravity CLI
231
+ const antigravityDir = path.join(getHome(), '.gemini', 'antigravity-cli');
232
+ let hasAntigravity = fs.existsSync(antigravityDir);
233
+ if (!hasAntigravity) {
234
+ try {
235
+ execSync('agy --version 2>/dev/null', { stdio: 'pipe', timeout: 3000 });
236
+ hasAntigravity = true;
237
+ } catch {
238
+ try {
239
+ execSync('antigravity --version 2>/dev/null', { stdio: 'pipe', timeout: 3000 });
240
+ hasAntigravity = true;
241
+ } catch { /* not installed */ }
242
+ }
243
+ }
244
+ if (hasAntigravity) {
245
+ detected.push({
246
+ id: 'antigravity',
247
+ name: 'Antigravity CLI',
248
+ configPath: path.join(antigravityDir, 'settings.json'),
249
+ format: 'antigravity-mcp',
250
+ });
251
+ }
252
+
230
253
  return detected;
231
254
  }
232
255
 
@@ -672,6 +695,58 @@ ${getDelimitSection()}
672
695
  return changes;
673
696
  }
674
697
 
698
+ /**
699
+ * Install hooks for Antigravity CLI.
700
+ */
701
+ function installAntigravityHooks(tool, hookConfig) {
702
+ const changes = [];
703
+ const antigravityDir = path.dirname(tool.configPath);
704
+ fs.mkdirSync(antigravityDir, { recursive: true });
705
+
706
+ // Update settings.json with custom instructions
707
+ let config = {};
708
+ if (fs.existsSync(tool.configPath)) {
709
+ try {
710
+ config = JSON.parse(fs.readFileSync(tool.configPath, 'utf-8'));
711
+ } catch { config = {}; }
712
+ }
713
+
714
+ const govInstructions = getDelimitSectionCondensed();
715
+ const DELIMIT_MARKER = '<!-- delimit:start';
716
+
717
+ if (!config.customInstructions || !config.customInstructions.includes(DELIMIT_MARKER)) {
718
+ config.customInstructions = govInstructions;
719
+ changes.push('customInstructions');
720
+ }
721
+
722
+ fs.writeFileSync(tool.configPath, JSON.stringify(config, null, 2));
723
+
724
+ // ANTIGRAVITY.md: use the same upsert pattern as CLAUDE.md
725
+ const antigravityMd = path.join(getHome(), 'ANTIGRAVITY.md');
726
+ const managedSection = getDelimitSection();
727
+ if (!fs.existsSync(antigravityMd)) {
728
+ fs.writeFileSync(antigravityMd, managedSection + '\n');
729
+ changes.push('ANTIGRAVITY.md');
730
+ } else {
731
+ const existing = fs.readFileSync(antigravityMd, 'utf-8');
732
+ if (existing.includes(DELIMIT_MARKER) && existing.includes('<!-- delimit:end -->')) {
733
+ const before = existing.substring(0, existing.indexOf(DELIMIT_MARKER));
734
+ const after = existing.substring(existing.indexOf('<!-- delimit:end -->') + '<!-- delimit:end -->'.length);
735
+ const updated = before + managedSection + after;
736
+ if (updated !== existing) {
737
+ fs.writeFileSync(antigravityMd, updated);
738
+ changes.push('ANTIGRAVITY.md');
739
+ }
740
+ } else {
741
+ const sep = existing.endsWith('\n') ? '\n' : '\n\n';
742
+ fs.writeFileSync(antigravityMd, existing + sep + managedSection + '\n');
743
+ changes.push('ANTIGRAVITY.md');
744
+ }
745
+ }
746
+
747
+ return changes;
748
+ }
749
+
675
750
  /**
676
751
  * Install hooks for Gemini CLI.
677
752
  * Gemini CLI uses MCP (already handled by setup) but we add governance
@@ -744,6 +819,8 @@ function installHooksForTool(tool, hookConfig) {
744
819
  return { tool, changes: installCodexHooks(tool, hookConfig) };
745
820
  case 'gemini':
746
821
  return { tool, changes: installGeminiHooks(tool, hookConfig) };
822
+ case 'antigravity':
823
+ return { tool, changes: installAntigravityHooks(tool, hookConfig) };
747
824
  default:
748
825
  return { tool, changes: [] };
749
826
  }
@@ -880,6 +957,35 @@ function removeGeminiHooks() {
880
957
  return changed;
881
958
  }
882
959
 
960
+ function removeAntigravityHooks() {
961
+ let changed = false;
962
+
963
+ // Remove custom instructions referencing delimit
964
+ const configPath = path.join(getHome(), '.gemini', 'antigravity-cli', 'settings.json');
965
+ if (fs.existsSync(configPath)) {
966
+ try {
967
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
968
+ if (config.customInstructions && config.customInstructions.includes('delimit-cli hook')) {
969
+ delete config.customInstructions;
970
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
971
+ changed = true;
972
+ }
973
+ } catch { /* ignore */ }
974
+ }
975
+
976
+ // Remove ANTIGRAVITY.md if it's ours
977
+ const antigravityMd = path.join(getHome(), 'ANTIGRAVITY.md');
978
+ if (fs.existsSync(antigravityMd)) {
979
+ const content = fs.readFileSync(antigravityMd, 'utf-8');
980
+ if (content.includes('Delimit Governance')) {
981
+ fs.unlinkSync(antigravityMd);
982
+ changed = true;
983
+ }
984
+ }
985
+
986
+ return changed;
987
+ }
988
+
883
989
  function removeAllHooks() {
884
990
  const results = [];
885
991
 
@@ -892,6 +998,9 @@ function removeAllHooks() {
892
998
  if (removeGeminiHooks()) {
893
999
  results.push('Gemini CLI');
894
1000
  }
1001
+ if (removeAntigravityHooks()) {
1002
+ results.push('Antigravity CLI');
1003
+ }
895
1004
 
896
1005
  return results;
897
1006
  }
@@ -1511,10 +1620,12 @@ module.exports = {
1511
1620
  installClaudeHooks,
1512
1621
  installCodexHooks,
1513
1622
  installGeminiHooks,
1623
+ installAntigravityHooks,
1514
1624
  removeAllHooks,
1515
1625
  removeClaudeHooks,
1516
1626
  removeCodexHooks,
1517
1627
  removeGeminiHooks,
1628
+ removeAntigravityHooks,
1518
1629
  loadHookConfig,
1519
1630
  hookSessionStart,
1520
1631
  hookBootstrap,
@@ -0,0 +1,60 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+
5
+ function generateTimeline(venturePath) {
6
+ const homedir = require('os').homedir();
7
+ const delimitHome = path.join(homedir, '.delimit');
8
+ const ledgerDir = path.join(delimitHome, 'ledger');
9
+
10
+ const events = [];
11
+
12
+ // Read all ledger files
13
+ const files = ['operations.jsonl', 'strategy.jsonl'];
14
+ for (const file of files) {
15
+ const filePath = path.join(ledgerDir, file);
16
+ if (fs.existsSync(filePath)) {
17
+ const lines = fs.readFileSync(filePath, 'utf-8').trim().split('\n');
18
+ for (const line of lines) {
19
+ if (!line.trim()) continue;
20
+ try {
21
+ const event = JSON.parse(line);
22
+ events.push(event);
23
+ } catch (e) {}
24
+ }
25
+ }
26
+ }
27
+
28
+ // Sort by timestamp (fallback to 0)
29
+ events.sort((a, b) => new Date(a.created_at || a.ts || 0) - new Date(b.created_at || b.ts || 0));
30
+
31
+ if (events.length === 0) {
32
+ return "No history found in ledger.";
33
+ }
34
+
35
+ let output = chalk.bold.blue("\n Delimit Venture Timeline โ€” civilization-style retrospective\n\n");
36
+
37
+ let lastDate = "";
38
+ for (const e of events) {
39
+ const ts = e.created_at || e.ts || new Date(0).toISOString();
40
+ const date = ts.split('T')[0];
41
+ if (date !== lastDate) {
42
+ output += chalk.bold.white(`\n --- ${date} ---\n`);
43
+ lastDate = date;
44
+ }
45
+
46
+ const time = ts.split('T')[1]?.slice(0, 5) || "??:??";
47
+ const priority = e.priority === 'P0' ? chalk.red('P0') : e.priority === 'P1' ? chalk.yellow('P1') : chalk.gray(e.priority || 'n/a');
48
+ const type = e.type === 'strategy' ? chalk.magenta('STR') : chalk.cyan('OPS');
49
+
50
+ output += ` ${chalk.gray(time)} [${type}] [${priority}] ${chalk.white(e.title || e.message || 'Untitled Event')}\n`;
51
+ if (e.status === 'done' || e.status === 'completed') {
52
+ output += ` ${chalk.green('โœ“ COMPLETED')}\n`;
53
+ }
54
+ }
55
+
56
+ output += chalk.dim(`\n Total events: ${events.length}\n`);
57
+ return output;
58
+ }
59
+
60
+ module.exports = { generateTimeline };