replen 0.3.0 → 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
@@ -29,6 +29,9 @@ npx replen logout # forget saved auth (token stays valid; rotate on /setti
29
29
  ```bash
30
30
  npx replen run # trigger a pipeline run
31
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)
32
35
  npx replen feed # show recent matches (default 2 days)
33
36
  npx replen watch # keep a terminal open — rings the bell when
34
37
  # a new match lands. Default poll 5min.
@@ -37,7 +40,9 @@ npx replen starred # starred matches + handoff PR status
37
40
  npx replen handoff <matchId> # open the handoff PR for a starred match
38
41
  ```
39
42
 
40
- `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. Pair with `--no-bell` for a silent log feed, or `--json` to pipe into anything.
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.
41
46
 
42
47
  Every data command accepts `--json` for scripting.
43
48
 
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,93 @@ 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
+ }
172
260
  // Watch for new matches in the background. Polls /api/mcp/today, diffs against
173
261
  // the matchIds seen on the previous poll, prints anything new, and rings the
174
262
  // terminal bell (\x07). First poll establishes a baseline — existing matches
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ 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, runWatch } from "./commands.js";
6
+ import { runCheckNew, runFeed, runHandoff, runProgress, runRun, runSearch, runStarred, runWatch } from "./commands.js";
7
7
  const HELP = `replen: Smarter AI Development workflows
8
8
 
9
9
  Usage:
@@ -18,6 +18,10 @@ 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]
@@ -96,6 +100,8 @@ async function main() {
96
100
  return runFeed(argv);
97
101
  if (cmd === "watch")
98
102
  return runWatch(argv);
103
+ if (cmd === "check-new")
104
+ return runCheckNew(argv);
99
105
  if (cmd === "search")
100
106
  return runSearch(argv);
101
107
  if (cmd === "starred")
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,6 +1,6 @@
1
1
  {
2
2
  "name": "replen",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
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": {