@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/convex/skills.ts
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { mutation, query } from "./_generated/server";
|
|
3
|
+
|
|
4
|
+
const defaultInstructions = [
|
|
5
|
+
"You are running this SkillHub skill as a bounded Codex task.",
|
|
6
|
+
"Follow the skill instructions, produce a concise result, and call out blockers or missing inputs.",
|
|
7
|
+
].join("\n\n");
|
|
8
|
+
|
|
9
|
+
export const list = query({
|
|
10
|
+
args: {
|
|
11
|
+
limit: v.optional(v.number()),
|
|
12
|
+
},
|
|
13
|
+
handler: async (ctx, args) => {
|
|
14
|
+
const limit = Math.min(args.limit ?? 50, 200);
|
|
15
|
+
const skills = await ctx.db
|
|
16
|
+
.query("skills")
|
|
17
|
+
.withIndex("by_updatedAt")
|
|
18
|
+
.order("desc")
|
|
19
|
+
.take(limit);
|
|
20
|
+
|
|
21
|
+
return await Promise.all(
|
|
22
|
+
skills.map(async (skill) => ({
|
|
23
|
+
skill,
|
|
24
|
+
latestVersion: skill.latestVersionId
|
|
25
|
+
? await ctx.db.get(skill.latestVersionId)
|
|
26
|
+
: null,
|
|
27
|
+
draftVersion: skill.draftVersionId
|
|
28
|
+
? await ctx.db.get(skill.draftVersionId)
|
|
29
|
+
: null,
|
|
30
|
+
})),
|
|
31
|
+
);
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export const get = query({
|
|
36
|
+
args: {
|
|
37
|
+
skillId: v.id("skills"),
|
|
38
|
+
},
|
|
39
|
+
handler: async (ctx, args) => {
|
|
40
|
+
const skill = await ctx.db.get(args.skillId);
|
|
41
|
+
if (!skill) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const versions = await ctx.db
|
|
46
|
+
.query("skillVersions")
|
|
47
|
+
.withIndex("by_skill_version", (q) => q.eq("skillId", args.skillId))
|
|
48
|
+
.order("desc")
|
|
49
|
+
.take(30);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
skill,
|
|
53
|
+
versions,
|
|
54
|
+
latestVersion: skill.latestVersionId
|
|
55
|
+
? await ctx.db.get(skill.latestVersionId)
|
|
56
|
+
: null,
|
|
57
|
+
draftVersion: skill.draftVersionId
|
|
58
|
+
? await ctx.db.get(skill.draftVersionId)
|
|
59
|
+
: null,
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export const createDraft = mutation({
|
|
65
|
+
args: {
|
|
66
|
+
name: v.string(),
|
|
67
|
+
summary: v.optional(v.string()),
|
|
68
|
+
instructions: v.optional(v.string()),
|
|
69
|
+
inputSpec: v.optional(v.string()),
|
|
70
|
+
outputSpec: v.optional(v.string()),
|
|
71
|
+
testPrompt: v.optional(v.string()),
|
|
72
|
+
tags: v.optional(v.array(v.string())),
|
|
73
|
+
},
|
|
74
|
+
handler: async (ctx, args) => {
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
const name = requireText(args.name, "Skill name");
|
|
77
|
+
const slug = await uniqueSlug(ctx, slugify(name));
|
|
78
|
+
|
|
79
|
+
const skillId = await ctx.db.insert("skills", {
|
|
80
|
+
slug,
|
|
81
|
+
name,
|
|
82
|
+
summary: cleanOptional(args.summary),
|
|
83
|
+
tags: normalizeTags(args.tags ?? []),
|
|
84
|
+
createdAt: now,
|
|
85
|
+
updatedAt: now,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const draftVersionId = await ctx.db.insert("skillVersions", {
|
|
89
|
+
skillId,
|
|
90
|
+
version: "draft",
|
|
91
|
+
status: "draft",
|
|
92
|
+
instructions: cleanOptional(args.instructions) || defaultInstructions,
|
|
93
|
+
inputSpec: cleanOptional(args.inputSpec),
|
|
94
|
+
outputSpec: cleanOptional(args.outputSpec),
|
|
95
|
+
testPrompt: cleanOptional(args.testPrompt),
|
|
96
|
+
createdAt: now,
|
|
97
|
+
updatedAt: now,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
await ctx.db.patch(skillId, {
|
|
101
|
+
draftVersionId,
|
|
102
|
+
updatedAt: now,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return skillId;
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
export const saveDraft = mutation({
|
|
110
|
+
args: {
|
|
111
|
+
skillId: v.id("skills"),
|
|
112
|
+
name: v.string(),
|
|
113
|
+
summary: v.optional(v.string()),
|
|
114
|
+
instructions: v.string(),
|
|
115
|
+
inputSpec: v.optional(v.string()),
|
|
116
|
+
outputSpec: v.optional(v.string()),
|
|
117
|
+
testPrompt: v.optional(v.string()),
|
|
118
|
+
tags: v.optional(v.array(v.string())),
|
|
119
|
+
},
|
|
120
|
+
handler: async (ctx, args) => {
|
|
121
|
+
const skill = await requireSkill(ctx, args.skillId);
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
const name = requireText(args.name, "Skill name");
|
|
124
|
+
const instructions = requireText(args.instructions, "Instructions");
|
|
125
|
+
const draftVersionId = await ensureDraftVersion(ctx, skill, now);
|
|
126
|
+
const skillPatch: any = {
|
|
127
|
+
name,
|
|
128
|
+
summary: cleanOptional(args.summary),
|
|
129
|
+
tags: normalizeTags(args.tags ?? []),
|
|
130
|
+
updatedAt: now,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
if (!skill.latestVersionId || skill.slug.startsWith("untitled-skill")) {
|
|
134
|
+
skillPatch.slug = await uniqueSlugForSkill(ctx, slugify(name), args.skillId);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
await ctx.db.patch(args.skillId, skillPatch);
|
|
138
|
+
|
|
139
|
+
await ctx.db.patch(draftVersionId, {
|
|
140
|
+
instructions,
|
|
141
|
+
inputSpec: cleanOptional(args.inputSpec),
|
|
142
|
+
outputSpec: cleanOptional(args.outputSpec),
|
|
143
|
+
testPrompt: cleanOptional(args.testPrompt),
|
|
144
|
+
updatedAt: now,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return draftVersionId;
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
export const publishDraft = mutation({
|
|
152
|
+
args: {
|
|
153
|
+
skillId: v.id("skills"),
|
|
154
|
+
},
|
|
155
|
+
handler: async (ctx, args) => {
|
|
156
|
+
const skill = await requireSkill(ctx, args.skillId);
|
|
157
|
+
if (!skill.draftVersionId) {
|
|
158
|
+
throw new Error("No draft exists to publish.");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const draft = await ctx.db.get(skill.draftVersionId);
|
|
162
|
+
if (!draft) {
|
|
163
|
+
throw new Error("Draft version not found.");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
requireText(draft.instructions, "Instructions");
|
|
167
|
+
const now = Date.now();
|
|
168
|
+
const version = await nextPublishedVersion(ctx, args.skillId);
|
|
169
|
+
|
|
170
|
+
await ctx.db.patch(draft._id, {
|
|
171
|
+
version,
|
|
172
|
+
status: "published",
|
|
173
|
+
publishedAt: now,
|
|
174
|
+
updatedAt: now,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await ctx.db.patch(args.skillId, {
|
|
178
|
+
latestVersionId: draft._id,
|
|
179
|
+
draftVersionId: undefined,
|
|
180
|
+
updatedAt: now,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
return draft._id;
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
export const runTest = mutation({
|
|
188
|
+
args: {
|
|
189
|
+
skillId: v.id("skills"),
|
|
190
|
+
promptOverride: v.optional(v.string()),
|
|
191
|
+
},
|
|
192
|
+
handler: async (ctx, args) => {
|
|
193
|
+
const skill = await requireSkill(ctx, args.skillId);
|
|
194
|
+
const versionId = skill.latestVersionId ?? skill.draftVersionId;
|
|
195
|
+
if (!versionId) {
|
|
196
|
+
throw new Error("Save a skill draft before running a test.");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const version = await ctx.db.get(versionId);
|
|
200
|
+
if (!version) {
|
|
201
|
+
throw new Error("Skill version not found.");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const now = Date.now();
|
|
205
|
+
const prompt = cleanOptional(args.promptOverride) || version.testPrompt || `Test ${skill.name}`;
|
|
206
|
+
const loopSlug = `skill:${skill.slug}`;
|
|
207
|
+
|
|
208
|
+
const runId = await ctx.db.insert("loopRuns", {
|
|
209
|
+
loopSlug,
|
|
210
|
+
status: "running",
|
|
211
|
+
activeNodeId: "skill.execute",
|
|
212
|
+
prompt,
|
|
213
|
+
graph: {
|
|
214
|
+
entryNodeId: "skill.execute",
|
|
215
|
+
nodes: [{ id: "skill.execute", type: "hostAction" }],
|
|
216
|
+
},
|
|
217
|
+
controls: {
|
|
218
|
+
source: "skill-authoring",
|
|
219
|
+
mode: "test",
|
|
220
|
+
skillId: skill._id,
|
|
221
|
+
skillVersionId: version._id,
|
|
222
|
+
},
|
|
223
|
+
context: {
|
|
224
|
+
skill: {
|
|
225
|
+
name: skill.name,
|
|
226
|
+
slug: skill.slug,
|
|
227
|
+
version: version.version,
|
|
228
|
+
status: version.status,
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
transitionCount: 0,
|
|
232
|
+
startedAt: now,
|
|
233
|
+
updatedAt: now,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
await ctx.db.insert("runEvents", {
|
|
237
|
+
runId,
|
|
238
|
+
sequence: 1,
|
|
239
|
+
eventType: "run.created",
|
|
240
|
+
level: "info",
|
|
241
|
+
message: `Skill test created for ${skill.name}`,
|
|
242
|
+
payload: {
|
|
243
|
+
skillId: skill._id,
|
|
244
|
+
skillVersionId: version._id,
|
|
245
|
+
prompt,
|
|
246
|
+
},
|
|
247
|
+
createdAt: now,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const actionId = await ctx.db.insert("hostActions", {
|
|
251
|
+
runId,
|
|
252
|
+
loopSlug,
|
|
253
|
+
nodeId: "skill.execute",
|
|
254
|
+
actionType: "codex.cli",
|
|
255
|
+
title: `Test skill: ${skill.name}`,
|
|
256
|
+
status: "pending",
|
|
257
|
+
idempotencyKey: `skill-test:${runId}`,
|
|
258
|
+
payload: {
|
|
259
|
+
prompt: renderSkillPrompt({ skill, version, prompt }),
|
|
260
|
+
skill: {
|
|
261
|
+
id: skill._id,
|
|
262
|
+
slug: skill.slug,
|
|
263
|
+
name: skill.name,
|
|
264
|
+
summary: skill.summary,
|
|
265
|
+
tags: skill.tags,
|
|
266
|
+
},
|
|
267
|
+
version: {
|
|
268
|
+
id: version._id,
|
|
269
|
+
version: version.version,
|
|
270
|
+
status: version.status,
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
codexTool: {
|
|
274
|
+
adapter: "codex-cli",
|
|
275
|
+
safeDefault: "dry-run",
|
|
276
|
+
},
|
|
277
|
+
createdAt: now,
|
|
278
|
+
updatedAt: now,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
await ctx.db.insert("runEvents", {
|
|
282
|
+
runId,
|
|
283
|
+
sequence: 2,
|
|
284
|
+
eventType: "host_action.enqueued",
|
|
285
|
+
level: "info",
|
|
286
|
+
nodeId: "skill.execute",
|
|
287
|
+
message: `Queued skill test for ${skill.name}`,
|
|
288
|
+
payload: {
|
|
289
|
+
actionId,
|
|
290
|
+
actionType: "codex.cli",
|
|
291
|
+
},
|
|
292
|
+
createdAt: now,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
return { runId, actionId };
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
async function requireSkill(ctx: any, skillId: any) {
|
|
300
|
+
const skill = await ctx.db.get(skillId);
|
|
301
|
+
if (!skill) {
|
|
302
|
+
throw new Error(`Skill not found: ${skillId}`);
|
|
303
|
+
}
|
|
304
|
+
return skill;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function ensureDraftVersion(ctx: any, skill: any, now: number) {
|
|
308
|
+
if (skill.draftVersionId) {
|
|
309
|
+
return skill.draftVersionId;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const latest = skill.latestVersionId ? await ctx.db.get(skill.latestVersionId) : null;
|
|
313
|
+
const draftVersionId = await ctx.db.insert("skillVersions", {
|
|
314
|
+
skillId: skill._id,
|
|
315
|
+
version: "draft",
|
|
316
|
+
status: "draft",
|
|
317
|
+
instructions: latest?.instructions ?? defaultInstructions,
|
|
318
|
+
inputSpec: latest?.inputSpec,
|
|
319
|
+
outputSpec: latest?.outputSpec,
|
|
320
|
+
testPrompt: latest?.testPrompt,
|
|
321
|
+
createdAt: now,
|
|
322
|
+
updatedAt: now,
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
await ctx.db.patch(skill._id, {
|
|
326
|
+
draftVersionId,
|
|
327
|
+
updatedAt: now,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
return draftVersionId;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function nextPublishedVersion(ctx: any, skillId: any) {
|
|
334
|
+
const published = await ctx.db
|
|
335
|
+
.query("skillVersions")
|
|
336
|
+
.withIndex("by_skill_status", (q: any) =>
|
|
337
|
+
q.eq("skillId", skillId).eq("status", "published"),
|
|
338
|
+
)
|
|
339
|
+
.collect();
|
|
340
|
+
|
|
341
|
+
return `v${published.length + 1}`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function uniqueSlug(ctx: any, base: string) {
|
|
345
|
+
const existing = await ctx.db
|
|
346
|
+
.query("skills")
|
|
347
|
+
.withIndex("by_slug", (q: any) => q.eq("slug", base))
|
|
348
|
+
.unique();
|
|
349
|
+
|
|
350
|
+
if (!existing) {
|
|
351
|
+
return base;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return `${base}-${Date.now().toString(36)}`;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function uniqueSlugForSkill(ctx: any, base: string, skillId: any) {
|
|
358
|
+
const existing = await ctx.db
|
|
359
|
+
.query("skills")
|
|
360
|
+
.withIndex("by_slug", (q: any) => q.eq("slug", base))
|
|
361
|
+
.unique();
|
|
362
|
+
|
|
363
|
+
if (!existing || existing._id === skillId) {
|
|
364
|
+
return base;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return `${base}-${Date.now().toString(36)}`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function renderSkillPrompt({ skill, version, prompt }: any) {
|
|
371
|
+
return [
|
|
372
|
+
`Skill: ${skill.name}`,
|
|
373
|
+
skill.summary ? `Summary: ${skill.summary}` : null,
|
|
374
|
+
skill.tags?.length ? `Tags: ${skill.tags.join(", ")}` : null,
|
|
375
|
+
"",
|
|
376
|
+
"Instructions:",
|
|
377
|
+
version.instructions,
|
|
378
|
+
"",
|
|
379
|
+
version.inputSpec ? `Inputs:\n${version.inputSpec}` : null,
|
|
380
|
+
version.outputSpec ? `Expected output:\n${version.outputSpec}` : null,
|
|
381
|
+
"",
|
|
382
|
+
"Test prompt:",
|
|
383
|
+
prompt,
|
|
384
|
+
]
|
|
385
|
+
.filter(Boolean)
|
|
386
|
+
.join("\n");
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function slugify(value: string) {
|
|
390
|
+
const slug = value
|
|
391
|
+
.toLowerCase()
|
|
392
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
393
|
+
.replace(/^-+|-+$/g, "");
|
|
394
|
+
|
|
395
|
+
return slug || "skill";
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function normalizeTags(tags: string[]) {
|
|
399
|
+
return [...new Set(tags.map((tag) => tag.trim()).filter(Boolean))].slice(0, 12);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function cleanOptional(value?: string) {
|
|
403
|
+
const cleaned = value?.trim();
|
|
404
|
+
return cleaned ? cleaned : undefined;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function requireText(value: string | undefined, label: string) {
|
|
408
|
+
const cleaned = cleanOptional(value);
|
|
409
|
+
if (!cleaned) {
|
|
410
|
+
throw new Error(`${label} is required.`);
|
|
411
|
+
}
|
|
412
|
+
return cleaned;
|
|
413
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
/* This TypeScript project config describes the environment that
|
|
3
|
+
* Convex functions run in and is used to typecheck them.
|
|
4
|
+
* You can modify it, but some settings are required to use Convex.
|
|
5
|
+
*/
|
|
6
|
+
"compilerOptions": {
|
|
7
|
+
/* These settings are not required by Convex and can be modified. */
|
|
8
|
+
"allowJs": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"moduleResolution": "Bundler",
|
|
11
|
+
"jsx": "react-jsx",
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"allowSyntheticDefaultImports": true,
|
|
14
|
+
|
|
15
|
+
/* These compiler options are required by Convex */
|
|
16
|
+
"target": "ESNext",
|
|
17
|
+
"lib": ["ES2023", "dom"],
|
|
18
|
+
"forceConsistentCasingInFileNames": true,
|
|
19
|
+
"module": "ESNext",
|
|
20
|
+
"isolatedModules": true,
|
|
21
|
+
"noEmit": true
|
|
22
|
+
},
|
|
23
|
+
"include": ["./**/*"],
|
|
24
|
+
"exclude": ["./_generated"]
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VITE_CONVEX_URL=https://your-deployment.convex.cloud
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>SkillHub Control</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.jsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|