@heart-of-gold/toolkit 0.1.32 → 0.1.34

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.
@@ -15,19 +15,19 @@
15
15
  "name": "deep-thought",
16
16
  "source": "./plugins/deep-thought",
17
17
  "description": "The Answer Computer — reasoning tools for brainstorming, planning, and deep thinking",
18
- "version": "0.2.2"
18
+ "version": "0.2.4"
19
19
  },
20
20
  {
21
21
  "name": "marvin",
22
22
  "source": "./plugins/marvin",
23
23
  "description": "The Paranoid Android — quality tools for code review, knowledge compounding, and work execution",
24
- "version": "0.3.0"
24
+ "version": "0.3.4"
25
25
  },
26
26
  {
27
27
  "name": "babel-fish",
28
28
  "source": "./plugins/babel-fish",
29
29
  "description": "Universal Translator — media generation tools for audio, image, and video content",
30
- "version": "0.2.0"
30
+ "version": "0.2.2"
31
31
  },
32
32
  {
33
33
  "name": "quellis",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heart-of-gold/toolkit",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "type": "module",
5
5
  "description": "Cross-platform installer for Heart of Gold skills — works with Codex, OpenCode, Pi, Claude Code, and more",
6
6
  "bin": {
@@ -11,6 +11,7 @@
11
11
  "check:security": "python3 scripts/check-security-regressions.py",
12
12
  "check:compat": "python3 scripts/check-harness-compatibility.py",
13
13
  "test:pi-guided": "node --test tests/pi-guided-workflows.test.mjs",
14
+ "test:visualize": "node --test tests/visualize-smart-render.test.mjs",
14
15
  "prepublishOnly": "npm run check:publish-safety && npm run check:compat"
15
16
  },
16
17
  "dependencies": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "babel-fish",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Universal Translator — media generation tools for audio, image, video, and visualization",
5
5
  "author": {
6
6
  "name": "ondrej-svec",
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  name: visualize
3
3
  description: >
4
- Render mind maps and tree visualizations from markdown. Prefer terminal output for quick local inspection,
5
- and generate shareable HTML when the user wants a browser view and share-html infrastructure is configured.
6
- Works on brainstorm docs, plan docs, markdown files, or any structured content. Triggers: visualize,
7
- mindmap, mind map, show me the structure, draw a map.
4
+ Render mind maps and tree visualizations from markdown. Prefer shareable HTML first for brainstorms, plans,
5
+ architecture docs, and other structured workflow artifacts when share-html infrastructure is configured;
6
+ otherwise fall back to terminal output for quick local inspection. Works on markdown files or any structured
7
+ content. Triggers: visualize, mindmap, mind map, show me the structure, draw a map.
8
8
  allowed-tools:
9
9
  - Read
10
10
  - Bash
@@ -21,24 +21,28 @@ Translating structured text into spatial understanding. Because walls of text hi
21
21
  - MAY: read files, generate terminal mind maps, generate temporary HTML artifacts, run renderer/share scripts via bash
22
22
  - MAY NOT: modify project files, create persistent files outside temp/output artifacts, install unrelated packages
23
23
 
24
- ## The Renderer
24
+ ## The Renderers
25
25
 
26
- A Node.js script that converts markdown headings into colored, spatial Unicode mind maps.
26
+ Visualization now has two layers:
27
+ - `scripts/smart-render.js` — renders one HTML artifact using the mode the coding agent chose (or a safe fallback)
28
+ - `scripts/render-mindmap/index.js` — specialized mind-map renderer for branchy content
27
29
 
28
- **Location:** `scripts/render-mindmap/index.js` (relative to this skill's directory)
30
+ **Locations:**
31
+ - `scripts/smart-render.js`
32
+ - `scripts/render-mindmap/index.js`
29
33
 
30
- **To find the script path**, locate it by searching for `render-mindmap/index.js`:
34
+ **To find the smart renderer path**, locate it by searching for `smart-render.js`:
31
35
  ```bash
32
36
  # Option 1: Use CLAUDE_PLUGIN_ROOT if available
33
- SCRIPT="${CLAUDE_PLUGIN_ROOT}/skills/visualize/scripts/render-mindmap/index.js"
37
+ SCRIPT="${CLAUDE_PLUGIN_ROOT}/skills/visualize/scripts/smart-render.js"
34
38
 
35
39
  # Option 2: Search for it
36
- SCRIPT=$(find ~/.claude/plugins -path "*/babel-fish/skills/visualize/scripts/render-mindmap/index.js" 2>/dev/null | head -1)
40
+ SCRIPT=$(find ~/.claude/plugins -path "*/babel-fish/skills/visualize/scripts/smart-render.js" 2>/dev/null | head -1)
37
41
  ```
38
42
 
39
- **First run:** If `node_modules/` doesn't exist in the renderer directory, run `npm install` there first:
43
+ **First run:** If `node_modules/` doesn't exist in the mind-map renderer directory, run `npm install` there first:
40
44
  ```bash
41
- RENDER_DIR=$(dirname "$SCRIPT")
45
+ RENDER_DIR=$(dirname "$SCRIPT")/render-mindmap
42
46
  if [ ! -d "$RENDER_DIR/node_modules" ]; then
43
47
  (cd "$RENDER_DIR" && npm install --silent)
44
48
  fi
@@ -47,33 +51,33 @@ fi
47
51
  ## Usage
48
52
 
49
53
  ```bash
50
- # Render a markdown file in the terminal
51
- node "$SCRIPT" path/to/file.md
52
-
53
- # With options
54
- node "$SCRIPT" --no-color path/to/file.md # plain Unicode, no ANSI
55
- node "$SCRIPT" --width 120 path/to/file.md # constrain to 120 columns
56
- node "$SCRIPT" --depth 2 path/to/file.md # limit tree depth
57
- node "$SCRIPT" --json path/to/data.json # JSON tree input
58
- node "$SCRIPT" --html /tmp/map.html path/to/file.md # generate HTML mind map
59
-
60
- # Pipe markdown
61
- echo "# Root\n## Branch A\n## Branch B" | node "$SCRIPT"
54
+ # Generate a safe default HTML visualization for a markdown file
55
+ node "$SCRIPT" path/to/file.md --out /tmp/view.html
56
+
57
+ # Usually the coding agent should choose the mode from context
58
+ node "$SCRIPT" path/to/file.md --mode roadmap --out /tmp/view.html
59
+ node "$SCRIPT" path/to/file.md --mode outline --out /tmp/view.html
60
+ node "$SCRIPT" path/to/file.md --mode architecture --out /tmp/view.html
61
+ node "$SCRIPT" path/to/file.md --mode mindmap --out /tmp/view.html
62
+
63
+ # Use the specialized mind-map renderer directly when needed
64
+ node "$(dirname "$SCRIPT")/render-mindmap/index.js" --html /tmp/map.html path/to/file.md
62
65
  ```
63
66
 
64
67
  ### Shareable HTML flow
65
68
 
66
- Use the helper script when the user wants a browser URL and the share server is already configured:
69
+ Use the helper script when the user wants a browser URL and the share server is already configured. It now generates one polished HTML artifact first, then publishes it:
67
70
 
68
71
  ```bash
69
72
  bash scripts/render-and-share.sh path/to/file.md
70
73
  ```
71
74
 
72
75
  This script:
73
- 1. generates an HTML mind map via the existing renderer
74
- 2. locates `share-html/scripts/publish.sh`
75
- 3. publishes the artifact to the configured local share server
76
- 4. prints the publish JSON so you can return the URL
76
+ 1. generates one HTML artifact via the smart renderer
77
+ 2. uses the mode the coding agent chose (or the renderer's safe default)
78
+ 3. locates `share-html/scripts/publish.sh`
79
+ 4. publishes the artifact to the configured local share server
80
+ 5. prints the publish result so you can return the URL
77
81
 
78
82
  ## Rendering Behavior
79
83
 
@@ -88,25 +92,28 @@ This script:
88
92
 
89
93
  ## Phase 0 — Determine What to Visualize
90
94
 
91
- First decide whether the user wants:
92
- - a quick terminal view
93
- - a browser/shareable HTML view
95
+ First decide whether this should be a browser/shareable HTML view or a quick terminal view.
94
96
 
95
- **Prefer browser/shareable HTML when:**
97
+ **Prefer browser/shareable HTML first when:**
98
+ - the source is a brainstorm, plan, architecture doc, or other structured workflow artifact
96
99
  - the user asks to open it in a browser
97
100
  - the user wants to share the result with another person or device
98
101
  - the structure is large enough that browser navigation is more useful than terminal rendering
102
+ - `share-html` is configured
99
103
 
100
104
  **Prefer terminal rendering when:**
101
- - the user wants a quick local look
102
- - the environment is SSH-heavy or browser access is not requested
103
105
  - share-html is not configured
106
+ - the user explicitly wants a quick terminal-only look
107
+ - the environment is SSH-heavy and the browser/share path is not requested
104
108
 
105
109
  When invoked as `/visualize [path]`:
106
110
 
107
111
  **If a file path is provided:**
108
112
  1. Read the file
109
- 2. Render it directly with the mind map renderer
113
+ 2. Decide what kind of visual artifact would help most from context
114
+ 3. If confidence is high enough, choose the mode and generate/share HTML first
115
+ 4. If confidence is not high enough, ask the user which direction would help most
116
+ 5. If sharing is unavailable or the user explicitly wants terminal output, fall back appropriately
110
117
 
111
118
  **If no path is provided:**
112
119
  1. Check if there's a recent brainstorm or plan document in the conversation context
@@ -140,27 +147,81 @@ The default mode is **vertical layout** — boxes on main branches, compact leav
140
147
 
141
148
  ### Path B — Shareable HTML
142
149
 
143
- If the user wants browser viewing or sharing and `share-html` is configured:
150
+ For substantial artifacts, prefer this path first when `share-html` is configured.
151
+
152
+ The coding agent should choose the mode from context. Toolkit guidance:
153
+ - plans often fit `roadmap` or `outline`
154
+ - architecture docs often fit `architecture` or `outline`
155
+ - concise branchy brainstorms may fit `mindmap`
156
+ - product/UI concepts may fit `mockup`
157
+ - stakeholder-friendly summaries may fit `explainer`
158
+
159
+ If uncertain, ask the user using the harness's structured choice UI when available; otherwise present concise plain-text options.
160
+
161
+ ### Uncertainty protocol
162
+
163
+ When the best visualization is not clear, do **not** silently guess if the choice would materially affect usefulness.
164
+
165
+ Ask **one concise question at a time**:
166
+ - state the decision in plain language
167
+ - offer 2-4 explicit options
168
+ - include a recommended option when you have one
169
+ - keep option labels outcome-focused, not renderer-jargon-first
170
+
171
+ Good pattern:
172
+ - "Which would help most here?"
173
+ - `Roadmap` — show phases, sequencing, and implementation progress
174
+ - `Outline` — show the document structure clearly
175
+ - `Mind map` — show branching ideas and relationships
176
+ - `Architecture view` — show components, boundaries, and decisions
177
+
178
+ If the harness supports structured choices, use them.
179
+ If not, use a short plain-text question such as:
180
+
181
+ ```text
182
+ I can visualize this a few different ways. Which would be most useful?
183
+ 1. Roadmap — phases and tasks
184
+ 2. Outline — document structure
185
+ 3. Mind map — branching ideas
186
+ 4. Architecture view — components and boundaries
187
+ ```
188
+
189
+ If the user does not care or says "you decide," choose the safest useful mode:
190
+ - default to `outline`
191
+ - use `roadmap` for clearly execution-heavy plans
192
+ - use `architecture` for clearly system-design-heavy docs
193
+ - use `mindmap` only when the artifact is genuinely concise and branchy
144
194
 
145
195
  1. Verify or assume the input markdown is ready
146
196
  2. Run:
147
197
  ```bash
148
- bash scripts/render-and-share.sh [file]
198
+ bash scripts/render-and-share.sh --mode <chosen-mode> --url-only [file]
149
199
  ```
150
- 3. Read the returned JSON
151
- 4. Return the primary URL to the user (`url` when present, otherwise `viewerUrl`)
152
- 5. Briefly explain what was published
200
+ 3. Read the returned URL from stdout
201
+ 4. Return that URL to the user as the primary result
202
+ 5. Briefly explain what was published and why this mode was chosen
203
+
204
+ If you need more detail for debugging, you may run the helper without `--url-only` and inspect the returned JSON.
153
205
 
154
206
  If publishing fails because the share server is not configured, say so clearly and fall back to terminal rendering unless the user wants to stop and run `share-server-setup` first.
155
207
 
156
208
  **For shell usage** (not through assistant panels): terminal rendering can use ANSI colors, or `--horizontal` for the wide spatial layout.
157
209
 
210
+ ## Current HTML modes
211
+
212
+ - `outline` — safe default for dense or unknown structured docs
213
+ - `roadmap` — useful for plans and phased execution views
214
+ - `architecture` — useful for architecture docs and architect outputs
215
+ - `mindmap` — useful for concise branchy artifacts where it truly helps
216
+ - `mockup` — reserved for future product/UI concept views
217
+ - `explainer` — reserved for future stakeholder/narrative views
218
+
158
219
  ## Phase 2 — Offer Next Steps
159
220
 
160
221
  After rendering or sharing, briefly note:
161
222
  - for terminal mode: "Use `--depth N` to see more/less detail"
162
223
  - for terminal mode: "Use `--width N` to fit a different terminal size"
163
- - for shared HTML: return the viewer URL and say whether it is local-only or publicly reachable on the user's tailnet
224
+ - for shared HTML: return the final browser URL as the main result and say whether it is local-only or publicly reachable on the user's tailnet
164
225
  - if publishing failed due to missing share infrastructure: suggest `share-server-setup`
165
226
  - if the source was a brainstorm/plan/architecture doc, offer to continue the workflow (e.g., proceed to `/plan`, `/work`, or implementation)
166
227
 
@@ -3,30 +3,36 @@ set -euo pipefail
3
3
 
4
4
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
5
5
  RENDER_DIR="$SCRIPT_DIR/render-mindmap"
6
- RENDER_SCRIPT="$RENDER_DIR/index.js"
6
+ RENDER_SCRIPT="$SCRIPT_DIR/smart-render.js"
7
7
  INPUT_PATH=""
8
8
  SLUG=""
9
9
  TITLE=""
10
10
  ALIAS=""
11
+ MODE="auto"
11
12
  KEEP_HTML=0
13
+ URL_ONLY=0
12
14
  HTML_OUT=""
13
15
  TEMP_DIR=""
14
16
 
15
17
  usage() {
16
18
  cat <<'EOF'
17
- Usage: render-and-share.sh <markdown-file> [--slug STEM] [--title TITLE] [--alias ALIAS] [--html-out PATH] [--keep-html]
19
+ Usage: render-and-share.sh <markdown-file> [--mode MODE] [--slug STEM] [--title TITLE] [--alias ALIAS] [--html-out PATH] [--keep-html] [--url-only]
18
20
 
19
- Generate an HTML mind map from a markdown file, publish it via share-html, and print the publish JSON.
21
+ Generate a polished HTML visualization from a markdown file, publish it via share-html, and print the publish JSON.
22
+ Use --mode to force a renderer (mindmap, outline, roadmap, architecture, mockup, explainer).
23
+ Use --url-only to print only the final browser URL.
20
24
  EOF
21
25
  }
22
26
 
23
27
  while [[ $# -gt 0 ]]; do
24
28
  case "$1" in
29
+ --mode) MODE="$2"; shift 2 ;;
25
30
  --slug) SLUG="$2"; shift 2 ;;
26
31
  --title) TITLE="$2"; shift 2 ;;
27
32
  --alias) ALIAS="$2"; shift 2 ;;
28
33
  --html-out) HTML_OUT="$2"; shift 2 ;;
29
34
  --keep-html) KEEP_HTML=1; shift ;;
35
+ --url-only) URL_ONLY=1; shift ;;
30
36
  --help) usage; exit 0 ;;
31
37
  --*) echo "Unknown argument: $1" >&2; exit 1 ;;
32
38
  *)
@@ -72,7 +78,7 @@ cleanup() {
72
78
  }
73
79
  trap cleanup EXIT
74
80
 
75
- node "$RENDER_SCRIPT" --html "$HTML_OUT" "$INPUT_PATH" >/dev/null
81
+ node "$RENDER_SCRIPT" "$INPUT_PATH" --mode "$MODE" --out "$HTML_OUT" >/dev/null
76
82
 
77
83
  find_share_publish_script() {
78
84
  local candidates=(
@@ -104,4 +110,17 @@ if [[ -n "$SLUG" ]]; then CMD+=(--slug "$SLUG"); fi
104
110
  if [[ -n "$TITLE" ]]; then CMD+=(--title "$TITLE"); fi
105
111
  if [[ -n "$ALIAS" ]]; then CMD+=(--alias "$ALIAS"); fi
106
112
 
107
- "${CMD[@]}"
113
+ PUBLISH_JSON="$("${CMD[@]}")"
114
+
115
+ if [[ "$URL_ONLY" -eq 1 ]]; then
116
+ python3 - <<'PY' "$PUBLISH_JSON"
117
+ import json, sys
118
+ payload = json.loads(sys.argv[1])
119
+ if not payload.get("ok", True):
120
+ print(json.dumps(payload))
121
+ raise SystemExit(1)
122
+ print(payload.get("url") or payload.get("viewerUrl") or "")
123
+ PY
124
+ else
125
+ printf '%s\n' "$PUBLISH_JSON"
126
+ fi
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync, writeFileSync } from 'node:fs';
4
+ import { basename, dirname, join } from 'node:path';
5
+ import { execFileSync } from 'node:child_process';
6
+
7
+ function parseArgs(args) {
8
+ const opts = { file: null, out: null, mode: 'auto' };
9
+ for (let i = 0; i < args.length; i++) {
10
+ const arg = args[i];
11
+ if (arg === '--out' && i + 1 < args.length) opts.out = args[++i];
12
+ else if (arg === '--mode' && i + 1 < args.length) opts.mode = args[++i];
13
+ else if (!arg.startsWith('-')) opts.file = arg;
14
+ }
15
+ return opts;
16
+ }
17
+
18
+ function parseDocument(markdown) {
19
+ const lines = markdown.split(/\r?\n/);
20
+ let i = 0;
21
+ const frontmatter = {};
22
+ if (lines[0]?.trim() === '---') {
23
+ i = 1;
24
+ for (; i < lines.length; i++) {
25
+ const line = lines[i].trim();
26
+ if (line === '---') { i++; break; }
27
+ const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
28
+ if (match) frontmatter[match[1]] = match[2].replace(/^"|"$/g, '');
29
+ }
30
+ }
31
+
32
+ const sections = [];
33
+ let current = null;
34
+ for (; i < lines.length; i++) {
35
+ const line = lines[i];
36
+ const heading = line.match(/^(#{1,6})\s+(.+)$/);
37
+ if (heading) {
38
+ current = { level: heading[1].length, title: heading[2].trim(), lines: [] };
39
+ sections.push(current);
40
+ } else if (current) {
41
+ current.lines.push(line);
42
+ }
43
+ }
44
+ return { frontmatter, sections, raw: markdown };
45
+ }
46
+
47
+ function collectChecklists(lines) {
48
+ return lines
49
+ .map((line) => line.match(/^\s*- \[( |x)\]\s+(.+)$/i))
50
+ .filter(Boolean)
51
+ .map((m) => ({ done: m[1].toLowerCase() === 'x', text: m[2].trim() }));
52
+ }
53
+
54
+ function textFromLines(lines) {
55
+ return lines.join('\n').trim();
56
+ }
57
+
58
+ function firstParagraph(lines) {
59
+ const blocks = textFromLines(lines).split(/\n\s*\n/).map((b) => b.trim()).filter(Boolean);
60
+ return blocks[0] || '';
61
+ }
62
+
63
+ function classifyMode(_filePath, _doc, preferredMode) {
64
+ if (preferredMode && preferredMode !== 'auto') return preferredMode;
65
+ return 'outline';
66
+ }
67
+
68
+ function escapeHtml(value) {
69
+ return String(value)
70
+ .replace(/&/g, '&amp;')
71
+ .replace(/</g, '&lt;')
72
+ .replace(/>/g, '&gt;')
73
+ .replace(/"/g, '&quot;')
74
+ .replace(/'/g, '&#39;');
75
+ }
76
+
77
+ function renderShell({ title, eyebrow, summary, toc, content, badge = '' }) {
78
+ return `<!doctype html>
79
+ <html lang="en">
80
+ <head>
81
+ <meta charset="utf-8" />
82
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
83
+ <title>${escapeHtml(title)}</title>
84
+ <style>
85
+ :root {
86
+ --bg: #0b1020;
87
+ --panel: rgba(255,255,255,0.06);
88
+ --panel-strong: rgba(255,255,255,0.1);
89
+ --text: #eef2ff;
90
+ --muted: #b8c0d9;
91
+ --border: rgba(255,255,255,0.12);
92
+ --accent: #7c9cff;
93
+ --accent-2: #80e0d0;
94
+ --success: #86efac;
95
+ --warn: #fbbf24;
96
+ --danger: #fca5a5;
97
+ --shadow: 0 20px 60px rgba(0,0,0,0.35);
98
+ }
99
+ * { box-sizing: border-box; }
100
+ html, body { margin: 0; padding: 0; background: radial-gradient(circle at top, #16213d 0%, var(--bg) 48%); color: var(--text); font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
101
+ body { line-height: 1.55; }
102
+ main { max-width: 1440px; margin: 0 auto; padding: 40px 24px 72px; }
103
+ .hero { display: grid; gap: 16px; margin-bottom: 28px; }
104
+ .eyebrow { color: var(--accent-2); text-transform: uppercase; letter-spacing: 0.14em; font-size: 12px; font-weight: 700; }
105
+ .hero h1 { margin: 0; font-size: clamp(32px, 4vw, 56px); line-height: 1.03; }
106
+ .hero p { margin: 0; max-width: 78ch; color: var(--muted); font-size: 17px; }
107
+ .badge { display: inline-flex; align-items: center; gap: 8px; width: fit-content; padding: 8px 12px; border-radius: 999px; background: rgba(124,156,255,0.18); border: 1px solid rgba(124,156,255,0.25); color: #dce5ff; font-size: 13px; }
108
+ .layout { display: grid; grid-template-columns: minmax(220px, 300px) minmax(0, 1fr); gap: 24px; }
109
+ nav { position: sticky; top: 16px; align-self: start; padding: 18px; border-radius: 20px; background: var(--panel); border: 1px solid var(--border); box-shadow: var(--shadow); backdrop-filter: blur(16px); }
110
+ nav h2 { margin: 0 0 10px; font-size: 14px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.1em; }
111
+ nav a { display: block; padding: 10px 12px; margin: 4px 0; color: var(--text); text-decoration: none; border-radius: 12px; }
112
+ nav a:hover { background: rgba(255,255,255,0.05); }
113
+ .section { margin-bottom: 18px; padding: 24px; border-radius: 24px; background: var(--panel); border: 1px solid var(--border); box-shadow: var(--shadow); backdrop-filter: blur(16px); }
114
+ .section h2 { margin: 0 0 14px; font-size: clamp(22px, 2vw, 30px); }
115
+ .section h3 { margin: 20px 0 8px; font-size: 18px; }
116
+ .section p, .section li, .section td, .section th { color: var(--muted); font-size: 15px; }
117
+ .section ul { padding-left: 18px; }
118
+ .grid { display: grid; gap: 16px; }
119
+ .grid.cols-2 { grid-template-columns: repeat(2, minmax(0,1fr)); }
120
+ .card { padding: 18px; border-radius: 18px; background: var(--panel-strong); border: 1px solid var(--border); }
121
+ .card h3, .card h4 { margin-top: 0; }
122
+ .kicker { font-size: 12px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--accent-2); margin-bottom: 8px; }
123
+ .checklist { display: grid; gap: 10px; }
124
+ .item { padding: 14px 16px; border-radius: 16px; background: rgba(255,255,255,0.04); border: 1px solid var(--border); }
125
+ .item.done { border-color: rgba(134,239,172,0.35); }
126
+ .item .status { font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 6px; }
127
+ .item.done .status { color: var(--success); }
128
+ .item.todo .status { color: var(--warn); }
129
+ .meta-grid { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }
130
+ .meta { padding: 14px 16px; border-radius: 16px; background: rgba(255,255,255,0.04); border: 1px solid var(--border); }
131
+ .meta .label { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.08em; }
132
+ .meta .value { margin-top: 8px; font-weight: 600; font-size: 15px; color: var(--text); }
133
+ code, pre { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
134
+ pre { white-space: pre-wrap; background: rgba(0,0,0,0.24); padding: 16px; border-radius: 16px; border: 1px solid var(--border); }
135
+ @media (max-width: 980px) { .layout { grid-template-columns: 1fr; } nav { position: static; } .grid.cols-2 { grid-template-columns: 1fr; } }
136
+ </style>
137
+ </head>
138
+ <body>
139
+ <main>
140
+ <section class="hero">
141
+ <div class="eyebrow">${escapeHtml(eyebrow)}</div>
142
+ <h1>${escapeHtml(title)}</h1>
143
+ <p>${escapeHtml(summary || 'Shareable HTML artifact generated by Heart of Gold Visualize.')}</p>
144
+ ${badge ? `<div class="badge">${escapeHtml(badge)}</div>` : ''}
145
+ </section>
146
+ <div class="layout">
147
+ <nav>
148
+ <h2>Contents</h2>
149
+ ${toc}
150
+ </nav>
151
+ <section>${content}</section>
152
+ </div>
153
+ </main>
154
+ </body>
155
+ </html>`;
156
+ }
157
+
158
+ function renderOutline(doc, title, eyebrow, badge) {
159
+ const toc = doc.sections.map((s, i) => `<a href="#section-${i}">${escapeHtml(s.title)}</a>`).join('');
160
+ const content = doc.sections.map((section, i) => {
161
+ const body = textFromLines(section.lines);
162
+ const paragraphs = body.split(/\n\s*\n/).map((b) => b.trim()).filter(Boolean).map((b) => {
163
+ if (b.includes('\n- ') || b.match(/^-/m)) {
164
+ const items = b.split(/\n/).map((line) => line.match(/^\s*[-*]\s+(.+)$/)?.[1]).filter(Boolean);
165
+ if (items.length) return `<ul>${items.map((item) => `<li>${escapeHtml(item)}</li>`).join('')}</ul>`;
166
+ }
167
+ return `<p>${escapeHtml(b)}</p>`;
168
+ }).join('');
169
+ return `<article id="section-${i}" class="section"><h2>${escapeHtml(section.title)}</h2>${paragraphs || '<p>No details captured.</p>'}</article>`;
170
+ }).join('');
171
+ return renderShell({ title, eyebrow, summary: firstParagraph(doc.sections[0]?.lines || []), toc, content, badge });
172
+ }
173
+
174
+ function renderRoadmap(doc, title, badge) {
175
+ const toc = ['Summary', ...doc.sections.map((s) => s.title)].map((s, i) => `<a href="#section-${i}">${escapeHtml(s)}</a>`).join('');
176
+ const tasksSection = doc.sections.find((s) => /implementation tasks/i.test(s.title));
177
+ const tasks = tasksSection ? collectChecklists(tasksSection.lines) : [];
178
+ const meta = [
179
+ ['Status', doc.frontmatter.status || 'unknown'],
180
+ ['Confidence', doc.frontmatter.confidence || 'n/a'],
181
+ ['Date', doc.frontmatter.date || 'n/a'],
182
+ ['Type', doc.frontmatter.type || 'plan'],
183
+ ];
184
+ let content = `<article id="section-0" class="section"><h2>Summary</h2><div class="meta-grid">${meta.map(([l,v]) => `<div class="meta"><div class="label">${escapeHtml(l)}</div><div class="value">${escapeHtml(v)}</div></div>`).join('')}</div></article>`;
185
+ if (tasks.length) {
186
+ content += `<article id="section-1" class="section"><h2>Implementation Tasks</h2><div class="checklist">${tasks.map((t) => `<div class="item ${t.done ? 'done' : 'todo'}"><div class="status">${t.done ? 'Done' : 'Planned'}</div><div>${escapeHtml(t.text)}</div></div>`).join('')}</div></article>`;
187
+ }
188
+ const remaining = doc.sections.filter((s) => !/implementation tasks/i.test(s.title));
189
+ content += remaining.map((section, idx) => {
190
+ const body = firstParagraph(section.lines);
191
+ return `<article id="section-${idx + 2}" class="section"><div class="kicker">Plan Section</div><h2>${escapeHtml(section.title)}</h2><p>${escapeHtml(body || 'See source markdown for full details.')}</p></article>`;
192
+ }).join('');
193
+ return renderShell({ title, eyebrow: 'Plan Dashboard', summary: firstParagraph(doc.sections[0]?.lines || []), toc, content, badge });
194
+ }
195
+
196
+ function renderArchitecture(doc, title, badge) {
197
+ const toc = doc.sections.map((s, i) => `<a href="#section-${i}">${escapeHtml(s.title)}</a>`).join('');
198
+ const cards = doc.sections.slice(0, 4).map((section) => `<div class="card"><div class="kicker">${escapeHtml(section.title)}</div><p>${escapeHtml(firstParagraph(section.lines) || 'See source markdown for detail.')}</p></div>`).join('');
199
+ const content = `<article class="section"><h2>Architecture Overview</h2><div class="grid cols-2">${cards}</div></article>` + doc.sections.map((section, i) => `<article id="section-${i}" class="section"><h2>${escapeHtml(section.title)}</h2><p>${escapeHtml(firstParagraph(section.lines) || 'See source markdown for detail.')}</p></article>`).join('');
200
+ return renderShell({ title, eyebrow: 'Architecture View', summary: firstParagraph(doc.sections[0]?.lines || []), toc, content, badge });
201
+ }
202
+
203
+ function renderMindmap(filePath, outFile) {
204
+ const script = join(dirname(new URL(import.meta.url).pathname), 'render-mindmap', 'index.js');
205
+ execFileSync('node', [script, '--html', outFile, filePath], { stdio: 'ignore' });
206
+ }
207
+
208
+ function main() {
209
+ const opts = parseArgs(process.argv.slice(2));
210
+ if (!opts.file || !opts.out) {
211
+ console.error('Usage: smart-render.js <file.md> --out <file.html> [--mode auto|mindmap|outline|roadmap|architecture|mockup|explainer]');
212
+ process.exit(1);
213
+ }
214
+ const markdown = readFileSync(opts.file, 'utf8');
215
+ const doc = parseDocument(markdown);
216
+ const mode = classifyMode(opts.file, doc, opts.mode);
217
+ const title = doc.frontmatter.title || doc.sections[0]?.title || basename(opts.file);
218
+ const badge = opts.mode && opts.mode !== 'auto'
219
+ ? `Mode: ${mode}`
220
+ : 'Mode: outline (safe default — agent may override)';
221
+
222
+ if (mode === 'mindmap') {
223
+ renderMindmap(opts.file, opts.out);
224
+ return;
225
+ }
226
+
227
+ const html = mode === 'roadmap'
228
+ ? renderRoadmap(doc, title, badge)
229
+ : mode === 'architecture'
230
+ ? renderArchitecture(doc, title, badge)
231
+ : renderOutline(doc, title, 'Structured View', badge);
232
+ writeFileSync(opts.out, html, 'utf8');
233
+ }
234
+
235
+ main();
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deep-thought",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "The Answer Computer — reasoning tools for brainstorming, planning, architecture design, and deep thinking",
5
5
  "author": {
6
6
  "name": "ondrej-svec",
@@ -176,12 +176,12 @@ Use **AskUserQuestion** with:
176
176
  - header: "Next step"
177
177
  - options:
178
178
  1. label: "Start implementation (Recommended)", description: "Proceed to scaffold or test writing"
179
- 2. label: "Visualize / Share", description: "Prefer a shareable HTML view when sharing is configured; otherwise render structure in the terminal"
179
+ 2. label: "Visualize / Share", description: "Generate a shareable HTML view first when sharing is configured; otherwise render structure in the terminal"
180
180
  3. label: "Review and refine", description: "Adjust stories or architecture based on feedback"
181
181
  4. label: "Done for now", description: "Return later"
182
182
  - multiSelect: false
183
183
 
184
- **If user selects "Visualize / Share":** Run `/babel-fish:visualize {stories_path}/{slug}.architecture.md` and prefer the shareable HTML flow when browser viewing or sharing is useful and `share-html` is configured. Otherwise render the terminal mind map. After rendering or sharing, return to this handoff with the remaining options.
184
+ **If user selects "Visualize / Share":** Run `/babel-fish:visualize {stories_path}/{slug}.architecture.md` and try the shareable HTML flow first when `share-html` is configured. Otherwise render the terminal mind map. After rendering or sharing, return to this handoff with the remaining options.
185
185
 
186
186
  **If pipeline mode (`$BRAINSTORM_PATH` set):**
187
187
  Complete without asking. Output paths for downstream consumers:
@@ -291,13 +291,13 @@ Ask the user what to do next.
291
291
  - Prefer the harness's structured choice UI if available
292
292
  - Otherwise present this short plain-text choice list:
293
293
  1. **Proceed to /plan** — Turn these decisions into an implementation plan
294
- 2. **Visualize / Share** — Prefer a shareable HTML mind map when sharing is configured; otherwise render in the terminal
294
+ 2. **Visualize / Share** — Generate a shareable HTML mind map first when sharing is configured; otherwise render in the terminal
295
295
  3. **Keep exploring** — More questions or refine decisions before moving on
296
296
  4. **Done for now** — Return later; to plan: `/plan {brainstorm-path}`
297
297
 
298
298
  **If user selects "Proceed to /plan":** Suggest running `/plan {brainstorm-path}`.
299
299
 
300
- **If user selects "Visualize / Share":** Run `/babel-fish:visualize {brainstorm-path}` and prefer the shareable HTML flow when browser viewing or sharing is useful and `share-html` is configured. Otherwise render the terminal mind map. After rendering or sharing, return to this handoff with the remaining options.
300
+ **If user selects "Visualize / Share":** Run `/babel-fish:visualize {brainstorm-path}` and try the shareable HTML flow first when `share-html` is configured. Otherwise render the terminal mind map. After rendering or sharing, return to this handoff with the remaining options.
301
301
 
302
302
  **If user selects "Keep exploring":** Return to Phase 3 and continue asking questions one at a time. When satisfied, update the document and return to this handoff.
303
303
 
@@ -283,13 +283,13 @@ Ask the user what to do next.
283
283
  - Prefer the harness's structured choice UI if available
284
284
  - Otherwise present this short plain-text choice list:
285
285
  1. **Start /work (Recommended)** — Begin implementing this plan
286
- 2. **Visualize / Share** — Prefer a shareable HTML mind map when sharing is configured; otherwise render in the terminal
286
+ 2. **Visualize / Share** — Generate a shareable HTML mind map first when sharing is configured; otherwise render in the terminal
287
287
  3. **Review and refine** — Adjust the plan based on feedback
288
288
  4. **Done for now** — Return later; to start: `/work {plan-path}`
289
289
 
290
290
  **If user selects "Start /work":** Suggest running `/work {plan-path}`.
291
291
 
292
- **If user selects "Visualize / Share":** Run `/babel-fish:visualize {plan-path}` and prefer the shareable HTML flow when browser viewing or sharing is useful and `share-html` is configured. Otherwise render the terminal mind map. After rendering or sharing, return to this handoff with the remaining options.
292
+ **If user selects "Visualize / Share":** Run `/babel-fish:visualize {plan-path}` and try the shareable HTML flow first when `share-html` is configured. Otherwise render the terminal mind map. After rendering or sharing, return to this handoff with the remaining options.
293
293
 
294
294
  **If user selects "Review and refine":** Accept feedback, update the plan, then present these options again.
295
295
 
@@ -82,15 +82,11 @@ print(fetch_body)
82
82
  PY
83
83
  }
84
84
 
85
- GMAIL_SETTINGS_RAW=$(read_gmail_settings "$CONFIG_PATH" 2>/dev/null) || GMAIL_SETTINGS_RAW=""
86
- if [[ -n "$GMAIL_SETTINGS_RAW" ]]; then
87
- GMAIL_LABEL=$(echo "$GMAIL_SETTINGS_RAW" | sed -n '1p')
88
- MAX_ITEMS=$(echo "$GMAIL_SETTINGS_RAW" | sed -n '2p')
89
- GMAIL_LABEL_ID=$(echo "$GMAIL_SETTINGS_RAW" | sed -n '3p')
90
- FETCH_BODY=$(echo "$GMAIL_SETTINGS_RAW" | sed -n '4p')
91
- GMAIL_LABEL="${GMAIL_LABEL:-Content-Feed}"
92
- MAX_ITEMS="${MAX_ITEMS:-20}"
93
- FETCH_BODY="${FETCH_BODY:-false}"
85
+ if mapfile -t GMAIL_SETTINGS < <(read_gmail_settings "$CONFIG_PATH" 2>/dev/null); then
86
+ GMAIL_LABEL="${GMAIL_SETTINGS[0]:-Content-Feed}"
87
+ MAX_ITEMS="${GMAIL_SETTINGS[1]:-20}"
88
+ GMAIL_LABEL_ID="${GMAIL_SETTINGS[2]:-}"
89
+ FETCH_BODY="${GMAIL_SETTINGS[3]:-false}"
94
90
  else
95
91
  GMAIL_LABEL="Content-Feed"
96
92
  MAX_ITEMS="20"
@@ -98,61 +94,14 @@ else
98
94
  FETCH_BODY="false"
99
95
  fi
100
96
 
101
- # Collect seen Gmail message IDs from previous days to avoid duplicates
102
- PIPELINE_DIR=$(python3 -c "
103
- import yaml, sys
104
- with open(sys.argv[1]) as f:
105
- c = yaml.safe_load(f)
106
- print(c.get('output', {}).get('pipeline_dir', 'content/pipeline'))
107
- " "$CONFIG_PATH" 2>/dev/null || echo "content/pipeline")
108
-
109
- SEEN_IDS=$(python3 -c "
110
- import json, glob, os, sys
111
-
112
- pipeline_dir = sys.argv[1]
113
- config_path = sys.argv[2]
114
-
115
- # Resolve relative pipeline_dir against project root (config's grandparent,
116
- # since config lives in content/ which is one level below project root)
117
- if not os.path.isabs(pipeline_dir):
118
- config_dir = os.path.dirname(os.path.abspath(config_path))
119
- # Walk up until we find .git or use config's parent's parent as fallback
120
- project_root = config_dir
121
- while project_root != '/':
122
- if os.path.isdir(os.path.join(project_root, '.git')):
123
- break
124
- project_root = os.path.dirname(project_root)
125
- pipeline_dir = os.path.join(project_root, pipeline_dir)
126
-
127
- seen = set()
128
- # Check last 7 days of signals files
129
- for path in sorted(glob.glob(os.path.join(pipeline_dir, '*/signals.json')))[-7:]:
130
- try:
131
- signals = json.load(open(path))
132
- for s in signals:
133
- if s.get('source') == 'gmail':
134
- mid = s.get('metadata', {}).get('message_id', '')
135
- if mid:
136
- seen.add(mid)
137
- except (json.JSONDecodeError, KeyError):
138
- pass
139
- # Output as comma-separated IDs
140
- print(','.join(seen))
141
- " "$PIPELINE_DIR" "$CONFIG_PATH" 2>/dev/null || echo "")
142
-
143
- if [[ -n "$SEEN_IDS" ]]; then
144
- SEEN_COUNT=$(echo "$SEEN_IDS" | tr ',' '\n' | wc -l | tr -d ' ')
145
- echo " · $SEEN_COUNT previously seen Gmail message IDs loaded" >&2
146
- fi
147
-
148
97
  # Fetch emails using gws +triage helper (more reliable than raw API calls —
149
98
  # the helper handles auth scopes internally, avoiding insufficientPermissions errors)
150
99
  # Fetch recent emails with labels, then filter by label ID in Python
151
100
  GWS_ERR=$(mktemp)
152
101
  trap "rm -f $GWS_ERR" EXIT
153
102
  EMAILS_JSON=$(gws gmail +triage \
154
- --query "newer_than:7d" \
155
- --max 200 \
103
+ --query "newer_than:3d" \
104
+ --max 50 \
156
105
  --format json \
157
106
  --labels \
158
107
  2>"$GWS_ERR") || {
@@ -173,21 +122,17 @@ EMAILS_JSON=$(gws gmail +triage \
173
122
  # Determine the scripts directory (where this script lives)
174
123
  SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
175
124
 
176
- export _GMAIL_LABEL_ID="$GMAIL_LABEL_ID"
177
- export _GMAIL_MAX_ITEMS="$MAX_ITEMS"
178
- export _GMAIL_CONFIG_PATH="$CONFIG_PATH"
179
- export _GMAIL_SEEN_IDS="$SEEN_IDS"
180
-
181
125
  if [[ "$FETCH_BODY" == "true" ]]; then
182
126
  # Deep mode: filter messages first, then pass to fetch-gmail-deep.py for
183
127
  # full body fetching, link extraction, and article following
184
- echo "$EMAILS_JSON" | python3 -c "
185
- import json, sys, yaml, os
128
+ echo "$EMAILS_JSON" | python3 - "$GMAIL_LABEL_ID" "$MAX_ITEMS" "$CONFIG_PATH" <<'PY' 2>/dev/null | python3 "$SCRIPTS_DIR/fetch-gmail-deep.py" || {
129
+ import json
130
+ import sys
131
+ import yaml
186
132
 
187
- label_id = os.environ['_GMAIL_LABEL_ID']
188
- max_items = int(os.environ['_GMAIL_MAX_ITEMS'])
189
- config_path = os.environ['_GMAIL_CONFIG_PATH']
190
- seen_ids = set(os.environ.get('_GMAIL_SEEN_IDS', '').split(',')) - {''}
133
+ label_id = sys.argv[1]
134
+ max_items = int(sys.argv[2])
135
+ config_path = sys.argv[3]
191
136
 
192
137
  data = json.load(sys.stdin)
193
138
  messages = data.get('messages', []) if isinstance(data, dict) else data
@@ -195,12 +140,6 @@ messages = data.get('messages', []) if isinstance(data, dict) else data
195
140
  if label_id:
196
141
  messages = [m for m in messages if label_id in m.get('labels', [])]
197
142
 
198
- before = len(messages)
199
- messages = [m for m in messages if m.get('id', '') not in seen_ids]
200
- skipped = before - len(messages)
201
- if skipped:
202
- print(f' Dedup: skipped {skipped} already-seen messages', file=sys.stderr)
203
-
204
143
  messages = messages[:max_items]
205
144
 
206
145
  with open(config_path) as f:
@@ -208,19 +147,19 @@ with open(config_path) as f:
208
147
  gmail_config = config.get('sources', {}).get('gmail', {}) or {}
209
148
 
210
149
  json.dump({'messages': messages, 'config': gmail_config}, sys.stdout)
211
- " | python3 "$SCRIPTS_DIR/fetch-gmail-deep.py" || {
150
+ PY
212
151
  echo "Error: Deep Gmail processing failed" >&2
213
152
  exit 1
214
153
  }
215
154
  else
216
155
  # Shallow mode: subject-only signals (original behavior)
217
- echo "$EMAILS_JSON" | python3 -c "
218
- import json, sys, os
156
+ echo "$EMAILS_JSON" | python3 - "$GMAIL_LABEL_ID" "$MAX_ITEMS" <<'PY' 2>/dev/null || {
157
+ import json
158
+ import sys
219
159
  from datetime import datetime, timezone
220
160
 
221
- label_id = os.environ.get('_GMAIL_LABEL_ID', '')
222
- max_items = int(os.environ.get('_GMAIL_MAX_ITEMS', '20'))
223
- seen_ids = set(os.environ.get('_GMAIL_SEEN_IDS', '').split(',')) - {''}
161
+ label_id = sys.argv[1]
162
+ max_items = int(sys.argv[2])
224
163
 
225
164
  data = json.load(sys.stdin)
226
165
  messages = data.get('messages', []) if isinstance(data, dict) else data
@@ -228,12 +167,6 @@ messages = data.get('messages', []) if isinstance(data, dict) else data
228
167
  if label_id:
229
168
  messages = [m for m in messages if label_id in m.get('labels', [])]
230
169
 
231
- before = len(messages)
232
- messages = [m for m in messages if m.get('id', '') not in seen_ids]
233
- skipped = before - len(messages)
234
- if skipped:
235
- print(f' Dedup: skipped {skipped} already-seen messages', file=sys.stderr)
236
-
237
170
  messages = messages[:max_items]
238
171
 
239
172
  signals = []
@@ -270,7 +203,7 @@ for email in messages:
270
203
  })
271
204
 
272
205
  json.dump(signals, sys.stdout, indent=2)
273
- " 2>/dev/null || {
206
+ PY
274
207
  echo "Error: Failed to parse gws output" >&2
275
208
  exit 1
276
209
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "marvin",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "The Paranoid Android — quality tools for code review, knowledge compounding, and work execution",
5
5
  "author": {
6
6
  "name": "ondrej-svec",