@webgrow/skillhub 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.
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
6
+ "allowJs": true,
7
+ "checkJs": false,
8
+ "jsx": "react-jsx",
9
+ "module": "ESNext",
10
+ "moduleResolution": "Bundler",
11
+ "resolveJsonModule": true,
12
+ "isolatedModules": true,
13
+ "noEmit": true,
14
+ "strict": false
15
+ },
16
+ "include": ["src"]
17
+ }
@@ -0,0 +1,23 @@
1
+ import react from "@vitejs/plugin-react";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { defineConfig } from "vite";
5
+
6
+ const root = fileURLToPath(new URL(".", import.meta.url));
7
+
8
+ export default defineConfig({
9
+ root,
10
+ plugins: [react()],
11
+ server: {
12
+ host: "127.0.0.1",
13
+ port: 5174,
14
+ },
15
+ preview: {
16
+ host: "127.0.0.1",
17
+ port: 4175,
18
+ },
19
+ build: {
20
+ outDir: path.resolve(root, "../dist/dashboard"),
21
+ emptyOutDir: true,
22
+ },
23
+ });
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@webgrow/skillhub",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "files": [
9
+ "bridge",
10
+ "!bridge/.env.local",
11
+ "convex",
12
+ "dashboard",
13
+ "!dashboard/.env.local",
14
+ "src",
15
+ "README.md"
16
+ ],
17
+ "bin": {
18
+ "skillhub": "src/cli.mjs"
19
+ },
20
+ "scripts": {
21
+ "cli": "node src/cli.mjs",
22
+ "connect": "node src/cli.mjs connect",
23
+ "bridge:start": "node src/cli.mjs bridge start",
24
+ "status": "node src/cli.mjs status",
25
+ "dashboard:dev": "vite --config dashboard/vite.config.mjs",
26
+ "dashboard:build": "vite build --config dashboard/vite.config.mjs",
27
+ "dashboard:preview": "vite preview --config dashboard/vite.config.mjs",
28
+ "harness": "node bridge/harness.mjs",
29
+ "harness:doctor": "node bridge/doctor.mjs",
30
+ "harness:once": "node bridge/harness.mjs --once",
31
+ "options": "node src/cloud-catalog.mjs list",
32
+ "choose": "node src/cloud-catalog.mjs run",
33
+ "convex:dev": "convex dev",
34
+ "convex:codegen": "convex codegen",
35
+ "convex:deploy": "convex deploy"
36
+ },
37
+ "dependencies": {
38
+ "@convex-dev/workflow": "^0.4.4",
39
+ "@convex-dev/workpool": "^0.4.7",
40
+ "@vitejs/plugin-react": "^6.0.3",
41
+ "convex": "^1.42.0",
42
+ "dotenv": "^17.4.2",
43
+ "lucide-react": "^1.21.0",
44
+ "react": "^19.2.7",
45
+ "react-dom": "^19.2.7",
46
+ "vite": "^8.1.0"
47
+ }
48
+ }
@@ -0,0 +1,101 @@
1
+ import { spawn } from "node:child_process";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import { fileURLToPath } from "node:url";
5
+ import { createBridgeEnv, readUserConfig } from "./config.mjs";
6
+
7
+ const srcDir = path.dirname(fileURLToPath(import.meta.url));
8
+ const packageRoot = path.dirname(srcDir);
9
+ const bridgePath = path.join(packageRoot, "bridge", "harness.mjs");
10
+
11
+ export async function runBridgeCommand(argv = []) {
12
+ const [subcommand = "start", ...rest] = argv;
13
+
14
+ if (subcommand === "start") {
15
+ return startBridge(rest);
16
+ }
17
+
18
+ if (subcommand === "status") {
19
+ return showBridgeStatus();
20
+ }
21
+
22
+ if (subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
23
+ return showHelp();
24
+ }
25
+
26
+ showHelp();
27
+ process.exitCode = 1;
28
+ }
29
+
30
+ async function startBridge(argv) {
31
+ const flags = parseFlags(argv);
32
+ const config = await readUserConfig();
33
+ if (!config.convexUrl && !process.env.CONVEX_URL) {
34
+ throw new Error("Run skillhub connect first, or provide a SkillHub connection URL.");
35
+ }
36
+
37
+ const args = [bridgePath];
38
+ if (flags.once) args.push("--once");
39
+ if (flags.mode) args.push("--mode", flags.mode);
40
+
41
+ console.log("Starting SkillHub bridge...");
42
+ const child = spawn(process.execPath, args, {
43
+ cwd: packageRoot,
44
+ env: createBridgeEnv({
45
+ ...config,
46
+ bridgeMode: flags.mode || config.bridgeMode,
47
+ }),
48
+ stdio: "inherit",
49
+ windowsHide: true,
50
+ });
51
+
52
+ const code = await new Promise((resolve, reject) => {
53
+ child.on("error", reject);
54
+ child.on("close", (exitCode) => resolve(exitCode ?? 1));
55
+ });
56
+ process.exitCode = code;
57
+ }
58
+
59
+ async function showBridgeStatus() {
60
+ const config = await readUserConfig();
61
+ const pid = config.bridge?.pid;
62
+ const status = pid && isProcessRunning(pid) ? "Connected" : "Disconnected";
63
+ console.log(`Bridge: ${status}`);
64
+ if (pid) console.log(`PID: ${pid}`);
65
+ if (config.bridge?.logPath) console.log(`Log: ${config.bridge.logPath}`);
66
+ }
67
+
68
+ function parseFlags(argv) {
69
+ const flags = {
70
+ mode: "",
71
+ once: false,
72
+ };
73
+
74
+ for (let index = 0; index < argv.length; index += 1) {
75
+ const arg = argv[index];
76
+ if (arg === "--mode") flags.mode = argv[++index] ?? "";
77
+ else if (arg === "--once") flags.once = true;
78
+ else throw new Error(`Unknown argument: ${arg}`);
79
+ }
80
+
81
+ return flags;
82
+ }
83
+
84
+ function isProcessRunning(pid) {
85
+ try {
86
+ process.kill(pid, 0);
87
+ return true;
88
+ } catch {
89
+ return false;
90
+ }
91
+ }
92
+
93
+ function showHelp() {
94
+ console.log(`SkillHub bridge
95
+
96
+ Commands:
97
+ skillhub bridge start Start the local bridge worker
98
+ skillhub bridge start --once Process one queued action
99
+ skillhub bridge status Show local bridge state
100
+ `);
101
+ }
package/src/cli.mjs ADDED
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env node
2
+
3
+ import process from "node:process";
4
+ import {
5
+ getCloudCatalogItems,
6
+ getRunStatus,
7
+ listRecentRuns,
8
+ renderCatalog,
9
+ runCloudCatalogCommand,
10
+ runCloudCatalogItem,
11
+ watchRun,
12
+ } from "./cloud-catalog.mjs";
13
+ import { runBridgeCommand } from "./bridge-command.mjs";
14
+ import { runConnectCommand } from "./connect.mjs";
15
+
16
+ const [command = "help", ...argv] = process.argv.slice(2);
17
+
18
+ function help() {
19
+ console.log(`SkillHub
20
+
21
+ Commands:
22
+ connect Connect SkillHub to Codex
23
+ options Show available Convex loops and skills
24
+ choose Pick and run a loop or skill from a numbered list
25
+ run [loop|skill] [number|name] Run a loop or skill; opens picker when omitted
26
+ status [runId] Show recent run status
27
+ bridge start Start the local bridge worker
28
+ mcp serve Start the Codex MCP server
29
+ help Show this help
30
+
31
+ Examples:
32
+ skillhub connect
33
+ skillhub options
34
+ skillhub choose
35
+ skillhub run 1
36
+ skillhub status
37
+ skillhub bridge start
38
+ skillhub run skill "PR Review Assistant"
39
+ `);
40
+ }
41
+
42
+ async function main() {
43
+ if (command === "help" || command === "--help" || command === "-h") return help();
44
+ if (command === "connect") return runConnectCommand(argv);
45
+ if (command === "options") return runCloudCatalogCommand(["list", ...argv]);
46
+ if (command === "choose") return runCloudCatalogCommand(["run", ...argv]);
47
+ if (command === "run") return runCloudCatalogCommand(["run", ...argv]);
48
+ if (command === "status") return runCloudCatalogCommand(["status", ...argv]);
49
+ if (command === "bridge") return runBridgeCommand(argv);
50
+ if (command === "mcp" && argv[0] === "serve") return mcpServe();
51
+
52
+ help();
53
+ process.exitCode = 1;
54
+ }
55
+
56
+ async function mcpServe() {
57
+ process.stdin.setEncoding("utf8");
58
+ let buffer = "";
59
+
60
+ process.stdin.on("data", async (chunk) => {
61
+ buffer += chunk;
62
+ const lines = buffer.split(/\r?\n/);
63
+ buffer = lines.pop() ?? "";
64
+
65
+ for (const line of lines) {
66
+ if (!line.trim()) continue;
67
+ try {
68
+ const message = JSON.parse(line);
69
+ const response = await handleMcpMessage(message);
70
+ if (response) process.stdout.write(`${JSON.stringify(response)}\n`);
71
+ } catch (error) {
72
+ process.stdout.write(`${JSON.stringify({
73
+ jsonrpc: "2.0",
74
+ error: {
75
+ code: -32603,
76
+ message: error instanceof Error ? error.message : String(error),
77
+ },
78
+ })}\n`);
79
+ }
80
+ }
81
+ });
82
+ }
83
+
84
+ async function handleMcpMessage(message) {
85
+ const base = { jsonrpc: "2.0", id: message.id };
86
+
87
+ if (message.method === "initialize") {
88
+ return {
89
+ ...base,
90
+ result: {
91
+ protocolVersion: "2025-06-18",
92
+ serverInfo: { name: "skillhub", version: "0.1.0" },
93
+ capabilities: { tools: {} },
94
+ instructions:
95
+ "SkillHub lists and runs the user's Convex-backed loops and skills. Use skillhub_list_options before running anything so the user can choose from visible options.",
96
+ },
97
+ };
98
+ }
99
+
100
+ if (message.method === "notifications/initialized") {
101
+ return null;
102
+ }
103
+
104
+ if (message.method === "tools/list") {
105
+ return {
106
+ ...base,
107
+ result: {
108
+ tools: [
109
+ {
110
+ name: "skillhub_list_options",
111
+ description:
112
+ "List the user's available SkillHub loops and skills so Codex can show runnable choices instead of requiring exact slugs.",
113
+ inputSchema: {
114
+ type: "object",
115
+ properties: {
116
+ query: { type: "string" },
117
+ kind: { type: "string", enum: ["loop", "skill"] },
118
+ json: { type: "boolean" },
119
+ },
120
+ },
121
+ },
122
+ {
123
+ name: "skillhub_run_option",
124
+ description:
125
+ "Run a SkillHub loop or skill by visible number, slug, name, or a unique query match.",
126
+ inputSchema: {
127
+ type: "object",
128
+ properties: {
129
+ target: { type: "string" },
130
+ query: { type: "string" },
131
+ kind: { type: "string", enum: ["loop", "skill"] },
132
+ prompt: { type: "string" },
133
+ },
134
+ },
135
+ },
136
+ {
137
+ name: "skillhub_get_run_status",
138
+ description:
139
+ "Get the current status and recent details for a SkillHub run. If runId is omitted, returns the most recent runs.",
140
+ inputSchema: {
141
+ type: "object",
142
+ properties: {
143
+ runId: { type: "string" },
144
+ limit: { type: "number" },
145
+ },
146
+ },
147
+ },
148
+ {
149
+ name: "skillhub_watch_run",
150
+ description:
151
+ "Poll a SkillHub run until it completes, fails, or times out. Use after skillhub_run_option when the user wants completion status.",
152
+ inputSchema: {
153
+ type: "object",
154
+ properties: {
155
+ runId: { type: "string" },
156
+ timeoutSeconds: { type: "number" },
157
+ intervalMs: { type: "number" },
158
+ },
159
+ required: ["runId"],
160
+ },
161
+ },
162
+ {
163
+ name: "skillhub_list_recent_runs",
164
+ description: "List recent SkillHub runs with their current statuses.",
165
+ inputSchema: {
166
+ type: "object",
167
+ properties: {
168
+ limit: { type: "number" },
169
+ },
170
+ },
171
+ },
172
+ ],
173
+ },
174
+ };
175
+ }
176
+
177
+ if (message.method === "tools/call") {
178
+ const name = message.params?.name;
179
+ const args = message.params?.arguments ?? {};
180
+ const result = await callTool(name, args);
181
+
182
+ return {
183
+ ...base,
184
+ result: {
185
+ content: [{ type: "text", text: result }],
186
+ },
187
+ };
188
+ }
189
+
190
+ if (!message.id) return null;
191
+ return { ...base, error: { code: -32601, message: `Unknown method: ${message.method}` } };
192
+ }
193
+
194
+ async function callTool(name, args) {
195
+ if (name === "skillhub_list_options") {
196
+ const items = await getCloudCatalogItems({
197
+ kind: args.kind,
198
+ query: args.query,
199
+ });
200
+
201
+ return args.json === true ? JSON.stringify({ items }, null, 2) : renderCatalog(items);
202
+ }
203
+
204
+ if (name === "skillhub_run_option") {
205
+ const result = await runCloudCatalogItem({
206
+ kind: args.kind,
207
+ prompt: args.prompt,
208
+ query: args.query,
209
+ target: args.target,
210
+ });
211
+
212
+ return result.ok
213
+ ? `${result.message}\nRun id: ${result.runId}\nUse skillhub_watch_run to report completion.`
214
+ : JSON.stringify(result, null, 2);
215
+ }
216
+
217
+ if (name === "skillhub_get_run_status") {
218
+ if (args.runId) {
219
+ return JSON.stringify(await getRunStatus(args.runId), null, 2);
220
+ }
221
+ return JSON.stringify(await listRecentRuns({ limit: args.limit }), null, 2);
222
+ }
223
+
224
+ if (name === "skillhub_watch_run") {
225
+ return JSON.stringify(
226
+ await watchRun({
227
+ runId: args.runId,
228
+ timeoutSeconds: args.timeoutSeconds,
229
+ intervalMs: args.intervalMs,
230
+ }),
231
+ null,
232
+ 2,
233
+ );
234
+ }
235
+
236
+ if (name === "skillhub_list_recent_runs") {
237
+ return JSON.stringify(await listRecentRuns({ limit: args.limit }), null, 2);
238
+ }
239
+
240
+ throw new Error(`Unknown tool: ${name}`);
241
+ }
242
+
243
+ main().catch((error) => {
244
+ console.error(error instanceof Error ? error.message : String(error));
245
+ process.exitCode = 1;
246
+ });