nexakit 0.1.0 → 0.2.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/.claude-plugin/marketplace.json +18 -1
- package/README.md +40 -30
- package/assets/brand/README.md +0 -2
- package/assets/runtime/package-lock.json +2 -2
- package/bin/agent-task.js +12 -10
- package/bin/agent-toolkit-gemini.js +11 -9
- package/bin/agent-toolkit-inbox.js +12 -10
- package/bin/agent-toolkit-mcp.js +14 -8
- package/bin/agent-toolkit.js +12 -9
- package/bin/nexakit.js +12 -9
- package/bin/project-memory-mcp.js +12 -9
- package/bin/project-memory.js +5 -3
- package/{skills/depurar → catalogs/skills/vercel-composition-patterns}/LICENSE.txt +6 -6
- package/catalogs/skills/vercel-react-best-practices/LICENSE.txt +27 -0
- package/catalogs/skills.json +70 -35
- package/docs/skills/evidencias/2026-10-28-recomendador-preparacion.md +1 -1
- package/package.json +5 -3
- package/plugins/nexakit-modelo/.claude-plugin/plugin.json +1 -0
- package/plugins/nexakit-modelo/hooks/hooks.json +1 -0
- package/plugins/nexakit-modelo/hooks/register.js +48 -0
- package/plugins/nexakit-modelo/hooks/status.js +8 -0
- package/plugins/nexakit-modelo/tests/register.test.ts +21 -0
- package/plugins/nexakit-panel/.claude-plugin/plugin.json +1 -0
- package/plugins/nexakit-panel/README.md +20 -0
- package/plugins/nexakit-panel/hooks/hooks.json +1 -0
- package/plugins/nexakit-panel/hooks/lienzo.js +382 -0
- package/plugins/nexakit-panel/hooks/panel.js +279 -0
- package/plugins/nexakit-panel/hooks/register.tsx +248 -0
- package/plugins/nexakit-panel/tests/lienzo.test.ts +142 -0
- package/plugins/nexakit-panel/tests/panel.test.ts +240 -0
- package/plugins/nexakit-panel/tests/register.test.ts +167 -0
- package/plugins/nexakit-panel/types/index.d.ts +41 -0
- package/plugins/nexakit-sesion/.claude-plugin/plugin.json +1 -0
- package/plugins/nexakit-sesion/README.md +12 -0
- package/plugins/nexakit-sesion/hooks/aviso.js +38 -0
- package/plugins/nexakit-sesion/hooks/hooks.json +1 -0
- package/plugins/nexakit-sesion/hooks/register.js +28 -0
- package/plugins/nexakit-sesion/tests/register.test.ts +96 -0
- package/schemas/skill-catalog.schema.json +9 -2
- package/skills/cerrar-memoria/SKILL.md +6 -2
- package/skills/cerrar-memoria/references/cierre.md +5 -1
- package/skills/elegir-modelo/SKILL.md +68 -0
- package/skills/elegir-modelo/modelos.mjs +108 -0
- package/skills/gemini/SKILL.md +1 -1
- package/skills/project-handoff/SKILL.md +39 -12
- package/skills/project-handoff/references/handoff-template.md +24 -17
- package/skills/quality-gate/SKILL.md +6 -12
- package/skills/revisar/SKILL.md +6 -16
- package/skills/{desarrollo-con-pruebas/LICENSE.txt → revisar-skill/LICENSE-find-skills.txt} +5 -5
- package/skills/revisar-skill/SKILL.md +94 -0
- package/skills/revision-cruzada/SKILL.md +53 -475
- package/skills/web-design-guidelines/AGENTS.md +156 -0
- package/skills/{spec-driven-development → web-design-guidelines}/LICENSE.txt +1 -6
- package/skills/web-design-guidelines/SKILL.md +20 -30
- package/src/agent-models.js +53 -13
- package/src/brand.js +117 -0
- package/src/cli-executable.js +5 -3
- package/src/cli.js +16 -9
- package/src/connect.js +56 -4
- package/src/init.js +9 -1
- package/src/install-lock.js +127 -0
- package/src/install-registry.js +61 -0
- package/src/legacy-repair.js +71 -0
- package/src/link-states.js +172 -0
- package/src/memory-locator.js +118 -0
- package/src/missing-dependencies.js +21 -0
- package/src/model-command.js +147 -0
- package/src/panel-cli.js +182 -0
- package/src/project-root.js +41 -0
- package/src/project-signals.js +339 -0
- package/src/recommend-report.js +77 -0
- package/src/review-instructions.js +211 -0
- package/src/review-policy.js +35 -0
- package/src/revision-adapters.js +157 -0
- package/src/revision-context.js +86 -0
- package/src/revision-isolation.js +222 -0
- package/src/revision-process.js +103 -0
- package/src/revision-provenance.js +89 -0
- package/src/revision-snapshot.js +120 -0
- package/src/revision.js +242 -0
- package/src/setup-apply.js +231 -0
- package/src/setup-plan.js +231 -0
- package/src/setup-state.js +277 -0
- package/src/setup-tui.js +555 -0
- package/src/skill-catalog-add.js +91 -0
- package/src/skill-catalog-audit.js +26 -27
- package/src/skill-files.js +39 -0
- package/src/skill-recommendations.js +321 -208
- package/src/skill-scan-report.js +14 -0
- package/src/skill-scan.js +178 -0
- package/src/stable-command.js +53 -0
- package/src/stable-install.js +222 -0
- package/src/superpowers.js +133 -0
- package/src/sync-apply.js +8 -5
- package/src/sync-files.js +2 -1
- package/src/terminal-text.js +68 -0
- package/src/toolkit-cli.js +397 -70
- package/src/toolkit-commands-guide.js +53 -39
- package/src/toolkit-setup.js +507 -110
- package/assets/brand/agent-toolkit-mark-v1.png +0 -0
- package/skills/depurar/SKILL.md +0 -69
- package/skills/depurar/agents/openai.yaml +0 -7
- package/skills/depurar/references/tecnicas-de-diagnostico.md +0 -37
- package/skills/desarrollo-con-pruebas/SKILL.md +0 -55
- package/skills/desarrollo-con-pruebas/agents/openai.yaml +0 -7
- package/skills/desarrollo-con-pruebas/references/buenas-pruebas.md +0 -33
- package/skills/estado/SKILL.md +0 -41
- package/skills/programar/SKILL.md +0 -19
- package/skills/spec-driven-development/SKILL.md +0 -48
- package/skills/spec-driven-development/agents/openai.yaml +0 -7
- package/skills/spec-driven-development/references/spec-template.md +0 -55
- package/skills/spec-driven-development/references/workflow.md +0 -70
- package/src/toolkit-tui.js +0 -961
package/src/toolkit-setup.js
CHANGED
|
@@ -6,158 +6,476 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
import { createRequire } from 'node:module';
|
|
7
7
|
import { parseTOML, getStaticTOMLValue } from 'toml-eslint-parser';
|
|
8
8
|
import { IS_WINDOWS } from './platform.js';
|
|
9
|
+
import { acquireLock } from './install-lock.js';
|
|
10
|
+
import { isRegistered, readRegistry, recordEntries } from './install-registry.js';
|
|
11
|
+
import {
|
|
12
|
+
ACTIONABLE_LINK_STATES,
|
|
13
|
+
READY_LINK_STATES,
|
|
14
|
+
REMOVED_BY_USER_STATE,
|
|
15
|
+
chooseActiveRoot,
|
|
16
|
+
classifyLink,
|
|
17
|
+
classifyReference,
|
|
18
|
+
isEphemeralPath,
|
|
19
|
+
legacyGroups,
|
|
20
|
+
recognizedRoot,
|
|
21
|
+
relinkIfUnchanged,
|
|
22
|
+
} from './link-states.js';
|
|
23
|
+
import { actualRoot, ensureStableInstall, nexakitDir } from './stable-install.js';
|
|
24
|
+
import { inspectSuperpowers } from './superpowers.js';
|
|
9
25
|
|
|
10
26
|
import packageJson from '../package.json' with { type: 'json' };
|
|
11
27
|
|
|
12
28
|
// The package declares a simple major-version floor (>=24).
|
|
13
29
|
const minimumNodeMajor = Number(packageJson.engines.node.match(/^>=(\d+)$/u)[1]);
|
|
14
|
-
export const isNodeSupported =
|
|
30
|
+
export const isNodeSupported = (version) =>
|
|
31
|
+
Number(version.replace(/^v/u, '').split('.')[0]) >= minimumNodeMajor;
|
|
15
32
|
|
|
16
33
|
export const TOOLKIT_ROOT = fileURLToPath(new URL('../', import.meta.url));
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
34
|
+
const SKILL_HOMES = {
|
|
35
|
+
codex: ['.agents', 'skills'],
|
|
36
|
+
claude: ['.claude', 'skills'],
|
|
37
|
+
antigravity: ['.gemini', 'antigravity-cli', 'skills'],
|
|
20
38
|
};
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
39
|
+
// Only Claude and Codex run the coordination MCP; Antigravity does not have one in this phase.
|
|
40
|
+
const MCP_CLIENTS = ['codex', 'claude'];
|
|
41
|
+
// Skills nexakit no longer ships. Links it created are removed; anything else with the name is reported, never
|
|
42
|
+
// touched. The last three are now covered by Superpowers (brainstorming/writing-plans, systematic-debugging and
|
|
43
|
+
// test-driven-development), which is required.
|
|
44
|
+
export const RETIRED_SKILLS = Object.freeze(['programar', 'estado', 'spec-driven-development', 'depurar', 'desarrollo-con-pruebas']);
|
|
45
|
+
const MCP_SUFFIX = 'bin/agent-toolkit-mcp.js';
|
|
46
|
+
const COMMANDS = { codex: 'codex', claude: 'claude', antigravity: 'agy' };
|
|
47
|
+
export const globalSkillTarget = (home, client, name) => path.join(home, ...SKILL_HOMES[client], name);
|
|
48
|
+
// Registry key of an MCP entry: it has no path of its own in the home folder.
|
|
49
|
+
export const mcpRegistryTarget = (client, name) => `mcp:${client}:${name}`;
|
|
50
|
+
|
|
51
|
+
// `all` means Claude and Codex, plus Antigravity when `agy` is installed or its folder already exists:
|
|
52
|
+
// nothing is created for someone who does not use it.
|
|
53
|
+
async function clientsFor(agent, environment, home) {
|
|
54
|
+
if (!['codex', 'claude', 'antigravity', 'all'].includes(agent))
|
|
55
|
+
throw new Error('El agente debe ser codex, claude, antigravity o all.');
|
|
56
|
+
if (agent !== 'all') return [agent];
|
|
57
|
+
const antigravity =
|
|
58
|
+
(await executable('agy', environment)) ||
|
|
59
|
+
Boolean(await stat(path.join(home, '.gemini', 'antigravity-cli')));
|
|
60
|
+
return antigravity ? ['codex', 'claude', 'antigravity'] : ['codex', 'claude'];
|
|
61
|
+
}
|
|
62
|
+
const quote = (value) => "'" + value.replaceAll("'", "'\\''") + "'";
|
|
63
|
+
const stat = (file) =>
|
|
64
|
+
fs.lstat(file).catch((e) => {
|
|
65
|
+
if (e.code === 'ENOENT') return null;
|
|
66
|
+
throw e;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Links point at the root in use, except when it is an npx cache: then they point at the stable copy.
|
|
70
|
+
export async function resolveRoots({ toolkitRoot, homeDirectory }) {
|
|
71
|
+
const sourceRoot = await fs.realpath(toolkitRoot);
|
|
72
|
+
const ephemeral = isEphemeralPath(sourceRoot);
|
|
73
|
+
return { sourceRoot, linkRoot: ephemeral ? actualRoot(homeDirectory) : sourceRoot, ephemeral };
|
|
74
|
+
}
|
|
75
|
+
// A node inside an npx cache disappears with it, so it is never the one registered, not even through a link.
|
|
76
|
+
const outsideNpxCache = async (file) =>
|
|
77
|
+
!isEphemeralPath(file) && !isEphemeralPath(await fs.realpath(file).catch(() => file));
|
|
78
|
+
export async function resolveNodeExecutable(environment) {
|
|
79
|
+
const stable = await findExecutable('node', environment, outsideNpxCache);
|
|
80
|
+
if (stable) return stable;
|
|
81
|
+
return (await outsideNpxCache(process.execPath)) ? process.execPath : 'node';
|
|
82
|
+
}
|
|
83
|
+
// Skills de catálogo para stacks concretos: el recomendador las instala por proyecto, así que no son base global.
|
|
84
|
+
// Los enlaces globales que ya existían no se tocan.
|
|
85
|
+
export const PROJECT_ONLY_SKILLS = Object.freeze(['frontend-design', 'web-design-guidelines', 'mcp-builder']);
|
|
86
|
+
async function baseSkillNames(root) {
|
|
87
|
+
const names = (await fs.readdir(path.join(root, 'skills'), { withFileTypes: true }))
|
|
88
|
+
.filter((e) => e.isDirectory() && /^[a-z0-9-]+$/u.test(e.name) && !PROJECT_ONLY_SKILLS.includes(e.name))
|
|
89
|
+
.map((e) => e.name)
|
|
90
|
+
.sort();
|
|
91
|
+
for (const name of names) {
|
|
92
|
+
const file = await stat(path.join(root, 'skills', name, 'SKILL.md'));
|
|
93
|
+
if (!file?.isFile() || file.isSymbolicLink()) throw new Error('Skill fuente inválida.');
|
|
94
|
+
}
|
|
95
|
+
return names;
|
|
96
|
+
}
|
|
97
|
+
async function catalogNames(root) {
|
|
98
|
+
try {
|
|
99
|
+
return JSON.parse(await fs.readFile(path.join(root, 'catalogs/skills.json'), 'utf8')).skills.map(
|
|
100
|
+
(s) => s.name,
|
|
101
|
+
);
|
|
102
|
+
} catch {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export async function packageVersion(root) {
|
|
107
|
+
return JSON.parse(await fs.readFile(path.join(root, 'package.json'), 'utf8')).version;
|
|
108
|
+
}
|
|
24
109
|
async function parents(home, target, create = false) {
|
|
25
110
|
let current = home;
|
|
26
111
|
for (const part of path.relative(home, path.dirname(target)).split(path.sep)) {
|
|
27
112
|
current = path.join(current, part);
|
|
28
113
|
let info = await stat(current);
|
|
29
114
|
if (!info && create) {
|
|
30
|
-
await fs.mkdir(current, { mode: 0o700 }).catch(e => {
|
|
115
|
+
await fs.mkdir(current, { mode: 0o700 }).catch((e) => {
|
|
116
|
+
if (e.code !== 'EEXIST') throw e;
|
|
117
|
+
});
|
|
31
118
|
info = await stat(current);
|
|
32
119
|
}
|
|
33
|
-
if (info && (!info.isDirectory() || info.isSymbolicLink()))
|
|
120
|
+
if (info && (!info.isDirectory() || info.isSymbolicLink()))
|
|
121
|
+
throw new Error('Un directorio de instalación está ocupado o es un enlace.');
|
|
34
122
|
}
|
|
35
123
|
}
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
124
|
+
export function registrationCommand(client, current, nodePath, entry) {
|
|
125
|
+
const add =
|
|
126
|
+
client === 'codex'
|
|
127
|
+
? `codex mcp add agent-toolkit-workflow -- ${quote(nodePath)} ${quote(entry)}`
|
|
128
|
+
: `claude mcp add --scope user --transport stdio agent-toolkit-workflow -- ${quote(nodePath)} ${quote(entry)}`;
|
|
129
|
+
if (current.state === 'falta') return add;
|
|
130
|
+
// Moving or repairing an existing entry: remove it first under the name it has now.
|
|
131
|
+
return `${client === 'codex' ? `codex mcp remove ${current.name}` : `claude mcp remove ${current.name} -s user`} && ${add}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Classifies every global skill link and the coordination MCP, and with `apply` creates what is missing and
|
|
135
|
+
// moves or repairs only what nexakit can prove is its own. Conflicts are skipped and reported, never replaced.
|
|
136
|
+
export async function setupToolkit({
|
|
137
|
+
agent = 'all',
|
|
138
|
+
apply = false,
|
|
139
|
+
homeDirectory = os.homedir(),
|
|
140
|
+
toolkitRoot = TOOLKIT_ROOT,
|
|
141
|
+
environment = process.env,
|
|
142
|
+
codexHome = environment.CODEX_HOME ?? path.join(homeDirectory, '.codex'),
|
|
143
|
+
codexProfile,
|
|
144
|
+
restore = [],
|
|
145
|
+
omit = [],
|
|
146
|
+
runNpm,
|
|
147
|
+
packageSpec,
|
|
148
|
+
lockOptions,
|
|
149
|
+
acquire = acquireLock,
|
|
150
|
+
} = {}) {
|
|
151
|
+
const home = await fs.realpath(homeDirectory);
|
|
152
|
+
const clients = await clientsFor(agent, environment, home);
|
|
153
|
+
const { sourceRoot, linkRoot, ephemeral } = await resolveRoots({ toolkitRoot, homeDirectory: home });
|
|
154
|
+
const names = await baseSkillNames(sourceRoot);
|
|
155
|
+
const registry = await readRegistry(nexakitDir(home));
|
|
156
|
+
const isOwned = (target, literal) => isRegistered(registry, target, literal);
|
|
157
|
+
const activeRoots = [linkRoot, sourceRoot, actualRoot(home)];
|
|
158
|
+
const retiredSkills = [];
|
|
159
|
+
for (const client of clients)
|
|
160
|
+
for (const name of RETIRED_SKILLS) {
|
|
161
|
+
const target = globalSkillTarget(home, client, name);
|
|
162
|
+
await parents(home, target);
|
|
163
|
+
const info = await stat(target);
|
|
164
|
+
if (!info) continue;
|
|
165
|
+
const literal = info.isSymbolicLink() ? await fs.readlink(target) : null;
|
|
166
|
+
// Registered, or pointing at `<nexakit copy>/skills/<name>`: links made before the registry existed
|
|
167
|
+
// are nexakit's too. A link into anything else is reported, never removed.
|
|
168
|
+
const owned = Boolean(
|
|
169
|
+
literal &&
|
|
170
|
+
(isOwned(target, literal) ||
|
|
171
|
+
(await recognizedRoot(path.resolve(path.dirname(target), literal), path.join('skills', name)))),
|
|
172
|
+
);
|
|
173
|
+
retiredSkills.push({ client, name, target, literal, owned });
|
|
174
|
+
}
|
|
175
|
+
|
|
50
176
|
const skills = [];
|
|
51
|
-
for (const client of clients)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
177
|
+
for (const client of clients)
|
|
178
|
+
for (const name of names) {
|
|
179
|
+
const target = globalSkillTarget(home, client, name);
|
|
180
|
+
await parents(home, target);
|
|
181
|
+
const { suffix, ...state } = await classifyLink({
|
|
182
|
+
target,
|
|
183
|
+
suffix: `skills/${name}`,
|
|
184
|
+
activeRoots,
|
|
185
|
+
isOwned,
|
|
186
|
+
});
|
|
187
|
+
skills.push({ client, name, ...state });
|
|
188
|
+
}
|
|
189
|
+
// Un enlace que falta pero que nexakit registró como suyo lo quitó el usuario: se respeta. Solo si esa IA aún
|
|
190
|
+
// conserva otros enlaces de nexakit; si se quedó sin ninguno (carpeta borrada), se reconstruye entera.
|
|
191
|
+
// `omit` declara lo mismo de forma explícita para enlaces antiguos que el registro no llegó a conocer; se anota
|
|
192
|
+
// en el registro al aplicar. `restore` gana sobre ambos.
|
|
193
|
+
const clientsWithLinks = new Set(skills.filter((s) => READY_LINK_STATES.includes(s.state)).map((s) => s.client));
|
|
194
|
+
const toRestore = new Set(restore);
|
|
195
|
+
const toOmit = new Set(omit);
|
|
196
|
+
const toRecord = new Map();
|
|
197
|
+
for (const item of skills) {
|
|
198
|
+
if (item.state !== 'falta' || toRestore.has(item.name)) continue;
|
|
199
|
+
if (registry.entradas.some((entry) => entry.destino === item.target)) {
|
|
200
|
+
if (clientsWithLinks.has(item.client)) item.state = REMOVED_BY_USER_STATE;
|
|
201
|
+
} else if (toOmit.has(item.name)) {
|
|
202
|
+
item.state = REMOVED_BY_USER_STATE;
|
|
203
|
+
toRecord.set(item.target, item);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Origins are never mixed: if most links already use another valid copy, that copy stays the root to link to.
|
|
208
|
+
const activeRoot = chooseActiveRoot(skills, linkRoot);
|
|
209
|
+
const usingOtherCopy = activeRoot !== linkRoot;
|
|
210
|
+
const missingInActive = [];
|
|
211
|
+
if (usingOtherCopy) {
|
|
212
|
+
for (const name of names)
|
|
213
|
+
if (!(await stat(path.join(activeRoot, 'skills', name, 'SKILL.md')))) missingInActive.push(name);
|
|
214
|
+
}
|
|
215
|
+
for (const item of skills) item.source = path.join(activeRoot, 'skills', item.name);
|
|
216
|
+
|
|
217
|
+
const mcpStates = {};
|
|
218
|
+
for (const client of clients.filter((c) => MCP_CLIENTS.includes(c))) {
|
|
219
|
+
mcpStates[client] = await inspectMcp(client, home, codexHome, { activeRoots, isOwned });
|
|
220
|
+
}
|
|
221
|
+
const mcpDeadRoot = Object.values(mcpStates).find((s) => s.state === 'roto-ajeno')?.root;
|
|
222
|
+
const legacy = legacyGroups(skills, {
|
|
223
|
+
skillNames: new Set([...names, ...(await catalogNames(sourceRoot))]),
|
|
224
|
+
mcpDeadRoot,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const pending = skills.filter(
|
|
228
|
+
(s) => ACTIONABLE_LINK_STATES.includes(s.state) && !missingInActive.includes(s.name),
|
|
229
|
+
);
|
|
230
|
+
const mcpPending = Object.keys(mcpStates).filter((client) =>
|
|
231
|
+
ACTIONABLE_LINK_STATES.includes(mcpStates[client].state),
|
|
232
|
+
);
|
|
233
|
+
let applied = false,
|
|
234
|
+
changed = 0,
|
|
235
|
+
created = 0,
|
|
236
|
+
stableInstall;
|
|
237
|
+
if (apply) {
|
|
238
|
+
const lock = await acquire(nexakitDir(home), lockOptions);
|
|
239
|
+
try {
|
|
240
|
+
if (ephemeral && !usingOtherCopy && (pending.length || mcpPending.length)) {
|
|
241
|
+
stableInstall = await ensureStableInstall({
|
|
242
|
+
homeDirectory: home,
|
|
243
|
+
version: await packageVersion(sourceRoot),
|
|
244
|
+
lock,
|
|
245
|
+
...(runNpm && { runNpm }),
|
|
246
|
+
...(packageSpec && { packageSpec }),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
for (const item of retiredSkills.filter((s) => s.owned)) {
|
|
250
|
+
await lock.assertHeld();
|
|
64
251
|
const current = await stat(item.target);
|
|
65
|
-
if (
|
|
66
|
-
|
|
252
|
+
if (current?.isSymbolicLink() && (await fs.readlink(item.target)) === item.literal)
|
|
253
|
+
await fs.unlink(item.target);
|
|
254
|
+
}
|
|
255
|
+
const entries = [];
|
|
256
|
+
for (const item of pending) {
|
|
257
|
+
await parents(home, item.target, true);
|
|
258
|
+
await lock.assertHeld();
|
|
259
|
+
const outcome = await relinkIfUnchanged({
|
|
260
|
+
target: item.target,
|
|
261
|
+
expectedLiteral: item.state === 'falta' ? null : item.literal,
|
|
262
|
+
source: item.source,
|
|
263
|
+
});
|
|
264
|
+
if (outcome === 'cambio') {
|
|
265
|
+
changed++;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
// The registry keeps what readlink returns, the value later compared.
|
|
269
|
+
const literal = await fs.readlink(item.target);
|
|
270
|
+
entries.push({ destino: item.target, apunta: literal, raiz: activeRoot });
|
|
271
|
+
Object.assign(item, { state: 'instalado', literal, root: activeRoot });
|
|
67
272
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
273
|
+
created = entries.length;
|
|
274
|
+
for (const [destino, item] of toRecord)
|
|
275
|
+
entries.push({ destino, apunta: path.join(activeRoot, 'skills', item.name), raiz: activeRoot });
|
|
276
|
+
await recordEntries(nexakitDir(home), entries, lock);
|
|
277
|
+
applied = true;
|
|
278
|
+
} finally {
|
|
279
|
+
await lock.release();
|
|
71
280
|
}
|
|
72
281
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
282
|
+
|
|
283
|
+
// A command is only suggested once the script it registers exists (after the stable copy is in place).
|
|
284
|
+
const nodePath = await resolveNodeExecutable(environment);
|
|
285
|
+
const entry = path.join(activeRoot, MCP_SUFFIX);
|
|
286
|
+
const entryExists = Boolean(await stat(entry));
|
|
287
|
+
const registrationCommands = {},
|
|
288
|
+
registrationPending = [];
|
|
289
|
+
for (const client of mcpPending) {
|
|
290
|
+
if (entryExists)
|
|
291
|
+
registrationCommands[client] = registrationCommand(client, mcpStates[client], nodePath, entry);
|
|
292
|
+
else registrationPending.push(client);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const obsolete = Object.entries(mcpStates)
|
|
296
|
+
.filter(([, state]) => state.name)
|
|
297
|
+
.map(([client, state]) => ({
|
|
298
|
+
message: 'Ya no se usa; si quieres quitarlo:',
|
|
299
|
+
command:
|
|
300
|
+
client === 'codex' ? `codex mcp remove ${state.name}` : `claude mcp remove ${state.name} -s user`,
|
|
301
|
+
}));
|
|
302
|
+
const agentsPath = path.join(codexHome, 'agents');
|
|
303
|
+
for (const name of await fs.readdir(agentsPath).catch(() => [])) {
|
|
304
|
+
if (!/^nivel-(ligero|normal|exigente|alto)\.(toml|md)$/u.test(name)) continue;
|
|
305
|
+
const file = path.join(agentsPath, name);
|
|
306
|
+
obsolete.push({
|
|
307
|
+
message: 'Ya no se usa; si quieres quitarlo:',
|
|
308
|
+
command: IS_WINDOWS
|
|
309
|
+
? `PowerShell: Remove-Item -LiteralPath '${file.replaceAll("'", "''")}'`
|
|
310
|
+
: `rm -- ${quote(file)}`,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
applied,
|
|
315
|
+
skills,
|
|
316
|
+
retiredSkills,
|
|
317
|
+
obsolete,
|
|
318
|
+
conflicts:
|
|
319
|
+
skills.filter((s) => ['conflicto', 'roto-ajeno'].includes(s.state)).length +
|
|
320
|
+
retiredSkills.filter((s) => !s.owned).length,
|
|
321
|
+
changed,
|
|
322
|
+
clients,
|
|
323
|
+
linkRoot,
|
|
324
|
+
ephemeral,
|
|
325
|
+
activeRoot,
|
|
326
|
+
legacy,
|
|
327
|
+
...(usingOtherCopy && { otherCopy: { root: activeRoot, missing: missingInActive } }),
|
|
328
|
+
...(stableInstall && { stableInstall }),
|
|
329
|
+
mcp: {
|
|
330
|
+
applied: false,
|
|
331
|
+
states: mcpStates,
|
|
332
|
+
registrationCommands,
|
|
333
|
+
...(registrationPending.length && { registrationPending }),
|
|
334
|
+
},
|
|
335
|
+
restartRequired: created > 0,
|
|
79
336
|
...(clients.includes('codex') && { codexSandbox: await codexSandbox(home, codexHome, codexProfile) }),
|
|
80
|
-
|
|
337
|
+
// Required, installed through each agent's plugin manager; `doctor` and `npx nexakit` check it.
|
|
338
|
+
superpowers: { included: false, required: true },
|
|
339
|
+
codexHome,
|
|
340
|
+
};
|
|
81
341
|
}
|
|
82
342
|
// Codex runs commands in a workspace-write sandbox by default, so `registrar` cannot write its reviewer record
|
|
83
343
|
// under ~/.agent-toolkit unless that folder is inside a writable root. Only reported; the user runs the printed
|
|
84
344
|
// command. What cannot be evaluated (permission profiles) is reported as unknown instead of asking for a change.
|
|
85
345
|
// Resolves links through the deepest folder that exists, so a root written through a link still matches.
|
|
86
346
|
async function canonical(target) {
|
|
87
|
-
let current = path.resolve(target),
|
|
347
|
+
let current = path.resolve(target),
|
|
348
|
+
rest = [];
|
|
88
349
|
while (true) {
|
|
89
|
-
try {
|
|
90
|
-
|
|
350
|
+
try {
|
|
351
|
+
return path.join(await fs.realpath(current), ...rest);
|
|
352
|
+
} catch {
|
|
353
|
+
const parent = path.dirname(current);
|
|
354
|
+
if (parent === current) return path.resolve(target);
|
|
355
|
+
rest.unshift(path.basename(current));
|
|
356
|
+
current = parent;
|
|
357
|
+
}
|
|
91
358
|
}
|
|
92
359
|
}
|
|
93
360
|
const PROFILE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
|
|
94
361
|
const layer = (base, top) => {
|
|
95
362
|
const out = { ...base };
|
|
96
363
|
for (const [key, value] of Object.entries(top)) {
|
|
97
|
-
out[key] =
|
|
98
|
-
|
|
364
|
+
out[key] =
|
|
365
|
+
value &&
|
|
366
|
+
typeof value === 'object' &&
|
|
367
|
+
!Array.isArray(value) &&
|
|
368
|
+
out[key] &&
|
|
369
|
+
typeof out[key] === 'object' &&
|
|
370
|
+
!Array.isArray(out[key])
|
|
371
|
+
? layer(out[key], value)
|
|
372
|
+
: value;
|
|
99
373
|
}
|
|
100
374
|
return out;
|
|
101
375
|
};
|
|
102
|
-
const readToml = async
|
|
376
|
+
const readToml = async (file) => {
|
|
377
|
+
const bytes = await boundedConfig(file);
|
|
378
|
+
return bytes ? getStaticTOMLValue(parseTOML(bytes.toString('utf8'), { tomlVersion: '1.0.0' })) : null;
|
|
379
|
+
};
|
|
103
380
|
async function codexSandbox(home, codexHome, codexProfile) {
|
|
104
|
-
const state = path.join(home, '.agent-toolkit'),
|
|
381
|
+
const state = path.join(home, '.agent-toolkit'),
|
|
382
|
+
file = path.join(codexHome, 'config.toml');
|
|
105
383
|
// A base64 payload keeps paths and JavaScript out of shell interpolation in cmd, PowerShell and POSIX shells.
|
|
106
|
-
const script =
|
|
384
|
+
const script =
|
|
385
|
+
`const fs = require('node:fs'); const file = ${JSON.stringify(file)}; ` +
|
|
107
386
|
`let content = ''; try { content = fs.readFileSync(file, 'utf8'); } catch (error) { if (error.code !== 'ENOENT') throw error; } ` +
|
|
108
387
|
`if (/^\\[sandbox_workspace_write\\]/mu.test(content)) console.log('Ya existe la sección: añade la ruta a writable_roots a mano'); ` +
|
|
109
388
|
`else fs.appendFileSync(file, ${JSON.stringify(`\n[sandbox_workspace_write]\nwritable_roots = [${JSON.stringify(state)}]\n`)});`;
|
|
110
389
|
const command = `node -e "eval(Buffer.from('${Buffer.from(script).toString('base64')}','base64').toString())"`;
|
|
111
|
-
const unknown = reason => ({
|
|
112
|
-
|
|
113
|
-
|
|
390
|
+
const unknown = (reason) => ({
|
|
391
|
+
state: 'unknown',
|
|
392
|
+
writableRoot: state,
|
|
393
|
+
hint: `${reason}: no se puede saber si Codex deja escribir en ${state}. Si $revisar no consigue registrarse, permite esa carpeta.`,
|
|
394
|
+
});
|
|
395
|
+
let config = {},
|
|
396
|
+
profileFiles = [];
|
|
114
397
|
try {
|
|
115
|
-
config = await readToml(file) ?? {};
|
|
398
|
+
config = (await readToml(file)) ?? {};
|
|
116
399
|
// `codex --profile <name>` layers $CODEX_HOME/<name>.config.toml over config.toml at launch time.
|
|
117
|
-
profileFiles = (await fs.readdir(codexHome).catch(() => []))
|
|
118
|
-
|
|
400
|
+
profileFiles = (await fs.readdir(codexHome).catch(() => []))
|
|
401
|
+
.filter((n) => n.endsWith('.config.toml'))
|
|
402
|
+
.map((n) => n.slice(0, -12))
|
|
403
|
+
.filter((n) => PROFILE.test(n))
|
|
404
|
+
.sort();
|
|
405
|
+
} catch {
|
|
406
|
+
return { state: 'unreadable', writableRoot: state };
|
|
407
|
+
}
|
|
119
408
|
let effective = config;
|
|
120
409
|
if (codexProfile !== undefined) {
|
|
121
410
|
if (!PROFILE.test(codexProfile)) throw new Error('Nombre de perfil de Codex no válido.');
|
|
122
411
|
let overlay;
|
|
123
|
-
try {
|
|
412
|
+
try {
|
|
413
|
+
overlay = await readToml(path.join(codexHome, `${codexProfile}.config.toml`));
|
|
414
|
+
} catch {
|
|
415
|
+
return { state: 'unreadable', writableRoot: state };
|
|
416
|
+
}
|
|
124
417
|
if (!overlay) return unknown(`No existe ${codexProfile}.config.toml en ${codexHome}`);
|
|
125
418
|
effective = layer(config, overlay);
|
|
126
419
|
} else if (config.profile !== undefined) {
|
|
127
420
|
// Older layout: a [profiles.<name>] table selected with `profile = "<name>"`.
|
|
128
421
|
const profile = typeof config.profile === 'string' ? config.profiles?.[config.profile] : undefined;
|
|
129
|
-
if (!profile || typeof profile !== 'object')
|
|
422
|
+
if (!profile || typeof profile !== 'object')
|
|
423
|
+
return unknown('El perfil de configuración elegido no se encuentra');
|
|
130
424
|
effective = layer(config, profile);
|
|
131
425
|
}
|
|
132
|
-
const result = value =>
|
|
133
|
-
|
|
134
|
-
|
|
426
|
+
const result = (value) =>
|
|
427
|
+
codexProfile === undefined && profileFiles.length
|
|
428
|
+
? {
|
|
429
|
+
...value,
|
|
430
|
+
profileFiles,
|
|
431
|
+
profileHint: `Hay perfiles de Codex (${profileFiles.join(', ')}). Si abres Codex con --profile, revisa con --perfil-codex <nombre>.`,
|
|
432
|
+
}
|
|
433
|
+
: value;
|
|
135
434
|
return result(await sandboxState(effective, home, state, file, command, unknown));
|
|
136
435
|
}
|
|
137
436
|
async function sandboxState(effective, home, state, file, command, unknown) {
|
|
138
437
|
const mode = effective.sandbox_mode;
|
|
139
438
|
// When sandbox_mode is present, Codex uses the legacy options and ignores default_permissions.
|
|
140
439
|
if (mode === undefined && effective.default_permissions !== undefined) {
|
|
141
|
-
if (effective.default_permissions === ':danger-full-access')
|
|
142
|
-
|
|
143
|
-
|
|
440
|
+
if (effective.default_permissions === ':danger-full-access')
|
|
441
|
+
return { state: 'not-needed', writableRoot: state };
|
|
442
|
+
if (effective.default_permissions === ':read-only')
|
|
443
|
+
return {
|
|
444
|
+
state: 'read-only',
|
|
445
|
+
writableRoot: state,
|
|
446
|
+
hint: `El perfil de permisos :read-only no deja escribir: $revisar no puede registrarse. Usa sandbox_mode = "workspace-write" con ${state} en writable_roots.`,
|
|
447
|
+
};
|
|
144
448
|
return unknown('Codex usa un perfil de permisos propio');
|
|
145
449
|
}
|
|
146
|
-
if (mode === undefined && effective.permissions !== undefined)
|
|
450
|
+
if (mode === undefined && effective.permissions !== undefined)
|
|
451
|
+
return unknown('Codex usa perfiles de permisos');
|
|
147
452
|
if (mode === 'danger-full-access') return { state: 'not-needed', writableRoot: state };
|
|
148
|
-
if (mode === 'read-only')
|
|
149
|
-
|
|
150
|
-
|
|
453
|
+
if (mode === 'read-only')
|
|
454
|
+
return {
|
|
455
|
+
state: 'read-only',
|
|
456
|
+
writableRoot: state,
|
|
457
|
+
hint: `Codex está en solo lectura: $revisar no puede registrarse. Usa sandbox_mode = "workspace-write" con ${state} en writable_roots.`,
|
|
458
|
+
};
|
|
459
|
+
if (mode !== undefined && mode !== 'workspace-write')
|
|
460
|
+
return unknown(`sandbox_mode = ${JSON.stringify(mode)} no es conocido`);
|
|
151
461
|
const roots = effective.sandbox_workspace_write?.writable_roots;
|
|
152
462
|
const target = await canonical(state);
|
|
153
463
|
for (const root of Array.isArray(roots) ? roots : []) {
|
|
154
464
|
if (typeof root !== 'string' || !root) continue;
|
|
155
465
|
const relative = path.relative(await canonical(root.replace(/^~(?=$|\/)/u, home)), target);
|
|
156
|
-
if (
|
|
466
|
+
if (
|
|
467
|
+
relative === '' ||
|
|
468
|
+
(relative !== '..' && !relative.startsWith('..' + path.sep) && !path.isAbsolute(relative))
|
|
469
|
+
) {
|
|
157
470
|
return { state: 'allowed', writableRoot: state };
|
|
158
471
|
}
|
|
159
472
|
}
|
|
160
|
-
return effective.sandbox_workspace_write
|
|
473
|
+
return effective.sandbox_workspace_write
|
|
474
|
+
? {
|
|
475
|
+
state: 'missing-root',
|
|
476
|
+
writableRoot: state,
|
|
477
|
+
hint: `Añade ${JSON.stringify(state)} a writable_roots de [sandbox_workspace_write] en ${file}.`,
|
|
478
|
+
}
|
|
161
479
|
: { state: 'missing', writableRoot: state, command };
|
|
162
480
|
}
|
|
163
481
|
async function boundedConfig(file) {
|
|
@@ -166,52 +484,131 @@ async function boundedConfig(file) {
|
|
|
166
484
|
if (!info.isFile() || info.isSymbolicLink() || info.size > 1_048_576) throw new Error('config');
|
|
167
485
|
return fs.readFile(file);
|
|
168
486
|
}
|
|
169
|
-
|
|
487
|
+
// The entry is recognized by the script it runs (`…/bin/agent-toolkit-mcp.js` under `node`), then classified like
|
|
488
|
+
// a link: installed only if that script exists in a root in use.
|
|
489
|
+
async function inspectMcp(client, home, codexHome, { activeRoots, isOwned }) {
|
|
490
|
+
let servers;
|
|
170
491
|
try {
|
|
171
|
-
const bytes = await boundedConfig(
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
return { state: '
|
|
186
|
-
|
|
492
|
+
const bytes = await boundedConfig(
|
|
493
|
+
client === 'codex' ? path.join(codexHome, 'config.toml') : path.join(home, '.claude.json'),
|
|
494
|
+
);
|
|
495
|
+
if (!bytes) return { state: 'falta' };
|
|
496
|
+
servers =
|
|
497
|
+
client === 'codex'
|
|
498
|
+
? getStaticTOMLValue(parseTOML(bytes.toString('utf8'), { tomlVersion: '1.0.0' })).mcp_servers
|
|
499
|
+
: JSON.parse(bytes.toString('utf8')).mcpServers;
|
|
500
|
+
} catch {
|
|
501
|
+
return { state: 'unreadable' };
|
|
502
|
+
}
|
|
503
|
+
for (const name of ['agent-toolkit-workflow', 'agent-toolkit']) {
|
|
504
|
+
const server = servers?.[name];
|
|
505
|
+
if (!server) continue;
|
|
506
|
+
if (server.enabled === false) return { state: 'disabled', name };
|
|
507
|
+
const entry = Array.isArray(server.args)
|
|
508
|
+
? server.args.find(
|
|
509
|
+
(arg) => typeof arg === 'string' && arg.replaceAll('\\', '/').endsWith(`/${MCP_SUFFIX}`),
|
|
510
|
+
)
|
|
511
|
+
: undefined;
|
|
512
|
+
const runsNode =
|
|
513
|
+
typeof server.command === 'string' && /^node(\.exe)?$/iu.test(path.basename(server.command));
|
|
514
|
+
if (!entry || !runsNode) return { state: 'conflicto', name };
|
|
515
|
+
const { state, root } = await classifyReference({
|
|
516
|
+
target: mcpRegistryTarget(client, name),
|
|
517
|
+
literal: entry,
|
|
518
|
+
suffix: MCP_SUFFIX,
|
|
519
|
+
activeRoots,
|
|
520
|
+
isOwned,
|
|
521
|
+
});
|
|
522
|
+
return { state, name, literal: entry, ...(root && { root }) };
|
|
523
|
+
}
|
|
524
|
+
return { state: 'falta' };
|
|
187
525
|
}
|
|
188
|
-
|
|
526
|
+
async function findExecutable(name, environment, accept = () => true) {
|
|
189
527
|
const extensions = IS_WINDOWS
|
|
190
|
-
? [
|
|
528
|
+
? [
|
|
529
|
+
'',
|
|
530
|
+
...(environment.PATHEXT || '.COM;.EXE;.BAT;.CMD')
|
|
531
|
+
.split(';')
|
|
532
|
+
.filter(Boolean)
|
|
533
|
+
.map((extension) => extension.toLowerCase()),
|
|
534
|
+
]
|
|
191
535
|
: [''];
|
|
192
536
|
for (const folder of (environment.PATH ?? '').split(path.delimiter).filter(Boolean)) {
|
|
193
537
|
for (const extension of extensions) {
|
|
194
|
-
try {
|
|
538
|
+
try {
|
|
539
|
+
const file = path.join(folder, name + extension);
|
|
540
|
+
await fs.access(file, constants.X_OK);
|
|
541
|
+
if ((await fs.stat(file)).isFile() && (await accept(file))) return file;
|
|
542
|
+
} catch {}
|
|
195
543
|
}
|
|
196
544
|
}
|
|
197
|
-
return
|
|
545
|
+
return null;
|
|
198
546
|
}
|
|
199
|
-
export async function
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
547
|
+
export async function executable(name, environment) {
|
|
548
|
+
return Boolean(await findExecutable(name, environment));
|
|
549
|
+
}
|
|
550
|
+
export async function doctorToolkit({
|
|
551
|
+
agent = 'all',
|
|
552
|
+
homeDirectory = os.homedir(),
|
|
553
|
+
toolkitRoot = TOOLKIT_ROOT,
|
|
554
|
+
environment = process.env,
|
|
555
|
+
codexHome = environment.CODEX_HOME ?? path.join(homeDirectory, '.codex'),
|
|
556
|
+
codexProfile,
|
|
557
|
+
runCommand,
|
|
558
|
+
} = {}) {
|
|
559
|
+
const setup = await setupToolkit({
|
|
560
|
+
agent,
|
|
561
|
+
homeDirectory,
|
|
562
|
+
toolkitRoot,
|
|
563
|
+
environment,
|
|
564
|
+
codexHome,
|
|
565
|
+
codexProfile,
|
|
566
|
+
});
|
|
567
|
+
const clients = {};
|
|
568
|
+
for (const name of setup.clients)
|
|
569
|
+
clients[name] = { available: await executable(COMMANDS[name], environment) };
|
|
570
|
+
const mcp = setup.mcp.states;
|
|
207
571
|
const dependencies = {};
|
|
208
572
|
const require = createRequire(path.join(toolkitRoot, 'package.json'));
|
|
209
|
-
for (const name of [
|
|
210
|
-
|
|
573
|
+
for (const name of [
|
|
574
|
+
'@modelcontextprotocol/server',
|
|
575
|
+
'@modelcontextprotocol/sdk',
|
|
576
|
+
'zod',
|
|
577
|
+
'ajv',
|
|
578
|
+
'toml-eslint-parser',
|
|
579
|
+
]) {
|
|
580
|
+
try {
|
|
581
|
+
require.resolve(name === '@modelcontextprotocol/sdk' ? name + '/client/index.js' : name);
|
|
582
|
+
dependencies[name] = true;
|
|
583
|
+
} catch {
|
|
584
|
+
dependencies[name] = false;
|
|
585
|
+
}
|
|
211
586
|
}
|
|
212
|
-
return {
|
|
213
|
-
|
|
587
|
+
return {
|
|
588
|
+
node: { version: process.version, supported: isNodeSupported(process.versions.node) },
|
|
589
|
+
dependencies,
|
|
590
|
+
clients,
|
|
591
|
+
mcp,
|
|
592
|
+
skills: setup.skills,
|
|
593
|
+
obsolete: setup.obsolete,
|
|
594
|
+
retiredSkills: setup.retiredSkills,
|
|
595
|
+
registrationCommands: setup.mcp.registrationCommands,
|
|
596
|
+
...(setup.mcp.registrationPending && { registrationPending: setup.mcp.registrationPending }),
|
|
597
|
+
activeRoot: setup.activeRoot,
|
|
598
|
+
legacy: setup.legacy,
|
|
599
|
+
...(setup.otherCopy && { otherCopy: setup.otherCopy }),
|
|
214
600
|
...(setup.codexSandbox && { codexSandbox: setup.codexSandbox }),
|
|
215
601
|
gemini: { available: await executable('agy', environment), provider: 'antigravity', apiFallback: false },
|
|
216
|
-
superpowers: {
|
|
602
|
+
superpowers: {
|
|
603
|
+
included: false,
|
|
604
|
+
required: true,
|
|
605
|
+
detection: 'client-plugin-cli',
|
|
606
|
+
states: await inspectSuperpowers({
|
|
607
|
+
agentsFound: Object.fromEntries(Object.entries(clients).map(([name, c]) => [name, c.available])),
|
|
608
|
+
environment,
|
|
609
|
+
...(runCommand && { runCommand }),
|
|
610
|
+
}),
|
|
611
|
+
},
|
|
612
|
+
nativeConnectionVerified: false,
|
|
613
|
+
};
|
|
217
614
|
}
|