replen 1.0.2 → 1.0.4

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
@@ -1,12 +1,21 @@
1
1
  # replen
2
2
 
3
- **Make your AI coding tools aware of the wider OSS ecosystem.** One command, no API keys.
3
+ **Make your AI coding tools aware of everything your code depends on, implements, and builds on.** One command, no API keys.
4
4
 
5
5
  ```bash
6
6
  npx replen
7
7
  ```
8
8
 
9
- Claude Code, Codex, Cursor already know your code. Replen tells them what else is out there — drop-in libraries, ideas worth porting, dead deps to swap. The match decision happens *inside your AI tool's session* on your subscription tokens. Your code stays on your laptop. Replen never sees it.
9
+ Claude Code, Codex, Cursor already know your code. Replen tells them what's happening *outside* it. The match decision happens *inside your AI tool's session* on your subscription tokens. Your code stays on your laptop. Replen never sees it.
10
+
11
+ Four lenses, all surfaced the same calm way — quietly, in your AI tool's next reply, only when there's something real:
12
+
13
+ - **🔭 OSS repos** — a new library that replaces code you maintain, a pattern worth porting, a dead dep to swap.
14
+ - **📦 Your stack** — a release in a dependency you actually use (`next`, `openai`, `prisma`, `viem`, … matched against your manifest).
15
+ - **📜 Standards you implement** — EIP/ERC, TC39, and Chrome-deprecation changes matched to what your code touches.
16
+ - **🩺 Upstream health** — a dep gone stale/archived, a hot bug others hit in your deps, or an incident on a service you use.
17
+
18
+ One discipline throughout: **silence beats a weak match** — if nothing clears the bar, Replen says nothing.
10
19
 
11
20
  ## What `npx replen` does
12
21
 
package/dist/commands.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // Plain-shell subcommands. Each one mirrors an MCP tool but renders for the
2
2
  // terminal rather than returning JSON to an agent. `--json` flag on every
3
3
  // command dumps raw JSON for piping/scripting.
4
+ import { readdirSync, existsSync } from "node:fs";
4
5
  import { apiGet, apiPost, loadConfigOrExit } from "./api.js";
5
6
  import { configPath } from "./config.js";
6
7
  function hasFlag(argv, flag) {
@@ -196,6 +197,33 @@ export async function runCheckNew(argv) {
196
197
  console.error("Not signed in. Run `npx replen` first.");
197
198
  process.exit(1);
198
199
  }
200
+ // Hook mode surfaces the repo-scoped SKILL inventory directly — the good data
201
+ // (semantic + dependency / spec / health matches, with the relevance floor +
202
+ // cool-off applied server-side), using the server's pre-formatted footnote.
203
+ // It deliberately skips the legacy hosted `check-new` below, which returns
204
+ // generic, un-scoped, high-star matches that aren't useful to skill-tier
205
+ // users. Output is injected into the agent's opening context by Claude Code's
206
+ // SessionStart-hook stdout injection; the framing nudges the agent to relay
207
+ // it verbatim (final surfacing is still the model's call).
208
+ if (hookMode && !json) {
209
+ const cwdRepo = await detectCwdRepo();
210
+ if (!cwdRepo) {
211
+ // No git remote → Replen can't scope this project. If the cwd looks like
212
+ // a real project (has source / a manifest), nudge the user to set it up;
213
+ // otherwise stay silent (don't nag in /tmp or a random dir). Surfacing is
214
+ // the model's call, but the explicit framing maximises it.
215
+ if (looksLikeProject()) {
216
+ console.log("[Replen] This project has no git remote, so Replen can't scope or match against it. Mention to the user, briefly, as a footnote: 'By the way — this project isn't set up with Replen yet (no git remote), so I can't surface matches for it. Want me to scope it — init git, create the repo, write the docs, and add tags?'");
217
+ }
218
+ return;
219
+ }
220
+ const inv = await fetchInventoryStatus(cfg, cwdRepo);
221
+ if (inv?.displayText) {
222
+ console.log("[Replen] Relay the following line to the user, verbatim, as a footnote at the very end of your first reply:");
223
+ console.log(inv.displayText);
224
+ }
225
+ return;
226
+ }
199
227
  const query = {};
200
228
  if (repo !== undefined)
201
229
  query.repo = repo;
@@ -455,6 +483,31 @@ async function detectCwdRepo() {
455
483
  return null;
456
484
  }
457
485
  }
486
+ // Cheap heuristic: does the cwd look like a real project worth nudging the user
487
+ // to scope with Replen? True when there's a recognised manifest, a src/lib/app
488
+ // dir, or a couple of source files. Keeps the "no git — want to scope?" prompt
489
+ // from firing in /tmp, $HOME, or an empty directory.
490
+ function looksLikeProject() {
491
+ try {
492
+ const manifests = ["package.json", "pyproject.toml", "requirements.txt", "Cargo.toml", "go.mod", "pom.xml", "build.gradle", "Gemfile", "composer.json", "pubspec.yaml"];
493
+ if (manifests.some((m) => existsSync(m)))
494
+ return true;
495
+ if (["src", "lib", "app", "cmd", "pkg"].some((d) => existsSync(d)))
496
+ return true;
497
+ const codeExt = /\.(ts|tsx|js|jsx|py|rs|go|java|rb|php|c|cc|cpp|h|hpp|swift|kt|scala|sol|ex|clj)$/i;
498
+ let codeFiles = 0;
499
+ for (const e of readdirSync(".", { withFileTypes: true })) {
500
+ if (e.isFile() && codeExt.test(e.name))
501
+ codeFiles++;
502
+ if (codeFiles >= 2)
503
+ return true;
504
+ }
505
+ return false;
506
+ }
507
+ catch {
508
+ return false;
509
+ }
510
+ }
458
511
  async function fetchInventoryStatus(cfg, repo) {
459
512
  // Hook mode is on the session-open critical path; cap latency hard.
460
513
  const ctrl = new AbortController();
@@ -482,6 +535,9 @@ async function fetchInventoryStatus(cfg, repo) {
482
535
  count: cands.length,
483
536
  topRepo: top.repo ?? null,
484
537
  topSimilarity: simMatch ? Number(simMatch[1]) : null,
538
+ // The server's pre-formatted, pattern-aware footnote ("By the way — a
539
+ // dependency you use just shipped: …" / "… N candidates queued …").
540
+ displayText: (typeof data.displayText === "string" && data.displayText) ? data.displayText : null,
485
541
  };
486
542
  }
487
543
  catch {
package/dist/mcp-setup.js CHANGED
@@ -22,6 +22,7 @@
22
22
  import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync, chmodSync } from "node:fs";
23
23
  import { homedir } from "node:os";
24
24
  import { join, dirname } from "node:path";
25
+ import { fileURLToPath } from "node:url";
25
26
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
26
27
  import { installSkills } from "./skill-install.js";
27
28
  const SERVER_NAME = "replen";
@@ -239,8 +240,27 @@ function backupIfExists(path) {
239
240
  // their current docs). Both rely on the AGENTS.md / GEMINI.md project-
240
241
  // context file being read at session start, which our inject step
241
242
  // covers.
242
- const HOOK_COMMAND = "npx --quiet replen check-new --hook";
243
- const HOOK_MARKER = "replen check-new --hook";
243
+ // Version-PIN the hook command. A bare `npx replen` resolves a LOCAL package
244
+ // named "replen" when one exists in cwd (e.g. the replen repo itself, whose
245
+ // server package is also "replen" and has no bin) → "could not determine
246
+ // executable to run", and the hook silently dies every session there. Pinning
247
+ // to the published version forces npx to the registry (collision-proof) and,
248
+ // because the exact version is cached, avoids the per-session `@latest`
249
+ // registry round-trip. Re-running setup refreshes the pin.
250
+ const HOOK_COMMAND = `npx --quiet replen@${cliVersion()} check-new --hook`;
251
+ // Match on the stable substring so we find/replace our hook regardless of the
252
+ // pinned version in any previously-written command.
253
+ const HOOK_MARKER = "check-new --hook";
254
+ function cliVersion() {
255
+ try {
256
+ const here = dirname(fileURLToPath(import.meta.url)); // cli/dist
257
+ const pkg = JSON.parse(readFileSync(join(here, "..", "package.json"), "utf8"));
258
+ return typeof pkg.version === "string" && pkg.version ? pkg.version : "latest";
259
+ }
260
+ catch {
261
+ return "latest";
262
+ }
263
+ }
244
264
  function installSessionStartHook(existing) {
245
265
  const sessionStart = (existing.SessionStart ?? []).slice();
246
266
  const ours = {
@@ -181,11 +181,40 @@ fresh candidate shows up at session start via the hook).
181
181
  **Inventory call returns 401.** User's token expired or got rotated.
182
182
  Tell them to run `npx replen` to re-auth.
183
183
 
184
- **Inventory returns `scopedTo: null` with a `note` about "repo not in
185
- your project list"**. The cwd isn't a known project. Ask the user:
186
- "This repo isn't in your Replen projects yet. Add it via /projects?"
187
- Then either stop or re-run with `?repo=` (empty) to see the global
188
- firehose.
184
+ **The project isn't scoped — `scopedTo: null`, a `note` about "repo not in
185
+ your project list", OR the cwd has no git remote.** Replen can only match
186
+ against a repo it has registered, and it scopes by the git remote. When it's
187
+ unscoped you'll get the global trending firehose which is noise for this
188
+ codebase. **Do NOT triage the firehose** (manufacturing reasons to care about
189
+ random trending repos is exactly what this skill must not do).
190
+
191
+ Instead, **offer to onboard the project.** Lead with ONE line, not the whole
192
+ checklist: *"This project isn't set up with Replen yet, so I can only see the
193
+ global firehose (not matches for your code). Want me to scope it — init git,
194
+ create the repo, write the docs, and add tags? Then Replen can surface things
195
+ that actually fit."* If the user agrees, run this checklist:
196
+
197
+ 1. **Git + GitHub.** If there's no git repo, `git init`. Create the GitHub
198
+ repo using the user's existing `gh` auth — ask for owner/name or suggest a
199
+ sensible default from the folder name, and confirm public vs private:
200
+ `gh repo create <owner>/<name> --private --source=. --remote=origin --push`.
201
+ If a repo exists locally but has no remote, just add + push the remote.
202
+ 2. **Docs Replen can read.** Replen's scorer reads your `README.md` +
203
+ `CLAUDE.md` to understand the project — that's the difference between
204
+ useful matches and noise. Write a concrete `README.md` (what it is, stack,
205
+ domain) if missing, and a `CLAUDE.md` optimised for Replen (run the
206
+ `/replen-project-init` protocol, or draft the seven sections directly:
207
+ what it is · stack · niche/domain · active areas · constraints/non-goals ·
208
+ anti-patterns · integration preferences). Use the project's real domain
209
+ vocabulary, not abstractions.
210
+ 3. **Register + tag.** Add the repo to Replen at **app.replen.dev/projects**
211
+ (toggle it watched), or it auto-registers on the next
212
+ `npx replen sync-projects`. Set **tags** that narrow the firehose to the
213
+ project's domain — derive them from the code (e.g. for a Python CCXT
214
+ trading engine: `crypto, trading, ccxt, python, quant, backtesting`) — at
215
+ /projects.
216
+ 4. **Re-run.** Once it has a remote + docs + tags, call `replen_match` again —
217
+ now it scopes to the project and matches against the real code.
189
218
 
190
219
  **Candidate's README is unreachable (WebFetch 404)**. Note it in the
191
220
  writeup (`Caveats: README unreachable; verdict based on description
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replen",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Make your AI coding tools smarter. One command, no API keys. Replen scouts the OSS firehose against your projects and surfaces drop-in libraries, ideas to port, and dead deps to swap — the match decision happens inside your AI tool's session on your subscription tokens. 1-3 actionable matches a month, by design.",
5
5
  "type": "module",
6
6
  "bin": {