@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.
- package/README.md +60 -0
- package/dist/index.js +1285 -0
- 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
|
+
}
|