replen 1.5.3 → 1.5.5
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/dist/commands.js +7 -2
- package/dist/discover-projects.js +1 -1
- package/dist/immerse.js +12 -2
- package/dist/index.js +6 -5
- package/dist/init.js +13 -5
- package/dist/inject-instruction.js +14 -6
- package/dist/mcp-setup.js +12 -1
- package/dist/sync-projects.js +15 -5
- package/dist/uninstall.js +5 -0
- package/extras/skills/replen-onboard/SKILL.md +6 -6
- package/package.json +2 -2
package/dist/commands.js
CHANGED
|
@@ -568,7 +568,7 @@ async function fetchInventoryStatus(cfg, repo) {
|
|
|
568
568
|
// (one source of truth) and writes them locally; you own and can open them.
|
|
569
569
|
export async function runAtlas(argv) {
|
|
570
570
|
const { mkdirSync, writeFileSync, rmSync } = await import("node:fs");
|
|
571
|
-
const { join, dirname } = await import("node:path");
|
|
571
|
+
const { join, dirname, resolve, sep } = await import("node:path");
|
|
572
572
|
const { homedir } = await import("node:os");
|
|
573
573
|
const cfg = await loadConfigOrExit();
|
|
574
574
|
const dir = getFlag(argv, "--out") ?? join(homedir(), ".replen", "atlas");
|
|
@@ -584,8 +584,13 @@ export async function runAtlas(argv) {
|
|
|
584
584
|
}
|
|
585
585
|
catch { /* */ }
|
|
586
586
|
}
|
|
587
|
+
// f.path is server-controlled: refuse any path that escapes the atlas dir
|
|
588
|
+
// (e.g. "../../.zshrc"). Mirrors the guard in mcp/src/atlas-sync.ts.
|
|
589
|
+
const root = resolve(dir);
|
|
587
590
|
for (const f of data.files) {
|
|
588
|
-
const full =
|
|
591
|
+
const full = resolve(dir, f.path);
|
|
592
|
+
if (full !== root && !full.startsWith(root + sep))
|
|
593
|
+
continue; // path traversal guard
|
|
589
594
|
mkdirSync(dirname(full), { recursive: true });
|
|
590
595
|
writeFileSync(full, f.content);
|
|
591
596
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// finds every git repo, and for each one extracts:
|
|
5
5
|
// - The repo's `owner/name` from `git remote get-url origin`
|
|
6
6
|
// - A slug derived from the GITHUB repo name (so a local folder named
|
|
7
|
-
// "drone" whose remote is acme/
|
|
7
|
+
// "drone" whose remote is acme/acme registers with slug "acme",
|
|
8
8
|
// matching what shows on GitHub). Falls back to dirname for repos
|
|
9
9
|
// without a GitHub remote.
|
|
10
10
|
// - A name (from package.json's `name` field if present, else slug)
|
package/dist/immerse.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// Self-host installs don't need this — they default on via REPLEN_SELF_HOST and
|
|
16
16
|
// the pipeline reads local disk directly.
|
|
17
17
|
import { readFileSync, statSync } from "node:fs";
|
|
18
|
-
import {
|
|
18
|
+
import { resolve, sep } from "node:path";
|
|
19
19
|
import { loadConfigOrExit, apiGet, apiPost } from "./api.js";
|
|
20
20
|
import { resolveAndWalk } from "./sync-projects.js";
|
|
21
21
|
const MAX_FILE_BYTES = 1_000_000; // mirror the server per-file cap
|
|
@@ -75,10 +75,20 @@ async function send(cfg) {
|
|
|
75
75
|
continue;
|
|
76
76
|
withPaths++;
|
|
77
77
|
// Read exactly the grounded files the server asked for (size-capped).
|
|
78
|
+
// The server sanitizes its manifest paths, but the client must not trust a
|
|
79
|
+
// server-supplied path either: reject `..` traversal and anything that
|
|
80
|
+
// resolves outside this repo so a compromised/hostile manifest can't make
|
|
81
|
+
// `immerse` read files elsewhere on disk. "Your code never leaves except
|
|
82
|
+
// these exact repo files" is only true if we enforce it here too.
|
|
83
|
+
const root = resolve(repo.localPath);
|
|
78
84
|
const files = [];
|
|
79
85
|
for (const rel of manifest.paths) {
|
|
80
86
|
try {
|
|
81
|
-
|
|
87
|
+
if (typeof rel !== "string" || rel.split(/[\\/]/).includes(".."))
|
|
88
|
+
continue;
|
|
89
|
+
const abs = resolve(root, rel);
|
|
90
|
+
if (abs !== root && !abs.startsWith(root + sep))
|
|
91
|
+
continue;
|
|
82
92
|
const st = statSync(abs);
|
|
83
93
|
if (!st.isFile() || st.size > MAX_FILE_BYTES)
|
|
84
94
|
continue;
|
package/dist/index.js
CHANGED
|
@@ -12,11 +12,12 @@ Usage:
|
|
|
12
12
|
npx replen mcp setup Re-wire MCP using saved auth
|
|
13
13
|
npx replen project-init Print a prompt your AI coding tool uses to draft
|
|
14
14
|
a CLAUDE.md tuned for replen
|
|
15
|
-
npx replen inject [-y]
|
|
16
|
-
|
|
17
|
-
Codex)
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
npx replen inject [-y] Add the "## Replen integration" section to
|
|
16
|
+
CLAUDE.md + AGENTS.md + GEMINI.md (Claude Code +
|
|
17
|
+
Codex + Gemini CLI), creating any that are
|
|
18
|
+
missing, in every repo under ~/github/, ~/code/,
|
|
19
|
+
~/projects/ so the agent auto-surfaces matches on
|
|
20
|
+
session start. Idempotent. Consent unless -y.
|
|
20
21
|
npx replen sync-projects Re-scan local repos for new GitHub remotes
|
|
21
22
|
[--root PATH ...] and register them with Replen. Run after
|
|
22
23
|
cloning a new repo, or pass --root to point
|
package/dist/init.js
CHANGED
|
@@ -13,11 +13,14 @@ function pickPort() {
|
|
|
13
13
|
return PORT_MIN + Math.floor(Math.random() * (PORT_MAX - PORT_MIN));
|
|
14
14
|
}
|
|
15
15
|
function openBrowser(url) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
// On Windows `start` is a cmd.exe builtin, not an executable — spawning it
|
|
17
|
+
// directly fails, so route through `cmd /c start "" <url>` (the "" is the
|
|
18
|
+
// required title arg). darwin/linux have real `open`/`xdg-open` binaries.
|
|
19
|
+
const [cmd, args] = platform() === "darwin" ? ["open", [url]]
|
|
20
|
+
: platform() === "win32" ? ["cmd", ["/c", "start", "", url]]
|
|
21
|
+
: ["xdg-open", [url]];
|
|
19
22
|
// Detach. We don't care about its exit.
|
|
20
|
-
const proc = spawn(cmd,
|
|
23
|
+
const proc = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
21
24
|
proc.on("error", () => {
|
|
22
25
|
// Fail silently. We print the URL anyway as fallback.
|
|
23
26
|
});
|
|
@@ -118,10 +121,15 @@ export async function runInit() {
|
|
|
118
121
|
console.log("");
|
|
119
122
|
console.log(" (Waiting for browser callback on http://127.0.0.1:" + port + "…)");
|
|
120
123
|
console.log("");
|
|
124
|
+
// Bind the loopback listener BEFORE opening the browser (waitForCallback's
|
|
125
|
+
// server.listen runs synchronously in the Promise executor). Otherwise a
|
|
126
|
+
// local port-squatter could grab the port between open and listen and
|
|
127
|
+
// capture the code+state from the callback, then exchange them for the token.
|
|
128
|
+
const cbPromise = waitForCallback(port, state);
|
|
121
129
|
openBrowser(authUrl);
|
|
122
130
|
let cb;
|
|
123
131
|
try {
|
|
124
|
-
cb = await
|
|
132
|
+
cb = await cbPromise;
|
|
125
133
|
}
|
|
126
134
|
catch (e) {
|
|
127
135
|
console.error(" ✗ " + (e?.message ?? String(e)));
|
|
@@ -262,7 +262,7 @@ function replaceSection(claudeMd) {
|
|
|
262
262
|
}
|
|
263
263
|
async function promptYes(question) {
|
|
264
264
|
if (!process.stdin.isTTY)
|
|
265
|
-
return
|
|
265
|
+
return false; // non-interactive + no --yes ⇒ refuse (mirrors uninstall)
|
|
266
266
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
267
267
|
return new Promise((resolve) => {
|
|
268
268
|
rl.question(question, (answer) => {
|
|
@@ -287,13 +287,21 @@ export async function injectInstructions(opts = {}) {
|
|
|
287
287
|
console.log(" · no git repos with GitHub remotes found — skipping CLAUDE.md inject. Pass --root <path> if your code lives somewhere non-conventional.");
|
|
288
288
|
return outcome;
|
|
289
289
|
}
|
|
290
|
-
// First-run consent. Shows the count + an example path so the user
|
|
291
|
-
//
|
|
290
|
+
// First-run consent. Shows the count + an example path so the user knows the
|
|
291
|
+
// blast radius, and names every file we touch (three per repo, created if
|
|
292
|
+
// missing). --yes bypasses; non-interactive without --yes is a safe no-op.
|
|
292
293
|
if (!opts.yes) {
|
|
294
|
+
if (!process.stdin.isTTY) {
|
|
295
|
+
outcome.declined = true;
|
|
296
|
+
console.log(`\n · non-interactive shell — skipping CLAUDE.md/AGENTS.md/GEMINI.md inject.`);
|
|
297
|
+
console.log(` Re-run with \`npx replen inject -y\` to apply without a prompt.`);
|
|
298
|
+
return outcome;
|
|
299
|
+
}
|
|
293
300
|
console.log(`\n Found ${repos.length} git repo(s) with GitHub remotes.`);
|
|
294
|
-
console.log(`
|
|
295
|
-
console.log(`
|
|
296
|
-
console.log(`
|
|
301
|
+
console.log(` Add a "## Replen integration" section to CLAUDE.md, AGENTS.md and GEMINI.md`);
|
|
302
|
+
console.log(` in each (creating any that don't exist) so Claude Code / Codex / Gemini CLI`);
|
|
303
|
+
console.log(` surface today's matches at session start. Idempotent; edit freely above the`);
|
|
304
|
+
console.log(` section. First 3:`);
|
|
297
305
|
for (const r of repos.slice(0, 3))
|
|
298
306
|
console.log(` • ${r}`);
|
|
299
307
|
if (repos.length > 3)
|
package/dist/mcp-setup.js
CHANGED
|
@@ -189,6 +189,15 @@ function setupCodex(token, base) {
|
|
|
189
189
|
const path = CODEX_CONFIG;
|
|
190
190
|
try {
|
|
191
191
|
backupIfExists(path);
|
|
192
|
+
// smol-toml preserves values but not comments, blank lines, or table order.
|
|
193
|
+
// If the user's config carries comments, warn (a .bak was already taken) so
|
|
194
|
+
// the loss isn't silent — the only faithful alternative is hand-editing.
|
|
195
|
+
if (existsSync(path)) {
|
|
196
|
+
const raw = readFileSync(path, "utf8");
|
|
197
|
+
if (/^\s*#/m.test(raw)) {
|
|
198
|
+
console.warn(` ⚠ ${path}: comments/formatting in this TOML aren't preserved when Replen edits it (a .bak was saved next to it).`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
192
201
|
const config = readToml(path);
|
|
193
202
|
const mcpServers = config.mcp_servers ?? {};
|
|
194
203
|
const existed = !!mcpServers[SERVER_NAME];
|
|
@@ -254,7 +263,9 @@ function backupIfExists(path) {
|
|
|
254
263
|
// previous backup. Each run preserves the prior state.
|
|
255
264
|
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
256
265
|
const backup = `${path}.bak.${ts}`;
|
|
257
|
-
|
|
266
|
+
// The backed-up config can carry the ingest token; keep it 0600 like the
|
|
267
|
+
// atomic write above, not the umask default (typically world-readable 0644).
|
|
268
|
+
writeFileSync(backup, readFileSync(path), { mode: 0o600 });
|
|
258
269
|
}
|
|
259
270
|
// ============================================================================
|
|
260
271
|
// Claude SessionStart hook
|
package/dist/sync-projects.js
CHANGED
|
@@ -31,6 +31,18 @@ export async function syncDiscoveredProjects({ token, base, explicitRoots = [],
|
|
|
31
31
|
if (prompted) {
|
|
32
32
|
await persistRoots(result.scannedRoots);
|
|
33
33
|
}
|
|
34
|
+
// The absolute local checkout path carries the OS username; it is only useful
|
|
35
|
+
// to a server running ON this machine (self-host at localhost) for Immersion.
|
|
36
|
+
// A remote/hosted server can't read it, so don't leak it there.
|
|
37
|
+
const isLocalBase = (() => {
|
|
38
|
+
try {
|
|
39
|
+
const h = new URL(base).hostname;
|
|
40
|
+
return h === "localhost" || h === "127.0.0.1" || h === "::1";
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
})();
|
|
34
46
|
const payload = {
|
|
35
47
|
projects: result.projects.map((p) => ({
|
|
36
48
|
slug: p.slug,
|
|
@@ -38,11 +50,9 @@ export async function syncDiscoveredProjects({ token, base, explicitRoots = [],
|
|
|
38
50
|
name: p.name,
|
|
39
51
|
tags: p.tags,
|
|
40
52
|
primaryLanguage: p.primaryLanguage ?? undefined,
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
// code; nothing is read unless the operator enables Immersion.
|
|
45
|
-
localPath: p.localPath,
|
|
53
|
+
// Only sent to a local (self-host) server; omitted for a remote host so
|
|
54
|
+
// the username-bearing absolute path never leaves the machine.
|
|
55
|
+
localPath: isLocalBase ? p.localPath : undefined,
|
|
46
56
|
})),
|
|
47
57
|
};
|
|
48
58
|
let res;
|
package/dist/uninstall.js
CHANGED
|
@@ -345,6 +345,11 @@ function stripTomlServer(path) {
|
|
|
345
345
|
if (Object.keys(servers).length === 0)
|
|
346
346
|
delete next.mcp_servers;
|
|
347
347
|
backupIfExists(path);
|
|
348
|
+
// smol-toml doesn't preserve comments/formatting on re-serialise; warn if the
|
|
349
|
+
// file had comments so their loss on uninstall isn't silent (a .bak is taken).
|
|
350
|
+
if (existsSync(path) && /^\s*#/m.test(readFileSync(path, "utf8"))) {
|
|
351
|
+
console.warn(` ⚠ ${path}: comments/formatting in this TOML aren't preserved (a .bak was saved next to it).`);
|
|
352
|
+
}
|
|
348
353
|
writeTomlAtomic(path, next);
|
|
349
354
|
}
|
|
350
355
|
// ============================================================================
|
|
@@ -56,8 +56,8 @@ Find the user's local repositories and filter to the ones worth onboarding.
|
|
|
56
56
|
- Has a git remote (so Replen can scope by `owner/name`). Note any local-only
|
|
57
57
|
repos and skip them.
|
|
58
58
|
3. **Present the list and get ONE green light.** Show the in-scope repos and ask
|
|
59
|
-
the user to confirm or trim — e.g. *"Found 8 active repos to onboard:
|
|
60
|
-
|
|
59
|
+
the user to confirm or trim — e.g. *"Found 8 active repos to onboard: acme-web,
|
|
60
|
+
acme-api, … — go, or want me to drop any?"* This is the **only** question you
|
|
61
61
|
ask. Once confirmed, run the rest autonomously.
|
|
62
62
|
|
|
63
63
|
## Step 1.5 — Pre-flight: decide the MINIMUM work per repo (do NOT skip this)
|
|
@@ -291,7 +291,7 @@ sanitize titles, no codenames.
|
|
|
291
291
|
|
|
292
292
|
**Also capture architectural PRACTICES as capabilities.** Beyond what the code
|
|
293
293
|
*does*, note the distinctive structural *moves* it makes — these are what let
|
|
294
|
-
Replen suggest a pattern to your OTHER projects ("
|
|
294
|
+
Replen suggest a pattern to your OTHER projects ("Acme made its domain model
|
|
295
295
|
data-driven; this data-heavy project should consider it"). Include a capability
|
|
296
296
|
for any deliberate practice you find: a **data-driven domain ontology** (entity
|
|
297
297
|
types/relationships stored as DATA/rows, not hardcoded — configurable without a
|
|
@@ -353,7 +353,7 @@ typed properties, computed properties as rows", never the entity names).
|
|
|
353
353
|
it (e.g. `{tag: "computer vision", …, paths: ["src/cv/transformations.py"]}`).
|
|
354
354
|
Paths only, never code. You just read these files; recording WHERE each
|
|
355
355
|
capability lives lets Replen point another of the user's projects straight
|
|
356
|
-
at the implementation worth porting ("
|
|
356
|
+
at the implementation worth porting ("acme solved this — see src/cv/…").
|
|
357
357
|
**If you grounded from a vault, also pass the `concepts` array from 2d** —
|
|
358
358
|
concept titles + their `[[wikilinks]]` only (never file/symbol notes, never
|
|
359
359
|
bodies). Replen turns these into graph nodes + your-own-graph leaps. Omit it
|
|
@@ -369,7 +369,7 @@ typed properties, computed properties as rows", never the entity names).
|
|
|
369
369
|
|
|
370
370
|
## Step 3 — Group multi-repo products
|
|
371
371
|
|
|
372
|
-
If several repos are one product (e.g. `
|
|
372
|
+
If several repos are one product (e.g. `acme-web`/`acme-api`/`acme-cv`), group
|
|
373
373
|
them with `replen_set_product` so Replen unions the whole product's capabilities
|
|
374
374
|
when the user is in any one of them.
|
|
375
375
|
|
|
@@ -383,7 +383,7 @@ when the user is in any one of them.
|
|
|
383
383
|
```
|
|
384
384
|
Replen onboarding done — 8 repos grounded.
|
|
385
385
|
· docs: 2 created, 1 augmented, 5 already good (untouched)
|
|
386
|
-
· capabilities + report set for all 8;
|
|
386
|
+
· capabilities + report set for all 8; acme-* grouped as one product
|
|
387
387
|
· doc changes are on branch `replen/onboarding-docs` for your review
|
|
388
388
|
Replen will now surface genuinely relevant tools for these repos.
|
|
389
389
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "replen",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.5",
|
|
4
4
|
"description": "Make your AI coding tools smarter. One command, no API keys, free. Replen watches what your projects actually do and surfaces a few things worth bringing in each month. Use one as is, port a piece of another, cherry pick an idea, or build it clean room. The match happens inside your AI tool's session. A few actionable matches a month, by design.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"build": "tsc && chmod +x dist/index.js",
|
|
11
11
|
"dev": "tsx src/index.ts",
|
|
12
12
|
"check:skill-sync": "node scripts/check-skill-sync.mjs",
|
|
13
|
-
"prepublishOnly": "npm run build && npm run check:skill-sync"
|
|
13
|
+
"prepublishOnly": "npm run build && npm run check:skill-sync && node ../scripts/check-no-private-slugs.mjs"
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"dist",
|