Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

agent-marketplace-seed.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

agent-marketplace-seed.tsBlame198 lines · 1 contributor
5ca514aClaude1/**
2 * Seed the agent marketplace with 4 example, admin-published listings on
3 * first boot. Idempotent — runs SELECT first and bails when any of the
4 * canonical slugs is already in the DB. Designed to fire-and-forget from
5 * `src/index.ts`; never throws past its own boundary.
6 *
7 * The listings wrap existing in-tree AI helpers:
8 * - "Gluecron AI Reviewer" → ai-review-trio
9 * - "Test Generator Bot" → ai-test-generator
10 * - "Doc Drift Watcher" → ai-doc-updater
11 * - "Security Patrol" → ai-patch-generator (weekly cadence)
12 *
13 * Publisher: the bootstrap admin (first user / SITE_ADMIN_USERNAME). When
14 * no admin exists yet we skip — the seed will pick up on a later boot.
15 */
16
17import { eq } from "drizzle-orm";
18import { db } from "../db";
19import { agentMarketplaceListings, users } from "../db/schema";
20import { getEnvAdminUsername } from "./admin-bootstrap";
21import { createListing } from "./agent-marketplace";
22
23interface SeedListing {
24 slug: string;
25 name: string;
26 tagline: string;
27 description: string;
28 category: "reviewer" | "tester" | "docs" | "security";
29 pricingModel: "free" | "per_invocation" | "per_repo_per_month";
30 priceCents: number;
31 agentTemplate: Record<string, unknown>;
32 sourceUrl?: string;
33}
34
35const SEEDS: SeedListing[] = [
36 {
37 slug: "gluecron-ai-reviewer",
38 name: "Gluecron AI Reviewer",
39 tagline:
40 "The official trio-review pass — security, correctness, style — on every PR.",
41 description:
42 "Runs the built-in `ai-review-trio` pipeline on every pull request. " +
43 "Three specialised passes (security, correctness, style) land as " +
44 "inline comments on the diff. Same agent the Gluecron team uses on " +
45 "its own monorepo.\n\n" +
46 "**Capabilities:** `pr:read`, `pr:comment:write`.\n",
47 category: "reviewer",
48 pricingModel: "per_invocation",
49 priceCents: 25,
50 agentTemplate: {
51 branchNamespace: "agents/ai-reviewer",
52 budgetCentsPerDay: 1000,
53 capabilities: ["pr:read", "pr:comment:write"],
54 handler: "ai-review-trio",
55 },
56 sourceUrl: "https://gluecron.com/ccantynz/Gluecron.com/blob/main/src/lib/ai-review-trio.ts",
57 },
58 {
59 slug: "test-generator-bot",
60 name: "Test Generator Bot",
61 tagline:
62 "Auto-writes failing tests for every uncovered function in a PR diff.",
63 description:
64 "Scans the PR diff, finds functions without test coverage, and opens " +
65 "a follow-up PR with generated unit tests. Backed by " +
66 "`ai-test-generator` — the same engine that powers the autopilot " +
67 "weekly test-gen sweep.\n",
68 category: "tester",
69 pricingModel: "per_invocation",
70 priceCents: 35,
71 agentTemplate: {
72 branchNamespace: "agents/test-gen",
73 budgetCentsPerDay: 1500,
74 capabilities: ["pr:read", "repo:write"],
75 handler: "ai-test-generator",
76 },
77 sourceUrl: "https://gluecron.com/ccantynz/Gluecron.com/blob/main/src/lib/ai-test-generator.ts",
78 },
79 {
80 slug: "doc-drift-watcher",
81 name: "Doc Drift Watcher",
82 tagline:
83 "Spots when code changes drift from docstrings and opens a doc-update PR.",
84 description:
85 "Watches every push and flags doc drift — functions whose " +
86 "implementation has changed but whose docstring hasn't. Opens a " +
87 "follow-up PR with the proposed doc update. Wraps " +
88 "`ai-doc-updater`.\n",
89 category: "docs",
90 pricingModel: "free",
91 priceCents: 0,
92 agentTemplate: {
93 branchNamespace: "agents/doc-drift",
94 budgetCentsPerDay: 500,
95 capabilities: ["repo:read", "pr:write"],
96 handler: "ai-doc-updater",
97 },
98 sourceUrl: "https://gluecron.com/ccantynz/Gluecron.com/blob/main/src/lib/ai-doc-updater.ts",
99 },
100 {
101 slug: "security-patrol",
102 name: "Security Patrol",
103 tagline:
104 "Weekly automated security scan + patch PRs for known CVEs in your deps.",
105 description:
106 "Runs once a week on a per-repo cadence. Scans your dependency " +
107 "tree for known CVEs (via the advisories DB), then opens a patch " +
108 "PR with the minimum-viable bump. Wraps `ai-patch-generator`.\n",
109 category: "security",
110 pricingModel: "per_repo_per_month",
111 priceCents: 500,
112 agentTemplate: {
113 branchNamespace: "agents/security-patrol",
114 budgetCentsPerDay: 2000,
115 capabilities: ["repo:read", "pr:write"],
116 handler: "ai-patch-generator",
117 cadence: "weekly",
118 },
119 sourceUrl: "https://gluecron.com/ccantynz/Gluecron.com/blob/main/src/lib/ai-patch-generator.ts",
120 },
121];
122
123/**
124 * Idempotent seed. Returns the number of new listings inserted. Skips
125 * silently when no admin user exists yet (so the very first boot of a
126 * fresh DB doesn't error).
127 */
128export async function ensureMarketplaceSeed(): Promise<number> {
129 let publisher: { id: string } | null = null;
130 try {
131 // Prefer the env-configured site admin if set, else oldest user.
132 const envAdminName = getEnvAdminUsername();
133 if (envAdminName) {
134 const [row] = await db
135 .select({ id: users.id })
136 .from(users)
137 .where(eq(users.username, envAdminName))
138 .limit(1);
139 if (row) publisher = row;
140 }
141 if (!publisher) {
142 const [row] = await db
143 .select({ id: users.id })
144 .from(users)
145 .orderBy(users.createdAt)
146 .limit(1);
147 if (row) publisher = row;
148 }
149 } catch {
150 return 0;
151 }
152 if (!publisher) return 0;
153
154 let inserted = 0;
155 for (const seed of SEEDS) {
156 try {
157 const [existing] = await db
158 .select({ id: agentMarketplaceListings.id })
159 .from(agentMarketplaceListings)
160 .where(eq(agentMarketplaceListings.slug, seed.slug))
161 .limit(1);
162 if (existing) continue;
163 const row = await createListing({
164 publisherUserId: publisher.id,
165 name: seed.name,
166 tagline: seed.tagline,
167 description: seed.description,
168 category: seed.category,
169 pricingModel: seed.pricingModel,
170 priceCents: seed.priceCents,
171 agentTemplate: seed.agentTemplate,
172 sourceUrl: seed.sourceUrl,
173 initialStatus: "approved",
174 });
175 if (row) {
176 // createListing sometimes invents a hex suffix on slug conflict; force
177 // the canonical slug back so subsequent runs see "already exists".
178 if (row.slug !== seed.slug) {
179 await db
180 .update(agentMarketplaceListings)
181 .set({ slug: seed.slug })
182 .where(eq(agentMarketplaceListings.id, row.id))
183 .catch(() => undefined);
184 }
185 inserted++;
186 }
187 } catch (err) {
188 console.warn(
189 `[agent-marketplace-seed] ${seed.slug} skipped:`,
190 err instanceof Error ? err.message : err
191 );
192 }
193 }
194 if (inserted > 0) {
195 console.log(`[agent-marketplace-seed] inserted ${inserted} listing(s)`);
196 }
197 return inserted;
198}