alegra-data-agent 0.7.0 → 0.7.2

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.
@@ -4,7 +4,7 @@ import { logWarn } from "./colors.js";
4
4
  import { hookCommandFor, runHookInstall, runHookUninstall, } from "./commands/hook.js";
5
5
  import { HERMES_PLUGIN_NAME, hermesPluginManifest, hermesPluginModule } from "./hermes-plugin.js";
6
6
  import { runHermesCliAt } from "./installer.js";
7
- import { COMMAND_NAME, getClaudeSettingsPath, getCodexHooksPath, getCursorHooksPath, getGeminiSettingsPath, getHermesHome, getHermesPluginsDir, getOpenCodePluginPath, readLockFile, } from "./lib.js";
7
+ import { COMMAND_NAME, getAntigravityHooksPath, getClaudeSettingsPath, getCodexHooksPath, getCursorHooksPath, getGeminiSettingsPath, getHermesHome, getHermesPluginsDir, getOpenCodePluginPath, readLockFile, } from "./lib.js";
8
8
  function readJsonSafe(path) {
9
9
  if (!existsSync(path))
10
10
  return {};
@@ -60,6 +60,7 @@ export function guiResilientCommand(base, opts) {
60
60
  `else mkdir -p "$(dirname ${logFile})" && ` +
61
61
  `echo "$(date '+%Y-%m-%d %H:%M:%S') [${opts.client}] pnpm no esta en el PATH del cliente; auto-update omitido" >> ${logFile}; fi`);
62
62
  }
63
+ const CURSOR_HOOK_EVENTS = ["workspaceOpen", "sessionStart"];
63
64
  function installCursor(command) {
64
65
  const path = getCursorHooksPath();
65
66
  const cfg = readJsonSafe(path);
@@ -69,9 +70,11 @@ function installCursor(command) {
69
70
  }
70
71
  if (typeof cfg.version !== "number")
71
72
  cfg.version = 1;
72
- const list = freshHookList(cfg, "workspaceOpen");
73
- list.push({ command });
74
- cfg.hooks.workspaceOpen = list;
73
+ for (const event of CURSOR_HOOK_EVENTS) {
74
+ const list = freshHookList(cfg, event);
75
+ list.push({ command, timeout: 120 });
76
+ cfg.hooks[event] = list;
77
+ }
75
78
  writeJson(path, cfg);
76
79
  }
77
80
  function installCodex(command) {
@@ -81,34 +84,74 @@ function installCodex(command) {
81
84
  logWarn(`${path} no es JSON válido; salteo el auto-update de Codex.`);
82
85
  return;
83
86
  }
87
+ const before = JSON.stringify(cfg);
84
88
  const list = freshHookList(cfg, "SessionStart");
85
89
  list.push({ matcher: "startup|resume", hooks: [{ type: "command", command }] });
86
90
  cfg.hooks.SessionStart = list;
91
+ if (JSON.stringify(cfg) === before)
92
+ return;
87
93
  writeJson(path, cfg);
88
94
  }
95
+ const ANTIGRAVITY_HOOK_NAME = "alegra-data-agent-autoupdate";
96
+ export function antigravityHookCommand(resilient, platform = process.platform) {
97
+ if (platform === "win32")
98
+ return resilient;
99
+ return `(${resilient}) </dev/null >/dev/null 2>&1 & echo '{}'`;
100
+ }
89
101
  function installAntigravity(command) {
90
- const path = getGeminiSettingsPath();
102
+ const path = getAntigravityHooksPath();
91
103
  const cfg = readJsonSafe(path);
92
104
  if (cfg === null) {
93
105
  logWarn(`${path} no es JSON válido; salteo el auto-update de Antigravity.`);
94
106
  return;
95
107
  }
96
- const list = freshHookList(cfg, "SessionStart");
97
- list.push({ hooks: [{ type: "command", command }] });
98
- cfg.hooks.SessionStart = list;
108
+ cfg[ANTIGRAVITY_HOOK_NAME] = {
109
+ SessionStart: [{ type: "command", command, timeout: 120 }],
110
+ };
99
111
  writeJson(path, cfg);
112
+ removeJsonHook(getGeminiSettingsPath(), "SessionStart");
113
+ }
114
+ function removeAntigravityHook() {
115
+ const path = getAntigravityHooksPath();
116
+ const cfg = readJsonSafe(path);
117
+ if (cfg && ANTIGRAVITY_HOOK_NAME in cfg) {
118
+ delete cfg[ANTIGRAVITY_HOOK_NAME];
119
+ writeJson(path, cfg);
120
+ }
121
+ removeJsonHook(getGeminiSettingsPath(), "SessionStart");
122
+ }
123
+ function antigravityHookIsOurs() {
124
+ const cfg = readJsonSafe(getAntigravityHooksPath());
125
+ return Boolean(cfg && ANTIGRAVITY_HOOK_NAME in cfg);
100
126
  }
101
127
  function installOpenCode(command) {
102
128
  const path = getOpenCodePluginPath();
103
129
  mkdirSync(dirname(path), { recursive: true });
104
130
  const plugin = `// alegra-data-agent: auto-update al iniciar OpenCode. Generado por el instalador.
105
131
  // Borrar este archivo desactiva el auto-update. No editar a mano.
106
- export const AlegraDataAgentAutoUpdate = async ({ $ }) => {
107
- if (!$) return {}
132
+ import { spawn } from "node:child_process"
133
+
134
+ const COMMAND = ${JSON.stringify(command)}
135
+
136
+ function runUpdate() {
108
137
  try {
109
- $\`${command}\`.quiet().nothrow().catch(() => {})
138
+ const child = spawn(COMMAND, { shell: true, detached: true, stdio: "ignore", windowsHide: true })
139
+ child.on("error", () => {})
140
+ child.unref()
110
141
  } catch (_e) {}
111
- return {}
142
+ }
143
+
144
+ export default {
145
+ id: "alegra-data-agent-autoupdate",
146
+ // OpenCode 1.x
147
+ server: async () => {
148
+ runUpdate()
149
+ return {}
150
+ },
151
+ // OpenCode 2.x
152
+ setup: async () => {
153
+ runUpdate()
154
+ },
112
155
  }
113
156
  `;
114
157
  writeFileSync(path, plugin, "utf-8");
@@ -187,7 +230,10 @@ export function installAutoUpdate(target, mode = "auto") {
187
230
  installCodex(hookCommandFor(effective));
188
231
  break;
189
232
  case "antigravity":
190
- installAntigravity(guiResilientCommand(hookCommandFor(effective), { client: "antigravity", silence: true }));
233
+ installAntigravity(antigravityHookCommand(guiResilientCommand(hookCommandFor(effective), {
234
+ client: "antigravity",
235
+ silence: true,
236
+ })));
191
237
  break;
192
238
  case "opencode":
193
239
  installOpenCode(hookCommandFor(effective));
@@ -208,13 +254,14 @@ export function removeAutoUpdate(target) {
208
254
  runHookUninstall({ quiet: true });
209
255
  break;
210
256
  case "cursor":
211
- removeJsonHook(getCursorHooksPath(), "workspaceOpen");
257
+ for (const event of CURSOR_HOOK_EVENTS)
258
+ removeJsonHook(getCursorHooksPath(), event);
212
259
  break;
213
260
  case "codex":
214
261
  removeJsonHook(getCodexHooksPath(), "SessionStart");
215
262
  break;
216
263
  case "antigravity":
217
- removeJsonHook(getGeminiSettingsPath(), "SessionStart");
264
+ removeAntigravityHook();
218
265
  break;
219
266
  case "opencode": {
220
267
  const p = getOpenCodePluginPath();
@@ -243,11 +290,11 @@ export function hasAutoUpdate(target) {
243
290
  case "claude-code":
244
291
  return jsonHookHasOurs(getClaudeSettingsPath(), "SessionStart");
245
292
  case "cursor":
246
- return jsonHookHasOurs(getCursorHooksPath(), "workspaceOpen");
293
+ return CURSOR_HOOK_EVENTS.some((event) => jsonHookHasOurs(getCursorHooksPath(), event));
247
294
  case "codex":
248
295
  return jsonHookHasOurs(getCodexHooksPath(), "SessionStart");
249
296
  case "antigravity":
250
- return jsonHookHasOurs(getGeminiSettingsPath(), "SessionStart");
297
+ return antigravityHookIsOurs() || jsonHookHasOurs(getGeminiSettingsPath(), "SessionStart");
251
298
  case "opencode":
252
299
  return existsSync(getOpenCodePluginPath());
253
300
  case "hermes": {
@@ -0,0 +1,128 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { createInterface } from "node:readline";
6
+ import { COMMAND_NAME, getConfigDir } from "./lib.js";
7
+ function consentPath() {
8
+ return join(getConfigDir(), "codex-hook-trust.json");
9
+ }
10
+ export function hasCodexTrustConsent() {
11
+ return existsSync(consentPath());
12
+ }
13
+ export function saveCodexTrustConsent() {
14
+ const path = consentPath();
15
+ mkdirSync(dirname(path), { recursive: true });
16
+ writeFileSync(path, `${JSON.stringify({ consented: true, at: new Date().toISOString() })}\n`);
17
+ }
18
+ function codexBin() {
19
+ const bin = process.env.CODEX_BIN;
20
+ return bin && bin.length > 0 ? bin : "codex";
21
+ }
22
+ async function withAppServer(fn, timeoutMs) {
23
+ const child = spawn(codexBin(), ["app-server"], {
24
+ stdio: ["pipe", "pipe", "ignore"],
25
+ windowsHide: true,
26
+ shell: process.platform === "win32",
27
+ });
28
+ const pending = new Map();
29
+ let nextId = 0;
30
+ const fail = (err) => {
31
+ for (const p of pending.values())
32
+ p.reject(err);
33
+ pending.clear();
34
+ };
35
+ child.on("error", (err) => fail(err));
36
+ child.on("exit", () => fail(new Error("codex app-server terminó antes de responder")));
37
+ child.stdin.on("error", (err) => fail(err));
38
+ const rl = createInterface({ input: child.stdout });
39
+ rl.on("line", (line) => {
40
+ let msg;
41
+ try {
42
+ msg = JSON.parse(line);
43
+ }
44
+ catch {
45
+ return;
46
+ }
47
+ if (typeof msg.id !== "number")
48
+ return;
49
+ const p = pending.get(msg.id);
50
+ if (!p)
51
+ return;
52
+ pending.delete(msg.id);
53
+ if (msg.error)
54
+ p.reject(new Error(msg.error.message ?? "error de codex app-server"));
55
+ else
56
+ p.resolve(msg.result);
57
+ });
58
+ const req = (method, params) => new Promise((resolve, reject) => {
59
+ const id = ++nextId;
60
+ pending.set(id, { resolve, reject });
61
+ child.stdin.write(`${JSON.stringify({ id, method, params })}\n`);
62
+ });
63
+ const timer = setTimeout(() => fail(new Error("codex app-server no respondió a tiempo")), timeoutMs);
64
+ try {
65
+ await req("initialize", {
66
+ clientInfo: { name: COMMAND_NAME, version: "1" },
67
+ capabilities: { experimentalApi: true },
68
+ });
69
+ child.stdin.write(`${JSON.stringify({ method: "initialized" })}\n`);
70
+ return await fn(req);
71
+ }
72
+ finally {
73
+ clearTimeout(timer);
74
+ rl.close();
75
+ child.stdout.destroy();
76
+ child.kill();
77
+ const force = setTimeout(() => child.kill("SIGKILL"), 2_000);
78
+ force.unref();
79
+ child.unref();
80
+ }
81
+ }
82
+ function isOurCommand(command) {
83
+ return new RegExp(`${COMMAND_NAME}@\\S+ update\\b`).test(command);
84
+ }
85
+ async function ourHooks(req) {
86
+ const res = (await req("hooks/list", { cwds: [homedir()] }));
87
+ return (res.data ?? [])
88
+ .flatMap((d) => d.hooks ?? [])
89
+ .filter((h) => h.source === "user" && isOurCommand(h.command ?? ""));
90
+ }
91
+ export async function approveCodexHook(timeoutMs = 20_000) {
92
+ try {
93
+ return await withAppServer(async (req) => {
94
+ const needs = (await ourHooks(req)).filter((h) => h.trustStatus !== "trusted" && h.trustStatus !== "managed" && h.currentHash);
95
+ if (needs.length > 0) {
96
+ await req("config/batchWrite", {
97
+ edits: [
98
+ {
99
+ keyPath: "hooks.state",
100
+ value: Object.fromEntries(needs.map((h) => [h.key, { trusted_hash: h.currentHash }])),
101
+ mergeStrategy: "upsert",
102
+ },
103
+ ],
104
+ reloadUserConfig: true,
105
+ });
106
+ }
107
+ const after = await ourHooks(req);
108
+ const pending = after.filter((h) => h.trustStatus !== "trusted" && h.trustStatus !== "managed").length;
109
+ return { ok: pending === 0 && after.length > 0, approved: needs.length, pending };
110
+ }, timeoutMs);
111
+ }
112
+ catch (err) {
113
+ return {
114
+ ok: false,
115
+ approved: 0,
116
+ pending: 0,
117
+ detail: err instanceof Error ? err.message : String(err),
118
+ };
119
+ }
120
+ }
121
+ export async function codexHookTrustStatus(timeoutMs = 20_000) {
122
+ try {
123
+ return await withAppServer(async (req) => (await ourHooks(req)).map((h) => h.trustStatus ?? "unknown"), timeoutMs);
124
+ }
125
+ catch {
126
+ return null;
127
+ }
128
+ }
@@ -1,9 +1,11 @@
1
1
  import { AuthRequiredError } from "../api.js";
2
2
  import { hasValidAuth, readAuth } from "../auth.js";
3
3
  import { installAutoUpdate } from "../autoupdate.js";
4
+ import { approveCodexHook, hasCodexTrustConsent, saveCodexTrustConsent } from "../codex-trust.js";
4
5
  import { bold, cyan, dim, gray, green, log, logError, red, yellow } from "../colors.js";
5
6
  import { installAll } from "../installer.js";
6
7
  import { COMMAND_NAME, formatBytes, getPackageVersion, getTargetSkillsDir } from "../lib.js";
8
+ import { confirm } from "../ui.js";
7
9
  import { runLogin } from "./login.js";
8
10
  const BANNER_WIDTH = 58;
9
11
  const SHORT_DESCRIPTIONS = {
@@ -147,6 +149,37 @@ function printNextSteps(noHook) {
147
149
  function isClaudeCode() {
148
150
  return process.env.CLAUDECODE === "1" || process.env.CLAUDE_CODE === "1";
149
151
  }
152
+ async function setUpCodex(manifest, results, opts) {
153
+ if (!opts.noHook) {
154
+ let consent = hasCodexTrustConsent();
155
+ if (!consent && !opts.yes && process.stdin.isTTY && process.stdout.isTTY) {
156
+ log(` ${bold("Codex")} pide aprobar cada hook antes de ejecutarlo. El nuestro solo corre ${cyan(`${COMMAND_NAME} update`)} al abrir una sesión.`);
157
+ consent = await confirm(" ¿Lo apruebo para que el auto-update funcione en Codex?", true);
158
+ if (consent)
159
+ saveCodexTrustConsent();
160
+ }
161
+ const res = consent ? await approveCodexHook() : null;
162
+ if (res?.ok) {
163
+ log(` ${green("✓")} Codex: auto-update aprobado.`);
164
+ }
165
+ else {
166
+ if (res?.detail)
167
+ log(` ${gray(`(no pude aprobarlo solo: ${res.detail})`)}`);
168
+ log(` ${bold(yellow("Codex:"))} al abrirlo elegí ${bold("«Trust all and continue»")} ${gray("(o /hooks)")} para activar el auto-update.`);
169
+ }
170
+ }
171
+ const installedMcps = new Set(results.filter((r) => r.kind === "mcp" && r.status !== "failed").map((r) => r.name));
172
+ const remotes = Object.entries(manifest.mcps ?? {})
173
+ .filter(([name, m]) => m.url && installedMcps.has(name))
174
+ .map(([name]) => name)
175
+ .sort();
176
+ if (remotes.length > 0) {
177
+ log(` ${gray("Conectá cada MCP de Codex una vez:")}`);
178
+ for (const name of remotes)
179
+ log(` ${cyan(`codex mcp login ${name}`)}`);
180
+ }
181
+ log("");
182
+ }
150
183
  export async function runInstall(opts = {}) {
151
184
  const compact = isClaudeCode();
152
185
  if (!compact)
@@ -243,6 +276,8 @@ export async function runInstall(opts = {}) {
243
276
  if (!compact)
244
277
  log("");
245
278
  }
279
+ if (result.targets.includes("codex"))
280
+ await setUpCodex(result.manifest, result.results, opts);
246
281
  if (hasFails)
247
282
  return 1;
248
283
  if (!compact) {
@@ -3,6 +3,8 @@ import { homedir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
4
  import { AuthRequiredError, fetchManifest } from "../api.js";
5
5
  import { readAuth } from "../auth.js";
6
+ import { hasAutoUpdate } from "../autoupdate.js";
7
+ import { codexHookTrustStatus } from "../codex-trust.js";
6
8
  import { bold, cyan, dim, gray, green, log, logError, logWarn, yellow } from "../colors.js";
7
9
  import { detectDrift, findMisplacedMcps, inspectMcpInTarget } from "../installer.js";
8
10
  import { COMMAND_NAME, GATEWAY_URL, getAutoUpdateLogPath, getClaudeJsonPath, getClaudeSkillsDir, getLockFilePath, getMcpScopeDir, getMcpScopeWarning, getPackageVersion, getTargetSkillsDir, readLockFile, } from "../lib.js";
@@ -27,6 +29,13 @@ function reportAutoUpdateLog() {
27
29
  catch {
28
30
  }
29
31
  }
32
+ async function reportCodexHookTrust() {
33
+ const states = await codexHookTrustStatus();
34
+ if (!states || states.every((s) => s === "trusted" || s === "managed"))
35
+ return;
36
+ logWarn(`El auto-update de Codex está sin aprobar (${states.join(", ")}): Codex no lo ejecuta. Abrí Codex y elegí «Trust all and continue» (o /hooks), o corré ${bold(`${COMMAND_NAME} install --target=codex`)} y aceptá la aprobación.`);
37
+ log("");
38
+ }
30
39
  function reportSkillBackups() {
31
40
  const backupsLog = join(dirname(getAutoUpdateLogPath()), "backups", "backups.log");
32
41
  if (!existsSync(backupsLog))
@@ -309,6 +318,8 @@ export async function runStatus() {
309
318
  log("");
310
319
  reportAutoUpdateLog();
311
320
  reportSkillBackups();
321
+ if (installedTargets.includes("codex") && hasAutoUpdate("codex"))
322
+ await reportCodexHookTrust();
312
323
  if (!auth) {
313
324
  logWarn(`Inicia sesión con: ${bold(`${COMMAND_NAME} login`)}`);
314
325
  log("");
@@ -2,7 +2,8 @@ import { appendFileSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "
2
2
  import { dirname } from "node:path";
3
3
  import { AuthRequiredError, ensureFreshAuth, fetchManifest } from "../api.js";
4
4
  import { hasValidAuth } from "../auth.js";
5
- import { refreshAutoUpdate } from "../autoupdate.js";
5
+ import { hasAutoUpdate, refreshAutoUpdate } from "../autoupdate.js";
6
+ import { approveCodexHook, hasCodexTrustConsent } from "../codex-trust.js";
6
7
  import { cyan, dim, gray, green, log, logError, yellow } from "../colors.js";
7
8
  import { detectDrift, installAll, summarize } from "../installer.js";
8
9
  import { COMMAND_NAME, getAutoUpdateLogPath, getLastCheckPath, getPackageVersion, } from "../lib.js";
@@ -123,6 +124,11 @@ export async function runUpdate(opts = {}) {
123
124
  }
124
125
  }
125
126
  }
127
+ async function reapproveCodexHook() {
128
+ if (!hasCodexTrustConsent() || !hasAutoUpdate("codex"))
129
+ return;
130
+ await approveCodexHook();
131
+ }
126
132
  async function runUpdateOnce(opts) {
127
133
  const manifest = await fetchManifest();
128
134
  if (!manifest) {
@@ -142,6 +148,7 @@ async function runUpdateOnce(opts) {
142
148
  markChecked();
143
149
  if (!opts.dryRun) {
144
150
  refreshAutoUpdate();
151
+ await reapproveCodexHook();
145
152
  clearAutoUpdateLog();
146
153
  }
147
154
  return 0;
@@ -173,6 +180,7 @@ async function runUpdateOnce(opts) {
173
180
  }
174
181
  summarize(result.results);
175
182
  refreshAutoUpdate();
183
+ await reapproveCodexHook();
176
184
  const failed = result.results.some((r) => r.status === "failed");
177
185
  if (!failed) {
178
186
  clearAutoUpdateLog();
package/dist/installer.js CHANGED
@@ -5,7 +5,7 @@ import { basename, dirname, join, resolve, sep } from "node:path";
5
5
  import { AuthRequiredError, fetchManifest, fetchSkillFile, recordManifestFloor } from "./api.js";
6
6
  import { dim, gray, green, logError, logWarn, red, yellow } from "./colors.js";
7
7
  import { ensureLazyProxy, lazyMcpDir, lazyProxyPath, lazyToolsPath, stdioLaunch, } from "./lazy-mcp.js";
8
- import { computeBundleHash, ensureDir, getAntigravityGeminiLegacyMcpConfigPath, getAntigravityGeminiLegacySkillsDir, getAntigravityLegacyMcpConfigPath, getAntigravityLegacySkillsDir, getClaudeSettingsPath, getConfigDir, getCursorLegacyRulesDir, getHermesBin, getLockFilePath, getMcpScopeDir, getMcpScopeWarning, getOpenCodeLegacyPromptsDir, getPackageVersion, getUserScopeMcpConfigPath, isMcpScopeEnvOverride, readLockFile, sha256File, walkSkillFiles, writeLockFile, } from "./lib.js";
8
+ import { computeBundleHash, ensureDir, getAntigravityGeminiLegacyMcpConfigPath, getAntigravityGeminiLegacySkillsDir, getAntigravityGeminiSkillsDir, getAntigravityLegacyMcpConfigPath, getAntigravityLegacySkillsDir, getClaudeSettingsPath, getConfigDir, getCursorLegacyRulesDir, getHermesBin, getLockFilePath, getMcpScopeDir, getMcpScopeWarning, getOpenCodeLegacyPromptsDir, getPackageVersion, getUserScopeMcpConfigPath, isMcpScopeEnvOverride, readLockFile, sha256File, walkSkillFiles, writeLockFile, } from "./lib.js";
9
9
  import { renderHermesMcpOps, renderMcpForTarget } from "./render.js";
10
10
  import { TARGETS, detectInstalledTargets } from "./targets.js";
11
11
  const SKILL_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
@@ -686,15 +686,65 @@ function tomlMarkers(name) {
686
686
  end: `# <<< alegra-data-agent:${name} <<<`,
687
687
  };
688
688
  }
689
+ function splitTomlBlock(inner, name) {
690
+ const lines = inner.split("\n");
691
+ const isOurs = (h) => h === `[mcp_servers.${name}]` || h === `[mcp_servers.${name}.env]`;
692
+ const at = lines.findIndex((l) => {
693
+ const t = l.trim();
694
+ return t.startsWith("[") && !isOurs(tomlHeader(t));
695
+ });
696
+ if (at === -1)
697
+ return { ours: inner, foreign: "" };
698
+ return {
699
+ ours: lines.slice(0, at).join("\n").replace(/\n+$/, ""),
700
+ foreign: lines.slice(at).join("\n").replace(/\n+$/, ""),
701
+ };
702
+ }
703
+ function tomlHeader(line) {
704
+ return line.replace(/\]\s*#.*$/, "]");
705
+ }
706
+ function dropServerSubtables(content, name) {
707
+ const prefix = `[mcp_servers.${name}.`;
708
+ const out = [];
709
+ let skipping = false;
710
+ let dropped = false;
711
+ for (const line of content.split("\n")) {
712
+ const t = line.trim();
713
+ const isMarker = t.startsWith("# >>> alegra-data-agent:") || t.startsWith("# <<< alegra-data-agent:");
714
+ if (t.startsWith("[") || isMarker)
715
+ skipping = !isMarker && tomlHeader(t).startsWith(prefix);
716
+ if (skipping)
717
+ dropped = true;
718
+ else
719
+ out.push(line);
720
+ }
721
+ return dropped ? out.join("\n").replace(/\n{3,}/g, "\n\n") : content;
722
+ }
723
+ function findTomlBlock(content, name) {
724
+ const { begin, end } = tomlMarkers(name);
725
+ const bi = content.indexOf(begin);
726
+ if (bi === -1)
727
+ return null;
728
+ const ei = content.indexOf(end, bi);
729
+ if (ei === -1)
730
+ return null;
731
+ return {
732
+ start: bi,
733
+ stop: ei + end.length,
734
+ inner: content
735
+ .slice(bi + begin.length, ei)
736
+ .replace(/^\n/, "")
737
+ .replace(/\n$/, ""),
738
+ };
739
+ }
689
740
  export function upsertTomlBlock(content, name, block) {
690
741
  const { begin, end } = tomlMarkers(name);
691
742
  const fenced = `${begin}\n${block}\n${end}`;
692
- const bi = content.indexOf(begin);
693
- if (bi !== -1) {
694
- const ei = content.indexOf(end, bi);
695
- if (ei !== -1) {
696
- return content.slice(0, bi) + fenced + content.slice(ei + end.length);
697
- }
743
+ const found = findTomlBlock(content, name);
744
+ if (found) {
745
+ const { foreign } = splitTomlBlock(found.inner, name);
746
+ const moved = foreign ? `\n\n${foreign}` : "";
747
+ return content.slice(0, found.start) + fenced + moved + content.slice(found.stop);
698
748
  }
699
749
  if (content.length === 0)
700
750
  return `${fenced}\n`;
@@ -702,33 +752,24 @@ export function upsertTomlBlock(content, name, block) {
702
752
  return `${content}${sep}${fenced}\n`;
703
753
  }
704
754
  export function extractTomlBlock(content, name) {
705
- const { begin, end } = tomlMarkers(name);
706
- const bi = content.indexOf(begin);
707
- if (bi === -1)
708
- return null;
709
- const ei = content.indexOf(end, bi);
710
- if (ei === -1)
711
- return null;
712
- return content
713
- .slice(bi + begin.length, ei)
714
- .replace(/^\n/, "")
715
- .replace(/\n$/, "");
755
+ const found = findTomlBlock(content, name);
756
+ return found ? splitTomlBlock(found.inner, name).ours : null;
716
757
  }
717
758
  export function removeTomlBlock(content, name) {
718
- const { begin, end } = tomlMarkers(name);
719
- const bi = content.indexOf(begin);
720
- if (bi === -1)
759
+ const found = findTomlBlock(content, name);
760
+ if (!found)
721
761
  return content;
722
- const ei = content.indexOf(end, bi);
723
- if (ei === -1)
724
- return content;
725
- let start = bi;
726
- let stop = ei + end.length;
762
+ const { foreign } = splitTomlBlock(found.inner, name);
763
+ if (foreign) {
764
+ return dropServerSubtables(content.slice(0, found.start) + foreign + content.slice(found.stop), name);
765
+ }
766
+ let start = found.start;
767
+ let stop = found.stop;
727
768
  if (content[stop] === "\n")
728
769
  stop++;
729
770
  if (start > 0 && content[start - 1] === "\n")
730
771
  start--;
731
- return content.slice(0, start) + content.slice(stop);
772
+ return dropServerSubtables(content.slice(0, start) + content.slice(stop), name);
732
773
  }
733
774
  function registerMcpInTomlBlock(mcp, spec) {
734
775
  const existing = existsSync(spec.mcpConfigPath) ? readFileSync(spec.mcpConfigPath, "utf-8") : "";
@@ -891,6 +932,7 @@ function cleanupLegacySkillArtifacts(spec) {
891
932
  const legacySkillDirs = [
892
933
  getAntigravityLegacySkillsDir(),
893
934
  getAntigravityGeminiLegacySkillsDir(),
935
+ getAntigravityGeminiSkillsDir(),
894
936
  ];
895
937
  for (const base of legacySkillDirs) {
896
938
  for (const name of [...Object.keys(lock.skills), ...LEGACY_RENAMED_SKILLS]) {
@@ -1309,10 +1351,6 @@ export function uninstallMcp(name, opts = {}) {
1309
1351
  export function uninstallAll(opts = {}) {
1310
1352
  const lock = readLockFile();
1311
1353
  const results = [];
1312
- for (const name of Object.keys(lock.skills))
1313
- results.push(uninstallSkill(name, opts));
1314
- for (const name of Object.keys(lock.mcps ?? {}))
1315
- results.push(uninstallMcp(name, opts));
1316
1354
  if (!opts.dryRun) {
1317
1355
  for (const spec of Object.values(TARGETS)) {
1318
1356
  try {
@@ -1321,6 +1359,12 @@ export function uninstallAll(opts = {}) {
1321
1359
  catch {
1322
1360
  }
1323
1361
  }
1362
+ }
1363
+ for (const name of Object.keys(lock.skills))
1364
+ results.push(uninstallSkill(name, opts));
1365
+ for (const name of Object.keys(lock.mcps ?? {}))
1366
+ results.push(uninstallMcp(name, opts));
1367
+ if (!opts.dryRun) {
1324
1368
  if (Object.keys(readLockFile().mcps ?? {}).length === 0) {
1325
1369
  try {
1326
1370
  rmSync(lazyMcpDir(), { recursive: true, force: true });
package/dist/lib.js CHANGED
@@ -174,6 +174,9 @@ export function getOpenCodeConfigPath() {
174
174
  export function getAntigravitySkillsDir() {
175
175
  return join(homedir(), ".gemini", "antigravity-cli", "skills");
176
176
  }
177
+ export function getAntigravityConfigSkillsDir() {
178
+ return join(homedir(), ".gemini", "config", "skills");
179
+ }
177
180
  export function getAntigravityGeminiSkillsDir() {
178
181
  return join(homedir(), ".gemini", "skills");
179
182
  }
@@ -183,6 +186,9 @@ export function getAntigravityMcpConfigPath() {
183
186
  export function getGeminiSettingsPath() {
184
187
  return join(homedir(), ".gemini", "settings.json");
185
188
  }
189
+ export function getAntigravityHooksPath() {
190
+ return join(homedir(), ".gemini", "config", "hooks.json");
191
+ }
186
192
  export function getAntigravityLegacySkillsDir() {
187
193
  return join(homedir(), ".antigravity", "skills");
188
194
  }
package/dist/main.js CHANGED
@@ -43,6 +43,7 @@ async function main() {
43
43
  only: args.only.length > 0 ? args.only : undefined,
44
44
  targets: args.targets.length > 0 ? args.targets : undefined,
45
45
  noHook: args.flags.noHook,
46
+ yes: args.flags.yes,
46
47
  hookMode: "auto",
47
48
  });
48
49
  break;
package/dist/targets.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
- import { getAntigravityGeminiSkillsDir, getAntigravityMcpConfigPath, getAntigravitySkillsDir, getClaudeMcpConfigPath, getClaudeSkillsDir, getCodexConfigPath, getCodexSkillsDir, getCursorMcpPath, getCursorSkillsDir, getHermesConfigPath, getHermesHome, getHermesSkillsDir, getOpenCodeConfigPath, getOpenCodeSkillsDir, } from "./lib.js";
4
+ import { getAntigravityConfigSkillsDir, getAntigravityMcpConfigPath, getAntigravitySkillsDir, getClaudeMcpConfigPath, getClaudeSkillsDir, getCodexConfigPath, getCodexSkillsDir, getCursorMcpPath, getCursorSkillsDir, getHermesConfigPath, getHermesHome, getHermesSkillsDir, getOpenCodeConfigPath, getOpenCodeSkillsDir, } from "./lib.js";
5
5
  export const TARGETS = {
6
6
  "claude-code": {
7
7
  id: "claude-code",
@@ -37,7 +37,7 @@ export const TARGETS = {
37
37
  id: "antigravity",
38
38
  label: "Antigravity",
39
39
  skillsDir: getAntigravitySkillsDir(),
40
- mirrorSkillsDirs: [getAntigravityGeminiSkillsDir()],
40
+ mirrorSkillsDirs: [getAntigravityConfigSkillsDir()],
41
41
  skillFormat: "folder",
42
42
  mcpConfigPath: getAntigravityMcpConfigPath(),
43
43
  mcpRootKey: "mcpServers",
@@ -76,7 +76,11 @@ export function detectInstalledTargets() {
76
76
  "claude-code": [join(home, ".claude")],
77
77
  cursor: [join(home, ".cursor")],
78
78
  opencode: [join(xdgConfig, "opencode")],
79
- antigravity: [join(home, ".gemini", "antigravity-cli"), join(home, ".gemini", "antigravity")],
79
+ antigravity: [
80
+ join(home, ".gemini", "antigravity-cli"),
81
+ join(home, ".gemini", "antigravity"),
82
+ join(home, ".gemini", "antigravity-ide"),
83
+ ],
80
84
  codex: [codexHome],
81
85
  hermes: [getHermesHome()],
82
86
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alegra-data-agent",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "private": false,
5
5
  "description": "Agente de Data interno de Alegra. Instala skills + MCPs en tu cliente de IA con un comando.",
6
6
  "license": "UNLICENSED",