origem 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -0
- package/dist/index.js +2376 -0
- package/package.json +35 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2376 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ../origem-adapters/dist/utils.js
|
|
4
|
+
function slugify(s) {
|
|
5
|
+
return s.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "item";
|
|
6
|
+
}
|
|
7
|
+
function byType(cards, type) {
|
|
8
|
+
return cards.filter((c2) => c2.type === type);
|
|
9
|
+
}
|
|
10
|
+
function tryJson(content) {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(content);
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function hasFrontmatter(content) {
|
|
18
|
+
return /^---\s*\n[\s\S]*?\n---\s*(\n|$)/.test(content);
|
|
19
|
+
}
|
|
20
|
+
function yamlScalar(value) {
|
|
21
|
+
const v = value.replace(/\n/g, " ").trim();
|
|
22
|
+
return /[:#"'{}\[\],&*?|<>=!%@`]/.test(v) ? JSON.stringify(v) : v;
|
|
23
|
+
}
|
|
24
|
+
var DEFAULT_SECTIONS = [
|
|
25
|
+
{ type: "instruction", title: "Instru\xE7\xF5es" },
|
|
26
|
+
{ type: "spec", title: "Especifica\xE7\xF5es (requisitos)" },
|
|
27
|
+
{ type: "boundary", title: "Limites (Sempre / Perguntar antes / Nunca)" },
|
|
28
|
+
{ type: "rule", title: "Regras & Instru\xE7\xF5es" },
|
|
29
|
+
{ type: "memory", title: "Mem\xF3rias & Contexto" },
|
|
30
|
+
{ type: "knowledge", title: "Conhecimento" },
|
|
31
|
+
{ type: "prompt", title: "Prompts" },
|
|
32
|
+
{ type: "integration", title: "Integra\xE7\xF5es" }
|
|
33
|
+
];
|
|
34
|
+
function renderMarkdown(brain, sections = DEFAULT_SECTIONS) {
|
|
35
|
+
const lines = [];
|
|
36
|
+
lines.push(`# ${brain.brain.name}`);
|
|
37
|
+
if (brain.brain.description)
|
|
38
|
+
lines.push(`
|
|
39
|
+
> ${brain.brain.description}`);
|
|
40
|
+
lines.push(`
|
|
41
|
+
_Configura\xE7\xE3o gerada pelo Origem. Adote as regras, mem\xF3rias e conhecimento abaixo como contexto operacional._`);
|
|
42
|
+
for (const section of sections) {
|
|
43
|
+
const group = byType(brain.cards, section.type);
|
|
44
|
+
if (group.length === 0)
|
|
45
|
+
continue;
|
|
46
|
+
lines.push(`
|
|
47
|
+
|
|
48
|
+
## ${section.title}`);
|
|
49
|
+
for (const card of group) {
|
|
50
|
+
const folder = card.folder ? ` _(${card.folder})_` : "";
|
|
51
|
+
lines.push(`
|
|
52
|
+
### ${card.title}${folder}`);
|
|
53
|
+
if (card.tags?.length)
|
|
54
|
+
lines.push(`\`${card.tags.join("` `")}\``);
|
|
55
|
+
lines.push(`
|
|
56
|
+
${card.content}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return lines.join("\n") + "\n";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ../origem-adapters/dist/adapters.js
|
|
63
|
+
var GENERIC_SECTIONS = [
|
|
64
|
+
{ type: "instruction", title: "Instru\xE7\xF5es" },
|
|
65
|
+
{ type: "spec", title: "Especifica\xE7\xF5es (requisitos)" },
|
|
66
|
+
{ type: "boundary", title: "Limites (Sempre / Perguntar antes / Nunca)" },
|
|
67
|
+
{ type: "rule", title: "Regras & Padr\xF5es" },
|
|
68
|
+
{ type: "memory", title: "Mem\xF3rias & Contexto" },
|
|
69
|
+
{ type: "knowledge", title: "Conhecimento" },
|
|
70
|
+
{ type: "skill", title: "Skills / Capacidades" },
|
|
71
|
+
{ type: "agent", title: "Subagentes" },
|
|
72
|
+
{ type: "prompt", title: "Prompts & Comandos" },
|
|
73
|
+
{ type: "integration", title: "Integra\xE7\xF5es" }
|
|
74
|
+
];
|
|
75
|
+
function toAgentsMd(brain) {
|
|
76
|
+
return [
|
|
77
|
+
{
|
|
78
|
+
target: "agents",
|
|
79
|
+
path: "AGENTS.md",
|
|
80
|
+
content: renderMarkdown(brain, GENERIC_SECTIONS),
|
|
81
|
+
risk: "safe",
|
|
82
|
+
strategy: "managed-block"
|
|
83
|
+
}
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
var CLAUDE_SECTIONS = [
|
|
87
|
+
{ type: "instruction", title: "Instru\xE7\xF5es" },
|
|
88
|
+
{ type: "spec", title: "Especifica\xE7\xF5es (requisitos)" },
|
|
89
|
+
{ type: "boundary", title: "Limites (Sempre / Perguntar antes / Nunca)" },
|
|
90
|
+
{ type: "rule", title: "Regras & Padr\xF5es" },
|
|
91
|
+
{ type: "memory", title: "Mem\xF3rias & Contexto" },
|
|
92
|
+
{ type: "knowledge", title: "Conhecimento" },
|
|
93
|
+
{ type: "integration", title: "Integra\xE7\xF5es" }
|
|
94
|
+
];
|
|
95
|
+
function toClaudeMd(brain) {
|
|
96
|
+
return [
|
|
97
|
+
{
|
|
98
|
+
target: "claude",
|
|
99
|
+
path: "CLAUDE.md",
|
|
100
|
+
content: renderMarkdown(brain, CLAUDE_SECTIONS),
|
|
101
|
+
risk: "safe",
|
|
102
|
+
strategy: "managed-block"
|
|
103
|
+
}
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
function toGeminiMd(brain) {
|
|
107
|
+
return [
|
|
108
|
+
{
|
|
109
|
+
target: "gemini",
|
|
110
|
+
path: "GEMINI.md",
|
|
111
|
+
content: renderMarkdown(brain, GENERIC_SECTIONS),
|
|
112
|
+
risk: "safe",
|
|
113
|
+
strategy: "managed-block"
|
|
114
|
+
}
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
function toCopilot(brain) {
|
|
118
|
+
return [
|
|
119
|
+
{
|
|
120
|
+
target: "copilot",
|
|
121
|
+
path: ".github/copilot-instructions.md",
|
|
122
|
+
content: renderMarkdown(brain, GENERIC_SECTIONS),
|
|
123
|
+
risk: "safe",
|
|
124
|
+
strategy: "managed-block"
|
|
125
|
+
}
|
|
126
|
+
];
|
|
127
|
+
}
|
|
128
|
+
function toCursorRules(brain) {
|
|
129
|
+
const rules = byType(brain.cards, "rule");
|
|
130
|
+
if (rules.length === 0)
|
|
131
|
+
return [];
|
|
132
|
+
return rules.map((card) => {
|
|
133
|
+
const desc = card.title.replace(/\n/g, " ");
|
|
134
|
+
const content = `---
|
|
135
|
+
description: ${desc}
|
|
136
|
+
alwaysApply: true
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
${card.content}
|
|
140
|
+
`;
|
|
141
|
+
return {
|
|
142
|
+
target: "cursor",
|
|
143
|
+
path: `.cursor/rules/${slugify(card.title)}.mdc`,
|
|
144
|
+
content,
|
|
145
|
+
risk: "safe",
|
|
146
|
+
strategy: "whole-file"
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function toMcpJson(brain) {
|
|
151
|
+
const cards = byType(brain.cards, "mcp");
|
|
152
|
+
if (cards.length === 0)
|
|
153
|
+
return [];
|
|
154
|
+
const servers = {};
|
|
155
|
+
for (const card of cards) {
|
|
156
|
+
try {
|
|
157
|
+
const parsed = JSON.parse(card.content);
|
|
158
|
+
const inner = parsed.mcpServers ?? parsed;
|
|
159
|
+
if ("command" in inner || "url" in inner) {
|
|
160
|
+
servers[slugify(card.title)] = inner;
|
|
161
|
+
} else {
|
|
162
|
+
Object.assign(servers, inner);
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (Object.keys(servers).length === 0)
|
|
168
|
+
return [];
|
|
169
|
+
const anyStdio = Object.values(servers).some((s) => s && typeof s === "object" && "command" in s);
|
|
170
|
+
const out = [
|
|
171
|
+
{
|
|
172
|
+
target: "mcp",
|
|
173
|
+
path: ".mcp.json",
|
|
174
|
+
content: JSON.stringify({ mcpServers: servers }, null, 2) + "\n",
|
|
175
|
+
risk: anyStdio ? "privileged" : "safe",
|
|
176
|
+
strategy: "merge-json"
|
|
177
|
+
}
|
|
178
|
+
];
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ../origem-adapters/dist/claude-adapters.js
|
|
183
|
+
function mergeSettings(base, add) {
|
|
184
|
+
for (const [k, v] of Object.entries(add)) {
|
|
185
|
+
const cur = base[k];
|
|
186
|
+
if (Array.isArray(v)) {
|
|
187
|
+
const prev = Array.isArray(cur) ? cur : [];
|
|
188
|
+
base[k] = Array.from(/* @__PURE__ */ new Set([...prev, ...v]));
|
|
189
|
+
} else if (v && typeof v === "object") {
|
|
190
|
+
const prev = cur && typeof cur === "object" && !Array.isArray(cur) ? cur : {};
|
|
191
|
+
base[k] = mergeSettings({ ...prev }, v);
|
|
192
|
+
} else {
|
|
193
|
+
base[k] = v;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return base;
|
|
197
|
+
}
|
|
198
|
+
function toClaudeSettings(brain) {
|
|
199
|
+
const cards = byType(brain.cards, "harness");
|
|
200
|
+
if (cards.length === 0)
|
|
201
|
+
return [];
|
|
202
|
+
let settings = {};
|
|
203
|
+
let parsedAny = false;
|
|
204
|
+
for (const card of cards) {
|
|
205
|
+
const json = tryJson(card.content);
|
|
206
|
+
if (json && typeof json === "object" && !Array.isArray(json)) {
|
|
207
|
+
settings = mergeSettings(settings, json);
|
|
208
|
+
parsedAny = true;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (!parsedAny)
|
|
212
|
+
return [];
|
|
213
|
+
return [
|
|
214
|
+
{
|
|
215
|
+
target: "claude-settings",
|
|
216
|
+
path: ".claude/settings.json",
|
|
217
|
+
content: JSON.stringify(settings, null, 2) + "\n",
|
|
218
|
+
risk: "privileged",
|
|
219
|
+
strategy: "merge-json"
|
|
220
|
+
}
|
|
221
|
+
];
|
|
222
|
+
}
|
|
223
|
+
function toClaudeSkills(brain) {
|
|
224
|
+
const cards = byType(brain.cards, "skill");
|
|
225
|
+
return cards.map((card) => {
|
|
226
|
+
const slug = slugify(card.title);
|
|
227
|
+
let content;
|
|
228
|
+
if (hasFrontmatter(card.content)) {
|
|
229
|
+
content = card.content.endsWith("\n") ? card.content : card.content + "\n";
|
|
230
|
+
} else {
|
|
231
|
+
const description = card.tags?.length ? card.tags.join(", ") : card.title;
|
|
232
|
+
content = `---
|
|
233
|
+
name: ${yamlScalar(slug)}
|
|
234
|
+
description: ${yamlScalar(description)}
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
# ${card.title}
|
|
238
|
+
|
|
239
|
+
${card.content}
|
|
240
|
+
`;
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
target: "claude-skills",
|
|
244
|
+
path: `.claude/skills/${slug}/SKILL.md`,
|
|
245
|
+
content,
|
|
246
|
+
risk: "safe",
|
|
247
|
+
strategy: "whole-file"
|
|
248
|
+
};
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
function toClaudeCommands(brain) {
|
|
252
|
+
const cards = byType(brain.cards, "prompt");
|
|
253
|
+
return cards.map((card) => {
|
|
254
|
+
const slug = slugify(card.title);
|
|
255
|
+
let content;
|
|
256
|
+
if (hasFrontmatter(card.content)) {
|
|
257
|
+
content = card.content.endsWith("\n") ? card.content : card.content + "\n";
|
|
258
|
+
} else {
|
|
259
|
+
content = `---
|
|
260
|
+
description: ${yamlScalar(card.title)}
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
${card.content}
|
|
264
|
+
`;
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
target: "claude-commands",
|
|
268
|
+
path: `.claude/commands/${slug}.md`,
|
|
269
|
+
content,
|
|
270
|
+
risk: "safe",
|
|
271
|
+
strategy: "whole-file"
|
|
272
|
+
};
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
function toClaudeAgents(brain) {
|
|
276
|
+
const cards = byType(brain.cards, "agent");
|
|
277
|
+
return cards.map((card) => {
|
|
278
|
+
const slug = slugify(card.title);
|
|
279
|
+
let content;
|
|
280
|
+
if (hasFrontmatter(card.content)) {
|
|
281
|
+
content = card.content.endsWith("\n") ? card.content : card.content + "\n";
|
|
282
|
+
} else {
|
|
283
|
+
const spec = tryJson(card.content) ?? {};
|
|
284
|
+
const description = spec.description ?? card.title;
|
|
285
|
+
const tools = Array.isArray(spec.tools) ? spec.tools.join(", ") : spec.tools;
|
|
286
|
+
const body = spec.prompt ?? (tryJson(card.content) ? "" : card.content);
|
|
287
|
+
const fm = [
|
|
288
|
+
`name: ${yamlScalar(slug)}`,
|
|
289
|
+
`description: ${yamlScalar(description)}`,
|
|
290
|
+
tools ? `tools: ${yamlScalar(tools)}` : null,
|
|
291
|
+
spec.model ? `model: ${yamlScalar(spec.model)}` : null
|
|
292
|
+
].filter(Boolean).join("\n");
|
|
293
|
+
content = `---
|
|
294
|
+
${fm}
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
${body}
|
|
298
|
+
`;
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
target: "claude-agents",
|
|
302
|
+
path: `.claude/agents/${slug}.md`,
|
|
303
|
+
content,
|
|
304
|
+
risk: "safe",
|
|
305
|
+
strategy: "whole-file"
|
|
306
|
+
};
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ../origem-adapters/dist/capabilities.js
|
|
311
|
+
var ALL_DEGRADED = {
|
|
312
|
+
instruction: "native",
|
|
313
|
+
spec: "native",
|
|
314
|
+
boundary: "native",
|
|
315
|
+
rule: "native",
|
|
316
|
+
memory: "native",
|
|
317
|
+
knowledge: "native",
|
|
318
|
+
prompt: "degraded",
|
|
319
|
+
integration: "native",
|
|
320
|
+
skill: "degraded",
|
|
321
|
+
agent: "degraded",
|
|
322
|
+
harness: "none",
|
|
323
|
+
mcp: "none"
|
|
324
|
+
};
|
|
325
|
+
var TARGET_CAPABILITIES = [
|
|
326
|
+
{
|
|
327
|
+
id: "claude",
|
|
328
|
+
label: "Claude Code \xB7 CLAUDE.md",
|
|
329
|
+
path: "CLAUDE.md",
|
|
330
|
+
support: {
|
|
331
|
+
instruction: "native",
|
|
332
|
+
spec: "native",
|
|
333
|
+
// renderizado no CLAUDE.md
|
|
334
|
+
boundary: "native",
|
|
335
|
+
// renderizado no CLAUDE.md
|
|
336
|
+
rule: "native",
|
|
337
|
+
memory: "native",
|
|
338
|
+
knowledge: "native",
|
|
339
|
+
integration: "native",
|
|
340
|
+
prompt: "none",
|
|
341
|
+
// vira slash command (claude-commands)
|
|
342
|
+
skill: "none",
|
|
343
|
+
// vira SKILL.md (claude-skills)
|
|
344
|
+
agent: "none",
|
|
345
|
+
// vira subagente (claude-agents)
|
|
346
|
+
harness: "none",
|
|
347
|
+
// vira settings.json (claude-settings)
|
|
348
|
+
mcp: "none"
|
|
349
|
+
// vira .mcp.json
|
|
350
|
+
},
|
|
351
|
+
note: "Instru\xE7\xF5es can\xF4nicas. Prompt/skill/agent/harness/mcp viram arquivos pr\xF3prios do Claude."
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
id: "claude-settings",
|
|
355
|
+
label: "Claude Code \xB7 settings.json",
|
|
356
|
+
path: ".claude/settings.json",
|
|
357
|
+
support: onlyNative("harness"),
|
|
358
|
+
note: "Permiss\xF5es, env, hooks e modelo. Privilegiado (executa na m\xE1quina)."
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
id: "claude-skills",
|
|
362
|
+
label: "Claude Code \xB7 Agent Skills",
|
|
363
|
+
path: ".claude/skills/<nome>/SKILL.md",
|
|
364
|
+
support: onlyNative("skill")
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
id: "claude-commands",
|
|
368
|
+
label: "Claude Code \xB7 Slash commands",
|
|
369
|
+
path: ".claude/commands/<nome>.md",
|
|
370
|
+
support: onlyNative("prompt")
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
id: "claude-agents",
|
|
374
|
+
label: "Claude Code \xB7 Subagentes",
|
|
375
|
+
path: ".claude/agents/<nome>.md",
|
|
376
|
+
support: onlyNative("agent")
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
id: "mcp",
|
|
380
|
+
label: "MCP servers \xB7 .mcp.json",
|
|
381
|
+
path: ".mcp.json",
|
|
382
|
+
support: onlyNative("mcp"),
|
|
383
|
+
note: "Port\xE1vel em conceito; o CAMINHO varia por ferramenta (Cursor: .cursor/mcp.json, VS Code: .vscode/mcp.json)."
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
id: "agents",
|
|
387
|
+
label: "AGENTS.md (Codex, Windsurf, Zed, Aider\u2026)",
|
|
388
|
+
path: "AGENTS.md",
|
|
389
|
+
support: ALL_DEGRADED,
|
|
390
|
+
note: "Padr\xE3o convergido de instru\xE7\xF5es. Sem conceito nativo de skills/subagentes \u2192 viram documenta\xE7\xE3o."
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
id: "gemini",
|
|
394
|
+
label: "Gemini CLI \xB7 GEMINI.md",
|
|
395
|
+
path: "GEMINI.md",
|
|
396
|
+
support: ALL_DEGRADED
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
id: "copilot",
|
|
400
|
+
label: "GitHub Copilot \xB7 instructions",
|
|
401
|
+
path: ".github/copilot-instructions.md",
|
|
402
|
+
support: ALL_DEGRADED
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
id: "cursor",
|
|
406
|
+
label: "Cursor \xB7 rules",
|
|
407
|
+
path: ".cursor/rules/*.mdc",
|
|
408
|
+
support: {
|
|
409
|
+
...ALL_DEGRADED,
|
|
410
|
+
instruction: "degraded",
|
|
411
|
+
// nosso adapter de cursor só emite `rule`
|
|
412
|
+
spec: "none",
|
|
413
|
+
boundary: "none",
|
|
414
|
+
memory: "none",
|
|
415
|
+
knowledge: "none",
|
|
416
|
+
prompt: "none",
|
|
417
|
+
integration: "none",
|
|
418
|
+
skill: "none",
|
|
419
|
+
agent: "none"
|
|
420
|
+
},
|
|
421
|
+
note: "Nosso adapter gera s\xF3 regras (.mdc). Cursor tamb\xE9m l\xEA AGENTS.md e .cursor/mcp.json (roadmap)."
|
|
422
|
+
}
|
|
423
|
+
];
|
|
424
|
+
function onlyNative(type) {
|
|
425
|
+
const base = {
|
|
426
|
+
instruction: "none",
|
|
427
|
+
spec: "none",
|
|
428
|
+
boundary: "none",
|
|
429
|
+
harness: "none",
|
|
430
|
+
rule: "none",
|
|
431
|
+
memory: "none",
|
|
432
|
+
knowledge: "none",
|
|
433
|
+
prompt: "none",
|
|
434
|
+
skill: "none",
|
|
435
|
+
agent: "none",
|
|
436
|
+
mcp: "none",
|
|
437
|
+
integration: "none"
|
|
438
|
+
};
|
|
439
|
+
base[type] = "native";
|
|
440
|
+
return base;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ../origem-adapters/dist/index.js
|
|
444
|
+
var ADAPTERS = {
|
|
445
|
+
agents: toAgentsMd,
|
|
446
|
+
claude: toClaudeMd,
|
|
447
|
+
gemini: toGeminiMd,
|
|
448
|
+
copilot: toCopilot,
|
|
449
|
+
cursor: toCursorRules,
|
|
450
|
+
mcp: toMcpJson,
|
|
451
|
+
"claude-settings": toClaudeSettings,
|
|
452
|
+
"claude-skills": toClaudeSkills,
|
|
453
|
+
"claude-commands": toClaudeCommands,
|
|
454
|
+
"claude-agents": toClaudeAgents
|
|
455
|
+
};
|
|
456
|
+
var ALL_TARGETS = Object.keys(ADAPTERS);
|
|
457
|
+
var CLAUDE_TARGETS = [
|
|
458
|
+
"claude",
|
|
459
|
+
"claude-settings",
|
|
460
|
+
"claude-skills",
|
|
461
|
+
"claude-commands",
|
|
462
|
+
"claude-agents",
|
|
463
|
+
"mcp"
|
|
464
|
+
];
|
|
465
|
+
function adapt(brain, targets = ALL_TARGETS) {
|
|
466
|
+
const out = [];
|
|
467
|
+
for (const target of targets) {
|
|
468
|
+
const fn = ADAPTERS[target];
|
|
469
|
+
if (fn)
|
|
470
|
+
out.push(...fn(brain));
|
|
471
|
+
}
|
|
472
|
+
return out;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// src/signing.ts
|
|
476
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
477
|
+
function stableStringify(value) {
|
|
478
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
479
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
480
|
+
const obj = value;
|
|
481
|
+
const keys = Object.keys(obj).sort();
|
|
482
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
|
|
483
|
+
}
|
|
484
|
+
function signPayload(payload, secret) {
|
|
485
|
+
return createHmac("sha256", secret).update(stableStringify(payload)).digest("hex");
|
|
486
|
+
}
|
|
487
|
+
function verifyPayload(payload, signature, secret) {
|
|
488
|
+
try {
|
|
489
|
+
const a = Buffer.from(signPayload(payload, secret), "hex");
|
|
490
|
+
const b = Buffer.from(signature, "hex");
|
|
491
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
492
|
+
} catch {
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// src/config.ts
|
|
498
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
499
|
+
import { homedir } from "node:os";
|
|
500
|
+
import { dirname, join } from "node:path";
|
|
501
|
+
function configPath() {
|
|
502
|
+
return join(homedir(), ".origem", "config.json");
|
|
503
|
+
}
|
|
504
|
+
function readConfig() {
|
|
505
|
+
const p = configPath();
|
|
506
|
+
if (!existsSync(p)) return {};
|
|
507
|
+
try {
|
|
508
|
+
return JSON.parse(readFileSync(p, "utf8"));
|
|
509
|
+
} catch {
|
|
510
|
+
return {};
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
function saveConfig(patch) {
|
|
514
|
+
const p = configPath();
|
|
515
|
+
const dir = dirname(p);
|
|
516
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
517
|
+
const next = { ...readConfig(), ...patch };
|
|
518
|
+
writeFileSync(p, `${JSON.stringify(next, null, 2)}
|
|
519
|
+
`, { mode: 384 });
|
|
520
|
+
try {
|
|
521
|
+
chmodSync(dir, 448);
|
|
522
|
+
chmodSync(p, 384);
|
|
523
|
+
} catch {
|
|
524
|
+
}
|
|
525
|
+
return p;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// src/lock.ts
|
|
529
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
530
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
531
|
+
var LOCK_DIR = ".origem";
|
|
532
|
+
var LOCK_FILE = join2(LOCK_DIR, "lock.json");
|
|
533
|
+
function lockPath(baseDir) {
|
|
534
|
+
return join2(baseDir, LOCK_FILE);
|
|
535
|
+
}
|
|
536
|
+
function readLock(baseDir) {
|
|
537
|
+
const p = lockPath(baseDir);
|
|
538
|
+
if (!existsSync2(p)) return null;
|
|
539
|
+
try {
|
|
540
|
+
return JSON.parse(readFileSync2(p, "utf8"));
|
|
541
|
+
} catch {
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
function writeLock(baseDir, lock) {
|
|
546
|
+
const p = lockPath(baseDir);
|
|
547
|
+
mkdirSync2(dirname2(p), { recursive: true });
|
|
548
|
+
writeFileSync2(p, JSON.stringify(lock, null, 2) + "\n", "utf8");
|
|
549
|
+
}
|
|
550
|
+
function deleteLock(baseDir) {
|
|
551
|
+
const dir = join2(baseDir, LOCK_DIR);
|
|
552
|
+
if (existsSync2(dir)) rmSync(dir, { recursive: true, force: true });
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// src/ui.ts
|
|
556
|
+
import { createInterface } from "node:readline";
|
|
557
|
+
var c = {
|
|
558
|
+
dim: (s) => `\x1B[2m${s}\x1B[0m`,
|
|
559
|
+
bold: (s) => `\x1B[1m${s}\x1B[0m`,
|
|
560
|
+
green: (s) => `\x1B[32m${s}\x1B[0m`,
|
|
561
|
+
yellow: (s) => `\x1B[33m${s}\x1B[0m`,
|
|
562
|
+
red: (s) => `\x1B[31m${s}\x1B[0m`,
|
|
563
|
+
cyan: (s) => `\x1B[36m${s}\x1B[0m`
|
|
564
|
+
};
|
|
565
|
+
var color = c;
|
|
566
|
+
function info(msg) {
|
|
567
|
+
console.log(msg);
|
|
568
|
+
}
|
|
569
|
+
function warn(msg) {
|
|
570
|
+
console.warn(c.yellow(`\u26A0 ${msg}`));
|
|
571
|
+
}
|
|
572
|
+
function err(msg) {
|
|
573
|
+
console.error(c.red(`\u2717 ${msg}`));
|
|
574
|
+
}
|
|
575
|
+
function ok(msg) {
|
|
576
|
+
console.log(c.green(`\u2713 ${msg}`));
|
|
577
|
+
}
|
|
578
|
+
async function confirm(question) {
|
|
579
|
+
if (!process.stdin.isTTY) return false;
|
|
580
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
581
|
+
try {
|
|
582
|
+
const answer = await new Promise(
|
|
583
|
+
(resolve) => rl.question(`${question} ${c.dim("[y/N]")} `, resolve)
|
|
584
|
+
);
|
|
585
|
+
return /^s|sim|y|yes$/i.test(answer.trim());
|
|
586
|
+
} finally {
|
|
587
|
+
rl.close();
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// src/http.ts
|
|
592
|
+
var DEFAULT_API = "https://origem-ai.web.app";
|
|
593
|
+
async function fetchBrain(apiUrl, key) {
|
|
594
|
+
const base = apiUrl.replace(/\/$/, "");
|
|
595
|
+
const url = `${base}/pull?k=${encodeURIComponent(key)}&format=json`;
|
|
596
|
+
let res;
|
|
597
|
+
try {
|
|
598
|
+
res = await fetch(url);
|
|
599
|
+
} catch (e) {
|
|
600
|
+
throw new Error(`N\xE3o consegui conectar em ${base}: ${e.message}`);
|
|
601
|
+
}
|
|
602
|
+
if (res.status === 401) {
|
|
603
|
+
throw new Error("Token inv\xE1lido, expirado ou revogado. Gere um novo link no Origem.");
|
|
604
|
+
}
|
|
605
|
+
if (res.status === 403) {
|
|
606
|
+
throw new Error("Token sem permiss\xE3o de leitura.");
|
|
607
|
+
}
|
|
608
|
+
if (!res.ok) {
|
|
609
|
+
throw new Error(`Falha ao puxar o Brain (HTTP ${res.status}).`);
|
|
610
|
+
}
|
|
611
|
+
const payload = await res.json();
|
|
612
|
+
const secret = process.env.ORIGEM_SIGNING_SECRET;
|
|
613
|
+
if (payload.signature) {
|
|
614
|
+
if (!secret) {
|
|
615
|
+
warn(
|
|
616
|
+
"O payload vem assinado, mas ORIGEM_SIGNING_SECRET n\xE3o est\xE1 definido \u2014 n\xE3o \xE9 poss\xEDvel verificar a origem. Config privilegiada ser\xE1 bloqueada."
|
|
617
|
+
);
|
|
618
|
+
payload.__unverified = true;
|
|
619
|
+
} else {
|
|
620
|
+
const { signature, ...rest } = payload;
|
|
621
|
+
if (!verifyPayload(rest, signature, secret)) {
|
|
622
|
+
throw new Error(
|
|
623
|
+
"Assinatura inv\xE1lida \u2014 o Brain pode ter sido adulterado. Abortando."
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
} else if (secret) {
|
|
628
|
+
warn(
|
|
629
|
+
"ORIGEM_SIGNING_SECRET est\xE1 definido, mas o payload N\xC3O vem assinado. Config privilegiada ser\xE1 bloqueada."
|
|
630
|
+
);
|
|
631
|
+
payload.__unverified = true;
|
|
632
|
+
}
|
|
633
|
+
return payload;
|
|
634
|
+
}
|
|
635
|
+
function resolveApiUrl(flag) {
|
|
636
|
+
const fromLock = readLock(process.cwd())?.apiUrl || void 0;
|
|
637
|
+
return (flag || process.env.ORIGEM_API_URL || fromLock || readConfig().apiUrl || DEFAULT_API).replace(/\/$/, "");
|
|
638
|
+
}
|
|
639
|
+
function resolveKey(flag) {
|
|
640
|
+
const key = flag || process.env.ORIGEM_TOKEN || readConfig().token || "";
|
|
641
|
+
if (!key) {
|
|
642
|
+
throw new Error(
|
|
643
|
+
"Token ausente. Rode `origem login --key <token>` uma vez (grava em ~/.origem/config.json), ou passe --key / ORIGEM_TOKEN. Gere o link em 'Conectar com IA' no app Origem."
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
return key;
|
|
647
|
+
}
|
|
648
|
+
async function probeToken(apiUrl, key) {
|
|
649
|
+
let res;
|
|
650
|
+
try {
|
|
651
|
+
res = await fetch(`${apiUrl}/run?k=${encodeURIComponent(key)}`, {
|
|
652
|
+
method: "POST",
|
|
653
|
+
headers: { "Content-Type": "application/json" },
|
|
654
|
+
body: JSON.stringify({ sessionId: "origem-login-probe", event: "poll", pullCommand: false }),
|
|
655
|
+
signal: AbortSignal.timeout(1e4)
|
|
656
|
+
});
|
|
657
|
+
} catch (e) {
|
|
658
|
+
throw new Error(`N\xE3o consegui falar com ${apiUrl}: ${e.message}`);
|
|
659
|
+
}
|
|
660
|
+
if (res.status === 401) throw new Error("Token inv\xE1lido, expirado ou revogado.");
|
|
661
|
+
if (res.status === 403) {
|
|
662
|
+
throw new Error(
|
|
663
|
+
"Token sem escopo de escrita. O watch e o hook precisam de um link de escrita (ingest\xE3o)."
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
if (res.status === 404) {
|
|
667
|
+
throw new Error(
|
|
668
|
+
`Esta API n\xE3o tem o endpoint /run (HTTP 404). Ela precisa estar publicada com o m\xF3dulo de runs.`
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
if (!res.ok) throw new Error(`Falha na verifica\xE7\xE3o (HTTP ${res.status}).`);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// src/fsutil.ts
|
|
675
|
+
import { createHash } from "node:crypto";
|
|
676
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, rmdirSync, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
677
|
+
import { dirname as dirname3, join as join3 } from "node:path";
|
|
678
|
+
import { homedir as homedir2 } from "node:os";
|
|
679
|
+
var MB_START = "<!-- origem:managed:start -->";
|
|
680
|
+
var MB_END = "<!-- origem:managed:end -->";
|
|
681
|
+
function sha256(s) {
|
|
682
|
+
return createHash("sha256").update(s).digest("hex");
|
|
683
|
+
}
|
|
684
|
+
function readText(abs) {
|
|
685
|
+
try {
|
|
686
|
+
return readFileSync3(abs, "utf8");
|
|
687
|
+
} catch {
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
function resolveTarget(scope, relPath) {
|
|
692
|
+
if (scope === "project") return join3(process.cwd(), relPath);
|
|
693
|
+
const home = homedir2();
|
|
694
|
+
if (relPath === "CLAUDE.md") return join3(home, ".claude", "CLAUDE.md");
|
|
695
|
+
if (relPath.startsWith(".claude/")) return join3(home, relPath);
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
function ensureDir(abs) {
|
|
699
|
+
mkdirSync3(dirname3(abs), { recursive: true });
|
|
700
|
+
}
|
|
701
|
+
function wrapBlock(content) {
|
|
702
|
+
const body = content.replace(/\n+$/, "");
|
|
703
|
+
return `${MB_START}
|
|
704
|
+
<!-- Gerado pelo Origem (origem apply). N\xE3o edite dentro do bloco. -->
|
|
705
|
+
${body}
|
|
706
|
+
${MB_END}
|
|
707
|
+
`;
|
|
708
|
+
}
|
|
709
|
+
function applyManagedBlock(existing, content) {
|
|
710
|
+
const block = wrapBlock(content);
|
|
711
|
+
if (existing == null) return block;
|
|
712
|
+
const start = existing.indexOf(MB_START);
|
|
713
|
+
const end = existing.indexOf(MB_END);
|
|
714
|
+
if (start !== -1 && end !== -1 && end > start) {
|
|
715
|
+
const before = existing.slice(0, start);
|
|
716
|
+
const after = existing.slice(end + MB_END.length).replace(/^\n/, "");
|
|
717
|
+
return `${before}${block}${after}`;
|
|
718
|
+
}
|
|
719
|
+
const sep = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
720
|
+
return `${existing}${sep}${block}`;
|
|
721
|
+
}
|
|
722
|
+
function extractManagedBlock(existing) {
|
|
723
|
+
const start = existing.indexOf(MB_START);
|
|
724
|
+
const end = existing.indexOf(MB_END);
|
|
725
|
+
if (start === -1 || end === -1 || end <= start) return null;
|
|
726
|
+
return existing.slice(start, end + MB_END.length);
|
|
727
|
+
}
|
|
728
|
+
function deepMergeJson(base, add) {
|
|
729
|
+
for (const [k, v] of Object.entries(add)) {
|
|
730
|
+
const cur = base[k];
|
|
731
|
+
if (Array.isArray(v)) {
|
|
732
|
+
const prev = Array.isArray(cur) ? cur : [];
|
|
733
|
+
base[k] = Array.from(/* @__PURE__ */ new Set([...prev, ...v]));
|
|
734
|
+
} else if (v && typeof v === "object") {
|
|
735
|
+
const prev = cur && typeof cur === "object" && !Array.isArray(cur) ? cur : {};
|
|
736
|
+
base[k] = deepMergeJson({ ...prev }, v);
|
|
737
|
+
} else {
|
|
738
|
+
base[k] = v;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return base;
|
|
742
|
+
}
|
|
743
|
+
function normBody(s) {
|
|
744
|
+
return s.replace(/^\n+/, "").replace(/\n+$/, "");
|
|
745
|
+
}
|
|
746
|
+
function managedHash(strategy, content) {
|
|
747
|
+
if (strategy === "merge-json") {
|
|
748
|
+
return sha256(stableStringify(JSON.parse(content)));
|
|
749
|
+
}
|
|
750
|
+
return sha256(strategy === "managed-block" ? normBody(content) : content);
|
|
751
|
+
}
|
|
752
|
+
function diskManagedHash(abs, strategy, jsonKeys) {
|
|
753
|
+
const existing = readText(abs);
|
|
754
|
+
if (existing == null) return null;
|
|
755
|
+
if (strategy === "whole-file") return sha256(existing);
|
|
756
|
+
if (strategy === "managed-block") {
|
|
757
|
+
const block = extractManagedBlock(existing);
|
|
758
|
+
return block == null ? null : sha256(unwrapBlock(block));
|
|
759
|
+
}
|
|
760
|
+
try {
|
|
761
|
+
const json = JSON.parse(existing);
|
|
762
|
+
const owned = {};
|
|
763
|
+
for (const k of jsonKeys ?? []) if (k in json) owned[k] = json[k];
|
|
764
|
+
return sha256(stableStringify(owned));
|
|
765
|
+
} catch {
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
function unwrapBlock(block) {
|
|
770
|
+
return block.replace(MB_START, "").replace(MB_END, "").replace(/^\s*<!-- Gerado pelo Origem[^\n]*-->\n?/, "").replace(/^\n+/, "").replace(/\n+$/, "");
|
|
771
|
+
}
|
|
772
|
+
function writeTarget(abs, strategy, content) {
|
|
773
|
+
ensureDir(abs);
|
|
774
|
+
if (strategy === "whole-file") {
|
|
775
|
+
writeFileSync3(abs, content, "utf8");
|
|
776
|
+
return { hash: sha256(content) };
|
|
777
|
+
}
|
|
778
|
+
if (strategy === "managed-block") {
|
|
779
|
+
const merged2 = applyManagedBlock(readText(abs), content);
|
|
780
|
+
writeFileSync3(abs, merged2, "utf8");
|
|
781
|
+
return { hash: sha256(normBody(content)) };
|
|
782
|
+
}
|
|
783
|
+
const incoming = JSON.parse(content);
|
|
784
|
+
const existing = readText(abs);
|
|
785
|
+
const base = existing ? JSON.parse(existing) : {};
|
|
786
|
+
const merged = deepMergeJson(base, structuredClone(incoming));
|
|
787
|
+
writeFileSync3(abs, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
788
|
+
return { hash: sha256(stableStringify(incoming)), jsonKeys: Object.keys(incoming) };
|
|
789
|
+
}
|
|
790
|
+
function removeTarget(abs, strategy, jsonKeys) {
|
|
791
|
+
const existing = readText(abs);
|
|
792
|
+
if (existing == null) return;
|
|
793
|
+
if (strategy === "whole-file") {
|
|
794
|
+
unlinkSync(abs);
|
|
795
|
+
pruneEmptyDirs(dirname3(abs));
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
if (strategy === "managed-block") {
|
|
799
|
+
const start = existing.indexOf(MB_START);
|
|
800
|
+
const end = existing.indexOf(MB_END);
|
|
801
|
+
if (start === -1 || end === -1) return;
|
|
802
|
+
const rest = (existing.slice(0, start) + existing.slice(end + MB_END.length)).replace(/\n{3,}/g, "\n\n").trim();
|
|
803
|
+
if (rest === "") unlinkSync(abs);
|
|
804
|
+
else writeFileSync3(abs, rest + "\n", "utf8");
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
try {
|
|
808
|
+
const json = JSON.parse(existing);
|
|
809
|
+
for (const k of jsonKeys ?? []) delete json[k];
|
|
810
|
+
if (Object.keys(json).length === 0) unlinkSync(abs);
|
|
811
|
+
else writeFileSync3(abs, JSON.stringify(json, null, 2) + "\n", "utf8");
|
|
812
|
+
} catch {
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
function pruneEmptyDirs(dir) {
|
|
816
|
+
for (let d = dir; d && d !== dirname3(d); d = dirname3(d)) {
|
|
817
|
+
if (!existsSync3(d)) continue;
|
|
818
|
+
try {
|
|
819
|
+
rmdirSync(d);
|
|
820
|
+
} catch {
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// src/commands.ts
|
|
827
|
+
function buildPlan(brain, scope, targets, lock) {
|
|
828
|
+
const outputs = adapt(brain, targets);
|
|
829
|
+
const byPath = new Map((lock?.files ?? []).map((f) => [f.path, f]));
|
|
830
|
+
const items = [];
|
|
831
|
+
const skipped = [];
|
|
832
|
+
for (const output of outputs) {
|
|
833
|
+
const abs = resolveTarget(scope, output.path);
|
|
834
|
+
if (!abs) {
|
|
835
|
+
skipped.push(output);
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
const strategy = output.strategy ?? "whole-file";
|
|
839
|
+
const risk = output.risk ?? "safe";
|
|
840
|
+
const newHash = managedHash(strategy, output.content);
|
|
841
|
+
const prev = byPath.get(output.path);
|
|
842
|
+
const action = !prev ? "create" : prev.hash !== newHash ? "update" : "unchanged";
|
|
843
|
+
const diskHash = diskManagedHash(abs, strategy, prev?.jsonKeys);
|
|
844
|
+
const driftedLocally = !!prev && diskHash !== null && diskHash !== prev.hash;
|
|
845
|
+
items.push({ output, abs, strategy, risk, action, newHash, driftedLocally });
|
|
846
|
+
}
|
|
847
|
+
return { items, skipped };
|
|
848
|
+
}
|
|
849
|
+
function actionLabel(a) {
|
|
850
|
+
if (a === "create") return color.green("criar ");
|
|
851
|
+
if (a === "update") return color.yellow("atualizar");
|
|
852
|
+
return color.dim("ok ");
|
|
853
|
+
}
|
|
854
|
+
function printPlan(items, skipped) {
|
|
855
|
+
info(color.bold("\nPlano:"));
|
|
856
|
+
for (const it of items) {
|
|
857
|
+
const risk = it.risk === "privileged" ? color.red(" [privilegiado]") : "";
|
|
858
|
+
const drift = it.driftedLocally ? color.yellow(" (editado localmente)") : "";
|
|
859
|
+
info(` ${actionLabel(it.action)} ${it.output.path}${risk}${drift}`);
|
|
860
|
+
}
|
|
861
|
+
for (const s of skipped) {
|
|
862
|
+
info(` ${color.dim("pular ")} ${s.path} ${color.dim("(n\xE3o se aplica a este escopo)")}`);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
async function runApply(opts) {
|
|
866
|
+
const { brain, scope, targets, dryRun, yes } = opts;
|
|
867
|
+
const baseDir = process.cwd();
|
|
868
|
+
const lock = readLock(baseDir);
|
|
869
|
+
const { items, skipped } = buildPlan(brain, scope, targets, lock);
|
|
870
|
+
const unverified = !!brain.__unverified;
|
|
871
|
+
const toWrite = items.filter((i) => i.action !== "unchanged");
|
|
872
|
+
const privileged = toWrite.filter((i) => i.risk === "privileged");
|
|
873
|
+
const currentPaths = new Set(items.map((i) => i.output.path));
|
|
874
|
+
const orphans = (lock?.files ?? []).filter((f) => !currentPaths.has(f.path));
|
|
875
|
+
printPlan(items, skipped);
|
|
876
|
+
if (orphans.length) {
|
|
877
|
+
info(color.bold("\nRemover (n\xE3o est\xE3o mais no Brain):"));
|
|
878
|
+
for (const o of orphans) info(` ${color.red("remover ")} ${o.path}`);
|
|
879
|
+
}
|
|
880
|
+
if (dryRun) {
|
|
881
|
+
info(color.dim("\n(dry-run \u2014 nada foi escrito.)"));
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
let allowPrivileged = privileged.length === 0;
|
|
885
|
+
if (privileged.length > 0) {
|
|
886
|
+
if (unverified) {
|
|
887
|
+
warn(
|
|
888
|
+
"Config privilegiada bloqueada: o payload n\xE3o p\xF4de ser verificado (assinatura ausente/ inv\xE1lida)."
|
|
889
|
+
);
|
|
890
|
+
allowPrivileged = false;
|
|
891
|
+
} else if (yes) {
|
|
892
|
+
allowPrivileged = true;
|
|
893
|
+
} else if (!process.stdin.isTTY) {
|
|
894
|
+
warn("Sem terminal interativo \u2014 pulando config privilegiada. Use --yes para aplicar.");
|
|
895
|
+
allowPrivileged = false;
|
|
896
|
+
} else {
|
|
897
|
+
info(color.bold("\n\u26A0 Config privilegiada (executa comandos/segredos na sua m\xE1quina):"));
|
|
898
|
+
for (const p of privileged) info(` \u2022 ${p.output.path}`);
|
|
899
|
+
allowPrivileged = await confirm("Aplicar tamb\xE9m esses arquivos privilegiados?");
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
for (const o of orphans) {
|
|
903
|
+
const abs = resolveTarget(scope, o.path);
|
|
904
|
+
if (abs) removeTarget(abs, o.strategy, o.jsonKeys);
|
|
905
|
+
}
|
|
906
|
+
const nextEntries = new Map(
|
|
907
|
+
(lock?.files ?? []).filter((f) => currentPaths.has(f.path)).map((f) => [f.path, f])
|
|
908
|
+
);
|
|
909
|
+
let written = 0;
|
|
910
|
+
for (const it of items) {
|
|
911
|
+
const isPriv = it.risk === "privileged";
|
|
912
|
+
if (it.action !== "unchanged" && (!isPriv || allowPrivileged)) {
|
|
913
|
+
const res = writeTarget(it.abs, it.strategy, it.output.content);
|
|
914
|
+
nextEntries.set(it.output.path, {
|
|
915
|
+
path: it.output.path,
|
|
916
|
+
target: it.output.target,
|
|
917
|
+
strategy: it.strategy,
|
|
918
|
+
risk: it.risk,
|
|
919
|
+
hash: res.hash,
|
|
920
|
+
jsonKeys: res.jsonKeys
|
|
921
|
+
});
|
|
922
|
+
written++;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
writeLock(baseDir, {
|
|
926
|
+
version: 1,
|
|
927
|
+
brainId: brain.brain.id ?? null,
|
|
928
|
+
brainName: brain.brain.name ?? null,
|
|
929
|
+
revision: brain.origem?.revision ?? null,
|
|
930
|
+
apiUrl: brain.__apiUrl ?? "",
|
|
931
|
+
scope,
|
|
932
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
933
|
+
files: [...nextEntries.values()]
|
|
934
|
+
});
|
|
935
|
+
ok(
|
|
936
|
+
`Aplicado: ${written} arquivo(s) escrito(s), ${orphans.length} removido(s). Estado em ${color.cyan(".origem/lock.json")}.`
|
|
937
|
+
);
|
|
938
|
+
if (!allowPrivileged && privileged.length > 0) {
|
|
939
|
+
warn(`${privileged.length} arquivo(s) privilegiado(s) N\xC3O aplicado(s).`);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
function runStatus(brain, scope, targets) {
|
|
943
|
+
const baseDir = process.cwd();
|
|
944
|
+
const lock = readLock(baseDir);
|
|
945
|
+
if (!lock) {
|
|
946
|
+
info("N\xE3o inicializado neste diret\xF3rio. Rode `origem apply`.");
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
const remoteRev = brain.origem?.revision ?? null;
|
|
950
|
+
const localRev = lock.revision;
|
|
951
|
+
info(color.bold(`Brain: ${lock.brainName ?? lock.brainId ?? "?"}`));
|
|
952
|
+
info(
|
|
953
|
+
`Revis\xE3o local: ${localRev ?? "?"} \xB7 remota: ${remoteRev ?? "?"} ` + (remoteRev !== null && localRev !== remoteRev ? color.yellow("(desatualizado \u2014 rode `origem apply`)") : color.green("(em dia)"))
|
|
954
|
+
);
|
|
955
|
+
const { items } = buildPlan(brain, scope, targets, lock);
|
|
956
|
+
info(color.bold("\nArquivos:"));
|
|
957
|
+
for (const it of items) {
|
|
958
|
+
const parts = [];
|
|
959
|
+
if (it.action !== "unchanged") parts.push(color.yellow(it.action));
|
|
960
|
+
if (it.driftedLocally) parts.push(color.yellow("editado localmente"));
|
|
961
|
+
if (parts.length === 0) parts.push(color.green("em dia"));
|
|
962
|
+
info(` ${it.output.path} \u2014 ${parts.join(", ")}`);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
function runRemove(scope) {
|
|
966
|
+
const baseDir = process.cwd();
|
|
967
|
+
const lock = readLock(baseDir);
|
|
968
|
+
if (!lock) {
|
|
969
|
+
info("Nada para remover (sem .origem/lock.json).");
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
for (const f of lock.files) {
|
|
973
|
+
const abs = resolveTarget(scope, f.path);
|
|
974
|
+
if (abs) removeTarget(abs, f.strategy, f.jsonKeys);
|
|
975
|
+
}
|
|
976
|
+
deleteLock(baseDir);
|
|
977
|
+
ok(`Removidos ${lock.files.length} arquivo(s) do Origem e limpo o lock.`);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// src/hook.ts
|
|
981
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync4 } from "node:fs";
|
|
982
|
+
import { join as join4, dirname as dirname4 } from "node:path";
|
|
983
|
+
import { tmpdir } from "node:os";
|
|
984
|
+
var STATE_DIR = join4(tmpdir(), "origem-hook");
|
|
985
|
+
function stateFile(sessionId) {
|
|
986
|
+
return join4(STATE_DIR, `${sessionId.replace(/[^\w.-]/g, "_")}.json`);
|
|
987
|
+
}
|
|
988
|
+
async function readStdin() {
|
|
989
|
+
const chunks = [];
|
|
990
|
+
for await (const c2 of process.stdin) chunks.push(Buffer.from(c2));
|
|
991
|
+
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
992
|
+
if (!raw) return {};
|
|
993
|
+
try {
|
|
994
|
+
return JSON.parse(raw);
|
|
995
|
+
} catch {
|
|
996
|
+
return {};
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
async function hookStart() {
|
|
1000
|
+
const input = await readStdin();
|
|
1001
|
+
if (!input.session_id) return;
|
|
1002
|
+
try {
|
|
1003
|
+
mkdirSync4(STATE_DIR, { recursive: true });
|
|
1004
|
+
writeFileSync4(stateFile(input.session_id), JSON.stringify({ startedAt: Date.now() }));
|
|
1005
|
+
} catch {
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
function turnDuration(sessionId) {
|
|
1009
|
+
try {
|
|
1010
|
+
const raw = readFileSync4(stateFile(sessionId), "utf8");
|
|
1011
|
+
const { startedAt } = JSON.parse(raw);
|
|
1012
|
+
if (typeof startedAt !== "number") return void 0;
|
|
1013
|
+
const ms = Date.now() - startedAt;
|
|
1014
|
+
return ms >= 0 && ms < 864e5 ? ms : void 0;
|
|
1015
|
+
} catch {
|
|
1016
|
+
return void 0;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
async function hookStop(opts) {
|
|
1020
|
+
const input = await readStdin();
|
|
1021
|
+
const sessionId = input.session_id;
|
|
1022
|
+
if (!sessionId) return;
|
|
1023
|
+
const body = {
|
|
1024
|
+
sessionId,
|
|
1025
|
+
event: opts.finish ? "finish" : "turn",
|
|
1026
|
+
message: input.last_assistant_message ?? ""
|
|
1027
|
+
};
|
|
1028
|
+
if (input.stop_reason) body.stopReason = input.stop_reason;
|
|
1029
|
+
if (input.cwd) body.cwd = input.cwd;
|
|
1030
|
+
if (process.env.ORIGEM_TRIGGER) body.trigger = process.env.ORIGEM_TRIGGER;
|
|
1031
|
+
const durationMs = turnDuration(sessionId);
|
|
1032
|
+
if (durationMs !== void 0) body.durationMs = durationMs;
|
|
1033
|
+
let data;
|
|
1034
|
+
try {
|
|
1035
|
+
const res = await fetch(`${opts.apiUrl}/run?k=${encodeURIComponent(opts.key)}`, {
|
|
1036
|
+
method: "POST",
|
|
1037
|
+
headers: { "Content-Type": "application/json" },
|
|
1038
|
+
body: JSON.stringify(body),
|
|
1039
|
+
// Um turno não pode ficar refém do Origem: se demorar, desiste e deixa parar.
|
|
1040
|
+
signal: AbortSignal.timeout(8e3)
|
|
1041
|
+
});
|
|
1042
|
+
data = await res.json();
|
|
1043
|
+
if (!res.ok) {
|
|
1044
|
+
if (!opts.quiet) process.stderr.write(`origem: ${data.error ?? `HTTP ${res.status}`}
|
|
1045
|
+
`);
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
} catch (e) {
|
|
1049
|
+
if (!opts.quiet) process.stderr.write(`origem: ${e.message}
|
|
1050
|
+
`);
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
const command = data.command;
|
|
1054
|
+
if (!command) return;
|
|
1055
|
+
const output = {
|
|
1056
|
+
hookSpecificOutput: {
|
|
1057
|
+
hookEventName: "Stop",
|
|
1058
|
+
continue: true,
|
|
1059
|
+
continueReason: command.prompt
|
|
1060
|
+
},
|
|
1061
|
+
// Texto que entra no contexto do Claude — em inglês, como todo prompt.
|
|
1062
|
+
systemMessage: `Origem \xB7 command from the site${command.specTitle ? ` \xB7 spec "${command.specTitle}"` : ""}${command.stage ? ` \xB7 stage ${command.stage}` : ""}`
|
|
1063
|
+
};
|
|
1064
|
+
process.stdout.write(JSON.stringify(output));
|
|
1065
|
+
}
|
|
1066
|
+
function shellQuote(v) {
|
|
1067
|
+
return `'${v.replace(/'/g, `'\\''`)}'`;
|
|
1068
|
+
}
|
|
1069
|
+
var HOOK_ENTRIES = (bin, args) => ({
|
|
1070
|
+
hooks: {
|
|
1071
|
+
UserPromptSubmit: [
|
|
1072
|
+
{ hooks: [{ type: "command", command: `${bin} hook start`, timeout: 5 }] }
|
|
1073
|
+
],
|
|
1074
|
+
Stop: [
|
|
1075
|
+
{
|
|
1076
|
+
hooks: [
|
|
1077
|
+
{
|
|
1078
|
+
type: "command",
|
|
1079
|
+
command: `${bin} hook stop${args}`,
|
|
1080
|
+
timeout: 15,
|
|
1081
|
+
statusMessage: "Reportando ao Origem\u2026"
|
|
1082
|
+
}
|
|
1083
|
+
]
|
|
1084
|
+
}
|
|
1085
|
+
],
|
|
1086
|
+
// Sem isto o run fica "running" pra sempre: nada avisaria que a sessão
|
|
1087
|
+
// acabou. Se ela for retomada, o próximo turno reabre o run sozinho.
|
|
1088
|
+
SessionEnd: [
|
|
1089
|
+
{
|
|
1090
|
+
hooks: [{ type: "command", command: `${bin} hook stop --finish --quiet${args}`, timeout: 10 }]
|
|
1091
|
+
}
|
|
1092
|
+
]
|
|
1093
|
+
}
|
|
1094
|
+
});
|
|
1095
|
+
async function hookInstall(opts) {
|
|
1096
|
+
const file = opts.local ? "settings.local.json" : "settings.json";
|
|
1097
|
+
const target = join4(process.cwd(), ".claude", file);
|
|
1098
|
+
const args = (opts.key ? ` --key ${shellQuote(opts.key)}` : "") + (opts.apiUrl ? ` --api-url ${shellQuote(opts.apiUrl)}` : "");
|
|
1099
|
+
const snippet = HOOK_ENTRIES(opts.bin, args);
|
|
1100
|
+
info(`${color.bold("Vai escrever em:")} ${target}`);
|
|
1101
|
+
info(JSON.stringify(snippet, null, 2));
|
|
1102
|
+
info(
|
|
1103
|
+
color.yellow(
|
|
1104
|
+
"\nIsto \xE9 config privilegiada: os hooks rodam um comando na sua m\xE1quina a cada\nturno, e o `stop` pode CONTINUAR a sess\xE3o com um prompt vindo do site.\nQuem escrever na fila do Brain dirige esta sess\xE3o."
|
|
1105
|
+
)
|
|
1106
|
+
);
|
|
1107
|
+
if (opts.key && !opts.local) {
|
|
1108
|
+
info(
|
|
1109
|
+
color.yellow(
|
|
1110
|
+
"O token vai ficar GRAVADO neste arquivo. Se ele for versionado, use --local\n(.claude/settings.local.json, ignorado pelo git) ou exporte ORIGEM_TOKEN."
|
|
1111
|
+
)
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
if (!opts.yes && !await confirm("Instalar os hooks?")) {
|
|
1115
|
+
warn("Nada foi escrito.");
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
let existing = {};
|
|
1119
|
+
if (existsSync4(target)) {
|
|
1120
|
+
try {
|
|
1121
|
+
existing = JSON.parse(readFileSync4(target, "utf8"));
|
|
1122
|
+
} catch {
|
|
1123
|
+
throw new Error(`${target} existe mas n\xE3o \xE9 JSON v\xE1lido \u2014 corrija antes de instalar.`);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
const merged = mergeHooks(existing, snippet.hooks);
|
|
1127
|
+
mkdirSync4(dirname4(target), { recursive: true });
|
|
1128
|
+
writeFileSync4(target, `${JSON.stringify(merged, null, 2)}
|
|
1129
|
+
`);
|
|
1130
|
+
ok(`Hooks instalados em ${target}`);
|
|
1131
|
+
if (opts.key || opts.apiUrl) {
|
|
1132
|
+
const p = saveConfig({
|
|
1133
|
+
...opts.key ? { token: opts.key } : {},
|
|
1134
|
+
...opts.apiUrl ? { apiUrl: opts.apiUrl } : {}
|
|
1135
|
+
});
|
|
1136
|
+
info(color.dim(`Credenciais tamb\xE9m gravadas em ${p} \u2014 \`origem watch\` j\xE1 roda sem flag.`));
|
|
1137
|
+
}
|
|
1138
|
+
if (!opts.key) {
|
|
1139
|
+
info(
|
|
1140
|
+
color.dim(
|
|
1141
|
+
"Sem --key: o hook usa ORIGEM_TOKEN do ambiente do processo do Claude Code.\nNuma sess\xE3o j\xE1 aberta isso n\xE3o pega \u2014 reinicie o Claude com a vari\xE1vel exportada,\nou reinstale com --key."
|
|
1142
|
+
)
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
function hookIdentity(command) {
|
|
1147
|
+
if (!command) return null;
|
|
1148
|
+
const stripped = command.replace(/\s+--key\s+(?:'(?:[^']|'\\'')*'|"[^"]*"|\S+)/g, "").replace(/\s+--api-url\s+(?:'(?:[^']|'\\'')*'|"[^"]*"|\S+)/g, "").trim();
|
|
1149
|
+
return /\bhook\s+(start|stop)\b/.test(stripped) ? stripped : null;
|
|
1150
|
+
}
|
|
1151
|
+
function mergeHooks(settings, add) {
|
|
1152
|
+
const current = settings.hooks ?? {};
|
|
1153
|
+
const next = { ...current };
|
|
1154
|
+
for (const [event, entries] of Object.entries(add)) {
|
|
1155
|
+
const existing = Array.isArray(next[event]) ? [...next[event]] : [];
|
|
1156
|
+
const incoming = new Set(
|
|
1157
|
+
entries.flatMap((m) => (m.hooks ?? []).map((h) => hookIdentity(h.command))).filter(Boolean)
|
|
1158
|
+
);
|
|
1159
|
+
const kept = existing.filter(
|
|
1160
|
+
(m) => !(m.hooks ?? []).some((h) => incoming.has(hookIdentity(h.command)))
|
|
1161
|
+
);
|
|
1162
|
+
next[event] = [...kept, ...entries];
|
|
1163
|
+
}
|
|
1164
|
+
return { ...settings, hooks: next };
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// src/watch.ts
|
|
1168
|
+
import { spawn } from "node:child_process";
|
|
1169
|
+
import { accessSync, constants, existsSync as existsSync5, readdirSync } from "node:fs";
|
|
1170
|
+
import { hostname, homedir as homedir3 } from "node:os";
|
|
1171
|
+
import { join as join5 } from "node:path";
|
|
1172
|
+
var WATCHER_ID = `watch:${hostname()}`;
|
|
1173
|
+
function isExecutable(p) {
|
|
1174
|
+
try {
|
|
1175
|
+
accessSync(p, constants.X_OK);
|
|
1176
|
+
return true;
|
|
1177
|
+
} catch {
|
|
1178
|
+
return false;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
function versionOf(dir) {
|
|
1182
|
+
const m = dir.match(/(\d+(?:\.\d+)*)/);
|
|
1183
|
+
return m ? m[1].split(".").map(Number) : [];
|
|
1184
|
+
}
|
|
1185
|
+
function newerFirst(a, b) {
|
|
1186
|
+
const va = versionOf(a);
|
|
1187
|
+
const vb = versionOf(b);
|
|
1188
|
+
for (let i = 0; i < Math.max(va.length, vb.length); i++) {
|
|
1189
|
+
const d = (vb[i] ?? 0) - (va[i] ?? 0);
|
|
1190
|
+
if (d !== 0) return d;
|
|
1191
|
+
}
|
|
1192
|
+
return 0;
|
|
1193
|
+
}
|
|
1194
|
+
function candidates() {
|
|
1195
|
+
const home = homedir3();
|
|
1196
|
+
const out = [];
|
|
1197
|
+
for (const dir of (process.env.PATH ?? "").split(":")) {
|
|
1198
|
+
if (dir) out.push(join5(dir, "claude"));
|
|
1199
|
+
}
|
|
1200
|
+
out.push(join5(home, ".claude", "local", "claude"));
|
|
1201
|
+
const extRoots = [
|
|
1202
|
+
join5(home, ".vscode", "extensions"),
|
|
1203
|
+
join5(home, ".vscode-insiders", "extensions"),
|
|
1204
|
+
join5(home, ".cursor", "extensions"),
|
|
1205
|
+
join5(home, ".windsurf", "extensions")
|
|
1206
|
+
];
|
|
1207
|
+
for (const root of extRoots) {
|
|
1208
|
+
if (!existsSync5(root)) continue;
|
|
1209
|
+
let dirs;
|
|
1210
|
+
try {
|
|
1211
|
+
dirs = readdirSync(root).filter((d) => d.startsWith("anthropic.claude-code-"));
|
|
1212
|
+
} catch {
|
|
1213
|
+
continue;
|
|
1214
|
+
}
|
|
1215
|
+
for (const d of dirs.sort(newerFirst)) {
|
|
1216
|
+
out.push(join5(root, d, "resources", "native-binary", "claude"));
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
out.push("/opt/homebrew/bin/claude", "/usr/local/bin/claude");
|
|
1220
|
+
return out;
|
|
1221
|
+
}
|
|
1222
|
+
function resolveClaudeBin(flag) {
|
|
1223
|
+
if (flag) return flag;
|
|
1224
|
+
const cached = readConfig().claudeBin;
|
|
1225
|
+
if (cached && isExecutable(cached)) return cached;
|
|
1226
|
+
const found = candidates().find(isExecutable);
|
|
1227
|
+
if (!found) {
|
|
1228
|
+
throw new Error(
|
|
1229
|
+
"N\xE3o achei o bin\xE1rio do Claude Code. Passe --claude <caminho> (no VS Code fica em ~/.vscode/extensions/anthropic.claude-code-*/resources/native-binary/claude)."
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
saveConfig({ claudeBin: found });
|
|
1233
|
+
return found;
|
|
1234
|
+
}
|
|
1235
|
+
function sleep(ms) {
|
|
1236
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
1237
|
+
}
|
|
1238
|
+
async function poll(opts) {
|
|
1239
|
+
const res = await fetch(`${opts.apiUrl}/run?k=${encodeURIComponent(opts.key)}`, {
|
|
1240
|
+
method: "POST",
|
|
1241
|
+
headers: { "Content-Type": "application/json" },
|
|
1242
|
+
body: JSON.stringify({ sessionId: WATCHER_ID, event: "poll" }),
|
|
1243
|
+
signal: AbortSignal.timeout(15e3)
|
|
1244
|
+
});
|
|
1245
|
+
const data = await res.json();
|
|
1246
|
+
if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
|
|
1247
|
+
return data.command ?? null;
|
|
1248
|
+
}
|
|
1249
|
+
function runClaude(bin, opts, prompt, resumeId) {
|
|
1250
|
+
const args = resumeId ? ["-r", resumeId, "-p", prompt] : ["-p", prompt];
|
|
1251
|
+
if (opts.permissionMode) args.push("--permission-mode", opts.permissionMode);
|
|
1252
|
+
return new Promise((resolve) => {
|
|
1253
|
+
const child = spawn(bin, args, {
|
|
1254
|
+
cwd: opts.cwd,
|
|
1255
|
+
stdio: "inherit",
|
|
1256
|
+
// O hook da sessão filha lê isto e reporta a origem do run. Sem isso todo
|
|
1257
|
+
// run apareceria como "Claude Code", sem distinguir o que veio do plantão.
|
|
1258
|
+
env: { ...process.env, ORIGEM_TRIGGER: "origem watch" }
|
|
1259
|
+
});
|
|
1260
|
+
child.on("error", (e) => {
|
|
1261
|
+
warn(e.message);
|
|
1262
|
+
resolve(1);
|
|
1263
|
+
});
|
|
1264
|
+
child.on("close", (code) => resolve(code ?? 0));
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
async function runWatch(opts) {
|
|
1268
|
+
const bin = resolveClaudeBin(opts.claudeBin);
|
|
1269
|
+
info(`${color.bold("origem watch")} \u2014 plant\xE3o local`);
|
|
1270
|
+
info(` fila: ${opts.apiUrl}/run`);
|
|
1271
|
+
info(` diret\xF3rio: ${opts.cwd}`);
|
|
1272
|
+
info(` claude: ${bin}`);
|
|
1273
|
+
info(opts.once ? " modo: uma passada" : ` intervalo: ${Math.round(opts.intervalMs / 1e3)}s`);
|
|
1274
|
+
info(
|
|
1275
|
+
color.yellow(
|
|
1276
|
+
"\nEnquanto isto roda, quem escrever na fila do Brain FAZ o Claude Code rodar\nnesta m\xE1quina, neste diret\xF3rio. O que trafega \xE9 prompt, e as permiss\xF5es do\nprojeto continuam valendo \u2014 mas o gatilho passa a ser remoto."
|
|
1277
|
+
)
|
|
1278
|
+
);
|
|
1279
|
+
if (opts.permissionMode === "bypassPermissions") {
|
|
1280
|
+
warn("--permission-mode bypassPermissions desliga TODA checagem. N\xE3o use com fila remota.");
|
|
1281
|
+
}
|
|
1282
|
+
if (!opts.dryRun && !opts.once && !opts.yes && !await confirm("\nIniciar o plant\xE3o?")) {
|
|
1283
|
+
warn("Cancelado.");
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
let stop = false;
|
|
1287
|
+
process.on("SIGINT", () => {
|
|
1288
|
+
stop = true;
|
|
1289
|
+
info("\nEncerrando o plant\xE3o\u2026");
|
|
1290
|
+
});
|
|
1291
|
+
let quiet = 0;
|
|
1292
|
+
while (!stop) {
|
|
1293
|
+
let command = null;
|
|
1294
|
+
try {
|
|
1295
|
+
command = await poll(opts);
|
|
1296
|
+
quiet = 0;
|
|
1297
|
+
} catch (e) {
|
|
1298
|
+
quiet++;
|
|
1299
|
+
if (opts.once) throw e;
|
|
1300
|
+
if (quiet <= 3) warn(`${e.message} \u2014 tentando de novo`);
|
|
1301
|
+
await sleep(Math.min(opts.intervalMs * quiet, 6e4));
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1304
|
+
if (!command) {
|
|
1305
|
+
if (opts.once) {
|
|
1306
|
+
info("Fila vazia.");
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
await sleep(opts.intervalMs);
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
const alvo = command.targetSessionId ? `retomando a sess\xE3o ${command.targetSessionId}` : "sess\xE3o nova";
|
|
1313
|
+
ok(`Comando recebido (${alvo})`);
|
|
1314
|
+
info(` ${color.dim(command.prompt)}`);
|
|
1315
|
+
if (opts.dryRun) {
|
|
1316
|
+
info(color.dim(" --dry-run: n\xE3o executei."));
|
|
1317
|
+
if (opts.once) return;
|
|
1318
|
+
await sleep(opts.intervalMs);
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
const code = await runClaude(bin, opts, command.prompt, command.targetSessionId);
|
|
1322
|
+
if (code === 0) ok("Comando conclu\xEDdo.");
|
|
1323
|
+
else warn(`Claude Code saiu com c\xF3digo ${code}.`);
|
|
1324
|
+
if (opts.once) return;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// src/auth.ts
|
|
1329
|
+
import { rmSync as rmSync2, existsSync as existsSync6 } from "node:fs";
|
|
1330
|
+
async function runLogin(opts) {
|
|
1331
|
+
const key = opts.key || process.env.ORIGEM_TOKEN;
|
|
1332
|
+
if (!key) {
|
|
1333
|
+
throw new Error("Passe --key <token>. Gere o link de escrita em 'Conectar com IA' no Origem.");
|
|
1334
|
+
}
|
|
1335
|
+
info(`Verificando o token em ${color.cyan(opts.apiUrl)}\u2026`);
|
|
1336
|
+
await probeToken(opts.apiUrl, key);
|
|
1337
|
+
const p = saveConfig({ token: key, apiUrl: opts.apiUrl });
|
|
1338
|
+
ok(`Token v\xE1lido, com escopo de escrita. Gravado em ${p} (0600).`);
|
|
1339
|
+
info(color.dim("Agora `origem watch` roda sem flag nenhuma."));
|
|
1340
|
+
}
|
|
1341
|
+
function runLogout() {
|
|
1342
|
+
const p = configPath();
|
|
1343
|
+
if (!existsSync6(p)) {
|
|
1344
|
+
warn("Nada gravado.");
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
const { claudeBin } = readConfig();
|
|
1348
|
+
rmSync2(p);
|
|
1349
|
+
if (claudeBin) saveConfig({ claudeBin });
|
|
1350
|
+
ok(`Credenciais removidas de ${p}.`);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// src/connect.ts
|
|
1354
|
+
import * as crypto from "node:crypto";
|
|
1355
|
+
import { createServer } from "node:http";
|
|
1356
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
1357
|
+
import { hostname as hostname2 } from "node:os";
|
|
1358
|
+
import { basename } from "node:path";
|
|
1359
|
+
|
|
1360
|
+
// src/skill.ts
|
|
1361
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
1362
|
+
import { dirname as dirname5, join as join6 } from "node:path";
|
|
1363
|
+
var SKILL_PATH = join6(".claude", "skills", "origem", "SKILL.md");
|
|
1364
|
+
var SKILL_MD = `---
|
|
1365
|
+
name: origem
|
|
1366
|
+
description: Sincroniza e inspeciona o harness deste reposit\xF3rio, que \xE9 gerenciado pelo Origem. Use quando o usu\xE1rio pedir para atualizar/sincronizar/puxar o harness ou a config da IA, perguntar de onde v\xEAm CLAUDE.md ou .claude/*, quiser saber se a config local divergiu do Origem, ou pedir para mudar uma regra, skill, subagente ou permiss\xE3o de forma permanente.
|
|
1367
|
+
---
|
|
1368
|
+
|
|
1369
|
+
# Harness gerenciado pelo Origem
|
|
1370
|
+
|
|
1371
|
+
Os arquivos de configura\xE7\xE3o deste reposit\xF3rio \u2014 \`CLAUDE.md\`, \`.claude/settings.json\`,
|
|
1372
|
+
\`.claude/skills/\`, \`.claude/agents/\`, \`.claude/commands/\` e \`.mcp.json\` \u2014 n\xE3o foram
|
|
1373
|
+
escritos \xE0 m\xE3o aqui. Eles s\xE3o materializados a partir de um harness guardado no Origem,
|
|
1374
|
+
e o invent\xE1rio do que foi escrito est\xE1 em \`.origem/lock.json\`.
|
|
1375
|
+
|
|
1376
|
+
## O que isso muda no seu comportamento
|
|
1377
|
+
|
|
1378
|
+
**Mudan\xE7a permanente vai para o Origem, n\xE3o para o arquivo.** Se o usu\xE1rio pedir uma regra
|
|
1379
|
+
nova, uma skill nova, uma permiss\xE3o diferente ou qualquer coisa que deva valer nas pr\xF3ximas
|
|
1380
|
+
sess\xF5es, editar o arquivo local funciona at\xE9 o pr\xF3ximo \`origem connect\` \u2014 e some ali.
|
|
1381
|
+
Diga isso ao usu\xE1rio e ofere\xE7a registrar a mudan\xE7a no Origem.
|
|
1382
|
+
|
|
1383
|
+
**Um arquivo listado no lock \xE9 territ\xF3rio do Origem.** Antes de editar \`CLAUDE.md\` ou
|
|
1384
|
+
qualquer coisa sob \`.claude/\`, confira \`.origem/lock.json\`. Arquivos fora dessa lista s\xE3o
|
|
1385
|
+
do projeto e voc\xEA edita normalmente.
|
|
1386
|
+
|
|
1387
|
+
## Comandos
|
|
1388
|
+
|
|
1389
|
+
| Comando | O que faz |
|
|
1390
|
+
|---|---|
|
|
1391
|
+
| \`npx origem status\` | Mostra o que mudaria e se h\xE1 edi\xE7\xE3o local divergente. \xC9 a leitura segura \u2014 n\xE3o escreve nada. |
|
|
1392
|
+
| \`npx origem connect\` | Autoriza (navegador), puxa a vers\xE3o mais nova e reescreve os arquivos. \xC9 tamb\xE9m o comando de sincronizar. |
|
|
1393
|
+
| \`npx origem apply\` | Reaplica com a credencial j\xE1 gravada, sem passar pelo navegador. |
|
|
1394
|
+
| \`npx origem apply --dry-run\` | S\xF3 imprime o plano. |
|
|
1395
|
+
| \`npx origem remove\` | Reverte tudo que o Origem escreveu, usando o lock. |
|
|
1396
|
+
| \`npx origem propose\` | Sugere uma mudan\xE7a no harness. **N\xE3o escreve nada** \u2014 fica pendente at\xE9 a pessoa aceitar no site. |
|
|
1397
|
+
| \`npx origem spec\` | Mostra os specs abertos e em que est\xE1gio cada um est\xE1. |
|
|
1398
|
+
| \`npx origem doctor\` | Compara o harness com este reposit\xF3rio e aponta o que divergiu. |
|
|
1399
|
+
| \`npx origem bootstrap\` | L\xEA o reposit\xF3rio e prop\xF5e um harness inteiro (para projeto sem harness). |
|
|
1400
|
+
|
|
1401
|
+
## No come\xE7o de uma tarefa
|
|
1402
|
+
|
|
1403
|
+
Rode \`npx origem spec\`. Se houver spec aberto relacionado ao que o usu\xE1rio pediu, use os
|
|
1404
|
+
crit\xE9rios de aceite dele como defini\xE7\xE3o de pronto, e diga em que est\xE1gio o trabalho est\xE1.
|
|
1405
|
+
Se n\xE3o houver, siga normalmente \u2014 trabalho avulso \xE9 leg\xEDtimo.
|
|
1406
|
+
|
|
1407
|
+
## Quando o comando que voc\xEA rodou falhar por n\xE3o existir
|
|
1408
|
+
|
|
1409
|
+
Rode \`npx origem doctor\`. Ele compara o que o harness afirma com o que o reposit\xF3rio
|
|
1410
|
+
declara e costuma encontrar a causa (script renomeado, gerenciador de pacotes errado).
|
|
1411
|
+
Corrigir o harness evita que o pr\xF3ximo turno tropece no mesmo lugar.
|
|
1412
|
+
|
|
1413
|
+
## Quando propor uma mudan\xE7a
|
|
1414
|
+
|
|
1415
|
+
Se durante a sess\xE3o o usu\xE1rio corrigir voc\xEA sobre a mesma coisa mais de uma vez, ou estabelecer
|
|
1416
|
+
uma conven\xE7\xE3o que deve valer nas pr\xF3ximas sess\xF5es, proponha:
|
|
1417
|
+
|
|
1418
|
+
\`\`\`
|
|
1419
|
+
npx origem propose \\
|
|
1420
|
+
--type rule \\
|
|
1421
|
+
--title "Terraform: apply \xE9 sempre manual" \\
|
|
1422
|
+
--why "O usu\xE1rio me corrigiu 3x sobre isto nesta sess\xE3o." \\
|
|
1423
|
+
--content "Nunca rode \`terraform apply\` sem mostrar o plan antes."
|
|
1424
|
+
\`\`\`
|
|
1425
|
+
|
|
1426
|
+
Compartimentos v\xE1lidos: \`instruction\`, \`rule\`, \`boundary\`, \`memory\`, \`knowledge\`,
|
|
1427
|
+
\`skill\`, \`agent\`, \`prompt\`, \`harness\`, \`mcp\`, \`integration\`.
|
|
1428
|
+
|
|
1429
|
+
Regras para propor bem:
|
|
1430
|
+
- **\`--why\` \xE9 o que a pessoa l\xEA para decidir.** Cite o que aconteceu na sess\xE3o, n\xE3o uma
|
|
1431
|
+
justificativa gen\xE9rica.
|
|
1432
|
+
- Proponha **uma coisa por vez**. Duas regras juntas viram uma decis\xE3o dif\xEDcil.
|
|
1433
|
+
- N\xE3o proponha o que j\xE1 est\xE1 no harness \u2014 leia antes.
|
|
1434
|
+
- Para alterar uma pe\xE7a existente, passe \`--card <id>\` em vez de criar uma nova.
|
|
1435
|
+
|
|
1436
|
+
## Quando o usu\xE1rio pedir para sincronizar
|
|
1437
|
+
|
|
1438
|
+
Rode \`npx origem status\` primeiro e mostre o resultado. Se houver edi\xE7\xE3o local dentro de um
|
|
1439
|
+
bloco gerenciado, avise **antes** de aplicar \u2014 o apply sobrescreve esse bloco.
|
|
1440
|
+
|
|
1441
|
+
## Limites
|
|
1442
|
+
|
|
1443
|
+
N\xE3o edite \`.origem/lock.json\` na m\xE3o: ele \xE9 o invent\xE1rio que o \`origem remove\` usa para
|
|
1444
|
+
saber o que reverter, e um lock inconsistente deixa arquivos \xF3rf\xE3os no projeto.
|
|
1445
|
+
`;
|
|
1446
|
+
function installOrigemSkill(baseDir, lock) {
|
|
1447
|
+
const fromHarness = (lock?.files ?? []).some(
|
|
1448
|
+
(f) => f.path.replace(/\\/g, "/") === SKILL_PATH.replace(/\\/g, "/")
|
|
1449
|
+
);
|
|
1450
|
+
if (fromHarness) {
|
|
1451
|
+
info(color.dim(`skill do Origem: veio do pr\xF3prio harness, mantida como est\xE1`));
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
const abs = join6(baseDir, SKILL_PATH);
|
|
1455
|
+
if (existsSync7(abs) && readFileSync5(abs, "utf8") === SKILL_MD) {
|
|
1456
|
+
info(color.dim(`skill do Origem j\xE1 atualizada em ${SKILL_PATH}`));
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
mkdirSync5(dirname5(abs), { recursive: true });
|
|
1460
|
+
writeFileSync5(abs, SKILL_MD, "utf8");
|
|
1461
|
+
ok(`skill do Origem instalada em ${SKILL_PATH}`);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
// src/connect.ts
|
|
1465
|
+
var CALLBACK_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
1466
|
+
function base64url(buf) {
|
|
1467
|
+
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1468
|
+
}
|
|
1469
|
+
function newPkce() {
|
|
1470
|
+
const verifier = base64url(crypto.randomBytes(32));
|
|
1471
|
+
const challenge = base64url(crypto.createHash("sha256").update(verifier).digest());
|
|
1472
|
+
return { verifier, challenge };
|
|
1473
|
+
}
|
|
1474
|
+
async function resolveEndpoints(apiUrl) {
|
|
1475
|
+
const base = apiUrl.replace(/\/$/, "");
|
|
1476
|
+
const pretty = {
|
|
1477
|
+
register: `${base}/oauth/register`,
|
|
1478
|
+
authorize: `${base}/oauth/authorize`,
|
|
1479
|
+
token: `${base}/oauth/token`
|
|
1480
|
+
};
|
|
1481
|
+
const direct = {
|
|
1482
|
+
register: `${base}/oauthRegister`,
|
|
1483
|
+
authorize: `${base}/oauthAuthorize`,
|
|
1484
|
+
token: `${base}/oauthToken`
|
|
1485
|
+
};
|
|
1486
|
+
for (const candidate of [pretty, direct]) {
|
|
1487
|
+
try {
|
|
1488
|
+
const res = await fetch(candidate.register, {
|
|
1489
|
+
method: "OPTIONS",
|
|
1490
|
+
signal: AbortSignal.timeout(1e4)
|
|
1491
|
+
});
|
|
1492
|
+
if (res.status !== 404) return candidate;
|
|
1493
|
+
} catch {
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
throw new Error(
|
|
1497
|
+
`N\xE3o encontrei os endpoints OAuth em ${base}. Confira a URL da API (--api-url ou ORIGEM_API_URL) e se ela est\xE1 publicada com o m\xF3dulo de OAuth.`
|
|
1498
|
+
);
|
|
1499
|
+
}
|
|
1500
|
+
function page(title, body, accent) {
|
|
1501
|
+
return `<!doctype html><meta charset="utf-8"><title>Origem \u2014 ${title}</title><div style="font-family:system-ui,-apple-system,sans-serif;max-width:30rem;margin:5rem auto;padding:0 1.5rem;line-height:1.6"><div style="width:2.25rem;height:2.25rem;border-radius:.6rem;background:${accent};margin-bottom:1.25rem"></div><h1 style="font-size:1.2rem;margin:0 0 .5rem">${title}</h1><p style="color:#666;margin:0">${body}</p></div>`;
|
|
1502
|
+
}
|
|
1503
|
+
async function startCallbackServer(state) {
|
|
1504
|
+
let settle = null;
|
|
1505
|
+
let fail = null;
|
|
1506
|
+
const waitForCode = new Promise((resolve, reject) => {
|
|
1507
|
+
settle = resolve;
|
|
1508
|
+
fail = reject;
|
|
1509
|
+
});
|
|
1510
|
+
const server = createServer((req, res) => {
|
|
1511
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
1512
|
+
if (url.pathname !== "/callback") {
|
|
1513
|
+
res.writeHead(404).end();
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
const error = url.searchParams.get("error");
|
|
1517
|
+
const code = url.searchParams.get("code");
|
|
1518
|
+
const gotState = url.searchParams.get("state");
|
|
1519
|
+
if (error) {
|
|
1520
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1521
|
+
res.end(page("Autoriza\xE7\xE3o negada", "Pode fechar esta aba e voltar ao terminal.", "#b3392e"));
|
|
1522
|
+
fail?.(new Error(`Autoriza\xE7\xE3o negada (${error}).`));
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
if (!code || gotState !== state) {
|
|
1526
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
1527
|
+
res.end(page("Retorno inv\xE1lido", "Recomece com `origem connect`.", "#b3392e"));
|
|
1528
|
+
fail?.(new Error("Retorno do navegador inv\xE1lido (state n\xE3o confere)."));
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1532
|
+
res.end(page("Conectado", "Pode fechar esta aba \u2014 o resto acontece no terminal.", "#2540d8"));
|
|
1533
|
+
settle?.({ code });
|
|
1534
|
+
});
|
|
1535
|
+
await new Promise((resolve, reject) => {
|
|
1536
|
+
server.once("error", reject);
|
|
1537
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
1538
|
+
});
|
|
1539
|
+
const timer = setTimeout(() => {
|
|
1540
|
+
fail?.(new Error("Tempo esgotado esperando a autoriza\xE7\xE3o no navegador (5 min)."));
|
|
1541
|
+
}, CALLBACK_TIMEOUT_MS);
|
|
1542
|
+
timer.unref?.();
|
|
1543
|
+
return {
|
|
1544
|
+
port: server.address().port,
|
|
1545
|
+
waitForCode,
|
|
1546
|
+
close: () => {
|
|
1547
|
+
clearTimeout(timer);
|
|
1548
|
+
server.close();
|
|
1549
|
+
}
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
function openBrowser(url) {
|
|
1553
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
1554
|
+
try {
|
|
1555
|
+
const child = spawn2(cmd, [url], {
|
|
1556
|
+
stdio: "ignore",
|
|
1557
|
+
detached: true,
|
|
1558
|
+
shell: process.platform === "win32"
|
|
1559
|
+
});
|
|
1560
|
+
child.on("error", () => void 0);
|
|
1561
|
+
child.unref();
|
|
1562
|
+
} catch {
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
async function registerClient(endpoint, redirectUri) {
|
|
1566
|
+
const res = await fetch(endpoint, {
|
|
1567
|
+
method: "POST",
|
|
1568
|
+
headers: { "Content-Type": "application/json" },
|
|
1569
|
+
body: JSON.stringify({
|
|
1570
|
+
client_name: `origem CLI \u2014 ${basename(process.cwd())} (${hostname2()})`,
|
|
1571
|
+
redirect_uris: [redirectUri],
|
|
1572
|
+
grant_types: ["authorization_code"],
|
|
1573
|
+
response_types: ["code"],
|
|
1574
|
+
token_endpoint_auth_method: "none"
|
|
1575
|
+
}),
|
|
1576
|
+
signal: AbortSignal.timeout(15e3)
|
|
1577
|
+
});
|
|
1578
|
+
if (!res.ok) {
|
|
1579
|
+
throw new Error(`N\xE3o consegui registrar o cliente OAuth (HTTP ${res.status}).`);
|
|
1580
|
+
}
|
|
1581
|
+
const data = await res.json();
|
|
1582
|
+
if (!data.client_id) throw new Error("Registro OAuth devolveu resposta sem client_id.");
|
|
1583
|
+
return data;
|
|
1584
|
+
}
|
|
1585
|
+
async function exchangeCode(endpoint, input) {
|
|
1586
|
+
const res = await fetch(endpoint, {
|
|
1587
|
+
method: "POST",
|
|
1588
|
+
headers: { "Content-Type": "application/json" },
|
|
1589
|
+
body: JSON.stringify({
|
|
1590
|
+
grant_type: "authorization_code",
|
|
1591
|
+
code: input.code,
|
|
1592
|
+
code_verifier: input.verifier,
|
|
1593
|
+
client_id: input.clientId,
|
|
1594
|
+
redirect_uri: input.redirectUri
|
|
1595
|
+
}),
|
|
1596
|
+
signal: AbortSignal.timeout(15e3)
|
|
1597
|
+
});
|
|
1598
|
+
const data = await res.json().catch(() => ({}));
|
|
1599
|
+
if (!res.ok || !data.access_token) {
|
|
1600
|
+
throw new Error(
|
|
1601
|
+
`Troca do c\xF3digo falhou (HTTP ${res.status}): ${data.error_description || data.error || "resposta sem access_token"}`
|
|
1602
|
+
);
|
|
1603
|
+
}
|
|
1604
|
+
if (!(data.scope ?? "").split(/\s+/).includes("write")) {
|
|
1605
|
+
warn(
|
|
1606
|
+
"O token veio sem escopo de escrita \u2014 a sess\xE3o n\xE3o vai conseguir reportar turnos. Refa\xE7a o connect e confirme a permiss\xE3o na tela de autoriza\xE7\xE3o."
|
|
1607
|
+
);
|
|
1608
|
+
}
|
|
1609
|
+
return data.access_token;
|
|
1610
|
+
}
|
|
1611
|
+
async function runConnect(opts) {
|
|
1612
|
+
const apiUrl = opts.apiUrl.replace(/\/$/, "");
|
|
1613
|
+
const { verifier, challenge } = newPkce();
|
|
1614
|
+
const state = base64url(crypto.randomBytes(16));
|
|
1615
|
+
const endpoints = await resolveEndpoints(apiUrl);
|
|
1616
|
+
const server = await startCallbackServer(state);
|
|
1617
|
+
const redirectUri = `http://127.0.0.1:${server.port}/callback`;
|
|
1618
|
+
let token;
|
|
1619
|
+
try {
|
|
1620
|
+
const client = await registerClient(endpoints.register, redirectUri);
|
|
1621
|
+
const authorizeUrl = new URL(endpoints.authorize);
|
|
1622
|
+
authorizeUrl.searchParams.set("response_type", "code");
|
|
1623
|
+
authorizeUrl.searchParams.set("client_id", client.client_id);
|
|
1624
|
+
authorizeUrl.searchParams.set("redirect_uri", redirectUri);
|
|
1625
|
+
authorizeUrl.searchParams.set("code_challenge", challenge);
|
|
1626
|
+
authorizeUrl.searchParams.set("code_challenge_method", "S256");
|
|
1627
|
+
authorizeUrl.searchParams.set("state", state);
|
|
1628
|
+
authorizeUrl.searchParams.set("scope", "read write");
|
|
1629
|
+
info(`${color.cyan("\u2192")} abrindo o navegador para autorizar\u2026 ${color.dim(redirectUri)}`);
|
|
1630
|
+
info(color.dim(` se nada abrir, cole no navegador:
|
|
1631
|
+
${authorizeUrl.toString()}`));
|
|
1632
|
+
openBrowser(authorizeUrl.toString());
|
|
1633
|
+
const { code } = await server.waitForCode;
|
|
1634
|
+
token = await exchangeCode(endpoints.token, {
|
|
1635
|
+
code,
|
|
1636
|
+
verifier,
|
|
1637
|
+
clientId: client.client_id,
|
|
1638
|
+
redirectUri
|
|
1639
|
+
});
|
|
1640
|
+
} finally {
|
|
1641
|
+
server.close();
|
|
1642
|
+
}
|
|
1643
|
+
saveConfig({ token, apiUrl });
|
|
1644
|
+
ok("autorizado \u2014 credencial gravada em ~/.origem/config.json (0600)");
|
|
1645
|
+
const brain = await fetchBrain(apiUrl, token);
|
|
1646
|
+
brain.__apiUrl = apiUrl;
|
|
1647
|
+
info(
|
|
1648
|
+
`${color.cyan("\u2192")} harness ${color.bold(brain.brain?.name ?? "(sem nome)")} \u2014 ${brain.cards?.length ?? 0} pe\xE7as`
|
|
1649
|
+
);
|
|
1650
|
+
await runApply({
|
|
1651
|
+
brain,
|
|
1652
|
+
scope: opts.scope,
|
|
1653
|
+
targets: opts.targets,
|
|
1654
|
+
dryRun: false,
|
|
1655
|
+
yes: opts.yes
|
|
1656
|
+
});
|
|
1657
|
+
installOrigemSkill(process.cwd(), readLock(process.cwd()));
|
|
1658
|
+
if (opts.noHooks) {
|
|
1659
|
+
info(color.dim("hooks pulados (--no-hooks): a sess\xE3o n\xE3o vai reportar turnos ao Origem."));
|
|
1660
|
+
} else {
|
|
1661
|
+
info("");
|
|
1662
|
+
await hookInstall({
|
|
1663
|
+
yes: opts.yes,
|
|
1664
|
+
bin: opts.bin,
|
|
1665
|
+
key: token,
|
|
1666
|
+
apiUrl,
|
|
1667
|
+
local: true
|
|
1668
|
+
// .claude/settings.local.json é ignorado pelo git — o token não vaza
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
info("");
|
|
1672
|
+
ok(color.bold("ligado."));
|
|
1673
|
+
info(color.dim("rode `origem connect` de novo a qualquer momento para sincronizar."));
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
// src/propose.ts
|
|
1677
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
1678
|
+
var PROPOSABLE = [
|
|
1679
|
+
"instruction",
|
|
1680
|
+
"rule",
|
|
1681
|
+
"boundary",
|
|
1682
|
+
"memory",
|
|
1683
|
+
"knowledge",
|
|
1684
|
+
"skill",
|
|
1685
|
+
"agent",
|
|
1686
|
+
"prompt",
|
|
1687
|
+
"harness",
|
|
1688
|
+
"mcp",
|
|
1689
|
+
"integration"
|
|
1690
|
+
];
|
|
1691
|
+
async function runPropose(opts) {
|
|
1692
|
+
if (!PROPOSABLE.includes(opts.type)) {
|
|
1693
|
+
throw new Error(`--type inv\xE1lido: ${opts.type}. Use um de: ${PROPOSABLE.join(", ")}`);
|
|
1694
|
+
}
|
|
1695
|
+
if (!opts.rationale?.trim()) {
|
|
1696
|
+
throw new Error("--why \xE9 obrigat\xF3rio: diga por que est\xE1 propondo (\xE9 o que a pessoa l\xEA pra decidir).");
|
|
1697
|
+
}
|
|
1698
|
+
const content = opts.file ? readFileSync6(opts.file, "utf8") : opts.content ?? "";
|
|
1699
|
+
if (!content.trim()) {
|
|
1700
|
+
throw new Error("Conte\xFAdo vazio. Passe --content <texto> ou --file <caminho>.");
|
|
1701
|
+
}
|
|
1702
|
+
const base = opts.apiUrl.replace(/\/$/, "");
|
|
1703
|
+
const res = await fetch(`${base}/propose?k=${encodeURIComponent(opts.key)}`, {
|
|
1704
|
+
method: "POST",
|
|
1705
|
+
headers: { "Content-Type": "application/json" },
|
|
1706
|
+
body: JSON.stringify({
|
|
1707
|
+
cardId: opts.cardId ?? null,
|
|
1708
|
+
type: opts.type,
|
|
1709
|
+
title: opts.title,
|
|
1710
|
+
content,
|
|
1711
|
+
rationale: opts.rationale,
|
|
1712
|
+
sessionId: opts.sessionId ?? process.env.CLAUDE_SESSION_ID ?? null
|
|
1713
|
+
}),
|
|
1714
|
+
signal: AbortSignal.timeout(15e3)
|
|
1715
|
+
});
|
|
1716
|
+
const data = await res.json().catch(() => ({}));
|
|
1717
|
+
if (res.status === 403) throw new Error("Token sem escopo de escrita \u2014 refa\xE7a o `origem connect`.");
|
|
1718
|
+
if (res.status === 429) throw new Error(data.error ?? "Rate limit ou fila de propostas cheia.");
|
|
1719
|
+
if (!res.ok || !data.proposalId) {
|
|
1720
|
+
throw new Error(data.error ?? `Falha ao propor (HTTP ${res.status}).`);
|
|
1721
|
+
}
|
|
1722
|
+
ok(`proposta enviada: ${color.bold(opts.title)}`);
|
|
1723
|
+
info(color.dim("Ela fica pendente at\xE9 ser aceita no Origem \u2014 nada foi escrito no harness."));
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
// src/spec.ts
|
|
1727
|
+
var STAGES = ["requisito", "design", "implementacao", "verificacao"];
|
|
1728
|
+
async function runSpec(opts) {
|
|
1729
|
+
const base = opts.apiUrl.replace(/\/$/, "");
|
|
1730
|
+
const res = await fetch(`${base}/specs?k=${encodeURIComponent(opts.key)}`, {
|
|
1731
|
+
signal: AbortSignal.timeout(15e3)
|
|
1732
|
+
});
|
|
1733
|
+
const data = await res.json().catch(() => ({}));
|
|
1734
|
+
if (res.status === 401) throw new Error("Token inv\xE1lido \u2014 refa\xE7a o `origem connect`.");
|
|
1735
|
+
if (!res.ok) throw new Error(data.error ?? `Falha ao ler os specs (HTTP ${res.status}).`);
|
|
1736
|
+
const specs = data.specs ?? [];
|
|
1737
|
+
if (specs.length === 0) {
|
|
1738
|
+
ok("nenhum spec aberto \u2014 o trabalho desta sess\xE3o \xE9 avulso.");
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
for (const s of specs) {
|
|
1742
|
+
const at = STAGES.indexOf(s.stage);
|
|
1743
|
+
const trail = STAGES.map(
|
|
1744
|
+
(stage, i) => i < at ? `${color.green("\u2713")} ${stage}` : i === at ? color.bold(`\u25D0 ${stage}`) : color.dim(`\xB7 ${stage}`)
|
|
1745
|
+
).join(" ");
|
|
1746
|
+
info(`
|
|
1747
|
+
${color.bold(s.title)} ${color.dim(`(${s.id})`)}`);
|
|
1748
|
+
info(` ${trail}`);
|
|
1749
|
+
if (s.content.trim()) {
|
|
1750
|
+
info("");
|
|
1751
|
+
for (const line of s.content.split("\n")) info(` ${line}`);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
info(
|
|
1755
|
+
color.dim(
|
|
1756
|
+
`
|
|
1757
|
+
${specs.length} spec(s) aberto(s). Ao concluir um est\xE1gio, reporte com \`origem hook stop\` (o hook j\xE1 faz isso) \u2014 o board avan\xE7a sozinho.`
|
|
1758
|
+
)
|
|
1759
|
+
);
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
// src/repo.ts
|
|
1763
|
+
import { execFileSync } from "node:child_process";
|
|
1764
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7, readdirSync as readdirSync2, statSync } from "node:fs";
|
|
1765
|
+
import { basename as basename2, extname, join as join7, relative } from "node:path";
|
|
1766
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
1767
|
+
"node_modules",
|
|
1768
|
+
".git",
|
|
1769
|
+
"dist",
|
|
1770
|
+
"build",
|
|
1771
|
+
".next",
|
|
1772
|
+
"out",
|
|
1773
|
+
"coverage",
|
|
1774
|
+
"vendor",
|
|
1775
|
+
"target",
|
|
1776
|
+
"__pycache__",
|
|
1777
|
+
".venv",
|
|
1778
|
+
"venv",
|
|
1779
|
+
".turbo",
|
|
1780
|
+
".cache"
|
|
1781
|
+
]);
|
|
1782
|
+
var DOC_EXT = /* @__PURE__ */ new Set([".md", ".mdx"]);
|
|
1783
|
+
var MAX_FILE_CHARS = 2e4;
|
|
1784
|
+
function read(path) {
|
|
1785
|
+
try {
|
|
1786
|
+
return readFileSync7(path, "utf8");
|
|
1787
|
+
} catch {
|
|
1788
|
+
return null;
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
function walk(dir, base, depth = 0, out = []) {
|
|
1792
|
+
if (depth > 3 || out.length > 4e3) return out;
|
|
1793
|
+
let entries;
|
|
1794
|
+
try {
|
|
1795
|
+
entries = readdirSync2(dir);
|
|
1796
|
+
} catch {
|
|
1797
|
+
return out;
|
|
1798
|
+
}
|
|
1799
|
+
for (const name of entries) {
|
|
1800
|
+
if (name.startsWith(".") && name !== ".github") continue;
|
|
1801
|
+
if (SKIP_DIRS.has(name)) continue;
|
|
1802
|
+
const full = join7(dir, name);
|
|
1803
|
+
let st;
|
|
1804
|
+
try {
|
|
1805
|
+
st = statSync(full);
|
|
1806
|
+
} catch {
|
|
1807
|
+
continue;
|
|
1808
|
+
}
|
|
1809
|
+
if (st.isDirectory()) walk(full, base, depth + 1, out);
|
|
1810
|
+
else out.push(relative(base, full));
|
|
1811
|
+
}
|
|
1812
|
+
return out;
|
|
1813
|
+
}
|
|
1814
|
+
function readRepo(baseDir) {
|
|
1815
|
+
const files = walk(baseDir, baseDir);
|
|
1816
|
+
const scripts = [];
|
|
1817
|
+
const languages = /* @__PURE__ */ new Set();
|
|
1818
|
+
const pkgRaw = read(join7(baseDir, "package.json"));
|
|
1819
|
+
if (pkgRaw) {
|
|
1820
|
+
try {
|
|
1821
|
+
const pkg = JSON.parse(pkgRaw);
|
|
1822
|
+
for (const [name, command] of Object.entries(pkg.scripts ?? {})) {
|
|
1823
|
+
scripts.push({ name, command, from: "package.json" });
|
|
1824
|
+
}
|
|
1825
|
+
} catch {
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
const makefile = read(join7(baseDir, "Makefile"));
|
|
1829
|
+
if (makefile) {
|
|
1830
|
+
for (const line of makefile.split("\n")) {
|
|
1831
|
+
const m = /^([a-zA-Z][\w-]*):(?!=)/.exec(line);
|
|
1832
|
+
if (m) scripts.push({ name: m[1], command: `make ${m[1]}`, from: "Makefile" });
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
const pyproject = read(join7(baseDir, "pyproject.toml"));
|
|
1836
|
+
if (pyproject) languages.add("python");
|
|
1837
|
+
for (const f of files) {
|
|
1838
|
+
const ext = extname(f);
|
|
1839
|
+
if (ext === ".ts" || ext === ".tsx") languages.add("typescript");
|
|
1840
|
+
else if (ext === ".js" || ext === ".jsx") languages.add("javascript");
|
|
1841
|
+
else if (ext === ".py") languages.add("python");
|
|
1842
|
+
else if (ext === ".go") languages.add("go");
|
|
1843
|
+
else if (ext === ".rs") languages.add("rust");
|
|
1844
|
+
else if (ext === ".tf") languages.add("terraform");
|
|
1845
|
+
else if (ext === ".java") languages.add("java");
|
|
1846
|
+
else if (ext === ".cs") languages.add("csharp");
|
|
1847
|
+
}
|
|
1848
|
+
const packageManager = existsSync8(join7(baseDir, "pnpm-lock.yaml")) ? "pnpm" : existsSync8(join7(baseDir, "yarn.lock")) ? "yarn" : existsSync8(join7(baseDir, "bun.lockb")) ? "bun" : existsSync8(join7(baseDir, "package-lock.json")) ? "npm" : null;
|
|
1849
|
+
const docs = files.filter((f) => DOC_EXT.has(extname(f)) && (!f.includes("/") || f.startsWith("docs/"))).slice(0, 12).map((f) => ({ path: f, content: (read(join7(baseDir, f)) ?? "").slice(0, MAX_FILE_CHARS) })).filter((d) => d.content.trim());
|
|
1850
|
+
return {
|
|
1851
|
+
name: basename2(baseDir),
|
|
1852
|
+
scripts,
|
|
1853
|
+
packageManager,
|
|
1854
|
+
languages: [...languages],
|
|
1855
|
+
docs,
|
|
1856
|
+
hasCi: files.some((f) => f.startsWith(".github/workflows/")),
|
|
1857
|
+
hasIac: files.some((f) => extname(f) === ".tf" || f.includes("bicep"))
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1860
|
+
function recentCommitSubjects(baseDir, n = 40) {
|
|
1861
|
+
try {
|
|
1862
|
+
const out = execFileSync("git", ["log", `-${n}`, "--pretty=%s"], {
|
|
1863
|
+
cwd: baseDir,
|
|
1864
|
+
encoding: "utf8",
|
|
1865
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1866
|
+
});
|
|
1867
|
+
return out.split("\n").filter(Boolean);
|
|
1868
|
+
} catch {
|
|
1869
|
+
return [];
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
// src/doctor.ts
|
|
1874
|
+
function harnessText(brain) {
|
|
1875
|
+
return (brain.cards ?? []).map((c2) => `${c2.title}
|
|
1876
|
+
${c2.content ?? ""}`).join("\n").toLowerCase();
|
|
1877
|
+
}
|
|
1878
|
+
function detectDrift(brain, repo) {
|
|
1879
|
+
const text = harnessText(brain);
|
|
1880
|
+
const findings = [];
|
|
1881
|
+
const declared = new Set(repo.scripts.map((s) => s.name));
|
|
1882
|
+
const mentionedScripts = [
|
|
1883
|
+
...text.matchAll(/\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?([a-z][\w:-]*)/g)
|
|
1884
|
+
].map((m) => m[1]).filter((n) => !["install", "ci", "add", "i", "exec", "dlx", "create"].includes(n));
|
|
1885
|
+
for (const name of new Set(mentionedScripts)) {
|
|
1886
|
+
if (declared.size === 0) break;
|
|
1887
|
+
if (declared.has(name)) continue;
|
|
1888
|
+
findings.push({
|
|
1889
|
+
severity: "critical",
|
|
1890
|
+
message: `O harness manda rodar "${name}", que n\xE3o existe no projeto.`,
|
|
1891
|
+
detail: `Dispon\xEDveis: ${[...declared].slice(0, 12).join(", ") || "(nenhum)"}`
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1894
|
+
const testScript = repo.scripts.find((s) => /^(test|tests|check|verify)$/.test(s.name));
|
|
1895
|
+
if (testScript && !/\btest/.test(text)) {
|
|
1896
|
+
findings.push({
|
|
1897
|
+
severity: "critical",
|
|
1898
|
+
message: "O projeto tem comando de teste e o harness n\xE3o menciona nenhum.",
|
|
1899
|
+
detail: `${testScript.from}: ${testScript.name} \u2192 ${testScript.command}`
|
|
1900
|
+
});
|
|
1901
|
+
}
|
|
1902
|
+
if (repo.packageManager) {
|
|
1903
|
+
const others = ["npm", "pnpm", "yarn", "bun"].filter((p) => p !== repo.packageManager);
|
|
1904
|
+
const wrong = others.filter((p) => new RegExp(`\\b${p}\\s+(run|install|test|add)\\b`).test(text));
|
|
1905
|
+
if (wrong.length > 0 && !new RegExp(`\\b${repo.packageManager}\\b`).test(text)) {
|
|
1906
|
+
findings.push({
|
|
1907
|
+
severity: "warning",
|
|
1908
|
+
message: `O harness usa ${wrong.join("/")}, mas o lockfile \xE9 de ${repo.packageManager}.`,
|
|
1909
|
+
detail: "Rodar o gerenciador errado reescreve o lock e quebra o CI."
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
if (repo.hasIac && !/\b(nunca|never|terraform|aprova|confirm)/.test(text)) {
|
|
1914
|
+
findings.push({
|
|
1915
|
+
severity: "warning",
|
|
1916
|
+
message: "O reposit\xF3rio tem infraestrutura como c\xF3digo e o harness n\xE3o declara limites.",
|
|
1917
|
+
detail: "Um `apply` sem gate \xE9 a diferen\xE7a entre um erro e um incidente."
|
|
1918
|
+
});
|
|
1919
|
+
}
|
|
1920
|
+
for (const lang of repo.languages.slice(0, 3)) {
|
|
1921
|
+
if (!text.includes(lang)) {
|
|
1922
|
+
findings.push({
|
|
1923
|
+
severity: "warning",
|
|
1924
|
+
message: `O projeto \xE9 ${lang} e o harness n\xE3o menciona a linguagem.`,
|
|
1925
|
+
detail: "Sem isso, conven\xE7\xF5es de estilo e ferramentas ficam por conta do palpite."
|
|
1926
|
+
});
|
|
1927
|
+
break;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
return findings;
|
|
1931
|
+
}
|
|
1932
|
+
async function runDoctor(opts) {
|
|
1933
|
+
const brain = await fetchBrain(opts.apiUrl, opts.key);
|
|
1934
|
+
const repo = readRepo(process.cwd());
|
|
1935
|
+
info(
|
|
1936
|
+
`${color.bold(repo.name)} \u2014 ${repo.languages.join(", ") || "linguagem indefinida"}${repo.packageManager ? ` \xB7 ${repo.packageManager}` : ""} \xB7 ${repo.scripts.length} comando(s)`
|
|
1937
|
+
);
|
|
1938
|
+
info(color.dim(`harness: ${brain.brain?.name ?? "?"} \u2014 ${brain.cards?.length ?? 0} pe\xE7as
|
|
1939
|
+
`));
|
|
1940
|
+
const findings = detectDrift(brain, repo);
|
|
1941
|
+
if (findings.length === 0) {
|
|
1942
|
+
ok("nenhum drift entre o harness e o reposit\xF3rio.");
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1945
|
+
for (const f of findings) {
|
|
1946
|
+
if (f.severity === "critical") {
|
|
1947
|
+
console.error(`${color.red("\u2717 cr\xEDtico")} ${f.message}`);
|
|
1948
|
+
} else {
|
|
1949
|
+
warn(f.message);
|
|
1950
|
+
}
|
|
1951
|
+
info(color.dim(` ${f.detail}`));
|
|
1952
|
+
}
|
|
1953
|
+
info(
|
|
1954
|
+
color.dim(
|
|
1955
|
+
`
|
|
1956
|
+
${findings.length} achado(s). Corrija no Origem (a mudan\xE7a vale nas pr\xF3ximas sess\xF5es) ou proponha com \`origem propose\`.`
|
|
1957
|
+
)
|
|
1958
|
+
);
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
// src/bootstrap.ts
|
|
1962
|
+
function buildEntries(repo, commits) {
|
|
1963
|
+
const entries = [];
|
|
1964
|
+
const pm = repo.packageManager ?? "npm";
|
|
1965
|
+
const build = repo.scripts.find((s) => /^(build|compile)$/.test(s.name));
|
|
1966
|
+
const test = repo.scripts.find((s) => /^(test|tests|check|verify)$/.test(s.name));
|
|
1967
|
+
const dev = repo.scripts.find((s) => /^(dev|start|serve)$/.test(s.name));
|
|
1968
|
+
const identity = [
|
|
1969
|
+
`# ${repo.name}`,
|
|
1970
|
+
"",
|
|
1971
|
+
repo.languages.length ? `Projeto em ${repo.languages.join(", ")}.` : "",
|
|
1972
|
+
repo.packageManager ? `Gerenciador de pacotes: **${repo.packageManager}** \u2014 use este, n\xE3o outro.` : "",
|
|
1973
|
+
"",
|
|
1974
|
+
"## Comandos",
|
|
1975
|
+
""
|
|
1976
|
+
];
|
|
1977
|
+
const cmd = (label, s) => s ? `- **${label}:** \`${s.from === "Makefile" ? s.command : `${pm} run ${s.name}`}\`` : "";
|
|
1978
|
+
identity.push(cmd("build", build), cmd("testes", test), cmd("dev", dev));
|
|
1979
|
+
const others = repo.scripts.filter((s) => ![build, test, dev].includes(s)).slice(0, 8).map((s) => `- \`${s.from === "Makefile" ? s.command : `${pm} run ${s.name}`}\` \u2014 ${s.command}`);
|
|
1980
|
+
if (others.length) identity.push("", "### Outros", "", ...others);
|
|
1981
|
+
entries.push({
|
|
1982
|
+
type: "instruction",
|
|
1983
|
+
title: `${repo.name} \u2014 vis\xE3o e comandos`,
|
|
1984
|
+
content: identity.filter(Boolean).join("\n"),
|
|
1985
|
+
tags: ["bootstrap"]
|
|
1986
|
+
});
|
|
1987
|
+
const boundaries = [];
|
|
1988
|
+
if (repo.hasIac) {
|
|
1989
|
+
boundaries.push(
|
|
1990
|
+
"- Nunca rode `terraform apply` sem mostrar o plan e obter confirma\xE7\xE3o.",
|
|
1991
|
+
"- Sempre pe\xE7a confirma\xE7\xE3o antes de qualquer opera\xE7\xE3o que destrua recurso."
|
|
1992
|
+
);
|
|
1993
|
+
}
|
|
1994
|
+
if (repo.packageManager) {
|
|
1995
|
+
boundaries.push(
|
|
1996
|
+
`- Nunca rode outro gerenciador que n\xE3o \`${repo.packageManager}\` \u2014 reescreve o lockfile e quebra o CI.`
|
|
1997
|
+
);
|
|
1998
|
+
}
|
|
1999
|
+
if (repo.hasCi) {
|
|
2000
|
+
boundaries.push("- Perguntar antes de alterar workflows em `.github/workflows/`.");
|
|
2001
|
+
}
|
|
2002
|
+
if (boundaries.length) {
|
|
2003
|
+
entries.push({
|
|
2004
|
+
type: "boundary",
|
|
2005
|
+
title: "Limites do projeto",
|
|
2006
|
+
content: ["Sempre / Perguntar antes / Nunca:", "", ...boundaries].join("\n"),
|
|
2007
|
+
tags: ["bootstrap"]
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
const conventional = commits.filter((c2) => /^(feat|fix|chore|docs|refactor|test)(\(.+\))?!?:/.test(c2));
|
|
2011
|
+
if (commits.length >= 10 && conventional.length / commits.length > 0.6) {
|
|
2012
|
+
entries.push({
|
|
2013
|
+
type: "rule",
|
|
2014
|
+
title: "Conven\xE7\xE3o de commits",
|
|
2015
|
+
content: [
|
|
2016
|
+
`Este reposit\xF3rio usa Conventional Commits \u2014 ${conventional.length} dos ${commits.length} commits recentes seguem o padr\xE3o.`,
|
|
2017
|
+
"",
|
|
2018
|
+
"Formato: `tipo(escopo): descri\xE7\xE3o`, ex.: `fix(auth): corrige refresh do token`."
|
|
2019
|
+
].join("\n"),
|
|
2020
|
+
tags: ["bootstrap"]
|
|
2021
|
+
});
|
|
2022
|
+
}
|
|
2023
|
+
for (const doc of repo.docs.slice(0, 6)) {
|
|
2024
|
+
entries.push({
|
|
2025
|
+
type: "knowledge",
|
|
2026
|
+
title: doc.path,
|
|
2027
|
+
content: doc.content,
|
|
2028
|
+
tags: ["bootstrap", "docs"]
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
return entries;
|
|
2032
|
+
}
|
|
2033
|
+
async function runBootstrap(opts) {
|
|
2034
|
+
const baseDir = process.cwd();
|
|
2035
|
+
const repo = readRepo(baseDir);
|
|
2036
|
+
const commits = recentCommitSubjects(baseDir);
|
|
2037
|
+
const entries = buildEntries(repo, commits);
|
|
2038
|
+
info(
|
|
2039
|
+
`${color.bold(repo.name)} \u2014 ${repo.languages.join(", ") || "linguagem indefinida"}${repo.packageManager ? ` \xB7 ${repo.packageManager}` : ""}`
|
|
2040
|
+
);
|
|
2041
|
+
info(`${repo.scripts.length} comando(s) \xB7 ${repo.docs.length} doc(s) \xB7 ${commits.length} commits lidos
|
|
2042
|
+
`);
|
|
2043
|
+
info(color.bold("Vai propor:"));
|
|
2044
|
+
for (const e of entries) {
|
|
2045
|
+
const preview = e.content.split("\n").find((l) => l.trim() && !l.startsWith("#")) ?? "";
|
|
2046
|
+
info(` ${color.cyan(e.type.padEnd(12))} ${e.title}`);
|
|
2047
|
+
info(color.dim(` ${preview.slice(0, 76)}`));
|
|
2048
|
+
}
|
|
2049
|
+
if (opts.dryRun) {
|
|
2050
|
+
info(color.dim("\n--dry-run: nada foi enviado."));
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
2053
|
+
if (!opts.yes && !await confirm(`
|
|
2054
|
+
Enviar ${entries.length} proposta(s)?`)) {
|
|
2055
|
+
warn("Nada foi enviado.");
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
const base = opts.apiUrl.replace(/\/$/, "");
|
|
2059
|
+
const res = await fetch(`${base}/ingest?k=${encodeURIComponent(opts.key)}`, {
|
|
2060
|
+
method: "POST",
|
|
2061
|
+
headers: { "Content-Type": "application/json" },
|
|
2062
|
+
body: JSON.stringify({ source: repo.name, entries }),
|
|
2063
|
+
signal: AbortSignal.timeout(3e4)
|
|
2064
|
+
});
|
|
2065
|
+
const data = await res.json().catch(() => ({}));
|
|
2066
|
+
if (res.status === 403) throw new Error("Token sem escopo de escrita \u2014 refa\xE7a o `origem connect`.");
|
|
2067
|
+
if (!res.ok) throw new Error(data.error ?? `Falha ao propor (HTTP ${res.status}).`);
|
|
2068
|
+
ok(`${data.proposed} pe\xE7a(s) propostas como um lote.`);
|
|
2069
|
+
info(color.dim("Aceite ou descarte o lote no Origem \u2014 nada foi escrito no harness ainda."));
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
// src/index.ts
|
|
2073
|
+
var HELP = `${color.bold("origem")} \u2014 materializa um Brain do Origem na config da sua IA.
|
|
2074
|
+
|
|
2075
|
+
${color.bold("Uso:")}
|
|
2076
|
+
origem connect [op\xE7\xF5es] Conecta este reposit\xF3rio a um harness (autoriza no
|
|
2077
|
+
navegador, aplica em disco, instala skill + hooks).
|
|
2078
|
+
Rodar de novo sincroniza.
|
|
2079
|
+
origem spec Mostra os specs abertos e em que est\xE1gio est\xE3o
|
|
2080
|
+
origem doctor Compara o harness com este reposit\xF3rio (drift)
|
|
2081
|
+
origem bootstrap L\xEA o reposit\xF3rio e prop\xF5e um harness inteiro
|
|
2082
|
+
origem propose [op\xE7\xF5es] Sugere uma mudan\xE7a no harness (fica pendente at\xE9
|
|
2083
|
+
ser aceita no site; n\xE3o escreve nada)
|
|
2084
|
+
origem apply [op\xE7\xF5es] Puxa o Brain e escreve os arquivos de config
|
|
2085
|
+
origem status [op\xE7\xF5es] Mostra o que mudaria e se h\xE1 edi\xE7\xF5es locais
|
|
2086
|
+
origem remove [op\xE7\xF5es] Reverte tudo que o Origem escreveu (usa o lock)
|
|
2087
|
+
origem login --key <t> Grava token + API em ~/.origem/config.json (0600)
|
|
2088
|
+
origem logout Remove as credenciais gravadas
|
|
2089
|
+
origem watch [op\xE7\xF5es] Plant\xE3o local: executa comandos da fila mesmo sem sess\xE3o aberta
|
|
2090
|
+
origem hook install Instala os hooks que ligam a sess\xE3o ao Origem
|
|
2091
|
+
origem hook start (uso interno) marca o in\xEDcio do turno
|
|
2092
|
+
origem hook stop (uso interno) reporta o turno e busca comando
|
|
2093
|
+
origem help Esta ajuda
|
|
2094
|
+
|
|
2095
|
+
${color.bold("Op\xE7\xF5es:")}
|
|
2096
|
+
-k, --key <token> Token de capacidade (ou env ORIGEM_TOKEN)
|
|
2097
|
+
--api-url <url> Base da API (ou env ORIGEM_API_URL)
|
|
2098
|
+
--scope <s> project (padr\xE3o) | user (~/.claude)
|
|
2099
|
+
--dry-run S\xF3 mostra o plano; n\xE3o escreve
|
|
2100
|
+
-y, --yes Aplica config privilegiada sem perguntar
|
|
2101
|
+
--targets a,b Alvos espec\xEDficos (ex: claude,claude-settings,mcp)
|
|
2102
|
+
--all Todos os alvos (AGENTS.md, Cursor, Gemini, Copilot\u2026)
|
|
2103
|
+
--finish (hook stop) fecha o run em vez de s\xF3 reportar o turno
|
|
2104
|
+
--quiet (hook stop) n\xE3o escreve erro no stderr
|
|
2105
|
+
--bin <cmd> (hook install) comando gravado no settings.json
|
|
2106
|
+
--local (hook install) grava em .claude/settings.local.json
|
|
2107
|
+
--once (watch) uma passada s\xF3 e sai (bom pra cron)
|
|
2108
|
+
--no-hooks (connect) n\xE3o instala os hooks de sess\xE3o
|
|
2109
|
+
--type <t> (propose) compartimento: rule, boundary, memory, skill\u2026
|
|
2110
|
+
--title <t> (propose) t\xEDtulo da pe\xE7a
|
|
2111
|
+
--content <t> (propose) conte\xFAdo, ou use --file <caminho>
|
|
2112
|
+
--why <t> (propose) por que est\xE1 propondo \u2014 obrigat\xF3rio
|
|
2113
|
+
--card <id> (propose) altera uma pe\xE7a existente em vez de criar
|
|
2114
|
+
--interval <s> (watch) segundos entre consultas \xE0 fila (padr\xE3o 5)
|
|
2115
|
+
--claude <path> (watch) bin\xE1rio do Claude Code (padr\xE3o: descobre sozinho)
|
|
2116
|
+
--cwd <dir> (watch) diret\xF3rio onde executar (padr\xE3o: o atual)
|
|
2117
|
+
--permission-mode (watch) repassado ao Claude Code
|
|
2118
|
+
|
|
2119
|
+
${color.bold("Alvos padr\xE3o (Claude Code):")} ${CLAUDE_TARGETS.join(", ")}
|
|
2120
|
+
${color.bold("Todos:")} ${ALL_TARGETS.join(", ")}
|
|
2121
|
+
|
|
2122
|
+
${color.bold("Come\xE7ando:")} \`origem connect\` \xE9 o \xFAnico comando necess\xE1rio \u2014 ele autoriza no
|
|
2123
|
+
navegador (OAuth + PKCE, sem token colado), grava a credencial, materializa o harness
|
|
2124
|
+
e instala a skill. \`login\`/\`apply\`/\`hook install\` continuam dispon\xEDveis pra quem
|
|
2125
|
+
precisa dos passos separados.
|
|
2126
|
+
|
|
2127
|
+
${color.bold("Controle pelo site:")} com os hooks instalados, cada turno \xE9 reportado ao
|
|
2128
|
+
Origem e a resposta pode trazer o pr\xF3ximo prompt da fila \u2014 a sess\xE3o continua sozinha.
|
|
2129
|
+
O que trafega \xE9 PROMPT, nunca shell: quem decide o que roda \xE9 a permiss\xE3o do Claude.
|
|
2130
|
+
|
|
2131
|
+
${color.bold("Confian\xE7a:")} arquivos privilegiados (settings.json com hooks/env, .mcp.json
|
|
2132
|
+
com command) exigem confirma\xE7\xE3o. Defina ORIGEM_SIGNING_SECRET pra exigir assinatura.
|
|
2133
|
+
`;
|
|
2134
|
+
function parseArgs(argv) {
|
|
2135
|
+
const cmd = argv[0] && !argv[0].startsWith("-") ? argv[0] : "help";
|
|
2136
|
+
const flags = {
|
|
2137
|
+
scope: "project",
|
|
2138
|
+
dryRun: false,
|
|
2139
|
+
yes: false,
|
|
2140
|
+
finish: false,
|
|
2141
|
+
quiet: false,
|
|
2142
|
+
bin: "origem",
|
|
2143
|
+
local: false,
|
|
2144
|
+
interval: 5,
|
|
2145
|
+
once: false,
|
|
2146
|
+
noHooks: false
|
|
2147
|
+
};
|
|
2148
|
+
let rest = argv.slice(cmd === argv[0] ? 1 : 0);
|
|
2149
|
+
if (cmd === "hook" && rest[0] && !rest[0].startsWith("-")) {
|
|
2150
|
+
flags.sub = rest[0];
|
|
2151
|
+
rest = rest.slice(1);
|
|
2152
|
+
}
|
|
2153
|
+
for (let i = 0; i < rest.length; i++) {
|
|
2154
|
+
const a = rest[i];
|
|
2155
|
+
switch (a) {
|
|
2156
|
+
case "-k":
|
|
2157
|
+
case "--key":
|
|
2158
|
+
flags.key = rest[++i];
|
|
2159
|
+
break;
|
|
2160
|
+
case "--api-url":
|
|
2161
|
+
flags.apiUrl = rest[++i];
|
|
2162
|
+
break;
|
|
2163
|
+
case "--scope": {
|
|
2164
|
+
const s = rest[++i];
|
|
2165
|
+
if (s !== "project" && s !== "user") throw new Error(`--scope inv\xE1lido: ${s}`);
|
|
2166
|
+
flags.scope = s;
|
|
2167
|
+
break;
|
|
2168
|
+
}
|
|
2169
|
+
case "--dry-run":
|
|
2170
|
+
flags.dryRun = true;
|
|
2171
|
+
break;
|
|
2172
|
+
case "-y":
|
|
2173
|
+
case "--yes":
|
|
2174
|
+
flags.yes = true;
|
|
2175
|
+
break;
|
|
2176
|
+
case "--targets":
|
|
2177
|
+
flags.targets = rest[++i].split(",").map((t) => t.trim());
|
|
2178
|
+
break;
|
|
2179
|
+
case "--all":
|
|
2180
|
+
flags.targets = ALL_TARGETS;
|
|
2181
|
+
break;
|
|
2182
|
+
case "--finish":
|
|
2183
|
+
flags.finish = true;
|
|
2184
|
+
break;
|
|
2185
|
+
case "--quiet":
|
|
2186
|
+
flags.quiet = true;
|
|
2187
|
+
break;
|
|
2188
|
+
case "--bin":
|
|
2189
|
+
flags.bin = rest[++i];
|
|
2190
|
+
break;
|
|
2191
|
+
case "--local":
|
|
2192
|
+
flags.local = true;
|
|
2193
|
+
break;
|
|
2194
|
+
case "--interval": {
|
|
2195
|
+
const n = Number(rest[++i]);
|
|
2196
|
+
if (!Number.isFinite(n) || n < 2) throw new Error("--interval deve ser >= 2 (segundos)");
|
|
2197
|
+
flags.interval = n;
|
|
2198
|
+
break;
|
|
2199
|
+
}
|
|
2200
|
+
case "--once":
|
|
2201
|
+
flags.once = true;
|
|
2202
|
+
break;
|
|
2203
|
+
case "--no-hooks":
|
|
2204
|
+
flags.noHooks = true;
|
|
2205
|
+
break;
|
|
2206
|
+
case "--type":
|
|
2207
|
+
flags.type = rest[++i];
|
|
2208
|
+
break;
|
|
2209
|
+
case "--title":
|
|
2210
|
+
flags.title = rest[++i];
|
|
2211
|
+
break;
|
|
2212
|
+
case "--content":
|
|
2213
|
+
flags.content = rest[++i];
|
|
2214
|
+
break;
|
|
2215
|
+
case "--file":
|
|
2216
|
+
flags.file = rest[++i];
|
|
2217
|
+
break;
|
|
2218
|
+
case "--why":
|
|
2219
|
+
flags.why = rest[++i];
|
|
2220
|
+
break;
|
|
2221
|
+
case "--card":
|
|
2222
|
+
flags.cardId = rest[++i];
|
|
2223
|
+
break;
|
|
2224
|
+
case "--session":
|
|
2225
|
+
flags.sessionId = rest[++i];
|
|
2226
|
+
break;
|
|
2227
|
+
case "--claude":
|
|
2228
|
+
flags.claudeBin = rest[++i];
|
|
2229
|
+
break;
|
|
2230
|
+
case "--cwd":
|
|
2231
|
+
flags.cwd = rest[++i];
|
|
2232
|
+
break;
|
|
2233
|
+
case "--permission-mode":
|
|
2234
|
+
flags.permissionMode = rest[++i];
|
|
2235
|
+
break;
|
|
2236
|
+
default:
|
|
2237
|
+
throw new Error(`Op\xE7\xE3o desconhecida: ${a}`);
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
return { cmd, flags };
|
|
2241
|
+
}
|
|
2242
|
+
async function main() {
|
|
2243
|
+
const { cmd, flags } = parseArgs(process.argv.slice(2));
|
|
2244
|
+
if (cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
2245
|
+
info(HELP);
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
const targets = flags.targets ?? CLAUDE_TARGETS;
|
|
2249
|
+
switch (cmd) {
|
|
2250
|
+
case "connect": {
|
|
2251
|
+
await runConnect({
|
|
2252
|
+
apiUrl: resolveApiUrl(flags.apiUrl),
|
|
2253
|
+
scope: flags.scope,
|
|
2254
|
+
// `targets` já traz o default (CLAUDE_TARGETS); passar flags.targets aqui
|
|
2255
|
+
// faria o apply cair no "todos os alvos" e escrever GEMINI.md, .cursor/…
|
|
2256
|
+
targets,
|
|
2257
|
+
yes: flags.yes,
|
|
2258
|
+
noHooks: flags.noHooks,
|
|
2259
|
+
bin: flags.bin
|
|
2260
|
+
});
|
|
2261
|
+
break;
|
|
2262
|
+
}
|
|
2263
|
+
case "propose": {
|
|
2264
|
+
if (!flags.type || !flags.title) {
|
|
2265
|
+
throw new Error("Use: origem propose --type <compartimento> --title <t\xEDtulo> --content <texto> --why <motivo>");
|
|
2266
|
+
}
|
|
2267
|
+
await runPropose({
|
|
2268
|
+
apiUrl: resolveApiUrl(flags.apiUrl),
|
|
2269
|
+
key: resolveKey(flags.key),
|
|
2270
|
+
type: flags.type,
|
|
2271
|
+
title: flags.title,
|
|
2272
|
+
content: flags.content,
|
|
2273
|
+
file: flags.file,
|
|
2274
|
+
rationale: flags.why ?? "",
|
|
2275
|
+
cardId: flags.cardId,
|
|
2276
|
+
sessionId: flags.sessionId
|
|
2277
|
+
});
|
|
2278
|
+
break;
|
|
2279
|
+
}
|
|
2280
|
+
case "spec": {
|
|
2281
|
+
await runSpec({ apiUrl: resolveApiUrl(flags.apiUrl), key: resolveKey(flags.key) });
|
|
2282
|
+
break;
|
|
2283
|
+
}
|
|
2284
|
+
case "doctor": {
|
|
2285
|
+
await runDoctor({ apiUrl: resolveApiUrl(flags.apiUrl), key: resolveKey(flags.key) });
|
|
2286
|
+
break;
|
|
2287
|
+
}
|
|
2288
|
+
case "bootstrap": {
|
|
2289
|
+
await runBootstrap({
|
|
2290
|
+
apiUrl: resolveApiUrl(flags.apiUrl),
|
|
2291
|
+
key: resolveKey(flags.key),
|
|
2292
|
+
yes: flags.yes,
|
|
2293
|
+
dryRun: flags.dryRun
|
|
2294
|
+
});
|
|
2295
|
+
break;
|
|
2296
|
+
}
|
|
2297
|
+
case "apply": {
|
|
2298
|
+
const apiUrl = resolveApiUrl(flags.apiUrl);
|
|
2299
|
+
const brain = await fetchBrain(apiUrl, resolveKey(flags.key));
|
|
2300
|
+
brain.__apiUrl = apiUrl;
|
|
2301
|
+
await runApply({ brain, scope: flags.scope, targets, dryRun: flags.dryRun, yes: flags.yes });
|
|
2302
|
+
break;
|
|
2303
|
+
}
|
|
2304
|
+
case "status": {
|
|
2305
|
+
const apiUrl = resolveApiUrl(flags.apiUrl);
|
|
2306
|
+
const brain = await fetchBrain(apiUrl, resolveKey(flags.key));
|
|
2307
|
+
runStatus(brain, flags.scope, targets);
|
|
2308
|
+
break;
|
|
2309
|
+
}
|
|
2310
|
+
case "remove": {
|
|
2311
|
+
runRemove(flags.scope);
|
|
2312
|
+
break;
|
|
2313
|
+
}
|
|
2314
|
+
case "login": {
|
|
2315
|
+
await runLogin({ key: flags.key, apiUrl: resolveApiUrl(flags.apiUrl) });
|
|
2316
|
+
break;
|
|
2317
|
+
}
|
|
2318
|
+
case "logout": {
|
|
2319
|
+
runLogout();
|
|
2320
|
+
break;
|
|
2321
|
+
}
|
|
2322
|
+
case "watch": {
|
|
2323
|
+
await runWatch({
|
|
2324
|
+
apiUrl: resolveApiUrl(flags.apiUrl),
|
|
2325
|
+
key: resolveKey(flags.key),
|
|
2326
|
+
intervalMs: flags.interval * 1e3,
|
|
2327
|
+
cwd: flags.cwd ?? process.cwd(),
|
|
2328
|
+
claudeBin: flags.claudeBin,
|
|
2329
|
+
dryRun: flags.dryRun,
|
|
2330
|
+
yes: flags.yes,
|
|
2331
|
+
once: flags.once,
|
|
2332
|
+
permissionMode: flags.permissionMode
|
|
2333
|
+
});
|
|
2334
|
+
break;
|
|
2335
|
+
}
|
|
2336
|
+
case "hook": {
|
|
2337
|
+
if (flags.sub === "install") {
|
|
2338
|
+
await hookInstall({
|
|
2339
|
+
yes: flags.yes,
|
|
2340
|
+
bin: flags.bin,
|
|
2341
|
+
key: flags.key,
|
|
2342
|
+
apiUrl: flags.apiUrl,
|
|
2343
|
+
local: flags.local
|
|
2344
|
+
});
|
|
2345
|
+
break;
|
|
2346
|
+
}
|
|
2347
|
+
try {
|
|
2348
|
+
if (flags.sub === "start") {
|
|
2349
|
+
await hookStart();
|
|
2350
|
+
} else if (flags.sub === "stop") {
|
|
2351
|
+
await hookStop({
|
|
2352
|
+
apiUrl: resolveApiUrl(flags.apiUrl),
|
|
2353
|
+
key: resolveKey(flags.key),
|
|
2354
|
+
finish: flags.finish,
|
|
2355
|
+
quiet: flags.quiet
|
|
2356
|
+
});
|
|
2357
|
+
} else {
|
|
2358
|
+
err("Use: origem hook install | origem hook start | origem hook stop");
|
|
2359
|
+
process.exitCode = 1;
|
|
2360
|
+
}
|
|
2361
|
+
} catch (e) {
|
|
2362
|
+
if (!flags.quiet) process.stderr.write(`origem: ${e.message}
|
|
2363
|
+
`);
|
|
2364
|
+
}
|
|
2365
|
+
break;
|
|
2366
|
+
}
|
|
2367
|
+
default:
|
|
2368
|
+
err(`Comando desconhecido: ${cmd}`);
|
|
2369
|
+
info(HELP);
|
|
2370
|
+
process.exitCode = 1;
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
main().catch((e) => {
|
|
2374
|
+
err(e.message);
|
|
2375
|
+
process.exitCode = 1;
|
|
2376
|
+
});
|