@heart-of-gold/toolkit 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -0
- package/extensions/pi/brainstorm.ts +67 -0
- package/extensions/pi/plan.ts +94 -0
- package/extensions/pi/work.ts +103 -0
- package/package.json +24 -2
- package/plugins/babel-fish/README.md +6 -0
- package/plugins/babel-fish/skills/visualize/SKILL.md +12 -4
- package/plugins/babel-fish/skills/visualize/scripts/render-mindmap/index.js +12 -1
- package/plugins/babel-fish/skills/visualize/scripts/render-mindmap/vertical.js +178 -0
- package/plugins/deep-thought/skills/brainstorm/SKILL.md +21 -22
- package/plugins/deep-thought/skills/plan/SKILL.md +12 -12
- package/plugins/guide/README.md +6 -0
- package/plugins/guide/scripts/fetch-gmail.sh +51 -52
- package/plugins/marvin/skills/work/SKILL.md +25 -23
- package/scripts/check-harness-compatibility.py +47 -0
- package/scripts/check-publish-safety.py +97 -0
- package/scripts/check-security-regressions.py +91 -0
- package/src/utils/transform.ts +25 -1
package/README.md
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
bunx @heart-of-gold/toolkit install --to codex
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
+
The Codex target also applies Codex-specific wording transforms for flagship shared skills so interactive flows like `brainstorm` and `plan` more strongly encourage Codex's structured user-input UI instead of falling back to plain text when richer selection UX is available.
|
|
21
|
+
|
|
20
22
|
### OpenCode
|
|
21
23
|
```bash
|
|
22
24
|
bunx @heart-of-gold/toolkit install --to opencode
|
|
@@ -24,16 +26,38 @@ bunx @heart-of-gold/toolkit install --to opencode
|
|
|
24
26
|
|
|
25
27
|
### Pi Coding Agent
|
|
26
28
|
```bash
|
|
29
|
+
# Install shared skills into Pi's native skill directory
|
|
27
30
|
bunx @heart-of-gold/toolkit install --to pi
|
|
31
|
+
|
|
32
|
+
# Or install the package directly in Pi to get skills + pi-native extensions
|
|
33
|
+
pi install npm:@heart-of-gold/toolkit
|
|
28
34
|
```
|
|
29
35
|
|
|
30
36
|
Pi also discovers skills from the shared `~/.agents/skills/` location, so installs done with the OpenCode target are usable from Pi too.
|
|
31
37
|
|
|
38
|
+
When installed as a Pi package, Heart of Gold also exposes pi-native enhancement commands for flagship workflows:
|
|
39
|
+
- `/hog-brainstorm` — guided brainstorm intake for the shared `brainstorm` skill
|
|
40
|
+
- `/hog-plan` — planning mode entrypoint with pi-friendly tool defaults
|
|
41
|
+
- `/hog-work` — execution mode entrypoint with stronger work guardrails
|
|
42
|
+
|
|
43
|
+
For Pi, these flagship commands intentionally replace the direct shared-skill entries for `brainstorm`, `plan`, and `work` so the command palette stays clean and the Pi-native interactive flow is the default.
|
|
44
|
+
|
|
32
45
|
### List available skills
|
|
33
46
|
```bash
|
|
34
47
|
bunx @heart-of-gold/toolkit list
|
|
35
48
|
```
|
|
36
49
|
|
|
50
|
+
## Security & Trust
|
|
51
|
+
|
|
52
|
+
Some skills in this repository can, when configured by the user, interact with personal services and local tooling such as:
|
|
53
|
+
|
|
54
|
+
- Gmail and newsletter content
|
|
55
|
+
- iMessage or Slack notifications
|
|
56
|
+
- audio/image provider APIs
|
|
57
|
+
- external URLs discovered in newsletters or prompts
|
|
58
|
+
|
|
59
|
+
Treat skills and helper scripts as executable automation, not passive text. Review their contents before installing them, especially for plugins like **Guide** and **Babel Fish** that can access personal data or third-party services.
|
|
60
|
+
|
|
37
61
|
---
|
|
38
62
|
|
|
39
63
|
## The Five Plugins
|
|
@@ -125,6 +149,7 @@ The toolkit ships as an npm package with a CLI for installing skills into any su
|
|
|
125
149
|
|
|
126
150
|
- `--to pi` installs to Pi's native `~/.pi/agent/skills/`
|
|
127
151
|
- `--to opencode` installs to shared `~/.agents/skills/`, which Pi also discovers
|
|
152
|
+
- `pi install npm:@heart-of-gold/toolkit` installs the package directly in Pi, including the shared skills plus pi-native extensions
|
|
128
153
|
|
|
129
154
|
```bash
|
|
130
155
|
# Install all plugins into Codex
|
|
@@ -146,6 +171,20 @@ bunx @heart-of-gold/toolkit list deep-thought
|
|
|
146
171
|
bunx @heart-of-gold/toolkit targets
|
|
147
172
|
```
|
|
148
173
|
|
|
174
|
+
## Release Safety
|
|
175
|
+
|
|
176
|
+
Before publishing to npm, run the safety checks:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
npm run check:publish-safety
|
|
180
|
+
npm run check:security
|
|
181
|
+
npm run check:compat
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
- `check:publish-safety` verifies the `npm pack` file list and fails if the publish would include blocked files such as `.env` or obvious secrets such as private keys, Slack webhooks, GitHub tokens, npm tokens, or AWS access keys.
|
|
185
|
+
- `check:security` runs lightweight regression checks for sensitive Guide scripts and is also enforced in GitHub Actions.
|
|
186
|
+
- `check:compat` ensures the flagship shared skills (`brainstorm`, `plan`, `work`) keep their harness-neutral interaction contract rather than drifting toward pi-only or Claude-only assumptions.
|
|
187
|
+
|
|
149
188
|
## Requirements
|
|
150
189
|
|
|
151
190
|
- **Codex/OpenCode/Pi**: Bun runtime (for `bunx`)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
function sendPrompt(pi: ExtensionAPI, prompt: string, ctx: Parameters<NonNullable<Parameters<ExtensionAPI["registerCommand"]>[1]["handler"]>>[1]) {
|
|
4
|
+
if (ctx.isIdle()) {
|
|
5
|
+
pi.sendUserMessage(prompt);
|
|
6
|
+
} else {
|
|
7
|
+
pi.sendUserMessage(prompt, { deliverAs: "followUp" });
|
|
8
|
+
ctx.ui.notify("Brainstorm prompt queued as follow-up", "info");
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function brainstormExtension(pi: ExtensionAPI) {
|
|
13
|
+
pi.registerCommand("hog-brainstorm", {
|
|
14
|
+
description: "Interactive pi-first intake for the shared brainstorm skill",
|
|
15
|
+
handler: async (args, ctx) => {
|
|
16
|
+
if (!ctx.hasUI) {
|
|
17
|
+
ctx.ui.notify("/hog-brainstorm requires interactive mode", "warning");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const topic = args.trim() || (await ctx.ui.editor("Brainstorm topic", ""))?.trim();
|
|
22
|
+
if (!topic) {
|
|
23
|
+
ctx.ui.notify("Cancelled", "info");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const clarity = await ctx.ui.select("How clear are the requirements right now?", [
|
|
28
|
+
"Unclear — need discovery",
|
|
29
|
+
"Partly clear — explore tradeoffs",
|
|
30
|
+
"Quite clear — validate before planning",
|
|
31
|
+
]);
|
|
32
|
+
if (!clarity) {
|
|
33
|
+
ctx.ui.notify("Cancelled", "info");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const goal = await ctx.ui.select("What should the brainstorm optimize for first?", [
|
|
38
|
+
"Find the right problem framing",
|
|
39
|
+
"Compare 2-3 viable approaches",
|
|
40
|
+
"Pressure-test the chosen direction",
|
|
41
|
+
]);
|
|
42
|
+
if (!goal) {
|
|
43
|
+
ctx.ui.notify("Cancelled", "info");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const prompt = [
|
|
48
|
+
`/skill:brainstorm ${topic}`,
|
|
49
|
+
"",
|
|
50
|
+
"Use a pi-friendly interactive flow:",
|
|
51
|
+
"- Ask one question at a time.",
|
|
52
|
+
"- Use explicit option lists whenever there are natural choices.",
|
|
53
|
+
"- Keep momentum high and avoid dumping questionnaires.",
|
|
54
|
+
"",
|
|
55
|
+
`Current clarity: ${clarity}`,
|
|
56
|
+
`Primary goal: ${goal}`,
|
|
57
|
+
].join("\n");
|
|
58
|
+
|
|
59
|
+
const theme = ctx.ui.theme;
|
|
60
|
+
ctx.ui.setStatus(
|
|
61
|
+
"hog-brainstorm",
|
|
62
|
+
theme.fg("accent", "◉") + theme.fg("dim", " Brainstorm flow active"),
|
|
63
|
+
);
|
|
64
|
+
sendPrompt(pi, prompt, ctx);
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
const PLAN_SAFE_TOOLS = ["read", "bash", "grep", "find", "ls"];
|
|
4
|
+
|
|
5
|
+
export default function planExtension(pi: ExtensionAPI) {
|
|
6
|
+
let previousTools: string[] | null = null;
|
|
7
|
+
|
|
8
|
+
const applyPlanTools = () => {
|
|
9
|
+
const available = new Set(pi.getAllTools().map((tool) => tool.name));
|
|
10
|
+
const nextTools = PLAN_SAFE_TOOLS.filter((tool) => available.has(tool));
|
|
11
|
+
if (nextTools.length > 0) {
|
|
12
|
+
pi.setActiveTools(nextTools);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
pi.registerCommand("hog-plan", {
|
|
17
|
+
description: "Interactive pi-first entrypoint for planning mode and the shared plan skill",
|
|
18
|
+
handler: async (args, ctx) => {
|
|
19
|
+
if (!ctx.hasUI) {
|
|
20
|
+
ctx.ui.notify("/hog-plan requires interactive mode", "warning");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const source = args.trim() || (await ctx.ui.editor("Plan topic or brainstorm path", ""))?.trim();
|
|
25
|
+
if (!source) {
|
|
26
|
+
ctx.ui.notify("Cancelled", "info");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const mode = await ctx.ui.select("How should planning mode behave?", [
|
|
31
|
+
"Read-only planning (recommended)",
|
|
32
|
+
"Allow normal tools, but stay in planning mode",
|
|
33
|
+
]);
|
|
34
|
+
if (!mode) {
|
|
35
|
+
ctx.ui.notify("Cancelled", "info");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (previousTools === null) {
|
|
40
|
+
previousTools = pi.getActiveTools();
|
|
41
|
+
}
|
|
42
|
+
if (mode.startsWith("Read-only")) {
|
|
43
|
+
applyPlanTools();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const prompt = [
|
|
47
|
+
`/skill:plan ${source}`,
|
|
48
|
+
"",
|
|
49
|
+
"Use a pi-friendly planning flow:",
|
|
50
|
+
"- Keep planning read-only unless the user explicitly exits planning mode.",
|
|
51
|
+
"- Ask one question at a time when clarification is needed.",
|
|
52
|
+
"- Use concise option lists when choosing between paths or handoffs.",
|
|
53
|
+
].join("\n");
|
|
54
|
+
|
|
55
|
+
const theme = ctx.ui.theme;
|
|
56
|
+
ctx.ui.setStatus(
|
|
57
|
+
"hog-plan",
|
|
58
|
+
theme.fg("accent", "◉") + theme.fg("dim", " Plan mode active"),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
if (ctx.isIdle()) {
|
|
62
|
+
pi.sendUserMessage(prompt);
|
|
63
|
+
} else {
|
|
64
|
+
pi.sendUserMessage(prompt, { deliverAs: "followUp" });
|
|
65
|
+
ctx.ui.notify("Plan prompt queued as follow-up", "info");
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
pi.registerCommand("hog-execute", {
|
|
71
|
+
description: "Exit planning mode, restore tools, and optionally start work",
|
|
72
|
+
handler: async (args, ctx) => {
|
|
73
|
+
if (previousTools) {
|
|
74
|
+
pi.setActiveTools(previousTools);
|
|
75
|
+
previousTools = null;
|
|
76
|
+
}
|
|
77
|
+
ctx.ui.setStatus("hog-plan", "");
|
|
78
|
+
|
|
79
|
+
const target = args.trim();
|
|
80
|
+
if (!target) {
|
|
81
|
+
ctx.ui.notify("Planning mode cleared. Run /hog-work <plan-path> when ready.", "info");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const prompt = `/skill:work ${target}`;
|
|
86
|
+
if (ctx.isIdle()) {
|
|
87
|
+
pi.sendUserMessage(prompt);
|
|
88
|
+
} else {
|
|
89
|
+
pi.sendUserMessage(prompt, { deliverAs: "followUp" });
|
|
90
|
+
ctx.ui.notify("Work prompt queued as follow-up", "info");
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
const PROTECTED_PATHS = [".env", ".git/", "node_modules/"];
|
|
4
|
+
const CONFIRM_COMMANDS = [/\bgit\s+push\b/i, /\bnpm\s+publish\b/i, /\bbun\s+publish\b/i, /\bgh\s+pr\s+create\b/i];
|
|
5
|
+
const BLOCKED_COMMANDS = [/\bgit\s+add\s+\.\b/i, /\brm\s+(-rf?|--recursive)/i];
|
|
6
|
+
|
|
7
|
+
export default function workExtension(pi: ExtensionAPI) {
|
|
8
|
+
let workMode = false;
|
|
9
|
+
|
|
10
|
+
pi.registerCommand("hog-work", {
|
|
11
|
+
description: "Interactive pi-first entrypoint for the shared work skill",
|
|
12
|
+
handler: async (args, ctx) => {
|
|
13
|
+
if (!ctx.hasUI) {
|
|
14
|
+
ctx.ui.notify("/hog-work requires interactive mode", "warning");
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const planPath = args.trim() || (await ctx.ui.editor("Plan path", ""))?.trim();
|
|
19
|
+
if (!planPath) {
|
|
20
|
+
ctx.ui.notify("Cancelled", "info");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const runMode = await ctx.ui.select("How strict should work mode be?", [
|
|
25
|
+
"Normal guardrails (recommended)",
|
|
26
|
+
"Strict guardrails for shipping work",
|
|
27
|
+
]);
|
|
28
|
+
if (!runMode) {
|
|
29
|
+
ctx.ui.notify("Cancelled", "info");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
workMode = true;
|
|
34
|
+
const theme = ctx.ui.theme;
|
|
35
|
+
ctx.ui.setStatus(
|
|
36
|
+
"hog-work",
|
|
37
|
+
theme.fg("accent", "◉") + theme.fg("dim", ` Work mode active — ${runMode}`),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const prompt = [
|
|
41
|
+
`/skill:work ${planPath}`,
|
|
42
|
+
"",
|
|
43
|
+
"Use pi guardrails while executing:",
|
|
44
|
+
"- keep progress visible as tasks move from in-progress to complete",
|
|
45
|
+
"- do not use `git add .`",
|
|
46
|
+
"- confirm push/publish actions deliberately",
|
|
47
|
+
"- protect .env, .git/, and node_modules/ from accidental edits",
|
|
48
|
+
].join("\n");
|
|
49
|
+
|
|
50
|
+
if (ctx.isIdle()) {
|
|
51
|
+
pi.sendUserMessage(prompt);
|
|
52
|
+
} else {
|
|
53
|
+
pi.sendUserMessage(prompt, { deliverAs: "followUp" });
|
|
54
|
+
ctx.ui.notify("Work prompt queued as follow-up", "info");
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
pi.registerCommand("hog-work-off", {
|
|
60
|
+
description: "Disable Heart of Gold work-mode guardrails",
|
|
61
|
+
handler: async (_args, ctx) => {
|
|
62
|
+
workMode = false;
|
|
63
|
+
ctx.ui.setStatus("hog-work", "");
|
|
64
|
+
ctx.ui.notify("Work mode disabled", "info");
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
69
|
+
if (!workMode) return undefined;
|
|
70
|
+
|
|
71
|
+
if (event.toolName === "write" || event.toolName === "edit") {
|
|
72
|
+
const path = String(event.input.path ?? "");
|
|
73
|
+
if (PROTECTED_PATHS.some((segment) => path.includes(segment))) {
|
|
74
|
+
if (ctx.hasUI) {
|
|
75
|
+
ctx.ui.notify(`Blocked protected path in work mode: ${path}`, "warning");
|
|
76
|
+
}
|
|
77
|
+
return { block: true, reason: `Protected path in work mode: ${path}` };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (event.toolName !== "bash") return undefined;
|
|
82
|
+
const command = String(event.input.command ?? "");
|
|
83
|
+
|
|
84
|
+
if (BLOCKED_COMMANDS.some((pattern) => pattern.test(command))) {
|
|
85
|
+
return { block: true, reason: `Blocked unsafe command in work mode: ${command}` };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!CONFIRM_COMMANDS.some((pattern) => pattern.test(command))) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!ctx.hasUI) {
|
|
93
|
+
return { block: true, reason: `Interactive confirmation required for: ${command}` };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const choice = await ctx.ui.select(`Confirm work-mode command:\n\n${command}`, ["Allow", "Block"]);
|
|
97
|
+
if (choice !== "Allow") {
|
|
98
|
+
return { block: true, reason: `Blocked by user: ${command}` };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return undefined;
|
|
102
|
+
});
|
|
103
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heart-of-gold/toolkit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
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": {
|
|
7
7
|
"heart-of-gold": "src/index.ts"
|
|
8
8
|
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"check:publish-safety": "python3 scripts/check-publish-safety.py",
|
|
11
|
+
"check:security": "python3 scripts/check-security-regressions.py",
|
|
12
|
+
"check:compat": "python3 scripts/check-harness-compatibility.py",
|
|
13
|
+
"prepublishOnly": "npm run check:publish-safety && npm run check:compat"
|
|
14
|
+
},
|
|
9
15
|
"dependencies": {
|
|
10
16
|
"citty": "^0.1.6",
|
|
11
17
|
"js-yaml": "^4.1.0"
|
|
@@ -25,11 +31,27 @@
|
|
|
25
31
|
"claude-code",
|
|
26
32
|
"opencode",
|
|
27
33
|
"pi",
|
|
34
|
+
"pi-package",
|
|
28
35
|
"ai-agents",
|
|
29
36
|
"skills",
|
|
30
37
|
"agents-md",
|
|
31
38
|
"agentskills",
|
|
32
39
|
"harness-engineering",
|
|
33
40
|
"heart-of-gold"
|
|
34
|
-
]
|
|
41
|
+
],
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@mariozechner/pi-ai": "*",
|
|
44
|
+
"@mariozechner/pi-coding-agent": "*"
|
|
45
|
+
},
|
|
46
|
+
"pi": {
|
|
47
|
+
"extensions": [
|
|
48
|
+
"./extensions/pi"
|
|
49
|
+
],
|
|
50
|
+
"skills": [
|
|
51
|
+
"./plugins/*/skills",
|
|
52
|
+
"!./plugins/deep-thought/skills/brainstorm",
|
|
53
|
+
"!./plugins/deep-thought/skills/plan",
|
|
54
|
+
"!./plugins/marvin/skills/work"
|
|
55
|
+
]
|
|
56
|
+
}
|
|
35
57
|
}
|
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
|
|
6
6
|
A media generation plugin for Claude Code. Turns text into audio content and generates images from prompts.
|
|
7
7
|
|
|
8
|
+
## Security & Trust
|
|
9
|
+
|
|
10
|
+
Babel Fish uses third-party media APIs when you enable its workflows. Review the skill instructions and helper scripts before use, and keep provider credentials in environment variables or local secret files outside the repository.
|
|
11
|
+
|
|
12
|
+
Generated media may also be sent to external providers, so treat prompts and source material accordingly.
|
|
13
|
+
|
|
8
14
|
## Skills
|
|
9
15
|
|
|
10
16
|
### `/babel-fish:audio`
|
|
@@ -90,15 +90,23 @@ When invoked as `/visualize [path]`:
|
|
|
90
90
|
|
|
91
91
|
## Phase 1 — Render
|
|
92
92
|
|
|
93
|
+
**IMPORTANT: Output the mind map in Claude's response text, NOT as bash tool output.**
|
|
94
|
+
|
|
95
|
+
Claude Code's bash output panel truncates long output and wraps wide content, breaking alignment. Instead:
|
|
96
|
+
|
|
93
97
|
1. Locate the renderer script (see above)
|
|
94
98
|
2. Ensure dependencies are installed
|
|
95
|
-
3. Run the renderer
|
|
99
|
+
3. Run the renderer with `--no-color`, redirect to a temp file:
|
|
96
100
|
```bash
|
|
97
|
-
node "$SCRIPT"
|
|
101
|
+
node "$SCRIPT" --no-color [file] > /tmp/mindmap-result.txt 2>&1
|
|
98
102
|
```
|
|
99
|
-
4.
|
|
103
|
+
4. Read `/tmp/mindmap-result.txt`
|
|
104
|
+
5. Output the contents inside a markdown fenced code block in your response text
|
|
105
|
+
6. Clean up: `rm /tmp/mindmap-result.txt`
|
|
106
|
+
|
|
107
|
+
The default mode is **vertical layout** — boxes on main branches, compact leaves, ~40 chars wide. Fits perfectly in Claude Code's response area.
|
|
100
108
|
|
|
101
|
-
**
|
|
109
|
+
**For shell usage** (not through Claude Code): Run without `--no-color` for ANSI colors, or use `--horizontal` for the wide spatial layout.
|
|
102
110
|
|
|
103
111
|
## Phase 2 — Offer Next Steps
|
|
104
112
|
|
|
@@ -24,6 +24,7 @@ import { execSync } from 'child_process';
|
|
|
24
24
|
import { parseMarkdown, parseJSON } from './parse.js';
|
|
25
25
|
import { renderMindmap } from './render.js';
|
|
26
26
|
import { renderCompact } from './compact.js';
|
|
27
|
+
import { renderVertical } from './vertical.js';
|
|
27
28
|
|
|
28
29
|
function main() {
|
|
29
30
|
const args = process.argv.slice(2);
|
|
@@ -82,13 +83,20 @@ function main() {
|
|
|
82
83
|
if (opts.compact) {
|
|
83
84
|
const output = renderCompact(tree, { color: opts.color, maxDepth: opts.depth });
|
|
84
85
|
console.log(output);
|
|
85
|
-
} else {
|
|
86
|
+
} else if (opts.horizontal) {
|
|
86
87
|
const output = renderMindmap(tree, {
|
|
87
88
|
maxWidth: opts.width,
|
|
88
89
|
maxDepth: opts.depth,
|
|
89
90
|
color: opts.color,
|
|
90
91
|
});
|
|
91
92
|
console.log(output);
|
|
93
|
+
} else {
|
|
94
|
+
// Default: vertical layout (fits narrow panels like Claude Code)
|
|
95
|
+
const output = renderVertical(tree, {
|
|
96
|
+
color: opts.color,
|
|
97
|
+
maxDepth: opts.depth,
|
|
98
|
+
});
|
|
99
|
+
console.log(output);
|
|
92
100
|
}
|
|
93
101
|
}
|
|
94
102
|
|
|
@@ -109,6 +117,7 @@ function parseArgs(args) {
|
|
|
109
117
|
color: true,
|
|
110
118
|
json: false,
|
|
111
119
|
compact: false,
|
|
120
|
+
horizontal: false,
|
|
112
121
|
html: false,
|
|
113
122
|
htmlOutput: null,
|
|
114
123
|
};
|
|
@@ -125,6 +134,8 @@ function parseArgs(args) {
|
|
|
125
134
|
opts.json = true;
|
|
126
135
|
} else if (arg === '--compact') {
|
|
127
136
|
opts.compact = true;
|
|
137
|
+
} else if (arg === '--horizontal') {
|
|
138
|
+
opts.horizontal = true;
|
|
128
139
|
} else if (arg === '--html') {
|
|
129
140
|
opts.html = true;
|
|
130
141
|
// Next arg might be output path (if it doesn't start with -)
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vertical.js — Vertical mind map renderer
|
|
3
|
+
*
|
|
4
|
+
* Renders a tree vertically (top-to-bottom flow, left-aligned).
|
|
5
|
+
* Root and depth-1 nodes get Unicode boxes. Deeper nodes use compact ├──/└── notation.
|
|
6
|
+
* Optimized for narrow output areas like Claude Code's response text.
|
|
7
|
+
*
|
|
8
|
+
* Width: ~40-60 chars. Height: unlimited (uses vertical space).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
|
|
13
|
+
const BOX = {
|
|
14
|
+
topLeft: '╭', top: '─', topRight: '╮',
|
|
15
|
+
left: '│', right: '│',
|
|
16
|
+
bottomLeft: '╰', bottom: '─', bottomRight: '╯',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const DEPTH_COLORS = [
|
|
20
|
+
(t) => chalk.bold.cyan(t), // root box border
|
|
21
|
+
(t) => chalk.green(t), // depth 1 box border
|
|
22
|
+
(t) => chalk.yellow(t), // depth 2 leaves
|
|
23
|
+
(t) => chalk.dim(t), // depth 3+
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const LABEL_COLORS = [
|
|
27
|
+
(t) => chalk.bold.white(t), // root label
|
|
28
|
+
(t) => chalk.bold.cyan(t), // depth 1 label
|
|
29
|
+
(t) => chalk.green(t), // depth 2 label
|
|
30
|
+
(t) => chalk.yellow(t), // depth 3+ label
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
function colorBox(char, depth, useColor) {
|
|
34
|
+
if (!useColor) return char;
|
|
35
|
+
const fn = DEPTH_COLORS[Math.min(depth, DEPTH_COLORS.length - 1)];
|
|
36
|
+
return fn(char);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function colorLabel(text, depth, useColor) {
|
|
40
|
+
if (!useColor) return text;
|
|
41
|
+
const fn = LABEL_COLORS[Math.min(depth, LABEL_COLORS.length - 1)];
|
|
42
|
+
return fn(text);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function colorTree(char, depth, useColor) {
|
|
46
|
+
if (!useColor) return char;
|
|
47
|
+
const fn = DEPTH_COLORS[Math.min(depth, DEPTH_COLORS.length - 1)];
|
|
48
|
+
return fn(char);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Render a box around text.
|
|
53
|
+
* Returns array of lines.
|
|
54
|
+
*/
|
|
55
|
+
function renderBox(label, indent, depth, useColor) {
|
|
56
|
+
const pad = 2;
|
|
57
|
+
const innerWidth = label.length + pad * 2;
|
|
58
|
+
const topLine = colorBox(BOX.topLeft + BOX.top.repeat(innerWidth) + BOX.topRight, depth, useColor);
|
|
59
|
+
const midLine = colorBox(BOX.left, depth, useColor)
|
|
60
|
+
+ ' '.repeat(pad) + colorLabel(label, depth, useColor) + ' '.repeat(pad)
|
|
61
|
+
+ colorBox(BOX.right, depth, useColor);
|
|
62
|
+
const botLine = colorBox(BOX.bottomLeft + BOX.bottom.repeat(innerWidth) + BOX.bottomRight, depth, useColor);
|
|
63
|
+
return [
|
|
64
|
+
indent + topLine,
|
|
65
|
+
indent + midLine,
|
|
66
|
+
indent + botLine,
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Render the vertical mind map.
|
|
72
|
+
*
|
|
73
|
+
* @param {{ label: string, children: any[] }} tree
|
|
74
|
+
* @param {Object} [opts]
|
|
75
|
+
* @param {boolean} [opts.color=true]
|
|
76
|
+
* @param {number} [opts.maxDepth=Infinity]
|
|
77
|
+
* @param {number} [opts.boxDepth=1] — max depth for boxed nodes (deeper = compact)
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
80
|
+
export function renderVertical(tree, opts = {}) {
|
|
81
|
+
const useColor = opts.color !== false;
|
|
82
|
+
const maxDepth = opts.maxDepth ?? Infinity;
|
|
83
|
+
const boxDepth = opts.boxDepth ?? 1;
|
|
84
|
+
const lines = [];
|
|
85
|
+
|
|
86
|
+
renderNode(lines, tree, '', true, 0, maxDepth, boxDepth, useColor, true);
|
|
87
|
+
return lines.join('\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function renderNode(lines, node, prefix, isLast, depth, maxDepth, boxDepth, useColor, isRoot) {
|
|
91
|
+
const label = node.label || '';
|
|
92
|
+
const children = node.children || [];
|
|
93
|
+
|
|
94
|
+
if (isRoot) {
|
|
95
|
+
// Root: centered box, no prefix
|
|
96
|
+
const boxLines = renderBox(label, '', depth, useColor);
|
|
97
|
+
lines.push(...boxLines);
|
|
98
|
+
|
|
99
|
+
if (children.length > 0) {
|
|
100
|
+
const connector = colorTree(' │', depth, useColor);
|
|
101
|
+
lines.push(connector);
|
|
102
|
+
}
|
|
103
|
+
} else if (depth <= boxDepth) {
|
|
104
|
+
// Boxed node: prefix + connector + box
|
|
105
|
+
const branch = isLast ? '└─' : '├─';
|
|
106
|
+
const cont = isLast ? ' ' : '│ ';
|
|
107
|
+
|
|
108
|
+
// Box with branch connector
|
|
109
|
+
const boxLines = renderBox(label, '', depth, useColor);
|
|
110
|
+
const branchChar = colorTree(prefix + branch, depth - 1, useColor);
|
|
111
|
+
lines.push(branchChar + boxLines[0]);
|
|
112
|
+
lines.push(colorTree(prefix + cont, depth - 1, useColor) + boxLines[1]);
|
|
113
|
+
lines.push(colorTree(prefix + cont, depth - 1, useColor) + boxLines[2]);
|
|
114
|
+
|
|
115
|
+
if (children.length > 0 && depth < maxDepth) {
|
|
116
|
+
lines.push(colorTree(prefix + cont + ' │', depth, useColor));
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
// Compact leaf: prefix + ├── label
|
|
120
|
+
const branch = isLast ? '└── ' : '├── ';
|
|
121
|
+
lines.push(colorTree(prefix + branch, depth - 1, useColor) + colorLabel(label, depth, useColor));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Render children
|
|
125
|
+
if (depth >= maxDepth) {
|
|
126
|
+
if (children.length > 0) {
|
|
127
|
+
const cont = isRoot ? ' ' : (isLast ? ' ' : '│ ');
|
|
128
|
+
const childPrefix = isRoot ? ' ' : prefix + cont;
|
|
129
|
+
const hint = `+${children.length} more`;
|
|
130
|
+
lines.push(colorTree(childPrefix + '└── ', depth, useColor) + colorLabel(hint, depth + 1, useColor));
|
|
131
|
+
}
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const maxChildren = depth === 0 ? 8 : (depth <= boxDepth ? 6 : 5);
|
|
136
|
+
let visibleChildren = children;
|
|
137
|
+
let overflow = 0;
|
|
138
|
+
|
|
139
|
+
if (children.length > maxChildren) {
|
|
140
|
+
overflow = children.length - maxChildren + 1;
|
|
141
|
+
visibleChildren = children.slice(0, maxChildren - 1);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (let i = 0; i < visibleChildren.length; i++) {
|
|
145
|
+
const child = visibleChildren[i];
|
|
146
|
+
const childIsLast = (i === visibleChildren.length - 1) && overflow === 0;
|
|
147
|
+
|
|
148
|
+
let childPrefix;
|
|
149
|
+
if (isRoot) {
|
|
150
|
+
childPrefix = ' ';
|
|
151
|
+
} else if (depth <= boxDepth) {
|
|
152
|
+
childPrefix = prefix + (isLast ? ' ' : '│ ') + ' ';
|
|
153
|
+
} else {
|
|
154
|
+
childPrefix = prefix + (isLast ? ' ' : '│ ');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
renderNode(lines, child, childPrefix, childIsLast, depth + 1, maxDepth, boxDepth, useColor, false);
|
|
158
|
+
|
|
159
|
+
// Add spacing between boxed siblings (not after last)
|
|
160
|
+
if (depth + 1 <= boxDepth && !childIsLast) {
|
|
161
|
+
const cont = isRoot ? ' │' : prefix + (isLast ? ' ' : '│ ') + ' │';
|
|
162
|
+
lines.push(colorTree(cont, depth, useColor));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (overflow > 0) {
|
|
167
|
+
let childPrefix;
|
|
168
|
+
if (isRoot) {
|
|
169
|
+
childPrefix = ' ';
|
|
170
|
+
} else if (depth <= boxDepth) {
|
|
171
|
+
childPrefix = prefix + (isLast ? ' ' : '│ ') + ' ';
|
|
172
|
+
} else {
|
|
173
|
+
childPrefix = prefix + (isLast ? ' ' : '│ ');
|
|
174
|
+
}
|
|
175
|
+
const hint = `+${overflow} more`;
|
|
176
|
+
lines.push(colorTree(childPrefix + '└── ', depth, useColor) + colorLabel(hint, depth + 1, useColor));
|
|
177
|
+
}
|
|
178
|
+
}
|