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 +23 -1
- package/dist/commands.js +149 -0
- package/dist/index.js +16 -2
- package/dist/init.js +9 -1
- package/dist/mcp-setup.js +34 -1
- package/dist/project-init.js +12 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# replen
|
|
2
2
|
|
|
3
|
-
**
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/project-init.js
CHANGED
|
@@ -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.
|
|
4
|
-
"description": "
|
|
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"
|