lopebase 0.4.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,6 +16,10 @@
16
16
  <a href="https://www.npmjs.com/package/@lopebase/adapter">@lopebase/adapter</a>
17
17
  </p>
18
18
 
19
+ <p align="center">
20
+ <a href="https://allmcps.com/mcp/lopebase"><img src="https://allmcps.com/api/badge/lopebase?style=shield" alt="AllMCPs Verified"></a>
21
+ </p>
22
+
19
23
  ---
20
24
 
21
25
  LopeBase is one place to see and run all of your products: traffic, search,
package/dist/lopebase.js CHANGED
@@ -1704,6 +1704,46 @@ var SOURCE_RULES = {
1704
1704
  },
1705
1705
  why: "Error counts next to your traffic, so a bad deploy shows up."
1706
1706
  },
1707
+ rollbar: {
1708
+ label: "Rollbar",
1709
+ rule: {
1710
+ deps: [
1711
+ /^rollbar$/,
1712
+ /^@rollbar\//,
1713
+ /^py:rollbar$/,
1714
+ /^gem:rollbar$/,
1715
+ /^composer:rollbar\//
1716
+ ],
1717
+ env: [/ROLLBAR_ACCESS_TOKEN/]
1718
+ },
1719
+ why: "Error counts next to your traffic, so a bad deploy shows up."
1720
+ },
1721
+ honeybadger: {
1722
+ label: "Honeybadger",
1723
+ rule: {
1724
+ deps: [
1725
+ /^@honeybadger-io\//,
1726
+ /^py:honeybadger$/,
1727
+ /^gem:honeybadger$/,
1728
+ /^composer:honeybadger-io\//
1729
+ ],
1730
+ env: [/HONEYBADGER_API_KEY/]
1731
+ },
1732
+ why: "Error counts next to your traffic, so a bad deploy shows up."
1733
+ },
1734
+ bugsnag: {
1735
+ label: "Bugsnag",
1736
+ rule: {
1737
+ deps: [
1738
+ /^@bugsnag\//,
1739
+ /^py:bugsnag$/,
1740
+ /^gem:bugsnag$/,
1741
+ /^composer:bugsnag\//
1742
+ ],
1743
+ env: [/BUGSNAG_API_KEY/]
1744
+ },
1745
+ why: "Error counts next to your traffic, so a bad deploy shows up."
1746
+ },
1707
1747
  stripe: {
1708
1748
  label: "Stripe",
1709
1749
  rule: {
@@ -2156,7 +2196,7 @@ function detectStack(facts) {
2156
2196
  why: SOURCE_RULES.posthog.why
2157
2197
  });
2158
2198
  }
2159
- if (!has("sentry")) {
2199
+ if (!["sentry", "rollbar", "honeybadger", "bugsnag"].some((id) => has(id))) {
2160
2200
  if (gaps.push("No error logging found."), !listed("sentry"))
2161
2201
  sources.push({
2162
2202
  source: "sentry",
@@ -2168,6 +2208,38 @@ function detectStack(facts) {
2168
2208
  }
2169
2209
  return { stack: tidy, sources, gaps };
2170
2210
  }
2211
+ var QUEUES = [
2212
+ [/^bullmq$|^bull$/, "Job queue (BullMQ)"],
2213
+ [/^inngest$/, "Background jobs (Inngest)"],
2214
+ [/^@trigger\.dev\//, "Background jobs (Trigger.dev)"],
2215
+ [/^@upstash\/qstash$/, "Message queue (QStash)"],
2216
+ [/^py:celery$/, "Job queue (Celery)"],
2217
+ [/^gem:sidekiq$/, "Job queue (Sidekiq)"]
2218
+ ], NOT_A_SERVICE = /* @__PURE__ */ new Set(["Prisma", "Drizzle"]);
2219
+ function watchList(d, facts) {
2220
+ let slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""), out = [];
2221
+ if (d.sources.some((s) => s.source === "uptime") || facts.siteUrl)
2222
+ out.push({ id: "site", label: facts.siteUrl ?? "Website", how: "address" });
2223
+ for (let s of d.stack)
2224
+ if (s.kind === "database" && !NOT_A_SERVICE.has(s.name))
2225
+ out.push({ id: slug(s.name), label: s.name, how: "service" });
2226
+ for (let [re, label] of QUEUES)
2227
+ if (facts.dependencies.some((dep) => re.test(dep)))
2228
+ out.push({
2229
+ id: slug(label.replace(/\s*\(.*\)/, "")) || "queue",
2230
+ label,
2231
+ how: "service"
2232
+ });
2233
+ let email = d.stack.find((s) => s.kind === "email" && s.name !== "Nodemailer");
2234
+ if (email)
2235
+ out.push({
2236
+ id: "email",
2237
+ label: `Email sending (${email.name})`,
2238
+ how: "service"
2239
+ });
2240
+ let seen = /* @__PURE__ */ new Set;
2241
+ return out.filter((w) => !seen.has(w.id) && seen.add(w.id));
2242
+ }
2171
2243
 
2172
2244
  // src/device-login.ts
2173
2245
  async function startDeviceLogin(url, opts = {}) {
@@ -2338,6 +2410,8 @@ function onboardingPrompt(input) {
2338
2410
  `- Stack: ${stack}`,
2339
2411
  `- Already in use: ${found.map((s) => s.label).join(", ") || "none of the sources LopeBase reads"}`,
2340
2412
  `- The human wants connected: ${chosen.join(", ") || "whatever get_onboarding_status recommends"}`,
2413
+ `- Worth watching for uptime: ${(input.watch ?? []).map((w) => `${w.label} (${w.how === "address" ? "uptime check on its address" : "adapter health service"})`).join(", ") || "the site, if it has one"}`,
2414
+ `- LopeBase pageviews: ${input.pageviews ? "yes, the human wants them" : "not chosen; offer once, do not push"}`,
2341
2415
  "",
2342
2416
  "How to work:",
2343
2417
  "1. Call get_onboarding_status first, and again after every step. Do the step it marks next.",
@@ -2346,7 +2420,9 @@ function onboardingPrompt(input) {
2346
2420
  "4. When a step needs the human (a browser sign-in, a payment, a permission, a key you cannot create), call request_human_action, tell them in one or two plain sentences what to do and why, and wait for them.",
2347
2421
  "5. Never print a secret in chat or write one to a tracked file. Pass keys straight to connect_source.",
2348
2422
  "6. For a source the project does not use yet (for example analytics), explain what it adds, ask before installing it, then add it the way this codebase does things.",
2349
- "7. Finish when get_onboarding_status says done. Summarize what is connected and anything left for later."
2423
+ "7. Uptime: confirm the list above against the code, and add anything it misses (a separate API or app host, a third-party API called on every request). Show the human the list. With the adapter, declare the services and use <app>/api/lopebase/health as the uptime address; otherwise use the site's address.",
2424
+ "8. Pageviews: if the human wants them, call get_pageview_snippet, add the tag once to the shared layout, deploy, and confirm with get_pageviews.",
2425
+ "9. Finish when get_onboarding_status says done. Summarize what is connected and anything left for later."
2350
2426
  ].join(`
2351
2427
  `);
2352
2428
  }
@@ -2695,7 +2771,28 @@ function scanProject(root, onProgress) {
2695
2771
  }
2696
2772
 
2697
2773
  // src/ui.ts
2698
- var import_picocolors3 = __toESM(require_picocolors(), 1), MARK = [
2774
+ var import_picocolors3 = __toESM(require_picocolors(), 1), RAMP = [
2775
+ [214, 202, 245],
2776
+ [195, 177, 240],
2777
+ [176, 152, 235],
2778
+ [163, 135, 231],
2779
+ [146, 112, 220],
2780
+ [131, 92, 208],
2781
+ [123, 82, 201],
2782
+ [103, 58, 183]
2783
+ ], BRAND = [163, 135, 231];
2784
+ function trueColor(env = process.env) {
2785
+ return /truecolor|24bit/i.test(env.COLORTERM ?? "");
2786
+ }
2787
+ function paint(rgb, text, bg = !1) {
2788
+ if (!import_picocolors3.default.isColorSupported)
2789
+ return text;
2790
+ if (!trueColor())
2791
+ return bg ? import_picocolors3.default.bgMagenta(import_picocolors3.default.black(text)) : import_picocolors3.default.magenta(text);
2792
+ let [r, g, b] = rgb;
2793
+ return `\x1B[${bg ? 48 : 38};2;${r};${g};${b}m${text}\x1B[${bg ? 49 : 39}m`;
2794
+ }
2795
+ var brand = (text) => paint(BRAND, text), badge = (text) => paint(BRAND, import_picocolors3.default.black(import_picocolors3.default.bold(` ${text} `)), !0), ramp = (i, text) => paint(RAMP[Math.min(i, RAMP.length - 1)], text), MARK = [
2699
2796
  " ▗▖ ▗▖ ",
2700
2797
  " ▐█▖▗█▌ ",
2701
2798
  " ▜██▛ ",
@@ -2711,11 +2808,11 @@ function motionAllowed(argv = process.argv, env = process.env, isTTY = Boolean(p
2711
2808
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
2712
2809
  function logoLines(opts) {
2713
2810
  let right = {
2714
- 3: `${import_picocolors3.default.bold("Lope")}${import_picocolors3.default.dim("Base")}${opts.version ? import_picocolors3.default.dim(` v${opts.version}`) : ""}`,
2811
+ 3: `${import_picocolors3.default.bold("Lope")}${brand("Base")}${opts.version ? import_picocolors3.default.dim(` v${opts.version}`) : ""}`,
2715
2812
  4: import_picocolors3.default.dim(opts.tagline)
2716
2813
  };
2717
2814
  return MARK.map((row, i) => {
2718
- let mark = HORIZON.has(i) ? import_picocolors3.default.yellow(import_picocolors3.default.dim(row)) : import_picocolors3.default.yellow(row);
2815
+ let mark = HORIZON.has(i) ? import_picocolors3.default.dim(ramp(i, row)) : ramp(i, row);
2719
2816
  return right[i] ? `${mark} ${right[i]}` : mark;
2720
2817
  });
2721
2818
  }
@@ -2729,7 +2826,7 @@ ${lines.join(`
2729
2826
  `);
2730
2827
  return;
2731
2828
  }
2732
- let blank = MARK.map((r) => " ".repeat(r.length)), frame = (shown) => MARK.map((row, i) => shown.has(i) ? HORIZON.has(i) ? import_picocolors3.default.yellow(import_picocolors3.default.dim(row)) : import_picocolors3.default.yellow(row) : blank[i]), up = `\x1B[${MARK.length}A`, onInterrupt = () => {
2829
+ let blank = MARK.map((r) => " ".repeat(r.length)), frame = (shown) => MARK.map((row, i) => shown.has(i) ? HORIZON.has(i) ? import_picocolors3.default.dim(ramp(i, row)) : ramp(i, row) : blank[i]), up = `\x1B[${MARK.length}A`, onInterrupt = () => {
2733
2830
  write(`\x1B[?25h
2734
2831
  `), process.exit(130);
2735
2832
  };
@@ -2805,16 +2902,34 @@ var PLAN = [
2805
2902
  }
2806
2903
  ];
2807
2904
  function planLines() {
2808
- return PLAN.map((s, i) => `${import_picocolors3.default.yellow(String(i + 1))} ${import_picocolors3.default.bold(s.title)}
2905
+ return PLAN.map((s, i) => `${brand(String(i + 1))} ${import_picocolors3.default.bold(s.title)}
2809
2906
  ${import_picocolors3.default.dim(s.detail)}`);
2810
2907
  }
2908
+ function stepRail(n, total = PLAN.length) {
2909
+ return Array.from({ length: total }, (_, i) => i < n ? brand("◆") : import_picocolors3.default.dim("◇")).join(import_picocolors3.default.dim(" ── "));
2910
+ }
2811
2911
  function stepTitle(n) {
2812
- return `${import_picocolors3.default.dim(`Step ${n} of ${PLAN.length}`)} ${import_picocolors3.default.bold(PLAN[n - 1].title)}`;
2912
+ return `${stepRail(n)} ${import_picocolors3.default.dim(`Step ${n} of ${PLAN.length}`)} ${import_picocolors3.default.bold(PLAN[n - 1].title)}`;
2913
+ }
2914
+ async function horizon(opts) {
2915
+ let write = opts.write ?? ((s) => process.stdout.write(s)), width = opts.width ?? 28, line = (n) => Array.from({ length: n }, (_, i) => ramp(Math.floor(i / width * RAMP.length), "━")).join("");
2916
+ if (!opts.motion) {
2917
+ write(` ${line(width)}
2918
+ `);
2919
+ return;
2920
+ }
2921
+ for (let n = 2;n <= width; n += 2)
2922
+ write(`\r ${line(n)}`), await sleep(10);
2923
+ write(`
2924
+ `);
2925
+ }
2926
+ function watchRows(items) {
2927
+ return items.map((w) => w.how === "address" ? `${brand("◉")} ${import_picocolors3.default.bold(w.label)} ${import_picocolors3.default.dim("uptime check, every 15 minutes")}` : `${brand("○")} ${import_picocolors3.default.bold(w.label)} ${import_picocolors3.default.dim("a service in the adapter's health list")}`);
2813
2928
  }
2814
2929
 
2815
2930
  // bin/lopebase.ts
2816
2931
  var [command = "init", ...rest] = process.argv.slice(2), HELP = `
2817
- ${import_picocolors4.default.bgYellow(import_picocolors4.default.black(" LopeBase "))} Set up LopeBase from this project's folder.
2932
+ ${badge("LopeBase")} Set up LopeBase from this project's folder.
2818
2933
 
2819
2934
  ${import_picocolors4.default.bold("Usage")}
2820
2935
  npx lopebase scan, connect, and hand over to your agent
@@ -2831,12 +2946,12 @@ ${import_picocolors4.default.bold("Options")}
2831
2946
  --no-motion no animation (also LOPEBASE_NO_MOTION=1)
2832
2947
 
2833
2948
  ${import_picocolors4.default.bold("Learn more")}
2834
- ${import_picocolors4.default.cyan("https://lopebase.com")}
2949
+ ${brand("https://lopebase.com")}
2835
2950
  Help: contact@jackalope.digital
2836
2951
  `, flag = (name) => rest.includes(`--${name}`), option = (name) => {
2837
2952
  let i = rest.indexOf(`--${name}`);
2838
2953
  return i >= 0 ? rest[i + 1] : rest.find((a) => a.startsWith(`--${name}=`))?.split("=")[1];
2839
- }, cwd = process.cwd(), motion = motionAllowed(), TAGLINE = "Every product, one place.";
2954
+ }, cwd = process.cwd(), motion = motionAllowed(), TAGLINE = "Every product, one place.", PAGEVIEWS = "LopeBase pageviews";
2840
2955
  function version() {
2841
2956
  try {
2842
2957
  return JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf8")).version;
@@ -2898,7 +3013,7 @@ async function login() {
2898
3013
  let device = await startDeviceLogin(url);
2899
3014
  if (device) {
2900
3015
  Me([
2901
- `Your code: ${import_picocolors4.default.bold(import_picocolors4.default.yellow(device.userCode))}`,
3016
+ `Your code: ${import_picocolors4.default.bold(brand(device.userCode))}`,
2902
3017
  "",
2903
3018
  "We opened LopeBase in your browser. Sign in, or create a free",
2904
3019
  "account if you are new. Then check the code matches and press",
@@ -2959,6 +3074,10 @@ async function printDetection(d, facts) {
2959
3074
  if (possible.length)
2960
3075
  Me(possible.map((s) => `${import_picocolors4.default.yellow("?")} ${import_picocolors4.default.bold(s.label)} ${import_picocolors4.default.dim(`only ${s.evidence.join(", ")}`)}`).join(`
2961
3076
  `), "Maybe in use, your agent will check"), await pace(motion, 150);
3077
+ let watch = watchList(d, facts);
3078
+ if (watch.length)
3079
+ Me(watchRows(watch).join(`
3080
+ `), "Worth watching for uptime"), await pace(motion, 150);
2962
3081
  for (let gap of d.gaps)
2963
3082
  M2.info(import_picocolors4.default.dim(gap));
2964
3083
  }
@@ -2990,7 +3109,7 @@ function registerMcp(agent) {
2990
3109
  return res.status === 0 || /already exists/i.test(`${res.stderr}${res.stdout}`);
2991
3110
  }
2992
3111
  async function init() {
2993
- if (console.clear(), await showLogo({ tagline: TAGLINE, version: version(), motion }), Ie(`${import_picocolors4.default.bgYellow(import_picocolors4.default.black(" LopeBase "))} set up this project`), Me(planLines().join(`
3112
+ if (console.clear(), await showLogo({ tagline: TAGLINE, version: version(), motion }), await horizon({ motion }), Ie(`${badge("LopeBase")} set up this project`), Me(planLines().join(`
2994
3113
  `), "Here is what happens"), !guard(await ye({
2995
3114
  message: `Scan ${import_picocolors4.default.bold(cwd)}?`,
2996
3115
  initialValue: !0
@@ -3015,16 +3134,32 @@ async function init() {
3015
3134
  initialValue: facts.name?.replace(/^@[^/]+\//, "") ?? ""
3016
3135
  })), chosen = guard(await fe({
3017
3136
  message: "What should we connect? (space to toggle, enter to go on)",
3018
- options: detection.sources.map((src) => ({
3019
- value: src.label,
3020
- label: src.status === "found" ? src.label : src.status === "possible" ? `${src.label} ${import_picocolors4.default.dim("(maybe in use, we'll check)")}` : `${src.label} ${import_picocolors4.default.dim("(not set up yet, we'll help)")}`,
3021
- hint: src.why
3022
- })),
3137
+ options: [
3138
+ ...detection.sources.map((src) => ({
3139
+ value: src.label,
3140
+ label: src.status === "found" ? src.label : src.status === "possible" ? `${src.label} ${import_picocolors4.default.dim("(maybe in use, we'll check)")}` : `${src.label} ${import_picocolors4.default.dim("(not set up yet, we'll help)")}`,
3141
+ hint: src.why
3142
+ })),
3143
+ ...watchList(detection, facts).some((w) => w.how === "address") ? [
3144
+ {
3145
+ value: PAGEVIEWS,
3146
+ label: `${PAGEVIEWS} ${import_picocolors4.default.dim("(one tag, no cookies)")}`,
3147
+ hint: "LopeBase counts pages, sources, and visits itself."
3148
+ }
3149
+ ] : []
3150
+ ],
3023
3151
  initialValues: detection.sources.filter((src) => src.status === "found" || src.source === "gsc" || src.source === "uptime").map((src) => src.label),
3024
3152
  required: !1
3025
3153
  }));
3026
3154
  M2.step(stepTitle(4));
3027
- let prompt = onboardingPrompt({ facts, detection, chosen, productName }), agents = installedAgents();
3155
+ let prompt = onboardingPrompt({
3156
+ facts,
3157
+ detection,
3158
+ chosen,
3159
+ productName,
3160
+ watch: watchList(detection, facts),
3161
+ pageviews: chosen.includes(PAGEVIEWS)
3162
+ }), agents = installedAgents();
3028
3163
  if (!agents.length || flag("print")) {
3029
3164
  if (!agents.length)
3030
3165
  M2.info("No coding agent found on this computer (Claude Code, Codex, and others). Paste the prompt below into the one you use.");
@@ -3051,9 +3186,10 @@ async function init() {
3051
3186
  s.stop(import_picocolors4.default.yellow(`Add an MCP server to ${agent.label} that runs: npx -y lopebase mcp`));
3052
3187
  Me([
3053
3188
  `${import_picocolors4.default.bold(agent.label)} will now:`,
3054
- ` ${import_picocolors4.default.dim("1.")} add ${import_picocolors4.default.bold(productName || "this product")} to your workspace`,
3055
- ` ${import_picocolors4.default.dim("2.")} connect ${chosen.length ? chosen.join(", ") : "what it recommends"}`,
3056
- ` ${import_picocolors4.default.dim("3.")} stop and ask you for any sign-in, key, or yes`,
3189
+ ` ${brand("1.")} add ${import_picocolors4.default.bold(productName || "this product")} to your workspace`,
3190
+ ` ${brand("2.")} connect ${chosen.length ? chosen.join(", ") : "what it recommends"}`,
3191
+ ` ${brand("3.")} pick what to watch for uptime: ${watchList(detection, facts).map((w) => w.label).join(", ") || "the site"}`,
3192
+ ` ${brand("4.")} stop and ask you for any sign-in, key, or yes`,
3057
3193
  "",
3058
3194
  import_picocolors4.default.dim("You can follow along in your workspace as each one turns green.")
3059
3195
  ].join(`
@@ -3069,12 +3205,12 @@ async function main() {
3069
3205
  case "init":
3070
3206
  return init();
3071
3207
  case "login":
3072
- return await showLogo({ tagline: TAGLINE, version: version(), motion }), Ie(`${import_picocolors4.default.bgYellow(import_picocolors4.default.black(" LopeBase "))} sign in`), await login(), Se("Ready. Run lopebase in any project.");
3208
+ return await showLogo({ tagline: TAGLINE, version: version(), motion }), Ie(`${badge("LopeBase")} sign in`), await login(), Se("Ready. Run lopebase in any project.");
3073
3209
  case "scan": {
3074
3210
  let facts = scanProject(cwd), detection = detectStack(facts);
3075
3211
  if (flag("json"))
3076
3212
  return console.log(JSON.stringify({ facts, detection }, null, 2));
3077
- return Ie(`${import_picocolors4.default.bgYellow(import_picocolors4.default.black(" LopeBase "))} scan`), await printDetection(detection, facts), Se(`Run ${import_picocolors4.default.cyan("npx lopebase")} to connect what it found.`);
3213
+ return Ie(`${badge("LopeBase")} scan`), await printDetection(detection, facts), Se(`Run ${import_picocolors4.default.cyan("npx lopebase")} to connect what it found.`);
3078
3214
  }
3079
3215
  case "prompt": {
3080
3216
  let facts = scanProject(cwd), detection = detectStack(facts);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lopebase",
3
- "version": "0.4.3",
3
+ "version": "0.5.0",
4
4
  "description": "Set up LopeBase from your project's folder: scan your stack, connect your workspace, and let your coding agent do the rest.",
5
5
  "type": "module",
6
6
  "bin": {