replen 1.2.0 → 1.2.2
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/index.js +55 -0
- package/dist/init.js +13 -6
- package/dist/inject-instruction.js +15 -2
- package/dist/vaults.js +102 -0
- package/extras/skills/replen/SKILL.md +16 -4
- package/extras/skills/replen-onboard/SKILL.md +13 -0
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -21,6 +21,15 @@ Usage:
|
|
|
21
21
|
[--root PATH ...] and register them with Replen. Run after
|
|
22
22
|
cloning a new repo, or pass --root to point
|
|
23
23
|
at a non-conventional layout.
|
|
24
|
+
npx replen vault <spec>... Point Replen at a knowledge-graph vault
|
|
25
|
+
(Obsidian / Graphify / ADRs) that lives OUTSIDE
|
|
26
|
+
a repo, so onboarding grounds from it. Repeatable.
|
|
27
|
+
A bare path is a global vault (covers all repos);
|
|
28
|
+
owner/name=path scopes one to a repo. e.g.
|
|
29
|
+
npx replen vault ~/ObsidianVault
|
|
30
|
+
npx replen vault me/drone=~/graphs/drone
|
|
31
|
+
\`npx replen vault --list\` shows configured vaults.
|
|
32
|
+
(--vault PATH also works on \`npx replen\` + sync.)
|
|
24
33
|
npx replen atlas Write your knowledge graph as an owned,
|
|
25
34
|
Obsidian-compatible markdown vault to
|
|
26
35
|
~/.replen/atlas/ (projects, capabilities,
|
|
@@ -134,6 +143,38 @@ async function main() {
|
|
|
134
143
|
console.log(summary);
|
|
135
144
|
return;
|
|
136
145
|
}
|
|
146
|
+
if (cmd === "vault") {
|
|
147
|
+
const cfg = await readConfig();
|
|
148
|
+
if (!cfg) {
|
|
149
|
+
console.error("Not signed in. Run `npx replen` first.");
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
const { collectVaultFlags, persistVaults, summariseVaults, hasVaultFlags } = await import("./vaults.js");
|
|
153
|
+
if (argv.includes("--list")) {
|
|
154
|
+
const v = cfg.vaults;
|
|
155
|
+
const empty = !v || ((v.global?.length ?? 0) === 0 && Object.keys(v.byRepo ?? {}).length === 0);
|
|
156
|
+
console.log(empty
|
|
157
|
+
? "No vaults configured.\n Add one: npx replen vault ~/ObsidianVault\n npx replen vault owner/name=~/graphs/thing"
|
|
158
|
+
: "Configured knowledge-graph vaults:\n" + summariseVaults(v));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const specs = argv.slice(1).filter((a) => !a.startsWith("-"));
|
|
162
|
+
if (specs.length === 0) {
|
|
163
|
+
console.error("Usage: npx replen vault <path | owner/name=path> ... (or --list)");
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
const parsed = collectVaultFlags(specs.flatMap((s) => ["--vault", s]));
|
|
167
|
+
if (!hasVaultFlags(parsed)) {
|
|
168
|
+
console.error("No valid vault paths given (each must be an existing directory).");
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
const vaults = await persistVaults(parsed);
|
|
172
|
+
if (vaults) {
|
|
173
|
+
console.log(summariseVaults(vaults));
|
|
174
|
+
console.log("\n Re-run /replen-onboard in Claude Code so the agent re-grounds from the vault.");
|
|
175
|
+
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
137
178
|
if (cmd === "sync-projects" || cmd === "sync") {
|
|
138
179
|
const cfg = await readConfig();
|
|
139
180
|
if (!cfg) {
|
|
@@ -141,6 +182,10 @@ async function main() {
|
|
|
141
182
|
process.exit(1);
|
|
142
183
|
}
|
|
143
184
|
const explicitRoots = collectRootFlags(argv);
|
|
185
|
+
const { collectVaultFlags, persistVaults, summariseVaults } = await import("./vaults.js");
|
|
186
|
+
const savedVaults = await persistVaults(collectVaultFlags(argv));
|
|
187
|
+
if (savedVaults)
|
|
188
|
+
console.log(summariseVaults(savedVaults));
|
|
144
189
|
const { syncDiscoveredProjects } = await import("./sync-projects.js");
|
|
145
190
|
await syncDiscoveredProjects({ token: cfg.token, base: cfg.base, explicitRoots });
|
|
146
191
|
return;
|
|
@@ -166,13 +211,23 @@ async function main() {
|
|
|
166
211
|
if (cmd === undefined) {
|
|
167
212
|
// Default: if already signed in, just rerun mcp setup. Otherwise, full flow.
|
|
168
213
|
const cfg = await readConfig();
|
|
214
|
+
const { collectVaultFlags, persistVaults, summariseVaults } = await import("./vaults.js");
|
|
215
|
+
const parsedVaults = collectVaultFlags(argv);
|
|
169
216
|
if (cfg) {
|
|
170
217
|
console.log(`Already signed in to ${cfg.base}. Re-wiring MCP config…`);
|
|
171
218
|
await setupMcp(cfg.token, cfg.base);
|
|
219
|
+
const v = await persistVaults(parsedVaults);
|
|
220
|
+
if (v)
|
|
221
|
+
console.log(summariseVaults(v));
|
|
172
222
|
console.log(`\nDone. Run \`npx replen status\` to inspect.`);
|
|
173
223
|
return;
|
|
174
224
|
}
|
|
175
225
|
await runInit();
|
|
226
|
+
// Config now exists — persist any --vault passed to first-run setup so the
|
|
227
|
+
// onboarding agent (run next in Claude Code) grounds from it.
|
|
228
|
+
const v = await persistVaults(parsedVaults);
|
|
229
|
+
if (v)
|
|
230
|
+
console.log(summariseVaults(v));
|
|
176
231
|
return;
|
|
177
232
|
}
|
|
178
233
|
console.error(`Unknown command: ${cmd}\n`);
|
package/dist/init.js
CHANGED
|
@@ -166,13 +166,20 @@ export async function runInit() {
|
|
|
166
166
|
const { runFirstIngest } = await import("./first-ingest.js");
|
|
167
167
|
await runFirstIngest({ token: exchange.token, base: exchange.base, savedAt: "" });
|
|
168
168
|
console.log("");
|
|
169
|
-
console.log(" All set. Restart Claude Code
|
|
170
|
-
console.log(" /replen → triage today's candidates against this repo,");
|
|
171
|
-
console.log(" in-session, using your subscription tokens");
|
|
172
|
-
console.log(" (no LLM API keys needed — the agent does the reasoning)");
|
|
169
|
+
console.log(" All set. Restart Claude Code, then do these two — IN THIS ORDER:");
|
|
173
170
|
console.log("");
|
|
174
|
-
console.log("
|
|
175
|
-
console.log("
|
|
171
|
+
console.log(" 1. /replen-onboard ONE-TIME, FIRST. Reads your repos and builds a");
|
|
172
|
+
console.log(" tailored profile so matches are relevant, not");
|
|
173
|
+
console.log(" generic. Runs in the background — kick it off,");
|
|
174
|
+
console.log(" then just keep working.");
|
|
175
|
+
console.log("");
|
|
176
|
+
console.log(" 2. /replen ANYTIME after that. Triages the candidates for");
|
|
177
|
+
console.log(" the current repo, in-session, on your own");
|
|
178
|
+
console.log(" subscription tokens — no LLM API key needed.");
|
|
179
|
+
console.log("");
|
|
180
|
+
console.log(" (Onboarding is once per project. After that, it's just /replen.)");
|
|
181
|
+
console.log("");
|
|
182
|
+
console.log(" Other MCP hosts (Codex / Cursor / Aider): \"use replen_match\" — same tool.");
|
|
176
183
|
console.log("");
|
|
177
184
|
console.log(` Dashboard: ${exchange.base}`);
|
|
178
185
|
console.log("");
|
|
@@ -33,12 +33,25 @@
|
|
|
33
33
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
34
34
|
import { join, basename } from "node:path";
|
|
35
35
|
import { createInterface } from "node:readline";
|
|
36
|
-
export const INTEGRATION_VERSION = "
|
|
36
|
+
export const INTEGRATION_VERSION = "10";
|
|
37
37
|
const SECTION_HEADER = "## Replen integration";
|
|
38
38
|
const MARKER_RE = /<!--\s*replen-integration:\s*v(\d+)\s*-->/;
|
|
39
39
|
const SECTION_BODY = `<!-- replen-integration: v${INTEGRATION_VERSION} -->
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
**FIRST, on your first response in a repo: check onboarding.** Call
|
|
42
|
+
\`replen_onboard_state\` once. If it reports this repo (or the user's
|
|
43
|
+
portfolio) is NOT yet onboarded/grounded, do NOT run the footnote flow
|
|
44
|
+
this turn. Answer the user's message normally, then offer this VERBATIM
|
|
45
|
+
as a new paragraph at the end:
|
|
46
|
+
|
|
47
|
+
> Replen isn't set up for your repos yet — want me to do that now? Replen builds a profile so matches are relevant. Onboarding runs in the background, ~a few minutes.
|
|
48
|
+
|
|
49
|
+
If they accept, invoke the \`replen-onboard\` skill. If they decline, carry
|
|
50
|
+
on and do NOT re-offer for the rest of this session. Once \`replen_onboard_state\`
|
|
51
|
+
shows the repo is onboarded, SKIP this check entirely and use the footnote
|
|
52
|
+
flow below.
|
|
53
|
+
|
|
54
|
+
Once onboarded, on your **very first response** of each session:
|
|
42
55
|
|
|
43
56
|
1. **Skip the whole flow ONLY if** the user's first message is a
|
|
44
57
|
pure trivial greeting with no content ("hi", "hey", "yo",
|
package/dist/vaults.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// `--vault` handling: let users point Replen at knowledge-graph vaults that
|
|
2
|
+
// live OUTSIDE a repo (a central Obsidian vault, a Graphify graph kept
|
|
3
|
+
// elsewhere, an ADR folder). In-repo vaults are auto-detected by the onboard
|
|
4
|
+
// skill; this is the escape hatch for everything that isn't sitting in the repo.
|
|
5
|
+
//
|
|
6
|
+
// Two shapes, both via `--vault`:
|
|
7
|
+
// --vault ~/ObsidianVault → GLOBAL: may cover many/all repos.
|
|
8
|
+
// --vault owner/name=~/graphs/thing → scoped to one repo (GitHub owner/name).
|
|
9
|
+
//
|
|
10
|
+
// Parsed values are validated (must be an existing directory), tilde-expanded to
|
|
11
|
+
// absolute paths, and merged into ~/.replen/config.json under `vaults`. The
|
|
12
|
+
// onboarding agent reads that config and consults these paths as grounding
|
|
13
|
+
// sources alongside the in-repo locations.
|
|
14
|
+
import { statSync } from "node:fs";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { join, resolve } from "node:path";
|
|
17
|
+
import { readConfig, writeConfig } from "./config.js";
|
|
18
|
+
function expandTilde(p) {
|
|
19
|
+
if (p === "~" || p.startsWith("~/"))
|
|
20
|
+
return p === "~" ? homedir() : join(homedir(), p.slice(2));
|
|
21
|
+
return resolve(p);
|
|
22
|
+
}
|
|
23
|
+
function isDir(p) {
|
|
24
|
+
try {
|
|
25
|
+
return statSync(p).isDirectory();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Looks like a GitHub "owner/name" (no spaces, exactly one slash, no leading ./~). */
|
|
32
|
+
function isRepoKey(s) {
|
|
33
|
+
return /^[^\s/]+\/[^\s/]+$/.test(s);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Pull every `--vault <val>` / `--vault=<val>` out of argv. A value of the form
|
|
37
|
+
* `owner/name=path` is repo-scoped; anything else is a global vault path.
|
|
38
|
+
* Invalid (non-directory) paths are reported and skipped, not fatal.
|
|
39
|
+
*/
|
|
40
|
+
export function collectVaultFlags(argv) {
|
|
41
|
+
const raw = [];
|
|
42
|
+
for (let i = 0; i < argv.length; i++) {
|
|
43
|
+
const a = argv[i];
|
|
44
|
+
if (a === "--vault" && argv[i + 1])
|
|
45
|
+
raw.push(argv[++i]);
|
|
46
|
+
else if (a.startsWith("--vault="))
|
|
47
|
+
raw.push(a.slice("--vault=".length));
|
|
48
|
+
}
|
|
49
|
+
const out = { global: [], byRepo: {} };
|
|
50
|
+
for (const val of raw) {
|
|
51
|
+
// Repo-scoped only when the part BEFORE the first "=" is a repo key — so a
|
|
52
|
+
// Windows-y or "="-containing path on the global side isn't misparsed.
|
|
53
|
+
const eq = val.indexOf("=");
|
|
54
|
+
if (eq > 0 && isRepoKey(val.slice(0, eq))) {
|
|
55
|
+
const repo = val.slice(0, eq);
|
|
56
|
+
const path = expandTilde(val.slice(eq + 1));
|
|
57
|
+
if (!isDir(path)) {
|
|
58
|
+
console.warn(` · --vault ${repo}: not a directory, skipping: ${path}`);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
out.byRepo[repo] = path;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
const path = expandTilde(val);
|
|
65
|
+
if (!isDir(path)) {
|
|
66
|
+
console.warn(` · --vault: not a directory, skipping: ${path}`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
out.global.push(path);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
export function hasVaultFlags(parsed) {
|
|
75
|
+
return parsed.global.length > 0 || Object.keys(parsed.byRepo).length > 0;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Merge newly-parsed vaults into the saved config (union global paths, overwrite
|
|
79
|
+
* per-repo entries). Idempotent. No-op if nothing was passed. Returns the
|
|
80
|
+
* resulting vaults block for an immediate confirmation message.
|
|
81
|
+
*/
|
|
82
|
+
export async function persistVaults(parsed) {
|
|
83
|
+
if (!hasVaultFlags(parsed))
|
|
84
|
+
return null;
|
|
85
|
+
const cfg = await readConfig();
|
|
86
|
+
if (!cfg)
|
|
87
|
+
return null;
|
|
88
|
+
const existing = cfg.vaults ?? {};
|
|
89
|
+
const global = Array.from(new Set([...(existing.global ?? []), ...parsed.global]));
|
|
90
|
+
const byRepo = { ...(existing.byRepo ?? {}), ...parsed.byRepo };
|
|
91
|
+
const vaults = { global, byRepo };
|
|
92
|
+
await writeConfig({ ...cfg, vaults });
|
|
93
|
+
return vaults;
|
|
94
|
+
}
|
|
95
|
+
export function summariseVaults(vaults) {
|
|
96
|
+
const lines = [];
|
|
97
|
+
for (const g of vaults.global ?? [])
|
|
98
|
+
lines.push(` ✓ vault (all repos): ${g}`);
|
|
99
|
+
for (const [repo, p] of Object.entries(vaults.byRepo ?? {}))
|
|
100
|
+
lines.push(` ✓ vault for ${repo}: ${p}`);
|
|
101
|
+
return lines.join("\n");
|
|
102
|
+
}
|
|
@@ -40,7 +40,10 @@ Parse the JSON response. Note:
|
|
|
40
40
|
|
|
41
41
|
- `filterMode` — `tags`, `zero-knowledge`, or `fingerprint`
|
|
42
42
|
- `scopedTo` — confirms the project context the user has open
|
|
43
|
-
- `candidates[]` — the actual list to triage
|
|
43
|
+
- `candidates[]` — the actual list to triage. Each carries `solid: true|false` —
|
|
44
|
+
the ones Replen counts as genuinely worth your time (clear domain-fit + posture +
|
|
45
|
+
not-already-covered, or a dependency-maintenance match). The footnote's count is
|
|
46
|
+
the number of `solid` ones; the rest are "worth a glance" laterals.
|
|
44
47
|
|
|
45
48
|
If `candidates.length === 0`, tell the user "No new candidates today for
|
|
46
49
|
`<owner/name>`. Calm-cadence working as designed — 1-3 actionable
|
|
@@ -50,8 +53,11 @@ If 1+ candidates, continue.
|
|
|
50
53
|
|
|
51
54
|
### Step 3 — Per-candidate analysis
|
|
52
55
|
|
|
53
|
-
**
|
|
54
|
-
by `whyShortlisted` strength + stars + recency
|
|
56
|
+
**Triage the `solid` candidates first** (cap at 5). If there are more than 5
|
|
57
|
+
solid, take the top 5 by `whyShortlisted` strength + stars + recency. The
|
|
58
|
+
non-`solid` "worth a glance" entries are optional — skim them only if the solid
|
|
59
|
+
set is thin or the user asks. A low-domain-fit lateral there can still be a real
|
|
60
|
+
port/cherry-pick, so don't dismiss them blindly, but they're not the headline.
|
|
55
61
|
|
|
56
62
|
For each candidate, do this loop:
|
|
57
63
|
|
|
@@ -210,9 +216,15 @@ For tech-news-site:
|
|
|
210
216
|
|
|
211
217
|
(Triaged 9 candidates; surfacing the 2 worth acting on + 1 idea worth keeping.)
|
|
212
218
|
|
|
213
|
-
What would you like to do with each?
|
|
219
|
+
What would you like to do with each? — star (save it for later; won't
|
|
220
|
+
re-surface), hide (dismiss; never show again), or handoff (open a pull
|
|
221
|
+
request against your repo for it). Or skip and decide later.
|
|
214
222
|
```
|
|
215
223
|
|
|
224
|
+
Always present the options WITH that one-line gloss the first time in a
|
|
225
|
+
session — never the bare "(star / hide / handoff)". A new user doesn't know
|
|
226
|
+
what they mean, and the calm-cadence promise dies if the user has to ask.
|
|
227
|
+
|
|
216
228
|
The one-line "(Triaged N…)" footer is the *only* acknowledgement that skips
|
|
217
229
|
happened — honest, but it doesn't parade them.
|
|
218
230
|
|
|
@@ -155,6 +155,19 @@ grounding source (faster AND richer than a cold code-read):
|
|
|
155
155
|
- **ADRs** (`docs/adr/*.md`, `doc/decisions/`) — architecture decisions are
|
|
156
156
|
high-grade descriptor material and often name the load-bearing files.
|
|
157
157
|
|
|
158
|
+
**Also check vaults the user pointed us at OUT of the repo.** Read
|
|
159
|
+
`~/.replen/config.json`; if it has a `vaults` block, consult those paths too —
|
|
160
|
+
`vaults.byRepo["<owner/name>"]` for this repo specifically, and every
|
|
161
|
+
`vaults.global[]` path (a central Obsidian vault that covers many repos). These
|
|
162
|
+
are first-class grounding sources, same treatment as an in-repo vault. This is
|
|
163
|
+
how a user feeds a central vault that auto-detection can't see.
|
|
164
|
+
|
|
165
|
+
If you find NO knowledge graph — not in the repo and none configured — and the
|
|
166
|
+
session is interactive, ask once: "Do you keep design notes or an Obsidian /
|
|
167
|
+
Graphify vault for this project? If so, point me at it with
|
|
168
|
+
`npx replen vault <path>` (or `npx replen vault <owner/name>=<path>`) and re-run
|
|
169
|
+
/replen-onboard — I'll ground from it." Then fall back to the code-read.
|
|
170
|
+
|
|
158
171
|
If you ground from one of these, START the report (2c) with one line:
|
|
159
172
|
`Grounding source: Graphify vault at <path>` (or `Obsidian vault…` / `ADRs…`)
|
|
160
173
|
— it renders into the user's Atlas tiles, linking the two tools. Then skim the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "replen",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
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": {
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"scripts": {
|
|
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
14
|
},
|
|
14
15
|
"files": [
|
|
15
16
|
"dist",
|