@higrowth/cli 0.1.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 (3) hide show
  1. package/README.md +60 -0
  2. package/dist/index.js +1285 -0
  3. package/package.json +37 -0
package/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # @higrowth/cli
2
+
3
+ Log in to a Higrowth workspace from your terminal and install the
4
+ Claude Code skills that drive it. The replacement for `cp -r skills/
5
+ higrowth ~/.claude/skills/` and copy-paste tokens.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g @higrowth/cli
11
+ # or, for the curl-piped flow:
12
+ curl -fsSL https://hg-engine.app/install.sh | sh
13
+ ```
14
+
15
+ ## Quick start
16
+
17
+ ```bash
18
+ # 60-second browser-auth handshake. Opens the approval page; you click
19
+ # Approve; token lands in ~/.claude/mcp.json.
20
+ higrowth login
21
+
22
+ # Install the five Claude Code skills (workspace-setup, populate-kb,
23
+ # marketing-strategy, apply-work-orders, login).
24
+ higrowth skills install
25
+
26
+ # Restart Claude Code. /mcp should now list higrowth.
27
+ ```
28
+
29
+ ## Commands
30
+
31
+ | Command | What it does |
32
+ |---------|--------------|
33
+ | `higrowth login [--host URL] [--device]` | Browser-auth handshake (PKCE by default; `--device` for SSH/containers). Writes the MCP entry into `~/.claude/mcp.json`. |
34
+ | `higrowth whoami` | Show the currently-configured workspace + token name. |
35
+ | `higrowth logout` | Remove the higrowth entry from `~/.claude/mcp.json`. Local-only — does NOT revoke the token (do that in Settings). |
36
+ | `higrowth skills install [--target TARGET]` | Copy the five skill files into your agent's skills dir. Auto-detects Claude Code, Cursor, Claude Desktop; pass `--target claude\|cursor\|claude-desktop\|all` to override. |
37
+ | `higrowth skills list` | Show installed skills + which are out of date relative to the bundle. |
38
+ | `higrowth skills update` | Re-install (overwrite) the latest skill versions. |
39
+
40
+ ## Flags
41
+
42
+ | Flag | Default | What it does |
43
+ |------|---------|--------------|
44
+ | `--host URL` | `https://hg-engine.app` | Override the Higrowth host (for self-hosted or staging). |
45
+ | `--device` | off | Use OAuth 2.0 device-code flow instead of PKCE. For SSH / containers. |
46
+ | `--target TARGET` | auto-detect | `claude` / `cursor` / `claude-desktop` / `all`. |
47
+ | `--config PATH` | platform default | Override the MCP config file path. |
48
+
49
+ ## What it does NOT do
50
+
51
+ - Doesn't manage tokens server-side. Token rotation / revocation
52
+ happens in the Higrowth UI under Settings → API tokens.
53
+ - Doesn't restart Claude Code for you. After `login` or `skills
54
+ install`, restart manually so the new config takes effect.
55
+ - Doesn't store secrets outside `~/.claude/mcp.json` (mode 600 where
56
+ possible).
57
+
58
+ ## License
59
+
60
+ Apache-2.0
package/dist/index.js ADDED
@@ -0,0 +1,1285 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/commands/login.ts
4
+ import { spawn } from "child_process";
5
+ import { hostname, platform as platform2 } from "os";
6
+
7
+ // src/lib/api.ts
8
+ var ApiError = class extends Error {
9
+ constructor(httpStatus, message) {
10
+ super(message);
11
+ this.httpStatus = httpStatus;
12
+ this.name = "ApiError";
13
+ }
14
+ httpStatus;
15
+ };
16
+ var AuthApi = class {
17
+ constructor(host) {
18
+ this.host = host;
19
+ }
20
+ host;
21
+ initPkce(input) {
22
+ return this.post("/api/auth/cli/init", {
23
+ ...input,
24
+ codeChallengeMethod: "S256"
25
+ });
26
+ }
27
+ exchangePkce(input) {
28
+ return this.post("/api/auth/cli/exchange", input);
29
+ }
30
+ initDevice(input) {
31
+ return this.post("/api/auth/device/code", input);
32
+ }
33
+ pollDevice(deviceCode) {
34
+ return this.post("/api/auth/device/token", {
35
+ deviceCode
36
+ });
37
+ }
38
+ async post(path, body) {
39
+ const res = await fetch(`${this.host}${path}`, {
40
+ method: "POST",
41
+ headers: { "Content-Type": "application/json" },
42
+ body: JSON.stringify(body)
43
+ });
44
+ if (!res.ok) {
45
+ const text = await res.text();
46
+ let message = `HTTP ${res.status} ${path}`;
47
+ try {
48
+ const parsed = JSON.parse(text);
49
+ if (parsed.error) message = parsed.error;
50
+ } catch {
51
+ if (text) message = text.slice(0, 300);
52
+ }
53
+ throw new ApiError(res.status, message);
54
+ }
55
+ return await res.json();
56
+ }
57
+ };
58
+
59
+ // src/lib/pkce.ts
60
+ import { randomBytes, createHash } from "crypto";
61
+ var VERIFIER_BYTES = 32;
62
+ function generatePkce() {
63
+ const verifier = base64UrlEncode(randomBytes(VERIFIER_BYTES));
64
+ const challenge = base64UrlEncode(
65
+ createHash("sha256").update(verifier).digest()
66
+ );
67
+ return { verifier, challenge };
68
+ }
69
+ function base64UrlEncode(buf) {
70
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
71
+ }
72
+
73
+ // src/lib/mcp-config.ts
74
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from "fs";
75
+ import { dirname, join } from "path";
76
+ import { homedir, platform } from "os";
77
+ function configPaths() {
78
+ const home = homedir();
79
+ return [
80
+ { target: "claude", path: join(home, ".claude", "mcp.json") },
81
+ { target: "cursor", path: join(home, ".cursor", "mcp.json") },
82
+ { target: "claude-desktop", path: claudeDesktopPath(home) }
83
+ ];
84
+ }
85
+ function claudeDesktopPath(home) {
86
+ const p = platform();
87
+ if (p === "darwin") {
88
+ return join(
89
+ home,
90
+ "Library",
91
+ "Application Support",
92
+ "Claude",
93
+ "claude_desktop_config.json"
94
+ );
95
+ }
96
+ if (p === "win32") {
97
+ return join(
98
+ process.env.APPDATA ?? join(home, "AppData", "Roaming"),
99
+ "Claude",
100
+ "claude_desktop_config.json"
101
+ );
102
+ }
103
+ return join(
104
+ process.env.XDG_CONFIG_HOME ?? join(home, ".config"),
105
+ "Claude",
106
+ "claude_desktop_config.json"
107
+ );
108
+ }
109
+ function detectExistingTarget() {
110
+ for (const { target, path } of configPaths()) {
111
+ if (existsSync(path)) return target;
112
+ }
113
+ return "claude";
114
+ }
115
+ function resolveConfigPath(target) {
116
+ const resolved = target === "auto" ? detectExistingTarget() : target;
117
+ const found = configPaths().find((c) => c.target === resolved);
118
+ if (!found) throw new Error(`Unknown target: ${resolved}`);
119
+ return found;
120
+ }
121
+ function readConfig(path) {
122
+ if (!existsSync(path)) return {};
123
+ try {
124
+ const raw = readFileSync(path, "utf-8");
125
+ return JSON.parse(raw);
126
+ } catch (err) {
127
+ throw new Error(
128
+ `Could not parse existing MCP config at ${path}: ${err instanceof Error ? err.message : String(err)}`
129
+ );
130
+ }
131
+ }
132
+ function writeHigrowthEntry(input) {
133
+ const existing = readConfig(input.path);
134
+ const next = {
135
+ ...existing,
136
+ mcpServers: {
137
+ ...existing.mcpServers ?? {},
138
+ higrowth: {
139
+ url: `${input.host}/api/mcp`,
140
+ headers: {
141
+ Authorization: `Bearer ${input.token}`
142
+ }
143
+ }
144
+ }
145
+ };
146
+ mkdirSync(dirname(input.path), { recursive: true });
147
+ writeFileSync(input.path, `${JSON.stringify(next, null, 2)}
148
+ `, "utf-8");
149
+ tryChmod600(input.path);
150
+ }
151
+ function removeHigrowthEntry(path) {
152
+ if (!existsSync(path)) return false;
153
+ const existing = readConfig(path);
154
+ if (!existing.mcpServers || !existing.mcpServers.higrowth) return false;
155
+ const { higrowth: _, ...rest } = existing.mcpServers;
156
+ const next = { ...existing, mcpServers: rest };
157
+ if (Object.keys(rest).length === 0) {
158
+ delete next.mcpServers;
159
+ }
160
+ writeFileSync(path, `${JSON.stringify(next, null, 2)}
161
+ `, "utf-8");
162
+ return true;
163
+ }
164
+ function readHigrowthEntry(path) {
165
+ if (!existsSync(path)) return null;
166
+ const cfg = readConfig(path);
167
+ const entry = cfg.mcpServers?.higrowth;
168
+ if (!entry?.url) return null;
169
+ const authHeader = entry.headers?.Authorization ?? "";
170
+ const match = authHeader.match(/^Bearer\s+(\S+)$/);
171
+ if (!match) return null;
172
+ return { url: entry.url, token: match[1] };
173
+ }
174
+ function tryChmod600(path) {
175
+ if (platform() === "win32") return;
176
+ try {
177
+ chmodSync(path, 384);
178
+ } catch {
179
+ }
180
+ }
181
+
182
+ // src/commands/login.ts
183
+ var POLL_INTERVAL_MS = 2e3;
184
+ var POLL_DEADLINE_MS = 10 * 6e4;
185
+ async function loginCommand(opts) {
186
+ const host = opts.host.replace(/\/+$/, "");
187
+ const api = new AuthApi(host);
188
+ const client = {
189
+ clientName: "higrowth-cli",
190
+ clientHostname: tryHostname()
191
+ };
192
+ let token;
193
+ let organizationId;
194
+ if (opts.device) {
195
+ ({ token, organizationId } = await runDeviceFlow(api, client));
196
+ } else {
197
+ ({ token, organizationId } = await runPkceFlow(api, client));
198
+ }
199
+ const cfg = opts.configOverride !== void 0 ? { target: opts.target === "auto" ? "claude" : opts.target, path: opts.configOverride } : resolveConfigPath(opts.target);
200
+ writeHigrowthEntry({ path: cfg.path, host, token });
201
+ process.stdout.write(`
202
+ \u2713 Logged in to ${host}
203
+ `);
204
+ process.stdout.write(` workspace: ${organizationId}
205
+ `);
206
+ process.stdout.write(` config: ${cfg.path}
207
+ `);
208
+ process.stdout.write(` target: ${cfg.target}
209
+
210
+ `);
211
+ process.stdout.write(
212
+ "Restart your agent so the MCP connection picks up the new token.\n"
213
+ );
214
+ process.stdout.write(
215
+ "Next: higrowth skills install (drops the 4 playbook SKILL.md files into your agent's skills dir)\n"
216
+ );
217
+ }
218
+ async function runPkceFlow(api, client) {
219
+ const { verifier, challenge } = generatePkce();
220
+ const init = await api.initPkce({ ...client, codeChallenge: challenge });
221
+ process.stdout.write(
222
+ `
223
+ Opening the approval page in your browser\u2026
224
+ \u2192 ${init.approvalUrl}
225
+
226
+ `
227
+ );
228
+ openInBrowser(init.approvalUrl);
229
+ process.stdout.write("Waiting for approval (Ctrl-C to cancel)\u2026\n");
230
+ const started = Date.now();
231
+ while (Date.now() - started < POLL_DEADLINE_MS) {
232
+ try {
233
+ const result = await api.exchangePkce({
234
+ flowId: init.flowId,
235
+ codeVerifier: verifier
236
+ });
237
+ return { token: result.token, organizationId: result.organizationId };
238
+ } catch (err) {
239
+ if (err instanceof ApiError) {
240
+ await sleep(POLL_INTERVAL_MS);
241
+ continue;
242
+ }
243
+ throw err;
244
+ }
245
+ }
246
+ throw new Error(
247
+ "Login timed out. Approval wasn't received within 10 minutes."
248
+ );
249
+ }
250
+ async function runDeviceFlow(api, client) {
251
+ const init = await api.initDevice(client);
252
+ process.stdout.write("\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
253
+ process.stdout.write(`Open this URL on any device with a browser:
254
+
255
+ ${init.verificationUri}
256
+
257
+ `);
258
+ process.stdout.write(`Enter the code: ${init.userCode}
259
+ `);
260
+ process.stdout.write(`Or open directly: ${init.verificationUriComplete}
261
+ `);
262
+ process.stdout.write("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n");
263
+ process.stdout.write(`Polling every ${init.interval}s. Ctrl-C to cancel.
264
+ `);
265
+ const intervalMs = init.interval * 1e3;
266
+ const started = Date.now();
267
+ while (Date.now() - started < POLL_DEADLINE_MS) {
268
+ await sleep(intervalMs);
269
+ const res = await api.pollDevice(init.deviceCode);
270
+ if (res.status === "approved") {
271
+ return { token: res.token, organizationId: res.organizationId };
272
+ }
273
+ if (res.status === "denied" || res.status === "expired") {
274
+ throw new Error(`Login ${res.status}.`);
275
+ }
276
+ process.stdout.write(".");
277
+ }
278
+ process.stdout.write("\n");
279
+ throw new Error("Login timed out. Approval wasn't received within 10 minutes.");
280
+ }
281
+ function openInBrowser(url) {
282
+ const cmd = platform2() === "darwin" ? "open" : platform2() === "win32" ? "start" : "xdg-open";
283
+ const child = spawn(cmd, [url], { stdio: "ignore", detached: true });
284
+ child.on("error", () => {
285
+ process.stdout.write(
286
+ `(Couldn't auto-open the browser. Open this URL manually:
287
+ ${url}
288
+ )
289
+ `
290
+ );
291
+ });
292
+ child.unref();
293
+ }
294
+ function sleep(ms) {
295
+ return new Promise((resolve) => setTimeout(resolve, ms));
296
+ }
297
+ function tryHostname() {
298
+ try {
299
+ return hostname();
300
+ } catch {
301
+ return "unknown";
302
+ }
303
+ }
304
+
305
+ // src/commands/logout.ts
306
+ async function logoutCommand() {
307
+ let removed = 0;
308
+ for (const cfg of configPaths()) {
309
+ if (removeHigrowthEntry(cfg.path)) {
310
+ process.stdout.write(`\u2713 Removed higrowth entry from ${cfg.path}
311
+ `);
312
+ removed += 1;
313
+ }
314
+ }
315
+ if (removed === 0) {
316
+ process.stdout.write("No higrowth entries found in any agent config.\n");
317
+ return;
318
+ }
319
+ process.stdout.write(
320
+ "\nNote: this only deletes the local config entry. The token itself is still valid\nuntil you revoke it in Settings \u2192 API tokens.\n"
321
+ );
322
+ }
323
+
324
+ // src/commands/whoami.ts
325
+ async function whoamiCommand() {
326
+ let foundAny = false;
327
+ for (const cfg of configPaths()) {
328
+ const entry = readHigrowthEntry(cfg.path);
329
+ if (!entry) continue;
330
+ foundAny = true;
331
+ process.stdout.write(`${cfg.target.padEnd(15)} ${entry.url}
332
+ `);
333
+ process.stdout.write(`${"".padEnd(15)} ${maskToken(entry.token)}
334
+ `);
335
+ process.stdout.write(`${"".padEnd(15)} ${cfg.path}
336
+
337
+ `);
338
+ const summary = await probeToken(entry.url, entry.token);
339
+ if (summary) {
340
+ process.stdout.write(`${"".padEnd(15)} \u2713 ${summary}
341
+
342
+ `);
343
+ } else {
344
+ process.stdout.write(
345
+ `${"".padEnd(15)} \u2717 token did not authenticate (revoked or stale?)
346
+
347
+ `
348
+ );
349
+ }
350
+ }
351
+ if (!foundAny) {
352
+ process.stdout.write("Not logged in to any agent config.\n");
353
+ process.stdout.write("Run: higrowth login\n");
354
+ }
355
+ }
356
+ function maskToken(token) {
357
+ if (token.length < 12) return "***";
358
+ return `${token.slice(0, 8)}\u2026${token.slice(-4)}`;
359
+ }
360
+ async function probeToken(baseUrl, token) {
361
+ try {
362
+ const url = baseUrl.replace(/\/api\/mcp$/, "/api/entities");
363
+ const res = await fetch(url, {
364
+ headers: { Authorization: `Bearer ${token}` }
365
+ });
366
+ if (res.status === 401) return null;
367
+ if (!res.ok) return `connected (HTTP ${res.status} on probe)`;
368
+ const body = await res.json();
369
+ const ents = body.entities ?? [];
370
+ if (ents.length === 0) return "connected, no entities in workspace";
371
+ return `connected \u2014 ${ents.length} entit${ents.length === 1 ? "y" : "ies"}: ${ents.slice(0, 3).map((e) => e.name ?? e.domain).join(", ")}${ents.length > 3 ? "\u2026" : ""}`;
372
+ } catch {
373
+ return null;
374
+ }
375
+ }
376
+
377
+ // src/commands/skills.ts
378
+ import {
379
+ existsSync as existsSync2,
380
+ mkdirSync as mkdirSync2,
381
+ readFileSync as readFileSync2,
382
+ writeFileSync as writeFileSync2
383
+ } from "fs";
384
+ import { dirname as dirname2, join as join2 } from "path";
385
+ import { homedir as homedir2, platform as platform3 } from "os";
386
+
387
+ // src/skills/workspace-setup.ts
388
+ var WORKSPACE_SETUP = `---
389
+ name: higrowth-workspace-setup
390
+ description: Walks a Higrowth user through first-time workspace setup \u2014 adding their website, connecting Google Search Console, and kicking off the first diagnostic. Use whenever a user mentions they're new to Higrowth, just signed up, or has an empty workspace.
391
+ allowed-tools: mcp__higrowth__execute_typescript
392
+ ---
393
+
394
+ # Higrowth \u2014 Workspace setup
395
+
396
+ You are helping a new user get Higrowth productive in the next 30 minutes.
397
+ You have one MCP tool: \`execute_typescript\`. The user has a bearer token
398
+ configured; you don't need to ask for credentials.
399
+
400
+ ## When to use this skill
401
+
402
+ Trigger on: "set up Higrowth", "I just signed up", "where do I start",
403
+ "new to Higrowth", or when \`api.get('/api/entities')\` returns an empty
404
+ list.
405
+
406
+ ## The interview
407
+
408
+ Don't dump everything on the user. Walk them through one question at a time.
409
+
410
+ ### 1. Confirm the website
411
+
412
+ Ask: **"What's the primary domain you want to analyze?"** Accept things
413
+ like \`acme.com\`, \`https://acme.com\`, or \`https://www.acme.com\` \u2014 strip
414
+ the protocol + \`www.\` yourself before creating the entity.
415
+
416
+ \`\`\`ts
417
+ const ents = await api.get('/api/entities');
418
+ const existing = ents.entities.find(e => e.domain === domain);
419
+ if (existing) {
420
+ console.log('Already exists:', existing.id);
421
+ } else {
422
+ const created = await api.post('/api/entities', { domain, name });
423
+ console.log('Created entity:', created.id);
424
+ }
425
+ \`\`\`
426
+
427
+ ### 2. Discover URLs
428
+
429
+ Run discovery and report back what was found.
430
+
431
+ \`\`\`ts
432
+ const result = await api.post(\`/api/entities/\${entityId}/discover\`, {});
433
+ console.log(\`Found \${result.totalUrls} URLs across \${result.groups.length} segments\`);
434
+ \`\`\`
435
+
436
+ If \`totalUrls\` is suspiciously low (< 20 for a real site), tell the user
437
+ their sitemap may be incomplete and ask if they want to point us at a
438
+ specific path prefix.
439
+
440
+ ### 3. Connect Search Console
441
+
442
+ You can't OAuth on the user's behalf. Tell them:
443
+
444
+ > "Open Settings \u2192 Integrations and click Connect Search Console.
445
+ > Come back when it's done \u2014 I'll verify it landed."
446
+
447
+ Then poll:
448
+
449
+ \`\`\`ts
450
+ const gsc = await api.get('/api/gsc/status');
451
+ console.log(gsc.connections.length > 0 ? 'GSC connected \u2713' : 'Still waiting\u2026');
452
+ \`\`\`
453
+
454
+ Don't be pushy. If they don't want GSC right now, tell them honestly:
455
+ *"You can run the diagnostic without it, but a third of the
456
+ opportunities will go dark and outcome verification won't work. It's a
457
+ 90-second OAuth flow \u2014 strongly recommended."*
458
+
459
+ ### 4. Suggest building a minimum-viable KB BEFORE the diagnostic
460
+
461
+ Diagnostic output is much better when the KB has at least personas and
462
+ voice. Offer to walk them through it: *"Want to spend 10 minutes on
463
+ your knowledge base now so the diagnostic respects your voice and ICP?
464
+ I can ask you a few questions."*
465
+
466
+ If yes, hand off to the **populate-kb** playbook
467
+ (\`api.get('/api/mcp/playbooks/populate-kb')\`).
468
+
469
+ If no, proceed \u2014 they can populate later.
470
+
471
+ ### 5. Run the first diagnostic
472
+
473
+ \`\`\`ts
474
+ const dx = await api.post(\`/api/diagnostics\`, { entityId });
475
+ console.log('Diagnostic started:', dx.id, '\u2014 estimated 8-12 minutes');
476
+ \`\`\`
477
+
478
+ Don't sit and poll. Tell the user the diagnostic is running, give them
479
+ the diagnostic ID, and offer to walk through the report when it lands
480
+ (use the **marketing-strategy** playbook).
481
+
482
+ ## What success looks like
483
+
484
+ End the session with:
485
+ - Entity created
486
+ - URLs discovered (report the count)
487
+ - GSC connected (or explicit user decline)
488
+ - Diagnostic dispatched (report the ID + ETA)
489
+ - One concrete next step for them ("Come back in ~10 minutes and ask
490
+ me to walk you through the report")
491
+
492
+ ## What NOT to do
493
+
494
+ - Don't run the diagnostic before confirming the entity is set up correctly.
495
+ - Don't run multiple diagnostics in a row "to be safe" \u2014 one is enough.
496
+ - Don't refuse to proceed without GSC. Inform, don't gatekeep.
497
+ - Don't ask 12 questions in one message. One at a time.
498
+ `;
499
+
500
+ // src/skills/populate-kb.ts
501
+ var POPULATE_KB = `---
502
+ name: higrowth-populate-kb
503
+ description: Interviews the user about their business and populates Higrowth's Knowledge Base (personas, use cases, voice, brand facts). Use when the user asks to set up the KB, fill in personas, or wants strategy output to sound on-brand.
504
+ allowed-tools: mcp__higrowth__execute_typescript
505
+ ---
506
+
507
+ # Higrowth \u2014 Populate the KB
508
+
509
+ You are the user's content strategist for the next 15 minutes. Your job
510
+ is to extract enough structured knowledge that every downstream brief
511
+ and work order respects their voice and ICP. The KB has four layers:
512
+ **personas, use cases, voice, brand facts**.
513
+
514
+ ## When to use this skill
515
+
516
+ Trigger on: "set up the KB", "fill in personas", "what's a use case
517
+ here", "make the briefs sound like us", or when the strategic profile
518
+ returns empty.
519
+
520
+ ## Mental model
521
+
522
+ The KB is a structured config, not a creative brief. Brevity beats
523
+ prose. Three sharp personas beat ten fuzzy ones. Don't let the user
524
+ over-engineer.
525
+
526
+ \`\`\`ts
527
+ // What you're filling in. All optional; you can do this in any order.
528
+ {
529
+ personas: [{ name, occupation, stance }], // 2-5
530
+ useCases: [{ name, theme, personas: [name] }], // 3-5
531
+ voice: string, // one paragraph
532
+ brandFacts: [{ name, description, locked: bool }], // 5+
533
+ important: [string], // 10+ entities
534
+ }
535
+ \`\`\`
536
+
537
+ ## The interview \u2014 six questions
538
+
539
+ Ask each in sequence. Don't combine. Wait for the answer before moving on.
540
+
541
+ ### Q1. Who does this product matter to?
542
+
543
+ > "Who buys or uses your product? Give me the top 2-3 by name and one
544
+ > sentence each on when they buy."
545
+
546
+ Listen for: titles ("VP RevOps", "DevOps lead"), buying triggers ("losing
547
+ deals to a cheaper competitor"), business context ("after their first
548
+ incident with the legacy tool"). Bad answers ("decision makers",
549
+ "businesses 50-500"). If you get a bad answer, gently push: *"Who
550
+ specifically \u2014 name a title or a real customer?"*
551
+
552
+ Write personas as you go:
553
+
554
+ \`\`\`ts
555
+ await api.post(\`/api/entities/\${entityId}/facts\`, {
556
+ factType: 'persona',
557
+ name: 'VP RevOps',
558
+ description: 'Owns the revenue stack at mid-market SaaS (50-500 reps). Buys when their AE comp plan needs a redesign after a comp committee blow-up.',
559
+ dataJson: {
560
+ occupation: 'VP RevOps',
561
+ stance: 'Buys when ...',
562
+ journeyStages: ['consideration', 'decision'],
563
+ },
564
+ });
565
+ \`\`\`
566
+
567
+ ### Q2. What are the jobs-to-be-done?
568
+
569
+ > "Forget your product for a second \u2014 what jobs is someone hiring it to
570
+ > do? Two-word phrases, 3-5 of them."
571
+
572
+ Good: "incident triage", "post-call summary", "revenue forecasting".
573
+ Bad: "make life easier", "save time". Push back on abstractions.
574
+
575
+ \`\`\`ts
576
+ await api.post(\`/api/entities/\${entityId}/facts\`, {
577
+ factType: 'use_case',
578
+ name: 'Incident triage',
579
+ description: 'Reducing time-to-first-acknowledge for production incidents.',
580
+ dataJson: {
581
+ theme: 'incident triage',
582
+ personas: ['DevOps lead', 'SRE manager'],
583
+ naturalness: 0.9, // how organically the product can be mentioned 0..1
584
+ },
585
+ });
586
+ \`\`\`
587
+
588
+ **Important \u2014 naturalness scoring.** Ask the user: *"For this use case,
589
+ on a scale of 1-10, how naturally can you mention your product in
590
+ content about this topic without it feeling forced?"* Map their answer
591
+ to 0..1. Default 0.6 if they don't know.
592
+
593
+ ### Q3. How do you talk?
594
+
595
+ > "Pick a piece of your best marketing \u2014 a landing page, an email, a
596
+ > launch post. Paste it. I'll extract the voice."
597
+
598
+ Read the sample. Write one paragraph that captures the voice with
599
+ specifics \u2014 banned words, sentence rhythm, formality. Show it to the
600
+ user, ask: *"Does this sound right?"* Edit until they say yes.
601
+
602
+ \`\`\`ts
603
+ const voiceFact = await api.post(\`/api/entities/\${entityId}/facts\`, {
604
+ factType: 'voice',
605
+ name: 'Brand voice',
606
+ description: "We write the way we talk in a sales call \u2014 direct, ...",
607
+ dataJson: { locked: true },
608
+ });
609
+ \`\`\`
610
+
611
+ ### Q4. Brand facts they want preserved
612
+
613
+ > "Give me 5 things every page on your site must respect.
614
+ > Positioning, pricing tier, what you call your product, who you sell
615
+ > to. I'll lock these so nothing overrides them."
616
+
617
+ \`\`\`ts
618
+ for (const fact of brandFacts) {
619
+ const created = await api.post(\`/api/entities/\${entityId}/facts\`, {
620
+ factType: 'brand_fact',
621
+ name: fact.name,
622
+ description: fact.description,
623
+ });
624
+ await api.post(\`/api/facts/\${created.id}/verify\`, {}); // marks as locked
625
+ }
626
+ \`\`\`
627
+
628
+ ### Q5. Important entities
629
+
630
+ > "Quick list \u2014 competitor names, your product names, key integrations,
631
+ > technologies you talk about. 10-20 strings is plenty."
632
+
633
+ These keep the engine from accidentally treating proper nouns as
634
+ common words during topic analysis.
635
+
636
+ \`\`\`ts
637
+ await api.post(\`/api/entities/\${entityId}/facts\`, {
638
+ factType: 'entity_list',
639
+ name: 'Important entities',
640
+ description: 'Proper nouns the engine should always preserve',
641
+ dataJson: { entities: ['Stripe', 'Snowflake', 'Looker', 'Acme RevOps', ...] },
642
+ });
643
+ \`\`\`
644
+
645
+ ### Q6. ICP weighting (last; can skip on v1)
646
+
647
+ > "Of the personas we listed, which one should our content lean toward
648
+ > hardest? Score each 0-1 by share-of-voice."
649
+
650
+ Defaults are fine for v1 (all 0.5). Don't burn time on this.
651
+
652
+ ## What success looks like
653
+
654
+ Read back the summary:
655
+
656
+ \`\`\`ts
657
+ const facts = await api.get(\`/api/entities/\${entityId}/facts\`);
658
+ console.log(\`KB now has \${facts.facts.length} entries\`);
659
+ \`\`\`
660
+
661
+ End with: *"I've saved \${N} entries to your KB. From now on, every brief
662
+ and work order will read this context. If you want to refine anything,
663
+ just say which one \u2014 you don't have to redo the whole interview."*
664
+
665
+ ## What NOT to do
666
+
667
+ - Don't auto-generate personas without asking. They sound off.
668
+ - Don't lock brand facts the user hasn't explicitly confirmed.
669
+ - Don't ask all six questions in one message.
670
+ - Don't try to fill in everything in one session. v1 KB is *enough to
671
+ start*; refinement is a separate session.
672
+ - Don't argue with the user about their voice. Reflect what they tell
673
+ you; if they say "we're playful", write playful.
674
+ `;
675
+
676
+ // src/skills/marketing-strategy.ts
677
+ var MARKETING_STRATEGY = `---
678
+ name: higrowth-marketing-strategy
679
+ description: Helps the user interpret a Higrowth diagnostic, pick which pillars to prioritize, and shape the next 2-4 week sprint. Use when the user wants to talk through their report, decide what to ship, or plan.
680
+ allowed-tools: mcp__higrowth__execute_typescript
681
+ ---
682
+
683
+ # Higrowth \u2014 Marketing strategy chat
684
+
685
+ You are the user's strategy partner for the next 30 minutes. They have a
686
+ diagnostic open; your job is to help them turn 80 opportunities into a
687
+ focused 5-10-item plan.
688
+
689
+ ## When to use this skill
690
+
691
+ Trigger on: "what should I focus on", "walk me through my report",
692
+ "prioritize this", "what's the move", "plan my sprint".
693
+
694
+ ## Mental model
695
+
696
+ The diagnostic surfaces opportunities ranked by data score. Strategy
697
+ ranks them by **leverage** \u2014 which work will compound. Your job is to
698
+ push the user from "rank by score" thinking to "rank by pillar
699
+ strategy" thinking.
700
+
701
+ ## The conversation
702
+
703
+ ### Step 1 \u2014 Pull the synthesis
704
+
705
+ Don't dump opportunities. Start with the LLM-written narrative.
706
+
707
+ \`\`\`ts
708
+ const dx = await api.get(\`/api/diagnostics/\${diagnosticId}\`);
709
+ console.log(dx.synthesis);
710
+ \`\`\`
711
+
712
+ Read it. Then ask the user: *"Did that match your read of how the site
713
+ is doing right now? Anything missing?"*
714
+
715
+ Listen for context the diagnostic can't know \u2014 recent launches,
716
+ seasonal pages, things that are about to change. Note these. They
717
+ should shape priorities.
718
+
719
+ ### Step 2 \u2014 Pick ONE pillar
720
+
721
+ This is the most important step. Most users want to spray-and-pray
722
+ across all pillars. Don't let them.
723
+
724
+ \`\`\`ts
725
+ const topics = await api.get(\`/api/diagnostics/\${diagnosticId}/topics\`);
726
+ const pillars = topics.topics.flatMap(c => c.pillars);
727
+ // Rank by impressions \xD7 priority
728
+ \`\`\`
729
+
730
+ Show the top 3 pillars by impressions, with their priority tier and a
731
+ one-line gap summary. Ask: *"If we could only fix ONE topic area in the
732
+ next 2 weeks, which one would change your business the most?"*
733
+
734
+ If they pick the one with the most opportunities: good.
735
+ If they pick the one with the most strategic value: better.
736
+ If they say "all of them": push back. *"Sprint focus beats sprint
737
+ breadth. We'll get to the others \u2014 pick the one that, if it wins, you
738
+ can point to."*
739
+
740
+ ### Step 3 \u2014 Read the pillar dashboard with them
741
+
742
+ \`\`\`ts
743
+ const pillar = await api.get(\`/api/topics/\${pillarId}\`);
744
+ const dashboard = await api.get(\`/api/topics/\${pillarId}/dashboard\`);
745
+ \`\`\`
746
+
747
+ Walk them through:
748
+ - **AEO readiness** \u2014 what's missing (schema gaps, low stat density,
749
+ buried answers)
750
+ - **Sub-topic coverage** \u2014 uncovered slots = new_brief candidates
751
+ - **"Who to beat"** \u2014 the named competitor(s) winning in this pillar
752
+ - **Top 5 striking-distance opportunities** \u2014 quick wins on existing pages
753
+
754
+ For each, give your opinion. Be a strategist, not a librarian.
755
+
756
+ > "Your fresh-page % is 28 \u2014 way below where it should be. Your top
757
+ > competitor refreshes monthly. I'd start with 3 \`freshness_refresh\`
758
+ > work orders on your highest-impression pages."
759
+
760
+ ### Step 4 \u2014 Build the shortlist
761
+
762
+ Together, pick 5-10 opportunities. Sequence matters: ship cheap-and-safe
763
+ first (schema, internal links), then medium (title rewrites, freshness),
764
+ then invasive (section rewrites, new briefs). Add to a plan as you go.
765
+
766
+ \`\`\`ts
767
+ const plan = await api.post('/api/plans', {
768
+ entityId,
769
+ name: \`\${pillar.name} \u2014 sprint 1\`,
770
+ description: 'What hypothesis are we testing this sprint',
771
+ });
772
+
773
+ for (const oppKey of selectedKeys) {
774
+ await api.post(\`/api/plans/\${plan.plan.id}/candidates\`, {
775
+ opportunityKeys: [oppKey],
776
+ });
777
+ }
778
+ \`\`\`
779
+
780
+ ### Step 5 \u2014 Generate briefs + propose work orders
781
+
782
+ \`\`\`ts
783
+ await api.post(\`/api/plans/\${plan.plan.id}/generate\`);
784
+ \`\`\`
785
+
786
+ Then for each plan item, propose its work order. Tell the user the
787
+ plan is ready for review and they should approve in the work-orders
788
+ UI when they're ready.
789
+
790
+ ### Step 6 \u2014 Set expectations
791
+
792
+ End with reality:
793
+
794
+ > "Outcome verdicts take 14 days minimum because GSC needs that long
795
+ > to stabilize. Re-run the diagnostic in 2 weeks and reconcile the
796
+ > plan. Plan to ship a second sprint at the same time you read sprint
797
+ > 1's outcomes \u2014 that's the flywheel."
798
+
799
+ ## Strong opinions to bring
800
+
801
+ The user will look to you to break ties. Hold these:
802
+
803
+ - **One pillar per sprint.** Always.
804
+ - **Schema and internal links first.** Lowest risk, highest velocity.
805
+ - **Don't promise CTR lift in week 1.** The data takes 14 days.
806
+ - **Inconclusive \u2260 failed.** Many flip to verified on the next window.
807
+ - **Re-run cadence is 2 weeks.** Not 2 days. Not 2 months.
808
+ - **Plans have a defined end.** Archive after 4-6 weeks, even if
809
+ unfinished \u2014 open-ended plans become graveyards.
810
+
811
+ ## What success looks like
812
+
813
+ - One pillar picked
814
+ - One plan created
815
+ - 5-10 candidates added
816
+ - Briefs generated
817
+ - Work orders proposed for at least 3 of them
818
+ - User knows when to come back (14 days for outcomes, ~immediately for
819
+ approvals)
820
+
821
+ ## What NOT to do
822
+
823
+ - Don't sort opportunities by score and list the top 10. That's not
824
+ strategy.
825
+ - Don't tell the user "it depends" when they ask what to ship first.
826
+ Have an opinion.
827
+ - Don't promise outcomes you can't deliver. The engine measures wins
828
+ honestly; you should too.
829
+ - Don't gloss over the KB. If their KB is empty, stop and run the
830
+ populate-kb playbook first. Strategy without KB context is generic.
831
+ `;
832
+
833
+ // src/skills/apply-work-orders.ts
834
+ var APPLY_WORK_ORDERS = `---
835
+ name: higrowth-apply-work-orders
836
+ description: Picks up dispatched Higrowth work orders, applies them to the user's project (repo or CMS), and reports back. Use when the user says "apply the next work order", "ship the dispatched work", or points you at a project directory like projects/phoenix.
837
+ allowed-tools: mcp__higrowth__execute_typescript, Read, Edit, Write, Bash, Grep, Glob
838
+ ---
839
+
840
+ # Higrowth \u2014 Apply work orders
841
+
842
+ You are the user's executor for dispatched Higrowth work orders.
843
+ Your job: fetch each bundle, make the edit in their repo or CMS,
844
+ report the result. Higrowth verifies; you don't.
845
+
846
+ ## When to use this skill
847
+
848
+ Trigger on: "apply the next work order", "ship dispatched work",
849
+ "run the agent against \`projects/X\`", or when the user opens a
850
+ project directory and asks you to do their backlog.
851
+
852
+ ## The loop, per work order
853
+
854
+ Five steps. Don't skip any.
855
+
856
+ ### 1. Fetch the dispatched work orders
857
+
858
+ \`\`\`ts
859
+ const wos = await api.get('/api/work-orders', {
860
+ entityId: '...',
861
+ status: 'dispatched',
862
+ });
863
+ console.log(\`\${wos.length} dispatched work orders\`);
864
+ \`\`\`
865
+
866
+ If there are 0, stop and tell the user. Don't pretend to work.
867
+
868
+ ### 2. Pick one and fetch the bundle
869
+
870
+ For each work order:
871
+
872
+ \`\`\`ts
873
+ const bundle = await api.get(\`/api/work-orders/\${wo.id}\`);
874
+ \`\`\`
875
+
876
+ Read the **whole bundle**:
877
+ - \`targetUrl\` \u2014 which page
878
+ - \`kbSnapshot\` \u2014 the frozen voice + ICP context to respect
879
+ - \`items\` \u2014 each one's \`kind\`, \`intent\`, \`proposedValue\`, \`grounding\`, \`acceptance\`
880
+ - \`beforeSnapshot\` \u2014 what the page looked like at dispatch (title + meta)
881
+
882
+ If anything in the bundle conflicts with what you see in the user's
883
+ repo (e.g. the file doesn't exist anymore, the page URL doesn't map
884
+ to a file), STOP. Report a \`skipped\` item with the reason. Don't
885
+ guess.
886
+
887
+ ### 3. Apply the items
888
+
889
+ For each item, do the kind-specific edit. Use the user's filesystem
890
+ tools (Read/Edit/Write) or CMS API (if the user has provided
891
+ credentials in a config file).
892
+
893
+ | Kind | What to do |
894
+ |------|------------|
895
+ | add_internal_link | Find the right paragraph; insert the link with the proposed anchor text |
896
+ | title_meta_rewrite | Update frontmatter (\`title\`, \`description\`) to proposedValue verbatim |
897
+ | section_refresh | Rewrite the named section to match intent.summary, within intent.constraints |
898
+ | add_schema | Inject the JSON-LD \`<script type="application/ld+json">\` block from proposedValue.scriptBlock |
899
+ | add_stat_or_quote | Add the proposed stat with its proposed source citation |
900
+ | answer_first_restructure | Move the lead so the first 100 words contain the direct answer; preserve existing copy below |
901
+ | freshness_refresh | Update "last updated" date in frontmatter; refresh stats/examples per intent |
902
+ | expand_subtopic_coverage | Add a new H2 section covering proposedValue.subtopic |
903
+ | entity_coverage | Add explicit mentions of proposedValue.entities, with one-sentence context each |
904
+ | new_brief | Create a new file at proposedValue.targetPath. Outline \u2192 flesh out per KB voice |
905
+ | consolidate | Add 301 redirect at proposedValue.sourceUrl \u2192 proposedValue.canonicalUrl. Append the loser's content into the winner |
906
+ | seed_reddit_quora | Stop. This isn't a repo task. Tell the user this WO is for them to handle manually |
907
+ | claim_review_profile | Same \u2014 manual, can't be automated |
908
+
909
+ **Respect the KB snapshot.** If the voice paragraph says "no
910
+ exclamation marks", don't add any. If brandFacts say the product is
911
+ called "X" not "Y", use "X".
912
+
913
+ ### 4. Commit (repo projects only)
914
+
915
+ One branch + one commit per work order:
916
+
917
+ \`\`\`bash
918
+ git checkout -b hg/wo-\${shortId}
919
+ git add <changed files>
920
+ git commit -m "hg: apply work order \${woId}
921
+
922
+ \${one-line per item}
923
+
924
+ Higrowth-WorkOrder: \${woId}
925
+ Higrowth-Items: \${kinds joined by ', '}"
926
+
927
+ git push -u origin hg/wo-\${shortId}
928
+ \`\`\`
929
+
930
+ If \`gh\` CLI is available, open a PR; otherwise just print the
931
+ \`git push\` URL.
932
+
933
+ ### 5. Report each item
934
+
935
+ \`\`\`ts
936
+ for (const item of bundle.items) {
937
+ await api.post(
938
+ \`/api/work-orders/\${bundle.id}/items/\${item.id}/result\`,
939
+ {
940
+ status: 'applied', // or 'skipped' / 'failed'
941
+ appliedReport: {
942
+ whatChanged: '...concrete sentence...',
943
+ urls: ['https://site.com/page'],
944
+ anchors: item.kind === 'add_internal_link' ? [theAnchorText] : undefined,
945
+ notes: \`Branch hg/wo-\${shortId}\${prNumber ? ', PR #' + prNumber : ''}\`,
946
+ },
947
+ },
948
+ );
949
+ }
950
+ \`\`\`
951
+
952
+ **\`whatChanged\` matters.** "Done." is not a useful report. Six months
953
+ from now the user will read it and not remember what happened. Spend
954
+ the extra 10 seconds.
955
+
956
+ ## Status codes
957
+
958
+ | Status | When |
959
+ |--------|------|
960
+ | \`applied\` | You made the edit and committed it |
961
+ | \`skipped\` | The edit doesn't make sense for this codebase (file missing, redundant change, page deleted). Always include \`skipReason\` |
962
+ | \`failed\` | You tried and it didn't work (test failure, build error, missing creds) |
963
+
964
+ Never report \`verified\` \u2014 that's Higrowth's job, not yours.
965
+
966
+ ## What success looks like
967
+
968
+ End the session with:
969
+
970
+ \`\`\`ts
971
+ const summary = bundleResults.map(b => ({
972
+ workOrder: b.id,
973
+ applied: b.items.filter(i => i.status === 'applied').length,
974
+ skipped: b.items.filter(i => i.status === 'skipped').length,
975
+ failed: b.items.filter(i => i.status === 'failed').length,
976
+ branch: \`hg/wo-\${b.shortId}\`,
977
+ }));
978
+ console.log(summary);
979
+ \`\`\`
980
+
981
+ Tell the user: *"Applied \${N} work orders. Branches pushed; review and
982
+ merge when ready. Mechanical verification runs automatically \u2014 you'll
983
+ see verdicts on the work-orders dashboard."*
984
+
985
+ ## What NOT to do
986
+
987
+ - Don't apply across multiple work orders without committing per WO.
988
+ Merge conflicts will eat you.
989
+ - Don't push to \`main\` directly. Always a \`hg/wo-\u2026\` branch.
990
+ - Don't edit pages outside the bundle's scope. If the agent finds a
991
+ typo on a different page, mention it but don't fix it inline \u2014 it
992
+ poisons outcome verification.
993
+ - Don't report \`applied\` if the change didn't actually happen.
994
+ - Don't run two of yourself in parallel against the same repo.
995
+ `;
996
+
997
+ // src/skills/index.ts
998
+ var SKILLS = [
999
+ {
1000
+ slug: "higrowth-workspace-setup",
1001
+ title: "First-time workspace setup",
1002
+ description: "Add the website, connect GSC, run the first diagnostic.",
1003
+ body: WORKSPACE_SETUP
1004
+ },
1005
+ {
1006
+ slug: "higrowth-populate-kb",
1007
+ title: "Populate the Knowledge Base",
1008
+ description: "Interview-driven KB population \u2014 personas, voice, brand facts.",
1009
+ body: POPULATE_KB
1010
+ },
1011
+ {
1012
+ slug: "higrowth-marketing-strategy",
1013
+ title: "Marketing strategy chat",
1014
+ description: "Walk a diagnostic, pick a pillar, build the sprint plan.",
1015
+ body: MARKETING_STRATEGY
1016
+ },
1017
+ {
1018
+ slug: "higrowth-apply-work-orders",
1019
+ title: "Apply dispatched work orders",
1020
+ description: "Fetch bundles, edit the repo or CMS, commit, post results.",
1021
+ body: APPLY_WORK_ORDERS
1022
+ }
1023
+ ];
1024
+
1025
+ // src/commands/skills.ts
1026
+ async function skillsInstallCommand(opts) {
1027
+ const targets = resolveTargets(opts.target);
1028
+ if (targets.length === 0) {
1029
+ process.stdout.write(
1030
+ "No supported skills directory detected. Pass --target claude|cursor|claude-desktop|all.\n"
1031
+ );
1032
+ process.exitCode = 1;
1033
+ return;
1034
+ }
1035
+ let totalInstalled = 0;
1036
+ let totalUpdated = 0;
1037
+ for (const t of targets) {
1038
+ const dir = skillsDir(t);
1039
+ process.stdout.write(`
1040
+ \u2192 ${t} (${dir})
1041
+ `);
1042
+ for (const skill of SKILLS) {
1043
+ const target = join2(dir, skill.slug, "SKILL.md");
1044
+ const existing = existsSync2(target) ? readFileSync2(target, "utf-8") : null;
1045
+ if (existing === skill.body) {
1046
+ process.stdout.write(` = ${skill.slug} (up to date)
1047
+ `);
1048
+ continue;
1049
+ }
1050
+ mkdirSync2(dirname2(target), { recursive: true });
1051
+ writeFileSync2(target, skill.body, "utf-8");
1052
+ if (existing === null) {
1053
+ process.stdout.write(` + ${skill.slug} (installed)
1054
+ `);
1055
+ totalInstalled += 1;
1056
+ } else {
1057
+ process.stdout.write(` \u21BB ${skill.slug} (updated)
1058
+ `);
1059
+ totalUpdated += 1;
1060
+ }
1061
+ }
1062
+ }
1063
+ process.stdout.write(
1064
+ `
1065
+ Done. ${totalInstalled} installed, ${totalUpdated} updated.
1066
+ Restart your agent so the new skills are picked up.
1067
+ `
1068
+ );
1069
+ }
1070
+ async function skillsListCommand(opts) {
1071
+ const targets = resolveTargets(opts.target);
1072
+ for (const t of targets) {
1073
+ const dir = skillsDir(t);
1074
+ process.stdout.write(`
1075
+ ${t} (${dir})
1076
+ `);
1077
+ for (const skill of SKILLS) {
1078
+ const target = join2(dir, skill.slug, "SKILL.md");
1079
+ const state = compareSkill(target, skill);
1080
+ process.stdout.write(` [${state}] ${skill.slug.padEnd(32)} ${skill.title}
1081
+ `);
1082
+ }
1083
+ }
1084
+ }
1085
+ async function skillsUpdateCommand(opts) {
1086
+ await skillsInstallCommand(opts);
1087
+ }
1088
+ function resolveTargets(t) {
1089
+ const all = [
1090
+ "claude",
1091
+ "cursor",
1092
+ "claude-desktop"
1093
+ ];
1094
+ if (t === "all") return all;
1095
+ if (t === "auto") {
1096
+ const present = all.filter((x) => existsSync2(skillsDir(x)));
1097
+ return present.length > 0 ? present : ["claude"];
1098
+ }
1099
+ return [t];
1100
+ }
1101
+ function skillsDir(t) {
1102
+ const home = homedir2();
1103
+ if (t === "claude") return join2(home, ".claude", "skills");
1104
+ if (t === "cursor") return join2(home, ".cursor", "skills");
1105
+ if (platform3() === "darwin") {
1106
+ return join2(home, "Library", "Application Support", "Claude", "skills");
1107
+ }
1108
+ if (platform3() === "win32") {
1109
+ return join2(
1110
+ process.env.APPDATA ?? join2(home, "AppData", "Roaming"),
1111
+ "Claude",
1112
+ "skills"
1113
+ );
1114
+ }
1115
+ return join2(
1116
+ process.env.XDG_CONFIG_HOME ?? join2(home, ".config"),
1117
+ "Claude",
1118
+ "skills"
1119
+ );
1120
+ }
1121
+ function compareSkill(path, skill) {
1122
+ if (!existsSync2(path)) return " missing ";
1123
+ try {
1124
+ const existing = readFileSync2(path, "utf-8");
1125
+ return existing === skill.body ? "installed" : " stale ";
1126
+ } catch {
1127
+ return " unread ";
1128
+ }
1129
+ }
1130
+
1131
+ // src/index.ts
1132
+ var VERSION = "0.1.0";
1133
+ var DEFAULT_HOST = "https://hg-engine.app";
1134
+ var USAGE = `higrowth \u2014 Connect your agent to a Higrowth workspace
1135
+
1136
+ USAGE
1137
+ higrowth <command> [flags]
1138
+
1139
+ COMMANDS
1140
+ login [--host URL] [--device] [--target TARGET]
1141
+ Browser-auth handshake. Writes the higrowth MCP entry into your
1142
+ agent's config (~/.claude/mcp.json by default). Pass --device for
1143
+ OAuth 2.0 device-code flow (SSH / no-browser environments).
1144
+
1145
+ whoami
1146
+ Show the currently-configured higrowth MCP entry across all
1147
+ detected agents, plus a live token-validity probe.
1148
+
1149
+ logout
1150
+ Remove the higrowth entry from every agent config. Does NOT
1151
+ revoke the token server-side \u2014 do that in Settings \u2192 API tokens.
1152
+
1153
+ skills install [--target TARGET]
1154
+ Install the four Higrowth playbook skill files into your agent's
1155
+ skills dir.
1156
+
1157
+ skills list [--target TARGET]
1158
+ Show which skills are installed and whether they're current.
1159
+
1160
+ skills update [--target TARGET]
1161
+ Re-install the bundled skill versions.
1162
+
1163
+ version
1164
+ Print version + exit.
1165
+
1166
+ FLAGS
1167
+ --host URL Override Higrowth host (default: ${DEFAULT_HOST}).
1168
+ --device Use OAuth device-code flow instead of PKCE.
1169
+ --target TARGET One of: auto (default) | claude | cursor |
1170
+ claude-desktop | all.
1171
+ --config PATH Override the MCP config file path.
1172
+
1173
+ EXAMPLES
1174
+ higrowth login
1175
+ higrowth login --device
1176
+ higrowth login --host https://staging.hg-engine.app
1177
+ higrowth skills install --target all
1178
+ higrowth whoami
1179
+ `;
1180
+ function parseArgs(argv) {
1181
+ const flags = {};
1182
+ const positional = [];
1183
+ for (let i = 0; i < argv.length; i++) {
1184
+ const a = argv[i];
1185
+ if (a.startsWith("--")) {
1186
+ const key = a.slice(2);
1187
+ const next = argv[i + 1];
1188
+ if (next !== void 0 && !next.startsWith("--")) {
1189
+ flags[key] = next;
1190
+ i += 1;
1191
+ } else {
1192
+ flags[key] = true;
1193
+ }
1194
+ } else if (a.startsWith("-")) {
1195
+ flags[a.slice(1)] = true;
1196
+ } else {
1197
+ positional.push(a);
1198
+ }
1199
+ }
1200
+ return {
1201
+ command: positional[0] ?? "",
1202
+ subcommand: positional[1],
1203
+ flags
1204
+ };
1205
+ }
1206
+ function resolveTarget(raw) {
1207
+ if (raw === void 0) return "auto";
1208
+ if (raw === true) return "auto";
1209
+ const s = String(raw).toLowerCase();
1210
+ if (s === "auto" || s === "claude" || s === "cursor" || s === "claude-desktop" || s === "all") {
1211
+ return s;
1212
+ }
1213
+ throw new Error(
1214
+ `Unknown target "${s}". Use auto | claude | cursor | claude-desktop | all.`
1215
+ );
1216
+ }
1217
+ async function main() {
1218
+ const args = parseArgs(process.argv.slice(2));
1219
+ const host = typeof args.flags.host === "string" ? args.flags.host : DEFAULT_HOST;
1220
+ const target = resolveTarget(args.flags.target);
1221
+ const device = args.flags.device === true;
1222
+ const configOverride = typeof args.flags.config === "string" ? args.flags.config : void 0;
1223
+ switch (args.command) {
1224
+ case "":
1225
+ case "help":
1226
+ case "--help":
1227
+ case "-h":
1228
+ process.stdout.write(USAGE);
1229
+ return;
1230
+ case "version":
1231
+ case "--version":
1232
+ case "-v":
1233
+ process.stdout.write(`higrowth ${VERSION}
1234
+ `);
1235
+ return;
1236
+ case "login":
1237
+ if (target === "all") {
1238
+ process.stderr.write(
1239
+ "login writes to a single config file. Use --target claude | cursor | claude-desktop (or omit for auto-detect). To install skills across all agents, use `higrowth skills install --target all` after login.\n"
1240
+ );
1241
+ process.exitCode = 2;
1242
+ return;
1243
+ }
1244
+ await loginCommand({ host, device, target, configOverride });
1245
+ return;
1246
+ case "whoami":
1247
+ await whoamiCommand();
1248
+ return;
1249
+ case "logout":
1250
+ await logoutCommand();
1251
+ return;
1252
+ case "skills":
1253
+ if (args.subcommand === "install" || args.subcommand === void 0) {
1254
+ await skillsInstallCommand({ target });
1255
+ return;
1256
+ }
1257
+ if (args.subcommand === "list") {
1258
+ await skillsListCommand({ target });
1259
+ return;
1260
+ }
1261
+ if (args.subcommand === "update") {
1262
+ await skillsUpdateCommand({ target });
1263
+ return;
1264
+ }
1265
+ process.stderr.write(
1266
+ `Unknown skills subcommand: ${args.subcommand}
1267
+ Use: higrowth skills install | list | update
1268
+ `
1269
+ );
1270
+ process.exitCode = 2;
1271
+ return;
1272
+ default:
1273
+ process.stderr.write(`Unknown command: ${args.command}
1274
+
1275
+ ${USAGE}`);
1276
+ process.exitCode = 2;
1277
+ return;
1278
+ }
1279
+ }
1280
+ main().catch((err) => {
1281
+ const msg = err instanceof Error ? err.message : String(err);
1282
+ process.stderr.write(`\u2717 ${msg}
1283
+ `);
1284
+ process.exitCode = 1;
1285
+ });
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@higrowth/cli",
3
+ "version": "0.1.0",
4
+ "description": "Higrowth CLI — log in via browser, install Claude Code skills, manage the MCP connection.",
5
+ "type": "module",
6
+ "license": "Apache-2.0",
7
+ "bin": {
8
+ "higrowth": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist/",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsup",
16
+ "dev": "tsup --watch",
17
+ "check": "tsc --noEmit"
18
+ },
19
+ "engines": {
20
+ "node": ">=20"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^22.10.5",
27
+ "tsup": "^8.5.0",
28
+ "typescript": "^5.6.3"
29
+ },
30
+ "keywords": ["higrowth", "seo", "aeo", "mcp", "claude-code", "agent"],
31
+ "homepage": "https://github.com/higrowth-ai/hg-engine",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/higrowth-ai/hg-engine.git",
35
+ "directory": "cli"
36
+ }
37
+ }