@davevdveen/spec-reader 0.1.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dave van der Veen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # SpecReader
2
+
3
+ Beautiful reading experience for [OpenSpec](https://openspec.dev) specification files and project documentation.
4
+
5
+ ## Why
6
+
7
+ AI-assisted development generates specs, proposals, and designs faster than ever. But reviewing structured markdown in a terminal or a GitHub diff is painful. When specs are hard to read, they don't get read.
8
+
9
+ SpecReader gives your specifications a proper reading experience. Collapsible requirements, themed reading modes, and structured navigation make it natural to review proposals and specs, whether they're yours or someone else's.
10
+
11
+ ## Quick start
12
+
13
+ ```bash
14
+ npx @davevdveen/spec-reader
15
+ ```
16
+
17
+ Works on any project directory. Auto-detects OpenSpec folders and renders everything readable: specs, proposals, READMEs, configs, and more.
18
+
19
+ Try it on the [OpenSpec framework](https://github.com/Fission-AI/OpenSpec) itself:
20
+
21
+ ```bash
22
+ git clone https://github.com/Fission-AI/OpenSpec && cd OpenSpec && npx @davevdveen/spec-reader
23
+ ```
24
+
25
+ ## AI coding agent skill
26
+
27
+ SpecReader includes a skill for AI coding agents (Claude Code, OpenCode) that opens the viewer for any PR or branch:
28
+
29
+ ```
30
+ /read-specs # open specs for current branch
31
+ /read-specs 42 # checkout PR #42 and open its specs
32
+ /read-specs feature/xyz # checkout branch and open specs
33
+ ```
34
+
35
+ Install the skill in your project:
36
+
37
+ ```bash
38
+ npx @davevdveen/spec-reader init
39
+ ```
40
+
41
+ This auto-detects your agent (Claude Code, OpenCode, or both) and installs the skill in the right place.
42
+
43
+ ## Scopes
44
+
45
+ | Scope | Shows |
46
+ |-------|-------|
47
+ | Specs | OpenSpec files |
48
+ | Docs | All markdown files |
49
+ | All | All readable files (.md, .yaml, .yml, .txt, extensionless) |
50
+ | Search | Filter across all files |
51
+
52
+ ## Themes
53
+
54
+ Four reading themes, each with light and dark variants:
55
+
56
+ | Theme | Font |
57
+ |-------|------|
58
+ | Original | Sans-serif |
59
+ | Paper | Georgia serif |
60
+ | Calm | Serif, warm tones |
61
+ | Mono | Monospace |
62
+
63
+ Follows system light/dark preference with manual override.
64
+
65
+ ## Commands
66
+
67
+ ```bash
68
+ spec-reader # serve current directory
69
+ spec-reader ~/projects/myapp # serve any project
70
+ spec-reader export openspec/ dist/ # export static site
71
+ spec-reader init # install AI agent skill
72
+ view-proposal add-dark-mode # open viewer on a proposal
73
+ ```
74
+
75
+ ## Keyboard shortcuts
76
+
77
+ | Key | Action |
78
+ |-----|--------|
79
+ | `←` `→` | Previous / next page |
80
+ | `↑` `↓` | Scroll content |
81
+ | `-` | Expand / collapse all sections in the document |
82
+ | `Cmd+Shift+R` | Toggle sidebar |
83
+
84
+ ## How it works
85
+
86
+ A single HTML file served by a shell script that walks your directory, generates a manifest, and starts Python's built-in HTTP server. Zero runtime dependencies beyond Python 3.
87
+
88
+ File changes are detected every 3 seconds. Edit a spec, see it update in the viewer.
89
+
90
+ ## Prerequisites
91
+
92
+ - **Python 3** (used to serve files locally)
93
+ - **Node.js** (for `npx` installation only)
94
+ - **gh CLI** (optional, for the `/read-specs` skill to checkout PRs)
95
+
96
+ Works on macOS, Linux, and Windows (WSL). Tested in Safari, should work in any modern browser.
97
+
98
+ ## Install
99
+
100
+ ```bash
101
+ npm install -g @davevdveen/spec-reader
102
+ npx @davevdveen/spec-reader
103
+ ```
104
+
105
+ ## License
106
+
107
+ MIT
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@davevdveen/spec-reader",
3
+ "version": "0.1.2",
4
+ "description": "Beautiful reading experience for OpenSpec specification files",
5
+ "bin": {
6
+ "spec-reader": "./spec-reader",
7
+ "view-proposal": "./view-proposal"
8
+ },
9
+ "files": [
10
+ "spec-reader",
11
+ "read-specs",
12
+ "view-proposal",
13
+ "spec-viewer.html",
14
+ "skills/"
15
+ ],
16
+ "keywords": [
17
+ "openspec",
18
+ "markdown",
19
+ "viewer",
20
+ "specification",
21
+ "documentation"
22
+ ],
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/davevdveen/spec-reader"
27
+ }
28
+ }
package/read-specs ADDED
@@ -0,0 +1,210 @@
1
+ #!/bin/bash
2
+ # read-specs — Discover files, generate manifest, and serve the viewer.
3
+ #
4
+ # Usage:
5
+ # ./read-specs [directory] Serve the viewer for the given directory (default: .)
6
+ # ./read-specs --manifest-only [dir] Generate manifest.json without starting a server
7
+ #
8
+ # The viewer HTML stays in this script's directory. Nothing is copied to the
9
+ # target directory — only a temporary manifest.json is created there and
10
+ # cleaned up on exit.
11
+
12
+ set -euo pipefail
13
+
14
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
15
+ TARGET_DIR="${2:-${1:-.}}"
16
+ MANIFEST_ONLY=false
17
+ FOCUS=""
18
+ PORT="${PORT:-3333}"
19
+
20
+ # Parse flags
21
+ if [[ "${1:-}" == "--manifest-only" ]]; then
22
+ MANIFEST_ONLY=true
23
+ TARGET_DIR="${2:-.}"
24
+ elif [[ "${1:-}" == "--focus" ]]; then
25
+ FOCUS="${2:-}"
26
+ TARGET_DIR="${3:-.}"
27
+ elif [[ "${1:-}" == --* ]]; then
28
+ echo "Unknown flag: $1" >&2
29
+ exit 1
30
+ fi
31
+
32
+ # Resolve target directory
33
+ TARGET_DIR="$(cd "$TARGET_DIR" && pwd)"
34
+
35
+ # ── Generate manifest.json ──
36
+ generate_manifest() {
37
+ local dir="$1"
38
+ local output="$2"
39
+ local first=true
40
+
41
+ local absdir
42
+ absdir="$(cd "$dir" && pwd)"
43
+ echo '{"generated":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'","root":"'"$absdir"'","files":[' > "$output"
44
+
45
+ while IFS= read -r filepath; do
46
+ # Strip leading ./
47
+ filepath="${filepath#./}"
48
+
49
+ # Skip build artifacts, git, and dependency folders
50
+ [[ "$filepath" == .git/* ]] && continue
51
+ [[ "$filepath" == .build/* ]] && continue
52
+ [[ "$filepath" == */.build/* ]] && continue
53
+ [[ "$filepath" == node_modules/* ]] && continue
54
+ [[ "$filepath" == */node_modules/* ]] && continue
55
+ [[ "$filepath" == Pods/* ]] && continue
56
+ [[ "$filepath" == */Pods/* ]] && continue
57
+ [[ "$filepath" == Carthage/* ]] && continue
58
+ [[ "$filepath" == */Carthage/* ]] && continue
59
+ [[ "$filepath" == */checkouts/* ]] && continue
60
+ [[ "$filepath" == *.docc/* ]] && continue
61
+ [[ "$filepath" == manifest.json ]] && continue
62
+ [[ "$(basename "$filepath")" == .openspec.yaml ]] && continue
63
+
64
+ local name
65
+ name="$(basename "$filepath")"
66
+
67
+ # Calculate task counts for tasks.md files
68
+ local task_checked="" task_total=""
69
+ if [[ "$name" == "tasks.md" ]]; then
70
+ local checked unchecked total
71
+ checked=$(grep -c '\- \[x\]' "$dir/$filepath" 2>/dev/null || true)
72
+ unchecked=$(grep -c '\- \[ \]' "$dir/$filepath" 2>/dev/null || true)
73
+ checked=${checked:-0}
74
+ unchecked=${unchecked:-0}
75
+ checked=$(echo "$checked" | tr -d '[:space:]')
76
+ unchecked=$(echo "$unchecked" | tr -d '[:space:]')
77
+ total=$((checked + unchecked))
78
+ if [[ $total -gt 0 ]]; then
79
+ task_checked=$checked
80
+ task_total=$total
81
+ fi
82
+ fi
83
+
84
+ # Emit JSON — just path and name, no section classification
85
+ if $first; then
86
+ first=false
87
+ else
88
+ printf ',' >> "$output"
89
+ fi
90
+
91
+ if [[ -n "$task_checked" ]]; then
92
+ printf '{"path":"%s","name":"%s","taskChecked":%s,"taskTotal":%s}' \
93
+ "$filepath" "$name" "$task_checked" "$task_total" >> "$output"
94
+ else
95
+ printf '{"path":"%s","name":"%s"}' \
96
+ "$filepath" "$name" >> "$output"
97
+ fi
98
+
99
+ done < <(cd "$dir" && find . -type f \( \
100
+ -name '*.md' -o -name '*.yaml' -o -name '*.yml' -o -name '*.txt' \
101
+ -o -name 'Brewfile' -o -name 'Gemfile' -o -name 'Podfile' -o -name 'Fastfile' \
102
+ -o -name 'Appfile' -o -name 'Matchfile' -o -name 'Pluginfile' -o -name 'Dangerfile' \
103
+ -o -name 'Dockerfile' -o -name 'Makefile' -o -name 'CODEOWNERS' -o -name 'LICENSE' \
104
+ \) | sort)
105
+
106
+ echo ']}' >> "$output"
107
+ }
108
+
109
+ # ── Main ──
110
+
111
+ # Generate manifest in a temp location
112
+ MANIFEST="$(mktemp)"
113
+ generate_manifest "$TARGET_DIR" "$MANIFEST"
114
+ echo "Generated manifest with $(grep -o '"path"' "$MANIFEST" | wc -l | tr -d ' ') files"
115
+
116
+ if $MANIFEST_ONLY; then
117
+ cp "$MANIFEST" "$TARGET_DIR/manifest.json"
118
+ rm -f "$MANIFEST"
119
+ echo "Wrote manifest.json to $TARGET_DIR"
120
+ exit 0
121
+ fi
122
+
123
+ # Start a Python server that serves:
124
+ # /spec-viewer.html → from SCRIPT_DIR
125
+ # /manifest.json → from temp file
126
+ # everything else → from TARGET_DIR
127
+ python3 -u - "$SCRIPT_DIR" "$TARGET_DIR" "$MANIFEST" "$PORT" <<'PYSERVER' &
128
+ import http.server
129
+ import os
130
+ import sys
131
+ import urllib.parse
132
+
133
+ script_dir = sys.argv[1]
134
+ target_dir = sys.argv[2]
135
+ manifest_path = sys.argv[3]
136
+ port = int(sys.argv[4])
137
+
138
+ class SpecHandler(http.server.SimpleHTTPRequestHandler):
139
+ def do_GET(self):
140
+ clean_path = self.path.split('?', 1)[0].split('#', 1)[0].lstrip('/')
141
+ if clean_path == 'api/refresh':
142
+ import subprocess
143
+ subprocess.run(
144
+ [os.path.join(script_dir, 'read-specs'), '--manifest-only', target_dir],
145
+ capture_output=True
146
+ )
147
+ import shutil
148
+ generated = os.path.join(target_dir, 'manifest.json')
149
+ if os.path.exists(generated):
150
+ shutil.copy(generated, manifest_path)
151
+ os.remove(generated)
152
+ self.send_response(200)
153
+ self.send_header('Content-Type', 'application/json')
154
+ self.send_header('Access-Control-Allow-Origin', '*')
155
+ self.end_headers()
156
+ self.wfile.write(b'{"ok":true}')
157
+ return
158
+ super().do_GET()
159
+
160
+ def translate_path(self, path):
161
+ # Strip query string and fragment
162
+ path = path.split('?', 1)[0].split('#', 1)[0]
163
+ # URL-decode (handles %20 for spaces, etc.)
164
+ path = urllib.parse.unquote(path)
165
+ # Remove leading /
166
+ path = path.lstrip('/')
167
+
168
+ # Serve viewer HTML from the script directory
169
+ if path == 'spec-viewer.html':
170
+ return os.path.join(script_dir, 'spec-viewer.html')
171
+
172
+ # Serve manifest from temp file
173
+ if path == 'manifest.json':
174
+ return manifest_path
175
+
176
+ # Everything else from target directory
177
+ return os.path.join(target_dir, path)
178
+
179
+ def log_message(self, format, *args):
180
+ pass # Silence request logs
181
+
182
+ server = http.server.HTTPServer(('', port), SpecHandler)
183
+ print(f"Serving specs from {target_dir}", flush=True)
184
+ server.serve_forever()
185
+ PYSERVER
186
+ SERVER_PID=$!
187
+
188
+ cleanup() {
189
+ kill "$SERVER_PID" 2>/dev/null
190
+ rm -f "$MANIFEST"
191
+ }
192
+ trap cleanup EXIT
193
+
194
+ sleep 1
195
+
196
+ URL="http://localhost:$PORT/spec-viewer.html"
197
+ if [[ -n "$FOCUS" ]]; then
198
+ ENCODED=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$FOCUS")
199
+ URL="${URL}#${ENCODED}"
200
+ fi
201
+ echo "Viewer: $URL"
202
+
203
+ if command -v open &>/dev/null; then
204
+ open "$URL"
205
+ elif command -v xdg-open &>/dev/null; then
206
+ xdg-open "$URL"
207
+ fi
208
+
209
+ echo "Press Ctrl+C to stop"
210
+ wait
@@ -0,0 +1,35 @@
1
+ ---
2
+ name: read-specs
3
+ description: Open the SpecReader viewer to read OpenSpec specifications, proposals, designs, tasks, and project documentation in a themed reading experience. Use this skill whenever the user wants to read specs, review a proposal, browse documentation, view OpenSpec files, read READMEs, or says "open specs", "read specs", "show me the specs", "review the proposal", "open the viewer", or "/read-specs". Also trigger when the user wants to review specs from a specific PR or branch.
4
+ ---
5
+
6
+ Open the SpecReader viewer for the current repo's OpenSpec files and documentation, or for a specific PR or branch.
7
+
8
+ Works with Claude Code and OpenCode.
9
+
10
+ ## Usage
11
+
12
+ - `/read-specs` — open specs for the current branch
13
+ - `/read-specs 42` — checkout PR #42 and open its specs
14
+ - `/read-specs feature/my-branch` — checkout branch and open its specs
15
+
16
+ ## Steps
17
+
18
+ 1. If an argument is provided:
19
+ - If it's a number, run `gh pr checkout <number>` (requires gh CLI)
20
+ - If it's a branch name, run `git checkout <branch>`
21
+
22
+ 2. Find the project root (look for `.git` directory).
23
+
24
+ 3. Run the viewer in the background:
25
+ ```bash
26
+ npx @davevdveen/spec-reader &
27
+ ```
28
+ This starts a local server and opens the browser.
29
+
30
+ 4. Tell the user:
31
+ - "Spec viewer is open at http://localhost:3333"
32
+ - "Changes are auto-detected every 3 seconds"
33
+ - "Use arrow keys to navigate between pages"
34
+ - "Use the scope bar to switch between Specs, Docs, All, or Search"
35
+ - "Use the Aa button to change themes and light/dark mode"
package/spec-reader ADDED
@@ -0,0 +1,104 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # spec-reader — View, export, or set up SpecReader
5
+ #
6
+ # Usage:
7
+ # spec-reader [directory] Serve the viewer (default: .)
8
+ # spec-reader export [dir] [output] Export static site to a directory
9
+ # spec-reader init Install the /read-specs skill for your AI coding agent
10
+
11
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
12
+
13
+ case "${1:-}" in
14
+ init)
15
+ SKILL_SRC="$SCRIPT_DIR/skills/read-specs/SKILL.md"
16
+ installed=false
17
+
18
+ if [[ -d ".claude" ]]; then
19
+ mkdir -p .claude/skills/read-specs
20
+ cp "$SKILL_SRC" .claude/skills/read-specs/SKILL.md
21
+ echo "Installed skill to .claude/skills/read-specs/"
22
+ installed=true
23
+ fi
24
+
25
+ if [[ -d ".opencode" ]]; then
26
+ mkdir -p .opencode/skills/read-specs
27
+ cp "$SKILL_SRC" .opencode/skills/read-specs/SKILL.md
28
+ echo "Installed skill to .opencode/skills/read-specs/"
29
+ installed=true
30
+ fi
31
+
32
+ if [[ "$installed" == false ]]; then
33
+ echo "No .claude/ or .opencode/ directory found."
34
+ echo ""
35
+ echo "Which agent do you use?"
36
+ echo " 1) Claude Code (.claude/skills/)"
37
+ echo " 2) OpenCode (.opencode/skills/)"
38
+ echo " 3) Both"
39
+ read -rp "Choice [1/2/3]: " choice
40
+ case "$choice" in
41
+ 1)
42
+ mkdir -p .claude/skills/read-specs
43
+ cp "$SKILL_SRC" .claude/skills/read-specs/SKILL.md
44
+ echo "Installed skill to .claude/skills/read-specs/"
45
+ ;;
46
+ 2)
47
+ mkdir -p .opencode/skills/read-specs
48
+ cp "$SKILL_SRC" .opencode/skills/read-specs/SKILL.md
49
+ echo "Installed skill to .opencode/skills/read-specs/"
50
+ ;;
51
+ 3)
52
+ mkdir -p .claude/skills/read-specs .opencode/skills/read-specs
53
+ cp "$SKILL_SRC" .claude/skills/read-specs/SKILL.md
54
+ cp "$SKILL_SRC" .opencode/skills/read-specs/SKILL.md
55
+ echo "Installed skill to .claude/skills/read-specs/ and .opencode/skills/read-specs/"
56
+ ;;
57
+ *)
58
+ echo "Cancelled."
59
+ exit 1
60
+ ;;
61
+ esac
62
+ fi
63
+
64
+ echo ""
65
+ echo "You can now use /read-specs in your AI coding agent."
66
+ ;;
67
+
68
+ export)
69
+ SOURCE_DIR="${2:-.}"
70
+ OUTPUT_DIR="${3:-spec-site}"
71
+
72
+ echo "Exporting spec site from $SOURCE_DIR to $OUTPUT_DIR..."
73
+
74
+ mkdir -p "$OUTPUT_DIR"
75
+
76
+ # Copy spec files
77
+ cd "$SOURCE_DIR"
78
+ find . -type f \( -name '*.md' -o -name '*.yaml' -o -name '*.yml' \) | while read -r f; do
79
+ f="${f#./}"
80
+ [[ "$f" == .git/* ]] && continue
81
+ [[ "$f" == */.build/* ]] && continue
82
+ [[ "$f" == */node_modules/* ]] && continue
83
+ mkdir -p "$OUTPUT_DIR/$(dirname "$f")"
84
+ cp "$f" "$OUTPUT_DIR/$f"
85
+ done
86
+ cd - > /dev/null
87
+
88
+ # Generate manifest
89
+ "$SCRIPT_DIR/read-specs" --manifest-only "$SOURCE_DIR"
90
+ mv "$SOURCE_DIR/manifest.json" "$OUTPUT_DIR/manifest.json"
91
+
92
+ # Copy viewer as index.html
93
+ cp "$SCRIPT_DIR/spec-viewer.html" "$OUTPUT_DIR/index.html"
94
+
95
+ echo "Spec site exported to $OUTPUT_DIR/"
96
+ echo " Files: $(find "$OUTPUT_DIR" -type f | wc -l | tr -d ' ')"
97
+ echo " Serve: cd $OUTPUT_DIR && python3 -m http.server 3333"
98
+ ;;
99
+
100
+ *)
101
+ # Serve mode — serve from the given directory
102
+ exec "$SCRIPT_DIR/read-specs" "${1:-.}"
103
+ ;;
104
+ esac