@jairodri/huckleberry-cli 1.0.0
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/agents/cos.json +20 -0
- package/agents/hermes/AGENTS.md +62 -0
- package/agents/hermes/SOUL.md +29 -0
- package/agents/hermes/fix-kimaki/SKILL.md +100 -0
- package/agents/hermes/hermes-fixer.md +115 -0
- package/agents/hermes.json +26 -0
- package/bin/huckleberry.js +4 -0
- package/kanban/SKILL.md +167 -0
- package/kanban/kanban +3 -0
- package/kanban/package.json +9 -0
- package/kanban/src/cli.mjs +553 -0
- package/kanban/src/dispatcher.mjs +479 -0
- package/kanban/src/server.mjs +108 -0
- package/kanban/templates/b2b-outreach.yaml +84 -0
- package/kanban/templates/blog-social-campaign.yaml +1161 -0
- package/kanban/templates/client-onboard.yaml +107 -0
- package/kanban/templates/cro-pipeline.yaml +296 -0
- package/kanban/templates/social-campaign.yaml +356 -0
- package/mcp/gemini-codegen-server.mjs +134 -0
- package/package.json +46 -0
- package/plugins/env-inject.ts +61 -0
- package/plugins/inkstone-gate.ts +55 -0
- package/scripts/heartbeat.sh +41 -0
- package/scripts/install-services.ps1 +82 -0
- package/scripts/install-services.sh +74 -0
- package/scripts/keep-awake.ps1 +48 -0
- package/scripts/keep-awake.sh +31 -0
- package/scripts/keepalive.sh +43 -0
- package/skills/curated.txt +68 -0
- package/src/cli.js +227 -0
- package/src/interview.js +78 -0
- package/src/postinstall.js +22 -0
- package/taskmaster/SKILL.md +78 -0
- package/taskmaster/dispatcher.mjs +146 -0
- package/taskmaster/next-task.js +123 -0
- package/taskmaster/update-status.js +118 -0
- package/templates/AGENTS.md +90 -0
- package/templates/COS_PROTOCOLS.md +71 -0
- package/templates/EXECUTIVE-MODE.md +29 -0
- package/templates/HEARTBEAT.md +42 -0
- package/templates/IDENTITY.md +22 -0
- package/templates/MEMORY.md +17 -0
- package/templates/ORG.md +33 -0
- package/templates/SOUL.md +43 -0
- package/templates/TOOLS.md +66 -0
- package/templates/business-context-gate.md +9 -0
- package/templates/evergreen.md +29 -0
- package/templates/opencode.json +80 -0
- package/wiki/discord.md +52 -0
- package/wiki/inkstone.md +64 -0
- package/wiki/kimaki.md +48 -0
- package/wiki/opencode.md +43 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
name: Social Campaign
|
|
2
|
+
description: >
|
|
3
|
+
Multi-platform social content pipeline. Research any topic via NLM + podcast/YouTube/X discovery,
|
|
4
|
+
generate hooks, write platform-native content (single tweet by default, thread only when warranted),
|
|
5
|
+
humanize, quality gate, approve, and learn. Brand-agnostic.
|
|
6
|
+
|
|
7
|
+
variables:
|
|
8
|
+
- name: topic
|
|
9
|
+
required: true
|
|
10
|
+
description: The content topic (any source — kaizen, news, idea, podcast, etc.)
|
|
11
|
+
- name: tenant
|
|
12
|
+
required: true
|
|
13
|
+
description: Brand/project tenant name (maps to businesses/{tenant}/)
|
|
14
|
+
- name: platforms
|
|
15
|
+
default: twitter,linkedin
|
|
16
|
+
description: Comma-separated target platforms
|
|
17
|
+
- name: funnel_stage
|
|
18
|
+
default: TOFU
|
|
19
|
+
description: "TOFU (awareness), MOFU (consideration), or BOFU (decision). Drives angles and gate scoring."
|
|
20
|
+
- name: icp_segment
|
|
21
|
+
required: false
|
|
22
|
+
description: "ICP segment to target. Uses brand default from skill-context.json if not set."
|
|
23
|
+
- name: cta_resource
|
|
24
|
+
required: false
|
|
25
|
+
description: "Lead magnet or resource for CTA (calculator, checklist, guide). Uses brand default if not set."
|
|
26
|
+
|
|
27
|
+
tasks:
|
|
28
|
+
# ── TASK 0: SHARP-THESIS VALIDATION ────────────────────────
|
|
29
|
+
- id: thesis_validation
|
|
30
|
+
title: "Validate hook/thesis: {{topic}}"
|
|
31
|
+
assignee: build
|
|
32
|
+
skills: [sharp-thesis-validator]
|
|
33
|
+
priority: 4
|
|
34
|
+
gate: true
|
|
35
|
+
body: |
|
|
36
|
+
Validate that "{{topic}}" passes the Sharp-Thesis (Cerebras) Test in social mode.
|
|
37
|
+
This blocks all downstream tasks until PASS.
|
|
38
|
+
|
|
39
|
+
Use the `sharp-thesis-validator` skill (already loaded by dispatcher).
|
|
40
|
+
Pass to the skill: topic="{{topic}}", brand="{{tenant}}", mode="social"
|
|
41
|
+
|
|
42
|
+
**Social mode criteria:**
|
|
43
|
+
1. Single arguable claim: REQUIRED
|
|
44
|
+
2. Concrete number: REQUIRED
|
|
45
|
+
3. Urgency: RELAXED (unless thesis-config.social.urgency_required=true)
|
|
46
|
+
4. Title/hook expressibility ≤280 chars (or thesis-config override)
|
|
47
|
+
|
|
48
|
+
**On PASS:** Mark complete. Proceed to research.
|
|
49
|
+
**On FAIL:** BLOCK. Present reframing suggestions to user.
|
|
50
|
+
User must approve a reframed topic before proceeding.
|
|
51
|
+
|
|
52
|
+
# ── TASK 1: RESEARCH + NLM ────────────────────────────────────
|
|
53
|
+
- id: research
|
|
54
|
+
title: "Research: {{topic}}"
|
|
55
|
+
assignee: researcher
|
|
56
|
+
skills: [nlm-cli, podcast-to-thread, x-research, subreddit-pain-extraction]
|
|
57
|
+
parents: [thesis_validation]
|
|
58
|
+
priority: 3
|
|
59
|
+
body: |
|
|
60
|
+
Deep research for social content about "{{topic}}" for {{tenant}}.
|
|
61
|
+
Goal: gather rich context that drives VALUE for the reader.
|
|
62
|
+
|
|
63
|
+
**0. Create campaign directory**
|
|
64
|
+
Derive a slug from the topic (first 5 words, lowercase, hyphenated).
|
|
65
|
+
Create: businesses/{{tenant}}/content/_drafts/{slug}/
|
|
66
|
+
ALL outputs from ALL tasks in this campaign go in that directory.
|
|
67
|
+
This prevents campaigns from overwriting each other.
|
|
68
|
+
|
|
69
|
+
**1. Brand context (load first)**
|
|
70
|
+
Read businesses/{{tenant}}/brand/skill-context.json for ICP, pains, positioning angles, offer, distribution rules.
|
|
71
|
+
Funnel stage: {{funnel_stage}}. Target ICP segment: {{icp_segment}}.
|
|
72
|
+
|
|
73
|
+
**2. Source discovery (use skills — do NOT search manually)**
|
|
74
|
+
- Load podcast-to-thread skill: find related podcast episodes and YouTube videos for "{{topic}}"
|
|
75
|
+
- Load x-research skill: search X for current narratives, what's already been said, sentiment, trending angles
|
|
76
|
+
- Load subreddit-pain-extraction skill: extract verbatim pain phrases from relevant subreddits
|
|
77
|
+
|
|
78
|
+
**3. NLM deep research**
|
|
79
|
+
Use the notebooklm CLI (notebooklm-py v0.7.3). Create or reuse notebook for this topic.
|
|
80
|
+
Add all discovered sources (podcasts, YouTube, articles, kaizen reports) to the notebook.
|
|
81
|
+
Do NOT generate mindmaps or topic clusters — that is SEO overhead.
|
|
82
|
+
|
|
83
|
+
Query NLM for social-specific research:
|
|
84
|
+
```
|
|
85
|
+
notebooklm generate report --format custom --wait --json \
|
|
86
|
+
"Research '{{topic}}' for social media content targeting {{funnel_stage}} audience.
|
|
87
|
+
|
|
88
|
+
Provide:
|
|
89
|
+
1. CONTRARIAN ANGLES: What is everyone getting wrong about this topic? 3-5 contrarian takes backed by sources.
|
|
90
|
+
2. GAP ANALYSIS: What angles are NOT being covered by existing content? What is underserved?
|
|
91
|
+
3. CORE INSIGHTS: The 3 most surprising or valuable insights from your sources. Include specific data points.
|
|
92
|
+
4. PAIN POINTS: What problems does the target audience face related to this topic? Use their language.
|
|
93
|
+
5. PLATFORM ANGLES: For each platform ({{platforms}}), what angle would work best?
|
|
94
|
+
|
|
95
|
+
Be specific. Cite sources. Use real data."
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**4. Output structured research brief**
|
|
99
|
+
Save to: businesses/{{tenant}}/content/_drafts/{slug}/research-brief.json
|
|
100
|
+
Schema:
|
|
101
|
+
{
|
|
102
|
+
"topic": "{{topic}}",
|
|
103
|
+
"campaign_slug": "{slug}",
|
|
104
|
+
"funnel_stage": "{{funnel_stage}}",
|
|
105
|
+
"icp_segment": "{{icp_segment}}",
|
|
106
|
+
"pain_phrases": ["verbatim phrases from subreddits"],
|
|
107
|
+
"current_narratives": ["what X and existing content already says"],
|
|
108
|
+
"contrarian_angles": ["angles backed by NLM sources"],
|
|
109
|
+
"gap_angles": ["underserved angles nobody covers"],
|
|
110
|
+
"core_insights": ["3 most valuable insights with data"],
|
|
111
|
+
"platform_angles": {
|
|
112
|
+
"twitter": "best angle for X",
|
|
113
|
+
"linkedin": "best angle for LI"
|
|
114
|
+
},
|
|
115
|
+
"nlm_notebook_id": "<id>",
|
|
116
|
+
"sources_used": ["list of URLs/files added to NLM"]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# ── TASK 2: HOOKS ─────────────────────────────────────────────
|
|
120
|
+
- id: hooks
|
|
121
|
+
title: "Hooks: {{topic}}"
|
|
122
|
+
assignee: writer
|
|
123
|
+
skills: [hook-generator]
|
|
124
|
+
parents: [research]
|
|
125
|
+
priority: 3
|
|
126
|
+
body: |
|
|
127
|
+
Generate hooks for the topic.
|
|
128
|
+
Load the hook-generator skill.
|
|
129
|
+
|
|
130
|
+
**1. Load research brief**
|
|
131
|
+
Read businesses/{{tenant}}/content/_drafts/{slug}/research-brief.json
|
|
132
|
+
This is your ONLY context. Do not re-research.
|
|
133
|
+
|
|
134
|
+
**2. Generate hooks (tournament scored)**
|
|
135
|
+
Run the hook-generator CLI:
|
|
136
|
+
```
|
|
137
|
+
node ~/.claude/skills/hook-generator/scripts/generate-hooks-unified.js \
|
|
138
|
+
--content "{{topic}}" \
|
|
139
|
+
--platform twitter \
|
|
140
|
+
--brand {{tenant}} \
|
|
141
|
+
--count 10
|
|
142
|
+
```
|
|
143
|
+
Take the top 5 from CLI output.
|
|
144
|
+
Also generate 3-5 hooks manually using the 6-formula system from the hook-generator SKILL.md.
|
|
145
|
+
Ground every hook in the research brief pain phrases. Use pain language verbatim.
|
|
146
|
+
|
|
147
|
+
**3. Present to Jairo**
|
|
148
|
+
Present the top 3 hooks in Discord.
|
|
149
|
+
Show: hook text, pattern, score, why it works.
|
|
150
|
+
Provide action buttons: "Hook 1", "Hook 2", "Hook 3", "Revise", "Kill".
|
|
151
|
+
|
|
152
|
+
**BLOCK this task. Do NOT proceed until Jairo picks a hook.**
|
|
153
|
+
|
|
154
|
+
Hook rules: No em dashes. No colons. No AI slop patterns (no "You aren't X. You are Y." negation-then-revelation). No "AI is changing everything" generics.
|
|
155
|
+
|
|
156
|
+
**4. Output**
|
|
157
|
+
Save to: businesses/{{tenant}}/content/_drafts/{slug}/hooks.json
|
|
158
|
+
Schema:
|
|
159
|
+
{
|
|
160
|
+
"approved_hook": "the hook text",
|
|
161
|
+
"pattern": "hook formula used",
|
|
162
|
+
"score": 95,
|
|
163
|
+
"runner_ups": [{"hook": "...", "score": 90}, ...]
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
# ── TASK 3: WRITE ─────────────────────────────────────────────
|
|
167
|
+
- id: write
|
|
168
|
+
title: "Write: {{topic}}"
|
|
169
|
+
assignee: writer
|
|
170
|
+
skills: [x-post-generator-openclaw]
|
|
171
|
+
parents: [hooks]
|
|
172
|
+
priority: 2
|
|
173
|
+
body: |
|
|
174
|
+
Write the social content using the approved hook and research brief.
|
|
175
|
+
Load the x-post-generator-openclaw skill.
|
|
176
|
+
|
|
177
|
+
**1. Load context**
|
|
178
|
+
Read businesses/{{tenant}}/content/_drafts/{slug}/hooks.json (approved hook)
|
|
179
|
+
Read businesses/{{tenant}}/content/_drafts/{slug}/research-brief.json (pain phrases, insights, platform angles)
|
|
180
|
+
Read businesses/{{tenant}}/brand/voice-dna.md (voice rules — apply as SEASONING, not poetry)
|
|
181
|
+
|
|
182
|
+
**2. FORMAT DECISION (most important step)**
|
|
183
|
+
|
|
184
|
+
Assess the core insight from the research brief. Can it land with full value in a SINGLE post?
|
|
185
|
+
|
|
186
|
+
- SINGLE TWEET (default): If the insight fits in 280 chars or less with all its value intact, write ONE tweet. Stop. Done.
|
|
187
|
+
- THREAD (only when warranted): Only write a thread if the insight genuinely requires multiple posts to deliver value. Examples that warrant a thread: a case study with setup and resolution, a multi-step framework the reader needs step by step, a narrative arc with a before/after transformation.
|
|
188
|
+
|
|
189
|
+
Do NOT write a thread when a single tweet works. Padding a one-tweet insight into 6 tweets destroys value.
|
|
190
|
+
|
|
191
|
+
**3. Thread rules (if thread is warranted)**
|
|
192
|
+
- Each tweet must deliver STANDALONE VALUE. A reader who sees only tweet 3 should get something useful.
|
|
193
|
+
- No tweet exists solely to pull the reader to the next. No manufactured curiosity gaps. No "slippery slide."
|
|
194
|
+
- Deliver the hook's promise FAST. If the hook says "one process fixes it," the reader should know what that process is by tweet 2 at the latest.
|
|
195
|
+
- Maximum density. Every word earns its place. If removing a tweet doesn't lose value, remove it.
|
|
196
|
+
- Thread body: ZERO links. Links kill reach.
|
|
197
|
+
- Reply tweet / first comment: contains the CTA with link.
|
|
198
|
+
|
|
199
|
+
**4. CTA strategy**
|
|
200
|
+
- The CTA must offer MORE VALUE (lead magnet, calculator, checklist, resource) — not a sales pitch.
|
|
201
|
+
- For single tweets: CTA goes in the reply.
|
|
202
|
+
- For threads: CTA goes in the reply to the last tweet.
|
|
203
|
+
|
|
204
|
+
**5. Voice rules (NON-NEGOTIABLE)**
|
|
205
|
+
- Voice DNA as seasoning (20%), reader value first (80%). Sound like a person, not a brand.
|
|
206
|
+
- Write like you are explaining to a smart, skeptical friend. Back every claim with specifics.
|
|
207
|
+
- No em dashes. No AI vocabulary. No negation-then-revelation patterns. No generic conclusions.
|
|
208
|
+
- RHYTHM is the tell. Real writing alternates punchy short sentences with longer ones. Same-length paragraphs scream AI.
|
|
209
|
+
- AVOID: "delve," "utilize," "game-changer," "leverage," anything that starts with "Let's dive in." Read it out loud.
|
|
210
|
+
|
|
211
|
+
**6. Generate per platform**
|
|
212
|
+
For each platform in {{platforms}}, adapt the content using the platform-specific angles from the research brief.
|
|
213
|
+
X/Twitter: hook-first, max density, max 280 chars per post
|
|
214
|
+
LinkedIn: professional framing, longer paragraphs acceptable, data-backed
|
|
215
|
+
|
|
216
|
+
**7. Output**
|
|
217
|
+
Save to: businesses/{{tenant}}/content/_drafts/{slug}/draft-{{platform}}.md (one per platform)
|
|
218
|
+
Save reply/CTA to: businesses/{{tenant}}/content/_drafts/{slug}/reply-{{platform}}.md
|
|
219
|
+
Include metadata header in each file:
|
|
220
|
+
---
|
|
221
|
+
topic: "{{topic}}"
|
|
222
|
+
campaign_slug: "{slug}"
|
|
223
|
+
funnel_stage: "{{funnel_stage}}"
|
|
224
|
+
platform: twitter
|
|
225
|
+
format: "single" or "thread"
|
|
226
|
+
post_count: 1
|
|
227
|
+
hook_pattern: "from hooks.json"
|
|
228
|
+
cta_type: "calculator / checklist / guide / none"
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
# ── TASK 4: HUMANIZE ──────────────────────────────────────────
|
|
232
|
+
- id: humanize
|
|
233
|
+
title: "Humanize: {{topic}}"
|
|
234
|
+
assignee: writer
|
|
235
|
+
skills: [humanizer, voice-brand-alignment]
|
|
236
|
+
parents: [write]
|
|
237
|
+
priority: 2
|
|
238
|
+
body: |
|
|
239
|
+
Strip all AI patterns and enforce brand voice DNA.
|
|
240
|
+
Load the humanizer skill and voice-brand-alignment skill.
|
|
241
|
+
|
|
242
|
+
**1. For each platform draft:**
|
|
243
|
+
Read businesses/{{tenant}}/content/_drafts/{slug}/draft-*.md
|
|
244
|
+
|
|
245
|
+
**2. Humanizer pass (all 30 AI patterns)**
|
|
246
|
+
- Content: significance inflation, promotional language, vague attributions, rule of three, generic conclusions
|
|
247
|
+
- Language: AI vocabulary, copula avoidance, negative parallelisms, elegant variation, false ranges, passive voice
|
|
248
|
+
- Style: em dashes (ZERO TOLERANCE), bold overuse, title case
|
|
249
|
+
- Communication: chatbot artifacts, sycophantic tone, filler, hedging
|
|
250
|
+
- KILL patterns: "You aren't X. You are Y." negation-then-revelation. Generic aphorisms. "The AI revolution is here."
|
|
251
|
+
|
|
252
|
+
**3. Voice-brand-alignment validation**
|
|
253
|
+
- Does it sound like a person explaining to a peer, not a brand broadcasting?
|
|
254
|
+
- Is voice DNA present as seasoning (not overpowering)?
|
|
255
|
+
- Would Jairo share this as-is?
|
|
256
|
+
- Any banned words from voice-dna.md?
|
|
257
|
+
|
|
258
|
+
**4. Output**
|
|
259
|
+
Save to: businesses/{{tenant}}/content/_drafts/{slug}/humanized-*.md
|
|
260
|
+
Include change log: what was stripped, what was rewritten, what passed clean.
|
|
261
|
+
|
|
262
|
+
# ── TASK 5: QUALITY GATE (ICP at funnel stage) ────────────────
|
|
263
|
+
- id: gate
|
|
264
|
+
title: "Quality gate: {{topic}}"
|
|
265
|
+
assignee: reviewer
|
|
266
|
+
skills: [quality-gate]
|
|
267
|
+
parents: [humanize]
|
|
268
|
+
priority: 1
|
|
269
|
+
body: |
|
|
270
|
+
Run quality gate with ICP impersonation at funnel stage.
|
|
271
|
+
Load the quality-gate skill. Use STANDARD mode (single judge).
|
|
272
|
+
|
|
273
|
+
**1. Load brand context**
|
|
274
|
+
Read businesses/{{tenant}}/brand/skill-context.json
|
|
275
|
+
Extract ICP, pains, positioning angles, offer, voice into a JSON object.
|
|
276
|
+
|
|
277
|
+
**2. For each platform's humanized draft:**
|
|
278
|
+
Read businesses/{{tenant}}/content/_drafts/{slug}/humanized-*.md
|
|
279
|
+
|
|
280
|
+
**3. Spawn ONE quality gate judge subagent per platform**
|
|
281
|
+
Use Task tool to spawn a build subagent with the standard quality-gate prompt template:
|
|
282
|
+
- Pass: draft_path, mode: social, original_prompt, attempt: 1, brand_context (the JSON object)
|
|
283
|
+
|
|
284
|
+
The judge will:
|
|
285
|
+
- Roleplay as the ICP at funnel stage {{funnel_stage}}
|
|
286
|
+
- Score 5 dimensions: stop-scroll, relevance, trust, next-step, ai-slop (0-10)
|
|
287
|
+
- Apply funnel-stage weights (TOFU: stop-scroll 3x, MOFU: relevance 3x, BOFU: trust 3x)
|
|
288
|
+
- Validate positioning alignment (must match at least one angle from skill-context.json)
|
|
289
|
+
- Return JSON with passed/failed, scores, weighted_average, rewrite_instructions
|
|
290
|
+
|
|
291
|
+
**4. Aggregate results**
|
|
292
|
+
Save to: businesses/{{tenant}}/content/_drafts/{slug}/gate-results.json
|
|
293
|
+
|
|
294
|
+
**5. Pass/Fail decision**
|
|
295
|
+
- PASS: weighted average >= 7.5 AND no dimension below 5.5 AND positioning aligned
|
|
296
|
+
- FAIL: anything else
|
|
297
|
+
|
|
298
|
+
**6. If FAIL (attempt < 3): rewrite with specific feedback, re-run humanize + gate**
|
|
299
|
+
**7. If FAIL (attempt = 3): present to Jairo with all scores**
|
|
300
|
+
**8. If PASS: proceed to approval**
|
|
301
|
+
|
|
302
|
+
# ── TASK 6: APPROVE ───────────────────────────────────────────
|
|
303
|
+
- id: approve
|
|
304
|
+
title: "Approve: {{topic}}"
|
|
305
|
+
assignee: reviewer
|
|
306
|
+
parents: [gate]
|
|
307
|
+
priority: 1
|
|
308
|
+
body: |
|
|
309
|
+
Present quality-gated content to Jairo for final approval.
|
|
310
|
+
|
|
311
|
+
**1. Present in Discord**
|
|
312
|
+
For each platform, show:
|
|
313
|
+
- The complete post(s) (each post clearly numbered)
|
|
314
|
+
- The reply/CTA separately
|
|
315
|
+
- Quality gate summary (weighted avg, pass/fail)
|
|
316
|
+
- Metadata: hook pattern, format (single/thread), CTA type
|
|
317
|
+
|
|
318
|
+
**2. Action buttons per platform: Approve / Revise / Kill**
|
|
319
|
+
|
|
320
|
+
**3. If APPROVE:**
|
|
321
|
+
- Save final content to: businesses/{{tenant}}/content/social/{{platform}}/
|
|
322
|
+
- Log to production database: businesses/{{tenant}}/content/content-tracker.json
|
|
323
|
+
- Note: Jairo posts manually. Do NOT auto-post to any platform.
|
|
324
|
+
|
|
325
|
+
**4. If REVISE:** collect feedback, go back to write task
|
|
326
|
+
**5. If KILL:** move to _killed/ with reason
|
|
327
|
+
|
|
328
|
+
# ── TASK 7: LEARN + IMPROVE ───────────────────────────────────
|
|
329
|
+
- id: learn
|
|
330
|
+
title: "Learn: {{topic}}"
|
|
331
|
+
assignee: reviewer
|
|
332
|
+
parents: [approve]
|
|
333
|
+
priority: 1
|
|
334
|
+
body: |
|
|
335
|
+
Debrief with Jairo and update brand intelligence for the next run.
|
|
336
|
+
|
|
337
|
+
**1. Ask Jairo directly (interactive)**
|
|
338
|
+
Present a short debrief in Discord:
|
|
339
|
+
"Quick debrief on '{{topic}}':
|
|
340
|
+
- What worked?
|
|
341
|
+
- What didn't?
|
|
342
|
+
- Single tweet or thread — which felt right?
|
|
343
|
+
- Any voice elements that landed or missed?"
|
|
344
|
+
|
|
345
|
+
**2. Log feedback**
|
|
346
|
+
Update the content-tracker.json entry with Jairo's feedback in the "learnings" field.
|
|
347
|
+
|
|
348
|
+
**3. Update brand intelligence**
|
|
349
|
+
Based on Jairo's feedback:
|
|
350
|
+
- If a hook pattern worked: note it in hook performance data
|
|
351
|
+
- If a voice element landed: reinforce in voice-dna.md learnings
|
|
352
|
+
- If an angle missed: add to "avoid" list in skill-context.json
|
|
353
|
+
- If a CTA type resonated: prioritize in future runs
|
|
354
|
+
|
|
355
|
+
**4. Output**
|
|
356
|
+
Save learnings to: businesses/{{tenant}}/content/learnings.json (append entry)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const ROOT = join(__dirname, "../..");
|
|
9
|
+
|
|
10
|
+
function getApiKey() {
|
|
11
|
+
if (process.env.GEMINI_API_KEY) return process.env.GEMINI_API_KEY;
|
|
12
|
+
const envPath = join(ROOT, ".env");
|
|
13
|
+
if (existsSync(envPath)) {
|
|
14
|
+
const match = readFileSync(envPath, "utf8").match(/^GEMINI_API_KEY=(.+)/m);
|
|
15
|
+
if (match) return match[1].trim();
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function gemini(req) {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
const key = getApiKey();
|
|
23
|
+
const env = { ...process.env, GEMINI_API_KEY: key };
|
|
24
|
+
const proc = spawn("gemini", ["-y", "-o", "json", req], { env, cwd: ROOT });
|
|
25
|
+
let stdout = "", stderr = "";
|
|
26
|
+
proc.stdout.on("data", d => stdout += d);
|
|
27
|
+
proc.stderr.on("data", d => stderr += d);
|
|
28
|
+
proc.on("close", code => {
|
|
29
|
+
if (code !== 0) return reject(new Error(stderr || `exit ${code}`));
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(stdout);
|
|
32
|
+
resolve({
|
|
33
|
+
response: parsed.response || stdout,
|
|
34
|
+
stats: parsed.stats || {},
|
|
35
|
+
files: parsed.stats?.files || {}
|
|
36
|
+
});
|
|
37
|
+
} catch {
|
|
38
|
+
resolve({ response: stdout, stats: {}, files: {} });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const TOOLS = [
|
|
45
|
+
{
|
|
46
|
+
name: "gemini_codegen",
|
|
47
|
+
description: "Generate TypeScript/JavaScript code using the free Gemini API. Use for: new file creation, boilerplate, component scaffolding, test scaffolding, data structure definitions. Do NOT use for: code review, debugging, analysis, research, planning.",
|
|
48
|
+
inputSchema: {
|
|
49
|
+
type: "object",
|
|
50
|
+
properties: {
|
|
51
|
+
prompt: { type: "string", description: "Detailed code generation prompt describing exactly what to create" }
|
|
52
|
+
},
|
|
53
|
+
required: ["prompt"]
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "gemini_prompt",
|
|
58
|
+
description: "Run any prompt through the free Gemini API without writing files. Use for: data extraction, classification, tagging, JSON transformation, regex generation, schema inference, summarization, format conversion (CSV->JSON, markdown->HTML), simple Q&A. Saves paid model tokens.",
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: {
|
|
62
|
+
prompt: { type: "string", description: "The prompt to send to Gemini" },
|
|
63
|
+
systemPrompt: { type: "string", description: "Optional system instruction to set context/behavior" }
|
|
64
|
+
},
|
|
65
|
+
required: ["prompt"]
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
function send(id, result) {
|
|
71
|
+
process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, ...result }) + "\n");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
process.stdin.on("data", async (chunk) => {
|
|
75
|
+
for (const line of chunk.toString().split("\n").filter(Boolean)) {
|
|
76
|
+
try {
|
|
77
|
+
const msg = JSON.parse(line);
|
|
78
|
+
const { method, id, params } = msg;
|
|
79
|
+
|
|
80
|
+
if (method === "initialize") {
|
|
81
|
+
send(id, {
|
|
82
|
+
result: {
|
|
83
|
+
protocolVersion: "2024-11-05",
|
|
84
|
+
capabilities: { tools: {} }
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
} else if (method === "notifications/initialized") {
|
|
88
|
+
// ignore
|
|
89
|
+
} else if (method === "tools/list") {
|
|
90
|
+
send(id, { result: { tools: TOOLS } });
|
|
91
|
+
} else if (method === "tools/call") {
|
|
92
|
+
const name = params?.name;
|
|
93
|
+
const args = params?.arguments || {};
|
|
94
|
+
const key = getApiKey();
|
|
95
|
+
if (!key) {
|
|
96
|
+
send(id, { error: { code: -32000, message: "GEMINI_API_KEY not found" } });
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const tool = TOOLS.find(t => t.name === name);
|
|
101
|
+
if (!tool) {
|
|
102
|
+
send(id, { error: { code: -32601, message: `Unknown tool: ${name}` } });
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
let fullPrompt = args.prompt || "";
|
|
108
|
+
if (args.systemPrompt) {
|
|
109
|
+
fullPrompt = `[System Instructions]\n${args.systemPrompt}\n\n[Task]\n${fullPrompt}`;
|
|
110
|
+
}
|
|
111
|
+
const result = await gemini(fullPrompt);
|
|
112
|
+
const tokens = result.stats?.tokens?.total;
|
|
113
|
+
const model = result.stats?.models && Object.keys(result.stats.models).join(", ");
|
|
114
|
+
const meta = [tokens ? `Tokens: ${tokens}` : null, model ? `Model: ${model}` : null].filter(Boolean).join(" | ");
|
|
115
|
+
|
|
116
|
+
send(id, {
|
|
117
|
+
result: {
|
|
118
|
+
content: [
|
|
119
|
+
{ type: "text", text: result.response },
|
|
120
|
+
{ type: "text", text: meta || "Done." }
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
} catch (err) {
|
|
125
|
+
send(id, { error: { code: -32000, message: err.message } });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} catch (e) {
|
|
129
|
+
// not JSON, ignore
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
process.stdin.on("end", () => process.exit(0));
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jairodri/huckleberry-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Huckleberry AI System — One-click sovereign AI installer",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"huckleberry": "./bin/huckleberry.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./src/cli.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"src/",
|
|
13
|
+
"templates/",
|
|
14
|
+
"agents/",
|
|
15
|
+
"plugins/",
|
|
16
|
+
"mcp/",
|
|
17
|
+
"skills/",
|
|
18
|
+
"kanban/",
|
|
19
|
+
"taskmaster/",
|
|
20
|
+
"scripts/",
|
|
21
|
+
"wiki/",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"postinstall": "node src/postinstall.js",
|
|
26
|
+
"build": "echo 'Nothing to build — this is a config-only package'"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"huckleberry",
|
|
30
|
+
"ai",
|
|
31
|
+
"sovereign",
|
|
32
|
+
"opencode",
|
|
33
|
+
"kimaki",
|
|
34
|
+
"cos",
|
|
35
|
+
"memory",
|
|
36
|
+
"inkstone"
|
|
37
|
+
],
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/jairodri/huckleberry-cli"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* env-inject.ts — OpenCode Plugin
|
|
3
|
+
* Loads environment variables from .env file in the project root.
|
|
4
|
+
* Run `kimaki send` or `opencode` from a project directory and
|
|
5
|
+
* this plugin auto-loads the .env file.
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
const PLUGIN_NAME = "env-inject";
|
|
11
|
+
|
|
12
|
+
export default function envInject() {
|
|
13
|
+
return {
|
|
14
|
+
name: PLUGIN_NAME,
|
|
15
|
+
version: "1.0.0",
|
|
16
|
+
setup(api: any) {
|
|
17
|
+
// Look for .env in current working directory
|
|
18
|
+
const cwd = process.cwd();
|
|
19
|
+
const envFiles = [".env", ".env.local", ".env.development"];
|
|
20
|
+
|
|
21
|
+
for (const file of envFiles) {
|
|
22
|
+
const envPath = join(cwd, file);
|
|
23
|
+
if (existsSync(envPath)) {
|
|
24
|
+
try {
|
|
25
|
+
const content = readFileSync(envPath, "utf-8");
|
|
26
|
+
const lines = content.split("\n");
|
|
27
|
+
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
const trimmed = line.trim();
|
|
30
|
+
// Skip empty lines and comments
|
|
31
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
32
|
+
|
|
33
|
+
const eqIndex = trimmed.indexOf("=");
|
|
34
|
+
if (eqIndex === -1) continue;
|
|
35
|
+
|
|
36
|
+
const key = trimmed.substring(0, eqIndex).trim();
|
|
37
|
+
let value = trimmed.substring(eqIndex + 1).trim();
|
|
38
|
+
|
|
39
|
+
// Remove surrounding quotes
|
|
40
|
+
if (
|
|
41
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
42
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
43
|
+
) {
|
|
44
|
+
value = value.slice(1, -1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Only set if not already set in environment
|
|
48
|
+
if (!process.env[key]) {
|
|
49
|
+
process.env[key] = value;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log(`[${PLUGIN_NAME}] Loaded ${file}`);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.error(`[${PLUGIN_NAME}] Error loading ${file}:`, err);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* inkstone-gate.ts — OpenCode Plugin
|
|
3
|
+
* Reminds agents to run Inkstone pre-injection checks before starting work.
|
|
4
|
+
* Soft dependency — logs a warning but never blocks tool execution.
|
|
5
|
+
*/
|
|
6
|
+
const PLUGIN_NAME = "inkstone-gate";
|
|
7
|
+
|
|
8
|
+
export default function inkstoneGate() {
|
|
9
|
+
let precheckDone = false;
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
name: PLUGIN_NAME,
|
|
13
|
+
version: "1.0.0",
|
|
14
|
+
setup(api: any) {
|
|
15
|
+
// Listen for session start
|
|
16
|
+
api.on?.("session:start", () => {
|
|
17
|
+
precheckDone = false;
|
|
18
|
+
console.log(
|
|
19
|
+
`[${PLUGIN_NAME}] Reminder: Run inkstone_memory_failures() or inkstone_memory_search() before starting work (AGENTS.md mandatory precheck)`
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Track when inkstone tools are called
|
|
24
|
+
api.on?.("tool:call", (event: any) => {
|
|
25
|
+
if (
|
|
26
|
+
event.toolName?.startsWith("inkstone_memory_") ||
|
|
27
|
+
event.toolName?.startsWith("inkstone_")
|
|
28
|
+
) {
|
|
29
|
+
precheckDone = true;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Soft reminder if tools are called before precheck
|
|
34
|
+
api.on?.("tool:before", (event: any) => {
|
|
35
|
+
if (!precheckDone && event.toolName) {
|
|
36
|
+
// Only remind once
|
|
37
|
+
if (!globalThis.__inkstoneGateReminded) {
|
|
38
|
+
console.warn(
|
|
39
|
+
`[${PLUGIN_NAME}] Precheck not done yet. ` +
|
|
40
|
+
`Run inkstone_memory_search() first per AGENTS.md. ` +
|
|
41
|
+
`This is a soft reminder — tools will still execute.`
|
|
42
|
+
);
|
|
43
|
+
globalThis.__inkstoneGateReminded = true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Reset reminder flag on session end
|
|
49
|
+
api.on?.("session:end", () => {
|
|
50
|
+
precheckDone = false;
|
|
51
|
+
(globalThis as any).__inkstoneGateReminded = false;
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|