@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.
- package/.claude-plugin/marketplace.json +3 -3
- package/package.json +2 -1
- package/plugins/babel-fish/.claude-plugin/plugin.json +1 -1
- package/plugins/babel-fish/skills/visualize/SKILL.md +103 -42
- package/plugins/babel-fish/skills/visualize/scripts/render-and-share.sh +24 -5
- package/plugins/babel-fish/skills/visualize/scripts/smart-render.js +235 -0
- package/plugins/deep-thought/.claude-plugin/plugin.json +1 -1
- package/plugins/deep-thought/skills/architect/SKILL.md +2 -2
- package/plugins/deep-thought/skills/brainstorm/SKILL.md +2 -2
- package/plugins/deep-thought/skills/plan/SKILL.md +2 -2
- package/plugins/guide/scripts/fetch-gmail.sh +21 -88
- package/plugins/marvin/.claude-plugin/plugin.json +1 -1
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: visualize
|
|
3
3
|
description: >
|
|
4
|
-
Render mind maps and tree visualizations from markdown. Prefer
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
24
|
+
## The Renderers
|
|
25
25
|
|
|
26
|
-
|
|
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
|
-
**
|
|
30
|
+
**Locations:**
|
|
31
|
+
- `scripts/smart-render.js`
|
|
32
|
+
- `scripts/render-mindmap/index.js`
|
|
29
33
|
|
|
30
|
-
**To find the
|
|
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
|
|
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
|
|
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
|
-
#
|
|
51
|
-
node "$SCRIPT" path/to/file.md
|
|
52
|
-
|
|
53
|
-
#
|
|
54
|
-
node "$SCRIPT"
|
|
55
|
-
node "$SCRIPT"
|
|
56
|
-
node "$SCRIPT"
|
|
57
|
-
node "$SCRIPT"
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
74
|
-
2.
|
|
75
|
-
3.
|
|
76
|
-
4.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
151
|
-
4. Return
|
|
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
|
|
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="$
|
|
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
|
|
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" --
|
|
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, '&')
|
|
71
|
+
.replace(/</g, '<')
|
|
72
|
+
.replace(/>/g, '>')
|
|
73
|
+
.replace(/"/g, '"')
|
|
74
|
+
.replace(/'/g, ''');
|
|
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();
|
|
@@ -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: "
|
|
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
|
|
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** —
|
|
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
|
|
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** —
|
|
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
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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:
|
|
155
|
-
--max
|
|
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 -
|
|
185
|
-
import json
|
|
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 =
|
|
188
|
-
max_items = int(
|
|
189
|
-
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
|
-
|
|
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 -
|
|
218
|
-
import json
|
|
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 =
|
|
222
|
-
max_items = int(
|
|
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
|
-
|
|
206
|
+
PY
|
|
274
207
|
echo "Error: Failed to parse gws output" >&2
|
|
275
208
|
exit 1
|
|
276
209
|
}
|