@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.
Files changed (52) hide show
  1. package/agents/cos.json +20 -0
  2. package/agents/hermes/AGENTS.md +62 -0
  3. package/agents/hermes/SOUL.md +29 -0
  4. package/agents/hermes/fix-kimaki/SKILL.md +100 -0
  5. package/agents/hermes/hermes-fixer.md +115 -0
  6. package/agents/hermes.json +26 -0
  7. package/bin/huckleberry.js +4 -0
  8. package/kanban/SKILL.md +167 -0
  9. package/kanban/kanban +3 -0
  10. package/kanban/package.json +9 -0
  11. package/kanban/src/cli.mjs +553 -0
  12. package/kanban/src/dispatcher.mjs +479 -0
  13. package/kanban/src/server.mjs +108 -0
  14. package/kanban/templates/b2b-outreach.yaml +84 -0
  15. package/kanban/templates/blog-social-campaign.yaml +1161 -0
  16. package/kanban/templates/client-onboard.yaml +107 -0
  17. package/kanban/templates/cro-pipeline.yaml +296 -0
  18. package/kanban/templates/social-campaign.yaml +356 -0
  19. package/mcp/gemini-codegen-server.mjs +134 -0
  20. package/package.json +46 -0
  21. package/plugins/env-inject.ts +61 -0
  22. package/plugins/inkstone-gate.ts +55 -0
  23. package/scripts/heartbeat.sh +41 -0
  24. package/scripts/install-services.ps1 +82 -0
  25. package/scripts/install-services.sh +74 -0
  26. package/scripts/keep-awake.ps1 +48 -0
  27. package/scripts/keep-awake.sh +31 -0
  28. package/scripts/keepalive.sh +43 -0
  29. package/skills/curated.txt +68 -0
  30. package/src/cli.js +227 -0
  31. package/src/interview.js +78 -0
  32. package/src/postinstall.js +22 -0
  33. package/taskmaster/SKILL.md +78 -0
  34. package/taskmaster/dispatcher.mjs +146 -0
  35. package/taskmaster/next-task.js +123 -0
  36. package/taskmaster/update-status.js +118 -0
  37. package/templates/AGENTS.md +90 -0
  38. package/templates/COS_PROTOCOLS.md +71 -0
  39. package/templates/EXECUTIVE-MODE.md +29 -0
  40. package/templates/HEARTBEAT.md +42 -0
  41. package/templates/IDENTITY.md +22 -0
  42. package/templates/MEMORY.md +17 -0
  43. package/templates/ORG.md +33 -0
  44. package/templates/SOUL.md +43 -0
  45. package/templates/TOOLS.md +66 -0
  46. package/templates/business-context-gate.md +9 -0
  47. package/templates/evergreen.md +29 -0
  48. package/templates/opencode.json +80 -0
  49. package/wiki/discord.md +52 -0
  50. package/wiki/inkstone.md +64 -0
  51. package/wiki/kimaki.md +48 -0
  52. 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
+ }