@heart-of-gold/toolkit 0.1.40 → 0.1.42
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 +1 -1
- package/README.md +10 -7
- package/heart-of-gold-toolkit-0.1.42.tgz +0 -0
- package/package.json +1 -1
- package/plugins/babel-fish/.claude-plugin/plugin.json +1 -1
- package/plugins/babel-fish/README.md +5 -1
- package/plugins/babel-fish/skills/linkedin-carousel/SKILL.md +139 -0
- package/src/commands/install.ts +31 -12
- package/src/index.ts +1 -1
- package/src/utils/pi-package-detection.js +81 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> **Don't Panic.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
31 skills for AI coding agents. Five plugins. Works with **Claude Code, Codex, OpenCode, Pi**, and any tool supporting the [agentskills.io](https://agentskills.io) standard. Named after *The Hitchhiker's Guide to the Galaxy* because the universe is absurd and your tools should at least have personality.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -36,6 +36,8 @@ bunx @heart-of-gold/toolkit install --to pi
|
|
|
36
36
|
**Important:** choose one Pi install path or the other.
|
|
37
37
|
Do **not** use both the Pi package install and `install --to pi` at the same time, or Pi will report duplicate skill collisions on reload.
|
|
38
38
|
|
|
39
|
+
The CLI now refuses `install --to pi` when your Pi settings already reference `@heart-of-gold/toolkit` as a package. If you intentionally want both paths for debugging, rerun with `--force`.
|
|
40
|
+
|
|
39
41
|
Pi also discovers skills from the shared `~/.agents/skills/` location, so installs done with the OpenCode target are usable from Pi too.
|
|
40
42
|
|
|
41
43
|
When installed as a Pi package, Heart of Gold exposes Pi-native extension commands for the flagship workflows:
|
|
@@ -136,14 +138,15 @@ Configurable sources (RSS, Gmail, HN, web search), narrative briefs, LinkedIn dr
|
|
|
136
138
|
|
|
137
139
|
### [Babel Fish](plugins/babel-fish/) — Universal Translator
|
|
138
140
|
|
|
139
|
-
Turn words into audio. Turn ideas into images. Visualize anything as a terminal mind map.
|
|
141
|
+
Turn words into audio. Turn ideas into images. Visualize anything as a terminal mind map. Stitch screenshots into a LinkedIn-ready carousel PDF.
|
|
140
142
|
|
|
141
|
-
|
|
143
|
+
4 skills
|
|
142
144
|
|
|
143
145
|
```
|
|
144
|
-
/babel-fish:audio
|
|
145
|
-
/babel-fish:image
|
|
146
|
-
/babel-fish:visualize
|
|
146
|
+
/babel-fish:audio # TTS, podcasts, voice cloning, sound effects
|
|
147
|
+
/babel-fish:image # AI image generation and editing
|
|
148
|
+
/babel-fish:visualize # terminal mind maps from any structured content
|
|
149
|
+
/babel-fish:linkedin-carousel # screenshots → LinkedIn document-post PDF with matched backgrounds
|
|
147
150
|
```
|
|
148
151
|
|
|
149
152
|
### [Quellis](plugins/quellis/) — AI Coaching Companion
|
|
@@ -167,7 +170,7 @@ The toolkit ships as an npm package with a CLI for installing skills into any su
|
|
|
167
170
|
|
|
168
171
|
- `--to pi` installs to Pi's native `~/.pi/agent/skills/`
|
|
169
172
|
- `--to opencode` installs to shared `~/.agents/skills/`, which Pi also discovers
|
|
170
|
-
- `pi install npm:@heart-of-gold/toolkit`
|
|
173
|
+
- `pi install npm:@heart-of-gold/toolkit` adds the package to Pi settings, then Pi installs and loads it as a package with shared skills plus pi-native extensions
|
|
171
174
|
|
|
172
175
|
```bash
|
|
173
176
|
# Install all plugins into Codex
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> "The Babel fish is small, yellow, leathery, and probably the oddest thing in the universe."
|
|
4
4
|
> It also translates your words into audio and your ideas into images.
|
|
5
5
|
|
|
6
|
-
A media generation plugin for Claude Code. Turns text into audio, ideas into images,
|
|
6
|
+
A media generation plugin for Claude Code. Turns text into audio, ideas into images, structured content into terminal mind maps, and screenshots into LinkedIn-ready carousel PDFs.
|
|
7
7
|
|
|
8
8
|
## Security & Trust
|
|
9
9
|
|
|
@@ -36,10 +36,14 @@ Supports Gemini and FLUX models via OpenRouter API.
|
|
|
36
36
|
### `/babel-fish:visualize`
|
|
37
37
|
Render mind maps and tree visualizations directly in the terminal using Unicode box-drawing characters and ANSI colors. Works over SSH — no browser needed. Use it on brainstorm docs, plan docs, markdown files, or any structured content.
|
|
38
38
|
|
|
39
|
+
### `/babel-fish:linkedin-carousel`
|
|
40
|
+
Turn an ordered set of screenshots or images into a LinkedIn document-post PDF (carousel). Auto-samples each source's corner pixel so per-page padding blends invisibly into the image, picks a sensible canvas aspect from the source dimensions, and renders at 2× with Lanczos resampling. Powered by ImageMagick.
|
|
41
|
+
|
|
39
42
|
## Requirements
|
|
40
43
|
|
|
41
44
|
- ElevenLabs API key (for audio)
|
|
42
45
|
- OpenRouter API key (for image generation)
|
|
46
|
+
- ImageMagick (`magick` on PATH) — for `linkedin-carousel`
|
|
43
47
|
|
|
44
48
|
## Install
|
|
45
49
|
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: linkedin-carousel
|
|
3
|
+
description: >
|
|
4
|
+
Turn a set of screenshots or images into a LinkedIn document-post PDF (carousel)
|
|
5
|
+
with per-page background matching so padding disappears into the source.
|
|
6
|
+
Triggers: linkedin carousel, carousel pdf, document post, slide pdf, screenshots to pdf, linkedin pdf, carousel from images.
|
|
7
|
+
allowed-tools:
|
|
8
|
+
- Read
|
|
9
|
+
- Write
|
|
10
|
+
- Bash
|
|
11
|
+
- Glob
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# LinkedIn Carousel — Babel Fish
|
|
15
|
+
|
|
16
|
+
Translating a stack of screenshots into a feed-ready document post. The trick isn't the PDF — it's making the padding invisible.
|
|
17
|
+
|
|
18
|
+
## Boundaries
|
|
19
|
+
|
|
20
|
+
- **MAY:** read source images, run `magick`, write PNGs and PDFs to the requested output path.
|
|
21
|
+
- **MAY NOT:** upload to LinkedIn, post on the user's behalf, or modify the source images in place.
|
|
22
|
+
|
|
23
|
+
## Prerequisites
|
|
24
|
+
|
|
25
|
+
- ImageMagick installed (`magick` on PATH). Check with `which magick`.
|
|
26
|
+
- Sources are raster images (PNG/JPG). For non-image inputs, ask the user to export first.
|
|
27
|
+
|
|
28
|
+
## Phase 0 — Understand
|
|
29
|
+
|
|
30
|
+
**Entry:** User asked for a LinkedIn carousel.
|
|
31
|
+
|
|
32
|
+
Gather, asking only what's missing:
|
|
33
|
+
|
|
34
|
+
- **Sources**: ordered list of image paths (order = page order in the carousel).
|
|
35
|
+
- **Slug**: kebab-case filename for the PDF (e.g. `harness-lab-carousel`). If the user gives a topic, derive it; otherwise ask.
|
|
36
|
+
- **Output path**: default `thoughts/social-media/carousels/<slug>.pdf` if the repo has that dir; otherwise ask.
|
|
37
|
+
- **Aspect preference**: portrait (best feed performance), square, or landscape. If the user has no preference, **infer from sources** in Phase 1.
|
|
38
|
+
|
|
39
|
+
**Exit:** Source list, output path, and aspect intent are known.
|
|
40
|
+
|
|
41
|
+
## Phase 1 — Plan
|
|
42
|
+
|
|
43
|
+
**Entry:** Inputs gathered.
|
|
44
|
+
|
|
45
|
+
Reason step-by-step before generating:
|
|
46
|
+
|
|
47
|
+
1. Run `magick identify` on each source to get width × height.
|
|
48
|
+
2. **Pick canvas aspect** to minimize padding:
|
|
49
|
+
- User asked for portrait → `1080×1350` (×2 = `2160×2700`).
|
|
50
|
+
- User asked for square → `1080×1080` (×2 = `2160×2160`).
|
|
51
|
+
- **No preference** → if the sources cluster around one aspect, pick the **median** w/h ratio — this minimizes total padding across the deck. If sources span a wide range (e.g. 1.6:1 → 2.6:1), pick the aspect that makes the *most* pages padding-free, and accept that the outliers will get bands.
|
|
52
|
+
3. **Sample background color per page** — the move that makes seams invisible. Need the dimensions first, then sample three corners:
|
|
53
|
+
```bash
|
|
54
|
+
read W H < <(magick identify -format "%w %h" "$src")
|
|
55
|
+
magick "$src" -format \
|
|
56
|
+
"tl=%[pixel:p{5,5}] tr=%[pixel:p{$((W-5)),5}] bl=%[pixel:p{5,$((H-5))}]\n" \
|
|
57
|
+
info:
|
|
58
|
+
```
|
|
59
|
+
If all three corners agree, use that color. If they disagree, the source has no clean border — fall back to `#F4EFE3` (or another neutral the user prefers) and tell them.
|
|
60
|
+
4. Render at **2× target resolution** with Lanczos resampling. 300 DPI in the final PDF.
|
|
61
|
+
|
|
62
|
+
**Exit:** Canvas dimensions chosen, per-page background colors sampled.
|
|
63
|
+
|
|
64
|
+
## Phase 2 — Build
|
|
65
|
+
|
|
66
|
+
**Entry:** Plan complete.
|
|
67
|
+
|
|
68
|
+
Write the build script to a temp file (avoids shell quoting traps), then run it:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
#!/bin/bash
|
|
72
|
+
set -e
|
|
73
|
+
OUT=/path/to/output/dir
|
|
74
|
+
SLUG=harness-lab-carousel # from Phase 0
|
|
75
|
+
CANVAS=2160x2700 # from Phase 1
|
|
76
|
+
declare -a SRCS=(/path/1.png /path/2.png /path/3.png)
|
|
77
|
+
declare -a BGS=('#EFF1F5' '#EFF1F5' '#F2ECEB') # from Phase 1 sampling
|
|
78
|
+
|
|
79
|
+
mkdir -p "$OUT"
|
|
80
|
+
PAGES=()
|
|
81
|
+
for i in "${!SRCS[@]}"; do
|
|
82
|
+
# Zero-pad page number so glob ordering survives 10+ pages.
|
|
83
|
+
n=$(printf "%02d" $((i+1)))
|
|
84
|
+
page="$OUT/page${n}.png"
|
|
85
|
+
magick "${SRCS[$i]}" \
|
|
86
|
+
-filter Lanczos \
|
|
87
|
+
-resize "$CANVAS" \
|
|
88
|
+
-background "${BGS[$i]}" \
|
|
89
|
+
-gravity center \
|
|
90
|
+
-extent "$CANVAS" \
|
|
91
|
+
-quality 95 \
|
|
92
|
+
"$page"
|
|
93
|
+
PAGES+=("$page")
|
|
94
|
+
done
|
|
95
|
+
|
|
96
|
+
# Pass pages explicitly in array order — never rely on shell glob ordering.
|
|
97
|
+
magick "${PAGES[@]}" -density 300 -quality 95 "$OUT/${SLUG}.pdf"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Notes:**
|
|
101
|
+
- `-resize WxH` (without `>` or `!`) fits inside the box preserving aspect; `-extent` then pads to exact canvas using `-background`.
|
|
102
|
+
- Pages are passed to the final `magick` call from the `PAGES` array, not via glob — `page*.png` would put `page10.png` before `page2.png` lexicographically and reorder the carousel.
|
|
103
|
+
- Keep the intermediate `page*.png` files — useful for spot fixes without rebuilding everything.
|
|
104
|
+
- LinkedIn document posts cap at **100 MB** and **300 pages**. A 3–10 page carousel at 2× is typically 1–5 MB.
|
|
105
|
+
|
|
106
|
+
**Exit:** PDF and page PNGs exist at the output path.
|
|
107
|
+
|
|
108
|
+
## Phase 3 — Review
|
|
109
|
+
|
|
110
|
+
**Entry:** PDF built.
|
|
111
|
+
|
|
112
|
+
Open the PDF for the user (`open <path>` on macOS). Report:
|
|
113
|
+
|
|
114
|
+
- Output path
|
|
115
|
+
- Page count, canvas dimensions, file size
|
|
116
|
+
- Per-page background colors used
|
|
117
|
+
- Any fallbacks (e.g., "page 2's corners disagreed — used neutral cream")
|
|
118
|
+
|
|
119
|
+
Ask whether any page needs a different aspect or a tighter crop. Common follow-ups:
|
|
120
|
+
|
|
121
|
+
- "Page X has too much padding" → re-sample with a different canvas aspect, or crop that source before rebuilding.
|
|
122
|
+
- "Backgrounds don't match" → the source likely has anti-aliased edges; sample further from the corner (`{20,20}`).
|
|
123
|
+
- "Quality looks soft" → confirm 2× and Lanczos; check the source isn't already low-res.
|
|
124
|
+
|
|
125
|
+
## Constraints
|
|
126
|
+
|
|
127
|
+
- Never modify source images in place.
|
|
128
|
+
- Never upload, share, or post the output. Hand the file to the user.
|
|
129
|
+
- Default to per-page background sampling. Single-color fallback only when sources disagree.
|
|
130
|
+
- Always render at 2× target and downsample at PDF assembly time — not the other way around.
|
|
131
|
+
- Keep the intermediate PNGs unless the user asks to clean them up.
|
|
132
|
+
|
|
133
|
+
## Output
|
|
134
|
+
|
|
135
|
+
The user receives:
|
|
136
|
+
|
|
137
|
+
- `<output-dir>/<slug>.pdf` — the carousel, ready to upload via LinkedIn's "Add a document".
|
|
138
|
+
- `<output-dir>/page1.png`, `page2.png`, … — per-page renders for inspection.
|
|
139
|
+
- A short summary message with path, dimensions, page count, and any fallbacks taken.
|
package/src/commands/install.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { defineCommand } from "citty";
|
|
2
2
|
import { loadAllPlugins, loadPlugin } from "../parsers/claude";
|
|
3
3
|
import { targets } from "../targets/index";
|
|
4
|
-
import { resolve
|
|
5
|
-
import {
|
|
6
|
-
import { execSync } from "child_process";
|
|
4
|
+
import { resolve } from "path";
|
|
5
|
+
import { findHeartOfGoldPiPackageInstalls } from "../utils/pi-package-detection.js";
|
|
7
6
|
|
|
8
7
|
export const installCommand = defineCommand({
|
|
9
8
|
meta: {
|
|
@@ -26,21 +25,41 @@ export const installCommand = defineCommand({
|
|
|
26
25
|
description: "Override output root directory",
|
|
27
26
|
required: false,
|
|
28
27
|
},
|
|
28
|
+
force: {
|
|
29
|
+
type: "boolean",
|
|
30
|
+
description: "Proceed even if Pi already loads @heart-of-gold/toolkit as a package",
|
|
31
|
+
required: false,
|
|
32
|
+
default: false,
|
|
33
|
+
},
|
|
29
34
|
},
|
|
30
35
|
async run({ args }) {
|
|
31
36
|
const targetName = args.to;
|
|
32
37
|
|
|
33
38
|
if (targetName === "pi") {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
const matches = findHeartOfGoldPiPackageInstalls(process.cwd());
|
|
40
|
+
if (matches.length > 0) {
|
|
41
|
+
const locations = matches
|
|
42
|
+
.map(({ settingsPath, source }) => ` - ${settingsPath} → ${source}`)
|
|
43
|
+
.join("\n");
|
|
44
|
+
|
|
45
|
+
const message = [
|
|
46
|
+
"Refusing to install duplicate Pi skills.",
|
|
47
|
+
"Pi is already configured to load @heart-of-gold/toolkit as a package from:",
|
|
48
|
+
locations,
|
|
49
|
+
"",
|
|
50
|
+
"Choose one Pi install path:",
|
|
51
|
+
" 1. keep the Pi package: pi install npm:@heart-of-gold/toolkit",
|
|
52
|
+
" 2. keep native skill copy: bunx @heart-of-gold/toolkit install --to pi",
|
|
53
|
+
"",
|
|
54
|
+
"Remove one side before continuing, or rerun with --force if you really want both.",
|
|
55
|
+
].join("\n");
|
|
56
|
+
|
|
57
|
+
if (!args.force) {
|
|
58
|
+
console.error(message);
|
|
59
|
+
process.exit(1);
|
|
41
60
|
}
|
|
42
|
-
|
|
43
|
-
|
|
61
|
+
|
|
62
|
+
console.warn(`${message}\n`);
|
|
44
63
|
}
|
|
45
64
|
}
|
|
46
65
|
const target = targets[targetName];
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { shareServerCommand } from "./commands/share-server";
|
|
|
8
8
|
const main = defineCommand({
|
|
9
9
|
meta: {
|
|
10
10
|
name: "heart-of-gold",
|
|
11
|
-
version: "0.1.
|
|
11
|
+
version: "0.1.42",
|
|
12
12
|
description:
|
|
13
13
|
"Cross-platform installer for Heart of Gold skills — Codex, OpenCode, Pi, Claude Code, and more",
|
|
14
14
|
},
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, parse, resolve } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
|
|
5
|
+
const HEART_OF_GOLD_PACKAGE_NAME = "@heart-of-gold/toolkit";
|
|
6
|
+
const HEART_OF_GOLD_REPO_HINT = /(^|[/:])heart-of-gold-toolkit(?:$|[@/.#?])/i;
|
|
7
|
+
|
|
8
|
+
function readJson(path) {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
11
|
+
} catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isLocalSource(source) {
|
|
17
|
+
return source.startsWith(".") || source.startsWith("/");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function localSourceIsHeartOfGold(source, settingsDir) {
|
|
21
|
+
const resolved = resolve(settingsDir, source);
|
|
22
|
+
const packageJsonPath = existsSync(resolved) && resolved.endsWith("package.json")
|
|
23
|
+
? resolved
|
|
24
|
+
: join(resolved, "package.json");
|
|
25
|
+
|
|
26
|
+
if (!existsSync(packageJsonPath)) return false;
|
|
27
|
+
|
|
28
|
+
const manifest = readJson(packageJsonPath);
|
|
29
|
+
return manifest?.name === HEART_OF_GOLD_PACKAGE_NAME;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sourceMatchesHeartOfGold(source, settingsDir) {
|
|
33
|
+
if (typeof source !== "string" || source.length === 0) return false;
|
|
34
|
+
|
|
35
|
+
if (source === HEART_OF_GOLD_PACKAGE_NAME) return true;
|
|
36
|
+
if (source.startsWith(`npm:${HEART_OF_GOLD_PACKAGE_NAME}`)) return true;
|
|
37
|
+
if (isLocalSource(source) && localSourceIsHeartOfGold(source, settingsDir)) return true;
|
|
38
|
+
if ((source.startsWith("git:") || source.startsWith("http://") || source.startsWith("https://") || source.startsWith("ssh://")) && HEART_OF_GOLD_REPO_HINT.test(source)) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function collectMatchingPackageSources(settingsPath) {
|
|
46
|
+
if (!existsSync(settingsPath)) return [];
|
|
47
|
+
|
|
48
|
+
const settings = readJson(settingsPath);
|
|
49
|
+
const packages = Array.isArray(settings?.packages) ? settings.packages : [];
|
|
50
|
+
const settingsDir = dirname(settingsPath);
|
|
51
|
+
const matches = [];
|
|
52
|
+
|
|
53
|
+
for (const entry of packages) {
|
|
54
|
+
const source = typeof entry === "string" ? entry : entry?.source;
|
|
55
|
+
if (sourceMatchesHeartOfGold(source, settingsDir)) {
|
|
56
|
+
matches.push({ settingsPath, source });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return matches;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function findNearestProjectSettings(startDir = process.cwd()) {
|
|
64
|
+
let current = resolve(startDir);
|
|
65
|
+
const root = parse(current).root;
|
|
66
|
+
|
|
67
|
+
while (true) {
|
|
68
|
+
const candidate = join(current, ".pi", "settings.json");
|
|
69
|
+
if (existsSync(candidate)) return candidate;
|
|
70
|
+
if (current === root) return null;
|
|
71
|
+
current = dirname(current);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function findHeartOfGoldPiPackageInstalls(startDir = process.cwd()) {
|
|
76
|
+
const globalSettings = join(homedir(), ".pi", "agent", "settings.json");
|
|
77
|
+
const projectSettings = findNearestProjectSettings(startDir);
|
|
78
|
+
const settingsPaths = [globalSettings, projectSettings].filter(Boolean);
|
|
79
|
+
|
|
80
|
+
return settingsPaths.flatMap((settingsPath) => collectMatchingPackageSources(settingsPath));
|
|
81
|
+
}
|