replen 0.3.0 → 0.4.1
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 +6 -1
- package/dist/commands.js +106 -0
- package/dist/index.js +7 -1
- package/dist/mcp-setup.js +34 -1
- package/dist/project-init.js +12 -1
- package/package.json +1 -1
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.
|
|
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,111 @@ 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. Bounded
|
|
177
|
+
// timeout (5s) so a slow API can't stall every Claude Code session
|
|
178
|
+
// opening. Uses AbortController to actually cancel the in-flight fetch
|
|
179
|
+
// on timeout — without that, an aborted hook would still let the server
|
|
180
|
+
// finish + bump the cursor, "consuming" the matches without ever
|
|
181
|
+
// surfacing them. Errors are swallowed and never fail the session.
|
|
182
|
+
// Output is shaped so Claude Code's SessionStart-hook stdout injection
|
|
183
|
+
// naturally surfaces the matches in the agent's opening context.
|
|
184
|
+
export async function runCheckNew(argv) {
|
|
185
|
+
const hookMode = hasFlag(argv, "--hook");
|
|
186
|
+
const json = hasFlag(argv, "--json");
|
|
187
|
+
const repo = getFlag(argv, "--repo");
|
|
188
|
+
// Hook mode runs on every Claude Code session for every user — including
|
|
189
|
+
// users who installed Claude Code but never ran `npx replen`. Silent exit
|
|
190
|
+
// (not the "Not signed in" prompt loadConfigOrExit prints) is required.
|
|
191
|
+
const { readConfig } = await import("./config.js");
|
|
192
|
+
const cfg = await readConfig();
|
|
193
|
+
if (!cfg) {
|
|
194
|
+
if (hookMode)
|
|
195
|
+
return;
|
|
196
|
+
console.error("Not signed in. Run `npx replen` first.");
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
const query = {};
|
|
200
|
+
if (repo !== undefined)
|
|
201
|
+
query.repo = repo;
|
|
202
|
+
let r;
|
|
203
|
+
try {
|
|
204
|
+
if (hookMode) {
|
|
205
|
+
// Direct fetch with AbortController so the timeout actually cancels
|
|
206
|
+
// the request (apiGet has no signal). Without this, a "timed out"
|
|
207
|
+
// hook would still let the server complete the call and bump the
|
|
208
|
+
// cursor, silently consuming the matches.
|
|
209
|
+
const ctrl = new AbortController();
|
|
210
|
+
const timer = setTimeout(() => ctrl.abort(), 5000);
|
|
211
|
+
try {
|
|
212
|
+
const url = new URL(cfg.base + "/api/mcp/check-new");
|
|
213
|
+
if (query.repo !== undefined)
|
|
214
|
+
url.searchParams.set("repo", query.repo);
|
|
215
|
+
const res = await fetch(url, {
|
|
216
|
+
headers: { "x-digest-token": cfg.token, accept: "application/json" },
|
|
217
|
+
signal: ctrl.signal,
|
|
218
|
+
});
|
|
219
|
+
if (!res.ok)
|
|
220
|
+
throw new Error(`HTTP ${res.status}`);
|
|
221
|
+
r = (await res.json());
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
clearTimeout(timer);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
r = await apiGet(cfg, "/api/mcp/check-new", query);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch (e) {
|
|
232
|
+
if (hookMode) {
|
|
233
|
+
// Silent failure: never disrupt a session for a non-critical signal.
|
|
234
|
+
// Trace goes to a log so we can debug if needed.
|
|
235
|
+
try {
|
|
236
|
+
const { appendFile } = await import("node:fs/promises");
|
|
237
|
+
await appendFile(configPath().replace(/config\.json$/, "check-new-hook.log"), `${new Date().toISOString()} ${e.message ?? String(e)}\n`);
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
// intentionally ignore — diagnostic only
|
|
241
|
+
}
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
handleApiError(e);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (json) {
|
|
248
|
+
console.log(JSON.stringify(r, null, 2));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (!r.hasNew) {
|
|
252
|
+
if (!hookMode) {
|
|
253
|
+
// Interactive: tell the user explicitly. In hook mode silence is the
|
|
254
|
+
// correct response (calm-cadence principle).
|
|
255
|
+
const scope = r.scopedTo ? ` for ${r.scopedTo}` : "";
|
|
256
|
+
console.log(`No new actionable matches${scope} since you last engaged.`);
|
|
257
|
+
}
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
// hasNew: print a tight block. Same shape in hook + interactive — Claude
|
|
261
|
+
// Code's SessionStart hook injects stdout verbatim into the agent's
|
|
262
|
+
// opening context, so the agent sees this and naturally surfaces it.
|
|
263
|
+
const scope = r.scopedTo ?? "your projects";
|
|
264
|
+
const banner = `Replen: ${r.count} new actionable match${r.count === 1 ? "" : "es"} for ${scope} since you last engaged.`;
|
|
265
|
+
console.log(banner);
|
|
266
|
+
console.log("");
|
|
267
|
+
for (const m of r.matches ?? []) {
|
|
268
|
+
const effort = m.effortBand ? ` · ${m.effortBand}` : "";
|
|
269
|
+
const score = m.relevanceScore != null ? ` ${m.relevanceScore}` : "";
|
|
270
|
+
const repoName = m.repo ?? "(no repo)";
|
|
271
|
+
console.log(` • ${repoName} (${m.relevance}${score}${effort})`);
|
|
272
|
+
if (m.oneLine)
|
|
273
|
+
console.log(` ${m.oneLine}`);
|
|
274
|
+
}
|
|
275
|
+
console.log("");
|
|
276
|
+
console.log("Call replen_today for the full writeups.");
|
|
277
|
+
}
|
|
172
278
|
// Watch for new matches in the background. Polls /api/mcp/today, diffs against
|
|
173
279
|
// the matchIds seen on the previous poll, prints anything new, and rings the
|
|
174
280
|
// 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
|
-
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "replen",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
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": {
|