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 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
- if (issues.length === 0) {
726
- console.log(kleur.green("✓ rr.json is valid."));
727
- console.log(` template: ${kleur.cyan(rr.template?.slug ?? "(none)")}`);
728
- console.log(` features: ${kleur.cyan(rr.features?.length ?? 0)}`);
729
- console.log(` skills: ${kleur.cyan(rr.skills?.length ?? 0)}`);
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
- console.log(kleur.red(`✖ rr.json has ${issues.length} issue(s):`));
733
- for (const i of issues) console.log(` · ${i}`);
734
- process.exit(1);
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 actually open a PR upstream.")}`);
1127
+ console.log(`\n${kleur.yellow("dry-run — pass --open-pr to scaffold the PR command.")}`);
1027
1128
  return;
1028
1129
  }
1029
1130
 
1030
- // Open a PR. We don't push branches automatically — instruct the user.
1031
- console.log(`\n${kleur.bold("Opening PR upstream:")}`);
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
- 1. Fork ${kleur.cyan("https://github.com/rahmanef63/resource-site")} (if not already).
1034
- 2. Clone your fork, copy this slice into ${kleur.cyan(`frontend/slices/<slug>/`)} and the
1035
- convex half into ${kleur.cyan(`convex/features/<slug>/`)}.
1036
- 3. Add an entry to ${kleur.cyan("lib/content/slices.ts")}.
1037
- 4. Run ${kleur.cyan("npm run slices:check")} locally.
1038
- 5. ${kleur.cyan(`gh pr create --title "feat(slices): add <slug>" --body "..."`)}.
1039
-
1040
- (Auto-PR via gh CLI not yet implemented — coming in a future release.)
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-09T13:25:41.865Z",
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 only. No Clerk, no NextAuth. Self-hosted Convex friendly. Hard mandate per kitab CLAUDE.md.",
979
- "source": "@convex-dev/auth",
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 { ResendOTP } from \"./auth/ResendOTP\";\n\nexport const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({\n providers: [ResendOTP],\n});\n\n// app/proxy.ts (Next 16 — NOT middleware.ts)\nimport { convexAuthNextjsMiddleware } from \"@convex-dev/auth/nextjs/server\";\nexport default convexAuthNextjsMiddleware();",
988
- "agentRecipe": "Mount auth in convex/auth.ts. Wire ResendOTP for magic-link delivery. Use convexAuthNextjsMiddleware in app/proxy.ts (Next 16 renamed middleware.ts proxy.ts). Forbid Clerk per CLAUDE.md.",
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
- "email-magic-link",
971
+ "magic-link",
993
972
  "no-clerk"
994
973
  ]
995
974
  },
996
975
  {
997
- "slug": "broadcast-channel-sync",
998
- "title": "BroadcastChannelCross-iframe Live Sync",
999
- "category": "realtime",
1000
- "description": "Same-origin iframe live sync without backend. Used in T1 split preview tab submit form di Public, action propagates ke Admin secara realtime via window.BroadcastChannel.",
1001
- "source": "Web Platform — BroadcastChannel API",
1002
- "docsUrl": "https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API",
1003
- "install": "// no install — Web Platform API",
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": "MidtransIndonesia 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
- "openai"
984
+ "midtrans-client"
1024
985
  ],
1025
- "exampleCode": "// convex/schema.ts\nposts: defineTable({\n title: v.string(),\n body: v.string(),\n embedding: v.array(v.float64()),\n}).vectorIndex(\"by_embedding\", {\n vectorField: \"embedding\",\n dimensions: 1536,\n filterFields: [\"workspaceId\", \"status\"],\n}),\n\n// convex/posts.ts\nexport const search = action({\n args: { query: v.string(), workspaceId: v.id(\"workspaces\") },\n handler: async (ctx, args) => {\n const emb = await embed(args.query);\n return await ctx.vectorSearch(\"posts\", \"by_embedding\", {\n vector: emb,\n limit: 10,\n filter: (q) => q.eq(\"workspaceId\", args.workspaceId),\n });\n },\n});",
1026
- "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.",
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
- "search",
1029
- "vector",
1030
- "convex",
1031
- "rag"
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": "resend + react-email",
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": "// convex/shared/email/resend.ts\nimport { Resend } from \"resend\";\nimport { action } from \"../../_generated/server\";\n\nconst resend = new Resend(process.env.RESEND_API_KEY!);\n\nexport const sendNewsletter = action({\n args: { audienceId: v.string(), subject: v.string(), html: v.string() },\n handler: async (_, args) => {\n await resend.broadcasts.create({\n audienceId: args.audienceId,\n from: \"lorem.dev <hi@lorem.dev>\",\n subject: args.subject,\n html: args.html,\n });\n },\n});",
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
- "transactional"
1014
+ "resend"
1054
1015
  ]
1055
1016
  },
1056
1017
  {
1057
- "slug": "midtrans-payment",
1058
- "title": "Midtrans Indonesia Payment",
1059
- "category": "payment",
1060
- "description": "Pembayaran lokal Indonesia via Midtrans Snap (BCA, Mandiri, BRI, e-wallet GoPay/OVO/Dana, QRIS). Webhook untuk konfirmasi.",
1061
- "source": "midtrans-client",
1062
- "docsUrl": "https://docs.midtrans.com",
1063
- "install": "npm i midtrans-client",
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
- "midtrans-client"
1026
+ "ai",
1027
+ "@openrouter/ai-sdk-provider"
1066
1028
  ],
1067
- "exampleCode": "// convex/shared/billing/midtrans.ts\nimport midtransClient from \"midtrans-client\";\nimport { action } from \"../../_generated/server\";\n\nconst snap = new midtransClient.Snap({\n isProduction: false,\n serverKey: process.env.MIDTRANS_SERVER_KEY!,\n});\n\nexport const createPayment = action({\n args: { orderId: v.string(), amount: v.number(), customer: v.any() },\n handler: async (_, args) => {\n const tx = await snap.createTransaction({\n transaction_details: { order_id: args.orderId, gross_amount: args.amount },\n customer_details: args.customer,\n });\n return tx.redirect_url;\n },\n});",
1068
- "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.",
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
- "payment",
1071
- "midtrans",
1072
- "indonesia",
1073
- "qris"
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 Blog Content",
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": "next-mdx-remote",
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": "// app/blog/[slug]/page.tsx\nimport { MDXRemote } from \"next-mdx-remote/rsc\";\nimport readingTime from \"reading-time\";\n\nexport default async function Page({ params }) {\n const { slug } = await params;\n const post = await getPost(slug);\n const stats = readingTime(post.body);\n\n return (\n <article>\n <h1>{post.title}</h1>\n <p>{stats.text}</p>\n <MDXRemote\n source={post.body}\n options={{ mdxOptions: { rehypePlugins: [rehypePrettyCode], remarkPlugins: [remarkGfm] } }}\n />\n </article>\n );\n}",
1091
- "agentRecipe": "Store post body sebagai markdown di Convex. Render dengan MDXRemote di [slug]/page.tsx. Auto-extract headings ke ToC via remark plugin custom.",
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
- "mdx",
1094
- "markdown",
1076
+ "content",
1095
1077
  "blog",
1096
- "content"
1078
+ "mdx",
1079
+ "static"
1097
1080
  ]
1098
1081
  },
1099
1082
  {
1100
1083
  "slug": "cal-com-booking",
1101
- "title": "Cal.com Booking Embed",
1084
+ "title": "Cal.com Booking",
1102
1085
  "category": "data",
1103
- "description": "Embed Cal.com booking widget di halaman Services. Self-hosted atau cloud. Webhook ke Convex untuk sync booking ke leads table.",
1104
- "source": "@calcom/embed-react",
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": "\"use client\";\nimport Cal from \"@calcom/embed-react\";\n\nexport function CalEmbed({ eventType }: { eventType: string }) {\n return (\n <Cal\n calLink={`lorem/${eventType}`}\n style={{ width: \"100%\", height: \"600px\", overflow: \"scroll\" }}\n config={{ layout: \"month_view\", theme: \"dark\" }}\n />\n );\n}",
1111
- "agentRecipe": "Embed Cal.com via @calcom/embed-react di halaman services. Configure webhook di Cal.com dashboard → POST ke /api/cal-webhook → create lead di Convex.",
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
- "booking",
1114
- "cal-com",
1096
+ "data",
1115
1097
  "scheduling",
1116
- "embed"
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 checkout + webhook + transaction history. Provider-isolated under components/providers/midtrans + actions/midtrans so Doku/Stripe land as siblings.",
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": "Subscribe form + admin send-broadcast pipeline via Resend.",
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 @convex-dev/vector-search. Embed on insert, query by vector similarity.",
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": "File-based MDX blog under content/blog/*.mdx. List + detail page + RSS feed. No backend.",
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": "Cross-tab + cross-iframe state sync via BroadcastChannel + localStorage fallback. Tiny, no backend.",
1414
- "source": "rahmanef63/resource-site",
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
  }
@@ -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.7.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",