@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.
- package/README.md +114 -0
- package/bridge/.env.example +14 -0
- package/bridge/README.md +74 -0
- package/bridge/adapters.mjs +209 -0
- package/bridge/convex-functions.mjs +13 -0
- package/bridge/doctor.mjs +448 -0
- package/bridge/harness.mjs +217 -0
- package/convex/_generated/ai/ai-files.state.json +6 -0
- package/convex/_generated/ai/guidelines.md +368 -0
- package/convex/_generated/api.d.ts +59 -0
- package/convex/_generated/api.js +23 -0
- package/convex/_generated/dataModel.d.ts +60 -0
- package/convex/_generated/server.d.ts +143 -0
- package/convex/_generated/server.js +93 -0
- package/convex/bridge.ts +709 -0
- package/convex/convex.config.ts +8 -0
- package/convex/http.ts +37 -0
- package/convex/loops.ts +546 -0
- package/convex/runs.ts +183 -0
- package/convex/schema.ts +220 -0
- package/convex/skills.ts +413 -0
- package/convex/tsconfig.json +25 -0
- package/dashboard/.env.example +1 -0
- package/dashboard/index.html +12 -0
- package/dashboard/src/App.jsx +1743 -0
- package/dashboard/src/convexRefs.js +32 -0
- package/dashboard/src/main.jsx +46 -0
- package/dashboard/src/styles.css +982 -0
- package/dashboard/tsconfig.json +17 -0
- package/dashboard/vite.config.mjs +23 -0
- package/package.json +48 -0
- package/src/bridge-command.mjs +101 -0
- package/src/cli.mjs +246 -0
- package/src/cloud-catalog.mjs +588 -0
- package/src/config.mjs +150 -0
- package/src/connect.mjs +410 -0
|
@@ -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
|
+
});
|