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
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// Pure: turns what nexakit found (setup-state.js) and the person's answers into the confirmation summary, in short
|
|
2
|
+
// sentences, and the ordered blocks that apply it. No I/O, so every screen and every decision is testable.
|
|
3
|
+
|
|
4
|
+
export const AGENTS = Object.freeze(['claude', 'codex', 'antigravity']);
|
|
5
|
+
export const AGENT_NAMES = Object.freeze({
|
|
6
|
+
claude: 'Claude Code',
|
|
7
|
+
codex: 'Codex',
|
|
8
|
+
antigravity: 'Antigravity',
|
|
9
|
+
});
|
|
10
|
+
// Claude reads project skills from .claude/skills; Codex and Antigravity share .agents/skills.
|
|
11
|
+
const folderOf = (agent) => (agent === 'claude' ? 'claude' : 'codex');
|
|
12
|
+
const FOLDER_PATHS = Object.freeze({ claude: '.claude/skills', codex: '.agents/skills' });
|
|
13
|
+
const PENDING_LINKS = new Set(['missing', 'replace']);
|
|
14
|
+
|
|
15
|
+
export function joinNames(names) {
|
|
16
|
+
return names.length <= 1 ? names.join('') : `${names.slice(0, -1).join(', ')} y ${names.at(-1)}`;
|
|
17
|
+
}
|
|
18
|
+
// ~/… for paths under the home folder, with either separator (Windows uses \\).
|
|
19
|
+
export const shortPath = (target, homeDirectory) =>
|
|
20
|
+
homeDirectory &&
|
|
21
|
+
(target === homeDirectory ||
|
|
22
|
+
target.startsWith(`${homeDirectory}/`) ||
|
|
23
|
+
target.startsWith(`${homeDirectory}\\`))
|
|
24
|
+
? `~${target.slice(homeDirectory.length)}`
|
|
25
|
+
: target;
|
|
26
|
+
const plural = (count, one, many) => (count === 1 ? one : many);
|
|
27
|
+
|
|
28
|
+
// "frontend usa React", "el README menciona Tauri": the reason for a suggestion, from where the signal was found.
|
|
29
|
+
export function shortReason(label, source) {
|
|
30
|
+
const file = source.replace(/\s*\(.*\)$/u, '');
|
|
31
|
+
const parts = file.split('/');
|
|
32
|
+
const folder = parts.length > 1 ? parts.slice(0, -1).join('/') : null;
|
|
33
|
+
if (/^readme\.md$/iu.test(parts.at(-1)))
|
|
34
|
+
return folder ? `${folder}/README menciona ${label}` : `el README menciona ${label}`;
|
|
35
|
+
return folder ? `${folder} usa ${label}` : `el proyecto usa ${label}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const linkStates = (skill, agents) =>
|
|
39
|
+
[...new Set(agents.map(folderOf))].map((folder) => [folder, skill.links?.[folder] ?? 'missing']);
|
|
40
|
+
|
|
41
|
+
export function suggestedAnswers(state) {
|
|
42
|
+
const names = new Set(state.skills.map((skill) => skill.name));
|
|
43
|
+
const saved = state.savedAnswers;
|
|
44
|
+
if (saved) {
|
|
45
|
+
const answered = new Set([...saved.skills.chosen, ...saved.skills.discarded]);
|
|
46
|
+
const chosen = state.skills
|
|
47
|
+
.filter(
|
|
48
|
+
(skill) => saved.skills.chosen.includes(skill.name) || (skill.suggested && !answered.has(skill.name)),
|
|
49
|
+
)
|
|
50
|
+
.map((skill) => skill.name);
|
|
51
|
+
return {
|
|
52
|
+
version: 1,
|
|
53
|
+
skills: { chosen, discarded: saved.skills.discarded.filter((name) => names.has(name)) },
|
|
54
|
+
agents: [...saved.agents],
|
|
55
|
+
memory: { ...saved.memory },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const installed = (skill) => Object.values(skill.links ?? {}).includes('installed');
|
|
59
|
+
const found = AGENTS.filter((agent) => state.agentsFound[agent]);
|
|
60
|
+
const memory = state.memory.existing
|
|
61
|
+
? { decision: 'path', path: state.memory.existing.vaultPath }
|
|
62
|
+
: state.project.git
|
|
63
|
+
? { decision: 'path', path: state.memory.suggestedPath }
|
|
64
|
+
: { decision: 'later' };
|
|
65
|
+
return {
|
|
66
|
+
version: 1,
|
|
67
|
+
skills: {
|
|
68
|
+
chosen: state.skills.filter((skill) => skill.suggested || installed(skill)).map((skill) => skill.name),
|
|
69
|
+
discarded: [],
|
|
70
|
+
},
|
|
71
|
+
agents: found.length ? found : [...AGENTS],
|
|
72
|
+
memory,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const sameAnswers = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
77
|
+
|
|
78
|
+
export function buildConfirmation(state, answers) {
|
|
79
|
+
const lines = [];
|
|
80
|
+
const blocks = [];
|
|
81
|
+
const add = (mark, text) => lines.push({ mark, text });
|
|
82
|
+
const block = (id, label) => blocks.push({ id, label });
|
|
83
|
+
const answersChanged = !state.savedAnswers || !sameAnswers(state.savedAnswers, answers);
|
|
84
|
+
const agents = AGENTS.filter((agent) => answers.agents.includes(agent));
|
|
85
|
+
const home = state.homeDirectory;
|
|
86
|
+
|
|
87
|
+
if (state.stableCopy.needed) {
|
|
88
|
+
add('+', 'Copia estable de nexakit en ~/.nexakit');
|
|
89
|
+
block('stable-copy', 'Copia estable de nexakit');
|
|
90
|
+
}
|
|
91
|
+
if (['falta', 'distinto'].includes(state.stableCommand.state)) {
|
|
92
|
+
add('+', 'Orden nexakit para tus IA');
|
|
93
|
+
block('stable-command', 'Orden nexakit');
|
|
94
|
+
} else if (state.stableCommand.state === 'conflicto') {
|
|
95
|
+
add('!', '~/.nexakit/nexakit existe y no es de nexakit; no lo toco');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Project skills, in the folders of the chosen agents.
|
|
99
|
+
const chosen = state.skills.filter((skill) => answers.skills.chosen.includes(skill.name));
|
|
100
|
+
const toInstall = [],
|
|
101
|
+
conflicts = [],
|
|
102
|
+
readySkills = [];
|
|
103
|
+
for (const skill of chosen) {
|
|
104
|
+
// Per folder: a user folder is left alone, and the skill is still installed where it is missing.
|
|
105
|
+
const states = linkStates(skill, agents);
|
|
106
|
+
for (const [folder, value] of states) if (value === 'conflict') conflicts.push([skill, folder]);
|
|
107
|
+
if (states.some(([, value]) => PENDING_LINKS.has(value))) toInstall.push(skill);
|
|
108
|
+
else if (states.every(([, value]) => value === 'installed')) readySkills.push(skill);
|
|
109
|
+
}
|
|
110
|
+
const removeSkills = state.skills
|
|
111
|
+
.filter(
|
|
112
|
+
(skill) =>
|
|
113
|
+
!answers.skills.chosen.includes(skill.name) && Object.values(skill.links ?? {}).includes('installed'),
|
|
114
|
+
)
|
|
115
|
+
.map((skill) => skill.name);
|
|
116
|
+
if (toInstall.length) {
|
|
117
|
+
add(
|
|
118
|
+
'+',
|
|
119
|
+
`${toInstall.length} ${plural(toInstall.length, 'skill en el proyecto (ignorada en Git)', 'skills en el proyecto (ignoradas en Git)')}`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
if (removeSkills.length)
|
|
123
|
+
add('−', `Quitar ${removeSkills.length} ${plural(removeSkills.length, 'skill', 'skills')} del proyecto`);
|
|
124
|
+
for (const [skill, folder] of conflicts) {
|
|
125
|
+
add('!', `${skill.name}: ya hay una carpeta tuya con ese nombre en ${FOLDER_PATHS[folder]}; no la toco`);
|
|
126
|
+
}
|
|
127
|
+
if (!toInstall.length && readySkills.length) add('✓', 'Skills del proyecto ya estaban listas');
|
|
128
|
+
if (toInstall.length || removeSkills.length) block('project-skills', 'Skills del proyecto');
|
|
129
|
+
|
|
130
|
+
// Base skills of each chosen agent: only what is missing or repairable; conflicts are reported.
|
|
131
|
+
const globalBlocks = [];
|
|
132
|
+
for (const agent of agents) {
|
|
133
|
+
const global = state.globalSkills[agent] ?? { pending: 0, conflicts: 0 };
|
|
134
|
+
if (global.retiredCount)
|
|
135
|
+
add('−', `Quitar ${global.retiredCount} comandos antiguos de ${AGENT_NAMES[agent]}`);
|
|
136
|
+
if (global.retiredCount && !global.pending) globalBlocks.push(agent);
|
|
137
|
+
if (global.pending) {
|
|
138
|
+
add(
|
|
139
|
+
'+',
|
|
140
|
+
`${global.pending} ${plural(global.pending, 'skill base', 'skills base')} en ${AGENT_NAMES[agent]}`,
|
|
141
|
+
);
|
|
142
|
+
globalBlocks.push(agent);
|
|
143
|
+
}
|
|
144
|
+
if (global.conflicts) {
|
|
145
|
+
add(
|
|
146
|
+
'!',
|
|
147
|
+
`${global.conflicts} ${plural(global.conflicts, 'skill base', 'skills base')} de ${AGENT_NAMES[agent]} ${plural(global.conflicts, 'choca con una carpeta tuya; no la toco', 'chocan con carpetas tuyas; no las toco')}`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
for (const agent of globalBlocks) block(`global-skills:${agent}`, `Skills base en ${AGENT_NAMES[agent]}`);
|
|
152
|
+
|
|
153
|
+
// Superpowers is required: the work process (brainstorming, plans, debugging, TDD, verification) comes from it.
|
|
154
|
+
const superpowers = state.superpowers ?? {};
|
|
155
|
+
const activeIn = [];
|
|
156
|
+
for (const agent of agents.filter((item) => Object.hasOwn(superpowers, item))) {
|
|
157
|
+
const status = superpowers[agent];
|
|
158
|
+
if (status === 'activo') activeIn.push(AGENT_NAMES[agent]);
|
|
159
|
+
else if (status === 'falta') {
|
|
160
|
+
add('+', `Superpowers en ${AGENT_NAMES[agent]}`);
|
|
161
|
+
block(`superpowers:${agent}`, `Superpowers en ${AGENT_NAMES[agent]}`);
|
|
162
|
+
} else if (status === 'desactivado' && agent !== 'codex') {
|
|
163
|
+
add('↻', `Activar Superpowers en ${AGENT_NAMES[agent]}`);
|
|
164
|
+
block(`superpowers:${agent}`, `Superpowers activo en ${AGENT_NAMES[agent]}`);
|
|
165
|
+
} else if (status === 'desactivado') {
|
|
166
|
+
add('!', `Superpowers está desactivado en ${AGENT_NAMES[agent]}; actívalo desde /plugins`);
|
|
167
|
+
} else {
|
|
168
|
+
add('!', `No pude comprobar Superpowers en ${AGENT_NAMES[agent]}; revisa ${agent === 'claude' ? '/plugin' : '/plugins'}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (activeIn.length) add('✓', `Superpowers activo en ${joinNames(activeIn)}`);
|
|
172
|
+
|
|
173
|
+
// Memory: create it if needed, then connect it to each chosen agent.
|
|
174
|
+
if (answers.memory.decision === 'path') {
|
|
175
|
+
const target = answers.memory.path;
|
|
176
|
+
const kind = state.memory.kinds?.[target] ?? 'new';
|
|
177
|
+
if (kind === 'invalid') {
|
|
178
|
+
add('!', `${shortPath(target, home)} no está vacía y no es una memoria; elige otra carpeta`);
|
|
179
|
+
} else if (kind === 'foreign') {
|
|
180
|
+
add('!', `${shortPath(target, home)} es la memoria de otro proyecto; elige otra carpeta`);
|
|
181
|
+
} else {
|
|
182
|
+
if (kind === 'new') {
|
|
183
|
+
add('+', `Memoria nueva en ${shortPath(target, home)}`);
|
|
184
|
+
block('memory-init', 'Memoria creada');
|
|
185
|
+
} else {
|
|
186
|
+
add('✓', `Memoria del proyecto en ${shortPath(target, home)}`);
|
|
187
|
+
}
|
|
188
|
+
const connections = state.memory.connections?.[target] ?? {};
|
|
189
|
+
const statusOf = (agent) => connections[agent] ?? (state.agentsFound[agent] ? 'create' : 'unavailable');
|
|
190
|
+
const need = agents.filter((agent) => statusOf(agent) === 'create');
|
|
191
|
+
const connected = agents.filter((agent) => statusOf(agent) === 'unchanged');
|
|
192
|
+
if (need.length)
|
|
193
|
+
add('+', `Conectar la memoria a ${joinNames(need.map((agent) => AGENT_NAMES[agent]))}`);
|
|
194
|
+
else if (connected.length)
|
|
195
|
+
add('✓', `Memoria conectada a ${joinNames(connected.map((agent) => AGENT_NAMES[agent]))}`);
|
|
196
|
+
for (const agent of agents) {
|
|
197
|
+
if (statusOf(agent) === 'conflict')
|
|
198
|
+
add('!', `${AGENT_NAMES[agent]} ya tiene otra memoria con el mismo nombre; no la toco`);
|
|
199
|
+
if (statusOf(agent) === 'unavailable')
|
|
200
|
+
add('!', `No encontré ${AGENT_NAMES[agent]}; no puedo conectarle la memoria`);
|
|
201
|
+
}
|
|
202
|
+
for (const agent of need) block(`memory-connect:${agent}`, `Memoria conectada a ${AGENT_NAMES[agent]}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const instructionNames = new Set(
|
|
207
|
+
agents.map((agent) => ({ claude: 'CLAUDE.md', codex: 'AGENTS.md', antigravity: 'GEMINI.md' })[agent]),
|
|
208
|
+
);
|
|
209
|
+
const instructionItems = (state.reviewInstructions?.items ?? []).filter((item) =>
|
|
210
|
+
instructionNames.has(item.name),
|
|
211
|
+
);
|
|
212
|
+
for (const item of instructionItems.filter((item) => item.state === 'conflict'))
|
|
213
|
+
add('!', `${item.name}: las instrucciones existentes no se pueden actualizar; no las toco`);
|
|
214
|
+
const pendingInstructions = instructionItems.filter((item) => item.state === 'pending');
|
|
215
|
+
if (pendingInstructions.length) {
|
|
216
|
+
add(
|
|
217
|
+
'+',
|
|
218
|
+
`Reglas de trabajo en tus instrucciones de ${joinNames(pendingInstructions.map((item) => AGENT_NAMES[item.client]))}`,
|
|
219
|
+
);
|
|
220
|
+
block('review-instructions', 'Reglas de trabajo de tus IA');
|
|
221
|
+
}
|
|
222
|
+
if (answersChanged && (blocks.length || state.savedAnswers)) {
|
|
223
|
+
// Saved first, before any block that can fail, so a retry starts from what the person decided.
|
|
224
|
+
blocks.unshift({ id: 'answers', label: 'Respuestas guardadas' });
|
|
225
|
+
if (!blocks.some((item) => item.id === 'project-skills'))
|
|
226
|
+
add('+', 'Guardar tus respuestas en .agent-toolkit (ignorado en Git)');
|
|
227
|
+
}
|
|
228
|
+
if (blocks.length) block('final-check', 'Comprobación final');
|
|
229
|
+
const allReady = !lines.some((line) => ['+', '−', '↻'].includes(line.mark));
|
|
230
|
+
return { lines, blocks, allReady, removeSkills };
|
|
231
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
// Gathers, read-only, everything the setup questions and the confirmation need: the project and what it uses, the
|
|
2
|
+
// skills and their links, the agents installed, their base skills, the stable command and the project memory.
|
|
3
|
+
// Nothing here writes; setup-apply.js does, after the person confirms.
|
|
4
|
+
import fs from 'node:fs/promises';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import { planReviewInstructions } from './review-instructions.js';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { pathToFileURL } from 'node:url';
|
|
9
|
+
import { connectMemory } from './connect.js';
|
|
10
|
+
import { ACTIONABLE_LINK_STATES } from './link-states.js';
|
|
11
|
+
import {
|
|
12
|
+
findKnownMemories,
|
|
13
|
+
memoryForRepository,
|
|
14
|
+
registeredServers,
|
|
15
|
+
suggestMemoryLocation,
|
|
16
|
+
} from './memory-locator.js';
|
|
17
|
+
import { AGENTS, shortReason } from './setup-plan.js';
|
|
18
|
+
import { loadSkillCatalog } from './skill-catalog.js';
|
|
19
|
+
import {
|
|
20
|
+
inspectSkillProject,
|
|
21
|
+
planProjectSkillInstall,
|
|
22
|
+
readSetupAnswers,
|
|
23
|
+
recommendSkills,
|
|
24
|
+
uncoveredSignals,
|
|
25
|
+
} from './skill-recommendations.js';
|
|
26
|
+
import { planStableCommand } from './stable-command.js';
|
|
27
|
+
import { inspectSuperpowers } from './superpowers.js';
|
|
28
|
+
import { compareVersions, readActualVersion } from './stable-install.js';
|
|
29
|
+
import {
|
|
30
|
+
TOOLKIT_ROOT,
|
|
31
|
+
executable,
|
|
32
|
+
packageVersion,
|
|
33
|
+
resolveNodeExecutable,
|
|
34
|
+
setupToolkit,
|
|
35
|
+
} from './toolkit-setup.js';
|
|
36
|
+
|
|
37
|
+
const CONFLICT_LINK_STATES = new Set(['conflicto', 'roto-ajeno']);
|
|
38
|
+
|
|
39
|
+
// What is at a memory path: this repository's memory, a place for a new one, another project's memory ('foreign'),
|
|
40
|
+
// or something else ('invalid'). Only 'memory' and 'new' are ever written; the other two are left alone.
|
|
41
|
+
export async function inspectMemoryPath(target, { repositoryRoot } = {}) {
|
|
42
|
+
const info = await fs
|
|
43
|
+
.lstat(target)
|
|
44
|
+
.catch((error) => (error.code === 'ENOENT' ? null : Promise.reject(error)));
|
|
45
|
+
if (!info) return 'new';
|
|
46
|
+
if (!info.isDirectory() || info.isSymbolicLink()) return 'invalid';
|
|
47
|
+
const manifestPath = path.join(target, 'project-memory.json');
|
|
48
|
+
const manifest = await fs.lstat(manifestPath).catch(() => null);
|
|
49
|
+
if (manifest?.isFile() && !manifest.isSymbolicLink()) {
|
|
50
|
+
if (!repositoryRoot) return 'memory';
|
|
51
|
+
try {
|
|
52
|
+
const source = JSON.parse(await fs.readFile(manifestPath, 'utf8'))?.source?.repositoryPath;
|
|
53
|
+
const real =
|
|
54
|
+
typeof source === 'string' ? await fs.realpath(source).catch(() => path.resolve(source)) : null;
|
|
55
|
+
return real && real === (await fs.realpath(repositoryRoot).catch(() => repositoryRoot))
|
|
56
|
+
? 'memory'
|
|
57
|
+
: 'foreign';
|
|
58
|
+
} catch {
|
|
59
|
+
return 'invalid';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return (await fs.readdir(target)).length === 0 ? 'new' : 'invalid';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function readMemoryManifest(vaultPath) {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(await fs.readFile(path.join(vaultPath, 'project-memory.json'), 'utf8'));
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// For an agent the memory does not reach yet: its MCP name must be free in that agent, or already serve this vault.
|
|
74
|
+
async function newConnection(agent, { mcpName, vaultPath, servers, agentsFound }) {
|
|
75
|
+
if (!agentsFound[agent]) return 'unavailable';
|
|
76
|
+
if (!Object.hasOwn(servers[agent] ?? {}, mcpName)) return 'create';
|
|
77
|
+
// Taken by this same vault with the server this agent's connection uses (Antigravity: nexakit's read-only
|
|
78
|
+
// server; Claude and Codex: server-filesystem) is fine; anything else under that name is a conflict.
|
|
79
|
+
const real = await fs.realpath(vaultPath).catch(() => path.resolve(vaultPath));
|
|
80
|
+
const current = servers[agent][mcpName];
|
|
81
|
+
const expected = agent === 'antigravity' ? 'guard' : 'filesystem';
|
|
82
|
+
return current?.vault === real && current.kind === expected ? 'create' : 'conflict';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// How an existing memory is connected to each agent: unchanged, create, conflict or unavailable (no CLI). An agent
|
|
86
|
+
// the memory does not declare yet (an older memory without Antigravity) is "create": connecting it declares it.
|
|
87
|
+
export async function memoryConnections(
|
|
88
|
+
vaultPath,
|
|
89
|
+
{ homeDirectory, codexHome, runCommand, nodePath, toolkitRoot, agentsFound, kind = 'memory', mcpName },
|
|
90
|
+
) {
|
|
91
|
+
const servers = await registeredServers({ homeDirectory, ...(codexHome && { codexHome }) });
|
|
92
|
+
const manifest = kind === 'memory' ? await readMemoryManifest(vaultPath) : null;
|
|
93
|
+
const name = manifest?.memory?.mcpServerName ?? mcpName;
|
|
94
|
+
const declared = (manifest?.agents ?? []).filter((agent) => agent.enabled).map((agent) => agent.id);
|
|
95
|
+
const result = {};
|
|
96
|
+
for (const agent of AGENTS) {
|
|
97
|
+
if (!declared.includes(agent)) {
|
|
98
|
+
result[agent] = await newConnection(agent, { mcpName: name, vaultPath, servers, agentsFound });
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const report = await connectMemory(
|
|
103
|
+
{ targetPath: vaultPath, agentId: agent },
|
|
104
|
+
{ homeDirectory, nodePath, toolkitRoot, ...(runCommand && { runCommand }) },
|
|
105
|
+
);
|
|
106
|
+
const mcp = report.components.find((component) => component.id === 'mcp');
|
|
107
|
+
if (/No se encontró el comando/u.test(mcp?.message ?? '')) result[agent] = 'unavailable';
|
|
108
|
+
else if (report.components.some((component) => component.action === 'conflict'))
|
|
109
|
+
result[agent] = 'conflict';
|
|
110
|
+
else
|
|
111
|
+
result[agent] = report.components.every((component) => component.action === 'unchanged')
|
|
112
|
+
? 'unchanged'
|
|
113
|
+
: 'create';
|
|
114
|
+
} catch {
|
|
115
|
+
result[agent] = 'conflict';
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function gatherSetupState({
|
|
122
|
+
start = process.cwd(),
|
|
123
|
+
homeDirectory = os.homedir(),
|
|
124
|
+
environment = process.env,
|
|
125
|
+
toolkitRoot = TOOLKIT_ROOT,
|
|
126
|
+
codexHome = environment.CODEX_HOME ?? path.join(homeDirectory, '.codex'),
|
|
127
|
+
runCommand,
|
|
128
|
+
memoryPaths = [],
|
|
129
|
+
} = {}) {
|
|
130
|
+
const catalog = await loadSkillCatalog(
|
|
131
|
+
toolkitRoot === TOOLKIT_ROOT
|
|
132
|
+
? {}
|
|
133
|
+
: { catalogUrl: pathToFileURL(path.join(toolkitRoot, 'catalogs/skills.json')) },
|
|
134
|
+
);
|
|
135
|
+
const inspection = await inspectSkillProject(start, { toolkitRoot, catalog, homeDirectory });
|
|
136
|
+
const project = inspection.project;
|
|
137
|
+
const recommendation = recommendSkills({ catalog, inspection });
|
|
138
|
+
const projectPlan = await planProjectSkillInstall({
|
|
139
|
+
projectRoot: project.root,
|
|
140
|
+
skillNames: catalog.skills.map((skill) => skill.name),
|
|
141
|
+
agent: 'all',
|
|
142
|
+
mode: 'symlink',
|
|
143
|
+
catalog,
|
|
144
|
+
toolkitRoot,
|
|
145
|
+
homeDirectory,
|
|
146
|
+
});
|
|
147
|
+
const signalById = new Map(inspection.signals.map((signal) => [signal.id, signal]));
|
|
148
|
+
const linkState = (name, client) =>
|
|
149
|
+
projectPlan.links.find((link) => link.name === name && link.client === client)?.state ?? 'missing';
|
|
150
|
+
const skills = [
|
|
151
|
+
...recommendation.recommended,
|
|
152
|
+
...recommendation.candidates,
|
|
153
|
+
...recommendation.optional,
|
|
154
|
+
].map((entry) => {
|
|
155
|
+
const first = entry.matchedSignals?.[0];
|
|
156
|
+
const signal = first && signalById.get(first);
|
|
157
|
+
return {
|
|
158
|
+
name: entry.name,
|
|
159
|
+
summary: entry.summary,
|
|
160
|
+
reason: signal ? shortReason(signal.label, signal.sources[0]) : null,
|
|
161
|
+
matchCount: entry.matchedSignals?.length ?? 0,
|
|
162
|
+
suggested: (entry.matchedSignals?.length ?? 0) > 0,
|
|
163
|
+
license: entry.license,
|
|
164
|
+
tested: entry.recommendationStatus === 'validated',
|
|
165
|
+
reviewNotes: entry.reviewStatus === 'reviewed-with-observations' ? entry.reviewNotes : null,
|
|
166
|
+
links: { claude: linkState(entry.name, 'claude'), codex: linkState(entry.name, 'codex') },
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const setup = await setupToolkit({
|
|
171
|
+
agent: 'all',
|
|
172
|
+
apply: false,
|
|
173
|
+
homeDirectory,
|
|
174
|
+
toolkitRoot,
|
|
175
|
+
environment,
|
|
176
|
+
codexHome,
|
|
177
|
+
});
|
|
178
|
+
const globalSkills = Object.fromEntries(
|
|
179
|
+
AGENTS.map((agent) => {
|
|
180
|
+
const items = setup.skills.filter((item) => item.client === agent);
|
|
181
|
+
return [
|
|
182
|
+
agent,
|
|
183
|
+
{
|
|
184
|
+
pending: items.filter((item) => ACTIONABLE_LINK_STATES.includes(item.state)).length,
|
|
185
|
+
conflicts:
|
|
186
|
+
items.filter((item) => CONFLICT_LINK_STATES.has(item.state)).length +
|
|
187
|
+
(setup.retiredSkills ?? []).filter((item) => item.client === agent && !item.owned).length,
|
|
188
|
+
},
|
|
189
|
+
];
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
192
|
+
const agentsFound = {
|
|
193
|
+
claude: await executable('claude', environment),
|
|
194
|
+
codex: await executable('codex', environment),
|
|
195
|
+
antigravity: setup.clients.includes('antigravity'),
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const version = await packageVersion(toolkitRoot);
|
|
199
|
+
const actualVersion = setup.ephemeral ? await readActualVersion(homeDirectory) : null;
|
|
200
|
+
for (const agent of Object.keys(globalSkills))
|
|
201
|
+
globalSkills[agent].retiredCount = (setup.retiredSkills ?? []).filter(
|
|
202
|
+
(s) => s.client === agent && s.owned,
|
|
203
|
+
).length;
|
|
204
|
+
const stableCopy = {
|
|
205
|
+
needed: Boolean(setup.ephemeral) && (!actualVersion || compareVersions(actualVersion, version) < 0),
|
|
206
|
+
};
|
|
207
|
+
const nodePath = await resolveNodeExecutable(environment);
|
|
208
|
+
const stableCommand = await planStableCommand({ homeDirectory, nodePath, toolkitRoot: setup.linkRoot });
|
|
209
|
+
|
|
210
|
+
const savedAnswers = await readSetupAnswers(project.root);
|
|
211
|
+
const known = await findKnownMemories({ homeDirectory, codexHome });
|
|
212
|
+
const existingEntry = memoryForRepository(known, project.root);
|
|
213
|
+
const suggestedPath = suggestMemoryLocation(known, { id: project.id, homeDirectory });
|
|
214
|
+
const memory = {
|
|
215
|
+
existing: existingEntry && { vaultPath: existingEntry.vaultPath },
|
|
216
|
+
suggestedPath,
|
|
217
|
+
kinds: {},
|
|
218
|
+
connections: {},
|
|
219
|
+
};
|
|
220
|
+
// The chosen path is always inspected, also one typed in "otra carpeta…" (memoryPaths).
|
|
221
|
+
const candidates = [
|
|
222
|
+
existingEntry?.vaultPath,
|
|
223
|
+
suggestedPath,
|
|
224
|
+
savedAnswers?.memory?.path,
|
|
225
|
+
...memoryPaths,
|
|
226
|
+
].filter(Boolean);
|
|
227
|
+
const context = {
|
|
228
|
+
homeDirectory,
|
|
229
|
+
codexHome,
|
|
230
|
+
runCommand,
|
|
231
|
+
nodePath,
|
|
232
|
+
toolkitRoot: setup.linkRoot,
|
|
233
|
+
agentsFound,
|
|
234
|
+
mcpName: `${project.id}-memory`,
|
|
235
|
+
};
|
|
236
|
+
for (const candidate of [...new Set(candidates)]) {
|
|
237
|
+
const kind = await inspectMemoryPath(candidate, { repositoryRoot: project.root });
|
|
238
|
+
memory.kinds[candidate] = kind;
|
|
239
|
+
if (kind === 'memory' || kind === 'new')
|
|
240
|
+
memory.connections[candidate] = await memoryConnections(candidate, { ...context, kind });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const reviewInstructions = await planReviewInstructions({ homeDirectory, environment, codexHome });
|
|
244
|
+
const superpowers = await inspectSuperpowers({ agentsFound, runCommand, environment });
|
|
245
|
+
return {
|
|
246
|
+
reviewInstructions,
|
|
247
|
+
superpowers,
|
|
248
|
+
homeDirectory,
|
|
249
|
+
version,
|
|
250
|
+
project,
|
|
251
|
+
signals: inspection.signals,
|
|
252
|
+
skills,
|
|
253
|
+
// Suggested but installed outside NexaKit (for example as a Claude plugin): listed, never selectable.
|
|
254
|
+
externalSkills: recommendation.externalCandidates.map(ref => ({ name: ref.name, reason: ref.reasons[0] ?? null })),
|
|
255
|
+
// Found in the project but covered by no catalog skill: reported only, never searched for automatically.
|
|
256
|
+
uncoveredTech: uncoveredSignals({ signals: inspection.signals, catalog }),
|
|
257
|
+
agentsFound,
|
|
258
|
+
globalSkills,
|
|
259
|
+
stableCopy,
|
|
260
|
+
stableCommand,
|
|
261
|
+
memory,
|
|
262
|
+
savedAnswers,
|
|
263
|
+
// For setup-apply.js and the detail screen; not part of the summary.
|
|
264
|
+
internal: {
|
|
265
|
+
catalog,
|
|
266
|
+
inspection,
|
|
267
|
+
projectPlan,
|
|
268
|
+
setup,
|
|
269
|
+
nodePath,
|
|
270
|
+
toolkitRoot,
|
|
271
|
+
linkRoot: setup.linkRoot,
|
|
272
|
+
codexHome,
|
|
273
|
+
environment,
|
|
274
|
+
runCommand,
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
}
|