alegra-data-agent 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.js CHANGED
@@ -1,5 +1,7 @@
1
+ import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
1
3
  import { readAuth } from "./auth.js";
2
- import { COMMAND_NAME, GATEWAY_URL, getPackageVersion } from "./lib.js";
4
+ import { COMMAND_NAME, GATEWAY_URL, getManifestCachePath, getPackageVersion } from "./lib.js";
3
5
  import { verifyManifestSignature } from "./signature.js";
4
6
  export class ManifestSignatureError extends Error {
5
7
  constructor(reason) {
@@ -29,11 +31,12 @@ function newRequestId() {
29
31
  return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
30
32
  }
31
33
  function userAgent() {
32
- return `${COMMAND_NAME}/${getPackageVersion()} (+https://github.com/alegradev/alegra-data-agent)`;
34
+ return `${COMMAND_NAME}/${getPackageVersion()} (+https://github.com/Alegra-Team/data-agent-selfservice)`;
33
35
  }
34
36
  function sleep(ms) {
35
37
  return new Promise((r) => setTimeout(r, ms));
36
38
  }
39
+ let rateLimitCooldownUntil = 0;
37
40
  async function authedFetch(path, opts = {}) {
38
41
  const auth = readAuth();
39
42
  if (!auth) {
@@ -49,6 +52,9 @@ async function authedFetch(path, opts = {}) {
49
52
  let attempt = 0;
50
53
  const delays = [250, 750, 2250];
51
54
  while (true) {
55
+ const pending = rateLimitCooldownUntil - Date.now();
56
+ if (pending > 0)
57
+ await sleep(pending);
52
58
  const ac = new AbortController();
53
59
  const timer = setTimeout(() => ac.abort(), timeoutMs);
54
60
  let res;
@@ -59,6 +65,7 @@ async function authedFetch(path, opts = {}) {
59
65
  Accept: "application/json,application/octet-stream",
60
66
  "User-Agent": userAgent(),
61
67
  "X-Request-Id": reqId,
68
+ ...opts.headers,
62
69
  },
63
70
  signal: ac.signal,
64
71
  });
@@ -86,13 +93,16 @@ async function authedFetch(path, opts = {}) {
86
93
  "Si saliste de Alegra, esto es esperado. Si no, contacta a #data.");
87
94
  }
88
95
  if (res.status === 429) {
96
+ const cap = maxRetries + 4;
89
97
  const ra = parseRetryAfter(res.headers.get("retry-after"));
90
- if (ra !== null && ra <= 5_000 && attempt < maxRetries) {
91
- await sleep(ra);
98
+ const wait = ra !== null ? ra : delays[Math.min(attempt, delays.length - 1)] * 2;
99
+ rateLimitCooldownUntil = Math.max(rateLimitCooldownUntil, Date.now() + wait);
100
+ if (attempt < cap) {
101
+ await sleep(wait);
92
102
  attempt++;
93
103
  continue;
94
104
  }
95
- throw new GatewayError(429, path, "Rate limit excedido. Espera un minuto y vuelve a intentar.");
105
+ throw new GatewayError(429, path, `Rate limit del gateway tras ${cap + 1} intentos. Esperá un minuto y reintentá (request-id ${reqId}).`);
96
106
  }
97
107
  if (res.status >= 500 && res.status < 600) {
98
108
  if (attempt < maxRetries) {
@@ -102,6 +112,8 @@ async function authedFetch(path, opts = {}) {
102
112
  }
103
113
  throw new GatewayError(res.status, path, `Gateway con error ${res.status}. Reintenta en unos segundos. Si persiste, reporta a #data (request-id ${reqId}).`);
104
114
  }
115
+ if (res.status === 304 && opts.allowNotModified)
116
+ return res;
105
117
  if (!res.ok) {
106
118
  throw new GatewayError(res.status, path, `Gateway respondió ${res.status} en ${path}`);
107
119
  }
@@ -151,16 +163,76 @@ function mapNetworkError(err, path, timeoutMs) {
151
163
  }
152
164
  return new GatewayError(0, path, `Error de red al hablar con el gateway: ${err.message}`);
153
165
  }
166
+ export function parseManifestCache(raw) {
167
+ try {
168
+ const parsed = JSON.parse(raw);
169
+ if (typeof parsed !== "object" || parsed === null)
170
+ return null;
171
+ const { etag, manifest } = parsed;
172
+ if (typeof etag !== "string" || etag.length === 0)
173
+ return null;
174
+ if (typeof manifest !== "object" || manifest === null)
175
+ return null;
176
+ return { etag, manifest: manifest };
177
+ }
178
+ catch {
179
+ return null;
180
+ }
181
+ }
182
+ function readManifestCache() {
183
+ try {
184
+ return parseManifestCache(readFileSync(getManifestCachePath(), "utf-8"));
185
+ }
186
+ catch {
187
+ return null;
188
+ }
189
+ }
190
+ function writeManifestCache(etag, manifest) {
191
+ const path = getManifestCachePath();
192
+ try {
193
+ mkdirSync(dirname(path), { recursive: true });
194
+ const tmp = `${path}.${process.pid}.tmp`;
195
+ writeFileSync(tmp, `${JSON.stringify({ etag, manifest })}\n`, { mode: 0o600 });
196
+ try {
197
+ renameSync(tmp, path);
198
+ }
199
+ catch (err) {
200
+ rmSync(tmp, { force: true });
201
+ throw err;
202
+ }
203
+ }
204
+ catch {
205
+ }
206
+ }
207
+ function verifiedOrThrow(manifest) {
208
+ const check = verifyManifestSignature(manifest, { softFail: false });
209
+ if (!check.ok) {
210
+ throw new ManifestSignatureError(check.reason);
211
+ }
212
+ return manifest;
213
+ }
154
214
  export async function fetchManifest(opts = {}) {
155
215
  try {
156
- const res = await authedFetch("/api/manifest", opts);
216
+ const cached = readManifestCache();
217
+ const res = await authedFetch("/api/manifest", {
218
+ ...opts,
219
+ allowNotModified: true,
220
+ headers: cached ? { ...opts.headers, "If-None-Match": cached.etag } : opts.headers,
221
+ });
222
+ if (res.status === 304) {
223
+ if (cached)
224
+ return verifiedOrThrow(cached.manifest);
225
+ const retry = await authedFetch("/api/manifest", { ...opts, allowNotModified: false });
226
+ if (!retry.ok)
227
+ return null;
228
+ return verifiedOrThrow((await retry.json()));
229
+ }
157
230
  if (!res.ok)
158
231
  return null;
159
- const manifest = (await res.json());
160
- const check = verifyManifestSignature(manifest, { softFail: false });
161
- if (!check.ok) {
162
- throw new ManifestSignatureError(check.reason);
163
- }
232
+ const manifest = verifiedOrThrow((await res.json()));
233
+ const etag = res.headers.get("etag");
234
+ if (etag)
235
+ writeManifestCache(etag, manifest);
164
236
  return manifest;
165
237
  }
166
238
  catch (err) {
@@ -178,8 +250,3 @@ export async function fetchSkillFile(skillName, relPath) {
178
250
  const buffer = await res.arrayBuffer();
179
251
  return new Uint8Array(buffer);
180
252
  }
181
- export async function fetchMcps(opts = {}) {
182
- const res = await authedFetch("/api/mcps", opts);
183
- const body = (await res.json());
184
- return body.mcps ?? {};
185
- }
package/dist/auth.js CHANGED
@@ -25,19 +25,26 @@ function constantTimeStringEq(a, b) {
25
25
  return false;
26
26
  return timingSafeEqual(ab, bb);
27
27
  }
28
+ let _authCache = null;
28
29
  export function readAuth() {
30
+ if (_authCache)
31
+ return _authCache.value;
29
32
  const path = getAuthFilePath();
30
- if (!existsSync(path))
31
- return null;
32
- try {
33
- return JSON.parse(readFileSync(path, "utf-8"));
34
- }
35
- catch {
36
- return null;
33
+ let value = null;
34
+ if (existsSync(path)) {
35
+ try {
36
+ value = JSON.parse(readFileSync(path, "utf-8"));
37
+ }
38
+ catch {
39
+ value = null;
40
+ }
37
41
  }
42
+ _authCache = { value };
43
+ return value;
38
44
  }
39
45
  export function writeAuth(record) {
40
46
  const path = getAuthFilePath();
47
+ _authCache = { value: record };
41
48
  mkdirSync(dirname(path), { recursive: true });
42
49
  writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, "utf-8");
43
50
  try {
@@ -48,6 +55,7 @@ export function writeAuth(record) {
48
55
  }
49
56
  export function clearAuth() {
50
57
  const path = getAuthFilePath();
58
+ _authCache = { value: null };
51
59
  if (existsSync(path))
52
60
  rmSync(path, { force: true });
53
61
  }
@@ -278,6 +286,13 @@ function htmlPage(kind, subject) {
278
286
  </body>
279
287
  </html>`;
280
288
  }
289
+ const HTML_ESCAPES = {
290
+ "&": "&amp;",
291
+ "<": "&lt;",
292
+ ">": "&gt;",
293
+ '"': "&quot;",
294
+ "'": "&#39;",
295
+ };
281
296
  function escapeHtml(s) {
282
- return s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
297
+ return s.replace(/[&<>"']/g, (c) => HTML_ESCAPES[c] ?? c);
283
298
  }
@@ -0,0 +1,216 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { logWarn } from "./colors.js";
4
+ import { hookCommandFor, runHookInstall, runHookUninstall, } from "./commands/hook.js";
5
+ import { COMMAND_NAME, getClaudeSettingsPath, getCodexHooksPath, getCursorHooksPath, getGeminiSettingsPath, getOpenCodePluginPath, readLockFile, } from "./lib.js";
6
+ function readJsonSafe(path) {
7
+ if (!existsSync(path))
8
+ return {};
9
+ try {
10
+ return JSON.parse(readFileSync(path, "utf-8"));
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ function writeJson(path, data) {
17
+ mkdirSync(dirname(path), { recursive: true });
18
+ const tmp = `${path}.tmp-${process.pid}`;
19
+ writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
20
+ renameSync(tmp, path);
21
+ }
22
+ function entryIsOurs(m) {
23
+ if (!m || typeof m !== "object")
24
+ return false;
25
+ const o = m;
26
+ if (typeof o.command === "string" && o.command.includes(COMMAND_NAME))
27
+ return true;
28
+ if (Array.isArray(o.hooks)) {
29
+ return o.hooks.some((h) => h &&
30
+ typeof h === "object" &&
31
+ typeof h.command === "string" &&
32
+ h.command.includes(COMMAND_NAME));
33
+ }
34
+ return false;
35
+ }
36
+ function freshHookList(cfg, key) {
37
+ const hooks = (cfg.hooks && typeof cfg.hooks === "object" ? cfg.hooks : {});
38
+ cfg.hooks = hooks;
39
+ const list = Array.isArray(hooks[key]) ? hooks[key] : [];
40
+ return list.filter((m) => !entryIsOurs(m));
41
+ }
42
+ export function guiResilientCommand(base, opts) {
43
+ const platform = opts.platform ?? process.platform;
44
+ if (platform === "win32")
45
+ return opts.silence ? `${base} >NUL 2>&1` : base;
46
+ const extraDirs = [
47
+ dirname(process.execPath),
48
+ process.env.PNPM_HOME ?? "",
49
+ "$HOME/.local/share/pnpm",
50
+ "$HOME/Library/pnpm",
51
+ "/opt/homebrew/bin",
52
+ "/usr/local/bin",
53
+ ].filter(Boolean);
54
+ const run = opts.silence ? `${base} >/dev/null 2>&1` : base;
55
+ const logFile = `"\${XDG_CONFIG_HOME:-$HOME/.config}/${COMMAND_NAME}/autoupdate.log"`;
56
+ return (`PATH="$PATH:${extraDirs.join(":")}"; ` +
57
+ `if command -v pnpm >/dev/null 2>&1; then ${run}; ` +
58
+ `else mkdir -p "$(dirname ${logFile})" && ` +
59
+ `echo "$(date '+%Y-%m-%d %H:%M:%S') [${opts.client}] pnpm no esta en el PATH del cliente; auto-update omitido" >> ${logFile}; fi`);
60
+ }
61
+ function installCursor(command) {
62
+ const path = getCursorHooksPath();
63
+ const cfg = readJsonSafe(path);
64
+ if (cfg === null) {
65
+ logWarn(`${path} no es JSON válido; salteo el auto-update de Cursor.`);
66
+ return;
67
+ }
68
+ if (typeof cfg.version !== "number")
69
+ cfg.version = 1;
70
+ const list = freshHookList(cfg, "workspaceOpen");
71
+ list.push({ command });
72
+ cfg.hooks.workspaceOpen = list;
73
+ writeJson(path, cfg);
74
+ }
75
+ function installCodex(command) {
76
+ const path = getCodexHooksPath();
77
+ const cfg = readJsonSafe(path);
78
+ if (cfg === null) {
79
+ logWarn(`${path} no es JSON válido; salteo el auto-update de Codex.`);
80
+ return;
81
+ }
82
+ const list = freshHookList(cfg, "SessionStart");
83
+ list.push({ matcher: "startup|resume", hooks: [{ type: "command", command }] });
84
+ cfg.hooks.SessionStart = list;
85
+ writeJson(path, cfg);
86
+ }
87
+ function installAntigravity(command) {
88
+ const path = getGeminiSettingsPath();
89
+ const cfg = readJsonSafe(path);
90
+ if (cfg === null) {
91
+ logWarn(`${path} no es JSON válido; salteo el auto-update de Antigravity.`);
92
+ return;
93
+ }
94
+ const list = freshHookList(cfg, "SessionStart");
95
+ list.push({ hooks: [{ type: "command", command }] });
96
+ cfg.hooks.SessionStart = list;
97
+ writeJson(path, cfg);
98
+ }
99
+ function installOpenCode(command) {
100
+ const path = getOpenCodePluginPath();
101
+ mkdirSync(dirname(path), { recursive: true });
102
+ const plugin = `// alegra-data-agent: auto-update al iniciar OpenCode. Generado por el instalador.
103
+ // Borrar este archivo desactiva el auto-update. No editar a mano.
104
+ export const AlegraDataAgentAutoUpdate = async ({ $ }) => {
105
+ if (!$) return {}
106
+ try {
107
+ $\`${command}\`.quiet().nothrow().catch(() => {})
108
+ } catch (_e) {}
109
+ return {}
110
+ }
111
+ `;
112
+ writeFileSync(path, plugin, "utf-8");
113
+ }
114
+ function removeJsonHook(path, key) {
115
+ const cfg = readJsonSafe(path);
116
+ if (!cfg)
117
+ return;
118
+ const hooks = cfg.hooks;
119
+ if (!hooks || !Array.isArray(hooks[key]))
120
+ return;
121
+ const list = hooks[key];
122
+ const kept = list.filter((m) => !entryIsOurs(m));
123
+ if (kept.length === list.length)
124
+ return;
125
+ if (kept.length === 0)
126
+ delete hooks[key];
127
+ else
128
+ hooks[key] = kept;
129
+ writeJson(path, cfg);
130
+ }
131
+ export function installAutoUpdate(target, mode = "auto") {
132
+ const effective = target === "claude-code" ? mode : "auto";
133
+ try {
134
+ switch (target) {
135
+ case "claude-code":
136
+ runHookInstall(effective);
137
+ break;
138
+ case "cursor":
139
+ installCursor(guiResilientCommand(hookCommandFor(effective), { client: "cursor" }));
140
+ break;
141
+ case "codex":
142
+ installCodex(hookCommandFor(effective));
143
+ break;
144
+ case "antigravity":
145
+ installAntigravity(guiResilientCommand(hookCommandFor(effective), { client: "antigravity", silence: true }));
146
+ break;
147
+ case "opencode":
148
+ installOpenCode(hookCommandFor(effective));
149
+ break;
150
+ }
151
+ }
152
+ catch (err) {
153
+ logWarn(`No pude instalar el auto-update de ${target}: ${err instanceof Error ? err.message : String(err)}`);
154
+ }
155
+ }
156
+ export function removeAutoUpdate(target) {
157
+ try {
158
+ switch (target) {
159
+ case "claude-code":
160
+ runHookUninstall({ quiet: true });
161
+ break;
162
+ case "cursor":
163
+ removeJsonHook(getCursorHooksPath(), "workspaceOpen");
164
+ break;
165
+ case "codex":
166
+ removeJsonHook(getCodexHooksPath(), "SessionStart");
167
+ break;
168
+ case "antigravity":
169
+ removeJsonHook(getGeminiSettingsPath(), "SessionStart");
170
+ break;
171
+ case "opencode": {
172
+ const p = getOpenCodePluginPath();
173
+ if (existsSync(p))
174
+ rmSync(p, { force: true });
175
+ break;
176
+ }
177
+ }
178
+ }
179
+ catch (err) {
180
+ logWarn(`No pude quitar el auto-update de ${target}: ${err instanceof Error ? err.message : String(err)}`);
181
+ }
182
+ }
183
+ function jsonHookHasOurs(path, key) {
184
+ const cfg = readJsonSafe(path);
185
+ const hooks = cfg?.hooks;
186
+ if (!hooks || !Array.isArray(hooks[key]))
187
+ return false;
188
+ return hooks[key].some(entryIsOurs);
189
+ }
190
+ export function hasAutoUpdate(target) {
191
+ switch (target) {
192
+ case "claude-code":
193
+ return jsonHookHasOurs(getClaudeSettingsPath(), "SessionStart");
194
+ case "cursor":
195
+ return jsonHookHasOurs(getCursorHooksPath(), "workspaceOpen");
196
+ case "codex":
197
+ return jsonHookHasOurs(getCodexHooksPath(), "SessionStart");
198
+ case "antigravity":
199
+ return jsonHookHasOurs(getGeminiSettingsPath(), "SessionStart");
200
+ case "opencode":
201
+ return existsSync(getOpenCodePluginPath());
202
+ }
203
+ }
204
+ export function refreshAutoUpdate() {
205
+ const lock = readLockFile();
206
+ const targets = [
207
+ ...new Set([
208
+ ...Object.values(lock.skills).flatMap((s) => s.targets ?? []),
209
+ ...Object.values(lock.mcps ?? {}).flatMap((m) => m.targets ?? []),
210
+ ]),
211
+ ];
212
+ for (const t of targets) {
213
+ if (hasAutoUpdate(t))
214
+ installAutoUpdate(t, "auto");
215
+ }
216
+ }
@@ -1,16 +1,22 @@
1
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
2
3
  import { logError, logOk, logWarn } from "../colors.js";
3
4
  import { COMMAND_NAME, PRODUCT_NAME, getClaudeSettingsPath, getPackageVersion } from "../lib.js";
5
+ function versionRangeForHook(version) {
6
+ const [rawMajor] = version.split(".");
7
+ const major = Number.parseInt(rawMajor ?? "0", 10);
8
+ if (!Number.isFinite(major))
9
+ return "latest";
10
+ return major === 0 ? "0.x" : `^${major}.0.0`;
11
+ }
4
12
  function hookCommandFor(mode) {
5
13
  const v = getPackageVersion();
6
- const parts = v.split(".");
7
- const major = parts[0] ?? "0";
8
- const minor = parts[1] ?? "0";
9
- const range = `~${major}.${minor}.0`;
10
- const verb = mode === "auto" ? "update --yes" : "check";
14
+ const range = versionRangeForHook(v);
15
+ const verb = mode === "auto" ? "update --yes --background" : "check";
11
16
  return `pnpm dlx --config.minimumReleaseAge=0 ${COMMAND_NAME}@${range} ${verb} --no-bounce`;
12
17
  }
13
18
  const HOOK_SIGNATURE = COMMAND_NAME;
19
+ const HOOK_TIMEOUT_SECONDS = 120;
14
20
  function readSettings() {
15
21
  const path = getClaudeSettingsPath();
16
22
  if (!existsSync(path))
@@ -20,11 +26,15 @@ function readSettings() {
20
26
  }
21
27
  catch {
22
28
  logError(`No se pudo parsear ${path}. Agente: reparalo vos mismo (la skill agent-onboarding tiene la guía) — no le pidas al usuario editar JSON.`);
23
- return {};
29
+ return null;
24
30
  }
25
31
  }
26
32
  function writeSettings(s) {
27
- writeFileSync(getClaudeSettingsPath(), `${JSON.stringify(s, null, 2)}\n`, "utf-8");
33
+ const path = getClaudeSettingsPath();
34
+ mkdirSync(dirname(path), { recursive: true });
35
+ const tmp = `${path}.tmp-${process.pid}`;
36
+ writeFileSync(tmp, `${JSON.stringify(s, null, 2)}\n`, "utf-8");
37
+ renameSync(tmp, path);
28
38
  }
29
39
  function isOurMatcher(m) {
30
40
  if (!m || typeof m !== "object")
@@ -39,10 +49,12 @@ function isOurMatcher(m) {
39
49
  h.command.includes(HOOK_SIGNATURE));
40
50
  }
41
51
  function buildOurMatcher(command) {
42
- return { hooks: [{ type: "command", command }] };
52
+ return { hooks: [{ type: "command", command, timeout: HOOK_TIMEOUT_SECONDS }] };
43
53
  }
44
54
  export function runHookInstall(mode = "auto") {
45
55
  const settings = readSettings();
56
+ if (settings === null)
57
+ return 1;
46
58
  settings.hooks = settings.hooks ?? {};
47
59
  const command = hookCommandFor(mode);
48
60
  const existing = settings.hooks.SessionStart;
@@ -82,63 +94,47 @@ export function runHookInstall(mode = "auto") {
82
94
  }
83
95
  return 0;
84
96
  }
85
- export function refreshHookCommand() {
86
- try {
87
- const settings = readSettings();
88
- if (!settings.hooks)
89
- return;
90
- const existing = settings.hooks.SessionStart;
91
- if (!Array.isArray(existing))
92
- return;
93
- const ours = existing.find(isOurMatcher);
94
- if (!ours)
95
- return;
96
- const current = ours.hooks.find((h) => h.type === "command")?.command ?? "";
97
- const fresh = hookCommandFor("auto");
98
- if (current === fresh)
99
- return;
100
- const others = existing.filter((m) => !isOurMatcher(m));
101
- others.push(buildOurMatcher(fresh));
102
- settings.hooks.SessionStart = others;
103
- writeSettings(settings);
104
- }
105
- catch {
106
- }
107
- }
108
- export function runHookUninstall() {
97
+ export function runHookUninstall(opts = {}) {
98
+ const say = opts.quiet ? () => { } : logWarn;
99
+ const sayOk = opts.quiet ? () => { } : logOk;
109
100
  const settings = readSettings();
110
- const existing = settings.hooks?.SessionStart;
111
- if (existing === undefined) {
112
- logWarn("No había hook que desinstalar.");
101
+ if (settings === null) {
102
+ say("settings.json corrupto — no lo toco. Agente: reparalo vos mismo (la skill agent-onboarding tiene la guía) y re-corré uninstall — no le pidas al usuario editar JSON.");
103
+ return 1;
104
+ }
105
+ const hooks = settings.hooks;
106
+ const existing = hooks?.SessionStart;
107
+ if (!hooks || existing === undefined) {
108
+ say("No había hook que desinstalar.");
113
109
  return 0;
114
110
  }
115
111
  if (typeof existing === "string") {
116
112
  if (!existing.includes(HOOK_SIGNATURE)) {
117
- logWarn(`El SessionStart hook actual no es de ${PRODUCT_NAME}. No lo toco.`);
113
+ say(`El SessionStart hook actual no es de ${PRODUCT_NAME}. No lo toco.`);
118
114
  return 1;
119
115
  }
120
- settings.hooks.SessionStart = undefined;
116
+ hooks.SessionStart = undefined;
121
117
  writeSettings(settings);
122
- logOk("Hook removido.");
118
+ sayOk("Hook removido.");
123
119
  return 0;
124
120
  }
125
121
  if (!Array.isArray(existing)) {
126
- logWarn("No había hook que desinstalar.");
122
+ say("No había hook que desinstalar.");
127
123
  return 0;
128
124
  }
129
125
  const filtered = existing.filter((m) => !isOurMatcher(m));
130
126
  if (filtered.length === existing.length) {
131
- logWarn(`No encontré ningún SessionStart hook de ${PRODUCT_NAME}. No lo toco.`);
127
+ say(`No encontré ningún SessionStart hook de ${PRODUCT_NAME}. No lo toco.`);
132
128
  return 1;
133
129
  }
134
130
  if (filtered.length === 0) {
135
- settings.hooks.SessionStart = undefined;
131
+ hooks.SessionStart = undefined;
136
132
  }
137
133
  else {
138
- settings.hooks.SessionStart = filtered;
134
+ hooks.SessionStart = filtered;
139
135
  }
140
136
  writeSettings(settings);
141
- logOk("Hook removido.");
137
+ sayOk("Hook removido.");
142
138
  return 0;
143
139
  }
144
- export { hookCommandFor };
140
+ export { hookCommandFor, versionRangeForHook };