alegra-data-agent 0.4.4 → 0.5.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.
package/README.md CHANGED
@@ -14,8 +14,8 @@ Solo accesible para cuentas `@alegra.com` (OAuth Google). El CLI te abre el nave
14
14
 
15
15
  - Node ≥ 22.6.0
16
16
  - pnpm ≥ 11.1.2
17
- - Uno de: Claude Code, Cursor, OpenCode, Antigravity, Codex
17
+ - Uno de: Claude Code, Cursor, OpenCode, Antigravity, Codex, Hermes Agent
18
18
 
19
19
  ## Soporte
20
20
 
21
- Documentación y soporte para el equipo de Alegra: canal interno de BI Data.
21
+ Guía de uso, documentación y soporte para el equipo de Alegra: entra a https://alegra-data-agent.vercel.app con tu cuenta `@alegra.com`.
package/dist/api.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
- import { readAuth } from "./auth.js";
4
- import { COMMAND_NAME, GATEWAY_URL, getManifestCachePath, getPackageVersion } from "./lib.js";
5
- import { verifyManifestSignature } from "./signature.js";
3
+ import { authRecordFromToken, authRemainingMs, readAuth, writeAuth } from "./auth.js";
4
+ import { COMMAND_NAME, GATEWAY_URL, getManifestCachePath, getPackageVersion, readLockFile, writeLockFile, } from "./lib.js";
5
+ import { manifestTimestamp, verifyManifestSignature } from "./signature.js";
6
6
  export class ManifestSignatureError extends Error {
7
7
  constructor(reason) {
8
8
  super(`Firma del manifest inválida: ${reason}. Posible compromiso del gateway.`);
@@ -37,8 +37,55 @@ function sleep(ms) {
37
37
  return new Promise((r) => setTimeout(r, ms));
38
38
  }
39
39
  let rateLimitCooldownUntil = 0;
40
- async function authedFetch(path, opts = {}) {
40
+ const REFRESH_AHEAD_MS = 2 * 24 * 3600_000;
41
+ let refreshTriedFor = null;
42
+ export function shouldRefreshAuth() {
43
+ const remaining = authRemainingMs(readAuth());
44
+ return remaining !== null && remaining < REFRESH_AHEAD_MS;
45
+ }
46
+ export async function refreshAuth() {
41
47
  const auth = readAuth();
48
+ if (!auth)
49
+ return false;
50
+ refreshTriedFor = auth.token;
51
+ try {
52
+ const res = await fetch(`${GATEWAY_URL}/api/cli/refresh`, {
53
+ method: "POST",
54
+ headers: {
55
+ Authorization: `Bearer ${auth.token}`,
56
+ Accept: "application/json",
57
+ "User-Agent": userAgent(),
58
+ "X-Request-Id": newRequestId(),
59
+ },
60
+ signal: AbortSignal.timeout(10_000),
61
+ });
62
+ if (!res.ok)
63
+ return false;
64
+ const body = (await res.json());
65
+ if (typeof body.token !== "string")
66
+ return false;
67
+ const record = authRecordFromToken(body.token);
68
+ if (!record)
69
+ return false;
70
+ writeAuth(record);
71
+ return true;
72
+ }
73
+ catch {
74
+ return false;
75
+ }
76
+ }
77
+ export async function ensureFreshAuth() {
78
+ if (readAuth()?.token !== refreshTriedFor && shouldRefreshAuth())
79
+ await refreshAuth();
80
+ const auth = readAuth();
81
+ if (!auth)
82
+ return false;
83
+ const remaining = authRemainingMs(auth);
84
+ return remaining === null || remaining > 60_000;
85
+ }
86
+ async function authedFetch(path, opts = {}) {
87
+ await ensureFreshAuth();
88
+ let auth = readAuth();
42
89
  if (!auth) {
43
90
  if (opts.requireAuth === false) {
44
91
  return new Response(null, { status: 401 });
@@ -84,6 +131,10 @@ async function authedFetch(path, opts = {}) {
84
131
  clearTimeout(timer);
85
132
  }
86
133
  if (res.status === 401) {
134
+ if (auth.token !== refreshTriedFor && (await refreshAuth())) {
135
+ auth = readAuth() ?? auth;
136
+ continue;
137
+ }
87
138
  if (opts.requireAuth === false)
88
139
  return res;
89
140
  throw new AuthRequiredError(`Sesión expirada. Agente: ejecutá vos mismo \`pnpm dlx ${COMMAND_NAME} login\` — al usuario solo le toca el click del navegador.`);
@@ -204,8 +255,49 @@ function writeManifestCache(etag, manifest) {
204
255
  catch {
205
256
  }
206
257
  }
207
- function verifiedOrThrow(manifest) {
208
- const check = verifyManifestSignature(manifest, { softFail: false });
258
+ export const LOCK_MANIFEST_FLOOR_KEY = "manifestGeneratedAt";
259
+ function lockFloor() {
260
+ try {
261
+ const raw = readLockFile()[LOCK_MANIFEST_FLOOR_KEY];
262
+ return manifestTimestamp({ generatedAt: raw });
263
+ }
264
+ catch {
265
+ return null;
266
+ }
267
+ }
268
+ export function recordManifestFloor(manifest) {
269
+ try {
270
+ const ts = manifestTimestamp(manifest);
271
+ if (ts === null)
272
+ return;
273
+ const current = lockFloor();
274
+ if (current !== null && current >= ts)
275
+ return;
276
+ const lock = readLockFile();
277
+ lock[LOCK_MANIFEST_FLOOR_KEY] = manifest.generatedAt;
278
+ writeLockFile(lock);
279
+ }
280
+ catch {
281
+ }
282
+ }
283
+ function trustedCacheFloor(cached) {
284
+ if (!cached)
285
+ return null;
286
+ try {
287
+ if (!verifyManifestSignature(cached.manifest, { softFail: false }).ok)
288
+ return null;
289
+ }
290
+ catch {
291
+ return null;
292
+ }
293
+ return manifestTimestamp(cached.manifest);
294
+ }
295
+ function maxFloor(...floors) {
296
+ const valid = floors.filter((f) => typeof f === "number");
297
+ return valid.length > 0 ? Math.max(...valid) : null;
298
+ }
299
+ function verifiedOrThrow(manifest, notOlderThan) {
300
+ const check = verifyManifestSignature(manifest, { softFail: false, notOlderThan });
209
301
  if (!check.ok) {
210
302
  throw new ManifestSignatureError(check.reason);
211
303
  }
@@ -221,15 +313,15 @@ export async function fetchManifest(opts = {}) {
221
313
  });
222
314
  if (res.status === 304) {
223
315
  if (cached)
224
- return verifiedOrThrow(cached.manifest);
316
+ return verifiedOrThrow(cached.manifest, lockFloor());
225
317
  const retry = await authedFetch("/api/manifest", { ...opts, allowNotModified: false });
226
318
  if (!retry.ok)
227
319
  return null;
228
- return verifiedOrThrow((await retry.json()));
320
+ return verifiedOrThrow((await retry.json()), lockFloor());
229
321
  }
230
322
  if (!res.ok)
231
323
  return null;
232
- const manifest = verifiedOrThrow((await res.json()));
324
+ const manifest = verifiedOrThrow((await res.json()), maxFloor(trustedCacheFloor(cached), lockFloor()));
233
325
  const etag = res.headers.get("etag");
234
326
  if (etag)
235
327
  writeManifestCache(etag, manifest);
package/dist/auth.js CHANGED
@@ -83,6 +83,18 @@ export function isAuthExpired(record) {
83
83
  return false;
84
84
  return Date.now() >= exp * 1000 - 60_000;
85
85
  }
86
+ export function authRemainingMs(record) {
87
+ if (!record)
88
+ return null;
89
+ const exp = decodeJwtExp(record.token);
90
+ return exp === null ? null : exp * 1000 - Date.now();
91
+ }
92
+ export function authRecordFromToken(token) {
93
+ const email = decodeJwtEmail(token);
94
+ if (!email)
95
+ return null;
96
+ return { email, token, obtainedAt: new Date().toISOString() };
97
+ }
86
98
  export function hasValidAuth() {
87
99
  const record = readAuth();
88
100
  return record !== null && !isAuthExpired(record);
package/dist/bounce.js CHANGED
@@ -23,6 +23,12 @@ export function shouldBounce(current, latest) {
23
23
  return false;
24
24
  return compareSemver(latest, current) > 0;
25
25
  }
26
+ const WIN32_CMD_UNSAFE = /[&|<>^%!"\s]/;
27
+ export function unsafeBounceArgs(argv, platform = process.platform) {
28
+ if (platform !== "win32")
29
+ return [];
30
+ return argv.filter((a) => WIN32_CMD_UNSAFE.test(a));
31
+ }
26
32
  async function fetchLatestVersion() {
27
33
  try {
28
34
  const res = await fetch(REGISTRY_URL, {
@@ -45,6 +51,11 @@ export async function maybeBounceToLatest(argv) {
45
51
  const latest = await fetchLatestVersion();
46
52
  if (!shouldBounce(current, latest))
47
53
  return;
54
+ const unsafe = unsafeBounceArgs(argv);
55
+ if (unsafe.length > 0) {
56
+ process.stderr.write(` ⚠ No reejecuto con ${COMMAND_NAME}@${latest}: estos argumentos tienen caracteres que cmd.exe interpreta (& | < > ^ % ! " o espacios): ${unsafe.map((a) => JSON.stringify(a)).join(", ")}. Sigo con la versión ${current}.\n`);
57
+ return;
58
+ }
48
59
  const child = spawnSync("pnpm", ["dlx", "--config.minimumReleaseAge=0", `${COMMAND_NAME}@${latest}`, ...argv], {
49
60
  stdio: "inherit",
50
61
  env: { ...process.env, [NO_BOUNCE_ENV]: "1" },
package/dist/colors.js CHANGED
@@ -5,14 +5,8 @@ export const dim = wrap("\x1b[2m", "\x1b[22m");
5
5
  export const red = wrap("\x1b[31m", "\x1b[39m");
6
6
  export const green = wrap("\x1b[32m", "\x1b[39m");
7
7
  export const yellow = wrap("\x1b[33m", "\x1b[39m");
8
- export const blue = wrap("\x1b[34m", "\x1b[39m");
9
- export const magenta = wrap("\x1b[35m", "\x1b[39m");
10
8
  export const cyan = wrap("\x1b[36m", "\x1b[39m");
11
9
  export const gray = wrap("\x1b[90m", "\x1b[39m");
12
- export const HIDE_CURSOR = SUPPORTS_COLOR ? "\x1b[?25l" : "";
13
- export const SHOW_CURSOR = SUPPORTS_COLOR ? "\x1b[?25h" : "";
14
- export const CLEAR_LINE = SUPPORTS_COLOR ? "\r\x1b[K" : "\r";
15
- export const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
16
10
  export function write(s) {
17
11
  process.stdout.write(s);
18
12
  }
@@ -28,6 +22,3 @@ export function logWarn(s) {
28
22
  export function logOk(s) {
29
23
  process.stdout.write(`${green("✓ ") + s}\n`);
30
24
  }
31
- export function logInfo(s) {
32
- process.stdout.write(`${cyan("ℹ ") + s}\n`);
33
- }
@@ -9,11 +9,12 @@ function versionRangeForHook(version) {
9
9
  return "latest";
10
10
  return major === 0 ? "0.x" : `^${major}.0.0`;
11
11
  }
12
+ export const AUTO_UPDATE_MIN_RELEASE_AGE_MINUTES = 0;
12
13
  function hookCommandFor(mode) {
13
14
  const v = getPackageVersion();
14
15
  const range = versionRangeForHook(v);
15
16
  const verb = mode === "auto" ? "update --yes --background" : "check";
16
- return `pnpm dlx --config.minimumReleaseAge=0 ${COMMAND_NAME}@${range} ${verb} --no-bounce`;
17
+ return `pnpm dlx --config.minimumReleaseAge=${AUTO_UPDATE_MIN_RELEASE_AGE_MINUTES} ${COMMAND_NAME}@${range} ${verb} --no-bounce`;
17
18
  }
18
19
  const HOOK_SIGNATURE = COMMAND_NAME;
19
20
  const HOOK_TIMEOUT_SECONDS = 120;
@@ -1,3 +1,4 @@
1
+ import { refreshAuth } from "../api.js";
1
2
  import { isAuthExpired, performBrowserLogin, readAuth, writeAuth } from "../auth.js";
2
3
  import { bold, dim, green, log, logError, logWarn } from "../colors.js";
3
4
  import { GATEWAY_URL, getAuthFilePath } from "../lib.js";
@@ -13,6 +14,13 @@ export async function runLogin(opts = {}) {
13
14
  log("");
14
15
  return 0;
15
16
  }
17
+ if (existing && (await refreshAuth())) {
18
+ const renewed = readAuth();
19
+ log(` ${green("✓")} Sesión renovada como ${bold(renewed?.email ?? existing.email)}.`);
20
+ log(dim(` Token: ${getAuthFilePath()}`));
21
+ log("");
22
+ return 0;
23
+ }
16
24
  if (existing) {
17
25
  logWarn(`Tu sesión (${existing.email}) expiró — reabro el login.`);
18
26
  }
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
- import { join } from "node:path";
3
+ import { dirname, join } from "node:path";
4
4
  import { AuthRequiredError, fetchManifest } from "../api.js";
5
5
  import { readAuth } from "../auth.js";
6
6
  import { bold, cyan, dim, gray, green, log, logError, logWarn, yellow } from "../colors.js";
@@ -15,7 +15,27 @@ function reportAutoUpdateLog() {
15
15
  const lines = readFileSync(logPath, "utf-8").trim().split("\n").filter(Boolean);
16
16
  if (lines.length === 0)
17
17
  return;
18
- logWarn(`El auto-update no pudo correr ${lines.length} vez/veces en un cliente GUI — última: "${lines[lines.length - 1]}". Agente: corré ${bold(`${COMMAND_NAME} update`)} (actualiza ya mismo y limpia este aviso); si reaparece, el cliente GUI no encuentra pnpm en su PATH y hay que reinstalar con ${bold(`${COMMAND_NAME} install`)} desde una terminal con pnpm disponible.`);
18
+ const last = lines[lines.length - 1];
19
+ if (/login no se completó/.test(last)) {
20
+ logWarn(`El auto-update necesita volver a iniciar sesión (el login automático no se completó ${lines.length} vez/veces) — última: "${last}". Agente: corré ${bold(`${COMMAND_NAME} update`)}, que abre el login y actualiza en el acto.`);
21
+ }
22
+ else {
23
+ logWarn(`El auto-update no pudo correr ${lines.length} vez/veces en un cliente GUI — última: "${last}". Agente: corré ${bold(`${COMMAND_NAME} update`)} (actualiza ya mismo y limpia este aviso); si reaparece, el cliente GUI no encuentra pnpm en su PATH y hay que reinstalar con ${bold(`${COMMAND_NAME} install`)} desde una terminal con pnpm disponible.`);
24
+ }
25
+ log("");
26
+ }
27
+ catch {
28
+ }
29
+ }
30
+ function reportSkillBackups() {
31
+ const backupsLog = join(dirname(getAutoUpdateLogPath()), "backups", "backups.log");
32
+ if (!existsSync(backupsLog))
33
+ return;
34
+ try {
35
+ const lines = readFileSync(backupsLog, "utf-8").trim().split("\n").filter(Boolean);
36
+ if (lines.length === 0)
37
+ return;
38
+ logWarn(`Se guardaron ${lines.length} backup(s) de skills que habías modificado antes de actualizarlas — último: "${lines[lines.length - 1]}". Están en ${dirname(backupsLog)}.`);
19
39
  log("");
20
40
  }
21
41
  catch {
@@ -288,6 +308,7 @@ export async function runStatus() {
288
308
  logWarn(`${scopeWarning} — los MCPs quedaron en scope usuario.`);
289
309
  log("");
290
310
  reportAutoUpdateLog();
311
+ reportSkillBackups();
291
312
  if (!auth) {
292
313
  logWarn(`Inicia sesión con: ${bold(`${COMMAND_NAME} login`)}`);
293
314
  log("");
@@ -1,6 +1,3 @@
1
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { join } from "node:path";
4
1
  import { removeAutoUpdate } from "../autoupdate.js";
5
2
  import { dim, gray, log, logOk, logWarn, red } from "../colors.js";
6
3
  import { uninstallAll } from "../installer.js";
@@ -37,7 +34,6 @@ export async function runUninstall(opts = {}) {
37
34
  if (!opts.dryRun) {
38
35
  for (const t of Object.keys(TARGETS))
39
36
  removeAutoUpdate(t);
40
- removeNpmrcCooldownExclude();
41
37
  }
42
38
  log("");
43
39
  if (opts.dryRun) {
@@ -48,15 +44,3 @@ export async function runUninstall(opts = {}) {
48
44
  }
49
45
  return 0;
50
46
  }
51
- function removeNpmrcCooldownExclude() {
52
- const npmrc = join(homedir(), ".npmrc");
53
- if (!existsSync(npmrc))
54
- return;
55
- const EXCLUDE = "minimum-release-age-exclude[]=alegra-data-agent";
56
- const lines = readFileSync(npmrc, "utf-8").split("\n");
57
- const kept = lines.filter((l) => l.trim() !== EXCLUDE);
58
- if (kept.length !== lines.length) {
59
- writeFileSync(npmrc, kept.join("\n"));
60
- logOk("Removido minimum-release-age-exclude de ~/.npmrc");
61
- }
62
- }
@@ -1,45 +1,94 @@
1
- import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
1
+ import { appendFileSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
- import { AuthRequiredError, fetchManifest } from "../api.js";
3
+ import { AuthRequiredError, ensureFreshAuth, fetchManifest } from "../api.js";
4
4
  import { hasValidAuth } from "../auth.js";
5
5
  import { refreshAutoUpdate } from "../autoupdate.js";
6
6
  import { cyan, dim, gray, green, log, logError, yellow } from "../colors.js";
7
7
  import { detectDrift, installAll, summarize } from "../installer.js";
8
- import { getAutoUpdateLogPath, getLastCheckPath } from "../lib.js";
8
+ import { COMMAND_NAME, getAutoUpdateLogPath, getLastCheckPath, getPackageVersion, } from "../lib.js";
9
9
  import { banner } from "../ui.js";
10
10
  import { runLogin } from "./login.js";
11
11
  const BACKGROUND_COOLDOWN_MS = 15 * 60_000;
12
- const BACKGROUND_LOGIN_TIMEOUT_MS = 90_000;
13
- function checkedRecently() {
12
+ const BACKGROUND_LOGIN_TIMEOUT_MS = (() => {
13
+ const v = Number(process.env.ALEGRA_DATA_AGENT_BG_LOGIN_TIMEOUT_MS);
14
+ return Number.isFinite(v) && v > 0 ? v : 90_000;
15
+ })();
16
+ export const BACKGROUND_AUTH_FAIL_COOLDOWN_MS = 60 * 60_000;
17
+ function readLastCheck() {
14
18
  try {
15
- const raw = readFileSync(getLastCheckPath(), "utf-8");
16
- const at = Date.parse(JSON.parse(raw).checkedAt ?? "");
17
- if (Number.isNaN(at))
18
- return false;
19
- const age = Date.now() - at;
20
- return age >= 0 && age < BACKGROUND_COOLDOWN_MS;
19
+ const parsed = JSON.parse(readFileSync(getLastCheckPath(), "utf-8"));
20
+ return parsed && typeof parsed === "object" ? parsed : {};
21
21
  }
22
22
  catch {
23
+ return {};
24
+ }
25
+ }
26
+ function withinWindow(iso, windowMs) {
27
+ const at = Date.parse(iso ?? "");
28
+ if (Number.isNaN(at))
23
29
  return false;
30
+ const age = Date.now() - at;
31
+ return age >= 0 && age < windowMs;
32
+ }
33
+ function checkedRecently() {
34
+ return withinWindow(readLastCheck().checkedAt, BACKGROUND_COOLDOWN_MS);
35
+ }
36
+ function authFailedRecently() {
37
+ return withinWindow(readLastCheck().authFailedAt, BACKGROUND_AUTH_FAIL_COOLDOWN_MS);
38
+ }
39
+ function writeLastCheck(data) {
40
+ try {
41
+ const path = getLastCheckPath();
42
+ mkdirSync(dirname(path), { recursive: true });
43
+ writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`);
44
+ }
45
+ catch {
24
46
  }
25
47
  }
26
48
  function markChecked() {
49
+ writeLastCheck({ checkedAt: new Date().toISOString() });
50
+ }
51
+ function markAuthFailed() {
52
+ writeLastCheck({ ...readLastCheck(), authFailedAt: new Date().toISOString() });
53
+ }
54
+ function appendHookLog(message) {
27
55
  try {
28
- const path = getLastCheckPath();
56
+ const path = getAutoUpdateLogPath();
29
57
  mkdirSync(dirname(path), { recursive: true });
30
- writeFileSync(path, `${JSON.stringify({ checkedAt: new Date().toISOString() }, null, 2)}\n`);
58
+ appendFileSync(path, `${new Date().toISOString()} ${message}\n`);
31
59
  }
32
60
  catch {
33
61
  }
34
62
  }
63
+ async function loginFromBackground(reason, force = false) {
64
+ const code = await runLogin({
65
+ timeoutMs: BACKGROUND_LOGIN_TIMEOUT_MS,
66
+ ...(force ? { force: true } : {}),
67
+ });
68
+ if (code === 0)
69
+ return true;
70
+ appendHookLog(`[update --background] ${reason} y el login no se completó. Se reintenta solo en la próxima sesión pasada 1 h (o al correr \`${COMMAND_NAME} update\`).`);
71
+ markAuthFailed();
72
+ return false;
73
+ }
74
+ function warnStaleFromBackground() {
75
+ log(yellow(` ⚠ ${COMMAND_NAME} v${getPackageVersion()}: no pude actualizar las skills porque la sesión venció. Agente: corré vos mismo \`pnpm dlx ${COMMAND_NAME} login\` y después \`pnpm dlx ${COMMAND_NAME} update\` — al usuario solo le toca el clic del navegador.`));
76
+ }
35
77
  export async function runUpdate(opts = {}) {
36
78
  if (opts.background && !opts.dryRun && checkedRecently())
37
79
  return 0;
80
+ const fresh = await ensureFreshAuth();
81
+ if (opts.background && !fresh) {
82
+ if (authFailedRecently() || !(await loginFromBackground("sesión vencida o ausente"))) {
83
+ warnStaleFromBackground();
84
+ return 0;
85
+ }
86
+ }
38
87
  banner();
39
88
  if (!hasValidAuth()) {
40
- const code = await runLogin(opts.background ? { timeoutMs: BACKGROUND_LOGIN_TIMEOUT_MS } : {});
89
+ const code = await runLogin({});
41
90
  if (code !== 0)
42
- return opts.background ? 0 : code;
91
+ return code;
43
92
  }
44
93
  let reauthed = false;
45
94
  for (;;) {
@@ -47,6 +96,14 @@ export async function runUpdate(opts = {}) {
47
96
  return await runUpdateOnce(opts);
48
97
  }
49
98
  catch (err) {
99
+ if (err instanceof AuthRequiredError && opts.background && !reauthed) {
100
+ reauthed = true;
101
+ if (!(await loginFromBackground("el gateway rechazó la sesión", true))) {
102
+ warnStaleFromBackground();
103
+ return 0;
104
+ }
105
+ continue;
106
+ }
50
107
  if (err instanceof AuthRequiredError && !reauthed) {
51
108
  reauthed = true;
52
109
  log(dim(" La sesión expiró durante la actualización; reabro el login y reintento.\n"));
@@ -56,6 +113,8 @@ export async function runUpdate(opts = {}) {
56
113
  continue;
57
114
  }
58
115
  if (err instanceof AuthRequiredError) {
116
+ if (opts.background)
117
+ return 0;
59
118
  logError(err.message);
60
119
  return 2;
61
120
  }