devguard 0.1.0 → 0.2.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.
@@ -0,0 +1,22 @@
1
+ export interface AgentFileMap {
2
+ agent_id: string;
3
+ name: string;
4
+ status: string;
5
+ files: string[];
6
+ }
7
+ export interface Collision {
8
+ file: string;
9
+ agents: string[];
10
+ }
11
+ /**
12
+ * Parse all agent files and extract their file lists.
13
+ */
14
+ export declare function getAgentFileMaps(projectPath: string): AgentFileMap[];
15
+ /**
16
+ * Find files claimed by more than one active agent.
17
+ */
18
+ export declare function detectCollisions(projectPath: string): Collision[];
19
+ /**
20
+ * Check if a specific set of files would collide with any active agent.
21
+ */
22
+ export declare function checkFilesForCollisions(projectPath: string, files: string[], excludeAgentId?: string): Collision[];
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.getAgentFileMaps = getAgentFileMaps;
37
+ exports.detectCollisions = detectCollisions;
38
+ exports.checkFilesForCollisions = checkFilesForCollisions;
39
+ const storage = __importStar(require("./storage.js"));
40
+ /**
41
+ * Parse all agent files and extract their file lists.
42
+ */
43
+ function getAgentFileMaps(projectPath) {
44
+ const agentFiles = storage.listAgentFiles(projectPath);
45
+ return agentFiles.map((f) => {
46
+ const agentId = f.replace(/\.md$/, "");
47
+ const content = storage.readAgentFile(projectPath, agentId) ?? "";
48
+ const statusMatch = content.match(/^status:\s*(.+)$/m);
49
+ const nameMatch = content.match(/^name:\s*(.+)$/m);
50
+ const filesSection = content.match(/^files_touching:\n((?:\s+-\s+.+\n?)*)/m);
51
+ const files = [];
52
+ if (filesSection?.[1]) {
53
+ const lines = filesSection[1].split("\n");
54
+ for (const line of lines) {
55
+ const match = line.match(/^\s+-\s+(.+)/);
56
+ if (match)
57
+ files.push(match[1].trim());
58
+ }
59
+ }
60
+ return {
61
+ agent_id: agentId,
62
+ name: nameMatch?.[1] ?? "",
63
+ status: statusMatch?.[1] ?? "unknown",
64
+ files,
65
+ };
66
+ });
67
+ }
68
+ /**
69
+ * Find files claimed by more than one active agent.
70
+ */
71
+ function detectCollisions(projectPath) {
72
+ const agents = getAgentFileMaps(projectPath).filter((a) => a.status === "active");
73
+ const fileToAgents = new Map();
74
+ for (const agent of agents) {
75
+ for (const file of agent.files) {
76
+ const existing = fileToAgents.get(file) ?? [];
77
+ existing.push(agent.name || agent.agent_id);
78
+ fileToAgents.set(file, existing);
79
+ }
80
+ }
81
+ const collisions = [];
82
+ for (const [file, agents] of fileToAgents) {
83
+ if (agents.length > 1) {
84
+ collisions.push({ file, agents });
85
+ }
86
+ }
87
+ return collisions;
88
+ }
89
+ /**
90
+ * Check if a specific set of files would collide with any active agent.
91
+ */
92
+ function checkFilesForCollisions(projectPath, files, excludeAgentId) {
93
+ const agents = getAgentFileMaps(projectPath).filter((a) => a.status === "active" && a.agent_id !== excludeAgentId);
94
+ const collisions = [];
95
+ for (const file of files) {
96
+ const claimants = agents
97
+ .filter((a) => a.files.includes(file))
98
+ .map((a) => a.name || a.agent_id);
99
+ if (claimants.length > 0) {
100
+ collisions.push({ file, agents: claimants });
101
+ }
102
+ }
103
+ return collisions;
104
+ }
@@ -3,4 +3,29 @@ export declare function getBranch(cwd: string): string;
3
3
  export declare function getStatus(cwd: string): string;
4
4
  export declare function getRecentCommits(cwd: string, count?: number): string;
5
5
  export declare function getDiffSummary(cwd: string): string;
6
+ export declare function getLocalBranches(cwd: string): string[];
7
+ export declare function getBranchLastCommit(cwd: string, branch: string): string;
8
+ export declare function getBranchCommitCount(cwd: string, branch: string, base: string): number;
9
+ export interface CommitNode {
10
+ shortHash: string;
11
+ message: string;
12
+ author: string;
13
+ date: string;
14
+ timestamp: number;
15
+ files?: string[];
16
+ insertions?: number;
17
+ deletions?: number;
18
+ body?: string;
19
+ }
20
+ export declare function getBranchCommits(cwd: string, branch: string, limit?: number): CommitNode[];
21
+ export declare function getBehindCount(cwd: string, branch: string, base: string): number;
22
+ export declare function getFilesChangedCount(cwd: string, branch: string, base: string): number;
23
+ export declare function getCommitFiles(cwd: string, hash: string): string[];
24
+ export declare function getCommitStats(cwd: string, hash: string): {
25
+ insertions: number;
26
+ deletions: number;
27
+ };
28
+ export declare function getCommitBody(cwd: string, hash: string): string;
29
+ export declare function getCommitDiffSummary(cwd: string, hash: string): string;
30
+ export declare function getHeadCommitHash(cwd: string): string;
6
31
  export declare function getDiffFull(cwd: string): string;
package/dist/utils/git.js CHANGED
@@ -5,6 +5,17 @@ exports.getBranch = getBranch;
5
5
  exports.getStatus = getStatus;
6
6
  exports.getRecentCommits = getRecentCommits;
7
7
  exports.getDiffSummary = getDiffSummary;
8
+ exports.getLocalBranches = getLocalBranches;
9
+ exports.getBranchLastCommit = getBranchLastCommit;
10
+ exports.getBranchCommitCount = getBranchCommitCount;
11
+ exports.getBranchCommits = getBranchCommits;
12
+ exports.getBehindCount = getBehindCount;
13
+ exports.getFilesChangedCount = getFilesChangedCount;
14
+ exports.getCommitFiles = getCommitFiles;
15
+ exports.getCommitStats = getCommitStats;
16
+ exports.getCommitBody = getCommitBody;
17
+ exports.getCommitDiffSummary = getCommitDiffSummary;
18
+ exports.getHeadCommitHash = getHeadCommitHash;
8
19
  exports.getDiffFull = getDiffFull;
9
20
  const child_process_1 = require("child_process");
10
21
  function run(cmd, cwd) {
@@ -37,6 +48,69 @@ function getDiffSummary(cwd) {
37
48
  parts.push(`Unstaged:\n${unstaged}`);
38
49
  return parts.join("\n\n") || "No changes";
39
50
  }
51
+ function getLocalBranches(cwd) {
52
+ const output = run("git branch --format='%(refname:short)'", cwd);
53
+ if (!output)
54
+ return [];
55
+ return output.split("\n").map((b) => b.trim().replace(/^'|'$/g, "")).filter(Boolean);
56
+ }
57
+ function getBranchLastCommit(cwd, branch) {
58
+ return run(`git log -1 --format="%s" "${branch}"`, cwd);
59
+ }
60
+ function getBranchCommitCount(cwd, branch, base) {
61
+ const output = run(`git rev-list --count "${base}..${branch}"`, cwd);
62
+ return parseInt(output, 10) || 0;
63
+ }
64
+ function getBranchCommits(cwd, branch, limit = 15) {
65
+ const sep = "|||";
66
+ const output = run(`git log "${branch}" --format="%h${sep}%s${sep}%an${sep}%ai${sep}%at" -n ${limit}`, cwd);
67
+ if (!output)
68
+ return [];
69
+ return output.split("\n").filter(Boolean).map(line => {
70
+ const parts = line.split(sep);
71
+ return {
72
+ shortHash: parts[0] || "",
73
+ message: parts[1] || "",
74
+ author: parts[2] || "",
75
+ date: parts[3] || "",
76
+ timestamp: parseInt(parts[4]) || 0,
77
+ };
78
+ });
79
+ }
80
+ function getBehindCount(cwd, branch, base) {
81
+ const output = run(`git rev-list --count "${branch}..${base}"`, cwd);
82
+ return parseInt(output, 10) || 0;
83
+ }
84
+ function getFilesChangedCount(cwd, branch, base) {
85
+ const output = run(`git diff --name-only "${base}...${branch}"`, cwd);
86
+ if (!output)
87
+ return 0;
88
+ return output.split("\n").filter(Boolean).length;
89
+ }
90
+ function getCommitFiles(cwd, hash) {
91
+ const output = run(`git diff-tree --no-commit-id --name-only -r "${hash}"`, cwd);
92
+ if (!output)
93
+ return [];
94
+ return output.split("\n").filter(Boolean);
95
+ }
96
+ function getCommitStats(cwd, hash) {
97
+ const output = run(`git diff-tree --no-commit-id --shortstat -r "${hash}"`, cwd);
98
+ const ins = output.match(/(\d+) insertion/);
99
+ const del = output.match(/(\d+) deletion/);
100
+ return {
101
+ insertions: ins ? parseInt(ins[1]) : 0,
102
+ deletions: del ? parseInt(del[1]) : 0,
103
+ };
104
+ }
105
+ function getCommitBody(cwd, hash) {
106
+ return run(`git log -1 --format="%b" "${hash}"`, cwd);
107
+ }
108
+ function getCommitDiffSummary(cwd, hash) {
109
+ return run(`git diff-tree --no-commit-id --stat -r "${hash}"`, cwd);
110
+ }
111
+ function getHeadCommitHash(cwd) {
112
+ return run("git rev-parse --short HEAD", cwd);
113
+ }
40
114
  function getDiffFull(cwd) {
41
115
  const staged = run("git diff --cached", cwd);
42
116
  const unstaged = run("git diff", cwd);
@@ -1,3 +1,22 @@
1
1
  export declare function ensureDiaryDir(projectPath: string): string;
2
2
  export declare function writeEntry(projectPath: string, content: string): string;
3
3
  export declare function readEntries(projectPath: string, count: number): string[];
4
+ export declare function ensureBranchesDir(projectPath: string): string;
5
+ export declare function writeBranchEntry(projectPath: string, branch: string, content: string): string;
6
+ export declare function readBranchEntry(projectPath: string, branch: string): string | null;
7
+ export declare function listBranchFiles(projectPath: string): {
8
+ branch: string;
9
+ content: string;
10
+ }[];
11
+ export declare function extractLatestSummary(content: string): string;
12
+ export interface DiaryEntry {
13
+ title: string;
14
+ summary: string;
15
+ date: string;
16
+ commit: string;
17
+ whatChanged: string[];
18
+ decisions: string[];
19
+ issues: string[];
20
+ nextSteps: string[];
21
+ }
22
+ export declare function parseDiaryEntries(content: string): DiaryEntry[];
@@ -3,6 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ensureDiaryDir = ensureDiaryDir;
4
4
  exports.writeEntry = writeEntry;
5
5
  exports.readEntries = readEntries;
6
+ exports.ensureBranchesDir = ensureBranchesDir;
7
+ exports.writeBranchEntry = writeBranchEntry;
8
+ exports.readBranchEntry = readBranchEntry;
9
+ exports.listBranchFiles = listBranchFiles;
10
+ exports.extractLatestSummary = extractLatestSummary;
11
+ exports.parseDiaryEntries = parseDiaryEntries;
6
12
  const fs_1 = require("fs");
7
13
  const path_1 = require("path");
8
14
  const DIARY_DIR = ".devguard";
@@ -41,6 +47,105 @@ function readEntries(projectPath, count) {
41
47
  .slice(0, count);
42
48
  return files.map((f) => (0, fs_1.readFileSync)((0, path_1.join)(dir, f), "utf-8"));
43
49
  }
50
+ // --- Branch entries ---
51
+ const BRANCHES_DIR = "branches";
52
+ function branchesPath(projectPath) {
53
+ return (0, path_1.join)(projectPath, DIARY_DIR, BRANCHES_DIR);
54
+ }
55
+ function sanitizeBranchName(branch) {
56
+ return branch.replace(/\//g, "-");
57
+ }
58
+ function ensureBranchesDir(projectPath) {
59
+ const dir = branchesPath(projectPath);
60
+ (0, fs_1.mkdirSync)(dir, { recursive: true });
61
+ return dir;
62
+ }
63
+ function writeBranchEntry(projectPath, branch, content) {
64
+ const dir = ensureBranchesDir(projectPath);
65
+ const filename = sanitizeBranchName(branch) + ".md";
66
+ const filePath = (0, path_1.join)(dir, filename);
67
+ const now = new Date();
68
+ const separator = `\n\n---\n\n<!-- ${formatDate(now)} ${formatTime(now)} -->\n\n`;
69
+ if ((0, fs_1.existsSync)(filePath)) {
70
+ (0, fs_1.appendFileSync)(filePath, separator + content, "utf-8");
71
+ }
72
+ else {
73
+ const header = `# Branch: ${branch}\n\n`;
74
+ (0, fs_1.writeFileSync)(filePath, header + content, "utf-8");
75
+ }
76
+ return filePath;
77
+ }
78
+ function readBranchEntry(projectPath, branch) {
79
+ const dir = branchesPath(projectPath);
80
+ const filename = sanitizeBranchName(branch) + ".md";
81
+ const filePath = (0, path_1.join)(dir, filename);
82
+ if (!(0, fs_1.existsSync)(filePath))
83
+ return null;
84
+ return (0, fs_1.readFileSync)(filePath, "utf-8");
85
+ }
86
+ function listBranchFiles(projectPath) {
87
+ const dir = branchesPath(projectPath);
88
+ if (!(0, fs_1.existsSync)(dir))
89
+ return [];
90
+ return (0, fs_1.readdirSync)(dir)
91
+ .filter((f) => f.endsWith(".md"))
92
+ .sort()
93
+ .map((f) => ({
94
+ branch: f.replace(".md", ""),
95
+ content: (0, fs_1.readFileSync)((0, path_1.join)(dir, f), "utf-8"),
96
+ }));
97
+ }
98
+ function extractLatestSummary(content) {
99
+ // Find the last "summary:" line in frontmatter
100
+ const summaryMatches = [...content.matchAll(/^summary:\s*"?(.+?)"?\s*$/gm)];
101
+ if (summaryMatches.length > 0) {
102
+ return summaryMatches[summaryMatches.length - 1][1];
103
+ }
104
+ // Fallback: grab the first heading after the last separator
105
+ const lastSep = content.lastIndexOf("---\n");
106
+ const tail = lastSep >= 0 ? content.slice(lastSep) : content;
107
+ const headingMatch = tail.match(/^#+ (.+)$/m);
108
+ if (headingMatch)
109
+ return headingMatch[1];
110
+ return "No summary available";
111
+ }
112
+ function parseDiaryEntries(content) {
113
+ // Split on frontmatter separators (--- blocks)
114
+ const blocks = content.split(/\n---\n/).filter(b => b.trim());
115
+ const entries = [];
116
+ for (const block of blocks) {
117
+ const summaryMatch = block.match(/^summary:\s*"?(.+?)"?\s*$/m);
118
+ const dateMatch = block.match(/^date:\s*(.+)$/m);
119
+ const commitMatch = block.match(/^commit:\s*(.+)$/m);
120
+ const titleMatch = block.match(/^#+ (.+)$/m);
121
+ if (!titleMatch && !summaryMatch)
122
+ continue;
123
+ const entry = {
124
+ title: titleMatch?.[1] || summaryMatch?.[1] || "",
125
+ summary: summaryMatch?.[1] || titleMatch?.[1] || "",
126
+ date: dateMatch?.[1] || "",
127
+ commit: commitMatch?.[1] || "",
128
+ whatChanged: extractSection(block, "What Changed"),
129
+ decisions: extractSection(block, "Decisions"),
130
+ issues: extractSection(block, "Issues"),
131
+ nextSteps: extractSection(block, "Next Steps"),
132
+ };
133
+ if (entry.title)
134
+ entries.push(entry);
135
+ }
136
+ return entries;
137
+ }
138
+ function extractSection(block, heading) {
139
+ const regex = new RegExp(`## ${heading}\\s*\\n([\\s\\S]*?)(?=\\n## |$)`, "m");
140
+ const match = block.match(regex);
141
+ if (!match)
142
+ return [];
143
+ const text = match[1].trim();
144
+ // Split by bullet points or newlines
145
+ const lines = text.split(/\n/).map(l => l.replace(/^[-*]\s+/, "").trim()).filter(Boolean);
146
+ return lines;
147
+ }
148
+ // --- Helpers ---
44
149
  function formatDate(date) {
45
150
  const y = date.getFullYear();
46
151
  const mo = String(date.getMonth() + 1).padStart(2, "0");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devguard",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "MCP server that auto-generates dev diary entries from git activity",
5
5
  "license": "MIT",
6
6
  "bin": {