synthesisui 0.16.413 → 0.16.415
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/agent-wiring.js +40 -22
- package/dist/agents-chosen.js +246 -0
- package/dist/claude-md.js +32 -27
- package/dist/codex-mcp.js +30 -6
- package/dist/commands/add.js +2 -1
- package/dist/commands/adopt.js +20 -4
- package/dist/commands/align.js +9 -3
- package/dist/commands/connect.js +211 -15
- package/dist/commands/upgrade.js +13 -1
- package/dist/copy/connect.pt-BR.js +22 -1
- package/dist/index.js +3 -1
- package/dist/install-marks.js +28 -1
- package/dist/memory/availability.js +40 -1
- package/package.json +1 -1
package/dist/agent-wiring.js
CHANGED
|
@@ -150,16 +150,18 @@ async function wireHook(root, command) {
|
|
|
150
150
|
/**
|
|
151
151
|
* O MESMO SERVIDOR MCP, NOS CAMINHOS QUE CADA AGENTE LÊ.
|
|
152
152
|
*
|
|
153
|
-
* `.mcp.json` é o escopo-de-projeto do Claude Code
|
|
153
|
+
* `.mcp.json` é o escopo-de-projeto do Claude Code. O Cursor lia `.cursor/mcp.json` com o mesmo shape
|
|
154
154
|
* (`mcpServers`). Então cobrir os dois é o mesmo JSON num segundo caminho, e as ferramentas passam
|
|
155
155
|
* a existir nos dois editores sem uma linha de motor novo - era a lacuna mais barata da análise de DX
|
|
156
156
|
* (05/08).
|
|
157
157
|
*
|
|
158
|
-
* O
|
|
159
|
-
*
|
|
160
|
-
*
|
|
158
|
+
* O CURSOR SAIU EM 10/09, por decisão dele: a lista do produto é Claude Code e Codex (ver
|
|
159
|
+
* `agents-chosen.ts` e `cursor-is-retired.spec.ts`). O que sobreviveu daquela linha é a regra que
|
|
160
|
+
* ela ensinou - escrever arquivo de um editor que a pessoa não usa é a primeira coisa que alguém
|
|
161
|
+
* apaga com raiva -, e ela agora vale para TODOS, inclusive o Claude Code: quem decide é a escolha
|
|
162
|
+
* dele, e a pasta apenas pré-marca.
|
|
161
163
|
*/
|
|
162
|
-
async function wireMcpAt(root,
|
|
164
|
+
async function wireMcpAt(root, rel) {
|
|
163
165
|
const path = join(root, rel);
|
|
164
166
|
const config = await readJson(path);
|
|
165
167
|
const servers = (config.mcpServers ?? {});
|
|
@@ -209,19 +211,24 @@ async function wireMcpAt(root, version, rel) {
|
|
|
209
211
|
return replacing ? "updated" : "added";
|
|
210
212
|
}
|
|
211
213
|
/**
|
|
212
|
-
* Os caminhos de MCP que
|
|
213
|
-
*
|
|
214
|
+
* Os caminhos de MCP que ELE ESCOLHEU - um por agente marcado, e nada dos outros. Devolve o que foi
|
|
215
|
+
* escrito, por caminho, para o `connect` poder dizer os nomes.
|
|
214
216
|
*/
|
|
215
|
-
|
|
217
|
+
/**
|
|
218
|
+
* E NENHUM DOS TRÊS RECEBE VERSÃO, desde 10/09 - o parâmetro morreu quando o Codex passou a seguir
|
|
219
|
+
* a régua de `@latest` (ver `codex-mcp.ts`). Os escritores de JSON nunca a usaram: a decisão de
|
|
220
|
+
* 06/08 é que o MCP flutua. Um argumento que ninguém lê é o próximo a ser lido por engano.
|
|
221
|
+
*/
|
|
222
|
+
async function wireMcp(root, agents) {
|
|
216
223
|
const wrote = [];
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (
|
|
224
|
+
/**
|
|
225
|
+
* O `.mcp.json` É DO CLAUDE CODE, e por isso ele entrou na régua (A2 da etapa 12). Era o único
|
|
226
|
+
* escritor sem condição nenhuma: num repositório que só usa Codex, ele nascia de todo jeito.
|
|
227
|
+
*/
|
|
228
|
+
if (agents.includes("claude"))
|
|
222
229
|
wrote.push({
|
|
223
|
-
path: ".
|
|
224
|
-
status: await wireMcpAt(root,
|
|
230
|
+
path: ".mcp.json",
|
|
231
|
+
status: await wireMcpAt(root, ".mcp.json"),
|
|
225
232
|
});
|
|
226
233
|
/**
|
|
227
234
|
* E O CODEX, quando `.codex/` já existe - a mesma regra de evidência que decide o Cursor e a casa
|
|
@@ -231,10 +238,10 @@ async function wireMcp(root, version) {
|
|
|
231
238
|
* região delimitada num TOML que a pessoa também edita. Ver `codex-mcp.ts` para o porquê de não
|
|
232
239
|
* haver parser.
|
|
233
240
|
*/
|
|
234
|
-
if (
|
|
241
|
+
if (agents.includes("codex"))
|
|
235
242
|
wrote.push({
|
|
236
243
|
path: ".codex/config.toml",
|
|
237
|
-
status: await wireCodexMcp(root
|
|
244
|
+
status: await wireCodexMcp(root),
|
|
238
245
|
});
|
|
239
246
|
return wrote;
|
|
240
247
|
}
|
|
@@ -256,11 +263,13 @@ let codexWas = null;
|
|
|
256
263
|
export function codexPinBefore() {
|
|
257
264
|
return codexWas;
|
|
258
265
|
}
|
|
259
|
-
async function wireCodexMcp(root
|
|
266
|
+
async function wireCodexMcp(root) {
|
|
260
267
|
const path = join(root, ".codex", "config.toml");
|
|
261
268
|
const current = await readFile(path, "utf8").catch(() => null);
|
|
262
269
|
const had = current != null && hasCodexBlock(current);
|
|
263
|
-
|
|
270
|
+
/** A MESMA pergunta que o escritor de JSON faz - ver `codexBlock`. */
|
|
271
|
+
const local = await exists(join(root, "node_modules", ".bin", "synthesisui"));
|
|
272
|
+
const next = withCodexBlock(current ?? "", local);
|
|
264
273
|
if (current === next)
|
|
265
274
|
return "already there";
|
|
266
275
|
/**
|
|
@@ -274,8 +283,17 @@ async function wireCodexMcp(root, version) {
|
|
|
274
283
|
return had ? "updated" : "added";
|
|
275
284
|
}
|
|
276
285
|
export async function wireAgent(root, version, want) {
|
|
286
|
+
/**
|
|
287
|
+
* A ESCOLHA DELE DECIDE QUEM É FIADO (A2 da etapa 12) - ver `agents-chosen.ts`.
|
|
288
|
+
*
|
|
289
|
+
* O hook e a verificação de abertura moram no `.claude/settings.json`, então eles são do Claude
|
|
290
|
+
* Code: num repositório onde ele não escolheu o Claude Code, escrever aquele arquivo é plantar a
|
|
291
|
+
* fiação de uma ferramenta que ninguém pediu. Medido em 10/09: eram 8 arquivos dele num repo que
|
|
292
|
+
* só provava o Codex.
|
|
293
|
+
*/
|
|
294
|
+
const claude = want.agents.includes("claude");
|
|
277
295
|
const proposed = await hookCommand(root, version);
|
|
278
|
-
const hook = want.hook
|
|
296
|
+
const hook = want.hook && claude
|
|
279
297
|
? await wireHook(root, proposed)
|
|
280
298
|
: { status: "skipped", command: proposed, was: undefined };
|
|
281
299
|
return {
|
|
@@ -289,10 +307,10 @@ export async function wireAgent(root, version, want) {
|
|
|
289
307
|
* único hook instalado rodava DEPOIS de uma escrita - tarde demais para dizer "você não está
|
|
290
308
|
* logado" ou "este sistema não sabe de onde foi medido".
|
|
291
309
|
*/
|
|
292
|
-
session: want.hook
|
|
310
|
+
session: want.hook && claude
|
|
293
311
|
? await wireSessionStart(root, await alignCommand(root, version))
|
|
294
312
|
: "skipped",
|
|
295
|
-
mcp: want.mcp ? await wireMcp(root,
|
|
313
|
+
mcp: want.mcp ? await wireMcp(root, want.agents) : "skipped",
|
|
296
314
|
};
|
|
297
315
|
}
|
|
298
316
|
/**
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QUAIS AGENTES ENTRAM NO REPOSITÓRIO DELE - e a resposta é a ESCOLHA dele, não a nossa dedução.
|
|
3
|
+
*
|
|
4
|
+
* O QUE ELE GANHA: nenhum arquivo de uma ferramenta que ele não usa. MEDIDO em 10/09, com o
|
|
5
|
+
* `connect` de verdade num repositório que só tem `.codex/`: **10 arquivos escritos, e 8 do Claude
|
|
6
|
+
* Code** - `.claude/settings.json`, cinco skills, `.mcp.json` e `CLAUDE.md`. A régua da evidência
|
|
7
|
+
* já existia e protegia o Codex e o Cursor; o Claude Code era escrito sempre, sem prova nenhuma.
|
|
8
|
+
*
|
|
9
|
+
* A DECISÃO DELE, 10/09: *"seria legal o próprio connect eu selecionar quais agentes eu quero
|
|
10
|
+
* colocar"*. Então a evidência deixa de DECIDIR e passa a PRÉ-MARCAR - ela continua sendo a melhor
|
|
11
|
+
* informação que a gente tem sobre o repositório dele, e deixa de ser a palavra final.
|
|
12
|
+
*
|
|
13
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
14
|
+
* DE ONDE A ESCOLHA VEM, e a saída SEMPRE diz qual dos três foi:
|
|
15
|
+
*
|
|
16
|
+
* flag ele mandou (`--agents claude,codex`, ou `--agents none`)
|
|
17
|
+
* asked ele confirmou ou mudou a lista pré-marcada, num terminal
|
|
18
|
+
* evidence não havia ninguém para perguntar - CI, pipe, agente -, então a pasta que
|
|
19
|
+
* existe decide, e a linha DIZ que foi ela
|
|
20
|
+
*
|
|
21
|
+
* O silêncio nunca é escolha: um comando que não pode perguntar e escreve tudo é o defeito que
|
|
22
|
+
* este módulo existe para acabar, e um que não pode perguntar e não escreve nada quebraria o CI de
|
|
23
|
+
* quem já usa. A evidência é o meio-termo que já era o comportamento de duas das ferramentas.
|
|
24
|
+
*
|
|
25
|
+
* A LISTA É DO PRODUTO, e é curta de propósito: o nome de uma ferramenta não decide nada sozinho -
|
|
26
|
+
* o que decide é o que ELE escolheu. O Cursor saiu em 10/09 por decisão dele (ver
|
|
27
|
+
* `cursor-is-retired.spec.ts`), e uma quarta ferramenta é ESCOPO DE CAPACIDADE: ela entra quando o
|
|
28
|
+
* produto passar a suportá-la, e essa pergunta é dele.
|
|
29
|
+
*/
|
|
30
|
+
import { access } from "node:fs/promises";
|
|
31
|
+
import { join } from "node:path";
|
|
32
|
+
export const AGENTS = [
|
|
33
|
+
{
|
|
34
|
+
id: "claude",
|
|
35
|
+
label: "Claude Code",
|
|
36
|
+
evidence: ".claude",
|
|
37
|
+
writes: [
|
|
38
|
+
/** O hook e a verificação de abertura moram aqui, junto dos hooks DELE. */
|
|
39
|
+
{ path: ".claude/settings.json", ours: "entry" },
|
|
40
|
+
/** Junto dos outros servidores MCP dele. */
|
|
41
|
+
{ path: ".mcp.json", ours: "entry" },
|
|
42
|
+
{ path: "CLAUDE.md", ours: "block" },
|
|
43
|
+
/** A pasta é nossa inteira: o conteúdo é o nosso texto, e ninguém edita skill gerada. */
|
|
44
|
+
{ path: ".claude/skills/", ours: "file" },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "codex",
|
|
49
|
+
label: "Codex",
|
|
50
|
+
evidence: ".codex",
|
|
51
|
+
writes: [
|
|
52
|
+
/** O `[tui]` dele mora neste arquivo - ver `Ours`. */
|
|
53
|
+
{ path: ".codex/config.toml", ours: "block" },
|
|
54
|
+
{ path: "AGENTS.md", ours: "block" },
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
const exists = (p) => access(p).then(() => true, () => false);
|
|
59
|
+
/** O que o repositório PROVA - a pré-marcação, nunca a decisão. */
|
|
60
|
+
export async function detected(root) {
|
|
61
|
+
const out = [];
|
|
62
|
+
for (const agent of AGENTS)
|
|
63
|
+
if (await exists(join(root, agent.evidence)))
|
|
64
|
+
out.push(agent.id);
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* O QUE ELE DIGITOU, lido do jeito mais largo possível - `claude,codex`, `claude codex`, `CLAUDE`.
|
|
69
|
+
*
|
|
70
|
+
* Um nome que a lista não conhece é DEVOLVIDO em `unknown` em vez de ignorado: ignorar em silêncio
|
|
71
|
+
* é o que faz alguém escrever `--agents cursor` e concluir que o comando obedeceu.
|
|
72
|
+
*/
|
|
73
|
+
export function parseAgents(raw) {
|
|
74
|
+
const words = raw
|
|
75
|
+
.split(/[\s,]+/)
|
|
76
|
+
.map((w) => w.trim().toLowerCase())
|
|
77
|
+
.filter(Boolean);
|
|
78
|
+
if (words.length === 1 && words[0] === "none")
|
|
79
|
+
return { agents: [], unknown: [], none: true };
|
|
80
|
+
const agents = [];
|
|
81
|
+
const unknown = [];
|
|
82
|
+
for (const word of words) {
|
|
83
|
+
const hit = AGENTS.find((a) => a.id === word);
|
|
84
|
+
if (hit) {
|
|
85
|
+
if (!agents.includes(hit.id))
|
|
86
|
+
agents.push(hit.id);
|
|
87
|
+
}
|
|
88
|
+
else
|
|
89
|
+
unknown.push(word);
|
|
90
|
+
}
|
|
91
|
+
return { agents, unknown, none: false };
|
|
92
|
+
}
|
|
93
|
+
/** Os arquivos que a fiação de um agente escreve - `[]` para um id que a lista não conhece. */
|
|
94
|
+
export const filesOf = (id) => AGENTS.find((a) => a.id === id)?.writes ?? [];
|
|
95
|
+
/**
|
|
96
|
+
* QUAIS AGENTES JÁ ESTÃO FIADOS NESTE REPOSITÓRIO - a memória da escolha, e ela é o próprio disco.
|
|
97
|
+
*
|
|
98
|
+
* A PREMISSA, declarada na abertura da etapa 12: a escolha é lembrada pelo que EXISTE no
|
|
99
|
+
* repositório, sem arquivo de preferência nosso. Um arquivo desses no repositório dele
|
|
100
|
+
* contraria a seção 0 da jornada, e `~/.synthesisui/` é preferência de MÁQUINA - a escolha é do
|
|
101
|
+
* projeto, e vai para outra máquina com o `git clone`.
|
|
102
|
+
*
|
|
103
|
+
* O TESTE É A NOSSA MARCA, nunca a existência do arquivo. Um `CLAUDE.md` existe em muitos
|
|
104
|
+
* repositórios sem ter uma linha nossa dentro, e `.codex/config.toml` é arquivo DELE com a nossa
|
|
105
|
+
* região no meio - concluir "está fiado" de um arquivo que a gente não escreveu faria os comandos de
|
|
106
|
+
* manutenção reescreverem a fiação de um agente que ele nunca escolheu.
|
|
107
|
+
*
|
|
108
|
+
* QUEM USA ISTO: os comandos que MANTÊM o que já existe (`add`, `adopt`, `upgrade`) - eles não
|
|
109
|
+
* perguntam nada, e a resposta certa para eles é *"continue com quem já está aqui"* - e a
|
|
110
|
+
* PRÉ-MARCAÇÃO da lista do `connect`, porque desmarcar não apaga a pasta: sem isto, a rodada
|
|
111
|
+
* seguinte re-marcava o que ele tinha acabado de tirar (revisão de DX, 10/09).
|
|
112
|
+
*/
|
|
113
|
+
export async function wiredAgents(root) {
|
|
114
|
+
const { readFile } = await import("node:fs/promises");
|
|
115
|
+
const has = async (rel, mark) => {
|
|
116
|
+
const raw = await readFile(join(root, rel), "utf8").catch(() => null);
|
|
117
|
+
return raw?.includes(mark) === true;
|
|
118
|
+
};
|
|
119
|
+
const out = [];
|
|
120
|
+
if ((await has(".mcp.json", "synthesisui")) ||
|
|
121
|
+
(await has("CLAUDE.md", MARK)) ||
|
|
122
|
+
(await has(".claude/settings.json", "synthesisui")))
|
|
123
|
+
out.push("claude");
|
|
124
|
+
if ((await has(".codex/config.toml", "synthesisui")) ||
|
|
125
|
+
(await has("AGENTS.md", MARK)))
|
|
126
|
+
out.push("codex");
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
/** A marca do bloco gerido - a mesma de `claude-md.ts`, e a única prova de que o texto é nosso. */
|
|
130
|
+
const MARK = "<!-- synthesisui:start -->";
|
|
131
|
+
/**
|
|
132
|
+
* A ESCOLHA, RESOLVIDA - e a saída sempre diz de onde ela veio.
|
|
133
|
+
*
|
|
134
|
+
* Três caminhos, e a ordem é a da autoridade: o que ele MANDOU vence o que ele CONFIRMA, que vence
|
|
135
|
+
* o que a pasta prova. `ask` é injetado para o `connect` decidir se há alguém para perguntar - um
|
|
136
|
+
* módulo que consulta `process.stdin` por conta própria não tem como ser provado sem um terminal.
|
|
137
|
+
*/
|
|
138
|
+
export async function chooseAgents(input) {
|
|
139
|
+
if (input.flag != null && input.flag.trim() !== "") {
|
|
140
|
+
const read = parseAgents(input.flag);
|
|
141
|
+
return { agents: read.agents, from: "flag", unknown: read.unknown };
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* A PRÉ-MARCAÇÃO É O QUE ESTÁ FIADO, quando há algo fiado - e só então a pasta.
|
|
145
|
+
*
|
|
146
|
+
* O DEFEITO, achado pela revisão de DX no fecho: a pré-marcação usava só a pasta, e desmarcar não
|
|
147
|
+
* apaga a pasta (é a decisão dele). Então a rodada seguinte re-marcava o que ele tinha acabado de
|
|
148
|
+
* tirar, e `Enter` - que a tela chama de *"keeps this"* - revertia a escolha dele sem ele digitar
|
|
149
|
+
* nada. A premissa desta etapa já dizia o certo: *"a escolha é derivada do que EXISTE no
|
|
150
|
+
* repositório (os arquivos fiados são a memória)"*.
|
|
151
|
+
*
|
|
152
|
+
* A ordem é a da autoridade, como em todo este módulo: o que ele REGISTROU vence o que sobrou no
|
|
153
|
+
* disco, que vence o que a pasta sugere - ver `rememberedChoice`, que é o que distingue "ele
|
|
154
|
+
* desmarcou" de "sobrou arquivo". Num repositório sem fiação nenhuma - o primeiro contato - não
|
|
155
|
+
* há escolha anterior, e aí a pasta é a melhor informação que existe.
|
|
156
|
+
*/
|
|
157
|
+
const remembered = await rememberedChoice(input.root);
|
|
158
|
+
const wired = await wiredAgents(input.root);
|
|
159
|
+
const pre = remembered ?? (wired.length > 0 ? wired : await detected(input.root));
|
|
160
|
+
if (!input.ask)
|
|
161
|
+
return { agents: pre, from: "evidence", unknown: [] };
|
|
162
|
+
const typed = await input.ask(pre);
|
|
163
|
+
/** Enter mantém a pré-marcação - a confirmação é uma tecla, e continua sendo escolha dele. */
|
|
164
|
+
if (typed.trim() === "")
|
|
165
|
+
return { agents: pre, from: "asked", unknown: [] };
|
|
166
|
+
const read = parseAgents(typed);
|
|
167
|
+
return { agents: read.agents, from: "asked", unknown: read.unknown };
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* A ESCOLHA REGISTRADA - e é ela que distingue "ele desmarcou" de "sobrou arquivo no disco".
|
|
171
|
+
*
|
|
172
|
+
* O DEFEITO QUE ISTO CONSERTA, medido pela revisão de QA no fecho da etapa 12, e ele é o caso que
|
|
173
|
+
* originou a jornada: quem JÁ TEM os oito arquivos do Claude Code e os desmarca. `wiredAgents` lê a
|
|
174
|
+
* nossa marca nos arquivos - e A5 promete que desmarcar NÃO apaga arquivo nenhum. Então a memória
|
|
175
|
+
* respondia "fiado" para exatamente quem tinha acabado de dizer "não quero":
|
|
176
|
+
*
|
|
177
|
+
* ```
|
|
178
|
+
* 1) escolhe claude wiredAgents -> [claude]
|
|
179
|
+
* 2) roda de novo, desmarca claude wiredAgents -> [claude, codex] <- a sobra fala por ele
|
|
180
|
+
* 3) a rodada seguinte pre-marca -> [claude, codex]
|
|
181
|
+
* 4) `Enter` ("keeps this") devolve o que ele tirou
|
|
182
|
+
* ```
|
|
183
|
+
*
|
|
184
|
+
* E o `upgrade` andava pela mesma porta: ele mantém `wiredAgents`, então re-pinava o hook que ele
|
|
185
|
+
* tinha recusado - a gente não só não parava, a gente ATUALIZAVA o que ele recusou.
|
|
186
|
+
*
|
|
187
|
+
* A PREMISSA DA ABERTURA JÁ TINHA ESCRITO A SAÍDA: *"a escolha é derivada do que EXISTE no
|
|
188
|
+
* repositório... se estiver errado: nasce um campo em `_synthesisui/config.json`, que já é nosso e
|
|
189
|
+
* já está no repo dele"*. A medição provou que estava errada, e este é o campo.
|
|
190
|
+
*
|
|
191
|
+
* ELE VAI NO REPOSITÓRIO, e não na máquina: a escolha é do PROJETO - vai junto no `git clone`, e o
|
|
192
|
+
* time inteiro herda. `~/.synthesisui/` é preferência de máquina, e seria a resposta errada aqui.
|
|
193
|
+
*
|
|
194
|
+
* A leitura e a escrita preservam o resto do arquivo: `config.json` é nosso, mas carrega o alvo, as
|
|
195
|
+
* pastas e o modo de estilo dele, e reserializar o objeto inteiro a partir de um tipo parcial
|
|
196
|
+
* apagaria campo que um CLI mais novo grave.
|
|
197
|
+
*/
|
|
198
|
+
const CONFIG = ["_synthesisui", "config.json"];
|
|
199
|
+
export async function rememberedChoice(root) {
|
|
200
|
+
const { readFile } = await import("node:fs/promises");
|
|
201
|
+
const raw = await readFile(join(root, ...CONFIG), "utf8").catch(() => null);
|
|
202
|
+
if (!raw)
|
|
203
|
+
return null;
|
|
204
|
+
try {
|
|
205
|
+
const parsed = JSON.parse(raw);
|
|
206
|
+
if (!Array.isArray(parsed.agents))
|
|
207
|
+
return null;
|
|
208
|
+
/** Um nome que a lista não conhece mais - um Cursor gravado antes da retirada - some aqui. */
|
|
209
|
+
return parsed.agents.filter((a) => AGENTS.some((known) => known.id === a));
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
export async function rememberChoice(root, agents) {
|
|
216
|
+
const { mkdir, readFile, writeFile } = await import("node:fs/promises");
|
|
217
|
+
const path = join(root, ...CONFIG);
|
|
218
|
+
const raw = await readFile(path, "utf8").catch(() => null);
|
|
219
|
+
let current = {};
|
|
220
|
+
if (raw) {
|
|
221
|
+
try {
|
|
222
|
+
current = JSON.parse(raw);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
/** Um config ilegível não é apagado por nós - a escolha simplesmente não é lembrada. */
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
await mkdir(join(root, CONFIG[0]), { recursive: true }).catch(() => { });
|
|
230
|
+
await writeFile(path, `${JSON.stringify({ ...current, agents: [...agents] }, null, 2)}\n`, "utf8");
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* COM QUEM OS COMANDOS DE MANUTENÇÃO CONTINUAM - `add`, `adopt`, `upgrade`.
|
|
234
|
+
*
|
|
235
|
+
* A ordem é a da autoridade: o que ele REGISTROU vence o que sobrou no disco, que vence a pasta.
|
|
236
|
+
* O último degrau existe para o primeiro contato - um `adopt` num repositório que nunca viu o
|
|
237
|
+
* `connect` não pode ficar sem casa nenhuma, senão o comando escreve o sistema e diz *"seu agente
|
|
238
|
+
* agora conhece o seu sistema"* sobre um repositório onde nenhum agente lê nada (medido em 10/09).
|
|
239
|
+
*/
|
|
240
|
+
export async function agentsToMaintain(root) {
|
|
241
|
+
const remembered = await rememberedChoice(root);
|
|
242
|
+
if (remembered)
|
|
243
|
+
return remembered;
|
|
244
|
+
const wired = await wiredAgents(root);
|
|
245
|
+
return wired.length > 0 ? wired : await detected(root);
|
|
246
|
+
}
|
package/dist/claude-md.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { hasHook } from "./agent-wiring.js";
|
|
4
4
|
import { declaredReference } from "./group-role.js";
|
|
5
5
|
import { recallAvailable } from "./memory/availability.js";
|
|
6
6
|
const START = "<!-- synthesisui:start -->";
|
|
@@ -564,28 +564,39 @@ _Block managed by the CLI - do not edit by hand; run \`synthesisui ${onlyAdopted
|
|
|
564
564
|
* ONDE O BLOCO GERENCIADO MORA, POR AGENTE.
|
|
565
565
|
*
|
|
566
566
|
* O bloco é o mesmo texto - as regras que um agente lê ANTES de escrever UI - e cada ferramenta
|
|
567
|
-
* escolheu um arquivo diferente para lê-lo. Escrever nos
|
|
567
|
+
* escolheu um arquivo diferente para lê-lo. Escrever nos dois custa um `writeFile` a mais e
|
|
568
568
|
* multiplica os editores cobertos sem escrever motor novo, que era a lacuna mais barata da análise de
|
|
569
569
|
* DX (05/08): a governança só chegava a quem usa Claude Code.
|
|
570
570
|
*
|
|
571
|
-
* `AGENTS.md` é o nome que Codex e vários outros leem
|
|
572
|
-
*
|
|
571
|
+
* `CLAUDE.md` é o do Claude Code; `AGENTS.md` é o nome que Codex e vários outros leem. O
|
|
572
|
+
* `.cursor/rules/*.mdc` do Cursor saiu em 10/09, por decisão dele - ver `agents-chosen.ts`.
|
|
573
|
+
*
|
|
574
|
+
* E QUEM DECIDE QUAIS DESTAS CASAS SÃO ESCRITAS É A ESCOLHA DELE (A2 da etapa 12), não a pasta:
|
|
575
|
+
* `syncClaudeMd` recebe os agentes marcados. A pasta continua sendo a melhor pré-marcação que a
|
|
576
|
+
* gente tem, e deixou de ser a palavra final.
|
|
573
577
|
*/
|
|
574
578
|
const HOMES = [
|
|
575
|
-
{ path: "CLAUDE.md" },
|
|
576
|
-
{ path: "AGENTS.md",
|
|
577
|
-
{
|
|
578
|
-
path: ".cursor/rules/synthesisui.mdc",
|
|
579
|
-
frontmatter: "---\ndescription: Design system rules - read before writing UI\nalwaysApply: true\n---\n\n",
|
|
580
|
-
bornWhen: ".cursor",
|
|
581
|
-
},
|
|
579
|
+
{ path: "CLAUDE.md", agent: "claude" },
|
|
580
|
+
{ path: "AGENTS.md", agent: "codex" },
|
|
582
581
|
];
|
|
583
|
-
export async function syncClaudeMd(projectRoot
|
|
582
|
+
export async function syncClaudeMd(projectRoot,
|
|
583
|
+
/**
|
|
584
|
+
* OS AGENTES QUE ELE ESCOLHEU - e é a escolha que autoriza cada casa (A2 da etapa 12).
|
|
585
|
+
*
|
|
586
|
+
* Argumento OBRIGATÓRIO, e não opcional com default: um default aqui seria a régua velha
|
|
587
|
+
* sobrevivendo escondida no dia em que um chamador novo esquecer de passar a escolha - e a régua
|
|
588
|
+
* velha é exatamente o que fazia 8 arquivos do Claude Code entrarem num repositório de quem usa
|
|
589
|
+
* Codex. O `tsc` recusa a meia-chamada (lei 6: fechar a porta em vez de vigiá-la).
|
|
590
|
+
*/
|
|
591
|
+
agents) {
|
|
584
592
|
const installed = await readInstalled(projectRoot);
|
|
585
593
|
const region = await renderRegion(projectRoot, installed);
|
|
586
594
|
let createdAny = false;
|
|
587
595
|
const changed = [];
|
|
588
596
|
for (const home of HOMES) {
|
|
597
|
+
/** A casa de um agente que ele não escolheu não é escrita nem reescrita. */
|
|
598
|
+
if (!agents.includes(home.agent))
|
|
599
|
+
continue;
|
|
589
600
|
const path = join(projectRoot, home.path);
|
|
590
601
|
let existing = null;
|
|
591
602
|
try {
|
|
@@ -594,20 +605,7 @@ export async function syncClaudeMd(projectRoot) {
|
|
|
594
605
|
catch {
|
|
595
606
|
existing = null;
|
|
596
607
|
}
|
|
597
|
-
/**
|
|
598
|
-
* O ARQUIVO NOVO NASCE QUANDO O REPOSITÓRIO JÁ MOSTROU QUEM ELE USA - ver `bornWhen`.
|
|
599
|
-
*
|
|
600
|
-
* `CLAUDE.md` sem condição, porque é a casa que a gente cria num repo que não tem nenhuma. Os
|
|
601
|
-
* outros dois só quando a pasta da ferramenta está lá: criar `AGENTS.md` num repo que só usa
|
|
602
|
-
* Claude Code continua sendo decidir por outra pessoa qual agente ela usa, e um arquivo que
|
|
603
|
-
* apareceu sozinho na raiz é a primeira coisa que alguém apaga com raiva.
|
|
604
|
-
*/
|
|
605
608
|
if (existing === null) {
|
|
606
|
-
const authorised = home.path === "CLAUDE.md" ||
|
|
607
|
-
(home.bornWhen != null &&
|
|
608
|
-
(await exists(join(projectRoot, home.bornWhen))));
|
|
609
|
-
if (!authorised)
|
|
610
|
-
continue;
|
|
611
609
|
await mkdir(dirname(path), { recursive: true }).catch(() => { });
|
|
612
610
|
await writeFile(path, `${home.frontmatter ?? ""}${region}\n`, "utf8");
|
|
613
611
|
createdAny = true;
|
|
@@ -666,9 +664,16 @@ const OPENS = {
|
|
|
666
664
|
/**
|
|
667
665
|
* Os comandos que abrem os agentes que ESTE repositório mostra ter - vazio quando nenhum deles
|
|
668
666
|
* carrega o bloco, e aí a saída não fala de abrir nada.
|
|
667
|
+
*
|
|
668
|
+
* E FILTRADO PELA ESCOLHA quando quem chama a conhece (etapa 12): o bloco de um agente desmarcado
|
|
669
|
+
* CONTINUA no arquivo, porque desmarcar não apaga nada. Sem o filtro, a tela recomendava abrir a
|
|
670
|
+
* ferramenta que ele tinha acabado de tirar - achado da revisão de QA, 10/09.
|
|
669
671
|
*/
|
|
670
|
-
export async function agentOpenCommands(projectRoot) {
|
|
671
|
-
const
|
|
672
|
+
export async function agentOpenCommands(projectRoot, agents) {
|
|
673
|
+
const allowed = agents
|
|
674
|
+
? new Set(HOMES.filter((h) => agents.includes(h.agent)).map((h) => h.path))
|
|
675
|
+
: null;
|
|
676
|
+
const homes = (await blockHomes(projectRoot)).filter((h) => allowed === null || allowed.has(h));
|
|
672
677
|
return homes.map((h) => OPENS[h]).filter((c) => Boolean(c));
|
|
673
678
|
}
|
|
674
679
|
export async function blockHomes(projectRoot) {
|
package/dist/codex-mcp.js
CHANGED
|
@@ -36,8 +36,10 @@ export function hasCodexBlock(text) {
|
|
|
36
36
|
/**
|
|
37
37
|
* A versão pinada dentro da nossa região - `null` quando não há região ou quando ela não pina.
|
|
38
38
|
*
|
|
39
|
-
* O `connect` usa isto para dizer de ONDE
|
|
40
|
-
*
|
|
39
|
+
* O `connect` usa isto para dizer de ONDE a fiação saiu, que é a diferença entre "atualizei" e uma
|
|
40
|
+
* linha que a pessoa fecha sem ler. Desde 10/09 a região nova nunca pina (ver `codexBlock`), então
|
|
41
|
+
* o que esta função encontra é sempre PASSADO - e é exatamente aí que ela vale mais: um arquivo que
|
|
42
|
+
* ainda diz `synthesisui@0.16.413` está preso naquele leitor, e a tela pode dizer que ele saiu.
|
|
41
43
|
*/
|
|
42
44
|
export function pinnedInCodex(text) {
|
|
43
45
|
const start = text.indexOf(BEGIN);
|
|
@@ -50,14 +52,36 @@ export function pinnedInCodex(text) {
|
|
|
50
52
|
/**
|
|
51
53
|
* A REGIÃO, montada. `args` em duas partes porque é assim que o Codex quer a linha de comando:
|
|
52
54
|
* o executável em `command`, e o resto quebrado em array.
|
|
55
|
+
*
|
|
56
|
+
* A MESMA RÉGUA DE VERSÃO DO OUTRO ESCRITOR, e isto é um conserto de 10/09. Este bloco nascia
|
|
57
|
+
* PINADO na versão do `connect`, e o `.mcp.json` do Claude Code nasce em `@latest` desde 06/08 -
|
|
58
|
+
* duas ferramentas, o mesmo repositório, o mesmo servidor nosso, e respostas opostas sobre quando
|
|
59
|
+
* ele fica mais novo.
|
|
60
|
+
*
|
|
61
|
+
* A decisão está escrita em `agent-wiring.ts`, e vale para os dois pela mesma razão: *"um servidor
|
|
62
|
+
* MCP sobe UMA VEZ quando o editor abre a sessão e fica estável durante ela. Pinado, ele nunca
|
|
63
|
+
* ficava mais novo - e a regra que manda o agente comparar `ledger.cli` com a versão do servidor
|
|
64
|
+
* quase nunca disparava, porque os dois congelavam juntos no dia do `connect`"* (dono, 06/08). O
|
|
65
|
+
* hook é o caso oposto e continua pinado: ele resolve a CADA escrita, e ali flutuar custa 720ms-1,5s
|
|
66
|
+
* por edição e um comportamento que muda sob a pessoa.
|
|
67
|
+
*
|
|
68
|
+
* O QUE O CLIENTE PERCEBIA: o agente dele no Claude Code recebia o leitor publicado na sessão
|
|
69
|
+
* seguinte; o agente dele no Codex ficava na versão do dia em que ele rodou o `connect`, e nada na
|
|
70
|
+
* tela dizia que os dois estavam em versões diferentes.
|
|
71
|
+
*
|
|
72
|
+
* E O INSTALL LOCAL CONTINUA GANHANDO, também como no outro: quem tem o CLI no `node_modules`
|
|
73
|
+
* escolheu a versão, e `--no-install` respeita a escolha - inclusive a de quem seguiu a sugestão
|
|
74
|
+
* que o próprio `connect` imprime, de pôr o CLI nas `devDependencies`.
|
|
53
75
|
*/
|
|
54
|
-
export function codexBlock(
|
|
76
|
+
export function codexBlock(local) {
|
|
55
77
|
return [
|
|
56
78
|
BEGIN,
|
|
57
79
|
"# Managed by synthesisui. Edit outside this block; this region is rewritten on connect.",
|
|
58
80
|
"[mcp_servers.synthesisui]",
|
|
59
81
|
'command = "npx"',
|
|
60
|
-
|
|
82
|
+
local
|
|
83
|
+
? 'args = ["--no-install", "synthesisui", "mcp"]'
|
|
84
|
+
: 'args = ["synthesisui@latest", "mcp"]',
|
|
61
85
|
END,
|
|
62
86
|
].join("\n");
|
|
63
87
|
}
|
|
@@ -73,8 +97,8 @@ export function codexBlock(version) {
|
|
|
73
97
|
* perdeu. Aí a gente devolve o arquivo COMO ESTÁ: apagar até o fim seria destruir o que vem
|
|
74
98
|
* depois, e é a mesma decisão que o `withHook` toma no perfil de shell.
|
|
75
99
|
*/
|
|
76
|
-
export function withCodexBlock(text,
|
|
77
|
-
const block = codexBlock(
|
|
100
|
+
export function withCodexBlock(text, local) {
|
|
101
|
+
const block = codexBlock(local);
|
|
78
102
|
if (!hasCodexBlock(text))
|
|
79
103
|
return `${text.replace(/\n*$/, "")}\n\n${block}\n`.replace(/^\n+/, "");
|
|
80
104
|
const start = text.indexOf(BEGIN);
|
package/dist/commands/add.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { agentsToMaintain } from "../agents-chosen.js";
|
|
3
4
|
import { syncClaudeMd } from "../claude-md.js";
|
|
4
5
|
import { readProjectConfig, readToken, resolveRegistry } from "../config.js";
|
|
5
6
|
import { customFontFamilies, googleFontsHref, nextFontSnippet, } from "../fonts.js";
|
|
@@ -432,7 +433,7 @@ export async function add(slug, opts) {
|
|
|
432
433
|
* (`doctrine.json`): o doctor e o hook precisam delas offline.
|
|
433
434
|
*/
|
|
434
435
|
// 6. discovery by the agent
|
|
435
|
-
const claudeMd = await syncClaudeMd(projectRoot);
|
|
436
|
+
const claudeMd = await syncClaudeMd(projectRoot, await agentsToMaintain(projectRoot));
|
|
436
437
|
// outcome line
|
|
437
438
|
const v = payload.version;
|
|
438
439
|
if (!prev) {
|
package/dist/commands/adopt.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join, relative, resolve } from "node:path";
|
|
3
|
+
import { agentsToMaintain } from "../agents-chosen.js";
|
|
3
4
|
import { syncClaudeMd } from "../claude-md.js";
|
|
4
5
|
import { parseRootTokens } from "../doctor/tokens.js";
|
|
5
|
-
import { body, section, snippet } from "../output.js";
|
|
6
|
+
import { body, paint, section, snippet } from "../output.js";
|
|
6
7
|
import { walk } from "./doctor.js";
|
|
7
8
|
/**
|
|
8
9
|
* `synthesisui adopt` - the system somebody ALREADY has, made legible.
|
|
@@ -239,12 +240,27 @@ export async function adopt(opts) {
|
|
|
239
240
|
// `version: 0` marks a system nobody published - the doctor and CLAUDE.md
|
|
240
241
|
// read this to know it is adopted rather than installed.
|
|
241
242
|
await writeFile(join(dir, ".lock"), `${JSON.stringify({ slug, name, version: 0, adopted: true }, null, 2)}\n`, "utf8");
|
|
242
|
-
|
|
243
|
+
/**
|
|
244
|
+
* A LINHA SAI DO QUE FOI ESCRITO, e não de uma promessa fixa - achado da revisão de QA no fecho
|
|
245
|
+
* da etapa 12, e é uma regressão que ela mesma criou.
|
|
246
|
+
*
|
|
247
|
+
* Desde que as casas do bloco passaram a obedecer à escolha dele, um repositório de PRIMEIRO
|
|
248
|
+
* CONTATO - nenhuma escolha registrada, nenhuma pasta de agente - não recebe casa nenhuma. E
|
|
249
|
+
* estas três linhas eram fixas: o comando dizia `✓ CLAUDE.md managed block added` e *"seu agente
|
|
250
|
+
* agora conhece o seu sistema"* sobre um repositório onde nenhum agente lê nada. O `adopt` é a
|
|
251
|
+
* porta que a home anuncia para quem quer o contrato sem conta.
|
|
252
|
+
*/
|
|
253
|
+
const claudeMd = await syncClaudeMd(root, await agentsToMaintain(root));
|
|
243
254
|
console.log("");
|
|
244
255
|
console.log(body(`✓ _synthesisui/ds/${slug}/ written`));
|
|
245
|
-
|
|
256
|
+
for (const home of claudeMd.changed)
|
|
257
|
+
console.log(body(`✓ ${home.padEnd(22)} managed block added`));
|
|
246
258
|
console.log("");
|
|
247
|
-
console.log(body(
|
|
259
|
+
console.log(body(claudeMd.changed.length > 0
|
|
260
|
+
? "Your agent now knows your system. Nothing of yours changed."
|
|
261
|
+
: "Nothing of yours changed - and no agent home was written, because this repo does not say which agent you use yet:"));
|
|
262
|
+
if (claudeMd.changed.length === 0)
|
|
263
|
+
console.log(body(paint.blue(" npx synthesisui@latest connect")));
|
|
248
264
|
console.log("");
|
|
249
265
|
console.log(snippet(["npx synthesisui@latest doctor"]));
|
|
250
266
|
console.log(body("see what it makes measurable"));
|
package/dist/commands/align.js
CHANGED
|
@@ -712,7 +712,13 @@ opts = {}) {
|
|
|
712
712
|
* repositório inteiro, e isto roda em TODA abertura de sessão. Um aviso que custa um walk completo
|
|
713
713
|
* a cada sessão é um aviso que alguém desliga.
|
|
714
714
|
*/
|
|
715
|
-
export async function nextStepFor(root
|
|
715
|
+
export async function nextStepFor(root,
|
|
716
|
+
/**
|
|
717
|
+
* OS AGENTES QUE ELE ESCOLHEU, quando quem chama sabe (etapa 12) - o bloco de um agente
|
|
718
|
+
* desmarcado continua no arquivo, porque desmarcar não apaga nada, e sem este filtro a tela
|
|
719
|
+
* recomendava abrir a ferramenta que ele tinha acabado de tirar.
|
|
720
|
+
*/
|
|
721
|
+
agents) {
|
|
716
722
|
const locks = (await locksIn(root)).filter((l) => l.slug && !l.adopted);
|
|
717
723
|
if (locks.length > 0)
|
|
718
724
|
return null;
|
|
@@ -742,7 +748,7 @@ export async function nextStepFor(root) {
|
|
|
742
748
|
: {
|
|
743
749
|
sentence: 'This repo has no design system contract yet. Ask me: "import my design system."',
|
|
744
750
|
headline: "Open a new agent session in this repo",
|
|
745
|
-
open: await agentOpenCommands(root).catch(() => []),
|
|
751
|
+
open: await agentOpenCommands(root, agents).catch(() => []),
|
|
746
752
|
say: "import my design system",
|
|
747
753
|
why: "I read what is already in your code - the colours, the type, the shapes, the components. Nothing is invented.",
|
|
748
754
|
};
|
|
@@ -830,7 +836,7 @@ export function renderNextStep(next, freshSession) {
|
|
|
830
836
|
*/
|
|
831
837
|
export async function reportWhatIsLeft(root, opts = {}) {
|
|
832
838
|
const items = await misalignments(root, opts).catch(() => []);
|
|
833
|
-
const next = await nextStepFor(root).catch(() => null);
|
|
839
|
+
const next = await nextStepFor(root, opts.agents).catch(() => null);
|
|
834
840
|
/**
|
|
835
841
|
* A AÇÃO RECOMENDADA GANHA DA LISTA - e a falta desta regra foi a regressão que o dono leu na
|
|
836
842
|
* 0.16.365.
|
package/dist/commands/connect.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import { codexPinBefore, wireAgent } from "../agent-wiring.js";
|
|
3
|
+
import { codexPinBefore, exists, wireAgent } from "../agent-wiring.js";
|
|
4
|
+
import { AGENTS, chooseAgents, filesOf, rememberChoice, } from "../agents-chosen.js";
|
|
4
5
|
import { blockHomes, syncClaudeMd } from "../claude-md.js";
|
|
5
6
|
import { resolveRegistry } from "../config.js";
|
|
6
7
|
import { notAProject, projectRootFrom } from "../is-a-project.js";
|
|
@@ -211,6 +212,53 @@ version) {
|
|
|
211
212
|
await writeFile(rc, withHook(current ?? "", shellSnippet(shell, version)), "utf8");
|
|
212
213
|
console.log(body(`Written. Open a new terminal, or run: source ${rc}`));
|
|
213
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* A LISTA, E ELE DECIDE (A1 da etapa 12) - com o que o repositório PROVA já marcado.
|
|
217
|
+
*
|
|
218
|
+
* A pré-marcação é a evidência: ela continua sendo a melhor informação que a gente tem sobre o
|
|
219
|
+
* repositório dele, e deixou de ser a palavra final. Enter confirma - a confirmação é uma tecla, e
|
|
220
|
+
* continua sendo escolha dele -, e digitar os nomes substitui a lista.
|
|
221
|
+
*
|
|
222
|
+
* SEM BIBLIOTECA DE CHECKBOX, de propósito: um seletor com setas exige controlar o terminal em raw
|
|
223
|
+
* mode, e este comando roda em terminal de CI, em terminal dentro de editor e em terminal de quem
|
|
224
|
+
* usa Windows. Uma linha por agente e uma pergunta é o que funciona nos três.
|
|
225
|
+
*/
|
|
226
|
+
async function askWhichAgents(pre) {
|
|
227
|
+
console.log("");
|
|
228
|
+
console.log(section(say("Which agents")));
|
|
229
|
+
for (const agent of AGENTS) {
|
|
230
|
+
const on = pre.includes(agent.id);
|
|
231
|
+
console.log(body(`${on ? "[x]" : "[ ]"} ${agent.label.padEnd(13)} ${on
|
|
232
|
+
? fmt(say("{dir} is here"), { dir: agent.evidence })
|
|
233
|
+
: fmt(say("no {dir} in this repo"), { dir: agent.evidence })}`));
|
|
234
|
+
}
|
|
235
|
+
console.log("");
|
|
236
|
+
console.log(body(say("Enter keeps this. To change it, type the ones you want - or `none`:")));
|
|
237
|
+
console.log(body(paint.faint(` ${AGENTS.map((a) => a.id).join(" ")}`)));
|
|
238
|
+
const { createInterface } = await import("node:readline/promises");
|
|
239
|
+
const rl = createInterface({
|
|
240
|
+
input: process.stdin,
|
|
241
|
+
output: process.stdout,
|
|
242
|
+
});
|
|
243
|
+
const typed = await rl.question(" > ").catch(() => "");
|
|
244
|
+
rl.close();
|
|
245
|
+
return typed;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* O QUE FICOU ESCOLHIDO, ecoado antes de agir - achado da revisão de DX no fecho.
|
|
249
|
+
*
|
|
250
|
+
* A frase da lista já diz que digitar SUBSTITUI o conjunto, e o risco não é de vocabulário: é de
|
|
251
|
+
* hábito. Quem está acostumado com seletor incremental vê `[x] Codex` marcado, digita `claude`
|
|
252
|
+
* querendo acrescentar, e perde o Codex. A consequência aparecia embaixo, misturada com as outras
|
|
253
|
+
* linhas; uma linha só, antes de qualquer escrita, torna o engano visível no lugar onde ele
|
|
254
|
+
* aconteceu.
|
|
255
|
+
*/
|
|
256
|
+
function echoChoice(agents) {
|
|
257
|
+
console.log("");
|
|
258
|
+
console.log(body(fmt(say("Selected: {agents}"), {
|
|
259
|
+
agents: agents.length > 0 ? agents.join(" ") : "none",
|
|
260
|
+
})));
|
|
261
|
+
}
|
|
214
262
|
export async function connect(opts) {
|
|
215
263
|
const root = opts.dir ?? process.cwd();
|
|
216
264
|
/**
|
|
@@ -234,7 +282,36 @@ export async function connect(opts) {
|
|
|
234
282
|
}
|
|
235
283
|
// Both unless one is explicitly turned off - somebody who says `--no-hook`
|
|
236
284
|
// means it, and somebody who says nothing wants the thing to work.
|
|
237
|
-
const
|
|
285
|
+
const layers = { hook: opts.hook !== false, mcp: opts.mcp !== false };
|
|
286
|
+
/**
|
|
287
|
+
* QUAIS AGENTES ENTRAM - a escolha DELE, e nunca a nossa dedução (A1..A4 da etapa 12).
|
|
288
|
+
*
|
|
289
|
+
* MEDIDO em 10/09, com este comando num repositório que só tem `.codex/`: 10 arquivos escritos, e
|
|
290
|
+
* 8 do Claude Code. A régua da evidência protegia o Codex e o Cursor; o Claude Code era escrito
|
|
291
|
+
* sempre. A decisão dele: *"seria legal o próprio connect eu selecionar quais agentes eu quero
|
|
292
|
+
* colocar"*.
|
|
293
|
+
*
|
|
294
|
+
* A PERGUNTA SÓ EXISTE ONDE HÁ ALGUÉM PARA RESPONDER. Em `--ci`, num pipe ou dentro de um agente,
|
|
295
|
+
* `ask` vai `null` e a pasta decide - e a linha DIZ que foi ela, porque silêncio nunca é escolha.
|
|
296
|
+
*/
|
|
297
|
+
const interactive = !opts.ci && Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
|
|
298
|
+
const chosen = await chooseAgents({
|
|
299
|
+
root,
|
|
300
|
+
flag: opts.agents,
|
|
301
|
+
ask: interactive ? (pre) => askWhichAgents(pre) : null,
|
|
302
|
+
});
|
|
303
|
+
const want = { ...layers, agents: chosen.agents };
|
|
304
|
+
if (chosen.from === "asked")
|
|
305
|
+
echoChoice(chosen.agents);
|
|
306
|
+
/**
|
|
307
|
+
* A ESCOLHA FICA REGISTRADA quando foi ELE quem a fez - ver `rememberChoice`.
|
|
308
|
+
*
|
|
309
|
+
* Só `asked` e `flag`: uma dedução da pasta não é escolha, e gravá-la faria a próxima rodada
|
|
310
|
+
* tratar o palpite de hoje como decisão dele. É a mesma linha que separa `from: "evidence"` dos
|
|
311
|
+
* outros dois em toda esta etapa.
|
|
312
|
+
*/
|
|
313
|
+
if (chosen.from !== "evidence")
|
|
314
|
+
await rememberChoice(root, chosen.agents).catch(() => { });
|
|
238
315
|
/**
|
|
239
316
|
* OS ARQUIVOS DO INSTALL ANTES DA FIAÇÃO: o `add` reescreve o `CLAUDE.md` por dentro, e o
|
|
240
317
|
* `syncClaudeMd` abaixo tem que ser o último a falar sobre ele.
|
|
@@ -251,8 +328,26 @@ export async function connect(opts) {
|
|
|
251
328
|
const wired = await wireAgent(root, opts.version, want);
|
|
252
329
|
// The block reads the settings we just wrote, so it must be regenerated
|
|
253
330
|
// after them, not before.
|
|
254
|
-
const contract = await syncClaudeMd(root);
|
|
331
|
+
const contract = await syncClaudeMd(root, chosen.agents);
|
|
255
332
|
console.log(section(say("Connected")));
|
|
333
|
+
/**
|
|
334
|
+
* NENHUM AGENTE ESCOLHIDO NÃO É COMANDO QUEBRADO (A3) - a frase diz o que falta e o caminho.
|
|
335
|
+
*
|
|
336
|
+
* Um comando que não escreve nada e não explica lê como defeito. E o caso é real: um repositório
|
|
337
|
+
* sem `.claude/` e sem `.codex/` não prova ferramenta nenhuma, e num CI não há quem responder.
|
|
338
|
+
*/
|
|
339
|
+
if (chosen.agents.length === 0)
|
|
340
|
+
console.log(body(say(chosen.from === "evidence"
|
|
341
|
+
? "· no agent chosen, and nothing here says which agent you use - run with `--agents claude` or `--agents codex`, or run this in a terminal to pick from the list"
|
|
342
|
+
: "· no agent chosen - nothing was wired. Run again with `--agents claude` or `--agents codex` when you want it")));
|
|
343
|
+
/** E DE ONDE A ESCOLHA VEIO, quando não foi ele quem respondeu: silêncio não é escolha. */
|
|
344
|
+
if (chosen.agents.length > 0 && chosen.from === "evidence")
|
|
345
|
+
console.log(body(paint.faint(fmt(say("· nobody to ask here, so the folders in this repo decided: {agents}"), { agents: chosen.agents.join(", ") }))));
|
|
346
|
+
/** UM NOME QUE A LISTA NÃO CONHECE VOLTA DITO - engoli-lo faria `--agents cursor` parecer aceito. */
|
|
347
|
+
if (chosen.unknown.length > 0)
|
|
348
|
+
console.log(body(fmt(say("· not an agent this CLI knows: {names}"), {
|
|
349
|
+
names: chosen.unknown.join(", "),
|
|
350
|
+
})));
|
|
256
351
|
/**
|
|
257
352
|
* A LISTA DA TELA, JUNTADA ANTES DE IMPRIMIR - e é isto que faz a rodada silenciosa caber numa
|
|
258
353
|
* linha.
|
|
@@ -327,7 +422,16 @@ export async function connect(opts) {
|
|
|
327
422
|
row(true, `✓ _synthesisui/ds/${refreshed.slug}/ rewritten by this CLI${refreshed.was
|
|
328
423
|
? ` - it was written by ${refreshed.was}`
|
|
329
424
|
: " - it carried no CLI version, so it predates this"}`);
|
|
330
|
-
|
|
425
|
+
/**
|
|
426
|
+
* O HOOK É DO CLAUDE CODE, então a linha dele só sai quando ele foi escolhido (etapa 12).
|
|
427
|
+
*
|
|
428
|
+
* MEDIDO na primeira rodada real desta etapa, num repositório que só prova o Codex: a tela dizia
|
|
429
|
+
* `· Claude Code not chosen - nothing of it was written` e, três linhas abaixo,
|
|
430
|
+
* `· .claude/settings.json already had it` com o comando do hook embaixo. É a mesma contradição
|
|
431
|
+
* de duas frases que a etapa 11 fechou no `template`, aparecendo na tela que a 12 acabou de
|
|
432
|
+
* escrever - e `wired.hook` diz `skipped`, então a informação para suprimir já estava aqui.
|
|
433
|
+
*/
|
|
434
|
+
if (want.hook && wired.hook !== "skipped") {
|
|
331
435
|
row(wired.hook !== "already there", (() => {
|
|
332
436
|
return wired.hook === "added"
|
|
333
437
|
? say("✓ .claude/settings.json the check now runs after every write")
|
|
@@ -367,16 +471,13 @@ export async function connect(opts) {
|
|
|
367
471
|
? `✓ ${one.path.padEnd(22)} ${fmt("{n} tools the agent can ask", { n: MCP_TOOL_COUNT })}`
|
|
368
472
|
: one.status === "updated"
|
|
369
473
|
? /**
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
* em 06/08, quando "already had it" escondia um pin antigo.
|
|
474
|
+
* O QUE MUDOU É SEMPRE A MESMA COISA AGORA: a entrada parou de segurar um leitor
|
|
475
|
+
* velho. As três ferramentas seguem a régua de 06/08 (ver `codexBlock`), então a
|
|
476
|
+
* frase é uma - e o Codex só ganha uma metade a mais, porque ele é o único cujo
|
|
477
|
+
* arquivo pode estar vindo de um PIN nosso, escrito antes de 10/09.
|
|
375
478
|
*/
|
|
376
|
-
one.path.endsWith(".toml")
|
|
377
|
-
? `✓ ${one.path.padEnd(22)} ${fmt(codexPinBefore()
|
|
378
|
-
? "the tools moved from {was} to this one"
|
|
379
|
-
: "{n} tools the agent can ask", { was: codexPinBefore() ?? "", n: MCP_TOOL_COUNT })}`
|
|
479
|
+
one.path.endsWith(".toml") && codexPinBefore()
|
|
480
|
+
? `✓ ${one.path.padEnd(22)} ${fmt("was pinned to {was} - now it follows the published reader", { was: codexPinBefore() ?? "" })}`
|
|
380
481
|
: `✓ ${one.path.padEnd(22)} ${say("unpinned - it follows the published reader")}`
|
|
381
482
|
: `· ${one.path.padEnd(22)} ${say("already had it")}`);
|
|
382
483
|
}
|
|
@@ -434,7 +535,18 @@ export async function connect(opts) {
|
|
|
434
535
|
* repositório é uma skill que só nós rodamos, e o caso de uso inteiro dela é o cliente rodando
|
|
435
536
|
* sozinho. Meia jornada não é meio valor.
|
|
436
537
|
*/
|
|
437
|
-
|
|
538
|
+
/**
|
|
539
|
+
* AS SKILLS SÃO DO CLAUDE CODE, e por isso elas obedecem à escolha (A2 da etapa 12).
|
|
540
|
+
*
|
|
541
|
+
* O caminho é `.claude/skills/<nome>/SKILL.md` - a casa de UMA ferramenta. Cinco arquivos numa
|
|
542
|
+
* pasta de um editor que a pessoa não usa era metade dos 8 que a medição de 10/09 encontrou num
|
|
543
|
+
* repositório que só prova o Codex.
|
|
544
|
+
*
|
|
545
|
+
* O DIA EM QUE OUTRA FERRAMENTA TIVER SKILLS, a lista de `agents-chosen.ts` ganha o caminho dela e
|
|
546
|
+
* este laço passa a andar por agente. Hoje ela é uma, e fingir uma abstração para uma população
|
|
547
|
+
* de um é o que este repositório chama de configuração vestida de produto.
|
|
548
|
+
*/
|
|
549
|
+
const skills = chosen.agents.includes("claude") ? SKILLS : [];
|
|
438
550
|
/**
|
|
439
551
|
* A PASTA VELHA SAI, e isto é obrigatório numa renomeação de skill distribuída.
|
|
440
552
|
*
|
|
@@ -472,6 +584,82 @@ export async function connect(opts) {
|
|
|
472
584
|
}
|
|
473
585
|
/** A tela sai agora, junta: o que mudou por nome, e o silêncio numa linha. */
|
|
474
586
|
flush();
|
|
587
|
+
/**
|
|
588
|
+
* O QUE ELE NÃO ESCOLHEU É DITO, com os caminhos (A2 e A5 da etapa 12) - lacuna declarada é
|
|
589
|
+
* confiança, lacuna calada é bug (lei 8).
|
|
590
|
+
*
|
|
591
|
+
* Os arquivos que a fiação daquele agente escreveria, nomeados: *"8 arquivos"* obrigaria a pessoa
|
|
592
|
+
* a adivinhar quais. E quando algum deles JÁ ESTÁ no repositório - ele desmarcou algo que estava
|
|
593
|
+
* fiado -, a linha diz que ele ficou lá e como remover a NOSSA parte: apagar arquivo dele não é
|
|
594
|
+
* nosso (decisão dele, 10/09), e o `.codex/config.toml` tem configuração DELE dentro - foi por
|
|
595
|
+
* isso que o `rm` do arquivo inteiro saiu daqui (revisão de DX, mesmo dia).
|
|
596
|
+
*/
|
|
597
|
+
for (const agent of AGENTS) {
|
|
598
|
+
if (chosen.agents.includes(agent.id))
|
|
599
|
+
continue;
|
|
600
|
+
const left = [];
|
|
601
|
+
for (const wrote of filesOf(agent.id))
|
|
602
|
+
if (await exists(join(root, wrote.path.replace(/\/$/, ""))))
|
|
603
|
+
left.push(wrote);
|
|
604
|
+
console.log(body(paint.faint(left.length > 0
|
|
605
|
+
? fmt(say("· {label} not chosen - these stay in the repo: {files}"), {
|
|
606
|
+
label: agent.label,
|
|
607
|
+
files: left.map((w) => w.path).join(", "),
|
|
608
|
+
})
|
|
609
|
+
: fmt(say("· {label} not chosen - nothing of it was written"), {
|
|
610
|
+
label: agent.label,
|
|
611
|
+
}))));
|
|
612
|
+
/**
|
|
613
|
+
* A INSTRUÇÃO DE REMOVER DEPENDE DE QUEM É O ARQUIVO - ver `Ours` em `agents-chosen.ts`.
|
|
614
|
+
*
|
|
615
|
+
* O `rm` só vale para o que a gente criou inteiro. Para o que é DELE com a nossa região dentro,
|
|
616
|
+
* a instrução é a MARCA: o `.codex/config.toml` tem a `status_line` dele, e o
|
|
617
|
+
* `.claude/settings.json` tem os hooks dele.
|
|
618
|
+
*/
|
|
619
|
+
const ours = left.filter((w) => w.ours === "file").map((w) => w.path);
|
|
620
|
+
const blocks = left.filter((w) => w.ours === "block").map((w) => w.path);
|
|
621
|
+
const entries = left.filter((w) => w.ours === "entry").map((w) => w.path);
|
|
622
|
+
if (ours.length > 0)
|
|
623
|
+
console.log(body(paint.faint(` rm -rf ${ours.join(" ")}`)));
|
|
624
|
+
if (blocks.length > 0)
|
|
625
|
+
console.log(body(paint.faint(fmt(say(" our part in {files} sits between `synthesisui:start` and `synthesisui:end` - the rest of each file is yours"), { files: blocks.join(", ") }))));
|
|
626
|
+
/**
|
|
627
|
+
* E NUM JSON NÃO HÁ MARCA PARA PROCURAR - a nossa parte é uma CHAVE. Dizer "entre as marcas"
|
|
628
|
+
* sobre o `.claude/settings.json` seria mandar procurar um botão que a tela não mostra.
|
|
629
|
+
*/
|
|
630
|
+
if (entries.length > 0)
|
|
631
|
+
console.log(body(paint.faint(fmt(say(" in {files} only our `synthesisui` entry is ours - the rest of the file is yours"), { files: entries.join(", ") }))));
|
|
632
|
+
/**
|
|
633
|
+
* E O `.claude/settings.json` É O ÚNICO QUE CONTINUA RODANDO, então ele é dito à parte - achado
|
|
634
|
+
* da revisão de DX: *"these stay in the repo"* lê como arquivo inerte, e o hook lá dentro é
|
|
635
|
+
* executado pelo editor a cada escrita, escolha ou não escolha. Quem desmarcou para parar de ser
|
|
636
|
+
* interrompido precisa saber que só apagar a região para isso.
|
|
637
|
+
*/
|
|
638
|
+
if (entries.includes(".claude/settings.json"))
|
|
639
|
+
console.log(body(paint.faint(say(" the write check inside it keeps running until that entry is gone"))));
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* O CURSOR SAIU DO PRODUTO (A6 da etapa 12), e quem tem os arquivos dele é avisado com os
|
|
643
|
+
* caminhos - por decisão dele, 10/09.
|
|
644
|
+
*
|
|
645
|
+
* A gente escrevia dois arquivos naquela pasta: `.cursor/mcp.json` (as ferramentas) e
|
|
646
|
+
* `.cursor/rules/synthesisui.mdc` (as regras). Eles ficam onde estão - apagar arquivo do
|
|
647
|
+
* repositório dele não é nosso -, e a linha diz o que eles são e o que dentro deles é nosso: o
|
|
648
|
+
* arquivo de regras é nosso inteiro, e no `.cursor/mcp.json` só a entrada `synthesisui` é.
|
|
649
|
+
*
|
|
650
|
+
* O aviso sai enquanto os arquivos estiverem lá, e é o próprio disco que decide: ele desaparece
|
|
651
|
+
* quando ele age. Sem arquivo de estado nosso para lembrar de um aviso.
|
|
652
|
+
*/
|
|
653
|
+
{
|
|
654
|
+
const left = [];
|
|
655
|
+
for (const rel of [".cursor/mcp.json", ".cursor/rules/synthesisui.mdc"])
|
|
656
|
+
if (await exists(join(root, rel)))
|
|
657
|
+
left.push(rel);
|
|
658
|
+
if (left.length > 0) {
|
|
659
|
+
console.log(body(fmt(say("· Cursor is no longer wired by this CLI - what we wrote is still here: {files}"), { files: left.join(", ") })));
|
|
660
|
+
console.log(body(paint.faint(say(" the rules file is ours to delete; in `.cursor/mcp.json` only the `synthesisui` entry is - the other servers there are yours"))));
|
|
661
|
+
}
|
|
662
|
+
}
|
|
475
663
|
/**
|
|
476
664
|
* O CI: escrito quando pedido, e NÃO MAIS OFERECIDO aqui.
|
|
477
665
|
*
|
|
@@ -527,6 +715,8 @@ export async function connect(opts) {
|
|
|
527
715
|
];
|
|
528
716
|
await reportWhatIsLeft(root, {
|
|
529
717
|
cli: opts.version,
|
|
718
|
+
/** A ação recomendada abre o que ele ESCOLHEU - ver `agentOpenCommands` (etapa 12). */
|
|
719
|
+
agents: chosen.agents,
|
|
530
720
|
...(needsRestart || skillsMoved
|
|
531
721
|
? { freshSession: { loads, mcp: mcpMoved } }
|
|
532
722
|
: {}),
|
|
@@ -544,7 +734,13 @@ export async function connect(opts) {
|
|
|
544
734
|
* 2525ms -> 295ms num repo com o pacote em `node_modules`. "Ten times" era otimista; "several" é o
|
|
545
735
|
* que se sustenta.
|
|
546
736
|
*/
|
|
547
|
-
|
|
737
|
+
/**
|
|
738
|
+
* E SÓ QUANDO EXISTE HOOK - achado da revisão de QA no fecho da etapa 12: `wired.command` é
|
|
739
|
+
* preenchido mesmo quando o hook foi PULADO (é o comando que SERIA escrito), então num
|
|
740
|
+
* repositório que escolheu só o Codex a tela explicava o custo de um hook que não existe ali.
|
|
741
|
+
*/
|
|
742
|
+
if (wired.hook !== "skipped" &&
|
|
743
|
+
wired.command.startsWith("npx synthesisui@")) {
|
|
548
744
|
/**
|
|
549
745
|
* O CUSTO DO HOOK, EM UMA LINHA - eram cinco, e elas fechavam a tela.
|
|
550
746
|
*
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { pinnedHookVersion, wireAgent } from "../agent-wiring.js";
|
|
4
|
+
import { agentsToMaintain } from "../agents-chosen.js";
|
|
4
5
|
import { isOlderCli } from "../cli-version.js";
|
|
5
6
|
import { generateComponentFiles } from "../component-codegen.js";
|
|
6
7
|
import { readProjectConfig, readToken, resolveRegistry } from "../config.js";
|
|
@@ -157,7 +158,18 @@ async function rewireIfBehind(root, cli) {
|
|
|
157
158
|
const pinned = await pinnedHookVersion(root).catch(() => null);
|
|
158
159
|
if (!pinned || pinned === cli)
|
|
159
160
|
return;
|
|
160
|
-
|
|
161
|
+
/**
|
|
162
|
+
* COM QUEM JÁ ESTÁ AQUI, e nunca com a lista inteira - ver `wiredAgents`.
|
|
163
|
+
*
|
|
164
|
+
* Este comando MANTÉM o que existe: ele não pergunta nada e não pode escolher por ele. Passar a
|
|
165
|
+
* lista do produto faria um `upgrade` plantar a fiação de um agente que ele nunca marcou, que é
|
|
166
|
+
* exatamente o defeito que a etapa 12 fechou no `connect`.
|
|
167
|
+
*/
|
|
168
|
+
await wireAgent(root, cli, {
|
|
169
|
+
hook: true,
|
|
170
|
+
mcp: true,
|
|
171
|
+
agents: await agentsToMaintain(root),
|
|
172
|
+
}).catch(() => null);
|
|
161
173
|
console.log(`↻ the check after every write moved from ${pinned} to ${cli}`);
|
|
162
174
|
console.log(" Reopen your editor session - hooks are read at startup.");
|
|
163
175
|
}
|
|
@@ -37,9 +37,30 @@ register("pt-BR", {
|
|
|
37
37
|
"✓ .claude/settings.json each session opens with what is missing": "✓ .claude/settings.json cada sessão abre com o que falta",
|
|
38
38
|
"✓ .claude/settings.json the session check moved to this one": "✓ .claude/settings.json a verificação de sessão veio para esta",
|
|
39
39
|
"· .claude/settings.json the session check was already there": "· .claude/settings.json a verificação de sessão já estava lá",
|
|
40
|
+
// ── a escolha dos agentes (etapa 12) ──
|
|
41
|
+
/**
|
|
42
|
+
* A LISTA E OS NOMES DOS AGENTES NÃO TRADUZEM - `claude` e `codex` são o que ele DIGITA, e a mesma
|
|
43
|
+
* razão da frase `import my design system`: onde a máquina lê, a precisão vence a gentileza.
|
|
44
|
+
*/
|
|
45
|
+
"Which agents": "Quais agentes",
|
|
46
|
+
"{dir} is here": "{dir} está aqui",
|
|
47
|
+
"no {dir} in this repo": "nenhum {dir} neste repo",
|
|
48
|
+
"Enter keeps this. To change it, type the ones you want - or `none`:": "Enter mantém isto. Para mudar, digite os que você quer - ou `none`:",
|
|
49
|
+
"· {label} not chosen - these stay in the repo: {files}": "· {label} não escolhido - estes ficam no repo: {files}",
|
|
50
|
+
"· {label} not chosen - nothing of it was written": "· {label} não escolhido - nada dele foi escrito",
|
|
51
|
+
"· Cursor is no longer wired by this CLI - what we wrote is still here: {files}": "· o Cursor não é mais fiado por este CLI - o que a gente escreveu continua aqui: {files}",
|
|
52
|
+
"· nobody to ask here, so the folders in this repo decided: {agents}": "· não havia ninguém para perguntar, então as pastas deste repo decidiram: {agents}",
|
|
53
|
+
"Selected: {agents}": "Selecionados: {agents}",
|
|
54
|
+
" our part in {files} sits between `synthesisui:start` and `synthesisui:end` - the rest of each file is yours": " a nossa parte em {files} fica entre `synthesisui:start` e `synthesisui:end` - o resto de cada arquivo é seu",
|
|
55
|
+
" in {files} only our `synthesisui` entry is ours - the rest of the file is yours": " em {files} só a nossa entrada `synthesisui` é nossa - o resto do arquivo é seu",
|
|
56
|
+
" the write check inside it keeps running until that entry is gone": " a verificação de escrita dentro dele continua rodando até aquela entrada sair",
|
|
57
|
+
" the rules file is ours to delete; in `.cursor/mcp.json` only the `synthesisui` entry is - the other servers there are yours": " o arquivo de regras é nosso para apagar; no `.cursor/mcp.json` só a entrada `synthesisui` é - os outros servidores ali são seus",
|
|
58
|
+
"· not an agent this CLI knows: {names}": "· não é um agente que este CLI conhece: {names}",
|
|
59
|
+
"· no agent chosen, and nothing here says which agent you use - run with `--agents claude` or `--agents codex`, or run this in a terminal to pick from the list": "· nenhum agente escolhido, e nada aqui diz qual agente você usa - rode com `--agents claude` ou `--agents codex`, ou rode isto num terminal para escolher da lista",
|
|
60
|
+
"· no agent chosen - nothing was wired. Run again with `--agents claude` or `--agents codex` when you want it": "· nenhum agente escolhido - nada foi fiado. Rode de novo com `--agents claude` ou `--agents codex` quando quiser",
|
|
40
61
|
// ── o MCP e as skills ──
|
|
41
62
|
"{n} tools the agent can ask": "{n} ferramentas que o agente pode consultar",
|
|
42
|
-
"
|
|
63
|
+
"was pinned to {was} - now it follows the published reader": "estava pinado na {was} - agora acompanha o leitor publicado",
|
|
43
64
|
"unpinned - it follows the published reader": "solto - acompanha o leitor publicado",
|
|
44
65
|
"already had it": "já estava lá",
|
|
45
66
|
"already current": "já está em dia",
|
package/dist/index.js
CHANGED
|
@@ -62,7 +62,8 @@ Usage - governance (deterministic, FREE):
|
|
|
62
62
|
synthesisui adopt [--write] turn the design system you ALREADY have into a contract
|
|
63
63
|
synthesisui import [--scope <p>] [--usage <p>] read what you already have and make it a system
|
|
64
64
|
your agent follows - without touching your CSS
|
|
65
|
-
synthesisui connect [--
|
|
65
|
+
synthesisui connect [--agents claude,codex] [--ci]
|
|
66
|
+
wire the agents YOU pick: the check as an editor hook, the system
|
|
66
67
|
as MCP tools, and a contract that stops repeating itself
|
|
67
68
|
synthesisui doctor [paths…] [--verbose] audit for DRIFT: every design value written by hand, the
|
|
68
69
|
token your system already has for it, coherence, and the
|
|
@@ -268,6 +269,7 @@ async function main() {
|
|
|
268
269
|
mcp: flags.mcp !== false,
|
|
269
270
|
ci: flags.ci === true,
|
|
270
271
|
shell: flags.shell === true,
|
|
272
|
+
...(typeof flags.agents === "string" ? { agents: flags.agents } : {}),
|
|
271
273
|
});
|
|
272
274
|
return;
|
|
273
275
|
case "hook":
|
package/dist/install-marks.js
CHANGED
|
@@ -187,7 +187,34 @@
|
|
|
187
187
|
* O que o cliente ganha ao rodar `upgrade`: os componentes que ele já tem voltam a ser reescritos
|
|
188
188
|
* pelo materializador que respeita a edição dele em TODO comando, e não só em dois.
|
|
189
189
|
*/
|
|
190
|
-
|
|
190
|
+
/**
|
|
191
|
+
* 0.16.412 -> 0.16.414 em 10/09: as ferramentas do sistema param de congelar no Codex. A região que
|
|
192
|
+
* a gente escreve em `.codex/config.toml` nascia PINADA na versão do `connect`, e o `.mcp.json` do
|
|
193
|
+
* Claude Code nasce solta desde 06/08 - o agente dele numa ferramenta recebia o leitor publicado na
|
|
194
|
+
* sessão seguinte, e na outra ficava no do dia em que ele rodou o comando. Medido no repositório do
|
|
195
|
+
* dono depois de um `connect` da 0.16.413: `@latest` num arquivo, `@0.16.413` no outro.
|
|
196
|
+
*
|
|
197
|
+
* São BYTES DIFERENTES na pasta dele, e o `upgrade` chama `wireAgent`: quem já instalou sai do pin
|
|
198
|
+
* rodando o comando, e a tela diz de qual versão ele saiu.
|
|
199
|
+
*
|
|
200
|
+
* O que o cliente ganha ao rodar `upgrade`: o agente dele no Codex volta a ler o interpretador
|
|
201
|
+
* publicado, em vez do que existia no dia em que ele conectou.
|
|
202
|
+
*/
|
|
203
|
+
/**
|
|
204
|
+
* 0.16.414 -> 0.16.415 em 10/09: o conjunto de arquivos que cai na pasta dele passa a ser o que ELE
|
|
205
|
+
* ESCOLHEU. Antes o Claude Code era escrito sempre, sem evidência nenhuma - medido no mesmo dia com
|
|
206
|
+
* o comando de verdade num repositório que só tem `.codex/`: **10 arquivos, e 8 do Claude Code**
|
|
207
|
+
* (`.claude/settings.json`, cinco skills, `.mcp.json`, `CLAUDE.md`). O Cursor saiu do produto na
|
|
208
|
+
* mesma decisão, então `.cursor/mcp.json` e `.cursor/rules/synthesisui.mdc` deixam de nascer.
|
|
209
|
+
*
|
|
210
|
+
* O `upgrade` chama `wireAgent`, e ele passa a manter APENAS quem já está fiado: um `upgrade` não
|
|
211
|
+
* pergunta nada, e plantar a fiação de um agente que ele nunca marcou seria o defeito voltando pela
|
|
212
|
+
* porta da manutenção.
|
|
213
|
+
*
|
|
214
|
+
* O que o cliente ganha ao rodar `upgrade`: nada de uma ferramenta que ele não usa é reescrito no
|
|
215
|
+
* repositório dele.
|
|
216
|
+
*/
|
|
217
|
+
export const MATERIALISER_SINCE = "0.16.415";
|
|
191
218
|
/**
|
|
192
219
|
* A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
|
|
193
220
|
*
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { hasCodexBlock, pinnedInCodex } from "../codex-mcp.js";
|
|
3
4
|
/**
|
|
4
5
|
* O RECALL ESTÁ AO ALCANCE DESTE REPOSITÓRIO? - e é isto que decide o tamanho do `CLAUDE.md`.
|
|
5
6
|
*
|
|
@@ -18,8 +19,32 @@ import { join } from "node:path";
|
|
|
18
19
|
*
|
|
19
20
|
* E ela não inventa configuração: o sinal é o registro que o `connect` já escreve.
|
|
20
21
|
*/
|
|
21
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Onde os agentes leem o servidor - o mesmo shape, dois caminhos.
|
|
24
|
+
*
|
|
25
|
+
* `.cursor/mcp.json` CONTINUA SENDO LIDO depois da retirada do Cursor em 10/09, e a diferença é
|
|
26
|
+
* escrever contra ler: o produto não põe mais nada lá, e um repositório que recebeu aquele arquivo
|
|
27
|
+
* de uma versão anterior REALMENTE tem o servidor fiado. Deixar de ler seria dizer a ele que a
|
|
28
|
+
* memória não está disponível num repositório onde ela está.
|
|
29
|
+
*/
|
|
22
30
|
const MCP_FILES = [".mcp.json", ".cursor/mcp.json"];
|
|
31
|
+
/**
|
|
32
|
+
* E O CODEX PROVA PELO ARQUIVO DELE - achado da revisão de QA no fecho da etapa 12, e é uma
|
|
33
|
+
* regressão que ela criou.
|
|
34
|
+
*
|
|
35
|
+
* O Codex declara o servidor em TOML, sob `[mcp_servers.synthesisui]`, e esta função só lia JSON.
|
|
36
|
+
* Enquanto o `.mcp.json` nascia SEMPRE, um repositório só-Codex passava por acidente: o arquivo do
|
|
37
|
+
* Claude Code estava lá mesmo sem ninguém usar o Claude Code. Desde que a escolha dele decide, ele
|
|
38
|
+
* não nasce - e a capacidade sumiu para todo usuário de Codex.
|
|
39
|
+
*
|
|
40
|
+
* O QUE ELE VIVIA: o mesmo comando imprimia `✓ .codex/config.toml 20 tools the agent can ask` e,
|
|
41
|
+
* no `AGENTS.md` que aquele agente lê, escrevia a variante que diz que não há ferramenta nenhuma -
|
|
42
|
+
* o dobro de texto mandando ler o guia à mão. `recall` e `remember` sumiam.
|
|
43
|
+
*
|
|
44
|
+
* A pergunta desta função é CAPACIDADE, nunca nome de agente: existe, neste repositório, um lugar
|
|
45
|
+
* onde um agente lê o nosso servidor? O TOML é um desses lugares desde 03/09.
|
|
46
|
+
*/
|
|
47
|
+
const MCP_TOML = ".codex/config.toml";
|
|
23
48
|
/** A versão em que `recall` e `remember` passaram a existir. */
|
|
24
49
|
export const RECALL_SINCE = "0.16.264";
|
|
25
50
|
const older = (mine, than) => {
|
|
@@ -64,6 +89,20 @@ export async function recallAvailable(root) {
|
|
|
64
89
|
because: `the MCP server is registered in ${rel}`,
|
|
65
90
|
};
|
|
66
91
|
}
|
|
92
|
+
/** E O TOML DO CODEX - ver `MCP_TOML`, e o pin é lido pelo mesmo leitor que o `connect` usa. */
|
|
93
|
+
const toml = await readFile(join(root, MCP_TOML), "utf8").catch(() => null);
|
|
94
|
+
if (toml && hasCodexBlock(toml)) {
|
|
95
|
+
const pin = pinnedInCodex(toml);
|
|
96
|
+
if (pin && older(pin, RECALL_SINCE))
|
|
97
|
+
return {
|
|
98
|
+
available: false,
|
|
99
|
+
because: `the MCP server in ${MCP_TOML} is pinned to ${pin}, older than ${RECALL_SINCE} - run \`synthesisui connect\` to let it float`,
|
|
100
|
+
};
|
|
101
|
+
return {
|
|
102
|
+
available: true,
|
|
103
|
+
because: `the MCP server is registered in ${MCP_TOML}`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
67
106
|
return {
|
|
68
107
|
available: false,
|
|
69
108
|
because: "no MCP server is registered in this repository - run `synthesisui connect` so the agent can look memory up",
|
package/package.json
CHANGED