replen 0.2.1 → 0.4.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
@@ -1,6 +1,6 @@
1
1
  # replen
2
2
 
3
- **Smart AI Development workflows.** One-command setup for [replen](https://replen.dev) - the AI that asks *"can we do this better?"* on your codebase, every morning.
3
+ **Smarter AI Development workflows.** One-command setup for [replen](https://replen.dev) - the AI that asks *"can we do this better?"* on your codebase. Calm cadence: 1-3 actionable matches a month, not a daily feed.
4
4
 
5
5
  ```bash
6
6
  npx replen
@@ -24,6 +24,28 @@ npx replen mcp setup # re-wire MCP using saved auth
24
24
  npx replen logout # forget saved auth (token stays valid; rotate on /settings to revoke)
25
25
  ```
26
26
 
27
+ ## Plain-shell usage (no Claude Code / Codex needed)
28
+
29
+ ```bash
30
+ npx replen run # trigger a pipeline run
31
+ npx replen progress # tail the run live; exits when done
32
+ npx replen check-new # one-shot: any new actionable matches since
33
+ # you last engaged? (also runs automatically
34
+ # at every Claude Code session start)
35
+ npx replen feed # show recent matches (default 2 days)
36
+ npx replen watch # keep a terminal open — rings the bell when
37
+ # a new match lands. Default poll 5min.
38
+ npx replen search <query> # full-text search past matches
39
+ npx replen starred # starred matches + handoff PR status
40
+ npx replen handoff <matchId> # open the handoff PR for a starred match
41
+ ```
42
+
43
+ `watch` is the calm-utility companion: leave it running in a `tmux` pane and forget about it. The first poll establishes a baseline; existing matches don't ring.
44
+
45
+ `check-new` is wired into Claude Code automatically by `npx replen` setup — it installs a SessionStart hook that runs `npx replen check-new --hook` whenever you open Claude Code. The hook is silent unless there's something new since you last engaged (across dashboard, email, or a prior session); when there is, the new matches show up in the agent's opening context without you having to ask. Calm-cadence by design.
46
+
47
+ Every data command accepts `--json` for scripting.
48
+
27
49
  ## Self-host
28
50
 
29
51
  Pointing at your own replen instance:
package/dist/commands.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // terminal rather than returning JSON to an agent. `--json` flag on every
3
3
  // command dumps raw JSON for piping/scripting.
4
4
  import { apiGet, apiPost, loadConfigOrExit } from "./api.js";
5
+ import { configPath } from "./config.js";
5
6
  function hasFlag(argv, flag) {
6
7
  return argv.includes(flag);
7
8
  }
@@ -169,6 +170,154 @@ export async function runStarred(argv) {
169
170
  handleApiError(e);
170
171
  }
171
172
  }
173
+ // One-shot "is there anything new" check. Used in two modes:
174
+ // 1. interactive (`replen check-new`): always prints — useful for ad-hoc
175
+ // "did anything land?" before opening Claude Code.
176
+ // 2. hook (`replen check-new --hook`): SILENT when nothing's new. Tight
177
+ // timeout so a slow API can't stall every Claude Code session opening.
178
+ // Errors are swallowed and never fail the session. Output is shaped
179
+ // so Claude Code's SessionStart-hook stdout injection naturally
180
+ // surfaces the matches in the agent's opening context.
181
+ export async function runCheckNew(argv) {
182
+ const hookMode = hasFlag(argv, "--hook");
183
+ const json = hasFlag(argv, "--json");
184
+ const repo = getFlag(argv, "--repo");
185
+ // Hook mode runs on every Claude Code session for every user — including
186
+ // users who installed Claude Code but never ran `npx replen`. Silent exit
187
+ // (not the "Not signed in" prompt loadConfigOrExit prints) is required.
188
+ const { readConfig } = await import("./config.js");
189
+ const cfg = await readConfig();
190
+ if (!cfg) {
191
+ if (hookMode)
192
+ return;
193
+ console.error("Not signed in. Run `npx replen` first.");
194
+ process.exit(1);
195
+ }
196
+ const query = {};
197
+ if (repo !== undefined)
198
+ query.repo = repo;
199
+ let r;
200
+ try {
201
+ if (hookMode) {
202
+ // Tight timeout so a slow API never blocks a session open. AbortSignal
203
+ // would be cleaner but apiGet doesn't surface it; race a manual timer.
204
+ r = await Promise.race([
205
+ apiGet(cfg, "/api/mcp/check-new", query),
206
+ new Promise((_, reject) => setTimeout(() => reject(new Error("hook-timeout")), 2000)),
207
+ ]);
208
+ }
209
+ else {
210
+ r = await apiGet(cfg, "/api/mcp/check-new", query);
211
+ }
212
+ }
213
+ catch (e) {
214
+ if (hookMode) {
215
+ // Silent failure: never disrupt a session for a non-critical signal.
216
+ // Trace goes to a log so we can debug if needed.
217
+ try {
218
+ const { appendFile } = await import("node:fs/promises");
219
+ await appendFile(configPath().replace(/config\.json$/, "check-new-hook.log"), `${new Date().toISOString()} ${e.message ?? String(e)}\n`);
220
+ }
221
+ catch {
222
+ // intentionally ignore — diagnostic only
223
+ }
224
+ return;
225
+ }
226
+ handleApiError(e);
227
+ return;
228
+ }
229
+ if (json) {
230
+ console.log(JSON.stringify(r, null, 2));
231
+ return;
232
+ }
233
+ if (!r.hasNew) {
234
+ if (!hookMode) {
235
+ // Interactive: tell the user explicitly. In hook mode silence is the
236
+ // correct response (calm-cadence principle).
237
+ const scope = r.scopedTo ? ` for ${r.scopedTo}` : "";
238
+ console.log(`No new actionable matches${scope} since you last engaged.`);
239
+ }
240
+ return;
241
+ }
242
+ // hasNew: print a tight block. Same shape in hook + interactive — Claude
243
+ // Code's SessionStart hook injects stdout verbatim into the agent's
244
+ // opening context, so the agent sees this and naturally surfaces it.
245
+ const scope = r.scopedTo ?? "your projects";
246
+ const banner = `Replen: ${r.count} new actionable match${r.count === 1 ? "" : "es"} for ${scope} since you last engaged.`;
247
+ console.log(banner);
248
+ console.log("");
249
+ for (const m of r.matches ?? []) {
250
+ const effort = m.effortBand ? ` · ${m.effortBand}` : "";
251
+ const score = m.relevanceScore != null ? ` ${m.relevanceScore}` : "";
252
+ const repoName = m.repo ?? "(no repo)";
253
+ console.log(` • ${repoName} (${m.relevance}${score}${effort})`);
254
+ if (m.oneLine)
255
+ console.log(` ${m.oneLine}`);
256
+ }
257
+ console.log("");
258
+ console.log("Call replen_today for the full writeups.");
259
+ }
260
+ // Watch for new matches in the background. Polls /api/mcp/today, diffs against
261
+ // the matchIds seen on the previous poll, prints anything new, and rings the
262
+ // terminal bell (\x07). First poll establishes a baseline — existing matches
263
+ // don't ring. Default interval 5 minutes (the pipeline runs daily by default,
264
+ // so polling tighter is pure overhead).
265
+ export async function runWatch(argv) {
266
+ const cfg = await loadConfigOrExit();
267
+ const json = hasFlag(argv, "--json");
268
+ const noBell = hasFlag(argv, "--no-bell");
269
+ const intervalSec = Math.max(30, Number(getFlag(argv, "--interval") ?? 300));
270
+ const days = Number(getFlag(argv, "--days") ?? 2);
271
+ const project = getFlag(argv, "--project");
272
+ const relevance = getFlag(argv, "--relevance") ?? "high,medium";
273
+ const seen = new Set();
274
+ let firstPass = true;
275
+ if (!json) {
276
+ const target = project ? ` · project=${project}` : "";
277
+ console.log(`Watching for new ${relevance} matches${target} · poll every ${intervalSec}s · Ctrl-C to stop.`);
278
+ }
279
+ while (true) {
280
+ let r;
281
+ try {
282
+ r = await apiGet(cfg, "/api/mcp/today", {
283
+ days,
284
+ relevance,
285
+ project,
286
+ });
287
+ }
288
+ catch (e) {
289
+ // Don't kill the watcher on a transient network blip — just log + retry.
290
+ console.error(`✗ ${e.message ?? String(e)} (retrying in ${intervalSec}s)`);
291
+ await sleep(intervalSec * 1000);
292
+ continue;
293
+ }
294
+ const fresh = [];
295
+ for (const m of r.matches) {
296
+ if (!seen.has(m.matchId)) {
297
+ if (!firstPass)
298
+ fresh.push(m);
299
+ seen.add(m.matchId);
300
+ }
301
+ }
302
+ if (json) {
303
+ // Emit one JSON line per poll, regardless of new/no-new — easy to pipe.
304
+ console.log(JSON.stringify({ ts: new Date().toISOString(), new: fresh.length, matches: fresh }));
305
+ }
306
+ else if (fresh.length > 0) {
307
+ const stamp = new Date().toISOString().slice(11, 19);
308
+ console.log(`\n[${stamp}] ${fresh.length} new:`);
309
+ for (const m of fresh)
310
+ renderMatchLine(m);
311
+ if (!noBell)
312
+ process.stdout.write("\x07");
313
+ }
314
+ else if (firstPass) {
315
+ console.log(`Baseline: ${seen.size} match(es) already in feed; will alert on anything new.`);
316
+ }
317
+ firstPass = false;
318
+ await sleep(intervalSec * 1000);
319
+ }
320
+ }
172
321
  // Open a handoff PR for a starred match in the matched project's own repo.
173
322
  export async function runHandoff(argv) {
174
323
  const cfg = await loadConfigOrExit();
package/dist/index.js CHANGED
@@ -3,8 +3,8 @@ import { runInit } from "./init.js";
3
3
  import { setupMcp } from "./mcp-setup.js";
4
4
  import { readConfig, configPath } from "./config.js";
5
5
  import { runProjectInit } from "./project-init.js";
6
- import { runFeed, runHandoff, runProgress, runRun, runSearch, runStarred } from "./commands.js";
7
- const HELP = `replen: Smart AI Development workflows
6
+ import { runCheckNew, runFeed, runHandoff, runProgress, runRun, runSearch, runStarred, runWatch } from "./commands.js";
7
+ const HELP = `replen: Smarter AI Development workflows
8
8
 
9
9
  Usage:
10
10
  npx replen Sign up / sign in + wire MCP into Claude Code
@@ -18,9 +18,19 @@ Usage:
18
18
  Use replen from a plain shell (no Claude Code / Codex needed):
19
19
  npx replen run Trigger a fresh pipeline run
20
20
  npx replen progress Live tail of the current run; exits when done
21
+ npx replen check-new One-shot: any new actionable matches
22
+ [--repo OWNER/NAME] since you last engaged? Used by the
23
+ SessionStart hook to surface new
24
+ matches automatically in Claude Code.
21
25
  npx replen feed [--days N] Today's matches (default 2 days)
22
26
  [--project SLUG] Limit to one project
23
27
  [--relevance high,medium]
28
+ npx replen watch Long-running poll: rings the terminal bell
29
+ [--interval SEC] when a new match lands (default 300s).
30
+ [--days N] First poll establishes baseline; existing
31
+ [--project SLUG] matches don't ring. Ctrl-C to stop.
32
+ [--relevance high,medium]
33
+ [--no-bell]
24
34
  npx replen search <query> Full-text search across past matches
25
35
  npx replen starred Starred matches + handoff PR status
26
36
  npx replen handoff <matchId> Open the handoff PR for a starred match
@@ -88,6 +98,10 @@ async function main() {
88
98
  return runProgress(argv);
89
99
  if (cmd === "feed")
90
100
  return runFeed(argv);
101
+ if (cmd === "watch")
102
+ return runWatch(argv);
103
+ if (cmd === "check-new")
104
+ return runCheckNew(argv);
91
105
  if (cmd === "search")
92
106
  return runSearch(argv);
93
107
  if (cmd === "starred")
package/dist/init.js CHANGED
@@ -55,7 +55,15 @@ function waitForCallback(port, expectedState) {
55
55
  res.end("state mismatch");
56
56
  return;
57
57
  }
58
- res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
58
+ // Referrer-Policy: no-referrer means anything rendered in SUCCESS_HTML
59
+ // (even if a future maintainer adds an external <img>, <link>, or fetch)
60
+ // cannot leak the callback URL — which carries the exchange code in
61
+ // its query string — via an outbound Referer header.
62
+ res.writeHead(200, {
63
+ "content-type": "text/html; charset=utf-8",
64
+ "referrer-policy": "no-referrer",
65
+ "cache-control": "no-store",
66
+ });
59
67
  res.end(SUCCESS_HTML);
60
68
  // Give the response a tick to flush before we shut down the listener.
61
69
  setTimeout(() => server.close(), 100);
package/dist/mcp-setup.js CHANGED
@@ -59,6 +59,39 @@ export async function setupMcp(token, base) {
59
59
  DIGEST_TOKEN: token,
60
60
  },
61
61
  };
62
- writeJsonAtomic(CONFIG_PATH, { ...config, mcpServers });
62
+ const hooks = installSessionStartHook(config.hooks ?? {});
63
+ writeJsonAtomic(CONFIG_PATH, { ...config, mcpServers, hooks });
63
64
  console.log(` ✓ ${overwrite ? "Updated" : "Added"} "${SERVER_NAME}" in ${CONFIG_PATH}`);
65
+ console.log(` ✓ Installed SessionStart hook (surfaces new matches automatically)`);
66
+ }
67
+ // SessionStart hook: on every Claude Code session, runs `replen check-new
68
+ // --hook`, which prints a one-block summary to stdout if (and only if)
69
+ // new actionable matches landed since the user last engaged. Claude Code
70
+ // injects that stdout into the agent's opening context, so the agent
71
+ // surfaces the matches to the user without anyone having to ask.
72
+ //
73
+ // When nothing's new the command prints nothing — silence is the calm-
74
+ // cadence position. The 2-second timeout (handled inside check-new --hook)
75
+ // ensures a slow API can't stall session opening.
76
+ //
77
+ // We use the command string `npx --quiet replen check-new --hook` as our
78
+ // idempotency marker: if a SessionStart entry already contains that
79
+ // substring we replace it; otherwise we append. Any other SessionStart
80
+ // hooks the user has are preserved verbatim.
81
+ const HOOK_COMMAND = "npx --quiet replen check-new --hook";
82
+ const HOOK_MARKER = "replen check-new --hook";
83
+ function installSessionStartHook(existing) {
84
+ const sessionStart = (existing.SessionStart ?? []).slice();
85
+ const ours = {
86
+ matcher: "*",
87
+ hooks: [{ type: "command", command: HOOK_COMMAND }],
88
+ };
89
+ const idx = sessionStart.findIndex((entry) => (entry.hooks ?? []).some((h) => typeof h.command === "string" && h.command.includes(HOOK_MARKER)));
90
+ if (idx >= 0) {
91
+ sessionStart[idx] = ours;
92
+ }
93
+ else {
94
+ sessionStart.push(ours);
95
+ }
96
+ return { ...existing, SessionStart: sessionStart };
64
97
  }
@@ -21,7 +21,18 @@ Be specific. Use the project's actual domain vocabulary, not generic abstraction
21
21
 
22
22
  When you're done, print the draft in chat and ask me to confirm before writing the file. If a CLAUDE.md already exists, don't overwrite it without my explicit say-so.
23
23
 
24
- Total length target: 200-500 words, bullet points over paragraphs.`;
24
+ Total length target: 200-500 words, bullet points over paragraphs.
25
+
26
+ Also append a short "## Replen integration" section with this exact guidance:
27
+
28
+ At the start of every session, if the \`replen_check_new\` tool is
29
+ available, call it once before asking the user what to work on.
30
+ If \`hasNew\` is true, summarise the count + repos in 1-2 lines and
31
+ ask if the user wants the full writeups (which they get via
32
+ \`replen_today\`). If \`hasNew\` is false, say nothing — silence is
33
+ the correct response, not "no new matches found".
34
+
35
+ This instruction is a fallback for AI tools that don't run Claude Code's SessionStart hooks (e.g. Codex, Cursor, Aider). For Claude Code users the hook calls \`replen check-new\` automatically and Claude Code injects the result into the opening context — but the CLAUDE.md guidance is durable across tools and survives hook misconfiguration.`;
25
36
  export function runProjectInit() {
26
37
  console.log("");
27
38
  console.log("Copy the prompt below into your AI coding tool (Claude Code / Codex / Cursor / Aider).");
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "replen",
3
- "version": "0.2.1",
4
- "description": "Smart AI Development workflows. The AI that asks 'can we do this better?' - replen reads your codebase against the live ecosystem every morning, surfaces drop-in libraries, ideas to port, and patterns to learn from. A proactive layer for your AI coding workflow. One-command setup: opens a browser to sign in, then wires the MCP server into Claude Code / Codex.",
3
+ "version": "0.4.0",
4
+ "description": "Smarter AI Development workflows. The AI that asks 'can we do this better?' - replen reads your codebase against the live ecosystem and surfaces drop-in libraries, ideas to port, and patterns to learn from. Calm cadence: 1-3 actionable matches a month. One-command setup: opens a browser to sign in, then wires the MCP server into Claude Code / Codex.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "replen": "dist/index.js"