replen 1.2.1 → 1.3.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/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/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
+ }
@@ -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
@@ -162,6 +175,15 @@ code only to VERIFY (the vault may be stale); don't re-derive what it already
162
175
  holds. Privacy is unchanged: only capabilities/descriptors/paths/tags leave
163
176
  the machine, exactly as with a code-read — never vault content or code.
164
177
 
178
+ **Use the note→note `[[wikilinks]]` BETWEEN concept/entity notes as capability-
179
+ BOUNDARY evidence** (today you only follow note→FILE links, for `paths`). A tight
180
+ same-as cluster of concepts → emit ONE capability tag (don't split a thing the
181
+ user models as one). A concept fanning out to distinct linked clusters → split
182
+ into the concrete techniques (this is the 2d "break broad capabilities" move,
183
+ now backed by the user's own structure). A typed/deliberate cross-link between
184
+ two distinct concepts → keep them as two RELATED tags. Then, if the vault gave
185
+ you this structure, assemble the optional `concepts` array described in 2d.
186
+
165
187
  ### 2a. Read the code (not just the README)
166
188
 
167
189
  Read enough source to actually understand the project: entry points, the core
@@ -255,6 +277,18 @@ strings. Each is `{tag, descriptor, modality}`:
255
277
  Break broad capabilities into the concrete techniques the code uses. Be
256
278
  specific — `cloudflare bypass`/`proxy rotation`, not just `web scraping`.
257
279
 
280
+ **If you grounded from a vault (2a-pre), also assemble a `concepts` array** so
281
+ Replen can traverse the user's OWN graph for cross-repo leaps. For each
282
+ concept/entity note — **NEVER a file/symbol/function/class/module note** — emit
283
+ `{title, grounds: [the capability tags this note grounded], links: [{to: <other
284
+ concept title>, rel}]}` where `rel` is one of `relates | refines | depends |
285
+ same-as | contrast`. Pass it to `replen_set_capabilities` in 2e (it's optional —
286
+ omit it entirely if there's no vault). Hard rules (already true for everything
287
+ else, restated because this is new structure leaving the machine): lift only
288
+ concept/capability-LEVEL notes; note BODIES and code never leave; a note→file
289
+ link stays as a capability `path`, never a concept link; respect the cover —
290
+ sanitize titles, no codenames.
291
+
258
292
  **Also capture architectural PRACTICES as capabilities.** Beyond what the code
259
293
  *does*, note the distinctive structural *moves* it makes — these are what let
260
294
  Replen suggest a pattern to your OTHER projects ("Aegis made its domain model
@@ -320,6 +354,10 @@ typed properties, computed properties as rows", never the entity names).
320
354
  Paths only, never code. You just read these files; recording WHERE each
321
355
  capability lives lets Replen point another of the user's projects straight
322
356
  at the implementation worth porting ("aegis solved this — see src/cv/…").
357
+ **If you grounded from a vault, also pass the `concepts` array from 2d** —
358
+ concept titles + their `[[wikilinks]]` only (never file/symbol notes, never
359
+ bodies). Replen turns these into graph nodes + your-own-graph leaps. Omit it
360
+ when there's no vault.
323
361
  4. **Report pinned versions** with `replen_set_versions` — the resolved DIRECT
324
362
  dependency versions from the lockfile (package-lock.json / poetry.lock /
325
363
  uv.lock / Cargo.lock), plus runtimes under canonical keys (`node` from
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replen",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
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
- "prepublishOnly": "npm run build"
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",