pilothub 0.0.1 → 0.0.2
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/LICENSE +1 -0
- package/README.md +36 -129
- package/dist/browserAuth.d.ts +20 -0
- package/dist/browserAuth.js +156 -0
- package/dist/browserAuth.js.map +1 -0
- package/dist/browserAuth.test.d.ts +1 -0
- package/dist/browserAuth.test.js +83 -0
- package/dist/browserAuth.test.js.map +1 -0
- package/dist/cli/buildInfo.d.ts +3 -0
- package/dist/cli/buildInfo.js +103 -0
- package/dist/cli/buildInfo.js.map +1 -0
- package/dist/cli/commands/auth.d.ts +9 -0
- package/dist/cli/commands/auth.js +75 -0
- package/dist/cli/commands/auth.js.map +1 -0
- package/dist/cli/commands/delete.d.ts +11 -0
- package/dist/cli/commands/delete.js +67 -0
- package/dist/cli/commands/delete.js.map +1 -0
- package/dist/cli/commands/delete.test.d.ts +1 -0
- package/dist/cli/commands/delete.test.js +52 -0
- package/dist/cli/commands/delete.test.js.map +1 -0
- package/dist/cli/commands/publish.d.ts +9 -0
- package/dist/cli/commands/publish.js +87 -0
- package/dist/cli/commands/publish.js.map +1 -0
- package/dist/cli/commands/publish.test.d.ts +1 -0
- package/dist/cli/commands/publish.test.js +104 -0
- package/dist/cli/commands/publish.test.js.map +1 -0
- package/dist/cli/commands/skills.d.ts +23 -0
- package/dist/cli/commands/skills.js +298 -0
- package/dist/cli/commands/skills.js.map +1 -0
- package/dist/cli/commands/skills.test.d.ts +1 -0
- package/dist/cli/commands/skills.test.js +156 -0
- package/dist/cli/commands/skills.test.js.map +1 -0
- package/dist/cli/commands/star.d.ts +8 -0
- package/dist/cli/commands/star.js +38 -0
- package/dist/cli/commands/star.js.map +1 -0
- package/dist/cli/commands/sync.d.ts +3 -0
- package/dist/cli/commands/sync.js +160 -0
- package/dist/cli/commands/sync.js.map +1 -0
- package/dist/cli/commands/sync.test.d.ts +1 -0
- package/dist/cli/commands/sync.test.js +277 -0
- package/dist/cli/commands/sync.test.js.map +1 -0
- package/dist/cli/commands/syncHelpers.d.ts +76 -0
- package/dist/cli/commands/syncHelpers.js +349 -0
- package/dist/cli/commands/syncHelpers.js.map +1 -0
- package/dist/cli/commands/syncHelpers.test.d.ts +1 -0
- package/dist/cli/commands/syncHelpers.test.js +22 -0
- package/dist/cli/commands/syncHelpers.test.js.map +1 -0
- package/dist/cli/commands/syncTypes.d.ts +24 -0
- package/dist/cli/commands/syncTypes.js +2 -0
- package/dist/cli/commands/syncTypes.js.map +1 -0
- package/dist/cli/commands/unstar.d.ts +8 -0
- package/dist/cli/commands/unstar.js +38 -0
- package/dist/cli/commands/unstar.js.map +1 -0
- package/dist/cli/helpStyle.d.ts +13 -0
- package/dist/cli/helpStyle.js +38 -0
- package/dist/cli/helpStyle.js.map +1 -0
- package/dist/cli/pilotbotConfig.d.ts +6 -0
- package/dist/cli/pilotbotConfig.js +110 -0
- package/dist/cli/pilotbotConfig.js.map +1 -0
- package/dist/cli/pilotbotConfig.test.d.ts +1 -0
- package/dist/cli/pilotbotConfig.test.js +133 -0
- package/dist/cli/pilotbotConfig.test.js.map +1 -0
- package/dist/cli/registry.d.ts +7 -0
- package/dist/cli/registry.js +42 -0
- package/dist/cli/registry.js.map +1 -0
- package/dist/cli/registry.test.d.ts +1 -0
- package/dist/cli/registry.test.js +48 -0
- package/dist/cli/registry.test.js.map +1 -0
- package/dist/cli/scanSkills.d.ts +7 -0
- package/dist/cli/scanSkills.js +75 -0
- package/dist/cli/scanSkills.js.map +1 -0
- package/dist/cli/scanSkills.test.d.ts +1 -0
- package/dist/cli/scanSkills.test.js +60 -0
- package/dist/cli/scanSkills.test.js.map +1 -0
- package/dist/cli/slug.d.ts +2 -0
- package/dist/cli/slug.js +16 -0
- package/dist/cli/slug.js.map +1 -0
- package/dist/cli/types.d.ts +15 -0
- package/dist/cli/types.js +2 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/cli/ui.d.ts +7 -0
- package/dist/cli/ui.js +72 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +268 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js +38 -0
- package/dist/config.js.map +1 -0
- package/dist/discovery.d.ts +5 -0
- package/dist/discovery.js +21 -0
- package/dist/discovery.js.map +1 -0
- package/dist/discovery.test.d.ts +1 -0
- package/dist/discovery.test.js +46 -0
- package/dist/discovery.test.js.map +1 -0
- package/dist/http.d.ts +32 -0
- package/dist/http.js +261 -0
- package/dist/http.js.map +1 -0
- package/dist/http.test.d.ts +1 -0
- package/dist/http.test.js +135 -0
- package/dist/http.test.js.map +1 -0
- package/dist/schema/ark.js.map +1 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/routes.js.map +1 -0
- package/{packages/schema/dist → dist/schema}/schemas.d.ts +0 -39
- package/{packages/schema/dist → dist/schema}/schemas.js +0 -22
- package/dist/schema/schemas.js.map +1 -0
- package/dist/schema/textFiles.js.map +1 -0
- package/dist/schema/textFiles.test.d.ts +1 -0
- package/dist/schema/textFiles.test.js +20 -0
- package/dist/schema/textFiles.test.js.map +1 -0
- package/dist/skills.d.ts +43 -0
- package/dist/skills.js +163 -0
- package/dist/skills.js.map +1 -0
- package/dist/skills.test.d.ts +1 -0
- package/dist/skills.test.js +144 -0
- package/dist/skills.test.js.map +1 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +27 -70
- package/.env.local.example +0 -19
- package/.github/workflows/ci.yml +0 -40
- package/.oxlintrc.json +0 -3
- package/AGENTS.md +0 -45
- package/CHANGELOG.md +0 -138
- package/DEPRECATIONS.md +0 -7
- package/biome.json +0 -41
- package/convex/_generated/api.d.ts +0 -153
- package/convex/_generated/api.js +0 -23
- package/convex/_generated/dataModel.d.ts +0 -60
- package/convex/_generated/server.d.ts +0 -143
- package/convex/_generated/server.js +0 -93
- package/convex/auth.config.ts +0 -8
- package/convex/auth.ts +0 -19
- package/convex/comments.ts +0 -88
- package/convex/crons.ts +0 -34
- package/convex/devSeed.ts +0 -459
- package/convex/devSeedExtra.ts +0 -541
- package/convex/downloads.ts +0 -78
- package/convex/githubBackups.ts +0 -170
- package/convex/githubBackupsNode.ts +0 -183
- package/convex/githubImport.ts +0 -317
- package/convex/githubSoulBackups.ts +0 -170
- package/convex/githubSoulBackupsNode.ts +0 -186
- package/convex/http.ts +0 -194
- package/convex/httpApi.handlers.test.ts +0 -488
- package/convex/httpApi.test.ts +0 -70
- package/convex/httpApi.ts +0 -305
- package/convex/httpApiV1.handlers.test.ts +0 -584
- package/convex/httpApiV1.ts +0 -1172
- package/convex/leaderboards.ts +0 -39
- package/convex/lib/access.ts +0 -36
- package/convex/lib/apiTokenAuth.ts +0 -36
- package/convex/lib/badges.ts +0 -50
- package/convex/lib/changelog.test.ts +0 -34
- package/convex/lib/changelog.ts +0 -278
- package/convex/lib/embeddings.ts +0 -38
- package/convex/lib/githubBackup.ts +0 -443
- package/convex/lib/githubImport.test.ts +0 -247
- package/convex/lib/githubImport.ts +0 -425
- package/convex/lib/githubSoulBackup.ts +0 -443
- package/convex/lib/leaderboards.ts +0 -103
- package/convex/lib/moderation.ts +0 -42
- package/convex/lib/public.ts +0 -89
- package/convex/lib/searchText.test.ts +0 -46
- package/convex/lib/searchText.ts +0 -27
- package/convex/lib/skillBackfill.test.ts +0 -34
- package/convex/lib/skillBackfill.ts +0 -67
- package/convex/lib/skillPublish.test.ts +0 -28
- package/convex/lib/skillPublish.ts +0 -284
- package/convex/lib/skillStats.ts +0 -80
- package/convex/lib/skills.test.ts +0 -197
- package/convex/lib/skills.ts +0 -273
- package/convex/lib/soulChangelog.ts +0 -273
- package/convex/lib/soulPublish.ts +0 -236
- package/convex/lib/tokens.test.ts +0 -33
- package/convex/lib/tokens.ts +0 -51
- package/convex/lib/webhooks.test.ts +0 -91
- package/convex/lib/webhooks.ts +0 -112
- package/convex/maintenance.test.ts +0 -270
- package/convex/maintenance.ts +0 -840
- package/convex/rateLimits.ts +0 -50
- package/convex/schema.ts +0 -472
- package/convex/search.test.ts +0 -12
- package/convex/search.ts +0 -254
- package/convex/seed.test.ts +0 -37
- package/convex/seed.ts +0 -254
- package/convex/seedSouls.ts +0 -111
- package/convex/skillStatEvents.ts +0 -568
- package/convex/skills.ts +0 -1606
- package/convex/soulComments.ts +0 -88
- package/convex/soulDownloads.ts +0 -14
- package/convex/soulStars.ts +0 -71
- package/convex/souls.ts +0 -570
- package/convex/stars.ts +0 -108
- package/convex/statsMaintenance.ts +0 -205
- package/convex/telemetry.ts +0 -434
- package/convex/tokens.ts +0 -88
- package/convex/tsconfig.json +0 -7
- package/convex/uploads.ts +0 -20
- package/convex/users.ts +0 -122
- package/convex/webhooks.ts +0 -50
- package/convex.json +0 -3
- package/docs/README.md +0 -32
- package/docs/api.md +0 -51
- package/docs/architecture.md +0 -61
- package/docs/auth.md +0 -54
- package/docs/cli.md +0 -117
- package/docs/deploy.md +0 -78
- package/docs/diffing.md +0 -84
- package/docs/github-import.md +0 -171
- package/docs/http-api.md +0 -187
- package/docs/manual-testing.md +0 -64
- package/docs/mintlify.md +0 -43
- package/docs/quickstart.md +0 -120
- package/docs/skill-format.md +0 -58
- package/docs/soul-format.md +0 -37
- package/docs/spec.md +0 -177
- package/docs/telemetry.md +0 -91
- package/docs/troubleshooting.md +0 -49
- package/docs/webhook.md +0 -51
- package/e2e/menu-smoke.pw.test.ts +0 -49
- package/e2e/pilothub.e2e.test.ts +0 -494
- package/e2e/search-exact.pw.test.ts +0 -97
- package/packages/pilothub/LICENSE +0 -22
- package/packages/pilothub/README.md +0 -57
- package/packages/pilothub/package.json +0 -41
- package/packages/pilothub/src/browserAuth.test.ts +0 -96
- package/packages/pilothub/src/browserAuth.ts +0 -174
- package/packages/pilothub/src/cli/buildInfo.ts +0 -94
- package/packages/pilothub/src/cli/commands/auth.ts +0 -97
- package/packages/pilothub/src/cli/commands/delete.test.ts +0 -73
- package/packages/pilothub/src/cli/commands/delete.ts +0 -83
- package/packages/pilothub/src/cli/commands/publish.test.ts +0 -122
- package/packages/pilothub/src/cli/commands/publish.ts +0 -108
- package/packages/pilothub/src/cli/commands/skills.test.ts +0 -191
- package/packages/pilothub/src/cli/commands/skills.ts +0 -380
- package/packages/pilothub/src/cli/commands/star.ts +0 -46
- package/packages/pilothub/src/cli/commands/sync.test.ts +0 -310
- package/packages/pilothub/src/cli/commands/sync.ts +0 -200
- package/packages/pilothub/src/cli/commands/syncHelpers.test.ts +0 -26
- package/packages/pilothub/src/cli/commands/syncHelpers.ts +0 -427
- package/packages/pilothub/src/cli/commands/syncTypes.ts +0 -27
- package/packages/pilothub/src/cli/commands/unstar.ts +0 -48
- package/packages/pilothub/src/cli/helpStyle.ts +0 -45
- package/packages/pilothub/src/cli/pilotbotConfig.test.ts +0 -159
- package/packages/pilothub/src/cli/pilotbotConfig.ts +0 -147
- package/packages/pilothub/src/cli/registry.test.ts +0 -63
- package/packages/pilothub/src/cli/registry.ts +0 -43
- package/packages/pilothub/src/cli/scanSkills.test.ts +0 -64
- package/packages/pilothub/src/cli/scanSkills.ts +0 -84
- package/packages/pilothub/src/cli/slug.ts +0 -16
- package/packages/pilothub/src/cli/types.ts +0 -12
- package/packages/pilothub/src/cli/ui.ts +0 -75
- package/packages/pilothub/src/cli.ts +0 -311
- package/packages/pilothub/src/config.ts +0 -36
- package/packages/pilothub/src/discovery.test.ts +0 -75
- package/packages/pilothub/src/discovery.ts +0 -19
- package/packages/pilothub/src/http.test.ts +0 -156
- package/packages/pilothub/src/http.ts +0 -301
- package/packages/pilothub/src/schema/ark.ts +0 -29
- package/packages/pilothub/src/schema/index.ts +0 -5
- package/packages/pilothub/src/schema/routes.ts +0 -22
- package/packages/pilothub/src/schema/schemas.ts +0 -260
- package/packages/pilothub/src/schema/textFiles.test.ts +0 -23
- package/packages/pilothub/src/schema/textFiles.ts +0 -66
- package/packages/pilothub/src/skills.test.ts +0 -191
- package/packages/pilothub/src/skills.ts +0 -172
- package/packages/pilothub/src/types.ts +0 -10
- package/packages/pilothub/tsconfig.json +0 -14
- package/packages/schema/README.md +0 -3
- package/packages/schema/dist/ark.js.map +0 -1
- package/packages/schema/dist/index.js.map +0 -1
- package/packages/schema/dist/routes.js.map +0 -1
- package/packages/schema/dist/schemas.js.map +0 -1
- package/packages/schema/dist/textFiles.js.map +0 -1
- package/packages/schema/package.json +0 -26
- package/packages/schema/src/ark.ts +0 -29
- package/packages/schema/src/index.ts +0 -5
- package/packages/schema/src/routes.ts +0 -22
- package/packages/schema/src/schemas.test.ts +0 -123
- package/packages/schema/src/schemas.ts +0 -287
- package/packages/schema/src/textFiles.test.ts +0 -23
- package/packages/schema/src/textFiles.ts +0 -66
- package/packages/schema/tsconfig.json +0 -15
- package/pilothub +0 -46
- package/playwright.config.ts +0 -33
- package/public/.well-known/pilothub.json +0 -6
- package/public/api/v1/openapi.json +0 -379
- package/public/favicon.ico +0 -0
- package/public/logo192.png +0 -0
- package/public/logo512.png +0 -0
- package/public/manifest.json +0 -25
- package/public/og.png +0 -0
- package/public/og.svg +0 -98
- package/public/pilot-logo.png +0 -0
- package/public/pilot-mark.png +0 -0
- package/public/robots.txt +0 -3
- package/public/tanstack-circle-logo.png +0 -0
- package/public/tanstack-word-logo-white.svg +0 -1
- package/scripts/check-peer-deps.ts +0 -56
- package/scripts/docs-list.ts +0 -148
- package/scripts/run-playwright-local.sh +0 -14
- package/server/og/fetchSkillOgMeta.ts +0 -27
- package/server/og/fetchSoulOgMeta.ts +0 -27
- package/server/og/ogAssets.ts +0 -80
- package/server/og/skillOgSvg.test.ts +0 -59
- package/server/og/skillOgSvg.ts +0 -258
- package/server/og/soulOgSvg.ts +0 -209
- package/server/routes/og/skill.png.ts +0 -103
- package/server/routes/og/soul.png.ts +0 -111
- package/src/__tests__/skill-detail-page.test.tsx +0 -86
- package/src/__tests__/skills-index.test.tsx +0 -145
- package/src/__tests__/upload.route.test.tsx +0 -228
- package/src/components/AppProviders.tsx +0 -19
- package/src/components/ClientOnly.tsx +0 -18
- package/src/components/Footer.tsx +0 -29
- package/src/components/Header.tsx +0 -295
- package/src/components/InstallSwitcher.tsx +0 -53
- package/src/components/SkillCard.tsx +0 -36
- package/src/components/SkillDetailPage.tsx +0 -817
- package/src/components/SkillDiffCard.tsx +0 -485
- package/src/components/SoulCard.tsx +0 -19
- package/src/components/SoulDetailPage.tsx +0 -263
- package/src/components/UserBootstrap.tsx +0 -18
- package/src/components/ui/dropdown-menu.tsx +0 -67
- package/src/components/ui/toggle-group.tsx +0 -35
- package/src/convex/client.ts +0 -3
- package/src/lib/badges.ts +0 -29
- package/src/lib/diffing.test.ts +0 -163
- package/src/lib/diffing.ts +0 -106
- package/src/lib/gravatar.test.ts +0 -9
- package/src/lib/gravatar.ts +0 -158
- package/src/lib/og.test.ts +0 -142
- package/src/lib/og.ts +0 -156
- package/src/lib/publicUser.ts +0 -39
- package/src/lib/roles.ts +0 -19
- package/src/lib/site.test.ts +0 -130
- package/src/lib/site.ts +0 -84
- package/src/lib/theme-transition.test.ts +0 -134
- package/src/lib/theme-transition.ts +0 -134
- package/src/lib/theme.test.tsx +0 -88
- package/src/lib/theme.ts +0 -43
- package/src/lib/uploadFiles.jsdom.test.ts +0 -33
- package/src/lib/uploadFiles.test.ts +0 -123
- package/src/lib/uploadFiles.ts +0 -245
- package/src/lib/uploadUtils.test.ts +0 -78
- package/src/lib/uploadUtils.ts +0 -93
- package/src/lib/useAuthStatus.ts +0 -12
- package/src/lib/utils.test.ts +0 -9
- package/src/lib/utils.ts +0 -6
- package/src/logo.svg +0 -12
- package/src/routeTree.gen.ts +0 -345
- package/src/router.tsx +0 -17
- package/src/routes/$owner/$slug.tsx +0 -55
- package/src/routes/__root.tsx +0 -136
- package/src/routes/admin.tsx +0 -11
- package/src/routes/cli/auth.tsx +0 -168
- package/src/routes/dashboard.tsx +0 -97
- package/src/routes/import.tsx +0 -415
- package/src/routes/index.tsx +0 -252
- package/src/routes/management.tsx +0 -529
- package/src/routes/settings.tsx +0 -203
- package/src/routes/skills/index.tsx +0 -422
- package/src/routes/souls/$slug.tsx +0 -55
- package/src/routes/souls/index.tsx +0 -243
- package/src/routes/stars.tsx +0 -68
- package/src/routes/u/$handle.tsx +0 -307
- package/src/routes/upload/utils.ts +0 -81
- package/src/routes/upload.tsx +0 -499
- package/src/styles.css +0 -2718
- package/tsconfig.json +0 -24
- package/tsconfig.oxlint.json +0 -16
- package/vercel.json +0 -8
- package/vite.config.ts +0 -48
- package/vitest.config.ts +0 -47
- package/vitest.e2e.config.ts +0 -11
- package/vitest.setup.ts +0 -1
- /package/{packages/pilothub/bin → bin}/pilothub.js +0 -0
- /package/{packages/schema/dist → dist/schema}/ark.d.ts +0 -0
- /package/{packages/schema/dist → dist/schema}/ark.js +0 -0
- /package/{packages/schema/dist → dist/schema}/index.d.ts +0 -0
- /package/{packages/schema/dist → dist/schema}/index.js +0 -0
- /package/{packages/schema/dist → dist/schema}/routes.d.ts +0 -0
- /package/{packages/schema/dist → dist/schema}/routes.js +0 -0
- /package/{packages/schema/dist → dist/schema}/textFiles.d.ts +0 -0
- /package/{packages/schema/dist → dist/schema}/textFiles.js +0 -0
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
import { internal } from '../_generated/api'
|
|
2
|
-
import type { Doc } from '../_generated/dataModel'
|
|
3
|
-
import type { ActionCtx } from '../_generated/server'
|
|
4
|
-
|
|
5
|
-
const CHANGELOG_MODEL = process.env.OPENAI_CHANGELOG_MODEL ?? 'gpt-4.1'
|
|
6
|
-
const MAX_README_CHARS = 8_000
|
|
7
|
-
const MAX_PATHS_IN_PROMPT = 30
|
|
8
|
-
|
|
9
|
-
type FileMeta = { path: string; sha256?: string }
|
|
10
|
-
|
|
11
|
-
type FileDiffSummary = {
|
|
12
|
-
added: string[]
|
|
13
|
-
removed: string[]
|
|
14
|
-
changed: string[]
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function clampText(value: string, maxChars: number) {
|
|
18
|
-
const trimmed = value.trim()
|
|
19
|
-
if (trimmed.length <= maxChars) return trimmed
|
|
20
|
-
return `${trimmed.slice(0, maxChars).trimEnd()}\n…`
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function summarizeFileDiff(oldFiles: FileMeta[], nextFiles: FileMeta[]): FileDiffSummary {
|
|
24
|
-
const oldByPath = new Map(oldFiles.map((f) => [f.path, f] as const))
|
|
25
|
-
const nextByPath = new Map(nextFiles.map((f) => [f.path, f] as const))
|
|
26
|
-
|
|
27
|
-
const added: string[] = []
|
|
28
|
-
const removed: string[] = []
|
|
29
|
-
const changed: string[] = []
|
|
30
|
-
|
|
31
|
-
for (const [path, file] of nextByPath.entries()) {
|
|
32
|
-
const prev = oldByPath.get(path)
|
|
33
|
-
if (!prev) {
|
|
34
|
-
added.push(path)
|
|
35
|
-
continue
|
|
36
|
-
}
|
|
37
|
-
if (file.sha256 && prev.sha256 && file.sha256 !== prev.sha256) changed.push(path)
|
|
38
|
-
}
|
|
39
|
-
for (const path of oldByPath.keys()) {
|
|
40
|
-
if (!nextByPath.has(path)) removed.push(path)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
added.sort()
|
|
44
|
-
removed.sort()
|
|
45
|
-
changed.sort()
|
|
46
|
-
return { added, removed, changed }
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function formatDiffSummary(diff: FileDiffSummary) {
|
|
50
|
-
const parts: string[] = []
|
|
51
|
-
if (diff.added.length) parts.push(`${diff.added.length} added`)
|
|
52
|
-
if (diff.changed.length) parts.push(`${diff.changed.length} changed`)
|
|
53
|
-
if (diff.removed.length) parts.push(`${diff.removed.length} removed`)
|
|
54
|
-
return parts.join(', ') || 'no file changes detected'
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function pickPaths(values: string[]) {
|
|
58
|
-
if (values.length <= MAX_PATHS_IN_PROMPT) return values
|
|
59
|
-
return values.slice(0, MAX_PATHS_IN_PROMPT)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function extractResponseText(payload: unknown) {
|
|
63
|
-
if (!payload || typeof payload !== 'object') return null
|
|
64
|
-
const output = (payload as { output?: unknown }).output
|
|
65
|
-
if (!Array.isArray(output)) return null
|
|
66
|
-
const chunks: string[] = []
|
|
67
|
-
for (const item of output) {
|
|
68
|
-
if (!item || typeof item !== 'object') continue
|
|
69
|
-
if ((item as { type?: unknown }).type !== 'message') continue
|
|
70
|
-
const content = (item as { content?: unknown }).content
|
|
71
|
-
if (!Array.isArray(content)) continue
|
|
72
|
-
for (const part of content) {
|
|
73
|
-
if (!part || typeof part !== 'object') continue
|
|
74
|
-
if ((part as { type?: unknown }).type !== 'output_text') continue
|
|
75
|
-
const text = (part as { text?: unknown }).text
|
|
76
|
-
if (typeof text === 'string' && text.trim()) chunks.push(text)
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
const joined = chunks.join('\n').trim()
|
|
80
|
-
return joined || null
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async function generateWithOpenAI(args: {
|
|
84
|
-
slug: string
|
|
85
|
-
version: string
|
|
86
|
-
oldReadme: string | null
|
|
87
|
-
nextReadme: string
|
|
88
|
-
fileDiff: FileDiffSummary | null
|
|
89
|
-
}) {
|
|
90
|
-
const apiKey = process.env.OPENAI_API_KEY
|
|
91
|
-
if (!apiKey) return null
|
|
92
|
-
|
|
93
|
-
const oldReadme = args.oldReadme ? clampText(args.oldReadme, MAX_README_CHARS) : ''
|
|
94
|
-
const nextReadme = clampText(args.nextReadme, MAX_README_CHARS)
|
|
95
|
-
|
|
96
|
-
const fileDiff = args.fileDiff
|
|
97
|
-
const diffSummary = fileDiff ? formatDiffSummary(fileDiff) : 'unknown'
|
|
98
|
-
const changedPaths = fileDiff ? pickPaths(fileDiff.changed) : []
|
|
99
|
-
const addedPaths = fileDiff ? pickPaths(fileDiff.added) : []
|
|
100
|
-
const removedPaths = fileDiff ? pickPaths(fileDiff.removed) : []
|
|
101
|
-
|
|
102
|
-
const input = [
|
|
103
|
-
`Soul: ${args.slug}`,
|
|
104
|
-
`Version: ${args.version}`,
|
|
105
|
-
`File changes: ${diffSummary}`,
|
|
106
|
-
changedPaths.length ? `Changed files (sample): ${changedPaths.join(', ')}` : null,
|
|
107
|
-
addedPaths.length ? `Added files (sample): ${addedPaths.join(', ')}` : null,
|
|
108
|
-
removedPaths.length ? `Removed files (sample): ${removedPaths.join(', ')}` : null,
|
|
109
|
-
oldReadme ? `Previous SOUL.md:\n${oldReadme}` : null,
|
|
110
|
-
`New SOUL.md:\n${nextReadme}`,
|
|
111
|
-
]
|
|
112
|
-
.filter(Boolean)
|
|
113
|
-
.join('\n\n')
|
|
114
|
-
|
|
115
|
-
const response = await fetch('https://api.openai.com/v1/responses', {
|
|
116
|
-
method: 'POST',
|
|
117
|
-
headers: {
|
|
118
|
-
'Content-Type': 'application/json',
|
|
119
|
-
Authorization: `Bearer ${apiKey}`,
|
|
120
|
-
},
|
|
121
|
-
body: JSON.stringify({
|
|
122
|
-
model: CHANGELOG_MODEL,
|
|
123
|
-
instructions:
|
|
124
|
-
'Write a concise changelog for this soul version. Audience: everyone. Output plain text. Prefer 2–6 bullet points. If it is a big change, include a short 1-line summary first, then bullets. Don’t mention that you are AI. Don’t invent details; only use the inputs.',
|
|
125
|
-
input,
|
|
126
|
-
max_output_tokens: 220,
|
|
127
|
-
}),
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
if (!response.ok) return null
|
|
131
|
-
const payload = (await response.json()) as unknown
|
|
132
|
-
return extractResponseText(payload)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function generateFallback(args: {
|
|
136
|
-
slug: string
|
|
137
|
-
version: string
|
|
138
|
-
oldReadme: string | null
|
|
139
|
-
nextReadme: string
|
|
140
|
-
fileDiff: FileDiffSummary | null
|
|
141
|
-
}) {
|
|
142
|
-
const lines: string[] = []
|
|
143
|
-
if (!args.oldReadme) {
|
|
144
|
-
lines.push(`- Initial release.`)
|
|
145
|
-
return lines.join('\n')
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const diff = args.fileDiff
|
|
149
|
-
if (diff) {
|
|
150
|
-
const parts: string[] = []
|
|
151
|
-
if (diff.added.length) parts.push(`added ${diff.added.length}`)
|
|
152
|
-
if (diff.changed.length) parts.push(`updated ${diff.changed.length}`)
|
|
153
|
-
if (diff.removed.length) parts.push(`removed ${diff.removed.length}`)
|
|
154
|
-
if (parts.length) lines.push(`- ${parts.join(', ')} file(s).`)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
lines.push(`- Updated SOUL.md.`)
|
|
158
|
-
return lines.join('\n')
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export async function generateSoulChangelogForPublish(
|
|
162
|
-
ctx: ActionCtx,
|
|
163
|
-
args: { slug: string; version: string; readmeText: string; files: FileMeta[] },
|
|
164
|
-
): Promise<string> {
|
|
165
|
-
try {
|
|
166
|
-
const soul = (await ctx.runQuery(internal.souls.getSoulBySlugInternal, {
|
|
167
|
-
slug: args.slug,
|
|
168
|
-
})) as Doc<'souls'> | null
|
|
169
|
-
const previous: Doc<'soulVersions'> | null =
|
|
170
|
-
soul?.latestVersionId && !soul.softDeletedAt
|
|
171
|
-
? ((await ctx.runQuery(internal.souls.getVersionByIdInternal, {
|
|
172
|
-
versionId: soul.latestVersionId,
|
|
173
|
-
})) as Doc<'soulVersions'> | null)
|
|
174
|
-
: null
|
|
175
|
-
|
|
176
|
-
const oldReadmeText: string | null = previous
|
|
177
|
-
? await readReadmeFromVersion(ctx, previous)
|
|
178
|
-
: null
|
|
179
|
-
const oldFiles = previous
|
|
180
|
-
? previous.files.map((file) => ({ path: file.path, sha256: file.sha256 }))
|
|
181
|
-
: []
|
|
182
|
-
const fileDiff = previous ? summarizeFileDiff(oldFiles, args.files) : null
|
|
183
|
-
|
|
184
|
-
const ai = await generateWithOpenAI({
|
|
185
|
-
slug: args.slug,
|
|
186
|
-
version: args.version,
|
|
187
|
-
oldReadme: oldReadmeText,
|
|
188
|
-
nextReadme: args.readmeText,
|
|
189
|
-
fileDiff,
|
|
190
|
-
}).catch(() => null)
|
|
191
|
-
|
|
192
|
-
return (
|
|
193
|
-
ai ??
|
|
194
|
-
generateFallback({
|
|
195
|
-
slug: args.slug,
|
|
196
|
-
version: args.version,
|
|
197
|
-
oldReadme: oldReadmeText,
|
|
198
|
-
nextReadme: args.readmeText,
|
|
199
|
-
fileDiff,
|
|
200
|
-
})
|
|
201
|
-
)
|
|
202
|
-
} catch {
|
|
203
|
-
return '- Updated soul.'
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
export async function generateSoulChangelogPreview(
|
|
208
|
-
ctx: ActionCtx,
|
|
209
|
-
args: {
|
|
210
|
-
slug: string
|
|
211
|
-
version: string
|
|
212
|
-
readmeText: string
|
|
213
|
-
filePaths?: string[]
|
|
214
|
-
},
|
|
215
|
-
): Promise<string> {
|
|
216
|
-
try {
|
|
217
|
-
const soul = (await ctx.runQuery(internal.souls.getSoulBySlugInternal, {
|
|
218
|
-
slug: args.slug,
|
|
219
|
-
})) as Doc<'souls'> | null
|
|
220
|
-
const previous: Doc<'soulVersions'> | null =
|
|
221
|
-
soul?.latestVersionId && !soul.softDeletedAt
|
|
222
|
-
? ((await ctx.runQuery(internal.souls.getVersionByIdInternal, {
|
|
223
|
-
versionId: soul.latestVersionId,
|
|
224
|
-
})) as Doc<'soulVersions'> | null)
|
|
225
|
-
: null
|
|
226
|
-
|
|
227
|
-
const oldReadmeText: string | null = previous
|
|
228
|
-
? await readReadmeFromVersion(ctx, previous)
|
|
229
|
-
: null
|
|
230
|
-
const oldPaths = previous ? previous.files.map((file) => file.path) : []
|
|
231
|
-
const nextPaths = args.filePaths ?? []
|
|
232
|
-
const diff = previous ? summarizeFileDiffFromPaths(oldPaths, nextPaths) : null
|
|
233
|
-
|
|
234
|
-
const ai = await generateWithOpenAI({
|
|
235
|
-
slug: args.slug,
|
|
236
|
-
version: args.version,
|
|
237
|
-
oldReadme: oldReadmeText,
|
|
238
|
-
nextReadme: args.readmeText,
|
|
239
|
-
fileDiff: diff,
|
|
240
|
-
}).catch(() => null)
|
|
241
|
-
|
|
242
|
-
return (
|
|
243
|
-
ai ??
|
|
244
|
-
generateFallback({
|
|
245
|
-
slug: args.slug,
|
|
246
|
-
version: args.version,
|
|
247
|
-
oldReadme: oldReadmeText,
|
|
248
|
-
nextReadme: args.readmeText,
|
|
249
|
-
fileDiff: diff,
|
|
250
|
-
})
|
|
251
|
-
)
|
|
252
|
-
} catch {
|
|
253
|
-
return '- Updated soul.'
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
async function readReadmeFromVersion(ctx: ActionCtx, version: Doc<'soulVersions'>) {
|
|
258
|
-
const file = version.files.find((entry) => entry.path.toLowerCase() === 'soul.md')
|
|
259
|
-
if (!file) return null
|
|
260
|
-
const blob = await ctx.storage.get(file.storageId)
|
|
261
|
-
if (!blob) return null
|
|
262
|
-
return blob.text()
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function summarizeFileDiffFromPaths(oldPaths: string[], nextPaths: string[]) {
|
|
266
|
-
const oldFiles = oldPaths.map((path) => ({ path }))
|
|
267
|
-
const nextFiles = nextPaths.map((path) => ({ path }))
|
|
268
|
-
return summarizeFileDiff(oldFiles, nextFiles)
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
export const __test = {
|
|
272
|
-
summarizeFileDiff,
|
|
273
|
-
}
|
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
import { ConvexError } from 'convex/values'
|
|
2
|
-
import semver from 'semver'
|
|
3
|
-
import { internal } from '../_generated/api'
|
|
4
|
-
import type { Doc, Id } from '../_generated/dataModel'
|
|
5
|
-
import type { ActionCtx } from '../_generated/server'
|
|
6
|
-
import { generateEmbedding } from './embeddings'
|
|
7
|
-
import {
|
|
8
|
-
buildEmbeddingText,
|
|
9
|
-
getFrontmatterMetadata,
|
|
10
|
-
getFrontmatterValue,
|
|
11
|
-
hashSkillFiles,
|
|
12
|
-
isTextFile,
|
|
13
|
-
parseFrontmatter,
|
|
14
|
-
sanitizePath,
|
|
15
|
-
} from './skills'
|
|
16
|
-
import { generateSoulChangelogForPublish } from './soulChangelog'
|
|
17
|
-
|
|
18
|
-
const MAX_TOTAL_BYTES = 50 * 1024 * 1024
|
|
19
|
-
|
|
20
|
-
const MAX_SUMMARY_LENGTH = 160
|
|
21
|
-
|
|
22
|
-
function deriveSoulSummary(readmeText: string) {
|
|
23
|
-
const lines = readmeText.split(/\r?\n/)
|
|
24
|
-
let inFrontmatter = false
|
|
25
|
-
for (const raw of lines) {
|
|
26
|
-
const trimmed = raw.trim()
|
|
27
|
-
if (!trimmed) continue
|
|
28
|
-
if (!inFrontmatter && trimmed === '---') {
|
|
29
|
-
inFrontmatter = true
|
|
30
|
-
continue
|
|
31
|
-
}
|
|
32
|
-
if (inFrontmatter) {
|
|
33
|
-
if (trimmed === '---') {
|
|
34
|
-
inFrontmatter = false
|
|
35
|
-
}
|
|
36
|
-
continue
|
|
37
|
-
}
|
|
38
|
-
const cleaned = trimmed.replace(/^#+\s*/, '')
|
|
39
|
-
if (!cleaned) continue
|
|
40
|
-
if (cleaned.length > MAX_SUMMARY_LENGTH) {
|
|
41
|
-
return `${cleaned.slice(0, MAX_SUMMARY_LENGTH - 3).trimEnd()}...`
|
|
42
|
-
}
|
|
43
|
-
return cleaned
|
|
44
|
-
}
|
|
45
|
-
return undefined
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export type PublishResult = {
|
|
49
|
-
soulId: Id<'souls'>
|
|
50
|
-
versionId: Id<'soulVersions'>
|
|
51
|
-
embeddingId: Id<'soulEmbeddings'>
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export type PublishVersionArgs = {
|
|
55
|
-
slug: string
|
|
56
|
-
displayName: string
|
|
57
|
-
version: string
|
|
58
|
-
changelog: string
|
|
59
|
-
tags?: string[]
|
|
60
|
-
source?: {
|
|
61
|
-
kind: 'github'
|
|
62
|
-
url: string
|
|
63
|
-
repo: string
|
|
64
|
-
ref: string
|
|
65
|
-
commit: string
|
|
66
|
-
path: string
|
|
67
|
-
importedAt: number
|
|
68
|
-
}
|
|
69
|
-
files: Array<{
|
|
70
|
-
path: string
|
|
71
|
-
size: number
|
|
72
|
-
storageId: Id<'_storage'>
|
|
73
|
-
sha256: string
|
|
74
|
-
contentType?: string
|
|
75
|
-
}>
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export async function publishSoulVersionForUser(
|
|
79
|
-
ctx: ActionCtx,
|
|
80
|
-
userId: Id<'users'>,
|
|
81
|
-
args: PublishVersionArgs,
|
|
82
|
-
): Promise<PublishResult> {
|
|
83
|
-
const version = args.version.trim()
|
|
84
|
-
const slug = args.slug.trim().toLowerCase()
|
|
85
|
-
const displayName = args.displayName.trim()
|
|
86
|
-
if (!slug || !displayName) throw new ConvexError('Slug and display name required')
|
|
87
|
-
if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) {
|
|
88
|
-
throw new ConvexError('Slug must be lowercase and url-safe')
|
|
89
|
-
}
|
|
90
|
-
if (!semver.valid(version)) {
|
|
91
|
-
throw new ConvexError('Version must be valid semver')
|
|
92
|
-
}
|
|
93
|
-
const suppliedChangelog = args.changelog.trim()
|
|
94
|
-
const changelogSource = suppliedChangelog ? ('user' as const) : ('auto' as const)
|
|
95
|
-
|
|
96
|
-
const sanitizedFiles = args.files.map((file) => {
|
|
97
|
-
const path = sanitizePath(file.path)
|
|
98
|
-
if (!path) throw new ConvexError('Invalid file paths')
|
|
99
|
-
if (!isTextFile(path, file.contentType ?? undefined)) {
|
|
100
|
-
throw new ConvexError('Only text-based files are allowed')
|
|
101
|
-
}
|
|
102
|
-
return { ...file, path }
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
const totalBytes = sanitizedFiles.reduce((sum, file) => sum + file.size, 0)
|
|
106
|
-
if (totalBytes > MAX_TOTAL_BYTES) {
|
|
107
|
-
throw new ConvexError('Soul bundle exceeds 50MB limit')
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const isSoulFile = (path: string) => path.toLowerCase() === 'soul.md'
|
|
111
|
-
const readmeFile = sanitizedFiles.find((file) => isSoulFile(file.path))
|
|
112
|
-
if (!readmeFile) throw new ConvexError('SOUL.md is required')
|
|
113
|
-
|
|
114
|
-
const nonSoulFiles = sanitizedFiles.filter((file) => !isSoulFile(file.path))
|
|
115
|
-
if (nonSoulFiles.length > 0) {
|
|
116
|
-
throw new ConvexError('Only SOUL.md is allowed for soul bundles')
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const readmeText = await fetchText(ctx, readmeFile.storageId)
|
|
120
|
-
const frontmatter = parseFrontmatter(readmeText)
|
|
121
|
-
const summary = getFrontmatterValue(frontmatter, 'description') ?? deriveSoulSummary(readmeText)
|
|
122
|
-
const metadata = mergeSourceIntoMetadata(getFrontmatterMetadata(frontmatter), args.source)
|
|
123
|
-
|
|
124
|
-
const embeddingText = buildEmbeddingText({
|
|
125
|
-
frontmatter,
|
|
126
|
-
readme: readmeText,
|
|
127
|
-
otherFiles: [],
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
const fingerprint = await hashSkillFiles(
|
|
131
|
-
sanitizedFiles.map((file) => ({
|
|
132
|
-
path: file.path ?? '',
|
|
133
|
-
sha256: file.sha256,
|
|
134
|
-
})),
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
const changelogPromise =
|
|
138
|
-
changelogSource === 'user'
|
|
139
|
-
? Promise.resolve(suppliedChangelog)
|
|
140
|
-
: generateSoulChangelogForPublish(ctx, {
|
|
141
|
-
slug,
|
|
142
|
-
version,
|
|
143
|
-
readmeText,
|
|
144
|
-
files: sanitizedFiles.map((file) => ({ path: file.path ?? '', sha256: file.sha256 })),
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
const embeddingPromise = generateEmbedding(embeddingText)
|
|
148
|
-
|
|
149
|
-
const [changelogText, embedding] = await Promise.all([
|
|
150
|
-
changelogPromise,
|
|
151
|
-
embeddingPromise.catch((error) => {
|
|
152
|
-
throw new ConvexError(formatEmbeddingError(error))
|
|
153
|
-
}),
|
|
154
|
-
])
|
|
155
|
-
|
|
156
|
-
const publishResult = (await ctx.runMutation(internal.souls.insertVersion, {
|
|
157
|
-
userId,
|
|
158
|
-
slug,
|
|
159
|
-
displayName,
|
|
160
|
-
version,
|
|
161
|
-
changelog: changelogText,
|
|
162
|
-
changelogSource,
|
|
163
|
-
tags: args.tags?.map((tag) => tag.trim()).filter(Boolean),
|
|
164
|
-
fingerprint,
|
|
165
|
-
files: sanitizedFiles,
|
|
166
|
-
parsed: {
|
|
167
|
-
frontmatter,
|
|
168
|
-
metadata,
|
|
169
|
-
},
|
|
170
|
-
summary,
|
|
171
|
-
embedding,
|
|
172
|
-
})) as PublishResult
|
|
173
|
-
|
|
174
|
-
const owner = (await ctx.runQuery(internal.users.getByIdInternal, {
|
|
175
|
-
userId,
|
|
176
|
-
})) as Doc<'users'> | null
|
|
177
|
-
const ownerHandle = owner?.handle ?? owner?.name ?? userId
|
|
178
|
-
|
|
179
|
-
void ctx.scheduler
|
|
180
|
-
.runAfter(0, internal.githubSoulBackupsNode.backupSoulForPublishInternal, {
|
|
181
|
-
slug,
|
|
182
|
-
version,
|
|
183
|
-
displayName,
|
|
184
|
-
ownerHandle,
|
|
185
|
-
files: sanitizedFiles,
|
|
186
|
-
publishedAt: Date.now(),
|
|
187
|
-
})
|
|
188
|
-
.catch((error) => {
|
|
189
|
-
console.error('GitHub soul backup scheduling failed', error)
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
return publishResult
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function mergeSourceIntoMetadata(metadata: unknown, source: PublishVersionArgs['source']) {
|
|
196
|
-
if (!source) return metadata === undefined ? undefined : metadata
|
|
197
|
-
const sourceValue = {
|
|
198
|
-
kind: source.kind,
|
|
199
|
-
url: source.url,
|
|
200
|
-
repo: source.repo,
|
|
201
|
-
ref: source.ref,
|
|
202
|
-
commit: source.commit,
|
|
203
|
-
path: source.path,
|
|
204
|
-
importedAt: source.importedAt,
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (!metadata) return { source: sourceValue }
|
|
208
|
-
if (typeof metadata !== 'object' || Array.isArray(metadata)) return { source: sourceValue }
|
|
209
|
-
return { ...(metadata as Record<string, unknown>), source: sourceValue }
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
export async function fetchText(
|
|
213
|
-
ctx: { storage: { get: (id: Id<'_storage'>) => Promise<Blob | null> } },
|
|
214
|
-
storageId: Id<'_storage'>,
|
|
215
|
-
) {
|
|
216
|
-
const blob = await ctx.storage.get(storageId)
|
|
217
|
-
if (!blob) throw new Error('File missing in storage')
|
|
218
|
-
return blob.text()
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function formatEmbeddingError(error: unknown) {
|
|
222
|
-
if (error instanceof Error) {
|
|
223
|
-
if (error.message.includes('OPENAI_API_KEY')) {
|
|
224
|
-
return 'OPENAI_API_KEY is not configured.'
|
|
225
|
-
}
|
|
226
|
-
if (error.message.startsWith('Embedding failed')) {
|
|
227
|
-
return error.message
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
return 'Embedding failed. Please try again.'
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
export const __test = {
|
|
234
|
-
getSummary: (frontmatter: Record<string, unknown>) =>
|
|
235
|
-
getFrontmatterValue(frontmatter, 'description'),
|
|
236
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/* @vitest-environment node */
|
|
2
|
-
|
|
3
|
-
import { describe, expect, it } from 'vitest'
|
|
4
|
-
import { __test, generateToken, hashToken } from './tokens'
|
|
5
|
-
|
|
6
|
-
describe('tokens', () => {
|
|
7
|
-
it('hashToken returns sha256 hex', async () => {
|
|
8
|
-
await expect(hashToken('test')).resolves.toBe(
|
|
9
|
-
'9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08',
|
|
10
|
-
)
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
it('generateToken returns token + prefix', () => {
|
|
14
|
-
const { token, prefix } = generateToken()
|
|
15
|
-
expect(token).toMatch(/^clh_[A-Za-z0-9_-]+$/)
|
|
16
|
-
expect(prefix).toBe(token.slice(0, 12))
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
it('toHex encodes bytes', () => {
|
|
20
|
-
expect(__test.toHex(new Uint8Array([0, 15, 255]))).toBe('000fff')
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
it('toBase64 encodes 1/2/3-byte tails', () => {
|
|
24
|
-
expect(__test.toBase64(new Uint8Array([0xff]))).toBe('/w==')
|
|
25
|
-
expect(__test.toBase64(new Uint8Array([0xff, 0xee]))).toBe('/+4=')
|
|
26
|
-
expect(__test.toBase64(new Uint8Array([0xff, 0xee, 0xdd]))).toBe('/+7d')
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
it('toBase64Url replaces alphabet and strips padding', () => {
|
|
30
|
-
expect(__test.toBase64Url(new Uint8Array([0xff]))).toBe('_w')
|
|
31
|
-
expect(__test.toBase64Url(new Uint8Array([0xfa, 0x00, 0x00]))).toBe('-gAA')
|
|
32
|
-
})
|
|
33
|
-
})
|
package/convex/lib/tokens.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
const encoder = new TextEncoder()
|
|
2
|
-
|
|
3
|
-
export const API_TOKEN_PREFIX = 'clh_'
|
|
4
|
-
|
|
5
|
-
export async function hashToken(token: string) {
|
|
6
|
-
const bytes = encoder.encode(token)
|
|
7
|
-
const digest = await crypto.subtle.digest('SHA-256', bytes)
|
|
8
|
-
return toHex(new Uint8Array(digest))
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function generateToken() {
|
|
12
|
-
const bytes = new Uint8Array(32)
|
|
13
|
-
crypto.getRandomValues(bytes)
|
|
14
|
-
const token = `${API_TOKEN_PREFIX}${toBase64Url(bytes)}`
|
|
15
|
-
const prefix = token.slice(0, 12)
|
|
16
|
-
return { token, prefix }
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function toHex(bytes: Uint8Array) {
|
|
20
|
-
let out = ''
|
|
21
|
-
for (const byte of bytes) out += byte.toString(16).padStart(2, '0')
|
|
22
|
-
return out
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function toBase64Url(bytes: Uint8Array) {
|
|
26
|
-
const base64 = toBase64(bytes)
|
|
27
|
-
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '')
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const BASE64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
|
31
|
-
|
|
32
|
-
function toBase64(bytes: Uint8Array) {
|
|
33
|
-
let output = ''
|
|
34
|
-
for (let i = 0; i < bytes.length; i += 3) {
|
|
35
|
-
const a = bytes[i] ?? 0
|
|
36
|
-
const b = bytes[i + 1] ?? 0
|
|
37
|
-
const c = bytes[i + 2] ?? 0
|
|
38
|
-
const triple = (a << 16) | (b << 8) | c
|
|
39
|
-
output += BASE64_ALPHABET[(triple >> 18) & 63]
|
|
40
|
-
output += BASE64_ALPHABET[(triple >> 12) & 63]
|
|
41
|
-
output += i + 1 < bytes.length ? BASE64_ALPHABET[(triple >> 6) & 63] : '='
|
|
42
|
-
output += i + 2 < bytes.length ? BASE64_ALPHABET[triple & 63] : '='
|
|
43
|
-
}
|
|
44
|
-
return output
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export const __test = {
|
|
48
|
-
toHex,
|
|
49
|
-
toBase64,
|
|
50
|
-
toBase64Url,
|
|
51
|
-
}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
/* @vitest-environment node */
|
|
2
|
-
import { afterEach, describe, expect, it } from 'vitest'
|
|
3
|
-
import { buildDiscordPayload, buildSkillUrl, getWebhookConfig, shouldSendWebhook } from './webhooks'
|
|
4
|
-
|
|
5
|
-
const originalEnv = { ...process.env }
|
|
6
|
-
|
|
7
|
-
afterEach(() => {
|
|
8
|
-
process.env = { ...originalEnv }
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
describe('webhook config', () => {
|
|
12
|
-
it('parses highlighted-only flag', () => {
|
|
13
|
-
process.env.DISCORD_WEBHOOK_URL = 'https://example.com'
|
|
14
|
-
process.env.DISCORD_WEBHOOK_HIGHLIGHTED_ONLY = 'true'
|
|
15
|
-
const config = getWebhookConfig()
|
|
16
|
-
expect(config.highlightedOnly).toBe(true)
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
it('defaults site url when missing', () => {
|
|
20
|
-
delete process.env.SITE_URL
|
|
21
|
-
process.env.DISCORD_WEBHOOK_URL = 'https://example.com'
|
|
22
|
-
const config = getWebhookConfig()
|
|
23
|
-
expect(config.siteUrl).toBe('https://pilothub.com')
|
|
24
|
-
})
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
describe('webhook filtering', () => {
|
|
28
|
-
it('skips when url missing', () => {
|
|
29
|
-
const config = getWebhookConfig({} as NodeJS.ProcessEnv)
|
|
30
|
-
expect(shouldSendWebhook('skill.publish', { slug: 'demo', displayName: 'Demo' }, config)).toBe(
|
|
31
|
-
false,
|
|
32
|
-
)
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('filters non-highlighted when highlighted-only', () => {
|
|
36
|
-
const config = {
|
|
37
|
-
url: 'https://example.com',
|
|
38
|
-
highlightedOnly: true,
|
|
39
|
-
siteUrl: 'https://pilothub.com',
|
|
40
|
-
}
|
|
41
|
-
const allowed = shouldSendWebhook(
|
|
42
|
-
'skill.publish',
|
|
43
|
-
{ slug: 'demo', displayName: 'Demo', highlighted: false },
|
|
44
|
-
config,
|
|
45
|
-
)
|
|
46
|
-
expect(allowed).toBe(false)
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
it('allows highlighted event when highlighted-only', () => {
|
|
50
|
-
const config = {
|
|
51
|
-
url: 'https://example.com',
|
|
52
|
-
highlightedOnly: true,
|
|
53
|
-
siteUrl: 'https://pilothub.com',
|
|
54
|
-
}
|
|
55
|
-
const allowed = shouldSendWebhook(
|
|
56
|
-
'skill.highlighted',
|
|
57
|
-
{ slug: 'demo', displayName: 'Demo', highlighted: true },
|
|
58
|
-
config,
|
|
59
|
-
)
|
|
60
|
-
expect(allowed).toBe(true)
|
|
61
|
-
})
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
describe('payload building', () => {
|
|
65
|
-
it('builds canonical url with owner', () => {
|
|
66
|
-
const url = buildSkillUrl(
|
|
67
|
-
{ slug: 'beeper', displayName: 'Beeper', ownerHandle: 'KrauseFx' },
|
|
68
|
-
'https://pilothub.com',
|
|
69
|
-
)
|
|
70
|
-
expect(url).toBe('https://pilothub.com/KrauseFx/beeper')
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it('builds a publish embed', () => {
|
|
74
|
-
const payload = buildDiscordPayload(
|
|
75
|
-
'skill.publish',
|
|
76
|
-
{
|
|
77
|
-
slug: 'demo',
|
|
78
|
-
displayName: 'Demo Skill',
|
|
79
|
-
summary: 'Nice skill',
|
|
80
|
-
version: '1.2.3',
|
|
81
|
-
ownerHandle: 'steipete',
|
|
82
|
-
tags: ['latest', 'discord'],
|
|
83
|
-
},
|
|
84
|
-
{ url: 'https://example.com', highlightedOnly: false, siteUrl: 'https://pilothub.com' },
|
|
85
|
-
)
|
|
86
|
-
const embed = payload.embeds[0]
|
|
87
|
-
expect(embed.title).toBe('Demo Skill')
|
|
88
|
-
expect(embed.description).toBe('Nice skill')
|
|
89
|
-
expect(embed.fields[0].value).toBe('v1.2.3')
|
|
90
|
-
})
|
|
91
|
-
})
|