devguard 0.2.0 → 0.3.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 CHANGED
@@ -1,21 +1,21 @@
1
- # dev-mark
1
+ # devguard
2
2
 
3
- MCP server that auto-generates dev diary entries from your git activity. What changed, what decisions were made, what broke, what's next so you can pick up where you left off.
3
+ MCP server that keeps a dev diary for you what changed, what decisions were made, what broke, what's next. Picks up where you left off so you never lose context between sessions.
4
4
 
5
5
  ## Why
6
6
 
7
- You vibe code for 3 hours, close your laptop, and come back the next day with no idea what you were doing. Dev-mark fixes that. It reads your git state and writes a diary entry automatically.
7
+ You vibe code for 3 hours, close your laptop, and come back the next day with no idea what you were doing. Devguard fixes that. It reads your git state, tracks your branches, and writes diary entries automatically.
8
8
 
9
9
  ## Install
10
10
 
11
11
  Add to Claude Code:
12
12
 
13
13
  ```bash
14
- claude mcp add devdiary -- npx devdiary
14
+ claude mcp add devguard -- npx devguard
15
15
  ```
16
16
 
17
- That's it. On first run, devdiary automatically:
18
- - Adds `.devdiary/` to your `.gitignore`
17
+ That's it. On first run, devguard automatically:
18
+ - Adds `.devguard/` to your `.gitignore`
19
19
  - Adds an auto-logging instruction to your `CLAUDE.md` (or `.cursorrules` if that exists)
20
20
 
21
21
  From then on, your AI writes diary entries on its own — after finishing a feature, after a big commit, before context gets lost. You never think about it. The diary just fills itself.
@@ -25,9 +25,40 @@ From then on, your AI writes diary entries on its own — after finishing a feat
25
25
  | Tool | What it does |
26
26
  |------|-------------|
27
27
  | `get_context` | Reads git branch, status, recent commits, and diffs |
28
- | `write_entry` | Saves a diary entry to `.devdiary/entries/` |
28
+ | `write_entry` | Saves a diary entry with what changed, decisions, issues, and next steps |
29
29
  | `read_entries` | Reads recent entries to catch you up |
30
- | `catch_me_up` | Morning briefing — diary entries + git state in one shot |
30
+ | `catch_me_up` | Morning briefing — diary entries + git state + branch map in one shot |
31
+ | `branch_map` | Opens a visual branch map in your browser |
31
32
  | `setup` | Re-run setup manually if needed |
32
33
 
33
- Entries are markdown files stored locally in your project under `.devdiary/`, one file per day. Multiple sessions and agents all append to the same daily file.
34
+ ## Branch Map
35
+
36
+ Run `branch_map` to open an interactive HTML visualization of your repo in the browser. It shows:
37
+
38
+ - **All branches** with status (ahead/behind main), files changed, and latest commit
39
+ - **Collapsible diary summaries** per branch — what changed, decisions made, issues hit, next steps
40
+ - **Commit navigator** — click any branch to explore its commits on a visual timeline
41
+ - **Per-commit detail** — files changed, insertions/deletions, and diary entries linked to each commit
42
+ - **Auto-generated summaries** for commits without diary entries — categorized by type (Feature, Fix, Refactor, etc.) with affected areas and change scale
43
+
44
+ Designed for people who don't want to think about git.
45
+
46
+ ## Branch-Aware Diary
47
+
48
+ Entries are automatically routed by branch:
49
+ - **main/master** → `.devguard/entries/` (daily files)
50
+ - **feature branches** → `.devguard/branches/<branch-name>.md` (one file per branch)
51
+
52
+ This means `catch_me_up` shows you the current branch's full story first, then the main stem, then other active branches — so you always know what's happening everywhere.
53
+
54
+ ## How It Works
55
+
56
+ Entries are markdown files stored locally in your project under `.devguard/`. Each entry captures:
57
+ - **Summary** — one-line description of what happened
58
+ - **What Changed** — files modified, features added, bugs fixed
59
+ - **Decisions** — key choices made and why
60
+ - **Issues** — what broke, what's stuck
61
+ - **Next Steps** — what to do next session
62
+ - **Commit hash** — links the entry to a specific commit for traceability
63
+
64
+ Multiple sessions and agents all append to the same file. The diary builds up over time, making summaries richer and the branch map more useful with every session.
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ const read_entries_js_1 = require("./tools/read-entries.js");
9
9
  const catch_me_up_js_1 = require("./tools/catch-me-up.js");
10
10
  const setup_js_1 = require("./tools/setup.js");
11
11
  const branch_map_js_1 = require("./tools/branch-map.js");
12
+ const daily_view_js_1 = require("./tools/daily-view.js");
12
13
  const auto_setup_js_1 = require("./utils/auto-setup.js");
13
14
  // Auto-setup on first run: adds .devdiary/ to .gitignore
14
15
  // and auto-logging instruction to CLAUDE.md or .cursorrules
@@ -23,6 +24,7 @@ const server = new mcp_js_1.McpServer({
23
24
  (0, catch_me_up_js_1.registerCatchMeUp)(server);
24
25
  (0, setup_js_1.registerSetup)(server);
25
26
  (0, branch_map_js_1.registerBranchMap)(server);
27
+ (0, daily_view_js_1.registerDailyView)(server);
26
28
  async function main() {
27
29
  const transport = new stdio_js_1.StdioServerTransport();
28
30
  await server.connect(transport);
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerDailyView(server: McpServer): void;
@@ -0,0 +1,87 @@
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.registerDailyView = registerDailyView;
37
+ const zod_1 = require("zod");
38
+ const git = __importStar(require("../utils/git.js"));
39
+ const storage = __importStar(require("../utils/storage.js"));
40
+ const daily_view_html_js_1 = require("../utils/daily-view-html.js");
41
+ function registerDailyView(server) {
42
+ server.tool("daily_view", "Opens a calendar dashboard in the browser. Each day shows bullet points of what you worked on — pulled from diary entries across all branches.", {
43
+ project_path: zod_1.z.string().describe("Absolute path to the project directory"),
44
+ }, async ({ project_path }) => {
45
+ if (!git.isGitRepo(project_path)) {
46
+ return {
47
+ content: [{ type: "text", text: "This project isn't using git yet — nothing to show." }],
48
+ };
49
+ }
50
+ const currentBranch = git.getBranch(project_path);
51
+ // Collect all diary entries from main stem
52
+ const mainEntries = storage.readEntries(project_path, 90);
53
+ const allParsed = [];
54
+ for (const content of mainEntries) {
55
+ const entries = storage.parseDiaryEntries(content);
56
+ for (const entry of entries) {
57
+ const date = entry.date ? entry.date.slice(0, 10) : "";
58
+ if (date)
59
+ allParsed.push({ date, entry });
60
+ }
61
+ }
62
+ // Collect diary entries from all branches
63
+ const branchFiles = storage.listBranchFiles(project_path);
64
+ for (const bf of branchFiles) {
65
+ const entries = storage.parseDiaryEntries(bf.content);
66
+ for (const entry of entries) {
67
+ const date = entry.date ? entry.date.slice(0, 10) : "";
68
+ if (date)
69
+ allParsed.push({ date, entry });
70
+ }
71
+ }
72
+ // Group by date
73
+ const dateMap = new Map();
74
+ for (const { date, entry } of allParsed) {
75
+ if (!dateMap.has(date))
76
+ dateMap.set(date, []);
77
+ dateMap.get(date).push(entry);
78
+ }
79
+ const days = Array.from(dateMap.entries())
80
+ .map(([date, entries]) => ({ date, entries }))
81
+ .sort((a, b) => a.date.localeCompare(b.date));
82
+ const filePath = (0, daily_view_html_js_1.generateAndOpenDailyView)(project_path, days, currentBranch);
83
+ return {
84
+ content: [{ type: "text", text: `Daily view opened in your browser.\n\nSaved to: ${filePath}` }],
85
+ };
86
+ });
87
+ }
@@ -0,0 +1,6 @@
1
+ import { DiaryEntry } from "./storage.js";
2
+ export interface DayData {
3
+ date: string;
4
+ entries: DiaryEntry[];
5
+ }
6
+ export declare function generateAndOpenDailyView(projectPath: string, days: DayData[], currentBranch: string): string;
@@ -0,0 +1,596 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateAndOpenDailyView = generateAndOpenDailyView;
4
+ const fs_1 = require("fs");
5
+ const path_1 = require("path");
6
+ const child_process_1 = require("child_process");
7
+ function generateAndOpenDailyView(projectPath, days, currentBranch) {
8
+ const projectName = projectPath.split("/").pop() || "project";
9
+ const html = buildHtml(days, projectName, currentBranch);
10
+ const dir = (0, path_1.join)(projectPath, ".devguard");
11
+ (0, fs_1.mkdirSync)(dir, { recursive: true });
12
+ const filePath = (0, path_1.join)(dir, "daily-view.html");
13
+ (0, fs_1.writeFileSync)(filePath, html, "utf-8");
14
+ try {
15
+ const platform = process.platform;
16
+ if (platform === "darwin") {
17
+ (0, child_process_1.execSync)(`open "${filePath}"`, { timeout: 5000 });
18
+ }
19
+ else if (platform === "linux") {
20
+ (0, child_process_1.execSync)(`xdg-open "${filePath}"`, { timeout: 5000 });
21
+ }
22
+ else if (platform === "win32") {
23
+ (0, child_process_1.execSync)(`start "" "${filePath}"`, { timeout: 5000 });
24
+ }
25
+ }
26
+ catch {
27
+ // Silently fail — file is still written
28
+ }
29
+ return filePath;
30
+ }
31
+ function esc(str) {
32
+ return str
33
+ .replace(/&/g, "&amp;")
34
+ .replace(/</g, "&lt;")
35
+ .replace(/>/g, "&gt;")
36
+ .replace(/"/g, "&quot;")
37
+ .replace(/'/g, "&#39;");
38
+ }
39
+ function buildHtml(days, projectName, currentBranch) {
40
+ // Build a JSON-safe data structure for the JS side
41
+ const daysJson = JSON.stringify(days.map((d) => ({
42
+ date: d.date,
43
+ entries: d.entries.map((e) => ({
44
+ title: e.title,
45
+ summary: e.summary,
46
+ date: e.date,
47
+ commit: e.commit,
48
+ whatChanged: e.whatChanged,
49
+ decisions: e.decisions,
50
+ issues: e.issues,
51
+ nextSteps: e.nextSteps,
52
+ })),
53
+ })));
54
+ return `<!DOCTYPE html>
55
+ <html lang="en">
56
+ <head>
57
+ <meta charset="UTF-8">
58
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
59
+ <title>${esc(projectName)} — Daily View</title>
60
+ <style>
61
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
62
+
63
+ * { margin: 0; padding: 0; box-sizing: border-box; }
64
+
65
+ body {
66
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
67
+ background: #1a1a2e;
68
+ color: #e0e0e0;
69
+ min-height: 100vh;
70
+ }
71
+
72
+ /* Header */
73
+ .header {
74
+ display: flex;
75
+ align-items: center;
76
+ justify-content: space-between;
77
+ padding: 20px 32px;
78
+ border-bottom: 1px solid #2a2a4a;
79
+ background: #16162a;
80
+ }
81
+
82
+ .header h1 {
83
+ font-size: 20px;
84
+ font-weight: 600;
85
+ color: #fff;
86
+ }
87
+
88
+ .header h1 span {
89
+ color: #4fc3f7;
90
+ font-weight: 400;
91
+ }
92
+
93
+ .header .branch-badge {
94
+ font-size: 13px;
95
+ background: #2a2a4a;
96
+ color: #a0a0c0;
97
+ padding: 4px 12px;
98
+ border-radius: 12px;
99
+ font-family: 'JetBrains Mono', monospace;
100
+ }
101
+
102
+ /* Nav */
103
+ .nav {
104
+ display: flex;
105
+ align-items: center;
106
+ justify-content: center;
107
+ gap: 16px;
108
+ padding: 16px 32px;
109
+ border-bottom: 1px solid #2a2a4a;
110
+ }
111
+
112
+ .nav button {
113
+ background: #2a2a4a;
114
+ color: #e0e0e0;
115
+ border: none;
116
+ padding: 8px 16px;
117
+ border-radius: 8px;
118
+ cursor: pointer;
119
+ font-family: inherit;
120
+ font-size: 14px;
121
+ transition: background 0.15s;
122
+ }
123
+
124
+ .nav button:hover { background: #3a3a5a; }
125
+
126
+ .nav .month-label {
127
+ font-size: 18px;
128
+ font-weight: 600;
129
+ min-width: 200px;
130
+ text-align: center;
131
+ }
132
+
133
+ /* Calendar grid */
134
+ .calendar-container {
135
+ max-width: 1200px;
136
+ margin: 24px auto;
137
+ padding: 0 24px;
138
+ }
139
+
140
+ .weekday-header {
141
+ display: grid;
142
+ grid-template-columns: repeat(7, 1fr);
143
+ gap: 4px;
144
+ margin-bottom: 4px;
145
+ }
146
+
147
+ .weekday-header div {
148
+ text-align: center;
149
+ font-size: 12px;
150
+ font-weight: 600;
151
+ color: #808098;
152
+ padding: 8px 0;
153
+ text-transform: uppercase;
154
+ letter-spacing: 0.5px;
155
+ }
156
+
157
+ .calendar-grid {
158
+ display: grid;
159
+ grid-template-columns: repeat(7, 1fr);
160
+ gap: 4px;
161
+ }
162
+
163
+ .day-cell {
164
+ min-height: 120px;
165
+ background: #20203a;
166
+ border-radius: 8px;
167
+ padding: 8px;
168
+ cursor: default;
169
+ transition: background 0.15s, box-shadow 0.15s;
170
+ position: relative;
171
+ overflow: hidden;
172
+ }
173
+
174
+ .day-cell.empty {
175
+ background: transparent;
176
+ }
177
+
178
+ .day-cell.today {
179
+ box-shadow: inset 0 0 0 2px #4fc3f7;
180
+ }
181
+
182
+ .day-cell.has-entries {
183
+ cursor: pointer;
184
+ }
185
+
186
+ .day-cell.has-entries:hover {
187
+ background: #2a2a4a;
188
+ box-shadow: 0 2px 12px rgba(79, 195, 247, 0.1);
189
+ }
190
+
191
+ .day-cell.selected {
192
+ background: #2a2a4a;
193
+ box-shadow: inset 0 0 0 2px #4fc3f7, 0 2px 12px rgba(79, 195, 247, 0.15);
194
+ }
195
+
196
+ .day-number {
197
+ font-size: 13px;
198
+ font-weight: 600;
199
+ color: #808098;
200
+ margin-bottom: 6px;
201
+ }
202
+
203
+ .day-cell.has-entries .day-number {
204
+ color: #4fc3f7;
205
+ }
206
+
207
+ .day-cell.today .day-number {
208
+ color: #fff;
209
+ }
210
+
211
+ .day-bullets {
212
+ list-style: none;
213
+ padding: 0;
214
+ }
215
+
216
+ .day-bullets li {
217
+ font-size: 11px;
218
+ color: #b0b0c8;
219
+ padding: 2px 0;
220
+ line-height: 1.4;
221
+ white-space: nowrap;
222
+ overflow: hidden;
223
+ text-overflow: ellipsis;
224
+ }
225
+
226
+ .day-bullets li::before {
227
+ content: "";
228
+ display: inline-block;
229
+ width: 5px;
230
+ height: 5px;
231
+ background: #4fc3f7;
232
+ border-radius: 50%;
233
+ margin-right: 6px;
234
+ vertical-align: middle;
235
+ }
236
+
237
+ .day-entry-count {
238
+ position: absolute;
239
+ top: 8px;
240
+ right: 8px;
241
+ background: #4fc3f7;
242
+ color: #1a1a2e;
243
+ font-size: 10px;
244
+ font-weight: 700;
245
+ width: 18px;
246
+ height: 18px;
247
+ border-radius: 50%;
248
+ display: flex;
249
+ align-items: center;
250
+ justify-content: center;
251
+ }
252
+
253
+ /* Detail panel */
254
+ .detail-overlay {
255
+ display: none;
256
+ position: fixed;
257
+ top: 0; left: 0; right: 0; bottom: 0;
258
+ background: rgba(10, 10, 20, 0.7);
259
+ z-index: 100;
260
+ }
261
+
262
+ .detail-overlay.open { display: flex; align-items: center; justify-content: center; }
263
+
264
+ .detail-panel {
265
+ background: #1e1e38;
266
+ border-radius: 16px;
267
+ width: 90%;
268
+ max-width: 700px;
269
+ max-height: 80vh;
270
+ overflow-y: auto;
271
+ padding: 28px 32px;
272
+ box-shadow: 0 12px 40px rgba(0,0,0,0.5);
273
+ }
274
+
275
+ .detail-panel::-webkit-scrollbar { width: 6px; }
276
+ .detail-panel::-webkit-scrollbar-track { background: transparent; }
277
+ .detail-panel::-webkit-scrollbar-thumb { background: #3a3a5a; border-radius: 3px; }
278
+
279
+ .detail-header {
280
+ display: flex;
281
+ align-items: center;
282
+ justify-content: space-between;
283
+ margin-bottom: 20px;
284
+ }
285
+
286
+ .detail-header h2 {
287
+ font-size: 18px;
288
+ font-weight: 600;
289
+ color: #fff;
290
+ }
291
+
292
+ .detail-close {
293
+ background: none;
294
+ border: none;
295
+ color: #808098;
296
+ font-size: 22px;
297
+ cursor: pointer;
298
+ padding: 4px 8px;
299
+ border-radius: 6px;
300
+ transition: background 0.15s;
301
+ }
302
+
303
+ .detail-close:hover { background: #2a2a4a; color: #e0e0e0; }
304
+
305
+ .detail-entry {
306
+ margin-bottom: 20px;
307
+ padding: 16px;
308
+ background: #252545;
309
+ border-radius: 10px;
310
+ border-left: 3px solid #4fc3f7;
311
+ }
312
+
313
+ .detail-entry:last-child { margin-bottom: 0; }
314
+
315
+ .detail-entry h3 {
316
+ font-size: 15px;
317
+ font-weight: 600;
318
+ color: #e0e0e0;
319
+ margin-bottom: 4px;
320
+ }
321
+
322
+ .detail-entry .entry-meta {
323
+ font-size: 12px;
324
+ color: #808098;
325
+ font-family: 'JetBrains Mono', monospace;
326
+ margin-bottom: 12px;
327
+ }
328
+
329
+ .detail-section {
330
+ margin-top: 10px;
331
+ }
332
+
333
+ .detail-section .section-label {
334
+ font-size: 11px;
335
+ font-weight: 600;
336
+ text-transform: uppercase;
337
+ letter-spacing: 0.5px;
338
+ margin-bottom: 4px;
339
+ display: block;
340
+ }
341
+
342
+ .detail-section.changes .section-label { color: #4fc3f7; }
343
+ .detail-section.decisions .section-label { color: #ce93d8; }
344
+ .detail-section.issues .section-label { color: #ef9a9a; }
345
+ .detail-section.next-steps .section-label { color: #a5d6a7; }
346
+
347
+ .detail-section ul {
348
+ list-style: none;
349
+ padding: 0;
350
+ }
351
+
352
+ .detail-section ul li {
353
+ font-size: 13px;
354
+ color: #c0c0d8;
355
+ padding: 3px 0;
356
+ padding-left: 14px;
357
+ position: relative;
358
+ line-height: 1.5;
359
+ }
360
+
361
+ .detail-section ul li::before {
362
+ content: "";
363
+ position: absolute;
364
+ left: 0;
365
+ top: 10px;
366
+ width: 5px;
367
+ height: 5px;
368
+ border-radius: 50%;
369
+ }
370
+
371
+ .detail-section.changes ul li::before { background: #4fc3f7; }
372
+ .detail-section.decisions ul li::before { background: #ce93d8; }
373
+ .detail-section.issues ul li::before { background: #ef9a9a; }
374
+ .detail-section.next-steps ul li::before { background: #a5d6a7; }
375
+
376
+ /* Empty state */
377
+ .empty-state {
378
+ text-align: center;
379
+ padding: 80px 32px;
380
+ color: #808098;
381
+ }
382
+
383
+ .empty-state h2 { font-size: 18px; margin-bottom: 8px; color: #a0a0c0; }
384
+ .empty-state p { font-size: 14px; }
385
+ </style>
386
+ </head>
387
+ <body>
388
+
389
+ <div class="header">
390
+ <h1>${esc(projectName)} <span>/ daily view</span></h1>
391
+ <div class="branch-badge">${esc(currentBranch)}</div>
392
+ </div>
393
+
394
+ <div class="nav">
395
+ <button onclick="prevMonth()">&larr; Prev</button>
396
+ <div class="month-label" id="month-label"></div>
397
+ <button onclick="nextMonth()">Next &rarr;</button>
398
+ <button onclick="goToday()" style="margin-left: 12px; background: #4fc3f7; color: #1a1a2e; font-weight: 600;">Today</button>
399
+ </div>
400
+
401
+ <div class="calendar-container">
402
+ <div class="weekday-header">
403
+ <div>Sun</div><div>Mon</div><div>Tue</div><div>Wed</div><div>Thu</div><div>Fri</div><div>Sat</div>
404
+ </div>
405
+ <div class="calendar-grid" id="calendar-grid"></div>
406
+ </div>
407
+
408
+ <div class="detail-overlay" id="detail-overlay" onclick="closeDetail(event)">
409
+ <div class="detail-panel" id="detail-panel" onclick="event.stopPropagation()">
410
+ <div class="detail-header">
411
+ <h2 id="detail-title"></h2>
412
+ <button class="detail-close" onclick="closeDetail()">&times;</button>
413
+ </div>
414
+ <div id="detail-body"></div>
415
+ </div>
416
+ </div>
417
+
418
+ <script>
419
+ const DAYS_DATA = ${daysJson};
420
+
421
+ // Index entries by date
422
+ const dateMap = {};
423
+ DAYS_DATA.forEach(d => { dateMap[d.date] = d.entries; });
424
+
425
+ // State
426
+ const today = new Date();
427
+ let viewYear = today.getFullYear();
428
+ let viewMonth = today.getMonth();
429
+
430
+ const MONTH_NAMES = ["January","February","March","April","May","June","July","August","September","October","November","December"];
431
+
432
+ function todayStr() {
433
+ const y = today.getFullYear();
434
+ const m = String(today.getMonth() + 1).padStart(2, "0");
435
+ const d = String(today.getDate()).padStart(2, "0");
436
+ return y + "-" + m + "-" + d;
437
+ }
438
+
439
+ function dateStr(y, m, d) {
440
+ return y + "-" + String(m + 1).padStart(2, "0") + "-" + String(d).padStart(2, "0");
441
+ }
442
+
443
+ function esc(s) {
444
+ const el = document.createElement("span");
445
+ el.textContent = s;
446
+ return el.innerHTML;
447
+ }
448
+
449
+ function render() {
450
+ document.getElementById("month-label").textContent = MONTH_NAMES[viewMonth] + " " + viewYear;
451
+
452
+ const grid = document.getElementById("calendar-grid");
453
+ grid.innerHTML = "";
454
+
455
+ const firstDay = new Date(viewYear, viewMonth, 1).getDay();
456
+ const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate();
457
+ const todayString = todayStr();
458
+
459
+ // Empty cells before first day
460
+ for (let i = 0; i < firstDay; i++) {
461
+ const cell = document.createElement("div");
462
+ cell.className = "day-cell empty";
463
+ grid.appendChild(cell);
464
+ }
465
+
466
+ for (let d = 1; d <= daysInMonth; d++) {
467
+ const ds = dateStr(viewYear, viewMonth, d);
468
+ const entries = dateMap[ds] || [];
469
+ const isToday = ds === todayString;
470
+ const hasEntries = entries.length > 0;
471
+
472
+ const cell = document.createElement("div");
473
+ cell.className = "day-cell" + (isToday ? " today" : "") + (hasEntries ? " has-entries" : "");
474
+
475
+ if (hasEntries) {
476
+ cell.onclick = function() { openDetail(ds, entries); };
477
+ }
478
+
479
+ let inner = '<div class="day-number">' + d + '</div>';
480
+
481
+ if (hasEntries) {
482
+ inner += '<div class="day-entry-count">' + entries.length + '</div>';
483
+ inner += '<ul class="day-bullets">';
484
+ // Show up to 4 bullet items from whatChanged across all entries
485
+ const bullets = [];
486
+ for (const e of entries) {
487
+ for (const item of e.whatChanged) {
488
+ bullets.push(item);
489
+ if (bullets.length >= 4) break;
490
+ }
491
+ if (bullets.length >= 4) break;
492
+ }
493
+ if (bullets.length === 0) {
494
+ // Fallback to entry titles
495
+ for (const e of entries) {
496
+ if (e.title) bullets.push(e.title);
497
+ if (bullets.length >= 4) break;
498
+ }
499
+ }
500
+ for (const b of bullets) {
501
+ inner += '<li>' + esc(b) + '</li>';
502
+ }
503
+ inner += '</ul>';
504
+ }
505
+
506
+ cell.innerHTML = inner;
507
+ grid.appendChild(cell);
508
+ }
509
+ }
510
+
511
+ function openDetail(ds, entries) {
512
+ const overlay = document.getElementById("detail-overlay");
513
+ const d = new Date(ds + "T12:00:00");
514
+ document.getElementById("detail-title").textContent =
515
+ MONTH_NAMES[d.getMonth()] + " " + d.getDate() + ", " + d.getFullYear();
516
+
517
+ let html = "";
518
+ for (const e of entries) {
519
+ html += '<div class="detail-entry">';
520
+ html += '<h3>' + esc(e.title) + '</h3>';
521
+
522
+ const meta = [];
523
+ if (e.commit) meta.push(e.commit);
524
+ if (e.date) {
525
+ try { meta.push(new Date(e.date).toLocaleTimeString()); } catch(x) {}
526
+ }
527
+ if (meta.length) html += '<div class="entry-meta">' + esc(meta.join(" / ")) + '</div>';
528
+
529
+ if (e.whatChanged.length) {
530
+ html += '<div class="detail-section changes"><span class="section-label">What Changed</span><ul>';
531
+ e.whatChanged.forEach(function(item) { html += '<li>' + esc(item) + '</li>'; });
532
+ html += '</ul></div>';
533
+ }
534
+ if (e.decisions.length) {
535
+ html += '<div class="detail-section decisions"><span class="section-label">Decisions</span><ul>';
536
+ e.decisions.forEach(function(item) { html += '<li>' + esc(item) + '</li>'; });
537
+ html += '</ul></div>';
538
+ }
539
+ if (e.issues.length) {
540
+ html += '<div class="detail-section issues"><span class="section-label">Issues</span><ul>';
541
+ e.issues.forEach(function(item) { html += '<li>' + esc(item) + '</li>'; });
542
+ html += '</ul></div>';
543
+ }
544
+ if (e.nextSteps.length) {
545
+ html += '<div class="detail-section next-steps"><span class="section-label">Next Steps</span><ul>';
546
+ e.nextSteps.forEach(function(item) { html += '<li>' + esc(item) + '</li>'; });
547
+ html += '</ul></div>';
548
+ }
549
+
550
+ html += '</div>';
551
+ }
552
+
553
+ if (!html) html = '<div style="color:#808098;text-align:center;padding:20px;">No details available</div>';
554
+
555
+ document.getElementById("detail-body").innerHTML = html;
556
+ overlay.classList.add("open");
557
+ }
558
+
559
+ function closeDetail(event) {
560
+ if (event && event.target !== document.getElementById("detail-overlay")) return;
561
+ document.getElementById("detail-overlay").classList.remove("open");
562
+ }
563
+
564
+ function prevMonth() {
565
+ viewMonth--;
566
+ if (viewMonth < 0) { viewMonth = 11; viewYear--; }
567
+ render();
568
+ }
569
+
570
+ function nextMonth() {
571
+ viewMonth++;
572
+ if (viewMonth > 11) { viewMonth = 0; viewYear++; }
573
+ render();
574
+ }
575
+
576
+ function goToday() {
577
+ viewYear = today.getFullYear();
578
+ viewMonth = today.getMonth();
579
+ render();
580
+ }
581
+
582
+ document.addEventListener("keydown", function(e) {
583
+ if (e.key === "Escape") {
584
+ document.getElementById("detail-overlay").classList.remove("open");
585
+ } else if (e.key === "ArrowLeft") {
586
+ prevMonth();
587
+ } else if (e.key === "ArrowRight") {
588
+ nextMonth();
589
+ }
590
+ });
591
+
592
+ render();
593
+ </script>
594
+ </body>
595
+ </html>`;
596
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devguard",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "MCP server that auto-generates dev diary entries from git activity",
5
5
  "license": "MIT",
6
6
  "bin": {