mod-bot 1.0.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 +103 -0
- package/agents/analyzer.yaml +12 -0
- package/agents/learner.yaml +12 -0
- package/agents/moderator.yaml +13 -0
- package/agents/swarm-coordinator.yaml +13 -0
- package/dist/index.js +1965 -0
- package/dist/index.js.map +1 -0
- package/mod-bot.config.schema.json +94 -0
- package/package.json +52 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1965 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// packages/server/src/index.ts
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
|
|
7
|
+
// packages/server/src/tools/init.ts
|
|
8
|
+
import { z as z2 } from "zod";
|
|
9
|
+
|
|
10
|
+
// packages/core/dist/types.js
|
|
11
|
+
var DEFAULT_CONFIG = {
|
|
12
|
+
version: "1.0.0",
|
|
13
|
+
user: {
|
|
14
|
+
name: "",
|
|
15
|
+
experienceLevel: "mid",
|
|
16
|
+
primaryLanguages: [],
|
|
17
|
+
learningLanguages: [],
|
|
18
|
+
visualPreference: "moderate",
|
|
19
|
+
explanationDepth: "standard",
|
|
20
|
+
workflowPreference: "adaptive"
|
|
21
|
+
},
|
|
22
|
+
project: {
|
|
23
|
+
status: "brownfield",
|
|
24
|
+
type: "personal",
|
|
25
|
+
priorities: ["quality"]
|
|
26
|
+
},
|
|
27
|
+
moderation: {
|
|
28
|
+
autoSuggest: true,
|
|
29
|
+
learnPatterns: true,
|
|
30
|
+
swarmBudget: "conservative",
|
|
31
|
+
tddEnforcement: "suggest",
|
|
32
|
+
reviewBeforeCommit: true
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// packages/core/dist/config.js
|
|
37
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
38
|
+
import { existsSync } from "fs";
|
|
39
|
+
import { join } from "path";
|
|
40
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
41
|
+
import { z } from "zod";
|
|
42
|
+
var MOD_BOT_DIR = ".mod-bot";
|
|
43
|
+
var CONFIG_FILE = "config.yaml";
|
|
44
|
+
var userProfileSchema = z.object({
|
|
45
|
+
name: z.string().default(""),
|
|
46
|
+
experienceLevel: z.enum(["junior", "mid", "senior", "lead"]).default("mid"),
|
|
47
|
+
primaryLanguages: z.array(z.string()).default([]),
|
|
48
|
+
learningLanguages: z.array(z.string()).default([]),
|
|
49
|
+
visualPreference: z.enum(["minimal", "moderate", "verbose"]).default("moderate"),
|
|
50
|
+
explanationDepth: z.enum(["terse", "standard", "detailed"]).default("standard"),
|
|
51
|
+
workflowPreference: z.enum(["guided", "autonomous", "adaptive"]).default("adaptive")
|
|
52
|
+
});
|
|
53
|
+
var projectConfigSchema = z.object({
|
|
54
|
+
status: z.enum(["greenfield", "brownfield", "fork"]).default("brownfield"),
|
|
55
|
+
type: z.enum(["personal", "team", "enterprise"]).default("personal"),
|
|
56
|
+
priorities: z.array(z.string()).default(["quality"])
|
|
57
|
+
});
|
|
58
|
+
var moderationConfigSchema = z.object({
|
|
59
|
+
autoSuggest: z.boolean().default(true),
|
|
60
|
+
learnPatterns: z.boolean().default(true),
|
|
61
|
+
swarmBudget: z.enum(["conservative", "moderate", "unlimited"]).default("conservative"),
|
|
62
|
+
tddEnforcement: z.enum(["off", "suggest", "enforce"]).default("suggest"),
|
|
63
|
+
reviewBeforeCommit: z.boolean().default(true)
|
|
64
|
+
});
|
|
65
|
+
var modBotConfigSchema = z.object({
|
|
66
|
+
version: z.string().default("1.0.0"),
|
|
67
|
+
user: userProfileSchema.default({}),
|
|
68
|
+
project: projectConfigSchema.default({}),
|
|
69
|
+
moderation: moderationConfigSchema.default({})
|
|
70
|
+
});
|
|
71
|
+
function getModBotDir(projectRoot) {
|
|
72
|
+
return join(projectRoot, MOD_BOT_DIR);
|
|
73
|
+
}
|
|
74
|
+
function getConfigPath(projectRoot) {
|
|
75
|
+
return join(getModBotDir(projectRoot), CONFIG_FILE);
|
|
76
|
+
}
|
|
77
|
+
async function ensureModBotDir(projectRoot) {
|
|
78
|
+
const dir = getModBotDir(projectRoot);
|
|
79
|
+
if (!existsSync(dir)) {
|
|
80
|
+
await mkdir(dir, { recursive: true });
|
|
81
|
+
}
|
|
82
|
+
const memoryDir = join(dir, "memory", "sessions");
|
|
83
|
+
if (!existsSync(memoryDir)) {
|
|
84
|
+
await mkdir(memoryDir, { recursive: true });
|
|
85
|
+
}
|
|
86
|
+
return dir;
|
|
87
|
+
}
|
|
88
|
+
async function loadConfig(projectRoot) {
|
|
89
|
+
const configPath = getConfigPath(projectRoot);
|
|
90
|
+
if (!existsSync(configPath)) {
|
|
91
|
+
return structuredClone(DEFAULT_CONFIG);
|
|
92
|
+
}
|
|
93
|
+
const raw = await readFile(configPath, "utf-8");
|
|
94
|
+
const parsed = parseYaml(raw);
|
|
95
|
+
return modBotConfigSchema.parse(parsed);
|
|
96
|
+
}
|
|
97
|
+
async function saveConfig(projectRoot, config) {
|
|
98
|
+
await ensureModBotDir(projectRoot);
|
|
99
|
+
const configPath = getConfigPath(projectRoot);
|
|
100
|
+
const yamlStr = stringifyYaml(config, { indent: 2 });
|
|
101
|
+
await writeFile(configPath, yamlStr, "utf-8");
|
|
102
|
+
}
|
|
103
|
+
function mergeConfig(base, overrides) {
|
|
104
|
+
return {
|
|
105
|
+
version: overrides.version ?? base.version,
|
|
106
|
+
user: { ...base.user, ...overrides.user },
|
|
107
|
+
project: { ...base.project, ...overrides.project },
|
|
108
|
+
moderation: { ...base.moderation, ...overrides.moderation }
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function isInitialized(projectRoot) {
|
|
112
|
+
return existsSync(getConfigPath(projectRoot));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// packages/core/dist/project-state.js
|
|
116
|
+
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
117
|
+
import { existsSync as existsSync2 } from "fs";
|
|
118
|
+
import { join as join2 } from "path";
|
|
119
|
+
import { randomUUID } from "crypto";
|
|
120
|
+
var STATE_FILE = "state.json";
|
|
121
|
+
function getStatePath(projectRoot) {
|
|
122
|
+
return join2(getModBotDir(projectRoot), STATE_FILE);
|
|
123
|
+
}
|
|
124
|
+
function defaultState() {
|
|
125
|
+
return {
|
|
126
|
+
lastScanAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
127
|
+
currentBranch: "main",
|
|
128
|
+
decisions: [],
|
|
129
|
+
openQuestions: [],
|
|
130
|
+
healthMetrics: { lastCheckedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
async function loadProjectState(projectRoot) {
|
|
134
|
+
const statePath = getStatePath(projectRoot);
|
|
135
|
+
if (!existsSync2(statePath)) {
|
|
136
|
+
return defaultState();
|
|
137
|
+
}
|
|
138
|
+
const raw = await readFile2(statePath, "utf-8");
|
|
139
|
+
return JSON.parse(raw);
|
|
140
|
+
}
|
|
141
|
+
async function saveProjectState(projectRoot, state) {
|
|
142
|
+
await ensureModBotDir(projectRoot);
|
|
143
|
+
const statePath = getStatePath(projectRoot);
|
|
144
|
+
await writeFile2(statePath, JSON.stringify(state, null, 2), "utf-8");
|
|
145
|
+
}
|
|
146
|
+
async function updateProjectState(projectRoot, updater) {
|
|
147
|
+
const state = await loadProjectState(projectRoot);
|
|
148
|
+
const updated = updater(state);
|
|
149
|
+
await saveProjectState(projectRoot, updated);
|
|
150
|
+
return updated;
|
|
151
|
+
}
|
|
152
|
+
function addDecision(state, description, rationale, category) {
|
|
153
|
+
const entry = {
|
|
154
|
+
id: randomUUID(),
|
|
155
|
+
description,
|
|
156
|
+
rationale,
|
|
157
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
158
|
+
category
|
|
159
|
+
};
|
|
160
|
+
return { ...state, decisions: [...state.decisions, entry] };
|
|
161
|
+
}
|
|
162
|
+
function setFocus(state, area) {
|
|
163
|
+
return { ...state, focusArea: area };
|
|
164
|
+
}
|
|
165
|
+
function setTask(state, task) {
|
|
166
|
+
return { ...state, currentTask: task };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// packages/core/dist/workflow-engine.js
|
|
170
|
+
function recommendWorkflow(config, taskDescription, complexity) {
|
|
171
|
+
const { user, moderation } = config;
|
|
172
|
+
if (complexity === "epic" && moderation.swarmBudget !== "conservative") {
|
|
173
|
+
return {
|
|
174
|
+
type: "swarm",
|
|
175
|
+
reason: "Task complexity warrants multi-agent coordination.",
|
|
176
|
+
steps: ["Estimate swarm cost", "Spawn analysis agents", "Synthesize results", "Review and apply"]
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const isNewFeature = /\b(add|create|implement|build|new)\b/i.test(taskDescription);
|
|
180
|
+
if (moderation.tddEnforcement === "enforce" && isNewFeature) {
|
|
181
|
+
return {
|
|
182
|
+
type: "tdd",
|
|
183
|
+
reason: "TDD enforcement is enabled for new features.",
|
|
184
|
+
steps: ["Write failing test", "Verify test fails", "Implement minimal code", "Verify test passes", "Refactor"]
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
if (user.experienceLevel === "junior" || user.workflowPreference === "guided") {
|
|
188
|
+
if (complexity === "trivial" || complexity === "simple") {
|
|
189
|
+
return {
|
|
190
|
+
type: "guided",
|
|
191
|
+
reason: "Step-by-step guidance for manageable task.",
|
|
192
|
+
steps: ["Understand the task", "Identify files to change", "Make changes", "Test", "Review"]
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
type: "plan-first",
|
|
197
|
+
reason: "Complex task benefits from upfront planning.",
|
|
198
|
+
steps: ["Analyze requirements", "Create implementation plan", "Execute steps", "Verify each step", "Final review"]
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
if (user.experienceLevel === "senior" || user.experienceLevel === "lead") {
|
|
202
|
+
if (complexity === "trivial" || complexity === "simple") {
|
|
203
|
+
return {
|
|
204
|
+
type: "direct",
|
|
205
|
+
reason: "Straightforward task for experienced developer.",
|
|
206
|
+
steps: ["Implement", "Verify"]
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (complexity === "moderate" || complexity === "complex") {
|
|
211
|
+
return {
|
|
212
|
+
type: "plan-first",
|
|
213
|
+
reason: "Task complexity warrants planning.",
|
|
214
|
+
steps: ["Analyze", "Plan", "Implement", "Test", "Review"]
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
if (moderation.tddEnforcement === "suggest" && isNewFeature) {
|
|
218
|
+
return {
|
|
219
|
+
type: "tdd",
|
|
220
|
+
reason: "Consider TDD for this new feature.",
|
|
221
|
+
steps: ["Write failing test", "Implement", "Refactor"]
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
type: "direct",
|
|
226
|
+
reason: "Simple task, direct approach.",
|
|
227
|
+
steps: ["Implement", "Verify"]
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// packages/core/dist/moderator.js
|
|
232
|
+
var Moderator = class {
|
|
233
|
+
ctx;
|
|
234
|
+
constructor(ctx) {
|
|
235
|
+
this.ctx = ctx;
|
|
236
|
+
}
|
|
237
|
+
updateContext(partial) {
|
|
238
|
+
this.ctx = { ...this.ctx, ...partial };
|
|
239
|
+
}
|
|
240
|
+
get config() {
|
|
241
|
+
return this.ctx.config;
|
|
242
|
+
}
|
|
243
|
+
get userLevel() {
|
|
244
|
+
return this.ctx.config.user.experienceLevel;
|
|
245
|
+
}
|
|
246
|
+
suggest(taskDescription, complexity) {
|
|
247
|
+
const workflow = recommendWorkflow(this.ctx.config, taskDescription, complexity.level);
|
|
248
|
+
const { user } = this.ctx.config;
|
|
249
|
+
let content;
|
|
250
|
+
if (user.experienceLevel === "senior" || user.experienceLevel === "lead") {
|
|
251
|
+
content = this.formatTerse(taskDescription, workflow, complexity);
|
|
252
|
+
} else if (user.experienceLevel === "junior") {
|
|
253
|
+
content = this.formatDetailed(taskDescription, workflow, complexity);
|
|
254
|
+
} else {
|
|
255
|
+
content = this.formatStandard(taskDescription, workflow, complexity);
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
content,
|
|
259
|
+
suggestions: workflow.steps,
|
|
260
|
+
metadata: { workflow: workflow.type, complexity: complexity.level }
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
review(changes, _context) {
|
|
264
|
+
const { user } = this.ctx.config;
|
|
265
|
+
const warnings = [];
|
|
266
|
+
const suggestions = [];
|
|
267
|
+
if (changes.includes("console.log") && !changes.includes("test")) {
|
|
268
|
+
warnings.push("Debug console.log detected in production code.");
|
|
269
|
+
}
|
|
270
|
+
if (changes.includes("any") && changes.includes(".ts")) {
|
|
271
|
+
warnings.push("TypeScript `any` type detected \u2014 consider adding proper types.");
|
|
272
|
+
}
|
|
273
|
+
if (changes.includes("TODO") || changes.includes("FIXME")) {
|
|
274
|
+
suggestions.push("TODO/FIXME comments found \u2014 consider tracking in issue tracker.");
|
|
275
|
+
}
|
|
276
|
+
const { userMemory } = this.ctx;
|
|
277
|
+
const filteredWarnings = warnings.filter((w) => {
|
|
278
|
+
const key = w.substring(0, 30).trim().toLowerCase().replace(/\s+/g, "-");
|
|
279
|
+
const ignored = userMemory.ignoredSuggestions[key];
|
|
280
|
+
return !ignored || ignored.action !== "suppress";
|
|
281
|
+
});
|
|
282
|
+
let content;
|
|
283
|
+
if (user.experienceLevel === "senior" || user.experienceLevel === "lead") {
|
|
284
|
+
content = this.formatReviewTerse(filteredWarnings, suggestions);
|
|
285
|
+
} else {
|
|
286
|
+
content = this.formatReviewDetailed(filteredWarnings, suggestions, changes);
|
|
287
|
+
}
|
|
288
|
+
return { content, warnings: filteredWarnings, suggestions };
|
|
289
|
+
}
|
|
290
|
+
plan(taskDescription, complexity) {
|
|
291
|
+
const workflow = recommendWorkflow(this.ctx.config, taskDescription, complexity.level);
|
|
292
|
+
const { user } = this.ctx.config;
|
|
293
|
+
const steps = workflow.steps.map((step, i) => {
|
|
294
|
+
if (user.experienceLevel === "junior") {
|
|
295
|
+
return `${i + 1}. **${step}**
|
|
296
|
+
_What:_ ${this.expandStep(step)}
|
|
297
|
+
_Why:_ ${this.explainStep(step)}`;
|
|
298
|
+
}
|
|
299
|
+
return `${i + 1}. ${step}`;
|
|
300
|
+
});
|
|
301
|
+
const content = [
|
|
302
|
+
`## Implementation Plan`,
|
|
303
|
+
``,
|
|
304
|
+
`**Workflow:** ${workflow.type}`,
|
|
305
|
+
`**Reason:** ${workflow.reason}`,
|
|
306
|
+
`**Complexity:** ${complexity.level} (${complexity.score}/100)`,
|
|
307
|
+
``,
|
|
308
|
+
`### Steps`,
|
|
309
|
+
``,
|
|
310
|
+
...steps
|
|
311
|
+
].join("\n");
|
|
312
|
+
return { content, metadata: { workflow: workflow.type } };
|
|
313
|
+
}
|
|
314
|
+
checkpoint() {
|
|
315
|
+
const { projectState } = this.ctx;
|
|
316
|
+
const parts = [
|
|
317
|
+
`## Checkpoint`,
|
|
318
|
+
``,
|
|
319
|
+
`**Branch:** ${projectState.currentBranch}`
|
|
320
|
+
];
|
|
321
|
+
if (projectState.currentTask) {
|
|
322
|
+
parts.push(`**Task:** ${projectState.currentTask}`);
|
|
323
|
+
}
|
|
324
|
+
if (projectState.focusArea) {
|
|
325
|
+
parts.push(`**Focus:** ${projectState.focusArea}`);
|
|
326
|
+
}
|
|
327
|
+
if (projectState.decisions.length > 0) {
|
|
328
|
+
parts.push(``, `### Recent Decisions`);
|
|
329
|
+
for (const d of projectState.decisions.slice(-5)) {
|
|
330
|
+
parts.push(`- ${d.description} \u2014 _${d.rationale}_`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (projectState.openQuestions.length > 0) {
|
|
334
|
+
parts.push(``, `### Open Questions`);
|
|
335
|
+
for (const q of projectState.openQuestions) {
|
|
336
|
+
parts.push(`- ${q}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return { content: parts.join("\n") };
|
|
340
|
+
}
|
|
341
|
+
retro(taskSummary) {
|
|
342
|
+
const content = [
|
|
343
|
+
`## Retrospective`,
|
|
344
|
+
``,
|
|
345
|
+
`**Task:** ${taskSummary}`,
|
|
346
|
+
``,
|
|
347
|
+
`### What went well`,
|
|
348
|
+
`- Task completed`,
|
|
349
|
+
``,
|
|
350
|
+
`### What could improve`,
|
|
351
|
+
`- (Review your decisions and note any patterns)`,
|
|
352
|
+
``,
|
|
353
|
+
`### Learned patterns`,
|
|
354
|
+
`- (Any new patterns detected will be saved to memory)`
|
|
355
|
+
].join("\n");
|
|
356
|
+
return { content };
|
|
357
|
+
}
|
|
358
|
+
formatTerse(_task, workflow, complexity) {
|
|
359
|
+
return `**${workflow.type}** (${complexity.level}): ${workflow.steps.join(" \u2192 ")}`;
|
|
360
|
+
}
|
|
361
|
+
formatStandard(_task, workflow, complexity) {
|
|
362
|
+
return [
|
|
363
|
+
`**Approach:** ${workflow.type}`,
|
|
364
|
+
`**Complexity:** ${complexity.level}`,
|
|
365
|
+
`${workflow.reason}`,
|
|
366
|
+
``,
|
|
367
|
+
`Steps: ${workflow.steps.map((s, i) => `${i + 1}. ${s}`).join("\n")}`
|
|
368
|
+
].join("\n");
|
|
369
|
+
}
|
|
370
|
+
formatDetailed(_task, workflow, complexity) {
|
|
371
|
+
const lines = [
|
|
372
|
+
`## Suggested Approach`,
|
|
373
|
+
``,
|
|
374
|
+
`I recommend the **${workflow.type}** workflow for this task.`,
|
|
375
|
+
``,
|
|
376
|
+
`**Why?** ${workflow.reason}`,
|
|
377
|
+
``,
|
|
378
|
+
`**Complexity:** ${complexity.level} (${complexity.score}/100)`
|
|
379
|
+
];
|
|
380
|
+
if (complexity.factors.length > 0) {
|
|
381
|
+
lines.push(`**Factors:** ${complexity.factors.join(", ")}`);
|
|
382
|
+
}
|
|
383
|
+
lines.push(``, `### Steps to Follow`, ``);
|
|
384
|
+
for (const [i, step] of workflow.steps.entries()) {
|
|
385
|
+
lines.push(`${i + 1}. **${step}**`);
|
|
386
|
+
lines.push(` ${this.expandStep(step)}`);
|
|
387
|
+
lines.push(``);
|
|
388
|
+
}
|
|
389
|
+
return lines.join("\n");
|
|
390
|
+
}
|
|
391
|
+
formatReviewTerse(warnings, suggestions) {
|
|
392
|
+
if (warnings.length === 0 && suggestions.length === 0)
|
|
393
|
+
return "LGTM";
|
|
394
|
+
const parts = [];
|
|
395
|
+
for (const w of warnings)
|
|
396
|
+
parts.push(`\u26A0 ${w}`);
|
|
397
|
+
for (const s of suggestions)
|
|
398
|
+
parts.push(`\u{1F4A1} ${s}`);
|
|
399
|
+
return parts.join("\n");
|
|
400
|
+
}
|
|
401
|
+
formatReviewDetailed(warnings, suggestions, _changes) {
|
|
402
|
+
const parts = [`## Code Review`, ``];
|
|
403
|
+
if (warnings.length > 0) {
|
|
404
|
+
parts.push(`### Warnings`, ``);
|
|
405
|
+
for (const w of warnings) {
|
|
406
|
+
parts.push(`- \u26A0 **${w}**`);
|
|
407
|
+
parts.push(` _Consider addressing this before committing._`);
|
|
408
|
+
}
|
|
409
|
+
parts.push(``);
|
|
410
|
+
}
|
|
411
|
+
if (suggestions.length > 0) {
|
|
412
|
+
parts.push(`### Suggestions`, ``);
|
|
413
|
+
for (const s of suggestions)
|
|
414
|
+
parts.push(`- \u{1F4A1} ${s}`);
|
|
415
|
+
parts.push(``);
|
|
416
|
+
}
|
|
417
|
+
if (warnings.length === 0 && suggestions.length === 0) {
|
|
418
|
+
parts.push(`Looks good! No issues detected.`);
|
|
419
|
+
}
|
|
420
|
+
return parts.join("\n");
|
|
421
|
+
}
|
|
422
|
+
expandStep(step) {
|
|
423
|
+
const expansions = {
|
|
424
|
+
"Write failing test": "Create a test that describes the expected behavior. It should fail because the feature does not exist yet.",
|
|
425
|
+
"Verify test fails": "Run the test and confirm it fails with the expected error message.",
|
|
426
|
+
"Implement minimal code": "Write only enough code to make the failing test pass. Resist adding extras.",
|
|
427
|
+
"Verify test passes": "Run the test again. All tests should now be green.",
|
|
428
|
+
"Refactor": "Clean up the code while keeping all tests passing.",
|
|
429
|
+
"Implement": "Write the code changes needed.",
|
|
430
|
+
"Verify": "Run tests and check that everything works correctly.",
|
|
431
|
+
"Analyze requirements": "Read the task description and identify exactly what needs to change.",
|
|
432
|
+
"Create implementation plan": "List the files to modify and the specific changes for each.",
|
|
433
|
+
"Test": "Run the test suite to verify your changes.",
|
|
434
|
+
"Review": "Look over your changes for any issues before committing."
|
|
435
|
+
};
|
|
436
|
+
return expansions[step] ?? "Complete this step and verify the result.";
|
|
437
|
+
}
|
|
438
|
+
explainStep(step) {
|
|
439
|
+
const explanations = {
|
|
440
|
+
"Write failing test": "TDD starts with a test so you know exactly when the feature works.",
|
|
441
|
+
"Verify test fails": "Confirming the test fails proves it actually tests something meaningful.",
|
|
442
|
+
"Implement minimal code": "Minimal code prevents over-engineering and keeps changes focused.",
|
|
443
|
+
"Refactor": "Now that tests protect you, you can safely improve code structure."
|
|
444
|
+
};
|
|
445
|
+
return explanations[step] ?? "This ensures quality and correctness.";
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
// packages/analyzer/dist/tech-detector.js
|
|
450
|
+
import { existsSync as existsSync3 } from "fs";
|
|
451
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
452
|
+
import { join as join3 } from "path";
|
|
453
|
+
var FRAMEWORK_DETECTORS = [
|
|
454
|
+
{ name: "Next.js", deps: ["next"] },
|
|
455
|
+
{ name: "React", deps: ["react"] },
|
|
456
|
+
{ name: "Vue", deps: ["vue"] },
|
|
457
|
+
{ name: "Angular", deps: ["@angular/core"] },
|
|
458
|
+
{ name: "Svelte", deps: ["svelte"] },
|
|
459
|
+
{ name: "Express", deps: ["express"] },
|
|
460
|
+
{ name: "Fastify", deps: ["fastify"] },
|
|
461
|
+
{ name: "Hono", deps: ["hono"] },
|
|
462
|
+
{ name: "NestJS", deps: ["@nestjs/core"] },
|
|
463
|
+
{ name: "Django", deps: [] },
|
|
464
|
+
// detected via requirements.txt/pyproject.toml
|
|
465
|
+
{ name: "Flask", deps: [] },
|
|
466
|
+
{ name: "FastAPI", deps: [] }
|
|
467
|
+
];
|
|
468
|
+
var TEST_FRAMEWORK_DETECTORS = [
|
|
469
|
+
{ name: "vitest", deps: ["vitest"] },
|
|
470
|
+
{ name: "jest", deps: ["jest", "@jest/core"] },
|
|
471
|
+
{ name: "mocha", deps: ["mocha"] },
|
|
472
|
+
{ name: "pytest", deps: [] },
|
|
473
|
+
{ name: "ava", deps: ["ava"] },
|
|
474
|
+
{ name: "tap", deps: ["tap"] }
|
|
475
|
+
];
|
|
476
|
+
var BUILD_TOOL_DETECTORS = [
|
|
477
|
+
{ name: "vite", files: ["vite.config.ts", "vite.config.js"] },
|
|
478
|
+
{ name: "webpack", files: ["webpack.config.js", "webpack.config.ts"] },
|
|
479
|
+
{ name: "esbuild", files: ["esbuild.config.js"] },
|
|
480
|
+
{ name: "rollup", files: ["rollup.config.js", "rollup.config.ts"] },
|
|
481
|
+
{ name: "turbo", files: ["turbo.json"] },
|
|
482
|
+
{ name: "nx", files: ["nx.json"] }
|
|
483
|
+
];
|
|
484
|
+
async function readPackageJson(rootPath) {
|
|
485
|
+
const pkgPath = join3(rootPath, "package.json");
|
|
486
|
+
if (!existsSync3(pkgPath))
|
|
487
|
+
return null;
|
|
488
|
+
const raw = await readFile3(pkgPath, "utf-8");
|
|
489
|
+
return JSON.parse(raw);
|
|
490
|
+
}
|
|
491
|
+
function allDeps(pkg) {
|
|
492
|
+
return { ...pkg.dependencies, ...pkg.devDependencies };
|
|
493
|
+
}
|
|
494
|
+
async function detectTechStack(rootPath) {
|
|
495
|
+
const stacks = [];
|
|
496
|
+
const pkg = await readPackageJson(rootPath);
|
|
497
|
+
if (pkg) {
|
|
498
|
+
const deps = allDeps(pkg);
|
|
499
|
+
const hasTypeScript = existsSync3(join3(rootPath, "tsconfig.json")) || "typescript" in deps;
|
|
500
|
+
const language = hasTypeScript ? "TypeScript" : "JavaScript";
|
|
501
|
+
let packageManager = "npm";
|
|
502
|
+
if (existsSync3(join3(rootPath, "pnpm-lock.yaml")) || existsSync3(join3(rootPath, "pnpm-workspace.yaml"))) {
|
|
503
|
+
packageManager = "pnpm";
|
|
504
|
+
} else if (existsSync3(join3(rootPath, "yarn.lock"))) {
|
|
505
|
+
packageManager = "yarn";
|
|
506
|
+
} else if (existsSync3(join3(rootPath, "bun.lockb"))) {
|
|
507
|
+
packageManager = "bun";
|
|
508
|
+
}
|
|
509
|
+
let framework;
|
|
510
|
+
for (const f of FRAMEWORK_DETECTORS) {
|
|
511
|
+
if (f.deps.some((d) => d in deps)) {
|
|
512
|
+
framework = f.name;
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
let testFramework;
|
|
517
|
+
for (const t of TEST_FRAMEWORK_DETECTORS) {
|
|
518
|
+
if (t.deps.some((d) => d in deps)) {
|
|
519
|
+
testFramework = t.name;
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
let buildTool;
|
|
524
|
+
for (const b of BUILD_TOOL_DETECTORS) {
|
|
525
|
+
if (b.files.some((f) => existsSync3(join3(rootPath, f)))) {
|
|
526
|
+
buildTool = b.name;
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
stacks.push({ language, framework, packageManager, testFramework, buildTool });
|
|
531
|
+
}
|
|
532
|
+
if (existsSync3(join3(rootPath, "requirements.txt")) || existsSync3(join3(rootPath, "pyproject.toml")) || existsSync3(join3(rootPath, "setup.py"))) {
|
|
533
|
+
const stack = { language: "Python" };
|
|
534
|
+
if (existsSync3(join3(rootPath, "pyproject.toml"))) {
|
|
535
|
+
const content = await readFile3(join3(rootPath, "pyproject.toml"), "utf-8");
|
|
536
|
+
if (content.includes("django"))
|
|
537
|
+
stack.framework = "Django";
|
|
538
|
+
else if (content.includes("flask"))
|
|
539
|
+
stack.framework = "Flask";
|
|
540
|
+
else if (content.includes("fastapi"))
|
|
541
|
+
stack.framework = "FastAPI";
|
|
542
|
+
if (content.includes("pytest"))
|
|
543
|
+
stack.testFramework = "pytest";
|
|
544
|
+
stack.packageManager = content.includes("[tool.poetry]") ? "poetry" : "pip";
|
|
545
|
+
}
|
|
546
|
+
stacks.push(stack);
|
|
547
|
+
}
|
|
548
|
+
if (existsSync3(join3(rootPath, "go.mod"))) {
|
|
549
|
+
stacks.push({ language: "Go", packageManager: "go modules" });
|
|
550
|
+
}
|
|
551
|
+
if (existsSync3(join3(rootPath, "Cargo.toml"))) {
|
|
552
|
+
stacks.push({ language: "Rust", packageManager: "cargo" });
|
|
553
|
+
}
|
|
554
|
+
if (existsSync3(join3(rootPath, "Gemfile"))) {
|
|
555
|
+
const stack = { language: "Ruby", packageManager: "bundler" };
|
|
556
|
+
if (existsSync3(join3(rootPath, "config", "routes.rb"))) {
|
|
557
|
+
stack.framework = "Rails";
|
|
558
|
+
}
|
|
559
|
+
stacks.push(stack);
|
|
560
|
+
}
|
|
561
|
+
if (existsSync3(join3(rootPath, "pom.xml"))) {
|
|
562
|
+
stacks.push({ language: "Java", packageManager: "maven" });
|
|
563
|
+
} else if (existsSync3(join3(rootPath, "build.gradle")) || existsSync3(join3(rootPath, "build.gradle.kts"))) {
|
|
564
|
+
const hasKotlin = existsSync3(join3(rootPath, "build.gradle.kts"));
|
|
565
|
+
stacks.push({ language: hasKotlin ? "Kotlin" : "Java", packageManager: "gradle" });
|
|
566
|
+
}
|
|
567
|
+
return stacks;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// packages/analyzer/dist/codebase-scanner.js
|
|
571
|
+
import { readdir, readFile as readFile4 } from "fs/promises";
|
|
572
|
+
import { existsSync as existsSync4 } from "fs";
|
|
573
|
+
import { join as join4, extname } from "path";
|
|
574
|
+
var LANGUAGE_MAP = {
|
|
575
|
+
".ts": "TypeScript",
|
|
576
|
+
".tsx": "TypeScript",
|
|
577
|
+
".js": "JavaScript",
|
|
578
|
+
".jsx": "JavaScript",
|
|
579
|
+
".mjs": "JavaScript",
|
|
580
|
+
".cjs": "JavaScript",
|
|
581
|
+
".py": "Python",
|
|
582
|
+
".go": "Go",
|
|
583
|
+
".rs": "Rust",
|
|
584
|
+
".rb": "Ruby",
|
|
585
|
+
".java": "Java",
|
|
586
|
+
".kt": "Kotlin",
|
|
587
|
+
".swift": "Swift",
|
|
588
|
+
".c": "C",
|
|
589
|
+
".h": "C",
|
|
590
|
+
".cpp": "C++",
|
|
591
|
+
".hpp": "C++",
|
|
592
|
+
".cc": "C++",
|
|
593
|
+
".cs": "C#",
|
|
594
|
+
".php": "PHP",
|
|
595
|
+
".vue": "Vue",
|
|
596
|
+
".svelte": "Svelte"
|
|
597
|
+
};
|
|
598
|
+
var IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
599
|
+
"node_modules",
|
|
600
|
+
".git",
|
|
601
|
+
"dist",
|
|
602
|
+
"build",
|
|
603
|
+
"out",
|
|
604
|
+
".next",
|
|
605
|
+
".nuxt",
|
|
606
|
+
"target",
|
|
607
|
+
"__pycache__",
|
|
608
|
+
".venv",
|
|
609
|
+
"venv",
|
|
610
|
+
".mod-bot",
|
|
611
|
+
"vendor",
|
|
612
|
+
"coverage",
|
|
613
|
+
".turbo",
|
|
614
|
+
".cache"
|
|
615
|
+
]);
|
|
616
|
+
var TEST_DIRS = ["test", "tests", "__tests__", "spec", "specs", "__test__"];
|
|
617
|
+
var TEST_PATTERNS = [/\.test\.[jt]sx?$/, /\.spec\.[jt]sx?$/, /test_.*\.py$/, /_test\.go$/];
|
|
618
|
+
var CI_FILES = [
|
|
619
|
+
{ pattern: ".github/workflows", tool: "GitHub Actions" },
|
|
620
|
+
{ pattern: ".gitlab-ci.yml", tool: "GitLab CI" },
|
|
621
|
+
{ pattern: "Jenkinsfile", tool: "Jenkins" },
|
|
622
|
+
{ pattern: ".circleci", tool: "CircleCI" },
|
|
623
|
+
{ pattern: ".travis.yml", tool: "Travis CI" },
|
|
624
|
+
{ pattern: "bitbucket-pipelines.yml", tool: "Bitbucket Pipelines" }
|
|
625
|
+
];
|
|
626
|
+
async function countLines(filePath) {
|
|
627
|
+
try {
|
|
628
|
+
const content = await readFile4(filePath, "utf-8");
|
|
629
|
+
return content.split("\n").length;
|
|
630
|
+
} catch {
|
|
631
|
+
return 0;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
async function walkDir(dir, callback, depth = 0, maxDepth = 8) {
|
|
635
|
+
if (depth > maxDepth)
|
|
636
|
+
return;
|
|
637
|
+
let entries;
|
|
638
|
+
try {
|
|
639
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
640
|
+
} catch {
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
for (const entry of entries) {
|
|
644
|
+
if (entry.name.startsWith(".") && entry.isDirectory() && entry.name !== ".github")
|
|
645
|
+
continue;
|
|
646
|
+
if (IGNORE_DIRS.has(entry.name))
|
|
647
|
+
continue;
|
|
648
|
+
const fullPath = join4(dir, entry.name);
|
|
649
|
+
if (entry.isDirectory()) {
|
|
650
|
+
await walkDir(fullPath, callback, depth + 1, maxDepth);
|
|
651
|
+
} else if (entry.isFile()) {
|
|
652
|
+
callback(fullPath, extname(entry.name));
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
async function detectProjectStatus(rootPath) {
|
|
657
|
+
if (existsSync4(join4(rootPath, ".git"))) {
|
|
658
|
+
try {
|
|
659
|
+
const { simpleGit } = await import("simple-git");
|
|
660
|
+
const git = simpleGit(rootPath);
|
|
661
|
+
const remotes = await git.getRemotes(true);
|
|
662
|
+
const hasUpstream = remotes.some((r) => r.name === "upstream");
|
|
663
|
+
if (hasUpstream)
|
|
664
|
+
return "fork";
|
|
665
|
+
const log = await git.log({ maxCount: 20 });
|
|
666
|
+
if (log.total <= 3)
|
|
667
|
+
return "greenfield";
|
|
668
|
+
} catch {
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
let fileCount = 0;
|
|
672
|
+
await walkDir(rootPath, () => {
|
|
673
|
+
fileCount++;
|
|
674
|
+
}, 0, 3);
|
|
675
|
+
if (fileCount <= 10)
|
|
676
|
+
return "greenfield";
|
|
677
|
+
return "brownfield";
|
|
678
|
+
}
|
|
679
|
+
async function scanProject(rootPath) {
|
|
680
|
+
const languages = {};
|
|
681
|
+
const allFiles = [];
|
|
682
|
+
let totalLines = 0;
|
|
683
|
+
let hasTests = false;
|
|
684
|
+
await walkDir(rootPath, (filePath, ext) => {
|
|
685
|
+
allFiles.push(filePath);
|
|
686
|
+
const lang = LANGUAGE_MAP[ext];
|
|
687
|
+
if (lang) {
|
|
688
|
+
languages[lang] = (languages[lang] ?? 0) + 1;
|
|
689
|
+
}
|
|
690
|
+
if (TEST_PATTERNS.some((p) => p.test(filePath))) {
|
|
691
|
+
hasTests = true;
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
if (!hasTests) {
|
|
695
|
+
for (const dir of TEST_DIRS) {
|
|
696
|
+
if (existsSync4(join4(rootPath, dir))) {
|
|
697
|
+
hasTests = true;
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
const sourceFiles = allFiles.filter((f) => Object.keys(LANGUAGE_MAP).includes(extname(f)));
|
|
703
|
+
const sampleFiles = sourceFiles.slice(0, 50);
|
|
704
|
+
for (const f of sampleFiles) {
|
|
705
|
+
totalLines += await countLines(f);
|
|
706
|
+
}
|
|
707
|
+
if (sourceFiles.length > 50) {
|
|
708
|
+
totalLines = Math.round(totalLines * (sourceFiles.length / 50));
|
|
709
|
+
}
|
|
710
|
+
const cicdTools = [];
|
|
711
|
+
for (const ci of CI_FILES) {
|
|
712
|
+
if (existsSync4(join4(rootPath, ci.pattern))) {
|
|
713
|
+
cicdTools.push(ci.tool);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
const hasDocumentation = existsSync4(join4(rootPath, "docs")) || existsSync4(join4(rootPath, "README.md")) || existsSync4(join4(rootPath, "doc"));
|
|
717
|
+
const techStacks = await detectTechStack(rootPath);
|
|
718
|
+
const frameworks = techStacks.map((t) => t.framework).filter((f) => !!f);
|
|
719
|
+
const packageManagers = [...new Set(techStacks.map((t) => t.packageManager).filter((p) => !!p))];
|
|
720
|
+
const testFrameworks = techStacks.map((t) => t.testFramework).filter((t) => !!t);
|
|
721
|
+
const status = await detectProjectStatus(rootPath);
|
|
722
|
+
return {
|
|
723
|
+
languages,
|
|
724
|
+
frameworks,
|
|
725
|
+
packageManagers,
|
|
726
|
+
hasTests,
|
|
727
|
+
testFrameworks,
|
|
728
|
+
hasCICD: cicdTools.length > 0,
|
|
729
|
+
cicdTools,
|
|
730
|
+
hasDocumentation,
|
|
731
|
+
totalFiles: allFiles.length,
|
|
732
|
+
totalLines,
|
|
733
|
+
status,
|
|
734
|
+
rootPath
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// packages/analyzer/dist/complexity.js
|
|
739
|
+
var FACTORS = [
|
|
740
|
+
{ name: "Multi-file change", weight: 15, test: (d) => /\b(multiple files?|across|several|many)\b/i.test(d) },
|
|
741
|
+
{ name: "New feature", weight: 20, test: (d) => /\b(add|create|implement|build|new feature)\b/i.test(d) },
|
|
742
|
+
{ name: "Refactoring", weight: 15, test: (d) => /\b(refactor|restructure|reorganize|migrate)\b/i.test(d) },
|
|
743
|
+
{ name: "Security-sensitive", weight: 25, test: (d) => /\b(auth\w*|security|encryption|token|credential|permission|rbac|oauth)\b/i.test(d) },
|
|
744
|
+
{ name: "Database changes", weight: 20, test: (d) => /\b(database|schema|migration|model|table|query|sql)\b/i.test(d) },
|
|
745
|
+
{ name: "API changes", weight: 15, test: (d) => /\b(api|endpoint|route|controller|graphql|rest)\b/i.test(d) },
|
|
746
|
+
{ name: "Testing required", weight: 10, test: (d) => /\b(test|coverage|spec|assert)\b/i.test(d) },
|
|
747
|
+
{ name: "Performance-critical", weight: 20, test: (d) => /\b(performance|optimize|speed|cache|latency)\b/i.test(d) },
|
|
748
|
+
{ name: "Cross-module", weight: 15, test: (_, c) => c?.touchesMultipleModules === true },
|
|
749
|
+
{ name: "Large file count", weight: 10, test: (_, c) => (c?.fileCount ?? 0) > 5 },
|
|
750
|
+
{ name: "Bug fix", weight: -10, test: (d) => /\b(fix|bug|patch|hotfix|issue)\b/i.test(d) },
|
|
751
|
+
{ name: "Simple change", weight: -15, test: (d) => /\b(typo|rename|update\s+version|bump|comment|readme)\b/i.test(d) }
|
|
752
|
+
];
|
|
753
|
+
function scoreToLevel(score) {
|
|
754
|
+
if (score <= 10)
|
|
755
|
+
return "trivial";
|
|
756
|
+
if (score <= 25)
|
|
757
|
+
return "simple";
|
|
758
|
+
if (score <= 50)
|
|
759
|
+
return "moderate";
|
|
760
|
+
if (score <= 75)
|
|
761
|
+
return "complex";
|
|
762
|
+
return "epic";
|
|
763
|
+
}
|
|
764
|
+
var TOKEN_ESTIMATES = {
|
|
765
|
+
trivial: 500,
|
|
766
|
+
simple: 2e3,
|
|
767
|
+
moderate: 8e3,
|
|
768
|
+
complex: 25e3,
|
|
769
|
+
epic: 8e4
|
|
770
|
+
};
|
|
771
|
+
function assessComplexity(description, context) {
|
|
772
|
+
let score = 20;
|
|
773
|
+
const matchedFactors = [];
|
|
774
|
+
for (const factor of FACTORS) {
|
|
775
|
+
if (factor.test(description, context)) {
|
|
776
|
+
score += factor.weight;
|
|
777
|
+
matchedFactors.push(factor.name);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
score = Math.max(0, Math.min(100, score));
|
|
781
|
+
const level = scoreToLevel(score);
|
|
782
|
+
return {
|
|
783
|
+
level,
|
|
784
|
+
score,
|
|
785
|
+
factors: matchedFactors,
|
|
786
|
+
estimatedTokens: TOKEN_ESTIMATES[level],
|
|
787
|
+
recommendSwarm: level === "complex" || level === "epic"
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// packages/analyzer/dist/change-tracker.js
|
|
792
|
+
import { existsSync as existsSync5 } from "fs";
|
|
793
|
+
import { join as join5 } from "path";
|
|
794
|
+
async function getGitState(rootPath) {
|
|
795
|
+
const defaultState2 = {
|
|
796
|
+
isRepo: false,
|
|
797
|
+
currentBranch: "",
|
|
798
|
+
hasUncommittedChanges: false,
|
|
799
|
+
stagedFiles: [],
|
|
800
|
+
modifiedFiles: [],
|
|
801
|
+
untrackedFiles: [],
|
|
802
|
+
aheadOfRemote: 0,
|
|
803
|
+
behindRemote: 0,
|
|
804
|
+
recentCommits: [],
|
|
805
|
+
remotes: []
|
|
806
|
+
};
|
|
807
|
+
if (!existsSync5(join5(rootPath, ".git"))) {
|
|
808
|
+
return defaultState2;
|
|
809
|
+
}
|
|
810
|
+
try {
|
|
811
|
+
const { simpleGit } = await import("simple-git");
|
|
812
|
+
const git = simpleGit(rootPath);
|
|
813
|
+
const [status, logResult, remoteList] = await Promise.all([
|
|
814
|
+
git.status(),
|
|
815
|
+
git.log({ maxCount: 10 }).catch(() => ({ all: [] })),
|
|
816
|
+
git.getRemotes(true).catch(() => [])
|
|
817
|
+
]);
|
|
818
|
+
const recentCommits = logResult.all.map((c) => ({
|
|
819
|
+
hash: c.hash.substring(0, 8),
|
|
820
|
+
message: c.message,
|
|
821
|
+
author: c.author_name,
|
|
822
|
+
date: c.date
|
|
823
|
+
}));
|
|
824
|
+
const remotes = remoteList.map((r) => ({
|
|
825
|
+
name: r.name,
|
|
826
|
+
url: (r.refs.fetch || r.refs.push) ?? ""
|
|
827
|
+
}));
|
|
828
|
+
return {
|
|
829
|
+
isRepo: true,
|
|
830
|
+
currentBranch: status.current ?? "unknown",
|
|
831
|
+
hasUncommittedChanges: !status.isClean(),
|
|
832
|
+
stagedFiles: status.staged,
|
|
833
|
+
modifiedFiles: status.modified,
|
|
834
|
+
untrackedFiles: status.not_added,
|
|
835
|
+
aheadOfRemote: status.ahead,
|
|
836
|
+
behindRemote: status.behind,
|
|
837
|
+
recentCommits,
|
|
838
|
+
remotes
|
|
839
|
+
};
|
|
840
|
+
} catch {
|
|
841
|
+
return { ...defaultState2, isRepo: true };
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// packages/memory/dist/store.js
|
|
846
|
+
import { readFile as readFile5, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
|
|
847
|
+
import { existsSync as existsSync6 } from "fs";
|
|
848
|
+
import { dirname } from "path";
|
|
849
|
+
var FileStore = class {
|
|
850
|
+
filePath;
|
|
851
|
+
defaultValue;
|
|
852
|
+
constructor(filePath, defaultValue) {
|
|
853
|
+
this.filePath = filePath;
|
|
854
|
+
this.defaultValue = defaultValue;
|
|
855
|
+
}
|
|
856
|
+
async read() {
|
|
857
|
+
if (!existsSync6(this.filePath)) {
|
|
858
|
+
return structuredClone(this.defaultValue);
|
|
859
|
+
}
|
|
860
|
+
const raw = await readFile5(this.filePath, "utf-8");
|
|
861
|
+
return JSON.parse(raw);
|
|
862
|
+
}
|
|
863
|
+
async write(data) {
|
|
864
|
+
const dir = dirname(this.filePath);
|
|
865
|
+
if (!existsSync6(dir)) {
|
|
866
|
+
await mkdir2(dir, { recursive: true });
|
|
867
|
+
}
|
|
868
|
+
await writeFile3(this.filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
869
|
+
}
|
|
870
|
+
async update(updater) {
|
|
871
|
+
const current = await this.read();
|
|
872
|
+
const updated = updater(current);
|
|
873
|
+
await this.write(updated);
|
|
874
|
+
return updated;
|
|
875
|
+
}
|
|
876
|
+
exists() {
|
|
877
|
+
return existsSync6(this.filePath);
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
// packages/memory/dist/project-memory.js
|
|
882
|
+
import { join as join6 } from "path";
|
|
883
|
+
var DEFAULT_PROJECT_MEMORY = {
|
|
884
|
+
techStack: [],
|
|
885
|
+
conventions: [],
|
|
886
|
+
knownIssues: [],
|
|
887
|
+
architecture: "",
|
|
888
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
889
|
+
};
|
|
890
|
+
var ProjectMemory = class {
|
|
891
|
+
store;
|
|
892
|
+
constructor(modBotDir) {
|
|
893
|
+
this.store = new FileStore(join6(modBotDir, "memory", "project.json"), DEFAULT_PROJECT_MEMORY);
|
|
894
|
+
}
|
|
895
|
+
async load() {
|
|
896
|
+
return this.store.read();
|
|
897
|
+
}
|
|
898
|
+
async save(data) {
|
|
899
|
+
await this.store.write({ ...data, lastUpdated: (/* @__PURE__ */ new Date()).toISOString() });
|
|
900
|
+
}
|
|
901
|
+
async setTechStack(stack) {
|
|
902
|
+
await this.store.update((d) => ({ ...d, techStack: stack, lastUpdated: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
903
|
+
}
|
|
904
|
+
async addConvention(convention) {
|
|
905
|
+
await this.store.update((d) => ({
|
|
906
|
+
...d,
|
|
907
|
+
conventions: [...d.conventions.filter((c) => c !== convention), convention],
|
|
908
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
909
|
+
}));
|
|
910
|
+
}
|
|
911
|
+
async removeConvention(convention) {
|
|
912
|
+
await this.store.update((d) => ({
|
|
913
|
+
...d,
|
|
914
|
+
conventions: d.conventions.filter((c) => c !== convention),
|
|
915
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
916
|
+
}));
|
|
917
|
+
}
|
|
918
|
+
async addKnownIssue(issue) {
|
|
919
|
+
await this.store.update((d) => ({
|
|
920
|
+
...d,
|
|
921
|
+
knownIssues: [...d.knownIssues, issue],
|
|
922
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
923
|
+
}));
|
|
924
|
+
}
|
|
925
|
+
async setArchitecture(description) {
|
|
926
|
+
await this.store.update((d) => ({ ...d, architecture: description, lastUpdated: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
// packages/memory/dist/user-memory.js
|
|
931
|
+
import { join as join7 } from "path";
|
|
932
|
+
var DEFAULT_USER_MEMORY = {
|
|
933
|
+
patterns: [],
|
|
934
|
+
ignoredSuggestions: {},
|
|
935
|
+
workflowPreferences: {}
|
|
936
|
+
};
|
|
937
|
+
var UserMemory = class {
|
|
938
|
+
store;
|
|
939
|
+
constructor(modBotDir) {
|
|
940
|
+
this.store = new FileStore(join7(modBotDir, "memory", "user.json"), DEFAULT_USER_MEMORY);
|
|
941
|
+
}
|
|
942
|
+
async load() {
|
|
943
|
+
return this.store.read();
|
|
944
|
+
}
|
|
945
|
+
async save(data) {
|
|
946
|
+
await this.store.write(data);
|
|
947
|
+
}
|
|
948
|
+
async addPattern(pattern) {
|
|
949
|
+
const data = await this.store.read();
|
|
950
|
+
const existing = data.patterns.find((p) => p.id === pattern.id);
|
|
951
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
952
|
+
if (existing) {
|
|
953
|
+
existing.occurrences += 1;
|
|
954
|
+
existing.lastSeen = now;
|
|
955
|
+
existing.confidence = Math.min(1, existing.confidence + 0.05);
|
|
956
|
+
existing.description = pattern.description;
|
|
957
|
+
await this.store.write(data);
|
|
958
|
+
return existing;
|
|
959
|
+
}
|
|
960
|
+
const newPattern = {
|
|
961
|
+
...pattern,
|
|
962
|
+
occurrences: 1,
|
|
963
|
+
firstSeen: now,
|
|
964
|
+
lastSeen: now
|
|
965
|
+
};
|
|
966
|
+
data.patterns.push(newPattern);
|
|
967
|
+
await this.store.write(data);
|
|
968
|
+
return newPattern;
|
|
969
|
+
}
|
|
970
|
+
async removePattern(id) {
|
|
971
|
+
const data = await this.store.read();
|
|
972
|
+
const before = data.patterns.length;
|
|
973
|
+
data.patterns = data.patterns.filter((p) => p.id !== id);
|
|
974
|
+
if (data.patterns.length < before) {
|
|
975
|
+
await this.store.write(data);
|
|
976
|
+
return true;
|
|
977
|
+
}
|
|
978
|
+
return false;
|
|
979
|
+
}
|
|
980
|
+
async getPatterns() {
|
|
981
|
+
const data = await this.store.read();
|
|
982
|
+
return data.patterns;
|
|
983
|
+
}
|
|
984
|
+
async recordIgnoredSuggestion(key) {
|
|
985
|
+
await this.store.update((d) => {
|
|
986
|
+
const existing = d.ignoredSuggestions[key];
|
|
987
|
+
const count = (existing?.count ?? 0) + 1;
|
|
988
|
+
const action = count >= 5 ? "suppress" : "reduce_frequency";
|
|
989
|
+
return {
|
|
990
|
+
...d,
|
|
991
|
+
ignoredSuggestions: {
|
|
992
|
+
...d.ignoredSuggestions,
|
|
993
|
+
[key]: { count, action }
|
|
994
|
+
}
|
|
995
|
+
};
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
async setWorkflowPreference(key, value) {
|
|
999
|
+
await this.store.update((d) => ({
|
|
1000
|
+
...d,
|
|
1001
|
+
workflowPreferences: { ...d.workflowPreferences, [key]: value }
|
|
1002
|
+
}));
|
|
1003
|
+
}
|
|
1004
|
+
async resetAll() {
|
|
1005
|
+
await this.store.write(structuredClone(DEFAULT_USER_MEMORY));
|
|
1006
|
+
}
|
|
1007
|
+
async resetPatterns() {
|
|
1008
|
+
await this.store.update((d) => ({ ...d, patterns: [] }));
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
|
|
1012
|
+
// packages/memory/dist/session-memory.js
|
|
1013
|
+
import { join as join8 } from "path";
|
|
1014
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1015
|
+
var DEFAULT_SESSION_MEMORY = { sessions: [] };
|
|
1016
|
+
var SessionMemory = class {
|
|
1017
|
+
store;
|
|
1018
|
+
currentSessionId = null;
|
|
1019
|
+
constructor(modBotDir) {
|
|
1020
|
+
this.store = new FileStore(join8(modBotDir, "memory", "sessions.json"), DEFAULT_SESSION_MEMORY);
|
|
1021
|
+
}
|
|
1022
|
+
async load() {
|
|
1023
|
+
return this.store.read();
|
|
1024
|
+
}
|
|
1025
|
+
async startSession(task) {
|
|
1026
|
+
const id = randomUUID2();
|
|
1027
|
+
this.currentSessionId = id;
|
|
1028
|
+
const entry = {
|
|
1029
|
+
id,
|
|
1030
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1031
|
+
task,
|
|
1032
|
+
decisions: []
|
|
1033
|
+
};
|
|
1034
|
+
await this.store.update((d) => ({
|
|
1035
|
+
sessions: [...d.sessions, entry]
|
|
1036
|
+
}));
|
|
1037
|
+
return id;
|
|
1038
|
+
}
|
|
1039
|
+
async endSession(summary) {
|
|
1040
|
+
if (!this.currentSessionId)
|
|
1041
|
+
return;
|
|
1042
|
+
const sessionId = this.currentSessionId;
|
|
1043
|
+
this.currentSessionId = null;
|
|
1044
|
+
await this.store.update((d) => ({
|
|
1045
|
+
sessions: d.sessions.map((s) => s.id === sessionId ? { ...s, endedAt: (/* @__PURE__ */ new Date()).toISOString(), summary } : s)
|
|
1046
|
+
}));
|
|
1047
|
+
}
|
|
1048
|
+
async addDecision(description, rationale, category) {
|
|
1049
|
+
if (!this.currentSessionId)
|
|
1050
|
+
return;
|
|
1051
|
+
const sessionId = this.currentSessionId;
|
|
1052
|
+
const decision = {
|
|
1053
|
+
id: randomUUID2(),
|
|
1054
|
+
description,
|
|
1055
|
+
rationale,
|
|
1056
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1057
|
+
category
|
|
1058
|
+
};
|
|
1059
|
+
await this.store.update((d) => ({
|
|
1060
|
+
sessions: d.sessions.map((s) => s.id === sessionId ? { ...s, decisions: [...s.decisions, decision] } : s)
|
|
1061
|
+
}));
|
|
1062
|
+
}
|
|
1063
|
+
async getRecentSessions(limit = 10) {
|
|
1064
|
+
const data = await this.store.read();
|
|
1065
|
+
return data.sessions.slice(-limit);
|
|
1066
|
+
}
|
|
1067
|
+
async getSession(id) {
|
|
1068
|
+
const data = await this.store.read();
|
|
1069
|
+
return data.sessions.find((s) => s.id === id);
|
|
1070
|
+
}
|
|
1071
|
+
getCurrentSessionId() {
|
|
1072
|
+
return this.currentSessionId;
|
|
1073
|
+
}
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
// packages/memory/dist/pattern-detector.js
|
|
1077
|
+
var PatternDetector = class {
|
|
1078
|
+
userMemory;
|
|
1079
|
+
recentActions = [];
|
|
1080
|
+
constructor(userMemory) {
|
|
1081
|
+
this.userMemory = userMemory;
|
|
1082
|
+
}
|
|
1083
|
+
recordAction(action) {
|
|
1084
|
+
this.recentActions.push({ action, timestamp: Date.now() });
|
|
1085
|
+
if (this.recentActions.length > 100) {
|
|
1086
|
+
this.recentActions = this.recentActions.slice(-100);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
async detectPatterns() {
|
|
1090
|
+
const detected = [];
|
|
1091
|
+
const actionCounts = /* @__PURE__ */ new Map();
|
|
1092
|
+
for (const { action } of this.recentActions) {
|
|
1093
|
+
actionCounts.set(action, (actionCounts.get(action) ?? 0) + 1);
|
|
1094
|
+
}
|
|
1095
|
+
for (const [action, count] of actionCounts) {
|
|
1096
|
+
if (count >= 3) {
|
|
1097
|
+
const confidence = Math.min(1, count / 10);
|
|
1098
|
+
detected.push({
|
|
1099
|
+
id: `auto-${action.toLowerCase().replace(/\s+/g, "-")}`,
|
|
1100
|
+
type: "preference",
|
|
1101
|
+
description: `User frequently: ${action}`,
|
|
1102
|
+
confidence
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
return detected;
|
|
1107
|
+
}
|
|
1108
|
+
async learnPattern(id, type, description, confidence = 0.5) {
|
|
1109
|
+
return this.userMemory.addPattern({ id, type, description, confidence });
|
|
1110
|
+
}
|
|
1111
|
+
async suggestWorkflowAdjustments() {
|
|
1112
|
+
const patterns = await this.userMemory.getPatterns();
|
|
1113
|
+
const suggestions = [];
|
|
1114
|
+
const skipReviewPattern = patterns.find((p) => p.id.includes("skip-review"));
|
|
1115
|
+
if (skipReviewPattern && skipReviewPattern.occurrences >= 5) {
|
|
1116
|
+
suggestions.push(`You've skipped reviews ${skipReviewPattern.occurrences} times. Want me to auto-approve changes under 10 lines?`);
|
|
1117
|
+
}
|
|
1118
|
+
const tddPattern = patterns.find((p) => p.id.includes("tdd") || p.id.includes("test-first"));
|
|
1119
|
+
if (tddPattern && tddPattern.confidence > 0.7) {
|
|
1120
|
+
suggestions.push("You consistently write tests before implementation. Want me to enforce TDD for new features?");
|
|
1121
|
+
}
|
|
1122
|
+
const directImplPattern = patterns.find((p) => p.id.includes("direct-impl") || p.id.includes("skip-plan"));
|
|
1123
|
+
if (directImplPattern && directImplPattern.confidence > 0.6) {
|
|
1124
|
+
suggestions.push("You often skip planning for simple tasks. Want me to only suggest plans for complex tasks?");
|
|
1125
|
+
}
|
|
1126
|
+
return suggestions;
|
|
1127
|
+
}
|
|
1128
|
+
clearRecentActions() {
|
|
1129
|
+
this.recentActions = [];
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1132
|
+
|
|
1133
|
+
// packages/server/src/tools/init.ts
|
|
1134
|
+
function getProjectRoot() {
|
|
1135
|
+
return process.cwd();
|
|
1136
|
+
}
|
|
1137
|
+
function registerInitTools(server2) {
|
|
1138
|
+
server2.tool(
|
|
1139
|
+
"init_setup",
|
|
1140
|
+
"Initialize mod-bot for the current project. Scans the codebase, creates config, and provides recommendations.",
|
|
1141
|
+
{
|
|
1142
|
+
name: z2.string().optional().describe("Your name"),
|
|
1143
|
+
experienceLevel: z2.enum(["junior", "mid", "senior", "lead"]).optional().describe("Your experience level"),
|
|
1144
|
+
primaryLanguages: z2.array(z2.string()).optional().describe("Languages you are strongest in"),
|
|
1145
|
+
learningLanguages: z2.array(z2.string()).optional().describe("Languages you are learning"),
|
|
1146
|
+
projectType: z2.enum(["personal", "team", "enterprise"]).optional().describe("Project type"),
|
|
1147
|
+
priorities: z2.array(z2.string()).optional().describe("Project priorities (e.g., speed, quality, learning)")
|
|
1148
|
+
},
|
|
1149
|
+
async (args) => {
|
|
1150
|
+
const projectRoot = getProjectRoot();
|
|
1151
|
+
const scanResult = await scanProject(projectRoot);
|
|
1152
|
+
await ensureModBotDir(projectRoot);
|
|
1153
|
+
const memory = new ProjectMemory(projectRoot + "/.mod-bot");
|
|
1154
|
+
await memory.setTechStack(
|
|
1155
|
+
scanResult.frameworks.map((f) => ({ language: Object.keys(scanResult.languages)[0] ?? "Unknown", framework: f }))
|
|
1156
|
+
);
|
|
1157
|
+
const userOverrides = {
|
|
1158
|
+
user: {
|
|
1159
|
+
...DEFAULT_CONFIG.user,
|
|
1160
|
+
name: args.name ?? "",
|
|
1161
|
+
experienceLevel: args.experienceLevel ?? "mid",
|
|
1162
|
+
primaryLanguages: args.primaryLanguages ?? Object.keys(scanResult.languages),
|
|
1163
|
+
learningLanguages: args.learningLanguages ?? []
|
|
1164
|
+
},
|
|
1165
|
+
project: {
|
|
1166
|
+
status: scanResult.status,
|
|
1167
|
+
type: args.projectType ?? "personal",
|
|
1168
|
+
priorities: args.priorities ?? ["quality"]
|
|
1169
|
+
}
|
|
1170
|
+
};
|
|
1171
|
+
const config = mergeConfig(DEFAULT_CONFIG, userOverrides);
|
|
1172
|
+
await saveConfig(projectRoot, config);
|
|
1173
|
+
const recommendations = [];
|
|
1174
|
+
if (!scanResult.hasTests) {
|
|
1175
|
+
recommendations.push("No tests detected. Consider setting up a test framework and enabling TDD enforcement.");
|
|
1176
|
+
} else if (scanResult.testFrameworks.length > 0) {
|
|
1177
|
+
recommendations.push(`Test framework detected: ${scanResult.testFrameworks.join(", ")}. TDD suggestion mode is active.`);
|
|
1178
|
+
}
|
|
1179
|
+
if (!scanResult.hasCICD) {
|
|
1180
|
+
recommendations.push("No CI/CD pipeline detected. Consider adding GitHub Actions or similar.");
|
|
1181
|
+
}
|
|
1182
|
+
if (!scanResult.hasDocumentation) {
|
|
1183
|
+
recommendations.push("No documentation directory found. Consider adding a docs/ folder.");
|
|
1184
|
+
}
|
|
1185
|
+
if (scanResult.status === "greenfield") {
|
|
1186
|
+
recommendations.push("This looks like a new project. Mod-bot will provide extra guidance during initial setup.");
|
|
1187
|
+
}
|
|
1188
|
+
const languageSummary = Object.entries(scanResult.languages).sort(([, a], [, b]) => b - a).map(([lang, count]) => `${lang} (${count} files)`).join(", ");
|
|
1189
|
+
const output = [
|
|
1190
|
+
`# Mod-Bot Initialized`,
|
|
1191
|
+
``,
|
|
1192
|
+
`## Project Profile`,
|
|
1193
|
+
`- **Status:** ${scanResult.status}`,
|
|
1194
|
+
`- **Languages:** ${languageSummary || "none detected"}`,
|
|
1195
|
+
`- **Frameworks:** ${scanResult.frameworks.join(", ") || "none detected"}`,
|
|
1196
|
+
`- **Tests:** ${scanResult.hasTests ? `Yes (${scanResult.testFrameworks.join(", ")})` : "No"}`,
|
|
1197
|
+
`- **CI/CD:** ${scanResult.hasCICD ? scanResult.cicdTools.join(", ") : "No"}`,
|
|
1198
|
+
`- **Total Files:** ${scanResult.totalFiles}`,
|
|
1199
|
+
`- **Estimated Lines:** ${scanResult.totalLines.toLocaleString()}`,
|
|
1200
|
+
``,
|
|
1201
|
+
`## User Profile`,
|
|
1202
|
+
`- **Name:** ${config.user.name || "(not set)"}`,
|
|
1203
|
+
`- **Level:** ${config.user.experienceLevel}`,
|
|
1204
|
+
`- **Explanation Depth:** ${config.user.explanationDepth}`,
|
|
1205
|
+
`- **Workflow:** ${config.user.workflowPreference}`,
|
|
1206
|
+
``,
|
|
1207
|
+
`## Recommendations`,
|
|
1208
|
+
...recommendations.map((r) => `- ${r}`),
|
|
1209
|
+
``,
|
|
1210
|
+
`Configuration saved to \`.mod-bot/config.yaml\`. Use \`init:reconfig\` to update preferences.`
|
|
1211
|
+
];
|
|
1212
|
+
return { content: [{ type: "text", text: output.join("\n") }] };
|
|
1213
|
+
}
|
|
1214
|
+
);
|
|
1215
|
+
server2.tool(
|
|
1216
|
+
"init_reconfig",
|
|
1217
|
+
"Update your mod-bot preferences without re-scanning the project.",
|
|
1218
|
+
{
|
|
1219
|
+
name: z2.string().optional().describe("Your name"),
|
|
1220
|
+
experienceLevel: z2.enum(["junior", "mid", "senior", "lead"]).optional(),
|
|
1221
|
+
primaryLanguages: z2.array(z2.string()).optional(),
|
|
1222
|
+
learningLanguages: z2.array(z2.string()).optional(),
|
|
1223
|
+
visualPreference: z2.enum(["minimal", "moderate", "verbose"]).optional(),
|
|
1224
|
+
explanationDepth: z2.enum(["terse", "standard", "detailed"]).optional(),
|
|
1225
|
+
workflowPreference: z2.enum(["guided", "autonomous", "adaptive"]).optional(),
|
|
1226
|
+
tddEnforcement: z2.enum(["off", "suggest", "enforce"]).optional(),
|
|
1227
|
+
swarmBudget: z2.enum(["conservative", "moderate", "unlimited"]).optional(),
|
|
1228
|
+
reviewBeforeCommit: z2.boolean().optional()
|
|
1229
|
+
},
|
|
1230
|
+
async (args) => {
|
|
1231
|
+
const projectRoot = getProjectRoot();
|
|
1232
|
+
if (!isInitialized(projectRoot)) {
|
|
1233
|
+
return {
|
|
1234
|
+
content: [{ type: "text", text: "Mod-bot is not initialized. Run `init:setup` first." }],
|
|
1235
|
+
isError: true
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
const current = await loadConfig(projectRoot);
|
|
1239
|
+
const updated = mergeConfig(current, {
|
|
1240
|
+
user: {
|
|
1241
|
+
...current.user,
|
|
1242
|
+
...args.name !== void 0 && { name: args.name },
|
|
1243
|
+
...args.experienceLevel !== void 0 && { experienceLevel: args.experienceLevel },
|
|
1244
|
+
...args.primaryLanguages !== void 0 && { primaryLanguages: args.primaryLanguages },
|
|
1245
|
+
...args.learningLanguages !== void 0 && { learningLanguages: args.learningLanguages },
|
|
1246
|
+
...args.visualPreference !== void 0 && { visualPreference: args.visualPreference },
|
|
1247
|
+
...args.explanationDepth !== void 0 && { explanationDepth: args.explanationDepth },
|
|
1248
|
+
...args.workflowPreference !== void 0 && { workflowPreference: args.workflowPreference }
|
|
1249
|
+
},
|
|
1250
|
+
moderation: {
|
|
1251
|
+
...current.moderation,
|
|
1252
|
+
...args.tddEnforcement !== void 0 && { tddEnforcement: args.tddEnforcement },
|
|
1253
|
+
...args.swarmBudget !== void 0 && { swarmBudget: args.swarmBudget },
|
|
1254
|
+
...args.reviewBeforeCommit !== void 0 && { reviewBeforeCommit: args.reviewBeforeCommit }
|
|
1255
|
+
}
|
|
1256
|
+
});
|
|
1257
|
+
await saveConfig(projectRoot, updated);
|
|
1258
|
+
return {
|
|
1259
|
+
content: [{
|
|
1260
|
+
type: "text",
|
|
1261
|
+
text: `Configuration updated. Level: ${updated.user.experienceLevel}, Depth: ${updated.user.explanationDepth}, Workflow: ${updated.user.workflowPreference}`
|
|
1262
|
+
}]
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
);
|
|
1266
|
+
server2.tool(
|
|
1267
|
+
"init_migrate",
|
|
1268
|
+
"Migrate mod-bot config to the latest version.",
|
|
1269
|
+
{},
|
|
1270
|
+
async () => {
|
|
1271
|
+
const projectRoot = getProjectRoot();
|
|
1272
|
+
if (!isInitialized(projectRoot)) {
|
|
1273
|
+
return {
|
|
1274
|
+
content: [{ type: "text", text: "Mod-bot is not initialized. Run `init:setup` first." }],
|
|
1275
|
+
isError: true
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
const config = await loadConfig(projectRoot);
|
|
1279
|
+
config.version = "1.0.0";
|
|
1280
|
+
await saveConfig(projectRoot, config);
|
|
1281
|
+
return {
|
|
1282
|
+
content: [{ type: "text", text: "Config migrated to v1.0.0." }]
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// packages/server/src/tools/moderate.ts
|
|
1289
|
+
import { z as z3 } from "zod";
|
|
1290
|
+
function getProjectRoot2() {
|
|
1291
|
+
return process.cwd();
|
|
1292
|
+
}
|
|
1293
|
+
async function getModerator() {
|
|
1294
|
+
const projectRoot = getProjectRoot2();
|
|
1295
|
+
if (!isInitialized(projectRoot)) return null;
|
|
1296
|
+
const config = await loadConfig(projectRoot);
|
|
1297
|
+
const projectState = await loadProjectState(projectRoot);
|
|
1298
|
+
const userMemory = new UserMemory(projectRoot + "/.mod-bot");
|
|
1299
|
+
const userMemoryData = await userMemory.load();
|
|
1300
|
+
const ctx = {
|
|
1301
|
+
config,
|
|
1302
|
+
projectState,
|
|
1303
|
+
userMemory: userMemoryData,
|
|
1304
|
+
projectRoot
|
|
1305
|
+
};
|
|
1306
|
+
return new Moderator(ctx);
|
|
1307
|
+
}
|
|
1308
|
+
function registerModerateTools(server2) {
|
|
1309
|
+
server2.tool(
|
|
1310
|
+
"mod_suggest",
|
|
1311
|
+
"Get a suggestion for how to approach a task, adapted to your experience level.",
|
|
1312
|
+
{
|
|
1313
|
+
task: z3.string().describe("Description of the task"),
|
|
1314
|
+
fileCount: z3.number().optional().describe("Number of files involved"),
|
|
1315
|
+
isNewFeature: z3.boolean().optional().describe("Is this a new feature?"),
|
|
1316
|
+
touchesMultipleModules: z3.boolean().optional().describe("Does this touch multiple modules?")
|
|
1317
|
+
},
|
|
1318
|
+
async (args) => {
|
|
1319
|
+
const moderator = await getModerator();
|
|
1320
|
+
if (!moderator) {
|
|
1321
|
+
return {
|
|
1322
|
+
content: [{ type: "text", text: "Mod-bot is not initialized. Run `init:setup` first." }],
|
|
1323
|
+
isError: true
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
const context = {
|
|
1327
|
+
fileCount: args.fileCount,
|
|
1328
|
+
isNewFeature: args.isNewFeature,
|
|
1329
|
+
touchesMultipleModules: args.touchesMultipleModules
|
|
1330
|
+
};
|
|
1331
|
+
const complexity = assessComplexity(args.task, context);
|
|
1332
|
+
const result = moderator.suggest(args.task, complexity);
|
|
1333
|
+
const projectRoot = getProjectRoot2();
|
|
1334
|
+
await updateProjectState(projectRoot, (s) => setTask(s, args.task));
|
|
1335
|
+
return { content: [{ type: "text", text: result.content }] };
|
|
1336
|
+
}
|
|
1337
|
+
);
|
|
1338
|
+
server2.tool(
|
|
1339
|
+
"mod_review",
|
|
1340
|
+
"Review code changes before committing. Checks for issues adapted to your level.",
|
|
1341
|
+
{
|
|
1342
|
+
changes: z3.string().describe("The diff or description of changes to review"),
|
|
1343
|
+
context: z3.string().optional().describe("Additional context about what was changed")
|
|
1344
|
+
},
|
|
1345
|
+
async (args) => {
|
|
1346
|
+
const moderator = await getModerator();
|
|
1347
|
+
if (!moderator) {
|
|
1348
|
+
return {
|
|
1349
|
+
content: [{ type: "text", text: "Mod-bot is not initialized. Run `init:setup` first." }],
|
|
1350
|
+
isError: true
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
const result = moderator.review(args.changes, args.context);
|
|
1354
|
+
return { content: [{ type: "text", text: result.content }] };
|
|
1355
|
+
}
|
|
1356
|
+
);
|
|
1357
|
+
server2.tool(
|
|
1358
|
+
"mod_plan",
|
|
1359
|
+
"Create a moderated implementation plan adapted to your experience level.",
|
|
1360
|
+
{
|
|
1361
|
+
task: z3.string().describe("Description of what to implement"),
|
|
1362
|
+
fileCount: z3.number().optional().describe("Estimated number of files involved")
|
|
1363
|
+
},
|
|
1364
|
+
async (args) => {
|
|
1365
|
+
const moderator = await getModerator();
|
|
1366
|
+
if (!moderator) {
|
|
1367
|
+
return {
|
|
1368
|
+
content: [{ type: "text", text: "Mod-bot is not initialized. Run `init:setup` first." }],
|
|
1369
|
+
isError: true
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
const complexity = assessComplexity(args.task, { fileCount: args.fileCount });
|
|
1373
|
+
const result = moderator.plan(args.task, complexity);
|
|
1374
|
+
return { content: [{ type: "text", text: result.content }] };
|
|
1375
|
+
}
|
|
1376
|
+
);
|
|
1377
|
+
server2.tool(
|
|
1378
|
+
"mod_checkpoint",
|
|
1379
|
+
"Create a checkpoint summarizing current progress, decisions, and open questions.",
|
|
1380
|
+
{
|
|
1381
|
+
note: z3.string().optional().describe("Additional note to include in the checkpoint")
|
|
1382
|
+
},
|
|
1383
|
+
async (args) => {
|
|
1384
|
+
const moderator = await getModerator();
|
|
1385
|
+
if (!moderator) {
|
|
1386
|
+
return {
|
|
1387
|
+
content: [{ type: "text", text: "Mod-bot is not initialized. Run `init:setup` first." }],
|
|
1388
|
+
isError: true
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
const result = moderator.checkpoint();
|
|
1392
|
+
let text = result.content;
|
|
1393
|
+
if (args.note) {
|
|
1394
|
+
text += `
|
|
1395
|
+
|
|
1396
|
+
**Note:** ${args.note}`;
|
|
1397
|
+
}
|
|
1398
|
+
return { content: [{ type: "text", text }] };
|
|
1399
|
+
}
|
|
1400
|
+
);
|
|
1401
|
+
server2.tool(
|
|
1402
|
+
"mod_retro",
|
|
1403
|
+
"Run a brief retrospective after completing a task.",
|
|
1404
|
+
{
|
|
1405
|
+
task: z3.string().describe("Summary of what was accomplished"),
|
|
1406
|
+
decision: z3.string().optional().describe("Key decision made during the task"),
|
|
1407
|
+
decisionRationale: z3.string().optional().describe("Why that decision was made")
|
|
1408
|
+
},
|
|
1409
|
+
async (args) => {
|
|
1410
|
+
const moderator = await getModerator();
|
|
1411
|
+
if (!moderator) {
|
|
1412
|
+
return {
|
|
1413
|
+
content: [{ type: "text", text: "Mod-bot is not initialized. Run `init:setup` first." }],
|
|
1414
|
+
isError: true
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
if (args.decision && args.decisionRationale) {
|
|
1418
|
+
const projectRoot = getProjectRoot2();
|
|
1419
|
+
await updateProjectState(
|
|
1420
|
+
projectRoot,
|
|
1421
|
+
(s) => addDecision(s, args.decision, args.decisionRationale, "implementation")
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
const result = moderator.retro(args.task);
|
|
1425
|
+
return { content: [{ type: "text", text: result.content }] };
|
|
1426
|
+
}
|
|
1427
|
+
);
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// packages/server/src/tools/context.ts
|
|
1431
|
+
import { z as z4 } from "zod";
|
|
1432
|
+
function getProjectRoot3() {
|
|
1433
|
+
return process.cwd();
|
|
1434
|
+
}
|
|
1435
|
+
function registerContextTools(server2) {
|
|
1436
|
+
server2.tool(
|
|
1437
|
+
"ctx_status",
|
|
1438
|
+
"Show the current project status: branch, task, recent changes, focus area, and health.",
|
|
1439
|
+
{},
|
|
1440
|
+
async () => {
|
|
1441
|
+
const projectRoot = getProjectRoot3();
|
|
1442
|
+
if (!isInitialized(projectRoot)) {
|
|
1443
|
+
return {
|
|
1444
|
+
content: [{ type: "text", text: "Mod-bot is not initialized. Run `init:setup` first." }],
|
|
1445
|
+
isError: true
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
const [config, state, gitState] = await Promise.all([
|
|
1449
|
+
loadConfig(projectRoot),
|
|
1450
|
+
loadProjectState(projectRoot),
|
|
1451
|
+
getGitState(projectRoot)
|
|
1452
|
+
]);
|
|
1453
|
+
const lines = [
|
|
1454
|
+
`# Project Status`,
|
|
1455
|
+
``,
|
|
1456
|
+
`## Git`,
|
|
1457
|
+
`- **Branch:** ${gitState.currentBranch || state.currentBranch}`,
|
|
1458
|
+
`- **Uncommitted changes:** ${gitState.hasUncommittedChanges ? "Yes" : "No"}`
|
|
1459
|
+
];
|
|
1460
|
+
if (gitState.stagedFiles.length > 0) {
|
|
1461
|
+
lines.push(`- **Staged:** ${gitState.stagedFiles.length} files`);
|
|
1462
|
+
}
|
|
1463
|
+
if (gitState.modifiedFiles.length > 0) {
|
|
1464
|
+
lines.push(`- **Modified:** ${gitState.modifiedFiles.length} files`);
|
|
1465
|
+
}
|
|
1466
|
+
if (gitState.aheadOfRemote > 0) {
|
|
1467
|
+
lines.push(`- **Ahead of remote:** ${gitState.aheadOfRemote} commits`);
|
|
1468
|
+
}
|
|
1469
|
+
if (gitState.behindRemote > 0) {
|
|
1470
|
+
lines.push(`- **Behind remote:** ${gitState.behindRemote} commits`);
|
|
1471
|
+
}
|
|
1472
|
+
lines.push(``);
|
|
1473
|
+
lines.push(`## Mod-Bot`);
|
|
1474
|
+
lines.push(`- **User Level:** ${config.user.experienceLevel}`);
|
|
1475
|
+
if (state.currentTask) {
|
|
1476
|
+
lines.push(`- **Current Task:** ${state.currentTask}`);
|
|
1477
|
+
}
|
|
1478
|
+
if (state.focusArea) {
|
|
1479
|
+
lines.push(`- **Focus Area:** ${state.focusArea}`);
|
|
1480
|
+
}
|
|
1481
|
+
if (state.openQuestions.length > 0) {
|
|
1482
|
+
lines.push(``, `## Open Questions`);
|
|
1483
|
+
for (const q of state.openQuestions) {
|
|
1484
|
+
lines.push(`- ${q}`);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
if (state.healthMetrics.buildPassing !== void 0) {
|
|
1488
|
+
lines.push(``, `## Health`);
|
|
1489
|
+
lines.push(`- **Build:** ${state.healthMetrics.buildPassing ? "Passing" : "Failing"}`);
|
|
1490
|
+
if (state.healthMetrics.testPassRate !== void 0) {
|
|
1491
|
+
lines.push(`- **Test Pass Rate:** ${(state.healthMetrics.testPassRate * 100).toFixed(0)}%`);
|
|
1492
|
+
}
|
|
1493
|
+
if (state.healthMetrics.lintErrorCount !== void 0) {
|
|
1494
|
+
lines.push(`- **Lint Errors:** ${state.healthMetrics.lintErrorCount}`);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
if (gitState.recentCommits.length > 0) {
|
|
1498
|
+
lines.push(``, `## Recent Commits`);
|
|
1499
|
+
for (const c of gitState.recentCommits.slice(0, 5)) {
|
|
1500
|
+
lines.push(`- \`${c.hash}\` ${c.message}`);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1504
|
+
}
|
|
1505
|
+
);
|
|
1506
|
+
server2.tool(
|
|
1507
|
+
"ctx_refresh",
|
|
1508
|
+
"Force a full context refresh: re-scan the codebase and update the knowledge graph.",
|
|
1509
|
+
{},
|
|
1510
|
+
async () => {
|
|
1511
|
+
const projectRoot = getProjectRoot3();
|
|
1512
|
+
if (!isInitialized(projectRoot)) {
|
|
1513
|
+
return {
|
|
1514
|
+
content: [{ type: "text", text: "Mod-bot is not initialized. Run `init:setup` first." }],
|
|
1515
|
+
isError: true
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
const [scanResult, gitState] = await Promise.all([
|
|
1519
|
+
scanProject(projectRoot),
|
|
1520
|
+
getGitState(projectRoot)
|
|
1521
|
+
]);
|
|
1522
|
+
await updateProjectState(projectRoot, (s) => ({
|
|
1523
|
+
...s,
|
|
1524
|
+
lastScanAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1525
|
+
currentBranch: gitState.currentBranch || s.currentBranch
|
|
1526
|
+
}));
|
|
1527
|
+
const lines = [
|
|
1528
|
+
`Context refreshed.`,
|
|
1529
|
+
`- **Files:** ${scanResult.totalFiles}`,
|
|
1530
|
+
`- **Languages:** ${Object.keys(scanResult.languages).join(", ")}`,
|
|
1531
|
+
`- **Branch:** ${gitState.currentBranch}`,
|
|
1532
|
+
`- **Uncommitted:** ${gitState.hasUncommittedChanges ? "Yes" : "No"}`
|
|
1533
|
+
];
|
|
1534
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1535
|
+
}
|
|
1536
|
+
);
|
|
1537
|
+
server2.tool(
|
|
1538
|
+
"ctx_focus",
|
|
1539
|
+
"Set the current focus area so mod-bot prioritizes context from this area.",
|
|
1540
|
+
{
|
|
1541
|
+
area: z4.string().describe('The focus area (e.g., "authentication module", "API endpoints")')
|
|
1542
|
+
},
|
|
1543
|
+
async (args) => {
|
|
1544
|
+
const projectRoot = getProjectRoot3();
|
|
1545
|
+
if (!isInitialized(projectRoot)) {
|
|
1546
|
+
return {
|
|
1547
|
+
content: [{ type: "text", text: "Mod-bot is not initialized. Run `init:setup` first." }],
|
|
1548
|
+
isError: true
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
await updateProjectState(projectRoot, (s) => setFocus(s, args.area));
|
|
1552
|
+
return {
|
|
1553
|
+
content: [{ type: "text", text: `Focus set to: **${args.area}**` }]
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
);
|
|
1557
|
+
server2.tool(
|
|
1558
|
+
"ctx_history",
|
|
1559
|
+
"Show the session history and decision log for the current project.",
|
|
1560
|
+
{
|
|
1561
|
+
limit: z4.number().optional().describe("Number of sessions to show (default: 10)")
|
|
1562
|
+
},
|
|
1563
|
+
async (args) => {
|
|
1564
|
+
const projectRoot = getProjectRoot3();
|
|
1565
|
+
if (!isInitialized(projectRoot)) {
|
|
1566
|
+
return {
|
|
1567
|
+
content: [{ type: "text", text: "Mod-bot is not initialized. Run `init:setup` first." }],
|
|
1568
|
+
isError: true
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
const sessionMemory = new SessionMemory(projectRoot + "/.mod-bot");
|
|
1572
|
+
const sessions = await sessionMemory.getRecentSessions(args.limit ?? 10);
|
|
1573
|
+
const state = await loadProjectState(projectRoot);
|
|
1574
|
+
const lines = [`# Session History`, ``];
|
|
1575
|
+
if (sessions.length === 0) {
|
|
1576
|
+
lines.push("No sessions recorded yet.");
|
|
1577
|
+
} else {
|
|
1578
|
+
for (const session of sessions.reverse()) {
|
|
1579
|
+
const status = session.endedAt ? "completed" : "in progress";
|
|
1580
|
+
lines.push(`## ${session.task}`);
|
|
1581
|
+
lines.push(`- **Started:** ${session.startedAt}`);
|
|
1582
|
+
lines.push(`- **Status:** ${status}`);
|
|
1583
|
+
if (session.summary) {
|
|
1584
|
+
lines.push(`- **Summary:** ${session.summary}`);
|
|
1585
|
+
}
|
|
1586
|
+
if (session.decisions.length > 0) {
|
|
1587
|
+
lines.push(`- **Decisions:** ${session.decisions.length}`);
|
|
1588
|
+
}
|
|
1589
|
+
lines.push(``);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
if (state.decisions.length > 0) {
|
|
1593
|
+
lines.push(`# Decision Log`, ``);
|
|
1594
|
+
for (const d of state.decisions.slice(-10)) {
|
|
1595
|
+
lines.push(`- **${d.description}** \u2014 ${d.rationale} _(${d.timestamp})_`);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1599
|
+
}
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
// packages/server/src/tools/learn.ts
|
|
1604
|
+
import { z as z5 } from "zod";
|
|
1605
|
+
function getProjectRoot4() {
|
|
1606
|
+
return process.cwd();
|
|
1607
|
+
}
|
|
1608
|
+
function getModBotDir2() {
|
|
1609
|
+
return getProjectRoot4() + "/.mod-bot";
|
|
1610
|
+
}
|
|
1611
|
+
function registerLearnTools(server2) {
|
|
1612
|
+
server2.tool(
|
|
1613
|
+
"learn_pattern",
|
|
1614
|
+
'Teach mod-bot a coding pattern or preference (e.g., "I always use early returns").',
|
|
1615
|
+
{
|
|
1616
|
+
id: z5.string().describe('A short identifier for the pattern (e.g., "early-returns")'),
|
|
1617
|
+
type: z5.enum(["code_style", "workflow", "naming", "architecture", "testing", "preference"]).describe("Pattern category"),
|
|
1618
|
+
description: z5.string().describe("Description of the pattern or preference"),
|
|
1619
|
+
confidence: z5.number().min(0).max(1).optional().describe("How confident you are (0-1, default: 0.8)")
|
|
1620
|
+
},
|
|
1621
|
+
async (args) => {
|
|
1622
|
+
if (!isInitialized(getProjectRoot4())) {
|
|
1623
|
+
return {
|
|
1624
|
+
content: [{ type: "text", text: "Mod-bot is not initialized. Run `init:setup` first." }],
|
|
1625
|
+
isError: true
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
const userMemory = new UserMemory(getModBotDir2());
|
|
1629
|
+
const pattern = await userMemory.addPattern({
|
|
1630
|
+
id: args.id,
|
|
1631
|
+
type: args.type,
|
|
1632
|
+
description: args.description,
|
|
1633
|
+
confidence: args.confidence ?? 0.8
|
|
1634
|
+
});
|
|
1635
|
+
return {
|
|
1636
|
+
content: [{
|
|
1637
|
+
type: "text",
|
|
1638
|
+
text: `Pattern learned: **${pattern.id}** (${pattern.type})
|
|
1639
|
+
${pattern.description}
|
|
1640
|
+
Confidence: ${(pattern.confidence * 100).toFixed(0)}% | Occurrences: ${pattern.occurrences}`
|
|
1641
|
+
}]
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
);
|
|
1645
|
+
server2.tool(
|
|
1646
|
+
"learn_show",
|
|
1647
|
+
"Show what mod-bot has learned about you and your project.",
|
|
1648
|
+
{
|
|
1649
|
+
type: z5.enum(["code_style", "workflow", "naming", "architecture", "testing", "preference", "all"]).optional().describe("Filter by pattern type")
|
|
1650
|
+
},
|
|
1651
|
+
async (args) => {
|
|
1652
|
+
if (!isInitialized(getProjectRoot4())) {
|
|
1653
|
+
return {
|
|
1654
|
+
content: [{ type: "text", text: "Mod-bot is not initialized. Run `init:setup` first." }],
|
|
1655
|
+
isError: true
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
const userMemory = new UserMemory(getModBotDir2());
|
|
1659
|
+
const data = await userMemory.load();
|
|
1660
|
+
let patterns = data.patterns;
|
|
1661
|
+
if (args.type && args.type !== "all") {
|
|
1662
|
+
patterns = patterns.filter((p) => p.type === args.type);
|
|
1663
|
+
}
|
|
1664
|
+
const lines = [`# Learned Patterns`, ``];
|
|
1665
|
+
if (patterns.length === 0) {
|
|
1666
|
+
lines.push("No patterns learned yet. Use `learn:pattern` to teach me your preferences.");
|
|
1667
|
+
} else {
|
|
1668
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1669
|
+
for (const p of patterns) {
|
|
1670
|
+
const group = grouped.get(p.type) ?? [];
|
|
1671
|
+
group.push(p);
|
|
1672
|
+
grouped.set(p.type, group);
|
|
1673
|
+
}
|
|
1674
|
+
for (const [type, group] of grouped) {
|
|
1675
|
+
lines.push(`## ${type}`);
|
|
1676
|
+
for (const p of group) {
|
|
1677
|
+
lines.push(`- **${p.id}**: ${p.description}`);
|
|
1678
|
+
lines.push(` Confidence: ${(p.confidence * 100).toFixed(0)}% | Seen ${p.occurrences}x | Last: ${p.lastSeen}`);
|
|
1679
|
+
}
|
|
1680
|
+
lines.push(``);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
const ignoredCount = Object.keys(data.ignoredSuggestions).length;
|
|
1684
|
+
if (ignoredCount > 0) {
|
|
1685
|
+
lines.push(`## Suppressed Suggestions`);
|
|
1686
|
+
for (const [key, value] of Object.entries(data.ignoredSuggestions)) {
|
|
1687
|
+
lines.push(`- **${key}**: ignored ${value.count}x (${value.action})`);
|
|
1688
|
+
}
|
|
1689
|
+
lines.push(``);
|
|
1690
|
+
}
|
|
1691
|
+
const prefCount = Object.keys(data.workflowPreferences).length;
|
|
1692
|
+
if (prefCount > 0) {
|
|
1693
|
+
lines.push(`## Workflow Preferences`);
|
|
1694
|
+
for (const [key, value] of Object.entries(data.workflowPreferences)) {
|
|
1695
|
+
lines.push(`- ${key}: ${value ? "enabled" : "disabled"}`);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1699
|
+
}
|
|
1700
|
+
);
|
|
1701
|
+
server2.tool(
|
|
1702
|
+
"learn_reset",
|
|
1703
|
+
"Reset specific learned patterns or all learned data.",
|
|
1704
|
+
{
|
|
1705
|
+
patternId: z5.string().optional().describe("ID of a specific pattern to remove"),
|
|
1706
|
+
resetAll: z5.boolean().optional().describe("Reset all learned patterns (default: false)")
|
|
1707
|
+
},
|
|
1708
|
+
async (args) => {
|
|
1709
|
+
if (!isInitialized(getProjectRoot4())) {
|
|
1710
|
+
return {
|
|
1711
|
+
content: [{ type: "text", text: "Mod-bot is not initialized. Run `init:setup` first." }],
|
|
1712
|
+
isError: true
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
const userMemory = new UserMemory(getModBotDir2());
|
|
1716
|
+
if (args.resetAll) {
|
|
1717
|
+
await userMemory.resetAll();
|
|
1718
|
+
return {
|
|
1719
|
+
content: [{ type: "text", text: "All learned patterns have been reset." }]
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
if (args.patternId) {
|
|
1723
|
+
const removed = await userMemory.removePattern(args.patternId);
|
|
1724
|
+
if (removed) {
|
|
1725
|
+
return {
|
|
1726
|
+
content: [{ type: "text", text: `Pattern "${args.patternId}" removed.` }]
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
return {
|
|
1730
|
+
content: [{ type: "text", text: `Pattern "${args.patternId}" not found.` }],
|
|
1731
|
+
isError: true
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
return {
|
|
1735
|
+
content: [{ type: "text", text: "Specify a patternId to remove or set resetAll to true." }],
|
|
1736
|
+
isError: true
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
);
|
|
1740
|
+
server2.tool(
|
|
1741
|
+
"learn_suggest-workflow",
|
|
1742
|
+
"Get workflow adjustment suggestions based on your accumulated patterns.",
|
|
1743
|
+
{},
|
|
1744
|
+
async () => {
|
|
1745
|
+
if (!isInitialized(getProjectRoot4())) {
|
|
1746
|
+
return {
|
|
1747
|
+
content: [{ type: "text", text: "Mod-bot is not initialized. Run `init:setup` first." }],
|
|
1748
|
+
isError: true
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
const userMemory = new UserMemory(getModBotDir2());
|
|
1752
|
+
const detector = new PatternDetector(userMemory);
|
|
1753
|
+
const suggestions = await detector.suggestWorkflowAdjustments();
|
|
1754
|
+
if (suggestions.length === 0) {
|
|
1755
|
+
return {
|
|
1756
|
+
content: [{
|
|
1757
|
+
type: "text",
|
|
1758
|
+
text: "No workflow suggestions yet. Keep using mod-bot and I'll learn your patterns over time."
|
|
1759
|
+
}]
|
|
1760
|
+
};
|
|
1761
|
+
}
|
|
1762
|
+
const lines = [
|
|
1763
|
+
`# Workflow Suggestions`,
|
|
1764
|
+
``,
|
|
1765
|
+
`Based on your usage patterns:`,
|
|
1766
|
+
``,
|
|
1767
|
+
...suggestions.map((s) => `- ${s}`),
|
|
1768
|
+
``,
|
|
1769
|
+
`Use \`init:reconfig\` to apply any of these suggestions.`
|
|
1770
|
+
];
|
|
1771
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1772
|
+
}
|
|
1773
|
+
);
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
// packages/server/src/tools/swarm.ts
|
|
1777
|
+
import { z as z6 } from "zod";
|
|
1778
|
+
function getProjectRoot5() {
|
|
1779
|
+
return process.cwd();
|
|
1780
|
+
}
|
|
1781
|
+
var TOKENS_PER_AGENT = 5e3;
|
|
1782
|
+
var COST_PER_1K_TOKENS = 3e-3;
|
|
1783
|
+
function estimateSwarm(_taskDescription, agentCount, budget) {
|
|
1784
|
+
const estimatedTokens = agentCount * TOKENS_PER_AGENT;
|
|
1785
|
+
const estimatedCostUsd = estimatedTokens / 1e3 * COST_PER_1K_TOKENS;
|
|
1786
|
+
const budgetLimits = {
|
|
1787
|
+
conservative: 0.05,
|
|
1788
|
+
moderate: 0.25,
|
|
1789
|
+
unlimited: Infinity
|
|
1790
|
+
};
|
|
1791
|
+
const limit = budgetLimits[budget] ?? 0.05;
|
|
1792
|
+
return {
|
|
1793
|
+
agentCount,
|
|
1794
|
+
estimatedTokens,
|
|
1795
|
+
estimatedCostUsd,
|
|
1796
|
+
tasks: [],
|
|
1797
|
+
withinBudget: estimatedCostUsd <= limit
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
function registerSwarmTools(server2) {
|
|
1801
|
+
server2.tool(
|
|
1802
|
+
"swarm_estimate",
|
|
1803
|
+
"Estimate the token cost of a multi-agent swarm before spawning it.",
|
|
1804
|
+
{
|
|
1805
|
+
task: z6.string().describe("Description of the task"),
|
|
1806
|
+
agentCount: z6.number().min(2).max(8).optional().describe("Number of agents (default: auto-determined)")
|
|
1807
|
+
},
|
|
1808
|
+
async (args) => {
|
|
1809
|
+
if (!isInitialized(getProjectRoot5())) {
|
|
1810
|
+
return {
|
|
1811
|
+
content: [{ type: "text", text: "Mod-bot is not initialized. Run `init:setup` first." }],
|
|
1812
|
+
isError: true
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1815
|
+
const config = await loadConfig(getProjectRoot5());
|
|
1816
|
+
const complexity = assessComplexity(args.task);
|
|
1817
|
+
const agentCount = args.agentCount ?? (complexity.level === "epic" ? 6 : complexity.level === "complex" ? 4 : 3);
|
|
1818
|
+
const estimate = estimateSwarm(args.task, agentCount, config.moderation.swarmBudget);
|
|
1819
|
+
const lines = [
|
|
1820
|
+
`## Swarm Estimate`,
|
|
1821
|
+
``,
|
|
1822
|
+
`- **Task Complexity:** ${complexity.level} (${complexity.score}/100)`,
|
|
1823
|
+
`- **Agents:** ${estimate.agentCount}`,
|
|
1824
|
+
`- **Estimated Tokens:** ~${estimate.estimatedTokens.toLocaleString()}`,
|
|
1825
|
+
`- **Estimated Cost:** $${estimate.estimatedCostUsd.toFixed(4)}`,
|
|
1826
|
+
`- **Budget:** ${config.moderation.swarmBudget}`,
|
|
1827
|
+
`- **Within Budget:** ${estimate.withinBudget ? "Yes" : "No \u2014 increase budget with `init:reconfig`"}`
|
|
1828
|
+
];
|
|
1829
|
+
if (!estimate.withinBudget) {
|
|
1830
|
+
lines.push(``, `> The estimated cost exceeds your ${config.moderation.swarmBudget} budget. Consider:`, `> - Reducing agent count`, `> - Increasing budget via \`init:reconfig\``, `> - Breaking the task into smaller pieces`);
|
|
1831
|
+
}
|
|
1832
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1833
|
+
}
|
|
1834
|
+
);
|
|
1835
|
+
server2.tool(
|
|
1836
|
+
"swarm_analyze",
|
|
1837
|
+
"Spawn a focused analysis swarm for complex tasks. One agent reads code, one checks tests, one reviews architecture.",
|
|
1838
|
+
{
|
|
1839
|
+
task: z6.string().describe("What to analyze"),
|
|
1840
|
+
files: z6.array(z6.string()).optional().describe("Specific files to focus on")
|
|
1841
|
+
},
|
|
1842
|
+
async (args) => {
|
|
1843
|
+
if (!isInitialized(getProjectRoot5())) {
|
|
1844
|
+
return {
|
|
1845
|
+
content: [{ type: "text", text: "Mod-bot is not initialized. Run `init:setup` first." }],
|
|
1846
|
+
isError: true
|
|
1847
|
+
};
|
|
1848
|
+
}
|
|
1849
|
+
const config = await loadConfig(getProjectRoot5());
|
|
1850
|
+
const complexity = assessComplexity(args.task);
|
|
1851
|
+
if (!complexity.recommendSwarm) {
|
|
1852
|
+
return {
|
|
1853
|
+
content: [{
|
|
1854
|
+
type: "text",
|
|
1855
|
+
text: `Task complexity is ${complexity.level} (${complexity.score}/100). A swarm is not recommended for this task \u2014 direct analysis should suffice.`
|
|
1856
|
+
}]
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
const estimate = estimateSwarm(args.task, 3, config.moderation.swarmBudget);
|
|
1860
|
+
if (!estimate.withinBudget) {
|
|
1861
|
+
return {
|
|
1862
|
+
content: [{
|
|
1863
|
+
type: "text",
|
|
1864
|
+
text: `Swarm cost ($${estimate.estimatedCostUsd.toFixed(4)}) exceeds ${config.moderation.swarmBudget} budget. Use \`swarm:estimate\` for details or increase budget.`
|
|
1865
|
+
}]
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
const agents = [
|
|
1869
|
+
{ role: "Code Analyst", task: `Analyze the code structure and logic for: ${args.task}` },
|
|
1870
|
+
{ role: "Test Reviewer", task: `Check test coverage and test quality for: ${args.task}` },
|
|
1871
|
+
{ role: "Architecture Reviewer", task: `Review architectural implications of: ${args.task}` }
|
|
1872
|
+
];
|
|
1873
|
+
const fileContext = args.files?.length ? `
|
|
1874
|
+
Focus files: ${args.files.join(", ")}` : "";
|
|
1875
|
+
const lines = [
|
|
1876
|
+
`## Analysis Swarm Ready`,
|
|
1877
|
+
``,
|
|
1878
|
+
`**Task:** ${args.task}${fileContext}`,
|
|
1879
|
+
`**Complexity:** ${complexity.level} (${complexity.score}/100)`,
|
|
1880
|
+
`**Estimated Cost:** $${estimate.estimatedCostUsd.toFixed(4)}`,
|
|
1881
|
+
``,
|
|
1882
|
+
`### Agent Tasks`,
|
|
1883
|
+
``,
|
|
1884
|
+
...agents.map((a, i) => `${i + 1}. **${a.role}:** ${a.task}`),
|
|
1885
|
+
``,
|
|
1886
|
+
`> Dispatch these as parallel subagents for best results.`,
|
|
1887
|
+
`> Each agent should return its findings, and the moderator will synthesize.`
|
|
1888
|
+
];
|
|
1889
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1890
|
+
}
|
|
1891
|
+
);
|
|
1892
|
+
server2.tool(
|
|
1893
|
+
"swarm_refactor",
|
|
1894
|
+
"Coordinate a multi-file refactoring with one agent per concern.",
|
|
1895
|
+
{
|
|
1896
|
+
description: z6.string().describe("What to refactor"),
|
|
1897
|
+
files: z6.array(z6.string()).describe("Files involved in the refactoring"),
|
|
1898
|
+
concerns: z6.array(z6.string()).optional().describe("Specific concerns to address (e.g., rename, move, update-imports)")
|
|
1899
|
+
},
|
|
1900
|
+
async (args) => {
|
|
1901
|
+
if (!isInitialized(getProjectRoot5())) {
|
|
1902
|
+
return {
|
|
1903
|
+
content: [{ type: "text", text: "Mod-bot is not initialized. Run `init:setup` first." }],
|
|
1904
|
+
isError: true
|
|
1905
|
+
};
|
|
1906
|
+
}
|
|
1907
|
+
const config = await loadConfig(getProjectRoot5());
|
|
1908
|
+
const concerns = args.concerns ?? ["rename", "update-imports", "update-tests"];
|
|
1909
|
+
const estimate = estimateSwarm(args.description, concerns.length, config.moderation.swarmBudget);
|
|
1910
|
+
if (!estimate.withinBudget) {
|
|
1911
|
+
return {
|
|
1912
|
+
content: [{
|
|
1913
|
+
type: "text",
|
|
1914
|
+
text: `Refactoring swarm cost ($${estimate.estimatedCostUsd.toFixed(4)}) exceeds budget. Consider reducing scope or increasing budget.`
|
|
1915
|
+
}]
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
const agents = concerns.map((concern) => ({
|
|
1919
|
+
role: concern,
|
|
1920
|
+
files: args.files,
|
|
1921
|
+
task: `Handle the "${concern}" aspect of: ${args.description}`
|
|
1922
|
+
}));
|
|
1923
|
+
const lines = [
|
|
1924
|
+
`## Refactoring Swarm Plan`,
|
|
1925
|
+
``,
|
|
1926
|
+
`**Description:** ${args.description}`,
|
|
1927
|
+
`**Files:** ${args.files.join(", ")}`,
|
|
1928
|
+
`**Estimated Cost:** $${estimate.estimatedCostUsd.toFixed(4)}`,
|
|
1929
|
+
``,
|
|
1930
|
+
`### Agent Assignments`,
|
|
1931
|
+
``,
|
|
1932
|
+
...agents.map((a, i) => `${i + 1}. **${a.role}:** ${a.task}
|
|
1933
|
+
Files: ${a.files.join(", ")}`),
|
|
1934
|
+
``,
|
|
1935
|
+
`> Each agent works in isolation on its concern.`,
|
|
1936
|
+
`> After all agents complete, review and merge the changes.`
|
|
1937
|
+
];
|
|
1938
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1939
|
+
}
|
|
1940
|
+
);
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
// packages/server/src/index.ts
|
|
1944
|
+
var server = new McpServer({
|
|
1945
|
+
name: "mod-bot",
|
|
1946
|
+
version: "1.0.0"
|
|
1947
|
+
}, {
|
|
1948
|
+
capabilities: {
|
|
1949
|
+
tools: {}
|
|
1950
|
+
}
|
|
1951
|
+
});
|
|
1952
|
+
registerInitTools(server);
|
|
1953
|
+
registerModerateTools(server);
|
|
1954
|
+
registerContextTools(server);
|
|
1955
|
+
registerLearnTools(server);
|
|
1956
|
+
registerSwarmTools(server);
|
|
1957
|
+
async function main() {
|
|
1958
|
+
const transport = new StdioServerTransport();
|
|
1959
|
+
await server.connect(transport);
|
|
1960
|
+
}
|
|
1961
|
+
main().catch((error) => {
|
|
1962
|
+
console.error("Failed to start mod-bot MCP server:", error);
|
|
1963
|
+
process.exit(1);
|
|
1964
|
+
});
|
|
1965
|
+
//# sourceMappingURL=index.js.map
|