project-logbook 0.3.4 → 0.4.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/dist/commands/build.js +22 -1
- package/dist/commands/lint.js +18 -0
- package/dist/commands/status.js +23 -7
- package/dist/lib/build-helpers.js +7 -22
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.js +7 -3
- package/dist/lib/git-helpers.d.ts +21 -0
- package/dist/lib/git-helpers.js +91 -0
- package/dist/lib/image-helpers.js +2 -2
- package/dist/lib/markdown-processors.d.ts +5 -1
- package/dist/lib/markdown-processors.js +28 -2
- package/dist/lib/rss.d.ts +29 -0
- package/dist/lib/rss.js +77 -0
- package/dist/lib/template-helpers.d.ts +3 -3
- package/dist/lib/template-helpers.js +19 -6
- package/dist/lib/template-types.d.ts +2 -0
- package/dist/lib/templates.d.ts +5 -2
- package/dist/lib/templates.js +10 -7
- package/dist/linters/index.js +2 -0
- package/dist/linters/technical-log.d.ts +7 -0
- package/dist/linters/technical-log.js +72 -0
- package/dist/templates/index.md +4 -0
- package/dist/templates/log.md +5 -0
- package/dist/templates/steer.txt +21 -5
- package/dist/templates/styles.css +116 -0
- package/dist/utils/date.d.ts +7 -0
- package/dist/utils/date.js +12 -0
- package/dist/utils/log-timeline.d.ts +69 -0
- package/dist/utils/log-timeline.js +218 -0
- package/package.json +3 -1
- package/src/templates/index.md +4 -0
- package/src/templates/log.md +5 -0
- package/src/templates/steer.txt +21 -5
- package/src/templates/styles.css +116 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for transforming technical log markdown into a timeline view.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Regex pattern to match log entries with ISO timestamps.
|
|
6
|
+
* Matches: "2026-05-27T14:17:02.250Z: Some message"
|
|
7
|
+
* Also matches bullet points: "- 2026-05-27T14:17:02.250Z: Some message"
|
|
8
|
+
*/
|
|
9
|
+
const LOG_ENTRY_REGEX = /^-?\s*(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?)\s*:\s*(.+)$/;
|
|
10
|
+
/**
|
|
11
|
+
* Parse technical log markdown into structured log entries.
|
|
12
|
+
* @param logMarkdown - The raw markdown content of the technical log
|
|
13
|
+
* @returns Array of parsed log entries, or null if parsing fails
|
|
14
|
+
*/
|
|
15
|
+
export function parseLogMarkdown(logMarkdown) {
|
|
16
|
+
if (!logMarkdown || typeof logMarkdown !== 'string') {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const lines = logMarkdown.split('\n');
|
|
20
|
+
const entries = [];
|
|
21
|
+
for (const line of lines) {
|
|
22
|
+
const match = line.match(LOG_ENTRY_REGEX);
|
|
23
|
+
if (match) {
|
|
24
|
+
const [, timestampStr, message] = match;
|
|
25
|
+
const timestamp = new Date(timestampStr);
|
|
26
|
+
if (isNaN(timestamp.getTime())) {
|
|
27
|
+
continue; // Skip invalid dates
|
|
28
|
+
}
|
|
29
|
+
entries.push({
|
|
30
|
+
timestamp,
|
|
31
|
+
isoTimestamp: timestampStr,
|
|
32
|
+
message: message.trim(),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Sort entries chronologically
|
|
37
|
+
entries.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
38
|
+
return entries.length > 0 ? entries : null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Transform log entries into a timeline with gap indicators.
|
|
42
|
+
* Shows ALL entries, but only displays timestamps for:
|
|
43
|
+
* - First entry
|
|
44
|
+
* - Entries after gaps >= threshold
|
|
45
|
+
* - Last entry
|
|
46
|
+
*
|
|
47
|
+
* This prevents timestamp clutter when LLM writes many entries in quick succession.
|
|
48
|
+
*
|
|
49
|
+
* @param entries - Parsed log entries (sorted chronologically)
|
|
50
|
+
* @param gapThresholdHours - Minimum gap in hours to show timestamp (default: 1)
|
|
51
|
+
* @returns Timeline items for rendering
|
|
52
|
+
*/
|
|
53
|
+
export function createTimelineFromEntries(entries, gapThresholdHours = 1) {
|
|
54
|
+
if (entries.length === 0) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
const timeline = [];
|
|
58
|
+
const gapThresholdMs = gapThresholdHours * 60 * 60 * 1000;
|
|
59
|
+
// Always show first entry with timestamp
|
|
60
|
+
timeline.push({
|
|
61
|
+
type: 'entry',
|
|
62
|
+
timestamp: entries[0].timestamp,
|
|
63
|
+
isoTimestamp: entries[0].isoTimestamp,
|
|
64
|
+
message: entries[0].message,
|
|
65
|
+
showTime: true,
|
|
66
|
+
});
|
|
67
|
+
// Process middle entries
|
|
68
|
+
for (let i = 1; i < entries.length - 1; i++) {
|
|
69
|
+
const prev = entries[i - 1];
|
|
70
|
+
const curr = entries[i];
|
|
71
|
+
const gapMs = curr.timestamp.getTime() - prev.timestamp.getTime();
|
|
72
|
+
const gapHours = (gapMs / gapThresholdMs) * gapThresholdHours;
|
|
73
|
+
if (gapHours >= gapThresholdHours) {
|
|
74
|
+
// Add gap indicator
|
|
75
|
+
timeline.push({
|
|
76
|
+
type: 'gap',
|
|
77
|
+
gapHours,
|
|
78
|
+
});
|
|
79
|
+
// Show this entry with timestamp after the gap
|
|
80
|
+
timeline.push({
|
|
81
|
+
type: 'entry',
|
|
82
|
+
timestamp: curr.timestamp,
|
|
83
|
+
isoTimestamp: curr.isoTimestamp,
|
|
84
|
+
message: curr.message,
|
|
85
|
+
showTime: true,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
// Show entry without timestamp (close to previous)
|
|
90
|
+
timeline.push({
|
|
91
|
+
type: 'entry',
|
|
92
|
+
message: curr.message,
|
|
93
|
+
showTime: false,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Always show last entry
|
|
98
|
+
if (entries.length > 1) {
|
|
99
|
+
const last = entries[entries.length - 1];
|
|
100
|
+
const prev = entries[entries.length - 2];
|
|
101
|
+
const gapMs = last.timestamp.getTime() - prev.timestamp.getTime();
|
|
102
|
+
const gapHours = (gapMs / gapThresholdMs) * gapThresholdHours;
|
|
103
|
+
if (gapHours >= gapThresholdHours) {
|
|
104
|
+
// Add gap indicator before last entry
|
|
105
|
+
timeline.push({
|
|
106
|
+
type: 'gap',
|
|
107
|
+
gapHours,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
// Last entry always shown with timestamp
|
|
111
|
+
timeline.push({
|
|
112
|
+
type: 'entry',
|
|
113
|
+
timestamp: last.timestamp,
|
|
114
|
+
isoTimestamp: last.isoTimestamp,
|
|
115
|
+
message: last.message,
|
|
116
|
+
showTime: true,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return timeline;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Format timestamp for display in timeline.
|
|
123
|
+
* Shows time in HH:MM format.
|
|
124
|
+
*/
|
|
125
|
+
function formatTime(timestamp) {
|
|
126
|
+
const hours = String(timestamp.getHours()).padStart(2, '0');
|
|
127
|
+
const minutes = String(timestamp.getMinutes()).padStart(2, '0');
|
|
128
|
+
return `${hours}:${minutes}`;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Escape HTML special characters.
|
|
132
|
+
*/
|
|
133
|
+
function escapeHtml(text) {
|
|
134
|
+
const htmlEscapes = {
|
|
135
|
+
'&': '&',
|
|
136
|
+
'<': '<',
|
|
137
|
+
'>': '>',
|
|
138
|
+
'"': '"',
|
|
139
|
+
"'": ''',
|
|
140
|
+
};
|
|
141
|
+
return text.replace(/[&<>"']/g, (char) => htmlEscapes[char]);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Render timeline items as HTML.
|
|
145
|
+
* Creates a beautiful timeline that shows:
|
|
146
|
+
* - ALL log entries as messages
|
|
147
|
+
* - Timestamps only for significant moments (start, after gaps, end)
|
|
148
|
+
* - Gap indicators for breaks > threshold
|
|
149
|
+
*
|
|
150
|
+
* @param timeline - Timeline items to render
|
|
151
|
+
* @returns HTML string for the timeline
|
|
152
|
+
*/
|
|
153
|
+
export function renderTimelineHtml(timeline) {
|
|
154
|
+
if (timeline.length === 0) {
|
|
155
|
+
return '<p>No log entries available.</p>';
|
|
156
|
+
}
|
|
157
|
+
const itemsHtml = timeline
|
|
158
|
+
.map((item) => {
|
|
159
|
+
if (item.type === 'gap') {
|
|
160
|
+
const gapHours = item.gapHours?.toFixed(1) ?? '??';
|
|
161
|
+
return `
|
|
162
|
+
<li class="timeline-gap">
|
|
163
|
+
<span class="gap-indicator">
|
|
164
|
+
⏸ ${gapHours}h break
|
|
165
|
+
</span>
|
|
166
|
+
</li>`;
|
|
167
|
+
}
|
|
168
|
+
if (item.type === 'entry' && item.message) {
|
|
169
|
+
const escapedMessage = escapeHtml(item.message);
|
|
170
|
+
if (item.showTime && item.timestamp && item.isoTimestamp) {
|
|
171
|
+
const time = formatTime(item.timestamp);
|
|
172
|
+
return `
|
|
173
|
+
<li class="timeline-entry has-time">
|
|
174
|
+
<time class="timeline-time" datetime="${escapeHtml(item.isoTimestamp)}">${time}</time>
|
|
175
|
+
<span class="timeline-message">${escapedMessage}</span>
|
|
176
|
+
</li>`;
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
// Entry without timestamp - just the message
|
|
180
|
+
return `
|
|
181
|
+
<li class="timeline-entry no-time">
|
|
182
|
+
<span class="timeline-message">${escapedMessage}</span>
|
|
183
|
+
</li>`;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return '';
|
|
187
|
+
})
|
|
188
|
+
.join('');
|
|
189
|
+
return `<ul class="timeline">${itemsHtml}</ul>`;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Get the last log entry from technical log markdown.
|
|
193
|
+
* Useful for displaying a "last logged" nudge in CLI output.
|
|
194
|
+
*
|
|
195
|
+
* @param logMarkdown - The raw markdown content of the technical log
|
|
196
|
+
* @returns The last LogEntry, or null if none found
|
|
197
|
+
*/
|
|
198
|
+
export function getLastLogEntry(logMarkdown) {
|
|
199
|
+
const entries = parseLogMarkdown(logMarkdown);
|
|
200
|
+
return entries ? entries[entries.length - 1] : null;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Main function to transform technical log markdown into timeline HTML.
|
|
204
|
+
* Falls back to plain markdown rendering if preprocessing fails.
|
|
205
|
+
*
|
|
206
|
+
* @param logMarkdown - The raw markdown content of the technical log
|
|
207
|
+
* @returns HTML string (timeline if successful, plain markdown otherwise)
|
|
208
|
+
*/
|
|
209
|
+
export async function transformLogToTimeline(logMarkdown) {
|
|
210
|
+
const entries = parseLogMarkdown(logMarkdown);
|
|
211
|
+
if (!entries || entries.length === 0) {
|
|
212
|
+
// Fall back to plain markdown rendering
|
|
213
|
+
const fallbackContent = logMarkdown && logMarkdown.trim() ? escapeHtml(logMarkdown).replace(/\n/g, '<br>') : 'No log entries available.';
|
|
214
|
+
return '<div class="log-fallback">' + fallbackContent + '</div>';
|
|
215
|
+
}
|
|
216
|
+
const timeline = createTimelineFromEntries(entries);
|
|
217
|
+
return renderTimelineHtml(timeline);
|
|
218
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "project-logbook",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "A command-line tool for project logbooks.",
|
|
5
5
|
"workspaces": [
|
|
6
6
|
"demo-app"
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"@types/fs-extra": "^11.0.4",
|
|
43
43
|
"@types/hast": "^3.0.4",
|
|
44
44
|
"@types/node": "^25.7.0",
|
|
45
|
+
"@types/rss": "^0.0.32",
|
|
45
46
|
"@typescript-eslint/eslint-plugin": "^8.32.1",
|
|
46
47
|
"@typescript-eslint/parser": "^8.32.1",
|
|
47
48
|
"@vitest/coverage-v8": "^3.1.4",
|
|
@@ -65,6 +66,7 @@
|
|
|
65
66
|
"remark-gfm": "^4.0.1",
|
|
66
67
|
"remark-parse": "^11.0.0",
|
|
67
68
|
"remark-rehype": "^11.1.2",
|
|
69
|
+
"rss": "^1.2.2",
|
|
68
70
|
"simple-git": "^3.36.0",
|
|
69
71
|
"unified": "^11.0.5"
|
|
70
72
|
}
|
package/src/templates/index.md
CHANGED
|
@@ -21,6 +21,10 @@ dateEnd: "[DATE_END]"
|
|
|
21
21
|
## Summary
|
|
22
22
|
TODO: Write a polished, highly readable ticket summary that reads like an engaging technical narrative (similar to a well-written dev blog post).
|
|
23
23
|
|
|
24
|
+
### Before You Start:
|
|
25
|
+
- **Check `log.md`**: Review your technical log for all the decisions, errors, and pivots you recorded during implementation.
|
|
26
|
+
- **Reference the log**: Use your real-time log entries as source material for the narrative — don't try to reconstruct from memory.
|
|
27
|
+
|
|
24
28
|
### Formatting & Style Rules:
|
|
25
29
|
- **Maintain the narrative tone:** TODO: Keep the storytelling flair (e.g., describing how problems accumulated or how gaps surfaced), but stay strictly factual based on the provided changes.
|
|
26
30
|
- **Add thematic headings:** TODO: Break the narrative down into logical chapters using Markdown headings (e.g., ### The Friction Points, ### The Fix, ### Closing the Gap).
|
package/src/templates/log.md
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
# Technical Log: {{id}}-{{slug}}
|
|
2
2
|
|
|
3
|
+
> **REMINDER**: Log your work in real-time using `logbook log "<message>"`. Don't wait until the end!
|
|
4
|
+
>
|
|
5
|
+
> Log after every significant step: investigation, errors, decisions, code changes, test runs, etc.
|
|
6
|
+
>
|
|
7
|
+
|
|
3
8
|
## Protocol
|
|
4
9
|
- {{fullIso}}: Started investigation.
|
package/src/templates/steer.txt
CHANGED
|
@@ -8,11 +8,27 @@ Phase 1: Understand
|
|
|
8
8
|
2. Review `AGENTS.md` and `CONTRIBUTING.md` if you need architectural or workflow context.
|
|
9
9
|
|
|
10
10
|
Phase 2: Execute & Trace
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
|
|
12
|
+
**CRITICAL: Real-Time Logging**
|
|
13
|
+
You MUST log your work continuously using `logbook log "<message>"`. This is not optional.
|
|
14
|
+
|
|
15
|
+
1. Use `log.md` as your live technical scratchpad.
|
|
16
|
+
2. After EVERY significant step, run: `logbook log "<what you just did>"`
|
|
17
|
+
- Investigated a file? Log it.
|
|
18
|
+
- Found an error? Log it.
|
|
19
|
+
- Made a design decision? Log it.
|
|
20
|
+
- Fixed a bug? Log it.
|
|
21
|
+
- Ran tests? Log the result.
|
|
22
|
+
|
|
23
|
+
Examples:
|
|
24
|
+
- `logbook log "Investigated src/lib/config.ts - found missing validation"`
|
|
25
|
+
- `logbook log "Created new RSS module with generateRssFeed() function"`
|
|
26
|
+
- `logbook log "Test failed: TypeScript error on line 42 - fixed type assertion"`
|
|
27
|
+
- `logbook log "All 127 tests pass, pre-commit suite successful"`
|
|
28
|
+
|
|
29
|
+
3. **One message per command call.** For multiple entries, call the command multiple times.
|
|
30
|
+
4. Do NOT reconstruct the log at the end. If the log is empty when you finish, you did it wrong.
|
|
31
|
+
5. Stick to the active entry; never modify past entries in the logbook folder.
|
|
16
32
|
|
|
17
33
|
Phase 3: Synthesize
|
|
18
34
|
1. When implementation is finished, write the narrative in `index.md`.
|
package/src/templates/styles.css
CHANGED
|
@@ -696,3 +696,119 @@ article img[src$='.webp'],
|
|
|
696
696
|
article img[src$='.svg'] {
|
|
697
697
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
698
698
|
}
|
|
699
|
+
|
|
700
|
+
/* Technical Log Timeline */
|
|
701
|
+
#log .timeline {
|
|
702
|
+
list-style: none;
|
|
703
|
+
padding: 0;
|
|
704
|
+
margin: 0;
|
|
705
|
+
position: relative;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
#log .timeline::before {
|
|
709
|
+
content: '';
|
|
710
|
+
position: absolute;
|
|
711
|
+
left: 8px;
|
|
712
|
+
top: 0;
|
|
713
|
+
bottom: 0;
|
|
714
|
+
width: 2px;
|
|
715
|
+
background: var(--border);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
#log .timeline-entry {
|
|
719
|
+
position: relative;
|
|
720
|
+
padding-left: 2rem;
|
|
721
|
+
margin-bottom: 0.75rem;
|
|
722
|
+
min-height: 1.5rem;
|
|
723
|
+
display: grid;
|
|
724
|
+
grid-template-columns: 55px 1fr;
|
|
725
|
+
gap: 0.5rem;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
#log .timeline-entry.no-time .timeline-message {
|
|
729
|
+
grid-column: 2;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
#log .timeline-entry.has-time::before {
|
|
733
|
+
content: '';
|
|
734
|
+
position: absolute;
|
|
735
|
+
left: 5px;
|
|
736
|
+
top: 4px;
|
|
737
|
+
width: 8px;
|
|
738
|
+
height: 8px;
|
|
739
|
+
border-radius: 50%;
|
|
740
|
+
background: var(--primary);
|
|
741
|
+
border: 2px solid var(--bg);
|
|
742
|
+
z-index: 1;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
#log .timeline-entry.no-time::before {
|
|
746
|
+
content: '';
|
|
747
|
+
position: absolute;
|
|
748
|
+
left: 7px;
|
|
749
|
+
top: 0.6rem;
|
|
750
|
+
width: 4px;
|
|
751
|
+
height: 4px;
|
|
752
|
+
border-radius: 50%;
|
|
753
|
+
background: var(--text-muted);
|
|
754
|
+
opacity: 0.6;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
#log .timeline-time {
|
|
758
|
+
font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, monospace;
|
|
759
|
+
font-size: 0.75rem;
|
|
760
|
+
font-weight: 600;
|
|
761
|
+
color: var(--primary);
|
|
762
|
+
display: inline-block;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
#log .timeline-message {
|
|
766
|
+
font-size: 0.9375rem;
|
|
767
|
+
color: var(--text);
|
|
768
|
+
line-height: 1.5;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
#log .timeline-entry.no-time .timeline-message {
|
|
772
|
+
color: var(--text);
|
|
773
|
+
opacity: 0.85;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
#log .timeline-gap {
|
|
777
|
+
position: relative;
|
|
778
|
+
padding-left: 2rem;
|
|
779
|
+
margin: 1.5rem 0;
|
|
780
|
+
min-height: 1rem;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
#log .timeline-gap::before {
|
|
784
|
+
content: '';
|
|
785
|
+
position: absolute;
|
|
786
|
+
left: 7px;
|
|
787
|
+
top: 0.4rem;
|
|
788
|
+
width: 4px;
|
|
789
|
+
height: 4px;
|
|
790
|
+
border-radius: 50%;
|
|
791
|
+
background: var(--text-muted);
|
|
792
|
+
opacity: 0.5;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
#log .gap-indicator {
|
|
796
|
+
font-size: 0.8125rem;
|
|
797
|
+
color: var(--text-muted);
|
|
798
|
+
font-style: italic;
|
|
799
|
+
display: inline-flex;
|
|
800
|
+
align-items: center;
|
|
801
|
+
gap: 0.25rem;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
#log .log-fallback {
|
|
805
|
+
font-size: 0.9375rem;
|
|
806
|
+
color: var(--text);
|
|
807
|
+
line-height: 1.6;
|
|
808
|
+
white-space: pre-wrap;
|
|
809
|
+
background: var(--primary-soft);
|
|
810
|
+
padding: 1rem;
|
|
811
|
+
border-radius: 0.5rem;
|
|
812
|
+
font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, monospace;
|
|
813
|
+
font-size: 0.8125rem;
|
|
814
|
+
}
|