@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 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",
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 via bash:
99
+ 3. Run the renderer with `--no-color`, redirect to a temp file:
96
100
  ```bash
97
- node "$SCRIPT" [options] [file]
101
+ node "$SCRIPT" --no-color [file] > /tmp/mindmap-result.txt 2>&1
98
102
  ```
99
- 4. The output appears inline in the terminal with colors
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
- **Width:** Default is the terminal width (`$COLUMNS` or 120). For narrow terminals, use `--depth 1` to ensure readability.
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
+ }