alegra-data-agent 0.1.0

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/lib.js ADDED
@@ -0,0 +1,221 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync, } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join, relative, sep } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ export const PRODUCT_NAME = "alegra-data-agent";
7
+ export const COMMAND_NAME = "alegra-data-agent";
8
+ export const REPO_SLUG = "alegradev/alegra-data-agent";
9
+ export const REPO_DEFAULT_BRANCH = "main";
10
+ export const GATEWAY_URL = process.env.ALEGRA_DATA_AGENT_GATEWAY_URL ??
11
+ "https://alegra-data-agent.vercel.app";
12
+ export const ALL_TARGETS = ["claude-code", "cursor", "opencode", "antigravity"];
13
+ const __dirname = dirname(fileURLToPath(import.meta.url));
14
+ export function getPackageRoot() {
15
+ return join(__dirname, "..");
16
+ }
17
+ export function getRegistryDir() {
18
+ return join(getPackageRoot(), "skills-registry");
19
+ }
20
+ export function getManifestPath() {
21
+ return join(getRegistryDir(), "index.json");
22
+ }
23
+ export function getClaudeSkillsDir() {
24
+ return join(homedir(), ".claude", "skills");
25
+ }
26
+ export function getClaudeSettingsPath() {
27
+ return join(homedir(), ".claude", "settings.json");
28
+ }
29
+ export function getCursorSkillsDir() {
30
+ return join(homedir(), ".cursor", "rules");
31
+ }
32
+ export function getCursorMcpPath() {
33
+ return join(homedir(), ".cursor", "mcp.json");
34
+ }
35
+ export function getOpenCodeSkillsDir() {
36
+ const xdgHome = process.env.XDG_CONFIG_HOME;
37
+ const base = xdgHome && xdgHome.length > 0 ? xdgHome : join(homedir(), ".config");
38
+ return join(base, "opencode", "prompts");
39
+ }
40
+ export function getOpenCodeConfigPath() {
41
+ const xdgHome = process.env.XDG_CONFIG_HOME;
42
+ const base = xdgHome && xdgHome.length > 0 ? xdgHome : join(homedir(), ".config");
43
+ return join(base, "opencode", "opencode.json");
44
+ }
45
+ export function getAntigravitySkillsDir() {
46
+ return join(homedir(), ".antigravity", "skills");
47
+ }
48
+ export function getAntigravitySettingsPath() {
49
+ return join(homedir(), ".antigravity", "settings.json");
50
+ }
51
+ export function getTargetSkillsDir(target) {
52
+ switch (target) {
53
+ case "claude-code":
54
+ return getClaudeSkillsDir();
55
+ case "cursor":
56
+ return getCursorSkillsDir();
57
+ case "opencode":
58
+ return getOpenCodeSkillsDir();
59
+ case "antigravity":
60
+ return getAntigravitySkillsDir();
61
+ }
62
+ }
63
+ export function getTargetMcpConfigPath(target) {
64
+ switch (target) {
65
+ case "claude-code":
66
+ return getClaudeSettingsPath();
67
+ case "cursor":
68
+ return getCursorMcpPath();
69
+ case "opencode":
70
+ return getOpenCodeConfigPath();
71
+ case "antigravity":
72
+ return getAntigravitySettingsPath();
73
+ }
74
+ }
75
+ export function getLockFilePath() {
76
+ return join(getConfigDir(), "lock.json");
77
+ }
78
+ export function getConfigDir() {
79
+ const xdgHome = process.env.XDG_CONFIG_HOME;
80
+ const base = xdgHome && xdgHome.length > 0 ? xdgHome : join(homedir(), ".config");
81
+ return join(base, PRODUCT_NAME);
82
+ }
83
+ export function getAuthFilePath() {
84
+ return join(getConfigDir(), "auth.json");
85
+ }
86
+ let _cachedVersion = null;
87
+ export function getPackageVersion() {
88
+ if (_cachedVersion)
89
+ return _cachedVersion;
90
+ try {
91
+ const pkg = JSON.parse(readFileSync(join(getPackageRoot(), "package.json"), "utf-8"));
92
+ _cachedVersion = pkg.version ?? "0.0.0";
93
+ }
94
+ catch {
95
+ _cachedVersion = "0.0.0";
96
+ }
97
+ return _cachedVersion;
98
+ }
99
+ export function readManifest() {
100
+ const path = getManifestPath();
101
+ if (!existsSync(path))
102
+ return null;
103
+ try {
104
+ return JSON.parse(readFileSync(path, "utf-8"));
105
+ }
106
+ catch {
107
+ return null;
108
+ }
109
+ }
110
+ export function readLockFile() {
111
+ const path = getLockFilePath();
112
+ if (!existsSync(path)) {
113
+ return { schemaVersion: 1, productName: PRODUCT_NAME, skills: {} };
114
+ }
115
+ try {
116
+ return JSON.parse(readFileSync(path, "utf-8"));
117
+ }
118
+ catch {
119
+ return { schemaVersion: 1, productName: PRODUCT_NAME, skills: {} };
120
+ }
121
+ }
122
+ export function writeLockFile(lock) {
123
+ const path = getLockFilePath();
124
+ mkdirSync(dirname(path), { recursive: true });
125
+ writeFileSync(path, JSON.stringify(lock, null, 2) + "\n", "utf-8");
126
+ }
127
+ export function sha256(buf) {
128
+ return createHash("sha256").update(buf).digest("hex");
129
+ }
130
+ export function sha256File(path) {
131
+ return sha256(readFileSync(path));
132
+ }
133
+ export function computeBundleHash(files) {
134
+ const lines = Object.keys(files)
135
+ .sort()
136
+ .map((k) => `${k}:${files[k]}`)
137
+ .join("\n");
138
+ return sha256(lines);
139
+ }
140
+ export function walkSkillFiles(skillDir) {
141
+ const out = [];
142
+ const walk = (dir) => {
143
+ for (const entry of readdirSync(dir)) {
144
+ const full = join(dir, entry);
145
+ const st = statSync(full);
146
+ if (st.isDirectory()) {
147
+ walk(full);
148
+ }
149
+ else if (st.isFile()) {
150
+ const rel = relative(skillDir, full).split(sep).join("/");
151
+ out.push(rel);
152
+ }
153
+ }
154
+ };
155
+ walk(skillDir);
156
+ return out.sort();
157
+ }
158
+ export function parseFrontmatter(markdown) {
159
+ const match = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---/);
160
+ if (!match)
161
+ return {};
162
+ const result = {};
163
+ const lines = match[1].split(/\r?\n/);
164
+ let state = { kind: "top" };
165
+ const finalizeBlock = (s) => {
166
+ const joined = s.mode === "folded" ? s.lines.join(" ").replace(/\s+/g, " ").trim() : s.lines.join("\n");
167
+ result[s.key] = joined;
168
+ };
169
+ for (let i = 0; i < lines.length; i++) {
170
+ const line = lines[i];
171
+ if (state.kind === "block") {
172
+ if (/^\s+/.test(line) || line === "") {
173
+ state.lines.push(line.replace(/^ {2}/, ""));
174
+ continue;
175
+ }
176
+ finalizeBlock(state);
177
+ state = { kind: "top" };
178
+ }
179
+ if (state.kind === "map") {
180
+ if (/^\s+/.test(line)) {
181
+ const sub = line.trim().match(/^([a-zA-Z_-]+):\s*(.*)$/);
182
+ if (sub) {
183
+ const [, k, v] = sub;
184
+ result[k] = v.replace(/^["']|["']$/g, "");
185
+ }
186
+ continue;
187
+ }
188
+ state = { kind: "top" };
189
+ }
190
+ const kv = line.match(/^([a-zA-Z_-]+):\s*(.*)$/);
191
+ if (!kv)
192
+ continue;
193
+ const [, k, v] = kv;
194
+ if (v === ">" || v === ">-" || v === ">+") {
195
+ state = { kind: "block", key: k, mode: "folded", lines: [] };
196
+ }
197
+ else if (v === "|" || v === "|-" || v === "|+") {
198
+ state = { kind: "block", key: k, mode: "literal", lines: [] };
199
+ }
200
+ else if (v === "") {
201
+ state = { kind: "map", key: k };
202
+ }
203
+ else {
204
+ result[k] = v.replace(/^["']|["']$/g, "");
205
+ }
206
+ }
207
+ if (state.kind === "block")
208
+ finalizeBlock(state);
209
+ return result;
210
+ }
211
+ export function formatBytes(n) {
212
+ if (n < 1024)
213
+ return `${n} B`;
214
+ if (n < 1024 * 1024)
215
+ return `${(n / 1024).toFixed(1)} KB`;
216
+ return `${(n / (1024 * 1024)).toFixed(1)} MB`;
217
+ }
218
+ export function ensureDir(path) {
219
+ if (!existsSync(path))
220
+ mkdirSync(path, { recursive: true });
221
+ }
package/dist/main.js ADDED
@@ -0,0 +1,162 @@
1
+ import { ALL_TARGETS, getPackageVersion } from "./lib.js";
2
+ import { helpText } from "./ui.js";
3
+ import { log, logError } from "./colors.js";
4
+ import { runCheck } from "./commands/check.js";
5
+ import { runHookInstall, runHookUninstall } from "./commands/hook.js";
6
+ import { runInstall } from "./commands/install.js";
7
+ import { runList } from "./commands/list.js";
8
+ import { runLogin } from "./commands/login.js";
9
+ import { runLogout } from "./commands/logout.js";
10
+ import { runStatus } from "./commands/status.js";
11
+ import { runUninstall } from "./commands/uninstall.js";
12
+ import { runUpdate } from "./commands/update.js";
13
+ import { runWhoami } from "./commands/whoami.js";
14
+ function parseTargetList(raw) {
15
+ const out = [];
16
+ for (const piece of raw.split(",").map((s) => s.trim()).filter(Boolean)) {
17
+ if (ALL_TARGETS.includes(piece)) {
18
+ out.push(piece);
19
+ }
20
+ else {
21
+ throw new Error(`Target desconocido: '${piece}'. Válidos: ${ALL_TARGETS.join(", ")}`);
22
+ }
23
+ }
24
+ return out;
25
+ }
26
+ function parseArgs(argv) {
27
+ const flags = {
28
+ yes: false,
29
+ dryRun: false,
30
+ force: false,
31
+ help: false,
32
+ version: false,
33
+ noHook: false,
34
+ autoUpdate: false,
35
+ };
36
+ const positional = [];
37
+ const only = [];
38
+ const targets = [];
39
+ let i = 0;
40
+ while (i < argv.length) {
41
+ const a = argv[i];
42
+ if (a === "-y" || a === "--yes")
43
+ flags.yes = true;
44
+ else if (a === "--dry-run")
45
+ flags.dryRun = true;
46
+ else if (a === "--force")
47
+ flags.force = true;
48
+ else if (a === "--no-hook")
49
+ flags.noHook = true;
50
+ else if (a === "--auto-update")
51
+ flags.autoUpdate = true;
52
+ else if (a === "-h" || a === "--help")
53
+ flags.help = true;
54
+ else if (a === "-v" || a === "--version")
55
+ flags.version = true;
56
+ else if (a === "--only") {
57
+ i++;
58
+ if (argv[i])
59
+ only.push(...argv[i].split(","));
60
+ }
61
+ else if (a.startsWith("--only=")) {
62
+ only.push(...a.slice("--only=".length).split(","));
63
+ }
64
+ else if (a === "--target") {
65
+ i++;
66
+ if (argv[i])
67
+ targets.push(...parseTargetList(argv[i]));
68
+ }
69
+ else if (a.startsWith("--target=")) {
70
+ targets.push(...parseTargetList(a.slice("--target=".length)));
71
+ }
72
+ else {
73
+ positional.push(a);
74
+ }
75
+ i++;
76
+ }
77
+ return {
78
+ command: positional[0] ?? "install",
79
+ subcommand: positional[1],
80
+ flags,
81
+ only,
82
+ targets,
83
+ };
84
+ }
85
+ async function main() {
86
+ const args = parseArgs(process.argv.slice(2));
87
+ if (args.flags.version) {
88
+ log(getPackageVersion());
89
+ process.exit(0);
90
+ }
91
+ if (args.flags.help || args.command === "help") {
92
+ log(helpText());
93
+ process.exit(0);
94
+ }
95
+ let exitCode = 0;
96
+ switch (args.command) {
97
+ case "login":
98
+ exitCode = await runLogin({ force: args.flags.force });
99
+ break;
100
+ case "logout":
101
+ exitCode = runLogout();
102
+ break;
103
+ case "whoami":
104
+ exitCode = runWhoami();
105
+ break;
106
+ case "install":
107
+ exitCode = await runInstall({
108
+ yes: args.flags.yes,
109
+ dryRun: args.flags.dryRun,
110
+ force: args.flags.force,
111
+ only: args.only.length > 0 ? args.only : undefined,
112
+ targets: args.targets.length > 0 ? args.targets : undefined,
113
+ noHook: args.flags.noHook,
114
+ hookMode: args.flags.autoUpdate ? "auto" : "check",
115
+ });
116
+ break;
117
+ case "update":
118
+ exitCode = await runUpdate({
119
+ yes: args.flags.yes,
120
+ dryRun: args.flags.dryRun,
121
+ targets: args.targets.length > 0 ? args.targets : undefined,
122
+ });
123
+ break;
124
+ case "list":
125
+ case "ls":
126
+ exitCode = await runList();
127
+ break;
128
+ case "status":
129
+ exitCode = await runStatus();
130
+ break;
131
+ case "check":
132
+ exitCode = await runCheck();
133
+ break;
134
+ case "uninstall":
135
+ exitCode = await runUninstall({ yes: args.flags.yes, dryRun: args.flags.dryRun });
136
+ break;
137
+ case "hook":
138
+ if (args.subcommand === "install") {
139
+ exitCode = runHookInstall(args.flags.autoUpdate ? "auto" : "check");
140
+ }
141
+ else if (args.subcommand === "uninstall") {
142
+ exitCode = runHookUninstall();
143
+ }
144
+ else {
145
+ logError(`Subcomando desconocido: 'hook ${args.subcommand ?? ""}'. Usa: hook install [--auto-update] | hook uninstall`);
146
+ exitCode = 1;
147
+ }
148
+ break;
149
+ case "version":
150
+ log(getPackageVersion());
151
+ break;
152
+ default:
153
+ logError(`Comando desconocido: '${args.command}'.`);
154
+ log(helpText());
155
+ exitCode = 1;
156
+ }
157
+ process.exit(exitCode);
158
+ }
159
+ main().catch((err) => {
160
+ logError(err instanceof Error ? err.message : String(err));
161
+ process.exit(1);
162
+ });
package/dist/render.js ADDED
@@ -0,0 +1,68 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { parseFrontmatter } from "./lib.js";
4
+ function readSkillEntryPoint(skillDir) {
5
+ const candidates = ["SKILL.md", "README.md"];
6
+ for (const c of candidates) {
7
+ const p = join(skillDir, c);
8
+ if (existsSync(p)) {
9
+ return { content: readFileSync(p, "utf-8"), path: p };
10
+ }
11
+ }
12
+ throw new Error(`No SKILL.md ni README.md en ${skillDir}`);
13
+ }
14
+ export function renderSkillForTarget(skillSrcDir, skillName, format) {
15
+ if (format === "folder") {
16
+ return { kind: "folder" };
17
+ }
18
+ const { content } = readSkillEntryPoint(skillSrcDir);
19
+ const fm = parseFrontmatter(content);
20
+ const body = stripFrontmatter(content);
21
+ if (format === "cursor-mdc") {
22
+ const desc = fm.description ?? `Skill ${skillName}`;
23
+ const cursorDesc = desc.startsWith("USE WHEN") ? desc : `USE WHEN ${desc}`;
24
+ const safeDesc = cursorDesc.replace(/\n/g, " ").replace(/"/g, '\\"');
25
+ const mdc = `---\ndescription: "${safeDesc}"\nalwaysApply: false\n---\n\n${body}`;
26
+ return { kind: "single", filename: `${skillName}.mdc`, content: mdc };
27
+ }
28
+ if (format === "flat-md") {
29
+ return { kind: "single", filename: `${skillName}.md`, content: body };
30
+ }
31
+ const _exhaustive = format;
32
+ throw new Error(`SkillFormat no soportado: ${String(_exhaustive)}`);
33
+ }
34
+ function stripFrontmatter(content) {
35
+ const m = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n([\s\S]*)$/);
36
+ if (!m)
37
+ return content;
38
+ return m[1].replace(/^\s+/, "");
39
+ }
40
+ export function renderMcpForTarget(mcp, spec) {
41
+ const obj = {
42
+ [spec.mcpUrlKey]: mcp.url,
43
+ };
44
+ if (spec.opencodeShape) {
45
+ obj.type = "remote";
46
+ obj.enabled = true;
47
+ }
48
+ if (mcp.auth.type === "gateway") {
49
+ if (!mcp.auth.tokenEnv) {
50
+ throw new Error(`MCP '${mcp.name}' es type=gateway pero no trae tokenEnv`);
51
+ }
52
+ obj.headers = {
53
+ Authorization: bearerInterpolation(mcp.auth.tokenEnv, spec.id),
54
+ };
55
+ }
56
+ return obj;
57
+ }
58
+ function bearerInterpolation(env, target) {
59
+ switch (target) {
60
+ case "claude-code":
61
+ case "antigravity":
62
+ return `Bearer \${${env}}`;
63
+ case "cursor":
64
+ return `Bearer \${env:${env}}`;
65
+ case "opencode":
66
+ return `Bearer {env:${env}}`;
67
+ }
68
+ }
@@ -0,0 +1,61 @@
1
+ import { createPublicKey, verify } from "node:crypto";
2
+ export const SIGNING_KEY_ID = "alegra-data-agent-v1";
3
+ export const PUBLIC_KEY_B64 = "MCowBQYDK2VwAyEApoojQPy6INQXl/xZ0gQumsI3tt9HvMrb0PaVlNn7TkE=";
4
+ export function canonicalize(obj) {
5
+ if (obj === null || typeof obj !== "object")
6
+ return JSON.stringify(obj);
7
+ if (Array.isArray(obj)) {
8
+ return "[" + obj.map((v) => canonicalize(v)).join(",") + "]";
9
+ }
10
+ const o = obj;
11
+ const keys = Object.keys(o).filter((k) => k !== "signature").sort();
12
+ return "{" + keys.map((k) => JSON.stringify(k) + ":" + canonicalize(o[k])).join(",") + "}";
13
+ }
14
+ const MAX_MANIFEST_AGE_DAYS = 90;
15
+ export function verifyManifestSignature(manifest, options = {}) {
16
+ const sig = manifest.signature;
17
+ if (!sig) {
18
+ if (options.softFail) {
19
+ return { ok: true, warning: "manifest no firmado (modo transición)" };
20
+ }
21
+ return { ok: false, reason: "manifest no incluye signature" };
22
+ }
23
+ if (sig.alg !== "Ed25519") {
24
+ return { ok: false, reason: `algoritmo no soportado: ${sig.alg}` };
25
+ }
26
+ if (sig.keyId !== SIGNING_KEY_ID) {
27
+ return { ok: false, reason: `keyId no reconocido: ${sig.keyId} (CLI espera ${SIGNING_KEY_ID})` };
28
+ }
29
+ if (typeof manifest.generatedAt === "string") {
30
+ const ts = Date.parse(manifest.generatedAt);
31
+ if (!isNaN(ts)) {
32
+ const ageMs = Date.now() - ts;
33
+ const maxAgeMs = MAX_MANIFEST_AGE_DAYS * 24 * 3600 * 1000;
34
+ if (ageMs > maxAgeMs) {
35
+ return {
36
+ ok: false,
37
+ reason: `manifest demasiado viejo (${Math.round(ageMs / (24 * 3600 * 1000))}d, máx ${MAX_MANIFEST_AGE_DAYS}d)`,
38
+ };
39
+ }
40
+ if (ageMs < -3600_000) {
41
+ return { ok: false, reason: "manifest tiene generatedAt en el futuro" };
42
+ }
43
+ }
44
+ }
45
+ const canonical = canonicalize(manifest);
46
+ const publicKey = createPublicKey({
47
+ key: Buffer.from(PUBLIC_KEY_B64, "base64"),
48
+ format: "der",
49
+ type: "spki",
50
+ });
51
+ if (publicKey.asymmetricKeyType !== "ed25519") {
52
+ return {
53
+ ok: false,
54
+ reason: `tipo de clave pública no es Ed25519 (got: ${publicKey.asymmetricKeyType})`,
55
+ };
56
+ }
57
+ const ok = verify(null, Buffer.from(canonical, "utf-8"), publicKey, Buffer.from(sig.value, "base64"));
58
+ if (!ok)
59
+ return { ok: false, reason: "firma criptográficamente inválida" };
60
+ return { ok: true };
61
+ }
@@ -0,0 +1,61 @@
1
+ import { existsSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { getAntigravitySettingsPath, getAntigravitySkillsDir, getClaudeSettingsPath, getClaudeSkillsDir, getCursorMcpPath, getCursorSkillsDir, getOpenCodeConfigPath, getOpenCodeSkillsDir, } from "./lib.js";
5
+ export const TARGETS = {
6
+ "claude-code": {
7
+ id: "claude-code",
8
+ label: "Claude Code",
9
+ skillsDir: getClaudeSkillsDir(),
10
+ skillFormat: "folder",
11
+ mcpConfigPath: getClaudeSettingsPath(),
12
+ mcpRootKey: "mcpServers",
13
+ mcpUrlKey: "url",
14
+ },
15
+ cursor: {
16
+ id: "cursor",
17
+ label: "Cursor",
18
+ skillsDir: getCursorSkillsDir(),
19
+ skillFormat: "cursor-mdc",
20
+ mcpConfigPath: getCursorMcpPath(),
21
+ mcpRootKey: "mcpServers",
22
+ mcpUrlKey: "url",
23
+ },
24
+ opencode: {
25
+ id: "opencode",
26
+ label: "OpenCode",
27
+ skillsDir: getOpenCodeSkillsDir(),
28
+ skillFormat: "flat-md",
29
+ mcpConfigPath: getOpenCodeConfigPath(),
30
+ mcpRootKey: "mcp",
31
+ mcpUrlKey: "url",
32
+ opencodeShape: true,
33
+ },
34
+ antigravity: {
35
+ id: "antigravity",
36
+ label: "Antigravity",
37
+ skillsDir: getAntigravitySkillsDir(),
38
+ skillFormat: "folder",
39
+ mcpConfigPath: getAntigravitySettingsPath(),
40
+ mcpRootKey: "mcpServers",
41
+ mcpUrlKey: "serverUrl",
42
+ },
43
+ };
44
+ export function detectInstalledTargets() {
45
+ const home = homedir();
46
+ const xdgConfig = process.env.XDG_CONFIG_HOME && process.env.XDG_CONFIG_HOME.length > 0
47
+ ? process.env.XDG_CONFIG_HOME
48
+ : join(home, ".config");
49
+ const sentinels = {
50
+ "claude-code": join(home, ".claude"),
51
+ cursor: join(home, ".cursor"),
52
+ opencode: join(xdgConfig, "opencode"),
53
+ antigravity: join(home, ".antigravity"),
54
+ };
55
+ const out = [];
56
+ for (const id of Object.keys(sentinels)) {
57
+ if (existsSync(sentinels[id]))
58
+ out.push(id);
59
+ }
60
+ return out;
61
+ }
package/dist/ui.js ADDED
@@ -0,0 +1,71 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { stdin as input, stdout as output } from "node:process";
3
+ import { bold, cyan, dim, gray, log } from "./colors.js";
4
+ import { COMMAND_NAME, getPackageVersion } from "./lib.js";
5
+ export function banner() {
6
+ const v = getPackageVersion();
7
+ log("");
8
+ log(bold(cyan(` ${COMMAND_NAME}`)) + dim(` v${v}`));
9
+ log(gray(" Agente de Data — equipo BI Data · Alegra"));
10
+ log("");
11
+ }
12
+ export async function confirm(question, defaultYes = true) {
13
+ const suffix = defaultYes ? "[Y/n]" : "[y/N]";
14
+ const rl = createInterface({ input, output });
15
+ try {
16
+ const answer = (await rl.question(`${question} ${dim(suffix)} `)).trim().toLowerCase();
17
+ if (answer === "")
18
+ return defaultYes;
19
+ return answer === "y" || answer === "yes" || answer === "si" || answer === "sí";
20
+ }
21
+ finally {
22
+ rl.close();
23
+ }
24
+ }
25
+ export async function ask(question, defaultValue) {
26
+ const rl = createInterface({ input, output });
27
+ try {
28
+ const suffix = defaultValue ? dim(`(${defaultValue})`) + " " : "";
29
+ const answer = (await rl.question(`${question} ${suffix}`)).trim();
30
+ return answer === "" && defaultValue !== undefined ? defaultValue : answer;
31
+ }
32
+ finally {
33
+ rl.close();
34
+ }
35
+ }
36
+ export function helpText() {
37
+ return `
38
+ ${bold(COMMAND_NAME)} — Agente de Data interno de Alegra
39
+
40
+ ${bold("Uso:")}
41
+ pnpm dlx ${COMMAND_NAME} <comando> [opciones]
42
+
43
+ ${bold("Comandos:")}
44
+ login Autenticarte con tu cuenta @alegra.com
45
+ logout Cerrar sesión (borra el token local)
46
+ whoami Mostrar la cuenta actualmente autenticada
47
+ install Instala skills + MCPs en tus clientes (Claude Code, Cursor, OpenCode, Antigravity)
48
+ update Re-descarga skills/MCPs que cambiaron
49
+ list Lista skills y MCPs disponibles en el gateway
50
+ status Muestra estado: auth, instalado, drift
51
+ check Compara local vs gateway (usado por el hook SessionStart)
52
+ hook install Habilita aviso automático de actualizaciones al abrir el cliente
53
+ hook uninstall Deshabilita el aviso automático
54
+ uninstall Elimina las skills/MCPs instaladas por este paquete
55
+ help Muestra esta ayuda
56
+ version Muestra la versión
57
+
58
+ ${bold("Opciones:")}
59
+ -y, --yes No pedir confirmación
60
+ --dry-run Mostrar qué se haría, sin tocar archivos
61
+ --only=a,b,c Instalar solo esas skills/MCPs
62
+ --target=NAME Restringir al cliente NAME (claude-code|cursor|opencode|antigravity)
63
+ --no-hook No instalar el hook de SessionStart
64
+ --auto-update Hook en modo auto-update (instala en silencio)
65
+ -h, --help Mostrar ayuda
66
+ -v, --version Mostrar versión
67
+
68
+ ${bold("Documentación:")}
69
+ https://github.com/alegradev/alegra-data-agent
70
+ `;
71
+ }