@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,588 @@
|
|
|
1
|
+
import { ConvexHttpClient } from "convex/browser";
|
|
2
|
+
import { makeFunctionReference } from "convex/server";
|
|
3
|
+
import dotenv from "dotenv";
|
|
4
|
+
import { createInterface } from "node:readline/promises";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { getRuntimeConfig } from "./config.mjs";
|
|
8
|
+
|
|
9
|
+
dotenv.config({ path: ".env", quiet: true });
|
|
10
|
+
dotenv.config({ path: ".env.local", quiet: true });
|
|
11
|
+
dotenv.config({ path: "bridge/.env.local", quiet: true });
|
|
12
|
+
|
|
13
|
+
const refs = {
|
|
14
|
+
loops: {
|
|
15
|
+
list: makeFunctionReference("loops:list"),
|
|
16
|
+
run: makeFunctionReference("loops:run"),
|
|
17
|
+
},
|
|
18
|
+
skills: {
|
|
19
|
+
list: makeFunctionReference("skills:list"),
|
|
20
|
+
runTest: makeFunctionReference("skills:runTest"),
|
|
21
|
+
},
|
|
22
|
+
runs: {
|
|
23
|
+
getRun: makeFunctionReference("runs:getRun"),
|
|
24
|
+
listRecent: makeFunctionReference("runs:listRecent"),
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export async function runCloudCatalogCommand(argv, io = process) {
|
|
29
|
+
const [subcommand = "run", ...rest] = argv;
|
|
30
|
+
|
|
31
|
+
if (subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
|
|
32
|
+
return showHelp();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (subcommand === "list" || subcommand === "options") {
|
|
36
|
+
return listOptions(rest, io);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (subcommand === "run" || subcommand === "choose") {
|
|
40
|
+
return runSelection(rest, io);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (subcommand === "status") {
|
|
44
|
+
return showStatus(rest);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return runSelection(argv, io);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function getCloudCatalogItems({
|
|
51
|
+
filter = "",
|
|
52
|
+
kind = "",
|
|
53
|
+
limit = 100,
|
|
54
|
+
query = "",
|
|
55
|
+
} = {}) {
|
|
56
|
+
const catalog = await fetchCatalog({ limit });
|
|
57
|
+
return filterCatalog(catalog, {
|
|
58
|
+
filter,
|
|
59
|
+
kind: normalizeKind(kind),
|
|
60
|
+
query,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function runCloudCatalogItem({
|
|
65
|
+
filter = "",
|
|
66
|
+
kind = "",
|
|
67
|
+
limit = 100,
|
|
68
|
+
prompt = "",
|
|
69
|
+
query = "",
|
|
70
|
+
target = "",
|
|
71
|
+
} = {}) {
|
|
72
|
+
const items = await getCloudCatalogItems({ filter, kind, limit, query });
|
|
73
|
+
const selected = target
|
|
74
|
+
? matchSelection(items, target)
|
|
75
|
+
: items.length === 1
|
|
76
|
+
? items[0]
|
|
77
|
+
: null;
|
|
78
|
+
|
|
79
|
+
if (!selected) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
reason: items.length ? "ambiguous_selection" : "no_matches",
|
|
83
|
+
items,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const client = createClient();
|
|
88
|
+
const promptOverride = cleanOptional(prompt);
|
|
89
|
+
const result =
|
|
90
|
+
selected.kind === "loop"
|
|
91
|
+
? await client.mutation(refs.loops.run, {
|
|
92
|
+
loopId: selected.id,
|
|
93
|
+
promptOverride,
|
|
94
|
+
})
|
|
95
|
+
: await client.mutation(refs.skills.runTest, {
|
|
96
|
+
skillId: selected.id,
|
|
97
|
+
promptOverride,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
ok: true,
|
|
102
|
+
selection: selected,
|
|
103
|
+
runId: result.runId,
|
|
104
|
+
actionId: result.actionId,
|
|
105
|
+
message: `Queued ${selected.name}. I will watch the run and report when it completes.`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function getRunStatus(runId) {
|
|
110
|
+
const client = createClient();
|
|
111
|
+
const packet = await client.query(refs.runs.getRun, {
|
|
112
|
+
runId,
|
|
113
|
+
eventLimit: 20,
|
|
114
|
+
logLimit: 20,
|
|
115
|
+
});
|
|
116
|
+
if (!packet?.run) {
|
|
117
|
+
return { ok: false, reason: "run_not_found", runId };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
ok: true,
|
|
122
|
+
run: summarizeRun(packet.run),
|
|
123
|
+
hostActions: packet.hostActions.map(summarizeAction),
|
|
124
|
+
latestEvent: packet.events.at(-1) ?? null,
|
|
125
|
+
latestLog: packet.logs.at(-1) ?? null,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function listRecentRuns({ limit = 10 } = {}) {
|
|
130
|
+
const client = createClient();
|
|
131
|
+
const runs = await client.query(refs.runs.listRecent, {
|
|
132
|
+
limit: Math.max(1, Math.min(Number(limit || 10), 50)),
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
ok: true,
|
|
136
|
+
runs: runs.map(summarizeRun),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function watchRun({
|
|
141
|
+
runId,
|
|
142
|
+
timeoutSeconds = 60,
|
|
143
|
+
intervalMs = 2000,
|
|
144
|
+
} = {}) {
|
|
145
|
+
if (!runId) {
|
|
146
|
+
return { ok: false, reason: "runId_required" };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const startedAt = Date.now();
|
|
150
|
+
const timeoutMs = Math.max(1000, Math.min(Number(timeoutSeconds || 60) * 1000, 5 * 60_000));
|
|
151
|
+
const waitMs = Math.max(500, Math.min(Number(intervalMs || 2000), 10_000));
|
|
152
|
+
let latest = await getRunStatus(runId);
|
|
153
|
+
|
|
154
|
+
while (latest.ok && !isTerminalRun(latest.run.status) && Date.now() - startedAt < timeoutMs) {
|
|
155
|
+
await sleep(waitMs);
|
|
156
|
+
latest = await getRunStatus(runId);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
...latest,
|
|
161
|
+
timedOut: latest.ok ? !isTerminalRun(latest.run.status) : false,
|
|
162
|
+
watchedMs: Date.now() - startedAt,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function listOptions(argv, io) {
|
|
167
|
+
const flags = parseFlags(argv);
|
|
168
|
+
const items = await getCloudCatalogItems(flags);
|
|
169
|
+
|
|
170
|
+
if (flags.json) {
|
|
171
|
+
console.log(JSON.stringify({ items }, null, 2));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
console.log(renderCatalog(items));
|
|
176
|
+
console.log("");
|
|
177
|
+
console.log("Run one with: skillhub run <number>");
|
|
178
|
+
console.log("Or open the picker with: skillhub choose");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function runSelection(argv, io) {
|
|
182
|
+
const flags = parseFlags(argv);
|
|
183
|
+
const items = await getCloudCatalogItems(flags);
|
|
184
|
+
const selected = await resolveSelection(items, flags, io);
|
|
185
|
+
|
|
186
|
+
if (!selected) {
|
|
187
|
+
if (flags.json) {
|
|
188
|
+
console.log(JSON.stringify({ ok: false, reason: "no_selection", items }, null, 2));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
console.log(renderCatalog(items));
|
|
193
|
+
console.log("");
|
|
194
|
+
console.log("Pick a number with: skillhub run <number>");
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const client = createClient();
|
|
199
|
+
const promptOverride = cleanOptional(flags.prompt);
|
|
200
|
+
const result =
|
|
201
|
+
selected.kind === "loop"
|
|
202
|
+
? await client.mutation(refs.loops.run, {
|
|
203
|
+
loopId: selected.id,
|
|
204
|
+
promptOverride,
|
|
205
|
+
})
|
|
206
|
+
: await client.mutation(refs.skills.runTest, {
|
|
207
|
+
skillId: selected.id,
|
|
208
|
+
promptOverride,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const payload = {
|
|
212
|
+
ok: true,
|
|
213
|
+
selection: selected,
|
|
214
|
+
runId: result.runId,
|
|
215
|
+
actionId: result.actionId,
|
|
216
|
+
message: `Queued ${selected.name}. I will watch the run and report when it completes.`,
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
if (flags.json) {
|
|
220
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
console.log(`Queued ${selected.kind}: ${selected.name}`);
|
|
225
|
+
console.log(`Run id: ${result.runId}`);
|
|
226
|
+
console.log(`Action id: ${result.actionId}`);
|
|
227
|
+
console.log("Watch it in Codex with: skillhub status");
|
|
228
|
+
console.log("Or keep the local bridge running with: skillhub bridge start");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function showStatus(argv) {
|
|
232
|
+
const flags = parseFlags(argv);
|
|
233
|
+
const target = flags.target || flags.pick;
|
|
234
|
+
|
|
235
|
+
if (target) {
|
|
236
|
+
const status = await getRunStatus(target);
|
|
237
|
+
if (flags.json) {
|
|
238
|
+
console.log(JSON.stringify(status, null, 2));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
printRunStatus(status);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const recent = await listRecentRuns({ limit: flags.limit || 10 });
|
|
246
|
+
if (flags.json) {
|
|
247
|
+
console.log(JSON.stringify(recent, null, 2));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
console.log(renderRecentRuns(recent.runs));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function fetchCatalog({ limit }) {
|
|
254
|
+
const client = createClient();
|
|
255
|
+
const size = Math.max(1, Math.min(Number(limit || 100), 200));
|
|
256
|
+
const [loopRows, skillRows] = await Promise.all([
|
|
257
|
+
client.query(refs.loops.list, { limit: size }),
|
|
258
|
+
client.query(refs.skills.list, { limit: size }),
|
|
259
|
+
]);
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
loops: loopRows.map(toLoopItem),
|
|
263
|
+
skills: skillRows.map(toSkillItem),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function createClient() {
|
|
268
|
+
const config = getRuntimeConfig();
|
|
269
|
+
const convexUrl = config.convexUrl;
|
|
270
|
+
if (!convexUrl) {
|
|
271
|
+
throw new Error("Run skillhub connect before using SkillHub options.");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const client = new ConvexHttpClient(convexUrl);
|
|
275
|
+
if (process.env.CONVEX_AUTH_TOKEN) {
|
|
276
|
+
client.setAuth(process.env.CONVEX_AUTH_TOKEN);
|
|
277
|
+
}
|
|
278
|
+
return client;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function summarizeRun(run) {
|
|
282
|
+
return {
|
|
283
|
+
id: run._id,
|
|
284
|
+
loopSlug: run.loopSlug,
|
|
285
|
+
status: run.status,
|
|
286
|
+
activeNodeId: run.activeNodeId ?? null,
|
|
287
|
+
prompt: run.prompt ?? "",
|
|
288
|
+
summary: run.summary ?? "",
|
|
289
|
+
startedAt: run.startedAt,
|
|
290
|
+
completedAt: run.completedAt ?? null,
|
|
291
|
+
updatedAt: run.updatedAt,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function summarizeAction(action) {
|
|
296
|
+
return {
|
|
297
|
+
id: action._id,
|
|
298
|
+
title: action.title,
|
|
299
|
+
status: action.status,
|
|
300
|
+
actionType: action.actionType,
|
|
301
|
+
error: action.error ?? "",
|
|
302
|
+
updatedAt: action.updatedAt,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function printRunStatus(status) {
|
|
307
|
+
if (!status.ok) {
|
|
308
|
+
console.log(`Run not found: ${status.runId}`);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
console.log(`${status.run.loopSlug}: ${status.run.status}`);
|
|
313
|
+
console.log(`Run id: ${status.run.id}`);
|
|
314
|
+
if (status.hostActions.length) {
|
|
315
|
+
console.log("");
|
|
316
|
+
console.log("Actions");
|
|
317
|
+
for (const action of status.hostActions) {
|
|
318
|
+
console.log(`- ${action.title}: ${action.status}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function renderRecentRuns(runs) {
|
|
324
|
+
if (!runs.length) return "No recent runs.";
|
|
325
|
+
return [
|
|
326
|
+
"Recent runs",
|
|
327
|
+
...runs.map((run, index) => `${index + 1}. ${run.loopSlug} - ${run.status} (${run.id})`),
|
|
328
|
+
].join("\n");
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function toLoopItem({ loop, latestVersion, draftVersion }) {
|
|
332
|
+
const version = latestVersion ?? draftVersion;
|
|
333
|
+
const manifest = version?.manifest ?? {};
|
|
334
|
+
return {
|
|
335
|
+
kind: "loop",
|
|
336
|
+
id: loop._id,
|
|
337
|
+
slug: loop.slug,
|
|
338
|
+
name: loop.name,
|
|
339
|
+
description: loop.description ?? manifest.objective ?? "",
|
|
340
|
+
status: latestVersion ? "published" : draftVersion ? "draft" : "empty",
|
|
341
|
+
version: version?.version ?? null,
|
|
342
|
+
tags: loop.tags ?? [],
|
|
343
|
+
updatedAt: loop.updatedAt,
|
|
344
|
+
stepCount: Array.isArray(manifest.steps) ? manifest.steps.length : 0,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function toSkillItem({ skill, latestVersion, draftVersion }) {
|
|
349
|
+
const version = latestVersion ?? draftVersion;
|
|
350
|
+
return {
|
|
351
|
+
kind: "skill",
|
|
352
|
+
id: skill._id,
|
|
353
|
+
slug: skill.slug,
|
|
354
|
+
name: skill.name,
|
|
355
|
+
description: skill.summary ?? version?.testPrompt ?? "",
|
|
356
|
+
status: latestVersion ? "published" : draftVersion ? "draft" : "empty",
|
|
357
|
+
version: version?.version ?? null,
|
|
358
|
+
tags: skill.tags ?? [],
|
|
359
|
+
updatedAt: skill.updatedAt,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function filterCatalog(catalog, flags) {
|
|
364
|
+
let items = [];
|
|
365
|
+
if (flags.kind !== "skill") items.push(...catalog.loops);
|
|
366
|
+
if (flags.kind !== "loop") items.push(...catalog.skills);
|
|
367
|
+
|
|
368
|
+
const query = cleanOptional(flags.query || flags.filter);
|
|
369
|
+
if (query) {
|
|
370
|
+
const needle = query.toLowerCase();
|
|
371
|
+
items = items.filter((item) =>
|
|
372
|
+
[
|
|
373
|
+
item.kind,
|
|
374
|
+
item.slug,
|
|
375
|
+
item.name,
|
|
376
|
+
item.description,
|
|
377
|
+
item.status,
|
|
378
|
+
...(item.tags ?? []),
|
|
379
|
+
]
|
|
380
|
+
.filter(Boolean)
|
|
381
|
+
.join(" ")
|
|
382
|
+
.toLowerCase()
|
|
383
|
+
.includes(needle),
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return items.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function resolveSelection(items, flags, io) {
|
|
391
|
+
const target = flags.pick || flags.target;
|
|
392
|
+
if (target) {
|
|
393
|
+
return matchSelection(items, target);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (!items.length || flags.noPrompt || !io.stdin?.isTTY) {
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
console.log(renderCatalog(items));
|
|
401
|
+
console.log("");
|
|
402
|
+
const rl = createInterface({
|
|
403
|
+
input: io.stdin,
|
|
404
|
+
output: io.stdout,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
const answer = await rl.question("Choose a number, or type search text: ");
|
|
409
|
+
const direct = matchSelection(items, answer);
|
|
410
|
+
if (direct) return direct;
|
|
411
|
+
|
|
412
|
+
const narrowed = filterCatalog({ loops: items.filter(isLoop), skills: items.filter(isSkill) }, {
|
|
413
|
+
query: answer,
|
|
414
|
+
});
|
|
415
|
+
if (narrowed.length === 1) return narrowed[0];
|
|
416
|
+
if (!narrowed.length) return null;
|
|
417
|
+
|
|
418
|
+
console.log("");
|
|
419
|
+
console.log(renderCatalog(narrowed));
|
|
420
|
+
console.log("");
|
|
421
|
+
const secondAnswer = await rl.question("Choose a number from these matches: ");
|
|
422
|
+
return matchSelection(narrowed, secondAnswer);
|
|
423
|
+
} finally {
|
|
424
|
+
rl.close();
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function matchSelection(items, target) {
|
|
429
|
+
const cleaned = cleanOptional(String(target));
|
|
430
|
+
if (!cleaned) return null;
|
|
431
|
+
|
|
432
|
+
if (/^\d+$/.test(cleaned)) {
|
|
433
|
+
return items[Number(cleaned) - 1] ?? null;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const needle = cleaned.toLowerCase();
|
|
437
|
+
const exact = items.find(
|
|
438
|
+
(item) =>
|
|
439
|
+
item.id.toLowerCase() === needle ||
|
|
440
|
+
item.slug.toLowerCase() === needle ||
|
|
441
|
+
item.name.toLowerCase() === needle,
|
|
442
|
+
);
|
|
443
|
+
if (exact) return exact;
|
|
444
|
+
|
|
445
|
+
const fuzzy = items.filter((item) =>
|
|
446
|
+
[item.slug, item.name, item.description, ...(item.tags ?? [])]
|
|
447
|
+
.filter(Boolean)
|
|
448
|
+
.join(" ")
|
|
449
|
+
.toLowerCase()
|
|
450
|
+
.includes(needle),
|
|
451
|
+
);
|
|
452
|
+
return fuzzy.length === 1 ? fuzzy[0] : null;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export function renderCatalog(items) {
|
|
456
|
+
if (!items.length) {
|
|
457
|
+
return "No loops or skills matched.";
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const loopItems = items.filter(isLoop);
|
|
461
|
+
const skillItems = items.filter(isSkill);
|
|
462
|
+
const sections = [];
|
|
463
|
+
let offset = 0;
|
|
464
|
+
|
|
465
|
+
if (loopItems.length) {
|
|
466
|
+
sections.push(renderSection("Loops", loopItems, offset));
|
|
467
|
+
offset += loopItems.length;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (skillItems.length) {
|
|
471
|
+
sections.push(renderSection("Skills", skillItems, offset));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return sections.join("\n\n");
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function renderSection(title, items, offset) {
|
|
478
|
+
return [
|
|
479
|
+
title,
|
|
480
|
+
...items.map((item, index) => {
|
|
481
|
+
const number = offset + index + 1;
|
|
482
|
+
const meta = [
|
|
483
|
+
item.status,
|
|
484
|
+
item.version,
|
|
485
|
+
item.stepCount ? `${item.stepCount} step${item.stepCount === 1 ? "" : "s"}` : null,
|
|
486
|
+
item.tags?.length ? item.tags.join(", ") : null,
|
|
487
|
+
]
|
|
488
|
+
.filter(Boolean)
|
|
489
|
+
.join(" | ");
|
|
490
|
+
const description = item.description ? ` - ${item.description}` : "";
|
|
491
|
+
return `${number}. ${item.name} (${item.slug}) [${meta}]${description}`;
|
|
492
|
+
}),
|
|
493
|
+
].join("\n");
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function showHelp() {
|
|
497
|
+
console.log(`SkillHub options
|
|
498
|
+
|
|
499
|
+
Commands:
|
|
500
|
+
skillhub options Show available loops and skills
|
|
501
|
+
skillhub choose Open a numbered picker and run one
|
|
502
|
+
skillhub run Same as choose
|
|
503
|
+
skillhub run <number|slug|name> Run a selected loop or skill
|
|
504
|
+
skillhub run loop <number|slug|name> Run a loop from the loop list
|
|
505
|
+
skillhub run skill <number|slug|name> Run a skill from the skill list
|
|
506
|
+
|
|
507
|
+
Flags:
|
|
508
|
+
--query <text> Filter by name, slug, tag, or summary
|
|
509
|
+
--prompt <text> Override the run/test prompt
|
|
510
|
+
--json Return machine-readable output
|
|
511
|
+
--no-prompt Print options instead of opening a picker
|
|
512
|
+
`);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function parseFlags(argv) {
|
|
516
|
+
const flags = {
|
|
517
|
+
filter: "",
|
|
518
|
+
json: false,
|
|
519
|
+
kind: "",
|
|
520
|
+
limit: 100,
|
|
521
|
+
noPrompt: false,
|
|
522
|
+
pick: "",
|
|
523
|
+
prompt: "",
|
|
524
|
+
query: "",
|
|
525
|
+
target: "",
|
|
526
|
+
};
|
|
527
|
+
const positional = [];
|
|
528
|
+
|
|
529
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
530
|
+
const arg = argv[index];
|
|
531
|
+
if (arg === "--filter") flags.filter = argv[++index] ?? "";
|
|
532
|
+
else if (arg === "--json") flags.json = true;
|
|
533
|
+
else if (arg === "--kind" || arg === "--type") flags.kind = normalizeKind(argv[++index] ?? "");
|
|
534
|
+
else if (arg === "--limit") flags.limit = Number(argv[++index] ?? 100);
|
|
535
|
+
else if (arg === "--no-prompt") flags.noPrompt = true;
|
|
536
|
+
else if (arg === "--pick") flags.pick = argv[++index] ?? "";
|
|
537
|
+
else if (arg === "--prompt") flags.prompt = argv[++index] ?? "";
|
|
538
|
+
else if (arg === "--query" || arg === "-q") flags.query = argv[++index] ?? "";
|
|
539
|
+
else positional.push(arg);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (positional[0] === "loop" || positional[0] === "loops") {
|
|
543
|
+
flags.kind = "loop";
|
|
544
|
+
flags.target = positional.slice(1).join(" ");
|
|
545
|
+
} else if (positional[0] === "skill" || positional[0] === "skills") {
|
|
546
|
+
flags.kind = "skill";
|
|
547
|
+
flags.target = positional.slice(1).join(" ");
|
|
548
|
+
} else {
|
|
549
|
+
flags.target = positional.join(" ");
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return flags;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function isTerminalRun(status) {
|
|
556
|
+
return ["completed", "failed", "canceled"].includes(status);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function sleep(ms) {
|
|
560
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function normalizeKind(value) {
|
|
564
|
+
if (value === "loops") return "loop";
|
|
565
|
+
if (value === "skills") return "skill";
|
|
566
|
+
if (value === "loop" || value === "skill") return value;
|
|
567
|
+
return "";
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function isLoop(item) {
|
|
571
|
+
return item.kind === "loop";
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function isSkill(item) {
|
|
575
|
+
return item.kind === "skill";
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function cleanOptional(value) {
|
|
579
|
+
const cleaned = value?.trim();
|
|
580
|
+
return cleaned ? cleaned : "";
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
584
|
+
await runCloudCatalogCommand(process.argv.slice(2)).catch((error) => {
|
|
585
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
586
|
+
process.exitCode = 1;
|
|
587
|
+
});
|
|
588
|
+
}
|