@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 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=
@@ -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
+ };