rahman-resources 0.7.0 → 0.9.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/bin/cli.js +165 -20
- package/lib/manifest.json +114 -111
- package/lib/rr-schema.json +14 -0
- package/lib/rr.mjs +19 -0
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
rrExists,
|
|
26
26
|
validateRr,
|
|
27
27
|
addFeature as rrAddFeature,
|
|
28
|
+
addSlice as rrAddSlice,
|
|
28
29
|
addSkill as rrAddSkill,
|
|
29
30
|
} from "../lib/rr.mjs";
|
|
30
31
|
import { runPostInit } from "../lib/post-init.mjs";
|
|
@@ -714,7 +715,7 @@ async function installSkill(slug, target) {
|
|
|
714
715
|
|
|
715
716
|
// ─── doctor ───────────────────────────────────────────────────────────────
|
|
716
717
|
|
|
717
|
-
function runDoctor() {
|
|
718
|
+
function runDoctor(rest = []) {
|
|
718
719
|
const target = process.cwd();
|
|
719
720
|
if (!rrExists(target)) {
|
|
720
721
|
console.log(kleur.yellow("⚠ No rr.json found in cwd. Run 'rahman-resources init <app>' first."));
|
|
@@ -722,16 +723,102 @@ function runDoctor() {
|
|
|
722
723
|
}
|
|
723
724
|
const rr = readRr(target);
|
|
724
725
|
const issues = validateRr(rr);
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
console.log(
|
|
729
|
-
console.log(`
|
|
726
|
+
const sliceCheck = rest.includes("--slices");
|
|
727
|
+
|
|
728
|
+
if (issues.length > 0) {
|
|
729
|
+
console.log(kleur.red(`✖ rr.json has ${issues.length} issue(s):`));
|
|
730
|
+
for (const i of issues) console.log(` · ${i}`);
|
|
731
|
+
process.exit(1);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
console.log(kleur.green("✓ rr.json is valid."));
|
|
735
|
+
console.log(` template: ${kleur.cyan(rr.template?.slug ?? "(none)")}`);
|
|
736
|
+
console.log(` features: ${kleur.cyan(rr.features?.length ?? 0)}`);
|
|
737
|
+
console.log(` slices: ${kleur.cyan(rr.slices?.length ?? 0)}`);
|
|
738
|
+
console.log(` skills: ${kleur.cyan(rr.skills?.length ?? 0)}`);
|
|
739
|
+
|
|
740
|
+
if (sliceCheck) doctorSlices(target, rr);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function doctorSlices(target, rr) {
|
|
744
|
+
console.log(kleur.bold(`\n→ Slice composition check\n`));
|
|
745
|
+
const installed = rr.slices ?? [];
|
|
746
|
+
if (installed.length === 0) {
|
|
747
|
+
console.log(kleur.dim(" (no slices installed — nothing to check)"));
|
|
730
748
|
return;
|
|
731
749
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
750
|
+
|
|
751
|
+
const errors = [];
|
|
752
|
+
const warnings = [];
|
|
753
|
+
const sliceMap = new Map((manifest.slices ?? []).map((s) => [s.slug, s]));
|
|
754
|
+
const present = new Set(installed.map((s) => s.slug));
|
|
755
|
+
|
|
756
|
+
for (const inst of installed) {
|
|
757
|
+
const def = sliceMap.get(inst.slug);
|
|
758
|
+
if (!def) {
|
|
759
|
+
warnings.push(`${inst.slug}: not in kitab manifest (custom slice — skipping peer check)`);
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// 1. Slice path exists locally
|
|
764
|
+
const slicePath = path.join(target, def.slicePath);
|
|
765
|
+
if (!existsSync(slicePath)) {
|
|
766
|
+
errors.push(`${inst.slug}: missing on disk at ${def.slicePath}`);
|
|
767
|
+
}
|
|
768
|
+
const sliceJsonPath = path.join(slicePath, "slice.json");
|
|
769
|
+
if (existsSync(slicePath) && !existsSync(sliceJsonPath)) {
|
|
770
|
+
errors.push(`${inst.slug}: slice.json missing at ${def.slicePath}/slice.json`);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// 2. Convex paths exist locally (when declared)
|
|
774
|
+
for (const cp of def.convexPaths ?? []) {
|
|
775
|
+
const cpAbs = path.join(target, cp);
|
|
776
|
+
if (!existsSync(cpAbs)) {
|
|
777
|
+
warnings.push(`${inst.slug}: convex path missing at ${cp} (lift again with 'npx rr add ${inst.slug}')`);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// 3. Peers transitively present
|
|
782
|
+
for (const peer of def.peers ?? []) {
|
|
783
|
+
if (!present.has(peer.slug)) {
|
|
784
|
+
errors.push(`${inst.slug} requires peer ${peer.slug} ${peer.range} — not installed`);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// 4. Convex table-name collision across installed slices
|
|
790
|
+
const tableOwners = new Map();
|
|
791
|
+
for (const inst of installed) {
|
|
792
|
+
const def = sliceMap.get(inst.slug);
|
|
793
|
+
if (!def) continue;
|
|
794
|
+
for (const cp of def.convexPaths ?? []) {
|
|
795
|
+
const schemaFile = path.join(target, cp, "schema.ts");
|
|
796
|
+
if (!existsSync(schemaFile)) continue;
|
|
797
|
+
const body = readFileSync(schemaFile, "utf8");
|
|
798
|
+
const matches = [...body.matchAll(/^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*defineTable\(/gm)];
|
|
799
|
+
for (const m of matches) {
|
|
800
|
+
const tName = m[1];
|
|
801
|
+
if (tableOwners.has(tName)) {
|
|
802
|
+
errors.push(`convex table "${tName}" declared by both ${tableOwners.get(tName)} and ${inst.slug}`);
|
|
803
|
+
} else {
|
|
804
|
+
tableOwners.set(tName, inst.slug);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (warnings.length > 0) {
|
|
811
|
+
console.log(kleur.yellow(` ⚠ ${warnings.length} warning(s):`));
|
|
812
|
+
for (const w of warnings) console.log(` · ${w}`);
|
|
813
|
+
}
|
|
814
|
+
if (errors.length > 0) {
|
|
815
|
+
console.log(kleur.red(`\n ✖ ${errors.length} error(s):`));
|
|
816
|
+
for (const e of errors) console.log(` · ${e}`);
|
|
817
|
+
process.exit(1);
|
|
818
|
+
}
|
|
819
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
820
|
+
console.log(kleur.green(` ✓ ${installed.length} slice(s) — composition healthy`));
|
|
821
|
+
}
|
|
735
822
|
}
|
|
736
823
|
|
|
737
824
|
function runMcpHint() {
|
|
@@ -910,6 +997,17 @@ async function runLift(rest) {
|
|
|
910
997
|
await maybeRunShadcnAdd({ slug: parsed.slug ?? "lifted", shadcnComponents: plan.shadcn }, target, false);
|
|
911
998
|
}
|
|
912
999
|
|
|
1000
|
+
// Register the slice in the consumer's rr.json (if present).
|
|
1001
|
+
if (parsed.kind === "rahman" && rrExists(target)) {
|
|
1002
|
+
const slice = (manifest.slices ?? []).find((s) => s.slug === parsed.slug);
|
|
1003
|
+
if (slice) {
|
|
1004
|
+
const rr = readRr(target);
|
|
1005
|
+
rrAddSlice(rr, parsed.slug, { version: slice.version, category: slice.category });
|
|
1006
|
+
writeRr(rr, target);
|
|
1007
|
+
console.log(kleur.dim(` rr.json: slices += ${parsed.slug}@${slice.version}`));
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
913
1011
|
console.log(`\n${kleur.green("✓")} Lift complete.`);
|
|
914
1012
|
if (plan.env.length > 0) {
|
|
915
1013
|
console.log(`${kleur.bold("Don't forget:")} set the env vars listed above before running.\n`);
|
|
@@ -1022,25 +1120,72 @@ async function runPublishSlice(rest) {
|
|
|
1022
1120
|
ps.on("exit", (code) => (code === 0 ? resolve() : reject(new Error(`validate-slice exited ${code}`))));
|
|
1023
1121
|
});
|
|
1024
1122
|
|
|
1123
|
+
// Read the slice's metadata so we can prefill the PR title + body.
|
|
1124
|
+
const sliceMeta = JSON.parse(readFileSync(path.join(abs, "slice.json"), "utf8"));
|
|
1125
|
+
|
|
1025
1126
|
if (!flags["open-pr"]) {
|
|
1026
|
-
console.log(`\n${kleur.yellow("dry-run — pass --open-pr to
|
|
1127
|
+
console.log(`\n${kleur.yellow("dry-run — pass --open-pr to scaffold the PR command.")}`);
|
|
1027
1128
|
return;
|
|
1028
1129
|
}
|
|
1029
1130
|
|
|
1030
|
-
|
|
1031
|
-
|
|
1131
|
+
const ghAvailable = await checkGhInstalled();
|
|
1132
|
+
|
|
1133
|
+
if (!ghAvailable) {
|
|
1134
|
+
console.log(`\n${kleur.yellow("⚠ `gh` CLI not found.")}`);
|
|
1135
|
+
console.log(`Install it: ${kleur.cyan("https://cli.github.com")} then re-run with --open-pr.`);
|
|
1136
|
+
console.log(`Manual fallback steps:`);
|
|
1137
|
+
console.log(`
|
|
1138
|
+
1. Fork ${kleur.cyan("https://github.com/rahmanef63/resource-site")} on GitHub.
|
|
1139
|
+
2. Clone your fork: ${kleur.cyan("git clone https://github.com/<you>/resource-site && cd resource-site")}
|
|
1140
|
+
3. Copy slice: ${kleur.cyan(`cp -r ${abs} frontend/slices/${sliceMeta.slug}`)}
|
|
1141
|
+
4. Copy convex half: ${kleur.cyan(`cp -r ${abs.replace("frontend/slices", "convex/features")} convex/features/${sliceMeta.slug}`)} (if exists)
|
|
1142
|
+
5. Add entry to ${kleur.cyan("lib/content/slices.ts")}
|
|
1143
|
+
6. ${kleur.cyan("npm run slices:check")}
|
|
1144
|
+
7. ${kleur.cyan(`git checkout -b feat/slice-${sliceMeta.slug} && git add -A && git commit -m "feat(slices): add ${sliceMeta.slug}" && git push -u origin feat/slice-${sliceMeta.slug}`)}
|
|
1145
|
+
8. Open PR via GitHub UI.
|
|
1146
|
+
`);
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// gh installed — emit ready-to-run gh commands.
|
|
1151
|
+
const branchName = `feat/slice-${sliceMeta.slug}`;
|
|
1152
|
+
const title = `feat(slices): add ${sliceMeta.slug}`;
|
|
1153
|
+
const body = [
|
|
1154
|
+
`Adds the **${sliceMeta.title}** slice (${sliceMeta.category}, v${sliceMeta.version}).`,
|
|
1155
|
+
"",
|
|
1156
|
+
sliceMeta.description,
|
|
1157
|
+
"",
|
|
1158
|
+
"Validated locally with `npm run slices:check`.",
|
|
1159
|
+
"",
|
|
1160
|
+
"🤖 Generated with [`rahman-resources publish-slice`](https://github.com/rahmanef63/resource-site)",
|
|
1161
|
+
].join("\n");
|
|
1162
|
+
|
|
1163
|
+
console.log(`\n${kleur.bold("✓")} ${kleur.green("gh CLI detected.")} Run these from inside YOUR fork of the kitab repo:`);
|
|
1032
1164
|
console.log(`
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
(
|
|
1165
|
+
# Inside your forked clone of rahmanef63/resource-site, copy the slice in:
|
|
1166
|
+
${kleur.cyan(`cp -r ${abs} frontend/slices/${sliceMeta.slug}`)}
|
|
1167
|
+
${kleur.cyan(`# Add ${sliceMeta.slug} entry to lib/content/slices.ts`)}
|
|
1168
|
+
${kleur.cyan(`npm run slices:check`)}
|
|
1169
|
+
|
|
1170
|
+
# Then open the PR (gh handles fork + push):
|
|
1171
|
+
${kleur.cyan(`git checkout -b ${branchName}`)}
|
|
1172
|
+
${kleur.cyan(`git add -A && git commit -m "${title}"`)}
|
|
1173
|
+
${kleur.cyan(`gh pr create --repo rahmanef63/resource-site \\
|
|
1174
|
+
--title "${title}" \\
|
|
1175
|
+
--body ${JSON.stringify(body)}`)}
|
|
1176
|
+
|
|
1177
|
+
${kleur.dim("(If you haven't forked yet, run `gh repo fork rahmanef63/resource-site --clone` first.)")}
|
|
1041
1178
|
`);
|
|
1042
1179
|
}
|
|
1043
1180
|
|
|
1181
|
+
async function checkGhInstalled() {
|
|
1182
|
+
return new Promise((resolve) => {
|
|
1183
|
+
const ps = spawn("gh", ["--version"], { stdio: "ignore", shell: true });
|
|
1184
|
+
ps.on("error", () => resolve(false));
|
|
1185
|
+
ps.on("exit", (code) => resolve(code === 0));
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1044
1189
|
// ─── helpers ──────────────────────────────────────────────────────────────
|
|
1045
1190
|
|
|
1046
1191
|
async function pull(repoPath, dest) {
|
package/lib/manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 2,
|
|
3
|
-
"generatedAt": "2026-05-
|
|
3
|
+
"generatedAt": "2026-05-09T14:19:40.545Z",
|
|
4
4
|
"repo": "rahmanef63/resource-site",
|
|
5
5
|
"branch": "main",
|
|
6
6
|
"layouts": [
|
|
@@ -950,33 +950,12 @@
|
|
|
950
950
|
}
|
|
951
951
|
],
|
|
952
952
|
"features": [
|
|
953
|
-
{
|
|
954
|
-
"slug": "ai-sdk-openrouter",
|
|
955
|
-
"title": "AI SDK — OpenRouter Router",
|
|
956
|
-
"category": "ai",
|
|
957
|
-
"description": "Tier-routed LLM calls via OpenRouter. Nano (Haiku/4o-mini) for classification, mid (Sonnet/4o) for drafting, flagship (Opus) for deep reasoning. Cost log + retry baked in.",
|
|
958
|
-
"source": "@openrouter/ai-sdk-provider + ai",
|
|
959
|
-
"docsUrl": "https://sdk.vercel.ai/docs",
|
|
960
|
-
"install": "npm i ai @openrouter/ai-sdk-provider",
|
|
961
|
-
"npmPackages": [
|
|
962
|
-
"ai",
|
|
963
|
-
"@openrouter/ai-sdk-provider"
|
|
964
|
-
],
|
|
965
|
-
"exampleCode": "// convex/shared/ai/router.ts\nimport { action } from \"./_generated/server\";\nimport { v } from \"convex/values\";\nimport { generateText } from \"ai\";\nimport { createOpenRouter } from \"@openrouter/ai-sdk-provider\";\n\nconst router = createOpenRouter({ apiKey: process.env.OPENROUTER_API_KEY! });\n\nconst TIER_TO_MODEL = {\n nano: \"anthropic/claude-haiku-4-5\",\n mid: \"anthropic/claude-sonnet-4-6\",\n flagship: \"anthropic/claude-opus-4-7\",\n};\n\nexport const callModel = action({\n args: {\n feature: v.string(),\n prompt: v.string(),\n tier: v.union(v.literal(\"nano\"), v.literal(\"mid\"), v.literal(\"flagship\")),\n },\n handler: async (ctx, { feature, prompt, tier }) => {\n const { text, usage } = await generateText({\n model: router(TIER_TO_MODEL[tier]),\n prompt,\n });\n await ctx.runMutation(internal.ai.logUsage, { feature, tier, usage });\n return text;\n },\n});",
|
|
966
|
-
"agentRecipe": "Wrap every AI call through ai-router action. Pick tier based on workload: nano for spam-flag/headline-suggest, mid for chat/draft, flagship for methodology-review. Log token usage to ai_usage table for cost dashboard.",
|
|
967
|
-
"tags": [
|
|
968
|
-
"ai",
|
|
969
|
-
"llm",
|
|
970
|
-
"openrouter",
|
|
971
|
-
"vercel-ai-sdk"
|
|
972
|
-
]
|
|
973
|
-
},
|
|
974
953
|
{
|
|
975
954
|
"slug": "convex-auth",
|
|
976
955
|
"title": "Convex Auth — Email Magic Link",
|
|
977
956
|
"category": "auth",
|
|
978
|
-
"description": "@convex-dev/auth with email magic link
|
|
979
|
-
"source": "
|
|
957
|
+
"description": "@convex-dev/auth with email magic link via Resend. Self-hosted Convex friendly. Hard mandate per kitab CLAUDE.md (no Clerk).",
|
|
958
|
+
"source": "rahmanef63/resource-site",
|
|
980
959
|
"docsUrl": "https://labs.convex.dev/auth",
|
|
981
960
|
"install": "npm i @convex-dev/auth @auth/core resend",
|
|
982
961
|
"npmPackages": [
|
|
@@ -984,51 +963,34 @@
|
|
|
984
963
|
"@auth/core",
|
|
985
964
|
"resend"
|
|
986
965
|
],
|
|
987
|
-
"exampleCode": "// convex/auth.ts\nimport { convexAuth } from \"@convex-dev/auth/server\";\nimport
|
|
988
|
-
"agentRecipe": "
|
|
966
|
+
"exampleCode": "// convex/auth.ts\nimport { convexAuth } from \"@convex-dev/auth/server\";\nimport Resend from \"@convex-dev/auth/providers/Resend\";\n\nexport const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({\n providers: [Resend({ from: \"auth@yourdomain.com\" })],\n});\n\n// app/proxy.ts (Next 16 — NOT middleware.ts)\nimport { convexAuthNextjsMiddleware } from \"@convex-dev/auth/nextjs/server\";\nexport default convexAuthNextjsMiddleware();",
|
|
967
|
+
"agentRecipe": "Run `rr add convex-auth`. Then create convex/auth.ts using the kitab pattern (Resend provider). Set env via `npx convex env set` for self-hosted.",
|
|
989
968
|
"tags": [
|
|
990
969
|
"auth",
|
|
991
970
|
"convex",
|
|
992
|
-
"
|
|
971
|
+
"magic-link",
|
|
993
972
|
"no-clerk"
|
|
994
973
|
]
|
|
995
974
|
},
|
|
996
975
|
{
|
|
997
|
-
"slug": "
|
|
998
|
-
"title": "
|
|
999
|
-
"category": "
|
|
1000
|
-
"description": "
|
|
1001
|
-
"source": "
|
|
1002
|
-
"docsUrl": "https://
|
|
1003
|
-
"install": "
|
|
1004
|
-
"npmPackages": [],
|
|
1005
|
-
"exampleCode": "\"use client\";\nimport * as React from \"react\";\n\nexport function StoreProvider({ children }) {\n const [state, baseDispatch] = React.useReducer(reducer, SEED_STATE);\n const channelRef = React.useRef<BroadcastChannel | null>(null);\n\n React.useEffect(() => {\n const ch = new BroadcastChannel(\"pbos:sync\");\n channelRef.current = ch;\n ch.onmessage = (e) => baseDispatch(e.data);\n return () => ch.close();\n }, []);\n\n const dispatch = React.useCallback((action) => {\n baseDispatch(action);\n channelRef.current?.postMessage(action);\n }, []);\n\n return <Ctx.Provider value={{ state, dispatch }}>{children}</Ctx.Provider>;\n}",
|
|
1006
|
-
"agentRecipe": "Use BroadcastChannel only for demo / cross-iframe state mirroring. Production data still goes through Convex realtime. The channel does not echo to the sender so no loop.",
|
|
1007
|
-
"tags": [
|
|
1008
|
-
"realtime",
|
|
1009
|
-
"broadcast-channel",
|
|
1010
|
-
"cross-iframe",
|
|
1011
|
-
"demo-pattern"
|
|
1012
|
-
]
|
|
1013
|
-
},
|
|
1014
|
-
{
|
|
1015
|
-
"slug": "convex-vector-search",
|
|
1016
|
-
"title": "Convex Vector Index — Semantic Search",
|
|
1017
|
-
"category": "search",
|
|
1018
|
-
"description": "Built-in vector index on any Convex table. Embed via OpenAI text-embedding-3-small (1536-dim), query via vectorIndex().",
|
|
1019
|
-
"source": "convex (built-in)",
|
|
1020
|
-
"docsUrl": "https://docs.convex.dev/database/vector-search",
|
|
1021
|
-
"install": "npm i openai",
|
|
976
|
+
"slug": "midtrans-payment",
|
|
977
|
+
"title": "Midtrans — Indonesia Payment",
|
|
978
|
+
"category": "payment",
|
|
979
|
+
"description": "Pembayaran lokal Indonesia via Midtrans Snap (BCA, Mandiri, BRI, e-wallet GoPay/OVO/Dana, QRIS). Webhook untuk konfirmasi. Provider-isolated under components/providers/midtrans + actions/midtrans so Doku/Stripe land as siblings.",
|
|
980
|
+
"source": "rahmanef63/resource-site",
|
|
981
|
+
"docsUrl": "https://docs.midtrans.com",
|
|
982
|
+
"install": "npm i midtrans-client",
|
|
1022
983
|
"npmPackages": [
|
|
1023
|
-
"
|
|
984
|
+
"midtrans-client"
|
|
1024
985
|
],
|
|
1025
|
-
"exampleCode": "
|
|
1026
|
-
"agentRecipe": "
|
|
986
|
+
"exampleCode": "",
|
|
987
|
+
"agentRecipe": "Midtrans Snap untuk pembayaran instant. Webhook ke Convex HTTP action /api/midtrans-callback untuk update order status. Ingat: PPN 11% sudah included di amount, jangan double-count.",
|
|
1027
988
|
"tags": [
|
|
1028
|
-
"
|
|
1029
|
-
"
|
|
1030
|
-
"
|
|
1031
|
-
"
|
|
989
|
+
"payment",
|
|
990
|
+
"midtrans",
|
|
991
|
+
"indonesia",
|
|
992
|
+
"qris",
|
|
993
|
+
"snap"
|
|
1032
994
|
]
|
|
1033
995
|
},
|
|
1034
996
|
{
|
|
@@ -1036,7 +998,7 @@
|
|
|
1036
998
|
"title": "Resend — Transactional & Newsletter",
|
|
1037
999
|
"category": "email",
|
|
1038
1000
|
"description": "Transactional email + newsletter blast via Resend. Double opt-in flow + audience segmentation. Magic-link delivery for Convex Auth.",
|
|
1039
|
-
"source": "
|
|
1001
|
+
"source": "rahmanef63/resource-site",
|
|
1040
1002
|
"docsUrl": "https://resend.com/docs",
|
|
1041
1003
|
"install": "npm i resend react-email @react-email/components",
|
|
1042
1004
|
"npmPackages": [
|
|
@@ -1044,41 +1006,62 @@
|
|
|
1044
1006
|
"react-email",
|
|
1045
1007
|
"@react-email/components"
|
|
1046
1008
|
],
|
|
1047
|
-
"exampleCode": "
|
|
1009
|
+
"exampleCode": "",
|
|
1048
1010
|
"agentRecipe": "Use Resend Audiences API for newsletter — store subscriber emails in Convex too for segmentation. Double opt-in: subscriber.create with status 'pending' → click link → status 'confirmed'.",
|
|
1049
1011
|
"tags": [
|
|
1050
1012
|
"email",
|
|
1051
|
-
"resend",
|
|
1052
1013
|
"newsletter",
|
|
1053
|
-
"
|
|
1014
|
+
"resend"
|
|
1054
1015
|
]
|
|
1055
1016
|
},
|
|
1056
1017
|
{
|
|
1057
|
-
"slug": "
|
|
1058
|
-
"title": "
|
|
1059
|
-
"category": "
|
|
1060
|
-
"description": "
|
|
1061
|
-
"source": "
|
|
1062
|
-
"docsUrl": "https://
|
|
1063
|
-
"install": "npm i
|
|
1018
|
+
"slug": "ai-router",
|
|
1019
|
+
"title": "AI Router (OpenRouter)",
|
|
1020
|
+
"category": "ai",
|
|
1021
|
+
"description": "Tier-routed LLM access via OpenRouter — nano (Haiku) for classification, mid (Sonnet) for chat, flagship (Opus) for deep reasoning. Per-call usage log.",
|
|
1022
|
+
"source": "rahmanef63/resource-site",
|
|
1023
|
+
"docsUrl": "https://sdk.vercel.ai/docs",
|
|
1024
|
+
"install": "npm i ai @openrouter/ai-sdk-provider",
|
|
1064
1025
|
"npmPackages": [
|
|
1065
|
-
"
|
|
1026
|
+
"ai",
|
|
1027
|
+
"@openrouter/ai-sdk-provider"
|
|
1066
1028
|
],
|
|
1067
|
-
"exampleCode": "
|
|
1068
|
-
"agentRecipe": "
|
|
1029
|
+
"exampleCode": "",
|
|
1030
|
+
"agentRecipe": "Wrap every AI call through ai-router action. Pick tier based on workload: nano for spam-flag/headline-suggest, mid for chat/draft, flagship for methodology-review. Log token usage to ai_usage table for cost dashboard.",
|
|
1069
1031
|
"tags": [
|
|
1070
|
-
"
|
|
1071
|
-
"
|
|
1072
|
-
"
|
|
1073
|
-
"
|
|
1032
|
+
"ai",
|
|
1033
|
+
"llm",
|
|
1034
|
+
"openrouter",
|
|
1035
|
+
"tier-routing"
|
|
1036
|
+
]
|
|
1037
|
+
},
|
|
1038
|
+
{
|
|
1039
|
+
"slug": "vector-search",
|
|
1040
|
+
"title": "Convex Vector Search",
|
|
1041
|
+
"category": "search",
|
|
1042
|
+
"description": "Embeddings-based search via Convex's built-in vector index. Embed via OpenAI text-embedding-3-small (1536-dim), query via vectorIndex().",
|
|
1043
|
+
"source": "rahmanef63/resource-site",
|
|
1044
|
+
"docsUrl": "https://docs.convex.dev/database/vector-search",
|
|
1045
|
+
"install": "npm i openai",
|
|
1046
|
+
"npmPackages": [
|
|
1047
|
+
"openai"
|
|
1048
|
+
],
|
|
1049
|
+
"exampleCode": "",
|
|
1050
|
+
"agentRecipe": "Add embedding field + vectorIndex per searchable table. Re-embed on upsert via Convex action. Cache embeddings — don't re-call OpenAI on every read.",
|
|
1051
|
+
"tags": [
|
|
1052
|
+
"search",
|
|
1053
|
+
"vector",
|
|
1054
|
+
"embeddings",
|
|
1055
|
+
"convex",
|
|
1056
|
+
"rag"
|
|
1074
1057
|
]
|
|
1075
1058
|
},
|
|
1076
1059
|
{
|
|
1077
1060
|
"slug": "mdx-blog",
|
|
1078
|
-
"title": "MDX
|
|
1061
|
+
"title": "MDX Blog",
|
|
1079
1062
|
"category": "content",
|
|
1080
|
-
"description": "Markdown-with-JSX untuk blog post. Auto-generate ToC, reading-time, syntax highlight, plus embed React components inline.",
|
|
1081
|
-
"source": "
|
|
1063
|
+
"description": "Markdown-with-JSX untuk blog post. File-based under content/blog/*.mdx. Auto-generate ToC, reading-time, syntax highlight, plus embed React components inline.",
|
|
1064
|
+
"source": "rahmanef63/resource-site",
|
|
1082
1065
|
"docsUrl": "https://github.com/hashicorp/next-mdx-remote",
|
|
1083
1066
|
"install": "npm i next-mdx-remote rehype-pretty-code remark-gfm reading-time",
|
|
1084
1067
|
"npmPackages": [
|
|
@@ -1087,33 +1070,51 @@
|
|
|
1087
1070
|
"remark-gfm",
|
|
1088
1071
|
"reading-time"
|
|
1089
1072
|
],
|
|
1090
|
-
"exampleCode": "
|
|
1091
|
-
"agentRecipe": "Store post body sebagai markdown di
|
|
1073
|
+
"exampleCode": "",
|
|
1074
|
+
"agentRecipe": "Store post body sebagai markdown di content/blog/*.mdx. Render dengan MDXRemote di [slug]/page.tsx. Auto-extract headings ke ToC via remark plugin custom.",
|
|
1092
1075
|
"tags": [
|
|
1093
|
-
"
|
|
1094
|
-
"markdown",
|
|
1076
|
+
"content",
|
|
1095
1077
|
"blog",
|
|
1096
|
-
"
|
|
1078
|
+
"mdx",
|
|
1079
|
+
"static"
|
|
1097
1080
|
]
|
|
1098
1081
|
},
|
|
1099
1082
|
{
|
|
1100
1083
|
"slug": "cal-com-booking",
|
|
1101
|
-
"title": "Cal.com
|
|
1084
|
+
"title": "Cal.com Booking",
|
|
1102
1085
|
"category": "data",
|
|
1103
|
-
"description": "
|
|
1104
|
-
"source": "
|
|
1086
|
+
"description": "Embedded Cal.com booking widget + webhook receiver to mirror bookings into Convex.",
|
|
1087
|
+
"source": "rahmanef63/resource-site",
|
|
1105
1088
|
"docsUrl": "https://cal.com/docs/integrations/web-app/embed",
|
|
1106
1089
|
"install": "npm i @calcom/embed-react",
|
|
1107
1090
|
"npmPackages": [
|
|
1108
1091
|
"@calcom/embed-react"
|
|
1109
1092
|
],
|
|
1110
|
-
"exampleCode": "
|
|
1111
|
-
"agentRecipe": "Embed Cal.com via @calcom/embed-react di halaman services. Configure webhook di Cal.com dashboard → POST ke /api/cal-webhook →
|
|
1093
|
+
"exampleCode": "",
|
|
1094
|
+
"agentRecipe": "Embed Cal.com via @calcom/embed-react di halaman services. Configure webhook di Cal.com dashboard → POST ke /api/cal-webhook → upsert booking di Convex.",
|
|
1112
1095
|
"tags": [
|
|
1113
|
-
"
|
|
1114
|
-
"cal-com",
|
|
1096
|
+
"data",
|
|
1115
1097
|
"scheduling",
|
|
1116
|
-
"
|
|
1098
|
+
"cal-com",
|
|
1099
|
+
"bookings"
|
|
1100
|
+
]
|
|
1101
|
+
},
|
|
1102
|
+
{
|
|
1103
|
+
"slug": "broadcast-channel-sync",
|
|
1104
|
+
"title": "BroadcastChannel — Cross-tab Sync",
|
|
1105
|
+
"category": "realtime",
|
|
1106
|
+
"description": "Same-origin cross-tab + cross-iframe state sync via BroadcastChannel API. Tiny, no backend, no install.",
|
|
1107
|
+
"source": "Web Platform — BroadcastChannel API",
|
|
1108
|
+
"docsUrl": "https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API",
|
|
1109
|
+
"install": "// no install — Web Platform API",
|
|
1110
|
+
"npmPackages": [],
|
|
1111
|
+
"exampleCode": "",
|
|
1112
|
+
"agentRecipe": "Use BroadcastChannel only for demo / cross-iframe state mirroring. Production data still goes through Convex realtime. Use the useBroadcastSync(channelName, initial) hook from @/features/broadcast-channel-sync.",
|
|
1113
|
+
"tags": [
|
|
1114
|
+
"realtime",
|
|
1115
|
+
"cross-tab",
|
|
1116
|
+
"broadcast-channel",
|
|
1117
|
+
"demo-pattern"
|
|
1117
1118
|
]
|
|
1118
1119
|
}
|
|
1119
1120
|
],
|
|
@@ -1172,10 +1173,10 @@
|
|
|
1172
1173
|
},
|
|
1173
1174
|
{
|
|
1174
1175
|
"slug": "midtrans-payment",
|
|
1175
|
-
"title": "Midtrans Payment",
|
|
1176
|
+
"title": "Midtrans — Indonesia Payment",
|
|
1176
1177
|
"category": "payment",
|
|
1177
1178
|
"version": "0.1.0",
|
|
1178
|
-
"description": "Midtrans Snap
|
|
1179
|
+
"description": "Pembayaran lokal Indonesia via Midtrans Snap (BCA, Mandiri, BRI, e-wallet GoPay/OVO/Dana, QRIS). Webhook untuk konfirmasi. Provider-isolated under components/providers/midtrans + actions/midtrans so Doku/Stripe land as siblings.",
|
|
1179
1180
|
"source": "rahmanef63/resource-site",
|
|
1180
1181
|
"slicePath": "frontend/slices/midtrans-payment",
|
|
1181
1182
|
"convexPaths": [
|
|
@@ -1224,14 +1225,14 @@
|
|
|
1224
1225
|
"qris",
|
|
1225
1226
|
"snap"
|
|
1226
1227
|
],
|
|
1227
|
-
"agentRecipe": ""
|
|
1228
|
+
"agentRecipe": "Midtrans Snap untuk pembayaran instant. Webhook ke Convex HTTP action /api/midtrans-callback untuk update order status. Ingat: PPN 11% sudah included di amount, jangan double-count."
|
|
1228
1229
|
},
|
|
1229
1230
|
{
|
|
1230
1231
|
"slug": "resend-newsletter",
|
|
1231
|
-
"title": "Resend Newsletter",
|
|
1232
|
+
"title": "Resend — Transactional & Newsletter",
|
|
1232
1233
|
"category": "email",
|
|
1233
1234
|
"version": "0.1.0",
|
|
1234
|
-
"description": "
|
|
1235
|
+
"description": "Transactional email + newsletter blast via Resend. Double opt-in flow + audience segmentation. Magic-link delivery for Convex Auth.",
|
|
1235
1236
|
"source": "rahmanef63/resource-site",
|
|
1236
1237
|
"slicePath": "frontend/slices/resend-newsletter",
|
|
1237
1238
|
"convexPaths": [
|
|
@@ -1266,7 +1267,7 @@
|
|
|
1266
1267
|
"newsletter",
|
|
1267
1268
|
"resend"
|
|
1268
1269
|
],
|
|
1269
|
-
"agentRecipe": ""
|
|
1270
|
+
"agentRecipe": "Use Resend Audiences API for newsletter — store subscriber emails in Convex too for segmentation. Double opt-in: subscriber.create with status 'pending' → click link → status 'confirmed'."
|
|
1270
1271
|
},
|
|
1271
1272
|
{
|
|
1272
1273
|
"slug": "ai-router",
|
|
@@ -1301,14 +1302,14 @@
|
|
|
1301
1302
|
"openrouter",
|
|
1302
1303
|
"tier-routing"
|
|
1303
1304
|
],
|
|
1304
|
-
"agentRecipe": ""
|
|
1305
|
+
"agentRecipe": "Wrap every AI call through ai-router action. Pick tier based on workload: nano for spam-flag/headline-suggest, mid for chat/draft, flagship for methodology-review. Log token usage to ai_usage table for cost dashboard."
|
|
1305
1306
|
},
|
|
1306
1307
|
{
|
|
1307
1308
|
"slug": "vector-search",
|
|
1308
1309
|
"title": "Convex Vector Search",
|
|
1309
1310
|
"category": "search",
|
|
1310
1311
|
"version": "0.1.0",
|
|
1311
|
-
"description": "Embeddings-based search via
|
|
1312
|
+
"description": "Embeddings-based search via Convex's built-in vector index. Embed via OpenAI text-embedding-3-small (1536-dim), query via vectorIndex().",
|
|
1312
1313
|
"source": "rahmanef63/resource-site",
|
|
1313
1314
|
"slicePath": "frontend/slices/vector-search",
|
|
1314
1315
|
"convexPaths": [
|
|
@@ -1334,16 +1335,17 @@
|
|
|
1334
1335
|
"search",
|
|
1335
1336
|
"vector",
|
|
1336
1337
|
"embeddings",
|
|
1337
|
-
"convex"
|
|
1338
|
+
"convex",
|
|
1339
|
+
"rag"
|
|
1338
1340
|
],
|
|
1339
|
-
"agentRecipe": ""
|
|
1341
|
+
"agentRecipe": "Add embedding field + vectorIndex per searchable table. Re-embed on upsert via Convex action. Cache embeddings — don't re-call OpenAI on every read."
|
|
1340
1342
|
},
|
|
1341
1343
|
{
|
|
1342
1344
|
"slug": "mdx-blog",
|
|
1343
1345
|
"title": "MDX Blog",
|
|
1344
1346
|
"category": "content",
|
|
1345
1347
|
"version": "0.1.0",
|
|
1346
|
-
"description": "
|
|
1348
|
+
"description": "Markdown-with-JSX untuk blog post. File-based under content/blog/*.mdx. Auto-generate ToC, reading-time, syntax highlight, plus embed React components inline.",
|
|
1347
1349
|
"source": "rahmanef63/resource-site",
|
|
1348
1350
|
"slicePath": "frontend/slices/mdx-blog",
|
|
1349
1351
|
"convexPaths": [],
|
|
@@ -1364,7 +1366,7 @@
|
|
|
1364
1366
|
"mdx",
|
|
1365
1367
|
"static"
|
|
1366
1368
|
],
|
|
1367
|
-
"agentRecipe": ""
|
|
1369
|
+
"agentRecipe": "Store post body sebagai markdown di content/blog/*.mdx. Render dengan MDXRemote di [slug]/page.tsx. Auto-extract headings ke ToC via remark plugin custom."
|
|
1368
1370
|
},
|
|
1369
1371
|
{
|
|
1370
1372
|
"slug": "cal-com-booking",
|
|
@@ -1403,15 +1405,15 @@
|
|
|
1403
1405
|
"cal-com",
|
|
1404
1406
|
"bookings"
|
|
1405
1407
|
],
|
|
1406
|
-
"agentRecipe": ""
|
|
1408
|
+
"agentRecipe": "Embed Cal.com via @calcom/embed-react di halaman services. Configure webhook di Cal.com dashboard → POST ke /api/cal-webhook → upsert booking di Convex."
|
|
1407
1409
|
},
|
|
1408
1410
|
{
|
|
1409
1411
|
"slug": "broadcast-channel-sync",
|
|
1410
|
-
"title": "BroadcastChannel Sync",
|
|
1412
|
+
"title": "BroadcastChannel — Cross-tab Sync",
|
|
1411
1413
|
"category": "realtime",
|
|
1412
1414
|
"version": "0.1.0",
|
|
1413
|
-
"description": "
|
|
1414
|
-
"source": "
|
|
1415
|
+
"description": "Same-origin cross-tab + cross-iframe state sync via BroadcastChannel API. Tiny, no backend, no install.",
|
|
1416
|
+
"source": "Web Platform — BroadcastChannel API",
|
|
1415
1417
|
"slicePath": "frontend/slices/broadcast-channel-sync",
|
|
1416
1418
|
"convexPaths": [],
|
|
1417
1419
|
"npm": [],
|
|
@@ -1422,9 +1424,10 @@
|
|
|
1422
1424
|
"tags": [
|
|
1423
1425
|
"realtime",
|
|
1424
1426
|
"cross-tab",
|
|
1425
|
-
"broadcast-channel"
|
|
1427
|
+
"broadcast-channel",
|
|
1428
|
+
"demo-pattern"
|
|
1426
1429
|
],
|
|
1427
|
-
"agentRecipe": ""
|
|
1430
|
+
"agentRecipe": "Use BroadcastChannel only for demo / cross-iframe state mirroring. Production data still goes through Convex realtime. Use the useBroadcastSync(channelName, initial) hook from @/features/broadcast-channel-sync."
|
|
1428
1431
|
}
|
|
1429
1432
|
]
|
|
1430
1433
|
}
|
package/lib/rr-schema.json
CHANGED
|
@@ -64,6 +64,20 @@
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
},
|
|
67
|
+
"slices": {
|
|
68
|
+
"type": "array",
|
|
69
|
+
"description": "Tier-3 portable slice references — each entry mirrors a slice that has been lifted into this project.",
|
|
70
|
+
"items": {
|
|
71
|
+
"type": "object",
|
|
72
|
+
"required": ["slug"],
|
|
73
|
+
"properties": {
|
|
74
|
+
"slug": { "type": "string" },
|
|
75
|
+
"version": { "type": "string" },
|
|
76
|
+
"category": { "type": "string" },
|
|
77
|
+
"addedAt": { "type": "string", "format": "date" }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
67
81
|
"skills": {
|
|
68
82
|
"type": "array",
|
|
69
83
|
"items": {
|
package/lib/rr.mjs
CHANGED
|
@@ -36,6 +36,7 @@ export const DEFAULT_RR = {
|
|
|
36
36
|
},
|
|
37
37
|
template: null,
|
|
38
38
|
features: [],
|
|
39
|
+
slices: [],
|
|
39
40
|
skills: [],
|
|
40
41
|
auth: { provider: "convex-auth" },
|
|
41
42
|
convex: { self_hosted: true },
|
|
@@ -91,6 +92,23 @@ export function addFeature(rr, slug, version = "main") {
|
|
|
91
92
|
return rr;
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
export function addSlice(rr, slug, opts = {}) {
|
|
96
|
+
rr.slices = rr.slices ?? [];
|
|
97
|
+
const existing = rr.slices.find((s) => s.slug === slug);
|
|
98
|
+
if (existing) {
|
|
99
|
+
if (opts.version) existing.version = opts.version;
|
|
100
|
+
if (opts.category) existing.category = opts.category;
|
|
101
|
+
return rr;
|
|
102
|
+
}
|
|
103
|
+
rr.slices.push({ slug, version: opts.version ?? "main", category: opts.category, addedAt: today() });
|
|
104
|
+
return rr;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function removeSlice(rr, slug) {
|
|
108
|
+
rr.slices = (rr.slices ?? []).filter((s) => s.slug !== slug);
|
|
109
|
+
return rr;
|
|
110
|
+
}
|
|
111
|
+
|
|
94
112
|
export function addSkill(rr, slug, source = "anthropics", version = "main") {
|
|
95
113
|
rr.skills = rr.skills ?? [];
|
|
96
114
|
if (rr.skills.find((s) => s.slug === slug && s.source === source)) return rr;
|
|
@@ -125,6 +143,7 @@ export function validateRr(rr) {
|
|
|
125
143
|
issues.push(`layout.kind must be vertical-slice|feature-folders (got ${rr.layout.kind})`);
|
|
126
144
|
}
|
|
127
145
|
if (rr.features && !Array.isArray(rr.features)) issues.push("features must be an array");
|
|
146
|
+
if (rr.slices && !Array.isArray(rr.slices)) issues.push("slices must be an array");
|
|
128
147
|
if (rr.skills && !Array.isArray(rr.skills)) issues.push("skills must be an array");
|
|
129
148
|
return issues;
|
|
130
149
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rahman-resources",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Scaffolder + installer for Rahman Resources kitab — npx rahman-resources init/add/lift/scaffold-slice/publish-slice. Tier-3 portable feature slices + manifest + skills + CRUD workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|