@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
package/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# SkillHub MVP
|
|
2
|
+
|
|
3
|
+
SkillHub is a Convex-first control plane for running Codex-backed loops and
|
|
4
|
+
skills.
|
|
5
|
+
|
|
6
|
+
The current product direction is:
|
|
7
|
+
|
|
8
|
+
- `convex/` stores skills, loops, runs, events, host actions, leases, and logs.
|
|
9
|
+
- `dashboard/` is the React/Vite realtime dashboard.
|
|
10
|
+
- `bridge/` is the worker harness that claims Convex actions and runs Codex.
|
|
11
|
+
- `src/cli.mjs` and `src/cloud-catalog.mjs` expose the small user-facing CLI
|
|
12
|
+
and MCP surface.
|
|
13
|
+
|
|
14
|
+
The old local JSON database, vanilla dashboard, and local API server prototype
|
|
15
|
+
have been removed. Useful feature ideas from that system are preserved in
|
|
16
|
+
`docs/legacy-system-feature-carryover.md`.
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
Connect SkillHub to Codex:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx @webgrow/skillhub connect
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
After connecting, open Codex and ask:
|
|
27
|
+
|
|
28
|
+
```txt
|
|
29
|
+
Show my SkillHub options.
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Run an option from Codex:
|
|
33
|
+
|
|
34
|
+
```txt
|
|
35
|
+
Run option 1.
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Start Convex:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm run convex:dev
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Start the dashboard:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm run dashboard:dev
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
List runnable options from the CLI:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm run options
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Run a loop or skill:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
node src/cli.mjs run 1
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Start the local bridge worker:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npm run bridge:start
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Check recent run status:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm run status
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Developer Diagnostics
|
|
75
|
+
|
|
76
|
+
Process one pending action through the internal harness:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npm run harness:once
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Use the doctor when setting up real Codex CLI execution:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npm run harness:doctor -- --mode codex-cli --codex-smoke
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Main Commands
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npm run connect
|
|
92
|
+
npm run bridge:start
|
|
93
|
+
npm run status
|
|
94
|
+
npm run dashboard:dev
|
|
95
|
+
npm run dashboard:build
|
|
96
|
+
npm run convex:dev
|
|
97
|
+
npm run convex:deploy
|
|
98
|
+
npm run harness
|
|
99
|
+
npm run harness:once
|
|
100
|
+
npm run harness:doctor
|
|
101
|
+
npm run options
|
|
102
|
+
npm run choose
|
|
103
|
+
node src/cli.mjs mcp serve
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Docs
|
|
107
|
+
|
|
108
|
+
- `docs/codex-user-onboarding-implementation-guide.md`: future user install and
|
|
109
|
+
Codex onboarding plan.
|
|
110
|
+
- `docs/convex-bridge-plan.md`: Convex bridge architecture and deployment
|
|
111
|
+
direction.
|
|
112
|
+
- `docs/options-picker.md`: CLI and MCP option picker behavior.
|
|
113
|
+
- `docs/legacy-system-feature-carryover.md`: old prototype features worth
|
|
114
|
+
rebuilding later if they still matter.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
CONVEX_URL=https://your-deployment.convex.cloud
|
|
2
|
+
SKILLHUB_WORKER_NAME=skillhub-local-worker
|
|
3
|
+
SKILLHUB_HARNESS_MODE=codex-cli
|
|
4
|
+
SKILLHUB_CAPABILITIES=codex.cli,codex.create_goal,codex.create_heartbeat,codex.create_thread,codex.notify_user
|
|
5
|
+
SKILLHUB_POLL_MS=1500
|
|
6
|
+
SKILLHUB_LEASE_MS=60000
|
|
7
|
+
|
|
8
|
+
CODEX_COMMAND=codex
|
|
9
|
+
CODEX_WORKDIR=C:\Users\you\Documents\code\skillhub-mvp
|
|
10
|
+
CODEX_ARGS_JSON=["--ask-for-approval","never","exec","--sandbox","workspace-write","--json"]
|
|
11
|
+
CODEX_TIMEOUT_MS=600000
|
|
12
|
+
|
|
13
|
+
# For cloud workers, prefer a worker-scoped secret rather than committing auth.
|
|
14
|
+
# CODEX_API_KEY=
|
package/bridge/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Bridge Harness
|
|
2
|
+
|
|
3
|
+
The harness is the external worker that connects SkillHub runs to Codex. It should not live inside the dashboard process.
|
|
4
|
+
|
|
5
|
+
## Runtime Loop
|
|
6
|
+
|
|
7
|
+
1. Start with `CONVEX_URL` and a worker identity.
|
|
8
|
+
2. Call `bridge.registerSession` once and keep the returned session id.
|
|
9
|
+
3. Expire stale leases so abandoned work can return to the queue.
|
|
10
|
+
4. Generate a fresh `claimId`.
|
|
11
|
+
5. Call `bridge.claimNextAction` with capabilities such as `["codex.cli"]` or `["*"]`.
|
|
12
|
+
6. If no action is returned, sleep briefly and poll again.
|
|
13
|
+
7. For claimed work, send status through `bridge.emitLog` and extend the lease with `bridge.heartbeatLease`.
|
|
14
|
+
8. Execute Codex through the selected adapter.
|
|
15
|
+
9. Finish with `bridge.completeAction`, `bridge.failAction`, or `bridge.releaseAction`.
|
|
16
|
+
|
|
17
|
+
## Required Environment
|
|
18
|
+
|
|
19
|
+
- `CONVEX_URL`: Convex deployment URL.
|
|
20
|
+
- `SKILLHUB_WORKER_NAME`: stable worker label for logs and leases.
|
|
21
|
+
- `SKILLHUB_HARNESS_MODE`: `dry-run` or `codex-cli`.
|
|
22
|
+
- `CODEX_HOME`: Codex configuration directory, when running the CLI with saved auth.
|
|
23
|
+
- `CODEX_API_KEY`: optional worker secret for `codex exec` automation.
|
|
24
|
+
- `CODEX_COMMAND`: Codex executable path or command name. Defaults to `codex`.
|
|
25
|
+
- `CODEX_WORKDIR`: git repository where Codex should run. Defaults to this process cwd.
|
|
26
|
+
- `CODEX_ARGS_JSON`: JSON array of base Codex CLI args.
|
|
27
|
+
|
|
28
|
+
## Adapters
|
|
29
|
+
|
|
30
|
+
The first adapter defaults to `dry-run`, which proves Convex claiming, heartbeats, logs, and completion without starting an external process.
|
|
31
|
+
|
|
32
|
+
Use `codex-cli` mode when the worker environment has Codex installed:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm run harness -- --mode codex-cli
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Set `CODEX_ARGS_JSON` to the base Codex CLI arguments. The harness appends the SkillHub action prompt unless `CODEX_STDIN_PROMPT=true`.
|
|
39
|
+
|
|
40
|
+
Recommended local starting point:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
CODEX_ARGS_JSON='["--ask-for-approval","never","exec","--sandbox","workspace-write","--json"]'
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Use `codex login` once for local saved auth, or provide `CODEX_API_KEY` as a worker-only secret in a hosted worker. Do not commit either credential.
|
|
47
|
+
|
|
48
|
+
## Doctor
|
|
49
|
+
|
|
50
|
+
Run the doctor before switching the worker to real Codex execution:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm run harness:doctor -- --mode codex-cli
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The doctor checks Convex connectivity, worker capabilities, `CODEX_ARGS_JSON`, `CODEX_WORKDIR`, git repo status, and whether the Codex command is runnable. After those checks pass, run the optional real Codex smoke test:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm run harness:doctor -- --mode codex-cli --codex-smoke
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The smoke test starts a tiny `codex exec` run, so use it only in an environment where Codex auth and billing are intentionally configured.
|
|
63
|
+
|
|
64
|
+
Keep adapters small. The durable state belongs in Convex, and the harness should be replaceable.
|
|
65
|
+
|
|
66
|
+
## Local Commands
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npm run harness:doctor
|
|
70
|
+
npm run harness:once
|
|
71
|
+
npm run harness -- --dry-run
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The same command can run as a Railway worker start command after environment variables are configured.
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { defaultCodexArgs, getRuntimeConfig } from "../src/config.mjs";
|
|
4
|
+
|
|
5
|
+
export const defaultCapabilities = [
|
|
6
|
+
"codex.cli",
|
|
7
|
+
"codex.create_goal",
|
|
8
|
+
"codex.create_heartbeat",
|
|
9
|
+
"codex.create_thread",
|
|
10
|
+
"codex.notify_user",
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export async function executeAction(action, options) {
|
|
14
|
+
const mode = options.mode ?? "dry-run";
|
|
15
|
+
|
|
16
|
+
if (mode === "dry-run") {
|
|
17
|
+
return await runDryAdapter(action, options);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (mode === "codex-cli") {
|
|
21
|
+
return await runCodexCliAdapter(action, options);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
throw new Error(`Unknown harness adapter mode: ${mode}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function runDryAdapter(action, { emitLog }) {
|
|
28
|
+
await emitLog("info", "Dry-run adapter accepted host action.", {
|
|
29
|
+
actionId: action._id,
|
|
30
|
+
actionType: action.actionType,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
source: "skillhub-bridge",
|
|
35
|
+
mode: "dry-run",
|
|
36
|
+
actionId: action._id,
|
|
37
|
+
actionType: action.actionType,
|
|
38
|
+
title: action.title,
|
|
39
|
+
summary: "No external Codex process was started.",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function runCodexCliAdapter(action, { emitLog, signal }) {
|
|
44
|
+
const { args, command, cwd, passPromptOnStdin, timeoutMs } = getCodexCliSettings();
|
|
45
|
+
const prompt = buildCodexPrompt(action);
|
|
46
|
+
const finalArgs = passPromptOnStdin ? args : [...args, prompt];
|
|
47
|
+
|
|
48
|
+
if (!args.length) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
"CODEX_ARGS_JSON must be a JSON array when SKILLHUB_HARNESS_MODE=codex-cli.",
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
await emitLog("info", "Starting Codex CLI adapter.", {
|
|
55
|
+
command,
|
|
56
|
+
args: redactArgs(finalArgs),
|
|
57
|
+
cwd,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return await spawnWithLogs({
|
|
61
|
+
command,
|
|
62
|
+
args: finalArgs,
|
|
63
|
+
cwd,
|
|
64
|
+
prompt: passPromptOnStdin ? prompt : "",
|
|
65
|
+
timeoutMs,
|
|
66
|
+
emitLog,
|
|
67
|
+
signal,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function getCodexCliSettings(env = process.env) {
|
|
72
|
+
const config = getRuntimeConfig(env);
|
|
73
|
+
return {
|
|
74
|
+
command: env.CODEX_COMMAND || config.codexCommand || "codex",
|
|
75
|
+
args: parseJsonArrayEnv("CODEX_ARGS_JSON", env) || config.codexArgs || defaultCodexArgs,
|
|
76
|
+
cwd: env.CODEX_WORKDIR || config.codexWorkdir || process.cwd(),
|
|
77
|
+
timeoutMs: Number(env.CODEX_TIMEOUT_MS || 10 * 60 * 1000),
|
|
78
|
+
passPromptOnStdin: env.CODEX_STDIN_PROMPT === "true",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function buildCodexPrompt(action) {
|
|
83
|
+
return [
|
|
84
|
+
`SkillHub host action: ${action._id}`,
|
|
85
|
+
`Title: ${action.title}`,
|
|
86
|
+
`Type: ${action.actionType}`,
|
|
87
|
+
`Run: ${action.runId}`,
|
|
88
|
+
action.nodeId ? `Node: ${action.nodeId}` : null,
|
|
89
|
+
"",
|
|
90
|
+
"Payload:",
|
|
91
|
+
JSON.stringify(action.payload ?? {}, null, 2),
|
|
92
|
+
"",
|
|
93
|
+
"Codex tool hint:",
|
|
94
|
+
JSON.stringify(action.codexTool ?? {}, null, 2),
|
|
95
|
+
"",
|
|
96
|
+
"Return a concise structured result. Do not mark the action complete yourself; the SkillHub bridge will write completion to Convex.",
|
|
97
|
+
]
|
|
98
|
+
.filter(Boolean)
|
|
99
|
+
.join("\n");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function parseJsonArrayEnv(name, env = process.env) {
|
|
103
|
+
const raw = env[name];
|
|
104
|
+
if (!raw) return null;
|
|
105
|
+
const parsed = JSON.parse(raw);
|
|
106
|
+
if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== "string")) {
|
|
107
|
+
throw new Error(`${name} must be a JSON array of strings.`);
|
|
108
|
+
}
|
|
109
|
+
return parsed;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function spawnWithLogs({
|
|
113
|
+
command,
|
|
114
|
+
args,
|
|
115
|
+
cwd,
|
|
116
|
+
prompt,
|
|
117
|
+
timeoutMs,
|
|
118
|
+
emitLog,
|
|
119
|
+
signal,
|
|
120
|
+
}) {
|
|
121
|
+
const startedAt = Date.now();
|
|
122
|
+
const stdout = [];
|
|
123
|
+
const stderr = [];
|
|
124
|
+
const pendingLogs = [];
|
|
125
|
+
|
|
126
|
+
const child = spawn(command, args, {
|
|
127
|
+
cwd,
|
|
128
|
+
env: process.env,
|
|
129
|
+
shell: false,
|
|
130
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
131
|
+
windowsHide: true,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const timeout = setTimeout(() => {
|
|
135
|
+
child.kill("SIGTERM");
|
|
136
|
+
}, timeoutMs);
|
|
137
|
+
|
|
138
|
+
const abort = () => child.kill("SIGTERM");
|
|
139
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
140
|
+
|
|
141
|
+
child.stdout.on("data", (chunk) => {
|
|
142
|
+
const text = chunk.toString();
|
|
143
|
+
stdout.push(text);
|
|
144
|
+
pendingLogs.push(emitLog("info", trimChunk(text), { stream: "stdout" }));
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
child.stderr.on("data", (chunk) => {
|
|
148
|
+
const text = chunk.toString();
|
|
149
|
+
stderr.push(text);
|
|
150
|
+
pendingLogs.push(emitLog("warn", trimChunk(text), { stream: "stderr" }));
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (prompt) {
|
|
154
|
+
child.stdin.end(prompt);
|
|
155
|
+
} else {
|
|
156
|
+
child.stdin.end();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const exit = await new Promise((resolve, reject) => {
|
|
160
|
+
child.on("error", reject);
|
|
161
|
+
child.on("close", (code, closeSignal) => resolve({ code, signal: closeSignal }));
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
clearTimeout(timeout);
|
|
165
|
+
signal?.removeEventListener("abort", abort);
|
|
166
|
+
await Promise.allSettled(pendingLogs);
|
|
167
|
+
|
|
168
|
+
const result = {
|
|
169
|
+
source: "skillhub-bridge",
|
|
170
|
+
mode: "codex-cli",
|
|
171
|
+
command,
|
|
172
|
+
args: redactArgs(args),
|
|
173
|
+
exitCode: exit.code,
|
|
174
|
+
signal: exit.signal,
|
|
175
|
+
durationMs: Date.now() - startedAt,
|
|
176
|
+
stdoutTail: tail(stdout.join("")),
|
|
177
|
+
stderrTail: tail(stderr.join("")),
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
if (exit.code !== 0) {
|
|
181
|
+
const reason = exit.signal
|
|
182
|
+
? `Codex CLI stopped by signal ${exit.signal}`
|
|
183
|
+
: `Codex CLI exited with code ${exit.code}`;
|
|
184
|
+
const error = new Error(reason);
|
|
185
|
+
error.result = result;
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function trimChunk(text) {
|
|
193
|
+
const normalized = text.replace(/\r/g, "").trim();
|
|
194
|
+
if (normalized.length <= 1800) return normalized;
|
|
195
|
+
return `${normalized.slice(0, 1800)}...`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function tail(text) {
|
|
199
|
+
const normalized = text.replace(/\r/g, "").trim();
|
|
200
|
+
if (normalized.length <= 4000) return normalized;
|
|
201
|
+
return normalized.slice(-4000);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function redactArgs(args) {
|
|
205
|
+
return args.map((arg) => {
|
|
206
|
+
if (/key|token|secret|password/i.test(arg)) return "<redacted>";
|
|
207
|
+
return arg.length > 180 ? `${arg.slice(0, 180)}...` : arg;
|
|
208
|
+
});
|
|
209
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { makeFunctionReference } from "convex/server";
|
|
2
|
+
|
|
3
|
+
export const bridgeFunctions = {
|
|
4
|
+
claimNextAction: makeFunctionReference("bridge:claimNextAction"),
|
|
5
|
+
completeAction: makeFunctionReference("bridge:completeAction"),
|
|
6
|
+
emitLog: makeFunctionReference("bridge:emitLog"),
|
|
7
|
+
expireStaleLeases: makeFunctionReference("bridge:expireStaleLeases"),
|
|
8
|
+
failAction: makeFunctionReference("bridge:failAction"),
|
|
9
|
+
heartbeatLease: makeFunctionReference("bridge:heartbeatLease"),
|
|
10
|
+
markApplying: makeFunctionReference("bridge:markApplying"),
|
|
11
|
+
registerSession: makeFunctionReference("bridge:registerSession"),
|
|
12
|
+
releaseAction: makeFunctionReference("bridge:releaseAction"),
|
|
13
|
+
};
|