superkit-mcp-server 1.1.1 → 1.1.3
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 +4 -0
- package/build/index.js +188 -0
- package/build/tools/__tests__/archTools.test.js +42 -0
- package/build/tools/__tests__/compoundTools.test.js +60 -0
- package/build/tools/__tests__/docsTools.test.js +44 -0
- package/build/tools/__tests__/gitTools.test.js +45 -0
- package/build/tools/__tests__/loggerTools.test.js +74 -0
- package/build/tools/__tests__/todoTools.test.js +73 -0
- package/build/tools/archTools.js +166 -0
- package/build/tools/compoundTools.js +334 -0
- package/build/tools/docsTools.js +198 -0
- package/build/tools/gitTools.js +196 -0
- package/build/tools/loggerTools.js +70 -0
- package/build/tools/todoTools.js +162 -0
- package/package.json +1 -1
- package/skills/meta/code-review/SKILL.md +1 -1
- package/skills/meta/compound-docs/SKILL.md +1 -1
- package/skills/meta/debug/SKILL.md +1 -1
- package/skills/meta/examples/supabase/SKILL.md +1 -1
- package/skills/meta/file-todos/SKILL.md +1 -1
- package/skills/meta/session-resume/SKILL.md +2 -2
- package/skills/meta/testing/SKILL.md +1 -1
- package/workflows/README.md +53 -41
- package/workflows/adr.md +4 -4
- package/workflows/changelog.md +3 -3
- package/workflows/compound.md +4 -4
- package/workflows/compound_health.md +4 -4
- package/workflows/create-agent-skill.md +3 -3
- package/workflows/cycle.md +4 -4
- package/workflows/deploy-docs.md +2 -2
- package/workflows/doc.md +3 -3
- package/workflows/explore.md +3 -3
- package/workflows/generate_command.md +2 -2
- package/workflows/heal-skill.md +3 -3
- package/workflows/housekeeping.md +14 -14
- package/workflows/plan-compound.md +4 -4
- package/workflows/plan_review.md +6 -6
- package/workflows/promote_pattern.md +2 -2
- package/workflows/release-docs.md +2 -2
- package/workflows/report-bug.md +5 -5
- package/workflows/reproduce-bug.md +4 -4
- package/workflows/resolve_pr.md +2 -2
- package/workflows/resolve_todo.md +3 -3
- package/workflows/review-compound.md +6 -6
- package/workflows/skill-review.md +4 -4
- package/workflows/specs.md +4 -4
- package/workflows/triage-sprint.md +2 -2
- package/workflows/triage.md +3 -3
- package/workflows/work.md +8 -8
- package/workflows/xcode-test.md +3 -3
package/README.md
CHANGED
|
@@ -32,6 +32,10 @@ npx -y superkit-mcp-server
|
|
|
32
32
|
- **`load_superkit_workflow`**: Loads the instructions for a specific slash-command workflow (e.g., `work`, `explore`).
|
|
33
33
|
- **`call_tool_checklist`**: Executes the native TypeScript validation suite (security, web accessibility, react performance, testing, API structure) on a target project location via the MCP environment instead of generic bash loops.
|
|
34
34
|
|
|
35
|
+
## Available Prompts
|
|
36
|
+
|
|
37
|
+
The Super-Kit MCP server exposes all workflows inside the `workflows/` directory dynamically as **MCP Prompts**. AI agents can invoke `GetPromptRequestSchema` to instantly load complex workflows (e.g., `plan-compound`, `review-compound`) along with their security and validation tooling straight into their context.
|
|
38
|
+
|
|
35
39
|
## Manual Installation and Configuration
|
|
36
40
|
|
|
37
41
|
If you cloned this repository locally, you can build and use the MCP server directly:
|
package/build/index.js
CHANGED
|
@@ -9,6 +9,12 @@ import { manageAutoPreview } from "./tools/autoPreview.js";
|
|
|
9
9
|
import { manageSession } from "./tools/sessionManager.js";
|
|
10
10
|
import { runChecklist } from "./tools/checklist.js";
|
|
11
11
|
import { runVerifyAll } from "./tools/verifyAll.js";
|
|
12
|
+
import { logSkill, logWorkflow, rotateLogs } from "./tools/loggerTools.js";
|
|
13
|
+
import { getNextTodoId, createTodo, startTodo, doneTodo, completeTodo } from "./tools/todoTools.js";
|
|
14
|
+
import { compoundSearch, updateSolutionRef, validateCompound, auditStateDrift, suggestSkills, compoundHealth, compoundDashboard, compoundMetrics } from "./tools/compoundTools.js";
|
|
15
|
+
import { bootstrapFolderDocs, checkDocsFreshness, discoverUndocumentedFolders, validateFolderDocs } from "./tools/docsTools.js";
|
|
16
|
+
import { generateChangelog, validateChangelog, archiveCompleted, prePushHousekeeping } from "./tools/gitTools.js";
|
|
17
|
+
import { validateSpecConsistency, completePlan, validateArchitecture, syncSpec, updateSpecPhase } from "./tools/archTools.js";
|
|
12
18
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
19
|
const __dirname = path.dirname(__filename);
|
|
14
20
|
const superKitRoot = path.resolve(__dirname, "../");
|
|
@@ -93,6 +99,100 @@ const TOOLS = [
|
|
|
93
99
|
required: ["projectPath", "url"]
|
|
94
100
|
}
|
|
95
101
|
},
|
|
102
|
+
{
|
|
103
|
+
name: "call_tool_logger_manager",
|
|
104
|
+
description: "Manages tool logging operations (skills, workflows, log rotation)",
|
|
105
|
+
inputSchema: {
|
|
106
|
+
type: "object",
|
|
107
|
+
properties: {
|
|
108
|
+
action: { type: "string", enum: ["logSkill", "logWorkflow", "rotateLogs"] },
|
|
109
|
+
name: { type: "string" },
|
|
110
|
+
outcome: { type: "string" },
|
|
111
|
+
projectPath: { type: "string", default: "." }
|
|
112
|
+
},
|
|
113
|
+
required: ["action", "projectPath"]
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: "call_tool_todo_manager",
|
|
118
|
+
description: "Manages todos (nextId, create, start, done, complete)",
|
|
119
|
+
inputSchema: {
|
|
120
|
+
type: "object",
|
|
121
|
+
properties: {
|
|
122
|
+
action: { type: "string", enum: ["nextId", "create", "start", "done", "complete"] },
|
|
123
|
+
title: { type: "string" },
|
|
124
|
+
description: { type: "string" },
|
|
125
|
+
priority: { type: "number" },
|
|
126
|
+
todoId: { type: "string" },
|
|
127
|
+
force: { type: "boolean", default: false },
|
|
128
|
+
projectPath: { type: "string", default: "." }
|
|
129
|
+
},
|
|
130
|
+
required: ["action", "projectPath"]
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: "call_tool_compound_manager",
|
|
135
|
+
description: "Manages compound knowledge capabilities (search, updateRef, validate, auditDrift, suggestSkills, health, dashboard, metrics)",
|
|
136
|
+
inputSchema: {
|
|
137
|
+
type: "object",
|
|
138
|
+
properties: {
|
|
139
|
+
action: { type: "string", enum: ["search", "updateRef", "validate", "auditDrift", "suggestSkills", "health", "dashboard", "metrics"] },
|
|
140
|
+
terms: { type: "array", items: { type: "string" } },
|
|
141
|
+
files: { type: "array", items: { type: "string" } },
|
|
142
|
+
fix: { type: "boolean", default: false },
|
|
143
|
+
force: { type: "boolean", default: false },
|
|
144
|
+
projectPath: { type: "string", default: "." }
|
|
145
|
+
},
|
|
146
|
+
required: ["action", "projectPath"]
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: "call_tool_docs_manager",
|
|
151
|
+
description: "Manages documentation (bootstrap, freshness, discover, validate)",
|
|
152
|
+
inputSchema: {
|
|
153
|
+
type: "object",
|
|
154
|
+
properties: {
|
|
155
|
+
action: { type: "string", enum: ["bootstrap", "freshness", "discover", "validate"] },
|
|
156
|
+
folder: { type: "string" },
|
|
157
|
+
skipDocs: { type: "boolean", default: false },
|
|
158
|
+
strict: { type: "boolean", default: false },
|
|
159
|
+
targetFolders: { type: "array", items: { type: "string" } },
|
|
160
|
+
projectPath: { type: "string", default: "." }
|
|
161
|
+
},
|
|
162
|
+
required: ["action", "projectPath"]
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: "call_tool_git_manager",
|
|
167
|
+
description: "Manages git and housekeeping tasks (changelog, validateChangelog, archive, housekeeping)",
|
|
168
|
+
inputSchema: {
|
|
169
|
+
type: "object",
|
|
170
|
+
properties: {
|
|
171
|
+
action: { type: "string", enum: ["changelog", "validateChangelog", "archive", "housekeeping"] },
|
|
172
|
+
applyFix: { type: "boolean", default: false },
|
|
173
|
+
projectPath: { type: "string", default: "." }
|
|
174
|
+
},
|
|
175
|
+
required: ["action", "projectPath"]
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: "call_tool_arch_manager",
|
|
180
|
+
description: "Manages architecture and specs tasks (validateSpecs, completePlan, validateArch, syncSpec, updatePhase)",
|
|
181
|
+
inputSchema: {
|
|
182
|
+
type: "object",
|
|
183
|
+
properties: {
|
|
184
|
+
action: { type: "string", enum: ["validateSpecs", "completePlan", "validateArch", "syncSpec", "updatePhase"] },
|
|
185
|
+
planFile: { type: "string" },
|
|
186
|
+
force: { type: "boolean", default: false },
|
|
187
|
+
specDir: { type: "string" },
|
|
188
|
+
specName: { type: "string" },
|
|
189
|
+
phaseNum: { type: "string" },
|
|
190
|
+
status: { type: "string" },
|
|
191
|
+
projectPath: { type: "string", default: "." }
|
|
192
|
+
},
|
|
193
|
+
required: ["action", "projectPath"]
|
|
194
|
+
}
|
|
195
|
+
},
|
|
96
196
|
{
|
|
97
197
|
name: "list_superkit_assets",
|
|
98
198
|
description: "Lists all available agents, skills, and workflows in the Super-Kit repository.",
|
|
@@ -225,6 +325,94 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
225
325
|
const res = await runVerifyAll(args.projectPath, args.url, args.skipE2E, args.stopOnFail);
|
|
226
326
|
return { content: [{ type: "text", text: res }] };
|
|
227
327
|
}
|
|
328
|
+
if (request.params.name === "call_tool_logger_manager") {
|
|
329
|
+
const args = request.params.arguments;
|
|
330
|
+
let res = "";
|
|
331
|
+
if (args.action === "logSkill")
|
|
332
|
+
res = await logSkill(args.name, args.outcome, args.projectPath);
|
|
333
|
+
else if (args.action === "logWorkflow")
|
|
334
|
+
res = await logWorkflow(args.name, args.outcome, args.projectPath);
|
|
335
|
+
else if (args.action === "rotateLogs")
|
|
336
|
+
res = await rotateLogs(args.projectPath);
|
|
337
|
+
return { content: [{ type: "text", text: res }] };
|
|
338
|
+
}
|
|
339
|
+
if (request.params.name === "call_tool_todo_manager") {
|
|
340
|
+
const args = request.params.arguments;
|
|
341
|
+
let res = "";
|
|
342
|
+
if (args.action === "nextId")
|
|
343
|
+
res = String(await getNextTodoId(args.projectPath));
|
|
344
|
+
else if (args.action === "create")
|
|
345
|
+
res = await createTodo(args.title, args.description, args.priority, args.projectPath);
|
|
346
|
+
else if (args.action === "start")
|
|
347
|
+
res = await startTodo(args.todoId, args.force, args.projectPath);
|
|
348
|
+
else if (args.action === "done")
|
|
349
|
+
res = await doneTodo(args.todoId, args.force, args.projectPath);
|
|
350
|
+
else if (args.action === "complete")
|
|
351
|
+
res = await completeTodo(args.todoId, args.force, args.projectPath);
|
|
352
|
+
return { content: [{ type: "text", text: res }] };
|
|
353
|
+
}
|
|
354
|
+
if (request.params.name === "call_tool_compound_manager") {
|
|
355
|
+
const args = request.params.arguments;
|
|
356
|
+
let res = "";
|
|
357
|
+
if (args.action === "search")
|
|
358
|
+
res = await compoundSearch(args.terms || [], args.projectPath);
|
|
359
|
+
else if (args.action === "updateRef")
|
|
360
|
+
res = await updateSolutionRef(args.files || [], args.projectPath);
|
|
361
|
+
else if (args.action === "validate")
|
|
362
|
+
res = await validateCompound(args.projectPath);
|
|
363
|
+
else if (args.action === "auditDrift")
|
|
364
|
+
res = await auditStateDrift(args.projectPath, args.fix);
|
|
365
|
+
else if (args.action === "suggestSkills")
|
|
366
|
+
res = await suggestSkills(args.projectPath);
|
|
367
|
+
else if (args.action === "health")
|
|
368
|
+
res = await compoundHealth(args.projectPath);
|
|
369
|
+
else if (args.action === "dashboard")
|
|
370
|
+
res = await compoundDashboard(args.projectPath);
|
|
371
|
+
else if (args.action === "metrics")
|
|
372
|
+
res = await compoundMetrics(args.projectPath, args.force);
|
|
373
|
+
return { content: [{ type: "text", text: res }] };
|
|
374
|
+
}
|
|
375
|
+
if (request.params.name === "call_tool_docs_manager") {
|
|
376
|
+
const args = request.params.arguments;
|
|
377
|
+
let res = "";
|
|
378
|
+
if (args.action === "bootstrap")
|
|
379
|
+
res = await bootstrapFolderDocs(args.folder, args.projectPath);
|
|
380
|
+
else if (args.action === "freshness")
|
|
381
|
+
res = await checkDocsFreshness(args.skipDocs, args.projectPath);
|
|
382
|
+
else if (args.action === "discover")
|
|
383
|
+
res = await discoverUndocumentedFolders(args.projectPath);
|
|
384
|
+
else if (args.action === "validate")
|
|
385
|
+
res = await validateFolderDocs(args.strict, args.targetFolders || [], args.projectPath);
|
|
386
|
+
return { content: [{ type: "text", text: res }] };
|
|
387
|
+
}
|
|
388
|
+
if (request.params.name === "call_tool_git_manager") {
|
|
389
|
+
const args = request.params.arguments;
|
|
390
|
+
let res = "";
|
|
391
|
+
if (args.action === "changelog")
|
|
392
|
+
res = await generateChangelog(args.projectPath);
|
|
393
|
+
else if (args.action === "validateChangelog")
|
|
394
|
+
res = await validateChangelog(args.projectPath);
|
|
395
|
+
else if (args.action === "archive")
|
|
396
|
+
res = await archiveCompleted(args.projectPath, args.applyFix);
|
|
397
|
+
else if (args.action === "housekeeping")
|
|
398
|
+
res = await prePushHousekeeping(args.projectPath, args.applyFix);
|
|
399
|
+
return { content: [{ type: "text", text: res }] };
|
|
400
|
+
}
|
|
401
|
+
if (request.params.name === "call_tool_arch_manager") {
|
|
402
|
+
const args = request.params.arguments;
|
|
403
|
+
let res = "";
|
|
404
|
+
if (args.action === "validateSpecs")
|
|
405
|
+
res = await validateSpecConsistency(args.projectPath);
|
|
406
|
+
else if (args.action === "completePlan")
|
|
407
|
+
res = await completePlan(args.planFile, args.force, args.projectPath);
|
|
408
|
+
else if (args.action === "validateArch")
|
|
409
|
+
res = await validateArchitecture(args.projectPath);
|
|
410
|
+
else if (args.action === "syncSpec")
|
|
411
|
+
res = await syncSpec(args.specDir, args.projectPath);
|
|
412
|
+
else if (args.action === "updatePhase")
|
|
413
|
+
res = await updateSpecPhase(args.specName, String(args.phaseNum), args.status, args.projectPath);
|
|
414
|
+
return { content: [{ type: "text", text: res }] };
|
|
415
|
+
}
|
|
228
416
|
if (request.params.name === "list_superkit_assets") {
|
|
229
417
|
const agentsPath = path.join(superKitRoot, "agents");
|
|
230
418
|
const skillsTechPath = path.join(superKitRoot, "skills", "tech");
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { completePlan, validateArchitecture } from '../archTools.js';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
describe('Arch Tools', () => {
|
|
7
|
+
let tempDir;
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'arch-test-'));
|
|
10
|
+
});
|
|
11
|
+
afterEach(async () => {
|
|
12
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
13
|
+
});
|
|
14
|
+
it('should complete a plan successfully', async () => {
|
|
15
|
+
const planPath = path.join(tempDir, 'plan1.md');
|
|
16
|
+
await fs.writeFile(planPath, '> Status: Draft\n\n- [x] Done item');
|
|
17
|
+
const res = await completePlan('plan1.md', false, tempDir);
|
|
18
|
+
expect(res).toContain('Plan marked as Implemented');
|
|
19
|
+
const content = await fs.readFile(planPath, 'utf8');
|
|
20
|
+
expect(content).toContain('> Status: Implemented');
|
|
21
|
+
});
|
|
22
|
+
it('should fail to complete a plan if unchecked items exist', async () => {
|
|
23
|
+
const planPath = path.join(tempDir, 'plan2.md');
|
|
24
|
+
await fs.writeFile(planPath, '> Status: Draft\n\n- [ ] Pending item');
|
|
25
|
+
const res = await completePlan('plan2.md', false, tempDir);
|
|
26
|
+
expect(res).toContain('unchecked acceptance criteria found');
|
|
27
|
+
});
|
|
28
|
+
it('should force complete a plan with unchecked items', async () => {
|
|
29
|
+
const planPath = path.join(tempDir, 'plan3.md');
|
|
30
|
+
await fs.writeFile(planPath, '> Status: Draft\n\n- [ ] Pending item');
|
|
31
|
+
const res = await completePlan('plan3.md', true, tempDir);
|
|
32
|
+
expect(res).toContain('Plan marked as Implemented');
|
|
33
|
+
});
|
|
34
|
+
it('should validate architecture counts', async () => {
|
|
35
|
+
const archDir = path.join(tempDir, 'docs', 'architecture');
|
|
36
|
+
await fs.mkdir(archDir, { recursive: true });
|
|
37
|
+
await fs.writeFile(path.join(archDir, 'compound-system.md'), '---\nskills: 1\nworkflows: 0\nscripts: 0\npatterns: 0\n---');
|
|
38
|
+
const res = await validateArchitecture(tempDir);
|
|
39
|
+
expect(res).toContain('Architecture Document is stale!');
|
|
40
|
+
expect(res).toContain('Skills mismatch');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { compoundSearch, updateSolutionRef, validateCompound, auditStateDrift } from '../compoundTools.js';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
describe('Compound Tools', () => {
|
|
7
|
+
let tempDir;
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'compound-test-'));
|
|
10
|
+
});
|
|
11
|
+
afterEach(async () => {
|
|
12
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
13
|
+
});
|
|
14
|
+
it('should search for solutions', async () => {
|
|
15
|
+
const solDir = path.join(tempDir, 'docs', 'solutions');
|
|
16
|
+
await fs.mkdir(solDir, { recursive: true });
|
|
17
|
+
await fs.writeFile(path.join(solDir, 'test.md'), '# Test Solution\n\nThis mentions React and Next.js.');
|
|
18
|
+
await fs.writeFile(path.join(solDir, 'test2.md'), '# Other Solution\n\nThis mentions Vue.');
|
|
19
|
+
const res = await compoundSearch(['React'], tempDir);
|
|
20
|
+
expect(res).toContain('test.md');
|
|
21
|
+
expect(res).toContain('Test Solution');
|
|
22
|
+
expect(res).not.toContain('test2.md');
|
|
23
|
+
});
|
|
24
|
+
it('should update solution ref', async () => {
|
|
25
|
+
const solDir = path.join(tempDir, 'docs', 'solutions');
|
|
26
|
+
await fs.mkdir(solDir, { recursive: true });
|
|
27
|
+
const filePath = path.join(solDir, 'test.md');
|
|
28
|
+
await fs.writeFile(filePath, '---\ntags: [test]\n---\n# Test Solution');
|
|
29
|
+
const res = await updateSolutionRef([path.relative(tempDir, filePath)], tempDir);
|
|
30
|
+
expect(res).toContain('Updated 1 files');
|
|
31
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
32
|
+
expect(content).toContain('last_referenced:');
|
|
33
|
+
});
|
|
34
|
+
it('should validate compound health', async () => {
|
|
35
|
+
const planPath = path.join(tempDir, 'implementation_plan.md');
|
|
36
|
+
await fs.writeFile(planPath, '- [ ] todo 1\n- [ ] todo 2\n');
|
|
37
|
+
const res = await validateCompound(tempDir);
|
|
38
|
+
expect(res).toContain('⚠️ Found 2 unchecked items in implementation_plan.md');
|
|
39
|
+
expect(res).toContain('❌ Validation failed.');
|
|
40
|
+
});
|
|
41
|
+
it('should audit state drift and report drift', async () => {
|
|
42
|
+
const todosDir = path.join(tempDir, 'todos');
|
|
43
|
+
await fs.mkdir(todosDir, { recursive: true });
|
|
44
|
+
const todoPath = path.join(todosDir, '001-pending-p1-test.md');
|
|
45
|
+
await fs.writeFile(todoPath, 'status: pending\n\n- [x] tick 1\n- [x] tick 2');
|
|
46
|
+
const res = await auditStateDrift(tempDir, false);
|
|
47
|
+
expect(res).toContain('DRIFT: 001-pending-p1-test.md');
|
|
48
|
+
expect(res).toContain('Checked: 2/2');
|
|
49
|
+
});
|
|
50
|
+
it('should audit state drift and fix drift', async () => {
|
|
51
|
+
const todosDir = path.join(tempDir, 'todos');
|
|
52
|
+
await fs.mkdir(todosDir, { recursive: true });
|
|
53
|
+
const todoPath = path.join(todosDir, '001-pending-p1-test.md');
|
|
54
|
+
await fs.writeFile(todoPath, 'status: pending\n\n- [x] tick 1\n- [x] tick 2');
|
|
55
|
+
const res = await auditStateDrift(tempDir, true);
|
|
56
|
+
expect(res).toContain('FIXED: 001-pending-p1-test.md');
|
|
57
|
+
const content = await fs.readFile(todoPath, 'utf8');
|
|
58
|
+
expect(content).toContain('status: done');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { bootstrapFolderDocs, checkDocsFreshness, discoverUndocumentedFolders, validateFolderDocs } from '../docsTools.js';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
describe('Docs Tools', () => {
|
|
7
|
+
let tempDir;
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'docs-test-'));
|
|
10
|
+
});
|
|
11
|
+
afterEach(async () => {
|
|
12
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
13
|
+
});
|
|
14
|
+
it('should bootstrap folder docs', async () => {
|
|
15
|
+
const targetDir = path.join(tempDir, 'src');
|
|
16
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
17
|
+
await fs.writeFile(path.join(targetDir, 'utils.ts'), 'content');
|
|
18
|
+
const result = await bootstrapFolderDocs('src', tempDir);
|
|
19
|
+
expect(result).toContain('Bootstrapped');
|
|
20
|
+
const readme = await fs.readFile(path.join(targetDir, 'README.md'), 'utf8');
|
|
21
|
+
expect(readme).toContain('# src');
|
|
22
|
+
expect(readme).toContain('`utils.ts`');
|
|
23
|
+
});
|
|
24
|
+
it('should discover undocumented folders', async () => {
|
|
25
|
+
const srcDir = path.join(tempDir, 'src');
|
|
26
|
+
await fs.mkdir(srcDir, { recursive: true });
|
|
27
|
+
await fs.writeFile(path.join(srcDir, 'logic.ts'), 'content');
|
|
28
|
+
// no readme -> should be discovered
|
|
29
|
+
const result = await discoverUndocumentedFolders(tempDir);
|
|
30
|
+
expect(result).toContain('src');
|
|
31
|
+
});
|
|
32
|
+
it('should validate folder docs', async () => {
|
|
33
|
+
const targetDir = path.join(tempDir, 'src');
|
|
34
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
35
|
+
await fs.writeFile(path.join(targetDir, 'README.md'), '# src\n');
|
|
36
|
+
const result = await validateFolderDocs(false, ['src'], tempDir);
|
|
37
|
+
expect(result).toContain('missing sections: Purpose, Components, Component Details, Changelog');
|
|
38
|
+
});
|
|
39
|
+
it('should check docs freshness without failing', async () => {
|
|
40
|
+
// Just tests the skip-docs flag since git repo isn't present
|
|
41
|
+
const result = await checkDocsFreshness(true, tempDir);
|
|
42
|
+
expect(result).toContain('Skipping documentation freshness check');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { validateChangelog, archiveCompleted, prePushHousekeeping } from '../gitTools.js';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
describe('Git Tools', () => {
|
|
7
|
+
let tempDir;
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'git-test-'));
|
|
10
|
+
});
|
|
11
|
+
afterEach(async () => {
|
|
12
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
13
|
+
});
|
|
14
|
+
it('should validate changelog', async () => {
|
|
15
|
+
const changelogPath = path.join(tempDir, 'CHANGELOG.md');
|
|
16
|
+
await fs.writeFile(changelogPath, '## [Unreleased]\n## [Unreleased]\n<<<<<<< HEAD');
|
|
17
|
+
const result = await validateChangelog(tempDir);
|
|
18
|
+
expect(result).toContain('Multiple [Unreleased] sections found');
|
|
19
|
+
expect(result).toContain('Merge conflict markers found');
|
|
20
|
+
});
|
|
21
|
+
it('should archive completed items in dry-run mode', async () => {
|
|
22
|
+
await fs.mkdir(path.join(tempDir, 'todos'), { recursive: true });
|
|
23
|
+
const todoPath = path.join(tempDir, 'todos', '001-done-test.md');
|
|
24
|
+
await fs.writeFile(todoPath, 'content');
|
|
25
|
+
const res = await archiveCompleted(tempDir, false);
|
|
26
|
+
expect(res).toContain('DRY-RUN');
|
|
27
|
+
expect(res).toContain('[ARCHIVED] 001-done-test.md');
|
|
28
|
+
// Still exists
|
|
29
|
+
await fs.access(todoPath);
|
|
30
|
+
});
|
|
31
|
+
it('should archive completed items and move them', async () => {
|
|
32
|
+
await fs.mkdir(path.join(tempDir, 'todos'), { recursive: true });
|
|
33
|
+
const todoPath = path.join(tempDir, 'todos', '001-done-test.md');
|
|
34
|
+
await fs.writeFile(todoPath, 'content');
|
|
35
|
+
const res = await archiveCompleted(tempDir, true);
|
|
36
|
+
expect(res).toContain('APPLYING CHANGES');
|
|
37
|
+
const archivedPath = path.join(tempDir, 'todos', 'archive', '001-done-test.md');
|
|
38
|
+
await fs.access(archivedPath); // Should not throw
|
|
39
|
+
});
|
|
40
|
+
it('should run pre-push housekeeping', async () => {
|
|
41
|
+
const res = await prePushHousekeeping(tempDir, false);
|
|
42
|
+
expect(res).toContain('Pre-Push Housekeeping Check');
|
|
43
|
+
expect(res).toContain('All checks passed');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { logSkill, logWorkflow, rotateLogs } from '../loggerTools.js';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
describe('Logger Tools', () => {
|
|
7
|
+
let tempDir;
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'logger-test-'));
|
|
10
|
+
});
|
|
11
|
+
afterEach(async () => {
|
|
12
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
13
|
+
vi.restoreAllMocks();
|
|
14
|
+
});
|
|
15
|
+
it('should log skill usage', async () => {
|
|
16
|
+
vi.useFakeTimers();
|
|
17
|
+
vi.setSystemTime(new Date('2026-03-04T00:00:00Z'));
|
|
18
|
+
const result = await logSkill('test-skill', 'manual', 'context', tempDir);
|
|
19
|
+
expect(result).toBe('Successfully logged skill usage for test-skill');
|
|
20
|
+
const logFile = path.join(tempDir, '.agent', 'logs', 'skill_usage.log');
|
|
21
|
+
const content = await fs.readFile(logFile, 'utf-8');
|
|
22
|
+
expect(content).toContain('2026-03-04T00:00:00Z|test-skill|manual|context\n');
|
|
23
|
+
vi.useRealTimers();
|
|
24
|
+
});
|
|
25
|
+
it('should log workflow usage', async () => {
|
|
26
|
+
vi.useFakeTimers();
|
|
27
|
+
vi.setSystemTime(new Date('2026-03-04T00:00:00Z'));
|
|
28
|
+
const result = await logWorkflow('test-workflow', 'session-123', tempDir);
|
|
29
|
+
expect(result).toBe('Successfully logged workflow usage for test-workflow');
|
|
30
|
+
const logFile = path.join(tempDir, '.agent', 'logs', 'workflow_usage.log');
|
|
31
|
+
const content = await fs.readFile(logFile, 'utf-8');
|
|
32
|
+
expect(content).toContain('2026-03-04T00:00:00Z|test-workflow|session-123\n');
|
|
33
|
+
vi.useRealTimers();
|
|
34
|
+
});
|
|
35
|
+
it('should generate a default session id if not provided', async () => {
|
|
36
|
+
vi.useFakeTimers();
|
|
37
|
+
vi.setSystemTime(new Date('2026-03-04T00:00:00Z')); // 1772582400 in seconds
|
|
38
|
+
await logWorkflow('test-workflow', '', tempDir);
|
|
39
|
+
const logFile = path.join(tempDir, '.agent', 'logs', 'workflow_usage.log');
|
|
40
|
+
const content = await fs.readFile(logFile, 'utf-8');
|
|
41
|
+
expect(content).toContain('2026-03-04T00:00:00Z|test-workflow|1772582400\n');
|
|
42
|
+
vi.useRealTimers();
|
|
43
|
+
});
|
|
44
|
+
it('should rotate logs older than retention days', async () => {
|
|
45
|
+
const logDir = path.join(tempDir, '.agent', 'logs');
|
|
46
|
+
await fs.mkdir(logDir, { recursive: true });
|
|
47
|
+
// Old log line (e.g. 100 days old)
|
|
48
|
+
const oldTimestamp = new Date();
|
|
49
|
+
oldTimestamp.setDate(oldTimestamp.getDate() - 100);
|
|
50
|
+
const oldStr = oldTimestamp.toISOString().replace(/\.[0-9]{3}Z$/, 'Z');
|
|
51
|
+
// New log line (10 days old)
|
|
52
|
+
const newTimestamp = new Date();
|
|
53
|
+
newTimestamp.setDate(newTimestamp.getDate() - 10);
|
|
54
|
+
const newStr = newTimestamp.toISOString().replace(/\.[0-9]{3}Z$/, 'Z');
|
|
55
|
+
const logContent = `${oldStr}|test|manual|context1\n${newStr}|test|manual|context2\n`;
|
|
56
|
+
await fs.writeFile(path.join(logDir, 'skill_usage.log'), logContent);
|
|
57
|
+
const output = await rotateLogs(tempDir, 90);
|
|
58
|
+
expect(output).toContain('Pruned 1 lines');
|
|
59
|
+
const finalContent = await fs.readFile(path.join(logDir, 'skill_usage.log'), 'utf-8');
|
|
60
|
+
expect(finalContent).not.toContain('context1');
|
|
61
|
+
expect(finalContent).toContain('context2');
|
|
62
|
+
});
|
|
63
|
+
it('should report if no logs need rotation', async () => {
|
|
64
|
+
const logDir = path.join(tempDir, '.agent', 'logs');
|
|
65
|
+
await fs.mkdir(logDir, { recursive: true });
|
|
66
|
+
const newTimestamp = new Date();
|
|
67
|
+
newTimestamp.setDate(newTimestamp.getDate() - 10);
|
|
68
|
+
const newStr = newTimestamp.toISOString().replace(/\.[0-9]{3}Z$/, 'Z');
|
|
69
|
+
const logContent = `${newStr}|test|manual|context2\n`;
|
|
70
|
+
await fs.writeFile(path.join(logDir, 'skill_usage.log'), logContent);
|
|
71
|
+
const output = await rotateLogs(tempDir, 90);
|
|
72
|
+
expect(output).toContain('(No logs needed rotation)');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { createTodo, startTodo, doneTodo, getNextTodoId } from '../todoTools.js';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
describe('Todo Tools', () => {
|
|
7
|
+
let tempDir;
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'todo-test-'));
|
|
10
|
+
});
|
|
11
|
+
afterEach(async () => {
|
|
12
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
13
|
+
});
|
|
14
|
+
it('should calculate next todo ID correctly', async () => {
|
|
15
|
+
const id1 = await getNextTodoId(tempDir);
|
|
16
|
+
expect(id1).toBe('001');
|
|
17
|
+
await fs.mkdir(path.join(tempDir, 'todos'), { recursive: true });
|
|
18
|
+
await fs.writeFile(path.join(tempDir, 'todos', '005-pending-p2-test.md'), 'test');
|
|
19
|
+
const id2 = await getNextTodoId(tempDir);
|
|
20
|
+
expect(id2).toBe('006');
|
|
21
|
+
});
|
|
22
|
+
it('should create a new todo file', async () => {
|
|
23
|
+
const result = await createTodo('p2', 'Fix Bug', 'The bug is terrible', ['Check 1', 'Check 2'], tempDir);
|
|
24
|
+
expect(result).toContain('001-pending-p2-fix-bug');
|
|
25
|
+
const files = await fs.readdir(path.join(tempDir, 'todos'));
|
|
26
|
+
expect(files).toContain('001-pending-p2-fix-bug.md');
|
|
27
|
+
const content = await fs.readFile(path.join(tempDir, 'todos', '001-pending-p2-fix-bug.md'), 'utf-8');
|
|
28
|
+
expect(content).toContain('status: pending');
|
|
29
|
+
expect(content).toContain('priority: p2');
|
|
30
|
+
expect(content).toContain('- [ ] Check 1');
|
|
31
|
+
});
|
|
32
|
+
it('should start a todo file', async () => {
|
|
33
|
+
await createTodo('p2', 'Fix Bug', 'The bug is terrible', ['Check 1'], tempDir);
|
|
34
|
+
const todoFile = path.join('todos', '001-pending-p2-fix-bug.md');
|
|
35
|
+
const result = await startTodo(todoFile, false, tempDir);
|
|
36
|
+
expect(result).toContain('001-in-progress-p2-fix-bug.md');
|
|
37
|
+
const newPath = path.join(tempDir, 'todos', '001-in-progress-p2-fix-bug.md');
|
|
38
|
+
const content = await fs.readFile(newPath, 'utf-8');
|
|
39
|
+
expect(content).toContain('status: in-progress');
|
|
40
|
+
});
|
|
41
|
+
it('should fail to done a todo if unchecked items exist', async () => {
|
|
42
|
+
await createTodo('p2', 'Fix Bug', 'The bug is terrible', ['Check 1'], tempDir);
|
|
43
|
+
// It's still pending so its name has pending
|
|
44
|
+
const todoFile = path.join('todos', '001-pending-p2-fix-bug.md');
|
|
45
|
+
await expect(doneTodo(todoFile, false, tempDir)).rejects.toThrow(/Unchecked items found/);
|
|
46
|
+
});
|
|
47
|
+
it('should done a todo if force is true with unchecked items', async () => {
|
|
48
|
+
await createTodo('p2', 'Fix Bug', 'The bug is terrible', ['Check 1'], tempDir);
|
|
49
|
+
const todoFile = path.join('todos', '001-pending-p2-fix-bug.md');
|
|
50
|
+
const result = await doneTodo(todoFile, true, tempDir);
|
|
51
|
+
expect(result).toContain('001-done-p2-fix-bug.md');
|
|
52
|
+
});
|
|
53
|
+
it('should done a todo if all items are checked', async () => {
|
|
54
|
+
await createTodo('p2', 'Fix Bug', '', ['Check 1'], tempDir);
|
|
55
|
+
const todoFile = path.normalize(path.join(tempDir, 'todos', '001-pending-p2-fix-bug.md'));
|
|
56
|
+
let content = await fs.readFile(todoFile, 'utf-8');
|
|
57
|
+
content = content.replace('- [ ] Check 1', '- [x] Check 1');
|
|
58
|
+
await fs.writeFile(todoFile, content);
|
|
59
|
+
const result = await doneTodo(path.join('todos', '001-pending-p2-fix-bug.md'), false, tempDir);
|
|
60
|
+
expect(result).toContain('001-done-p2-fix-bug.md');
|
|
61
|
+
const newPath = path.join(tempDir, 'todos', '001-done-p2-fix-bug.md');
|
|
62
|
+
const newContent = await fs.readFile(newPath, 'utf-8');
|
|
63
|
+
expect(newContent).toContain('status: done');
|
|
64
|
+
});
|
|
65
|
+
it('should reject startTodo if in terminal state without force', async () => {
|
|
66
|
+
await createTodo('p2', 'Fix Bug', '', [], tempDir);
|
|
67
|
+
const todoFile = path.normalize(path.join(tempDir, 'todos', '001-pending-p2-fix-bug.md'));
|
|
68
|
+
let content = await fs.readFile(todoFile, 'utf-8');
|
|
69
|
+
content = content.replace('status: pending', 'status: done');
|
|
70
|
+
await fs.writeFile(todoFile, content);
|
|
71
|
+
await expect(startTodo(path.join('todos', '001-pending-p2-fix-bug.md'), false, tempDir)).rejects.toThrow(/terminal state/);
|
|
72
|
+
});
|
|
73
|
+
});
|