zugzbot-sdd 1.5.13 → 1.5.15
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/.opencode/plugins/plugin_sdd_core.ts +54 -0
- package/.opencode/plugins/plugin_tui.tsx +319 -0
- package/.opencode/skills/sdd-dependency-cooldown/SKILL.md +40 -0
- package/.opencode/skills/sdd-tree-generator/SKILL.md +40 -0
- package/.opencode/tools/brain-utils.js +112 -0
- package/.opencode/tools/check_dependency_cooldown.js +121 -0
- package/.opencode/tools/index.js +14 -0
- package/.opencode/tools/sdd_archive_and_commit.js +222 -0
- package/.opencode/tools/sdd_bdd_tester.js +152 -0
- package/.opencode/tools/sdd_brain_sync.js +141 -0
- package/.opencode/tools/sdd_checkpoint.js +122 -0
- package/.opencode/tools/sdd_compact_context.js +109 -0
- package/.opencode/tools/sdd_generate_tree.js +58 -0
- package/.opencode/tools/sdd_install_autoskills.js +105 -0
- package/.opencode/tools/sdd_regression_detector.js +258 -0
- package/.opencode/tools/sdd_requirement_tracker.js +204 -0
- package/.opencode/tools/sdd_secret_scanner.js +228 -0
- package/.opencode/tools/sdd_spec_validator.js +115 -0
- package/.opencode/tools/sdd_transition.js +379 -0
- package/.opencode/tools/sdd_ui_auditor.js +295 -0
- package/.openspec/brain.md +4 -0
- package/.openspec/sdd-lock.json +20 -0
- package/bin/zugzbot.js +24 -0
- package/docs_opencode/acp.md +165 -0
- package/docs_opencode/acp.pdf +0 -0
- package/docs_opencode/agents.md +803 -0
- package/docs_opencode/agents.pdf +0 -0
- package/docs_opencode/commands.md +354 -0
- package/docs_opencode/commands.pdf +0 -0
- package/docs_opencode/custom-tools.md +209 -0
- package/docs_opencode/custom-tools.pdf +0 -0
- package/docs_opencode/ecosystem.md +81 -0
- package/docs_opencode/ecosystem.pdf +0 -0
- package/docs_opencode/formatters.md +142 -0
- package/docs_opencode/formatters.pdf +0 -0
- package/docs_opencode/keybinds.md +205 -0
- package/docs_opencode/keybinds.pdf +0 -0
- package/docs_opencode/lsp.md +202 -0
- package/docs_opencode/lsp.pdf +0 -0
- package/docs_opencode/mcp-servers.md +565 -0
- package/docs_opencode/mcp-servers.pdf +0 -0
- package/docs_opencode/models.md +234 -0
- package/docs_opencode/models.pdf +0 -0
- package/docs_opencode/permissions.md +248 -0
- package/docs_opencode/permissions.pdf +0 -0
- package/docs_opencode/plugins.md +409 -0
- package/docs_opencode/plugins.pdf +0 -0
- package/docs_opencode/rules.md +189 -0
- package/docs_opencode/rules.pdf +0 -0
- package/docs_opencode/sdk.md +522 -0
- package/docs_opencode/sdk.pdf +0 -0
- package/docs_opencode/server.md +324 -0
- package/docs_opencode/server.pdf +0 -0
- package/docs_opencode/skills.md +235 -0
- package/docs_opencode/skills.pdf +0 -0
- package/docs_opencode/themes.md +378 -0
- package/docs_opencode/themes.pdf +0 -0
- package/docs_opencode/tools.md +364 -0
- package/docs_opencode/tools.pdf +0 -0
- package/index.js +1 -0
- package/package.json +1 -1
- package/tui.json +6 -0
- package/eslint.config.js +0 -51
- package/tests/static/dom_structure.test.js +0 -57
- package/tests/static/tag_balance.test.js +0 -78
- package/tests/unit/harness_structure.test.js +0 -65
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode-ai/plugin"
|
|
2
|
+
import fs from "fs"
|
|
3
|
+
import path from "path"
|
|
4
|
+
|
|
5
|
+
export const SddCorePlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
|
|
6
|
+
return {
|
|
7
|
+
"experimental.session.compacting": async (input, output) => {
|
|
8
|
+
const projectRoot = worktree || directory;
|
|
9
|
+
const lockfilePath = path.join(projectRoot, ".openspec/sdd-lock.json");
|
|
10
|
+
const altLockPath = path.join(projectRoot, "openspec/sdd-lock.json");
|
|
11
|
+
const activeLockPath = fs.existsSync(lockfilePath) ? lockfilePath : (fs.existsSync(altLockPath) ? altLockPath : null);
|
|
12
|
+
|
|
13
|
+
let lockInfo = "No active change or SDD phase detected.";
|
|
14
|
+
let amnesiaInstruction = "";
|
|
15
|
+
|
|
16
|
+
if (activeLockPath) {
|
|
17
|
+
try {
|
|
18
|
+
const lockObj = JSON.parse(fs.readFileSync(activeLockPath, "utf-8"));
|
|
19
|
+
lockInfo = `
|
|
20
|
+
### SDD SWARM ACTIVE STATUS (DO NOT FORGET)
|
|
21
|
+
- Active Change: \`${lockObj.change_name || "Ninguno"}\`
|
|
22
|
+
- Active Phase: \`${lockObj.active_phase ?? 0}\` (@${lockObj.active_subagent || "sdd-planner"})
|
|
23
|
+
- Status: \`${lockObj.status || "idle"}\`
|
|
24
|
+
- Iteration: \`${lockObj.iteration || 0}\`
|
|
25
|
+
- Last Updated: \`${lockObj.last_updated || ""}\`
|
|
26
|
+
- Complexity: \`${lockObj.complexity || "high"}\`
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
// Si ya estamos en Fase 2 (Construcción) o posterior, instruimos al compactador a purgar todo el chat del planner anterior
|
|
30
|
+
if (lockObj.active_phase >= 2) {
|
|
31
|
+
amnesiaInstruction = `
|
|
32
|
+
5. AMNESIA DE PLANIFICACIÓN [CRÍTICO]: Since we are already in Phase ${lockObj.active_phase} (Implementation/Closure) and the technical spec is approved, you MUST completely purge all conversational Q&A and planning discussions from Phase 1. Condense all Phase 1 conversations into a single high-density status line (e.g. "[Phase 1 Q&A resolved. Spec approved: see specs/spec.md]"). Focus 100% of the active context on code files, logical edits, and test validation logs.`;
|
|
33
|
+
}
|
|
34
|
+
} catch (e) {}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 1. Inyectar estado activo en el contexto de compactación para preservar memoria metodológica
|
|
38
|
+
output.context.push(lockInfo);
|
|
39
|
+
|
|
40
|
+
// 2. Personalizar el prompt de compactación para recortar logs redundantes de terminal y discusiones previas de planificación
|
|
41
|
+
output.prompt = `
|
|
42
|
+
You are generating a highly condensed continuation prompt for an active Spec-Driven Development (SDD) multi-agent session.
|
|
43
|
+
|
|
44
|
+
To maximize token savings and avoid long latency, you MUST:
|
|
45
|
+
1. Summarize any long command terminal output, compiler errors, or test logs into a single high-density line (e.g., "[Linter: 3 syntax errors resolved in auth.ts]" or "[Vitest: 12 tests passed, 0 failed in 1.2s]"). Do NOT repeat full traceback logs.
|
|
46
|
+
2. Keep the current task status, active file modifications, and direct goals in a concise, bulleted list.
|
|
47
|
+
3. Keep the active SDD phase and change name verbatim.
|
|
48
|
+
4. Keep the overall continuation prompt extremely brief (under 15 lines if possible).${amnesiaInstruction}
|
|
49
|
+
`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default SddCorePlugin;
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
|
3
|
+
import { createSignal, onCleanup } from "solid-js"
|
|
4
|
+
import fs from "fs"
|
|
5
|
+
import path from "path"
|
|
6
|
+
|
|
7
|
+
const PluginTuiSidebar: TuiPlugin = async (api) => {
|
|
8
|
+
api.slots.register({
|
|
9
|
+
order: 100,
|
|
10
|
+
slots: {
|
|
11
|
+
sidebar_content(_ctx, props: { session_id: string; children?: any }) {
|
|
12
|
+
// --- Estado reactivo y Polling de IDs de Sesión ---
|
|
13
|
+
const [sessionIds, setSessionIds] = createSignal<string[]>([props.session_id])
|
|
14
|
+
|
|
15
|
+
// --- Helper para leer el progreso SDD ---
|
|
16
|
+
const getSddProgress = () => {
|
|
17
|
+
try {
|
|
18
|
+
const lockPath = path.join(process.cwd(), ".openspec/sdd-lock.json")
|
|
19
|
+
const altPath = path.join(process.cwd(), "openspec/sdd-lock.json")
|
|
20
|
+
const actualPath = fs.existsSync(lockPath) ? lockPath : (fs.existsSync(altPath) ? altPath : null)
|
|
21
|
+
|
|
22
|
+
if (actualPath) {
|
|
23
|
+
const data = JSON.parse(fs.readFileSync(actualPath, "utf-8"))
|
|
24
|
+
return {
|
|
25
|
+
changeName: data.change_name || "Ninguno",
|
|
26
|
+
activePhase: typeof data.active_phase === "number" ? data.active_phase : 0,
|
|
27
|
+
status: data.status || "idle",
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
} catch (e) { }
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Función para actualizar recursivamente la lista de sesión IDs usando api.client
|
|
35
|
+
const updateSessionIds = async () => {
|
|
36
|
+
try {
|
|
37
|
+
const results: string[] = []
|
|
38
|
+
const visited = new Set<string>()
|
|
39
|
+
|
|
40
|
+
async function traverse(sid: string) {
|
|
41
|
+
if (!sid || visited.has(sid)) return
|
|
42
|
+
visited.add(sid)
|
|
43
|
+
results.push(sid)
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const response = await api.client.session.children({ sessionID: sid })
|
|
47
|
+
const children = Array.isArray(response) ? response : (response?.data || [])
|
|
48
|
+
for (const child of children) {
|
|
49
|
+
const childId = child?.id
|
|
50
|
+
if (childId) {
|
|
51
|
+
await traverse(childId)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch { }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await traverse(props.session_id)
|
|
58
|
+
|
|
59
|
+
// Solo actualizamos el signal si el conjunto de IDs cambió, para evitar re-renderizados innecesarios
|
|
60
|
+
if (JSON.stringify(results) !== JSON.stringify(sessionIds())) {
|
|
61
|
+
setSessionIds(results)
|
|
62
|
+
}
|
|
63
|
+
} catch { }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function extractAgentName(messages: any[], sessionInfo: any, sessionId: string): string {
|
|
67
|
+
const userMsg = messages.find((m) => m.role === "user")
|
|
68
|
+
if (userMsg && "agent" in userMsg && userMsg.agent) {
|
|
69
|
+
return userMsg.agent
|
|
70
|
+
}
|
|
71
|
+
if (sessionInfo?.title) {
|
|
72
|
+
return sessionInfo.title
|
|
73
|
+
}
|
|
74
|
+
return `Sesión ${sessionId.slice(0, 4)}`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function formatCost(cost: number): string {
|
|
78
|
+
if (cost === 0) return "$0"
|
|
79
|
+
if (cost < 0.001) return "$" + cost.toFixed(4)
|
|
80
|
+
return "$" + cost.toFixed(3)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function formatTokens(num: number): string {
|
|
84
|
+
if (num >= 1000000) return Math.round(num / 1000000) + "M"
|
|
85
|
+
if (num >= 1000) return Math.round(num / 1000) + "k"
|
|
86
|
+
return num.toString()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface AgentMetrics {
|
|
90
|
+
name: string
|
|
91
|
+
cost: number
|
|
92
|
+
tokensInput: number
|
|
93
|
+
tokensOutput: number
|
|
94
|
+
isSubagent: boolean
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface TotalMetrics {
|
|
98
|
+
agents: AgentMetrics[]
|
|
99
|
+
totalCost: number
|
|
100
|
+
totalInput: number
|
|
101
|
+
totalOutput: number
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getMetrics(currentSessionIds: string[]): TotalMetrics {
|
|
105
|
+
let totalCost = 0
|
|
106
|
+
let totalInput = 0
|
|
107
|
+
let totalOutput = 0
|
|
108
|
+
|
|
109
|
+
const agentMap: Record<string, AgentMetrics> = {}
|
|
110
|
+
|
|
111
|
+
for (const sid of currentSessionIds) {
|
|
112
|
+
const isSubagent = sid !== props.session_id
|
|
113
|
+
let messages: any[] = []
|
|
114
|
+
try {
|
|
115
|
+
messages = api.state.session.messages(sid) || []
|
|
116
|
+
} catch { }
|
|
117
|
+
|
|
118
|
+
let sessionInfo: any = null
|
|
119
|
+
try {
|
|
120
|
+
sessionInfo = api.state.session.get?.(sid)
|
|
121
|
+
} catch { }
|
|
122
|
+
|
|
123
|
+
const defaultAgentName = extractAgentName(messages, sessionInfo, sid)
|
|
124
|
+
|
|
125
|
+
for (let i = 0; i < messages.length; i++) {
|
|
126
|
+
const msg = messages[i]
|
|
127
|
+
if (msg.role === "assistant" && "cost" in msg) {
|
|
128
|
+
const cost = typeof msg.cost === "number" && Number.isFinite(msg.cost) ? msg.cost : 0
|
|
129
|
+
const input = msg.tokens?.input ?? 0
|
|
130
|
+
const output = msg.tokens?.output ?? 0
|
|
131
|
+
|
|
132
|
+
totalCost += cost
|
|
133
|
+
totalInput += input
|
|
134
|
+
totalOutput += output
|
|
135
|
+
|
|
136
|
+
let agentName = ""
|
|
137
|
+
if (msg.agent) {
|
|
138
|
+
agentName = msg.agent
|
|
139
|
+
} else {
|
|
140
|
+
for (let j = i; j >= 0; j--) {
|
|
141
|
+
if (messages[j]?.agent) {
|
|
142
|
+
agentName = messages[j].agent
|
|
143
|
+
break
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!agentName) {
|
|
149
|
+
agentName = defaultAgentName
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!agentMap[agentName]) {
|
|
153
|
+
agentMap[agentName] = {
|
|
154
|
+
name: agentName,
|
|
155
|
+
cost: 0,
|
|
156
|
+
tokensInput: 0,
|
|
157
|
+
tokensOutput: 0,
|
|
158
|
+
isSubagent: isSubagent,
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
agentMap[agentName].cost += cost
|
|
163
|
+
agentMap[agentName].tokensInput += input
|
|
164
|
+
agentMap[agentName].tokensOutput += output
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!agentMap[defaultAgentName]) {
|
|
169
|
+
agentMap[defaultAgentName] = {
|
|
170
|
+
name: defaultAgentName,
|
|
171
|
+
cost: 0,
|
|
172
|
+
tokensInput: 0,
|
|
173
|
+
tokensOutput: 0,
|
|
174
|
+
isSubagent: isSubagent,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const agents = Object.values(agentMap)
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
agents,
|
|
183
|
+
totalCost,
|
|
184
|
+
totalInput,
|
|
185
|
+
totalOutput,
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// --- Estado reactivo y Polling ---
|
|
190
|
+
const [metrics, setMetrics] = createSignal<TotalMetrics>(getMetrics([props.session_id]))
|
|
191
|
+
const [sddProgress, setSddProgress] = createSignal<{ changeName: string; activePhase: number; status: string } | null>(getSddProgress())
|
|
192
|
+
const [colorIndex, setColorIndex] = createSignal(0)
|
|
193
|
+
|
|
194
|
+
// Actualizamos los IDs de las sesiones cada 2 segundos
|
|
195
|
+
const idsInterval = setInterval(() => {
|
|
196
|
+
updateSessionIds()
|
|
197
|
+
setSddProgress(getSddProgress())
|
|
198
|
+
}, 2000)
|
|
199
|
+
|
|
200
|
+
// Actualizamos las métricas cada segundo basadas en el signal de sesión IDs
|
|
201
|
+
const metricsInterval = setInterval(() => {
|
|
202
|
+
setMetrics(getMetrics(sessionIds()))
|
|
203
|
+
}, 1000)
|
|
204
|
+
|
|
205
|
+
// Ticker lento para una animación de ola vertical suave
|
|
206
|
+
const colorInterval = setInterval(() => {
|
|
207
|
+
setColorIndex((prev) => (prev + 1) % 100)
|
|
208
|
+
}, 500)
|
|
209
|
+
|
|
210
|
+
// Paleta premium de naranjas fuertes, ámbar y cobrizos
|
|
211
|
+
const orangePalette = [
|
|
212
|
+
"#E04F00", // Naranjo oscuro / Fuego
|
|
213
|
+
"#FF7300", // Naranjo brillante / Ámbar
|
|
214
|
+
"#B33600", // Siena tostado / Óxido
|
|
215
|
+
"#FF8C00", // Ámbar oscuro
|
|
216
|
+
]
|
|
217
|
+
|
|
218
|
+
const getLineColor = (lineIdx: number) => {
|
|
219
|
+
const targetIndex = (colorIndex() + lineIdx) % orangePalette.length
|
|
220
|
+
return orangePalette[targetIndex]
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const logoLines = [
|
|
224
|
+
"███████╗██╗ ██╗ ██████╗ ███████╗",
|
|
225
|
+
"╚══███╔╝██║ ██║██╔════╝ ╚══███╔╝",
|
|
226
|
+
" ███╔╝ ██║ ██║██║ ███╗ ███╔╝ ",
|
|
227
|
+
" ███╔╝ ██║ ██║██║ ██║ ███╔╝ ",
|
|
228
|
+
"███████╗╚██████╔╝╚██████╔╝███████╗",
|
|
229
|
+
"╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝"
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
// Ejecutar inmediatamente al inicio
|
|
233
|
+
updateSessionIds()
|
|
234
|
+
setSddProgress(getSddProgress())
|
|
235
|
+
|
|
236
|
+
onCleanup(() => {
|
|
237
|
+
clearInterval(idsInterval)
|
|
238
|
+
clearInterval(metricsInterval)
|
|
239
|
+
clearInterval(colorInterval)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<box gap={0}>
|
|
244
|
+
{/* Cabecera Logo ZUGZ con Efecto Ola Vertical Naranja */}
|
|
245
|
+
<box gap={0} paddingTop={1} paddingLeft={1}>
|
|
246
|
+
{logoLines.map((line, idx) => (
|
|
247
|
+
<text fg={getLineColor(idx)}>
|
|
248
|
+
{line}
|
|
249
|
+
</text>
|
|
250
|
+
))}
|
|
251
|
+
</box>
|
|
252
|
+
|
|
253
|
+
{/* Componente Visual de Progreso SDD de 3 Fases */}
|
|
254
|
+
{sddProgress() && (
|
|
255
|
+
<box gap={0} paddingLeft={1} paddingTop={1} paddingBottom={0}>
|
|
256
|
+
<text fg="#FF7300">
|
|
257
|
+
{`SDD: ${sddProgress()?.changeName ?? ""}`}
|
|
258
|
+
</text>
|
|
259
|
+
<box gap={0} paddingTop={1}>
|
|
260
|
+
{[
|
|
261
|
+
{ id: 0, name: "F0 Exploración", agent: "@sdd-explorer" },
|
|
262
|
+
{ id: 1, name: "F1 Planificación", agent: "@sdd-planner" },
|
|
263
|
+
{ id: 2, name: "F2 Construcción", agent: "@sdd-builder" },
|
|
264
|
+
{ id: 3, name: "F3 Pruebas", agent: "@sdd-tester" },
|
|
265
|
+
{ id: 4, name: "F4 Deploy", agent: "@sdd-deployer" },
|
|
266
|
+
{ id: 5, name: "F5 Cierre", agent: "@sdd-archiver" }
|
|
267
|
+
].map((ph) => {
|
|
268
|
+
const isActive = sddProgress()?.activePhase === ph.id
|
|
269
|
+
const isCompleted = (sddProgress()?.activePhase ?? 0) > ph.id
|
|
270
|
+
|
|
271
|
+
let prefix = " ○ "
|
|
272
|
+
let fgColor = api.theme.current.textMuted
|
|
273
|
+
|
|
274
|
+
if (isCompleted) {
|
|
275
|
+
prefix = " ✓ "
|
|
276
|
+
fgColor = api.theme.current.success
|
|
277
|
+
} else if (isActive) {
|
|
278
|
+
prefix = " ⚡ "
|
|
279
|
+
fgColor = "#FF7300"
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return (
|
|
283
|
+
<text fg={fgColor}>
|
|
284
|
+
{`${prefix}${ph.name} ${ph.agent}`}
|
|
285
|
+
</text>
|
|
286
|
+
)
|
|
287
|
+
})}
|
|
288
|
+
</box>
|
|
289
|
+
<text fg={api.theme.current.borderSubtle} paddingTop={1}>
|
|
290
|
+
{"────────────────────────────────────"}
|
|
291
|
+
</text>
|
|
292
|
+
</box>
|
|
293
|
+
)}
|
|
294
|
+
|
|
295
|
+
{/* Monitor de Agentes Compacto y Plano (Efecto Sándwich) */}
|
|
296
|
+
<box gap={0} paddingLeft={1} paddingTop={1} paddingBottom={0}>
|
|
297
|
+
{metrics().agents.map((agent) => (
|
|
298
|
+
<text fg={agent.isSubagent ? api.theme.current.textMuted : api.theme.current.text}>
|
|
299
|
+
{`${agent.isSubagent ? " " : ""}${agent.name}: ${formatCost(agent.cost)} (${formatTokens(agent.tokensInput)}/${formatTokens(agent.tokensOutput)})`}
|
|
300
|
+
</text>
|
|
301
|
+
))}
|
|
302
|
+
<text fg={api.theme.current.borderSubtle}>
|
|
303
|
+
{"────────────────────────────────────"}
|
|
304
|
+
</text>
|
|
305
|
+
<text fg={api.theme.current.success}>
|
|
306
|
+
{`Total: ${formatCost(metrics().totalCost)} (${formatTokens(metrics().totalInput)}/${formatTokens(metrics().totalOutput)})`}
|
|
307
|
+
</text>
|
|
308
|
+
</box>
|
|
309
|
+
|
|
310
|
+
{/* Chat original de OpenCode */}
|
|
311
|
+
{props.children}
|
|
312
|
+
</box>
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export default { id: "plugin_tui", tui: PluginTuiSidebar } satisfies TuiPluginModule & { id: string }
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Skill: Dependency Cooldown Checker
|
|
2
|
+
|
|
3
|
+
Verifica que las dependencias tengan al menos **3 días** de publicadas antes de ser importadas.
|
|
4
|
+
|
|
5
|
+
## Trigger
|
|
6
|
+
|
|
7
|
+
Cuando se requiera agregar una nueva dependencia npm, antes de cualquier `import` o `require`.
|
|
8
|
+
|
|
9
|
+
## Uso
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx -y npm-check-updates <package-name> --dep prod
|
|
13
|
+
# o verificar en https://www.npmjs.com/package/<package-name>
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Regla SDD Cooldown
|
|
17
|
+
|
|
18
|
+
| Tipo | Tiempo mínimo |
|
|
19
|
+
|:---|:---|
|
|
20
|
+
| Dependencias nuevas | 3 días desde publicación |
|
|
21
|
+
| Updates mayores (major) | 7 días |
|
|
22
|
+
| Dependencias критични (security) | Sin cooldown |
|
|
23
|
+
|
|
24
|
+
## Verificación Manual
|
|
25
|
+
|
|
26
|
+
1. Ir a https://www.npmjs.com/package/<nombre-paquete>
|
|
27
|
+
2. Buscar "Published" en la sección de metadata
|
|
28
|
+
3. Calcular días desde publicación
|
|
29
|
+
4. Si >= 3 días, APPROVED
|
|
30
|
+
5. Si < 3 días, WAIT con fecha de aprobación
|
|
31
|
+
|
|
32
|
+
## Criterios de Bloqueo
|
|
33
|
+
|
|
34
|
+
- ❌ Paquetes con < 3 días
|
|
35
|
+
- ❌ Paquetes sin downloads recientes (posible abandonware)
|
|
36
|
+
- ❌ Paquetes con vulnerabilities conocidas sin fix
|
|
37
|
+
|
|
38
|
+
## Tags
|
|
39
|
+
|
|
40
|
+
#sdd #dependency #npm #cooldown
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Skill: SDD Tree Generator
|
|
2
|
+
|
|
3
|
+
Genera un árbol de estructura del proyecto en milisegundos sin costo de tokens.
|
|
4
|
+
|
|
5
|
+
## Trigger
|
|
6
|
+
|
|
7
|
+
Cuando se requiera visualizar la estructura de archivos del proyecto.
|
|
8
|
+
|
|
9
|
+
## Uso
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
node .opencode/tools/sdd_generate_tree.js
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
O desde OpenCode usando la herramienta `sdd_generate_tree`.
|
|
16
|
+
|
|
17
|
+
## Output
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
proyecto/
|
|
21
|
+
├── src/
|
|
22
|
+
│ ├── components/
|
|
23
|
+
│ │ └── Button.tsx
|
|
24
|
+
│ └── index.ts
|
|
25
|
+
├── tests/
|
|
26
|
+
│ └── unit/
|
|
27
|
+
├── package.json
|
|
28
|
+
└── README.md
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Características
|
|
32
|
+
|
|
33
|
+
- Profundidad máxima: 3 niveles
|
|
34
|
+
- Ignora: node_modules, .git, archivos ocultos
|
|
35
|
+
- Muestra tamaño de archivos
|
|
36
|
+
- Ejecución en < 100ms
|
|
37
|
+
|
|
38
|
+
## Tags
|
|
39
|
+
|
|
40
|
+
#sdd #tooling #tree #structure
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
export function today() {
|
|
4
|
+
return new Date().toISOString().split("T")[0];
|
|
5
|
+
}
|
|
6
|
+
export function nextId(entries) {
|
|
7
|
+
let max = 0;
|
|
8
|
+
for (const e of entries) {
|
|
9
|
+
const num = parseInt(e.id.substring(1), 10);
|
|
10
|
+
if (!isNaN(num) && num > max)
|
|
11
|
+
max = num;
|
|
12
|
+
}
|
|
13
|
+
return `L${String(max + 1).padStart(3, "0")}`;
|
|
14
|
+
}
|
|
15
|
+
export function parseEntries(content) {
|
|
16
|
+
const entries = [];
|
|
17
|
+
const blocks = content.split("\n### ");
|
|
18
|
+
for (const block of blocks) {
|
|
19
|
+
if (!block.trim())
|
|
20
|
+
continue;
|
|
21
|
+
const lines = block.split("\n");
|
|
22
|
+
const header = lines[0].trim();
|
|
23
|
+
const colonIdx = header.indexOf(": ");
|
|
24
|
+
if (colonIdx === -1)
|
|
25
|
+
continue;
|
|
26
|
+
const id = header.substring(0, colonIdx).trim();
|
|
27
|
+
if (!id || !/^L\d{3}$/.test(id))
|
|
28
|
+
continue;
|
|
29
|
+
const tag = header.substring(colonIdx + 2).trim();
|
|
30
|
+
if (!tag)
|
|
31
|
+
continue;
|
|
32
|
+
let category = "";
|
|
33
|
+
let problem = "";
|
|
34
|
+
let solution = "";
|
|
35
|
+
let date = "";
|
|
36
|
+
for (const line of lines) {
|
|
37
|
+
const t = line.trim();
|
|
38
|
+
if (t.startsWith("- **Tags**:")) {
|
|
39
|
+
const m = t.match(/#(\w+)/);
|
|
40
|
+
if (m)
|
|
41
|
+
category = m[1];
|
|
42
|
+
}
|
|
43
|
+
else if (t.startsWith("- **Problema**:")) {
|
|
44
|
+
problem = t.substring("- **Problema**: ".length).trim();
|
|
45
|
+
}
|
|
46
|
+
else if (t.startsWith("- **Solución**:")) {
|
|
47
|
+
solution = t.substring("- **Solución**: ".length).trim();
|
|
48
|
+
}
|
|
49
|
+
else if (t.startsWith("- **Fecha**:")) {
|
|
50
|
+
date = t.substring("- **Fecha**: ".length).trim();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (id && problem) {
|
|
54
|
+
entries.push({ id, category, tag, problem, solution, date: date || today() });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return entries;
|
|
58
|
+
}
|
|
59
|
+
export function buildIndex(entries) {
|
|
60
|
+
if (entries.length === 0)
|
|
61
|
+
return "_No hay lecciones registradas todavía._";
|
|
62
|
+
const header = "| ID | Categoría | Tag | Problema |\n| :--- | :--- | :--- | :--- |\n";
|
|
63
|
+
const rows = entries.map(e => {
|
|
64
|
+
const problemTrunc = e.problem.length > 55 ? e.problem.slice(0, 52) + "..." : e.problem;
|
|
65
|
+
return `| ${e.id} | ${e.category || "-"} | ${e.tag} | ${problemTrunc} |`;
|
|
66
|
+
}).join("\n");
|
|
67
|
+
return header + rows;
|
|
68
|
+
}
|
|
69
|
+
export function buildEntryBlock(e) {
|
|
70
|
+
const tags = `#${e.category || "general"} #${e.tag.replace(/[-\s]/g, "_")}`;
|
|
71
|
+
return [
|
|
72
|
+
`### ${e.id}: ${e.tag}`,
|
|
73
|
+
`- **Tags**: ${tags}`,
|
|
74
|
+
`- **Problema**: ${e.problem}`,
|
|
75
|
+
`- **Solución**: ${e.solution}`,
|
|
76
|
+
`- **Fecha**: ${e.date}`,
|
|
77
|
+
].join("\n") + "\n";
|
|
78
|
+
}
|
|
79
|
+
export function buildFullBrain(entries) {
|
|
80
|
+
const lines = [
|
|
81
|
+
"# 🧠 Cerebro del Proyecto",
|
|
82
|
+
"",
|
|
83
|
+
"> Base de conocimiento técnico a largo plazo. Solo registra aprendizajes de alto valor y no triviales.",
|
|
84
|
+
"",
|
|
85
|
+
"## Índice",
|
|
86
|
+
"",
|
|
87
|
+
buildIndex(entries),
|
|
88
|
+
"",
|
|
89
|
+
"## Lecciones",
|
|
90
|
+
"",
|
|
91
|
+
];
|
|
92
|
+
for (const e of entries) {
|
|
93
|
+
lines.push(buildEntryBlock(e));
|
|
94
|
+
}
|
|
95
|
+
return lines.join("\n");
|
|
96
|
+
}
|
|
97
|
+
export function readBrainFile(brainPath) {
|
|
98
|
+
if (!fs.existsSync(brainPath)) {
|
|
99
|
+
return { entries: [] };
|
|
100
|
+
}
|
|
101
|
+
const content = fs.readFileSync(brainPath, "utf-8");
|
|
102
|
+
const leccionesIdx = content.indexOf("## Lecciones");
|
|
103
|
+
const leccionesContent = leccionesIdx >= 0
|
|
104
|
+
? content.substring(leccionesIdx)
|
|
105
|
+
: content;
|
|
106
|
+
const entries = parseEntries(leccionesContent);
|
|
107
|
+
return { entries };
|
|
108
|
+
}
|
|
109
|
+
export function writeBrainFile(brainPath, entries) {
|
|
110
|
+
fs.mkdirSync(path.dirname(brainPath), { recursive: true });
|
|
111
|
+
fs.writeFileSync(brainPath, buildFullBrain(entries), "utf-8");
|
|
112
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
export default tool({
|
|
3
|
+
description: "Audita programáticamente si una dependencia de terceros cumple con la regla de estabilidad y seguridad de cooldown (mínimo 3 días / 4320 minutos de antigüedad). Consulta APIs reales de NPM y PyPI.",
|
|
4
|
+
args: {
|
|
5
|
+
package: tool.schema.string().describe("Nombre del paquete o dependencia (ej: 'lodash', 'fastapi')"),
|
|
6
|
+
version: tool.schema.string().optional().describe("Versión del paquete a auditar. Si no se especifica, audita la última versión."),
|
|
7
|
+
ecosystem: tool.schema.enum(["npm", "pypi"]).optional().describe("El ecosistema del paquete. Por defecto se infiere del stack.")
|
|
8
|
+
},
|
|
9
|
+
async execute(args) {
|
|
10
|
+
const pkg = args.package;
|
|
11
|
+
let targetVersion = args.version;
|
|
12
|
+
let eco = args.ecosystem;
|
|
13
|
+
// Inferir ecosistema si no está presente
|
|
14
|
+
if (!eco) {
|
|
15
|
+
if (pkg.startsWith("@") || pkg.includes("/") || pkg === "lodash" || pkg === "axios" || pkg === "express") {
|
|
16
|
+
eco = "npm";
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
// Enfoque por defecto: intentar NPM primero, luego PyPI
|
|
20
|
+
eco = "npm";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
if (eco === "npm") {
|
|
25
|
+
const url = `https://registry.npmjs.org/${encodeURIComponent(pkg)}`;
|
|
26
|
+
const res = await fetch(url);
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
// Si falla NPM, chequear si es PyPI
|
|
29
|
+
if (!args.ecosystem) {
|
|
30
|
+
return await checkPyPI(pkg, targetVersion);
|
|
31
|
+
}
|
|
32
|
+
return `[Cooldown Blocked] Error: No se encontró el paquete '${pkg}' en NPM.`;
|
|
33
|
+
}
|
|
34
|
+
const data = await res.json();
|
|
35
|
+
// Obtener última versión si no se provee
|
|
36
|
+
if (!targetVersion) {
|
|
37
|
+
targetVersion = data["dist-tags"]?.latest;
|
|
38
|
+
}
|
|
39
|
+
if (!targetVersion) {
|
|
40
|
+
return `[Cooldown Blocked] Error: No se pudo resolver la versión para el paquete '${pkg}' en NPM.`;
|
|
41
|
+
}
|
|
42
|
+
const publishTimeString = data.time?.[targetVersion];
|
|
43
|
+
if (!publishTimeString) {
|
|
44
|
+
return `[Cooldown Blocked] Error: No se encontró la versión '${targetVersion}' para '${pkg}' en NPM.`;
|
|
45
|
+
}
|
|
46
|
+
return evaluateCooldown(pkg, targetVersion, publishTimeString, "NPM");
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
return await checkPyPI(pkg, targetVersion);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
return `[Cooldown Warning] Error al consultar la API de dependencias: ${e.message || e}. Por seguridad, verifica manualmente.`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
async function checkPyPI(pkg, targetVersion) {
|
|
58
|
+
try {
|
|
59
|
+
const url = `https://pypi.org/pypi/${encodeURIComponent(pkg)}/json`;
|
|
60
|
+
const res = await fetch(url);
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
return `[Cooldown Blocked] Error: No se encontró el paquete '${pkg}' en NPM ni PyPI.`;
|
|
63
|
+
}
|
|
64
|
+
const data = await res.json();
|
|
65
|
+
// Resolver versión
|
|
66
|
+
const version = targetVersion || data.info?.version;
|
|
67
|
+
if (!version) {
|
|
68
|
+
return `[Cooldown Blocked] Error: No se pudo resolver la versión para '${pkg}' en PyPI.`;
|
|
69
|
+
}
|
|
70
|
+
const releases = data.releases?.[version];
|
|
71
|
+
if (!releases || releases.length === 0) {
|
|
72
|
+
return `[Cooldown Blocked] Error: No se encontró la versión '${version}' para '${pkg}' en PyPI.`;
|
|
73
|
+
}
|
|
74
|
+
// PyPI expone fecha de subida del primer archivo
|
|
75
|
+
const uploadTime = releases[0]?.upload_time_iso_8601 || releases[0]?.upload_time;
|
|
76
|
+
if (!uploadTime) {
|
|
77
|
+
return `[Cooldown Blocked] Error: No se encontró fecha de publicación para '${pkg}@${version}' en PyPI.`;
|
|
78
|
+
}
|
|
79
|
+
return evaluateCooldown(pkg, version, uploadTime, "PyPI");
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
return `[Cooldown Blocked] Error de conexión con PyPI: ${e.message || e}`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function evaluateCooldown(pkg, version, publishTimeStr, registryName) {
|
|
86
|
+
const publishTime = new Date(publishTimeStr);
|
|
87
|
+
const now = new Date();
|
|
88
|
+
const diffMs = now.getTime() - publishTime.getTime();
|
|
89
|
+
const diffMinutes = Math.floor(diffMs / (1000 * 60));
|
|
90
|
+
const cooldownRequired = 4320; // 3 días en minutos
|
|
91
|
+
const publishDateStr = publishTime.toISOString().split('T')[0];
|
|
92
|
+
if (diffMinutes < cooldownRequired) {
|
|
93
|
+
const remainingMinutes = cooldownRequired - diffMinutes;
|
|
94
|
+
const remainingHours = Math.ceil(remainingMinutes / 60);
|
|
95
|
+
const remainingDays = (remainingMinutes / 1440).toFixed(1);
|
|
96
|
+
return JSON.stringify({
|
|
97
|
+
status: "BLOCKED",
|
|
98
|
+
package: pkg,
|
|
99
|
+
version: version,
|
|
100
|
+
registry: registryName,
|
|
101
|
+
publishDate: publishDateStr,
|
|
102
|
+
ageMinutes: diffMinutes,
|
|
103
|
+
ageDays: (diffMinutes / 1440).toFixed(1),
|
|
104
|
+
cooldownMinutesRequired: cooldownRequired,
|
|
105
|
+
message: `❌ COOLDOWN BLOQUEADO: El paquete '${pkg}@${version}' fue publicado hace solo ${(diffMinutes / 1440).toFixed(1)} días (${diffMinutes} minutos) el ${publishDateStr}. Requiere superar los 3 días de estabilidad. Faltan aproximadamente ${remainingDays} días (${remainingHours} horas) para que expire el cooldown.`
|
|
106
|
+
}, null, 2);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
return JSON.stringify({
|
|
110
|
+
status: "APPROVED",
|
|
111
|
+
package: pkg,
|
|
112
|
+
version: version,
|
|
113
|
+
registry: registryName,
|
|
114
|
+
publishDate: publishDateStr,
|
|
115
|
+
ageMinutes: diffMinutes,
|
|
116
|
+
ageDays: (diffMinutes / 1440).toFixed(1),
|
|
117
|
+
cooldownMinutesRequired: cooldownRequired,
|
|
118
|
+
message: `✅ COOLDOWN APROBADO: El paquete '${pkg}@${version}' fue publicado el ${publishDateStr} (hace ${(diffMinutes / 1440).toFixed(1)} días). Cumple plenamente con la regla de estabilidad mínima de 3 días.`
|
|
119
|
+
}, null, 2);
|
|
120
|
+
}
|
|
121
|
+
}
|