alegra-data-agent 0.2.0 → 0.2.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/dist/auth.js CHANGED
@@ -54,6 +54,31 @@ export function clearAuth() {
54
54
  export function hasAuth() {
55
55
  return readAuth() !== null;
56
56
  }
57
+ function decodeJwtExp(jwt) {
58
+ const parts = jwt.split(".");
59
+ if (parts.length !== 3)
60
+ return null;
61
+ try {
62
+ const padded = parts[1].replace(/-/g, "+").replace(/_/g, "/");
63
+ const payload = JSON.parse(Buffer.from(padded, "base64").toString("utf-8"));
64
+ return typeof payload.exp === "number" ? payload.exp : null;
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ export function isAuthExpired(record) {
71
+ if (!record)
72
+ return true;
73
+ const exp = decodeJwtExp(record.token);
74
+ if (exp === null)
75
+ return false;
76
+ return Date.now() >= exp * 1000 - 60_000;
77
+ }
78
+ export function hasValidAuth() {
79
+ const record = readAuth();
80
+ return record !== null && !isAuthExpired(record);
81
+ }
57
82
  export async function performBrowserLogin(timeoutMs = 5 * 60 * 1000) {
58
83
  const state = randomBytes(24).toString("hex");
59
84
  return new Promise((resolve, reject) => {
@@ -199,6 +224,7 @@ function openBrowser(url) {
199
224
  args: ["url.dll,FileProtocolHandler", url],
200
225
  });
201
226
  candidates.push({ cmd: "cmd.exe", args: ["/c", "start", '""', `"${url}"`] });
227
+ candidates.push({ cmd: "wslview", args: [url] });
202
228
  candidates.push({ cmd: "xdg-open", args: [url] });
203
229
  }
204
230
  else {
@@ -8,7 +8,7 @@ function hookCommandFor(mode) {
8
8
  const minor = parts[1] ?? "0";
9
9
  const range = `~${major}.${minor}.0`;
10
10
  const verb = mode === "auto" ? "update --yes" : "check";
11
- return `pnpm dlx ${COMMAND_NAME}@${range} ${verb}`;
11
+ return `pnpm dlx --config.minimumReleaseAge=0 ${COMMAND_NAME}@${range} ${verb}`;
12
12
  }
13
13
  const HOOK_SIGNATURE = COMMAND_NAME;
14
14
  function readSettings() {
@@ -1,5 +1,5 @@
1
1
  import { AuthRequiredError } from "../api.js";
2
- import { hasAuth, readAuth } from "../auth.js";
2
+ import { hasValidAuth, readAuth } from "../auth.js";
3
3
  import { bold, cyan, dim, gray, green, log, logError, red, yellow } from "../colors.js";
4
4
  import { installAll } from "../installer.js";
5
5
  import { COMMAND_NAME, formatBytes, getClaudeSkillsDir, getPackageVersion } from "../lib.js";
@@ -147,9 +147,9 @@ export async function runInstall(opts = {}) {
147
147
  const compact = isClaudeCode();
148
148
  if (!compact)
149
149
  fancyBanner();
150
- if (!hasAuth()) {
150
+ if (!hasValidAuth()) {
151
151
  if (!compact)
152
- log(dim(" No hay sesión activa. Inicio el flow de login antes de instalar.\n"));
152
+ log(dim(" No hay sesión activa (o expiró). Inicio el login antes de instalar.\n"));
153
153
  const code = await runLogin({});
154
154
  if (code !== 0)
155
155
  return code;
@@ -166,21 +166,13 @@ export async function runInstall(opts = {}) {
166
166
  if (!compact) {
167
167
  section("Descargando skills del gateway");
168
168
  }
169
- let result;
170
- try {
171
- result = await installAll({
172
- dryRun: opts.dryRun,
173
- force: opts.force,
174
- only: opts.only,
175
- targets: opts.targets,
176
- });
177
- }
178
- catch (err) {
179
- if (err instanceof AuthRequiredError) {
180
- logError(err.message);
181
- log(dim(` Corre: ${COMMAND_NAME} login`));
182
- return 2;
183
- }
169
+ const installOpts = {
170
+ dryRun: opts.dryRun,
171
+ force: opts.force,
172
+ only: opts.only,
173
+ targets: opts.targets,
174
+ };
175
+ const reportInstallError = (err) => {
184
176
  const msg = err instanceof Error ? err.message : String(err);
185
177
  logError(msg);
186
178
  if (msg.includes("EACCES") || msg.includes("permission denied")) {
@@ -195,6 +187,29 @@ export async function runInstall(opts = {}) {
195
187
  log(dim(" Verifica tu conexión a internet y el estado del gateway."));
196
188
  }
197
189
  return 1;
190
+ };
191
+ let result;
192
+ try {
193
+ result = await installAll(installOpts);
194
+ }
195
+ catch (err) {
196
+ if (!(err instanceof AuthRequiredError))
197
+ return reportInstallError(err);
198
+ if (!compact)
199
+ log(dim(" La sesión expiró durante la sincronización; reabro el login y reintento.\n"));
200
+ const code = await runLogin({ force: true });
201
+ if (code !== 0)
202
+ return code;
203
+ try {
204
+ result = await installAll(installOpts);
205
+ }
206
+ catch (err2) {
207
+ if (err2 instanceof AuthRequiredError) {
208
+ logError(err2.message);
209
+ return 2;
210
+ }
211
+ return reportInstallError(err2);
212
+ }
198
213
  }
199
214
  if ("error" in result) {
200
215
  logError(result.error);
@@ -1,18 +1,21 @@
1
- import { performBrowserLogin, readAuth, writeAuth } from "../auth.js";
2
- import { bold, dim, green, log, logError } from "../colors.js";
1
+ import { isAuthExpired, performBrowserLogin, readAuth, writeAuth } from "../auth.js";
2
+ import { bold, dim, green, log, logError, logWarn } from "../colors.js";
3
3
  import { GATEWAY_URL, getAuthFilePath } from "../lib.js";
4
4
  import { banner } from "../ui.js";
5
5
  export async function runLogin(opts = {}) {
6
6
  banner();
7
7
  if (!opts.force) {
8
8
  const existing = readAuth();
9
- if (existing) {
9
+ if (existing && !isAuthExpired(existing)) {
10
10
  log(` ${green("✓")} Ya estás autenticado como ${bold(existing.email)}.`);
11
11
  log(dim(` Token: ${getAuthFilePath()}`));
12
12
  log(dim(" Para volver a iniciar sesión: --force"));
13
13
  log("");
14
14
  return 0;
15
15
  }
16
+ if (existing) {
17
+ logWarn(`Tu sesión (${existing.email}) expiró — reabro el login.`);
18
+ }
16
19
  }
17
20
  log(` Gateway: ${dim(GATEWAY_URL)}`);
18
21
  try {
@@ -1,3 +1,6 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
1
4
  import { dim, gray, log, logOk, logWarn, red } from "../colors.js";
2
5
  import { uninstallAll } from "../installer.js";
3
6
  import { readLockFile } from "../lib.js";
@@ -22,7 +25,26 @@ export async function runUninstall(opts = {}) {
22
25
  }
23
26
  }
24
27
  const results = uninstallAll({ dryRun: opts.dryRun });
28
+ if (!opts.dryRun)
29
+ removeNpmrcCooldownExclude();
25
30
  log("");
26
- logOk(`${results.length} skill(s) eliminadas.`);
31
+ if (opts.dryRun) {
32
+ logOk(`${results.length} skill(s) se eliminarían (dry-run, nada borrado).`);
33
+ }
34
+ else {
35
+ logOk(`${results.length} skill(s) eliminadas.`);
36
+ }
27
37
  return 0;
28
38
  }
39
+ function removeNpmrcCooldownExclude() {
40
+ const npmrc = join(homedir(), ".npmrc");
41
+ if (!existsSync(npmrc))
42
+ return;
43
+ const EXCLUDE = "minimum-release-age-exclude[]=alegra-data-agent";
44
+ const lines = readFileSync(npmrc, "utf-8").split("\n");
45
+ const kept = lines.filter((l) => l.trim() !== EXCLUDE);
46
+ if (kept.length !== lines.length) {
47
+ writeFileSync(npmrc, kept.join("\n"));
48
+ logOk("Removido minimum-release-age-exclude de ~/.npmrc");
49
+ }
50
+ }
@@ -1,21 +1,41 @@
1
1
  import { AuthRequiredError, fetchManifest } from "../api.js";
2
+ import { hasValidAuth } from "../auth.js";
2
3
  import { cyan, dim, gray, green, log, logError, yellow } from "../colors.js";
3
4
  import { detectDrift, installAll, summarize } from "../installer.js";
4
5
  import { banner } from "../ui.js";
6
+ import { runLogin } from "./login.js";
5
7
  export async function runUpdate(opts = {}) {
6
8
  banner();
7
- let manifest;
8
- try {
9
- manifest = await fetchManifest();
9
+ if (!hasValidAuth()) {
10
+ const code = await runLogin({});
11
+ if (code !== 0)
12
+ return code;
10
13
  }
11
- catch (err) {
12
- if (err instanceof AuthRequiredError) {
13
- logError(err.message);
14
- return 2;
14
+ let reauthed = false;
15
+ for (;;) {
16
+ try {
17
+ return await runUpdateOnce(opts);
18
+ }
19
+ catch (err) {
20
+ if (err instanceof AuthRequiredError && !reauthed) {
21
+ reauthed = true;
22
+ log(dim(" La sesión expiró durante la actualización; reabro el login y reintento.\n"));
23
+ const code = await runLogin({ force: true });
24
+ if (code !== 0)
25
+ return code;
26
+ continue;
27
+ }
28
+ if (err instanceof AuthRequiredError) {
29
+ logError(err.message);
30
+ return 2;
31
+ }
32
+ logError(err instanceof Error ? err.message : String(err));
33
+ return 1;
15
34
  }
16
- logError(err instanceof Error ? err.message : String(err));
17
- return 1;
18
35
  }
36
+ }
37
+ async function runUpdateOnce(opts) {
38
+ const manifest = await fetchManifest();
19
39
  if (!manifest) {
20
40
  logError("No se pudo cargar el manifest.");
21
41
  return 1;
package/dist/installer.js CHANGED
@@ -4,7 +4,7 @@ import { dirname, join, resolve, sep } from "node:path";
4
4
  import { fetchManifest, fetchSkillFile } from "./api.js";
5
5
  import { dim, gray, green, logError, red, yellow } from "./colors.js";
6
6
  import { ensureDir, getLockFilePath, getPackageVersion, readLockFile, sha256File, writeLockFile, } from "./lib.js";
7
- import { renderMcpForTarget } from "./render.js";
7
+ import { inlineReferences, renderMcpForTarget } from "./render.js";
8
8
  import { TARGETS, detectInstalledTargets } from "./targets.js";
9
9
  const SKILL_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
10
10
  const MCP_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
@@ -32,6 +32,14 @@ function validateRelPath(rel) {
32
32
  }
33
33
  }
34
34
  }
35
+ function mcpConfigHash(mcp) {
36
+ const salient = JSON.stringify({
37
+ url: mcp.url,
38
+ type: mcp.auth.type,
39
+ tokenEnv: mcp.auth.tokenEnv ?? null,
40
+ });
41
+ return createHash("sha256").update(salient).digest("hex");
42
+ }
35
43
  function safeJoinSkillFile(skillRoot, rel) {
36
44
  const candidate = resolve(skillRoot, rel);
37
45
  const root = resolve(skillRoot);
@@ -95,17 +103,21 @@ function writeSkillToTarget(skillName, staged, spec) {
95
103
  if (!entry) {
96
104
  throw new Error(`Para target ${spec.id} se requiere SKILL.md o README.md en la skill`);
97
105
  }
106
+ const references = staged
107
+ .filter((f) => f.rel !== "SKILL.md" && f.rel !== "README.md")
108
+ .map((f) => ({ rel: f.rel, text: new TextDecoder().decode(f.data) }));
98
109
  const content = new TextDecoder().decode(entry.data);
99
- const rendered = renderSkillForTargetFromContent(content, skillName, spec.skillFormat);
110
+ const rendered = renderSkillForTargetFromContent(content, skillName, spec.skillFormat, references);
100
111
  ensureDir(spec.skillsDir);
101
112
  const dst = join(spec.skillsDir, rendered.filename);
102
113
  atomicWriteFile(dst, rendered.content);
103
114
  }
104
- function renderSkillForTargetFromContent(content, skillName, format) {
115
+ function renderSkillForTargetFromContent(content, skillName, format, references = []) {
105
116
  if (format === "folder") {
106
117
  throw new Error("renderSkillForTargetFromContent no soporta 'folder'");
107
118
  }
108
119
  const stripped = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "").replace(/^\s+/, "");
120
+ const body = stripped + inlineReferences(references);
109
121
  const fm = parseFrontmatterMinimal(content);
110
122
  if (format === "cursor-mdc") {
111
123
  const desc = fm.description ?? `Skill ${skillName}`;
@@ -113,10 +125,10 @@ function renderSkillForTargetFromContent(content, skillName, format) {
113
125
  const safeDesc = cursorDesc.replace(/\n/g, " ").replace(/"/g, '\\"');
114
126
  return {
115
127
  filename: `${skillName}.mdc`,
116
- content: `---\ndescription: "${safeDesc}"\nalwaysApply: false\n---\n\n${stripped}`,
128
+ content: `---\ndescription: "${safeDesc}"\nalwaysApply: false\n---\n\n${body}`,
117
129
  };
118
130
  }
119
- return { filename: `${skillName}.md`, content: stripped };
131
+ return { filename: `${skillName}.md`, content: body };
120
132
  }
121
133
  function parseFrontmatterMinimal(content) {
122
134
  const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
@@ -156,6 +168,10 @@ function readJsonOrEmpty(path) {
156
168
  }
157
169
  function registerMcpInTarget(mcp, spec) {
158
170
  validateMcpName(mcp.name);
171
+ if (spec.mcpMechanism === "toml-block") {
172
+ registerMcpInTomlBlock(mcp, spec);
173
+ return;
174
+ }
159
175
  const cfg = readJsonOrEmpty(spec.mcpConfigPath);
160
176
  const section = cfg[spec.mcpRootKey] ?? {};
161
177
  section[mcp.name] = renderMcpForTarget(mcp, spec);
@@ -164,6 +180,9 @@ function registerMcpInTarget(mcp, spec) {
164
180
  atomicWriteFile(spec.mcpConfigPath, `${JSON.stringify(cfg, null, 2)}\n`);
165
181
  }
166
182
  function unregisterMcpInTarget(mcpName, spec) {
183
+ if (spec.mcpMechanism === "toml-block") {
184
+ return unregisterMcpInTomlBlock(mcpName, spec);
185
+ }
167
186
  if (!existsSync(spec.mcpConfigPath))
168
187
  return false;
169
188
  const cfg = readJsonOrEmpty(spec.mcpConfigPath);
@@ -176,6 +195,77 @@ function unregisterMcpInTarget(mcpName, spec) {
176
195
  atomicWriteFile(spec.mcpConfigPath, `${JSON.stringify(cfg, null, 2)}\n`);
177
196
  return true;
178
197
  }
198
+ function tomlBasicString(s) {
199
+ return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
200
+ }
201
+ export function renderMcpToml(mcp) {
202
+ const lines = [`[mcp_servers.${mcp.name}]`, `url = ${tomlBasicString(mcp.url)}`];
203
+ if (mcp.auth.type === "gateway") {
204
+ if (!mcp.auth.tokenEnv) {
205
+ throw new Error(`MCP '${mcp.name}' es type=gateway pero no trae tokenEnv`);
206
+ }
207
+ lines.push(`bearer_token_env_var = ${tomlBasicString(mcp.auth.tokenEnv)}`);
208
+ }
209
+ return lines.join("\n");
210
+ }
211
+ function tomlMarkers(name) {
212
+ return {
213
+ begin: `# >>> alegra-data-agent:${name} >>>`,
214
+ end: `# <<< alegra-data-agent:${name} <<<`,
215
+ };
216
+ }
217
+ export function upsertTomlBlock(content, name, block) {
218
+ const { begin, end } = tomlMarkers(name);
219
+ const fenced = `${begin}\n${block}\n${end}`;
220
+ const bi = content.indexOf(begin);
221
+ if (bi !== -1) {
222
+ const ei = content.indexOf(end, bi);
223
+ if (ei !== -1) {
224
+ return content.slice(0, bi) + fenced + content.slice(ei + end.length);
225
+ }
226
+ }
227
+ if (content.length === 0)
228
+ return `${fenced}\n`;
229
+ const sep = content.endsWith("\n\n") ? "" : content.endsWith("\n") ? "\n" : "\n\n";
230
+ return `${content}${sep}${fenced}\n`;
231
+ }
232
+ export function removeTomlBlock(content, name) {
233
+ const { begin, end } = tomlMarkers(name);
234
+ const bi = content.indexOf(begin);
235
+ if (bi === -1)
236
+ return content;
237
+ const ei = content.indexOf(end, bi);
238
+ if (ei === -1)
239
+ return content;
240
+ let start = bi;
241
+ let stop = ei + end.length;
242
+ if (content[stop] === "\n")
243
+ stop++;
244
+ if (start > 0 && content[start - 1] === "\n")
245
+ start--;
246
+ return content.slice(0, start) + content.slice(stop);
247
+ }
248
+ function registerMcpInTomlBlock(mcp, spec) {
249
+ const existing = existsSync(spec.mcpConfigPath)
250
+ ? readFileSync(spec.mcpConfigPath, "utf-8")
251
+ : "";
252
+ const next = upsertTomlBlock(existing, mcp.name, renderMcpToml(mcp));
253
+ if (next === existing)
254
+ return;
255
+ backupIfExists(spec.mcpConfigPath);
256
+ atomicWriteFile(spec.mcpConfigPath, next);
257
+ }
258
+ function unregisterMcpInTomlBlock(name, spec) {
259
+ if (!existsSync(spec.mcpConfigPath))
260
+ return false;
261
+ const existing = readFileSync(spec.mcpConfigPath, "utf-8");
262
+ const next = removeTomlBlock(existing, name);
263
+ if (next === existing)
264
+ return false;
265
+ backupIfExists(spec.mcpConfigPath);
266
+ atomicWriteFile(spec.mcpConfigPath, next);
267
+ return true;
268
+ }
179
269
  function resolveTargets(opts) {
180
270
  const ids = opts.targets?.length ? opts.targets : detectInstalledTargets();
181
271
  if (ids.length === 0) {
@@ -274,7 +364,7 @@ export function installMcp(mcp, opts = {}) {
274
364
  const targetsMatch = prev?.targets &&
275
365
  prev.targets.length === targetIds.length &&
276
366
  prev.targets.every((t) => targetIds.includes(t));
277
- if (prev && targetsMatch && !opts.force) {
367
+ if (prev && targetsMatch && prev.configHash === mcpConfigHash(mcp) && !opts.force) {
278
368
  return { name: mcp.name, kind: "mcp", status: "unchanged", targets: targetIds };
279
369
  }
280
370
  if (opts.dryRun) {
@@ -293,6 +383,7 @@ export function installMcp(mcp, opts = {}) {
293
383
  name: mcp.name,
294
384
  installedFrom: getPackageVersion(),
295
385
  installedAt: new Date().toISOString(),
386
+ configHash: mcpConfigHash(mcp),
296
387
  targets: targetIds,
297
388
  };
298
389
  writeLockFile(lock);
@@ -399,8 +490,18 @@ export function uninstallMcp(name, opts = {}) {
399
490
  }
400
491
  const targetIds = (prev.targets ?? ["claude-code"]);
401
492
  if (!opts.dryRun) {
402
- for (const tId of targetIds) {
403
- unregisterMcpInTarget(name, TARGETS[tId]);
493
+ try {
494
+ for (const tId of targetIds) {
495
+ unregisterMcpInTarget(name, TARGETS[tId]);
496
+ }
497
+ }
498
+ catch (err) {
499
+ return {
500
+ name,
501
+ kind: "mcp",
502
+ status: "failed",
503
+ error: err instanceof Error ? err.message : String(err),
504
+ };
404
505
  }
405
506
  delete lock.mcps[name];
406
507
  writeLockFile(lock);
@@ -447,8 +548,11 @@ export function detectDrift(manifest) {
447
548
  };
448
549
  const manifestMcps = manifest.mcps ?? {};
449
550
  for (const name of Object.keys(manifestMcps)) {
450
- if (!lock.mcps?.[name])
551
+ const prevMcp = lock.mcps?.[name];
552
+ if (!prevMcp)
451
553
  mcps.toInstall.push(name);
554
+ else if (prevMcp.configHash !== mcpConfigHash(manifestMcps[name]))
555
+ mcps.toUpdate.push(name);
452
556
  else
453
557
  mcps.unchanged.push(name);
454
558
  }
package/dist/lib.js CHANGED
@@ -47,6 +47,14 @@ export function getAntigravitySkillsDir() {
47
47
  export function getAntigravitySettingsPath() {
48
48
  return join(homedir(), ".antigravity", "settings.json");
49
49
  }
50
+ export function getCodexSkillsDir() {
51
+ return join(homedir(), ".agents", "skills");
52
+ }
53
+ export function getCodexConfigPath() {
54
+ const codexHome = process.env.CODEX_HOME;
55
+ const base = codexHome && codexHome.length > 0 ? codexHome : join(homedir(), ".codex");
56
+ return join(base, "config.toml");
57
+ }
50
58
  export function getTargetSkillsDir(target) {
51
59
  switch (target) {
52
60
  case "claude-code":
@@ -57,6 +65,8 @@ export function getTargetSkillsDir(target) {
57
65
  return getOpenCodeSkillsDir();
58
66
  case "antigravity":
59
67
  return getAntigravitySkillsDir();
68
+ case "codex":
69
+ return getCodexSkillsDir();
60
70
  }
61
71
  }
62
72
  export function getTargetMcpConfigPath(target) {
@@ -69,6 +79,8 @@ export function getTargetMcpConfigPath(target) {
69
79
  return getOpenCodeConfigPath();
70
80
  case "antigravity":
71
81
  return getAntigravitySettingsPath();
82
+ case "codex":
83
+ return getCodexConfigPath();
72
84
  }
73
85
  }
74
86
  export function getLockFilePath() {
package/dist/render.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import { join } from "node:path";
2
+ import { extname, join } from "node:path";
3
3
  import { parseFrontmatter } from "./lib.js";
4
4
  function readSkillEntryPoint(skillDir) {
5
5
  const candidates = ["SKILL.md", "README.md"];
@@ -37,6 +37,39 @@ function stripFrontmatter(content) {
37
37
  return content;
38
38
  return m[1].replace(/^\s+/, "");
39
39
  }
40
+ const INLINE_TEXT_EXT = new Set([".md", ".sql", ".txt", ".json", ".yaml", ".yml", ".csv"]);
41
+ const INLINE_MAX_BYTES = 64 * 1024;
42
+ const FENCE_LANG = {
43
+ ".sql": "sql",
44
+ ".json": "json",
45
+ ".yaml": "yaml",
46
+ ".yml": "yaml",
47
+ ".csv": "csv",
48
+ ".txt": "",
49
+ };
50
+ export function inlineReferences(refs) {
51
+ const sections = [];
52
+ for (const ref of refs) {
53
+ const ext = extname(ref.rel).toLowerCase();
54
+ const bytes = Buffer.byteLength(ref.text, "utf-8");
55
+ if (!INLINE_TEXT_EXT.has(ext)) {
56
+ sections.push(`### \`${ref.rel}\`\n\n> (omitido: tipo \`${ext || "sin extensión"}\` no inlineable)`);
57
+ }
58
+ else if (bytes > INLINE_MAX_BYTES) {
59
+ sections.push(`### \`${ref.rel}\`\n\n> (omitido: ${Math.round(bytes / 1024)} KB; supera el límite de inline)`);
60
+ }
61
+ else if (ext === ".md") {
62
+ sections.push(`### \`${ref.rel}\`\n\n${ref.text.trim()}`);
63
+ }
64
+ else {
65
+ const lang = FENCE_LANG[ext] ?? "";
66
+ sections.push(`### \`${ref.rel}\`\n\n\`\`\`${lang}\n${ref.text.replace(/\s+$/, "")}\n\`\`\``);
67
+ }
68
+ }
69
+ if (sections.length === 0)
70
+ return "";
71
+ return `\n\n---\n\n# Referencias (incluidas inline)\n\n> Este cliente instala la skill como un solo archivo; el contenido de \`references/\` se incluye acá para que las menciones del cuerpo sigan disponibles.\n\n${sections.join("\n\n")}\n`;
72
+ }
40
73
  export function renderMcpForTarget(mcp, spec) {
41
74
  const obj = {
42
75
  [spec.mcpUrlKey]: mcp.url,
@@ -64,5 +97,7 @@ function bearerInterpolation(env, target) {
64
97
  return `Bearer \${env:${env}}`;
65
98
  case "opencode":
66
99
  return `Bearer {env:${env}}`;
100
+ case "codex":
101
+ throw new Error("codex usa config TOML (bearer_token_env_var), no headers JSON");
67
102
  }
68
103
  }
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 { getAntigravitySettingsPath, getAntigravitySkillsDir, getClaudeSettingsPath, getClaudeSkillsDir, getCursorMcpPath, getCursorSkillsDir, getOpenCodeConfigPath, getOpenCodeSkillsDir, } from "./lib.js";
4
+ import { getAntigravitySettingsPath, getAntigravitySkillsDir, getClaudeSettingsPath, getClaudeSkillsDir, getCodexConfigPath, getCodexSkillsDir, getCursorMcpPath, getCursorSkillsDir, getOpenCodeConfigPath, getOpenCodeSkillsDir, } from "./lib.js";
5
5
  export const TARGETS = {
6
6
  "claude-code": {
7
7
  id: "claude-code",
@@ -40,17 +40,31 @@ export const TARGETS = {
40
40
  mcpRootKey: "mcpServers",
41
41
  mcpUrlKey: "serverUrl",
42
42
  },
43
+ codex: {
44
+ id: "codex",
45
+ label: "Codex",
46
+ skillsDir: getCodexSkillsDir(),
47
+ skillFormat: "folder",
48
+ mcpConfigPath: getCodexConfigPath(),
49
+ mcpRootKey: "mcpServers",
50
+ mcpUrlKey: "url",
51
+ mcpMechanism: "toml-block",
52
+ },
43
53
  };
44
54
  export function detectInstalledTargets() {
45
55
  const home = homedir();
46
56
  const xdgConfig = process.env.XDG_CONFIG_HOME && process.env.XDG_CONFIG_HOME.length > 0
47
57
  ? process.env.XDG_CONFIG_HOME
48
58
  : join(home, ".config");
59
+ const codexHome = process.env.CODEX_HOME && process.env.CODEX_HOME.length > 0
60
+ ? process.env.CODEX_HOME
61
+ : join(home, ".codex");
49
62
  const sentinels = {
50
63
  "claude-code": join(home, ".claude"),
51
64
  cursor: join(home, ".cursor"),
52
65
  opencode: join(xdgConfig, "opencode"),
53
66
  antigravity: join(home, ".antigravity"),
67
+ codex: codexHome,
54
68
  };
55
69
  const out = [];
56
70
  for (const id of Object.keys(sentinels)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alegra-data-agent",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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",