replen 1.3.1 → 1.5.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.
@@ -148,6 +148,9 @@ async function postProjectsBulk(base, token, projects) {
148
148
  name: p.name,
149
149
  tags: p.tags,
150
150
  primaryLanguage: p.primaryLanguage ?? undefined,
151
+ // Absolute local checkout path — see sync-projects.ts. Inert unless
152
+ // the server is a self-host install with Immersion enabled.
153
+ localPath: p.localPath,
151
154
  })),
152
155
  }),
153
156
  signal: ctrl.signal,
@@ -0,0 +1,113 @@
1
+ // `npx replen immerse` — hosted Immersion sender (M2).
2
+ //
3
+ // On the hosted service Replen can't read your disk, so to ground matching on
4
+ // your actual code you opt in and this command does the transmit: for each
5
+ // tracked repo it asks the server which grounded files to send (the manifest),
6
+ // reads exactly those files locally, and POSTs them to the ingest endpoint —
7
+ // which embeds them and keeps ONLY the vectors, discarding the source. Nothing
8
+ // is read or sent for a repo whose tier is off.
9
+ //
10
+ // npx replen immerse on opt in (vectors-only) + send now
11
+ // npx replen immerse send for an already-opted-in account
12
+ // npx replen immerse status show the account default
13
+ // npx replen immerse off opt out
14
+ //
15
+ // Self-host installs don't need this — they default on via REPLEN_SELF_HOST and
16
+ // the pipeline reads local disk directly.
17
+ import { readFileSync, statSync } from "node:fs";
18
+ import { join } from "node:path";
19
+ import { loadConfigOrExit, apiGet, apiPost } from "./api.js";
20
+ import { resolveAndWalk } from "./sync-projects.js";
21
+ const MAX_FILE_BYTES = 1_000_000; // mirror the server per-file cap
22
+ export async function runImmerse(argv) {
23
+ const sub = (argv[0] ?? "").toLowerCase();
24
+ const cfg = await loadConfigOrExit();
25
+ if (sub === "status") {
26
+ const { tier } = await apiGet(cfg, "/api/settings/immersion");
27
+ console.log(`Immersion (account default): ${tier}`);
28
+ if (tier === "off")
29
+ console.log("Run `npx replen immerse on` to enable it. Your code is embedded, then discarded — only vectors are kept.");
30
+ return;
31
+ }
32
+ if (sub === "off") {
33
+ await apiPost(cfg, "/api/settings/immersion", { tier: "off" });
34
+ console.log("Immersion turned OFF for your account. Existing code vectors are dropped on the next run.");
35
+ return;
36
+ }
37
+ if (sub === "on") {
38
+ await apiPost(cfg, "/api/settings/immersion", { tier: "embeddings" });
39
+ console.log("Immersion ON (vectors-only). Your code is embedded server-side and the source discarded — only the vectors persist.\n");
40
+ await send(cfg);
41
+ return;
42
+ }
43
+ // No subcommand: send for an already-opted-in account.
44
+ const { tier } = await apiGet(cfg, "/api/settings/immersion");
45
+ if (tier === "off") {
46
+ console.log("Immersion is off for your account. Run `npx replen immerse on` to enable it (vectors-only — your code is embedded, then discarded).");
47
+ return;
48
+ }
49
+ await send(cfg);
50
+ }
51
+ // Discover local repos, then for each tracked + opted-in repo: fetch the
52
+ // manifest, read the listed files, and ingest them.
53
+ async function send(cfg) {
54
+ console.log("Scanning local repos…");
55
+ const { result } = await resolveAndWalk([]);
56
+ const repos = result.projects.filter((p) => p.githubFullName && p.localPath);
57
+ if (repos.length === 0) {
58
+ console.log(" · No local git repos with GitHub remotes found. Nothing to send.");
59
+ return;
60
+ }
61
+ let grounded = 0, totalFiles = 0, totalChunks = 0, unchanged = 0;
62
+ for (const repo of repos) {
63
+ let manifest;
64
+ try {
65
+ manifest = await apiPost(cfg, "/api/immersion/manifest", { githubFullName: repo.githubFullName });
66
+ }
67
+ catch {
68
+ continue; // not tracked on this account (404) / transient — skip quietly
69
+ }
70
+ if (manifest.tier === "off" || !Array.isArray(manifest.paths) || manifest.paths.length === 0)
71
+ continue;
72
+ // Read exactly the grounded files the server asked for (size-capped).
73
+ const files = [];
74
+ for (const rel of manifest.paths) {
75
+ try {
76
+ const abs = join(repo.localPath, rel);
77
+ const st = statSync(abs);
78
+ if (!st.isFile() || st.size > MAX_FILE_BYTES)
79
+ continue;
80
+ const content = readFileSync(abs, "utf8");
81
+ if (content.trim())
82
+ files.push({ rel, content });
83
+ }
84
+ catch {
85
+ /* file gone / unreadable — skip */
86
+ }
87
+ }
88
+ if (files.length === 0)
89
+ continue;
90
+ let res;
91
+ try {
92
+ res = await apiPost(cfg, "/api/immersion/ingest", { githubFullName: repo.githubFullName, files });
93
+ }
94
+ catch (e) {
95
+ console.warn(` ✗ ${repo.githubFullName}: ${e.message}`);
96
+ continue;
97
+ }
98
+ if (res.unchanged) {
99
+ unchanged++;
100
+ continue;
101
+ }
102
+ grounded++;
103
+ totalFiles += res.filesEmbedded ?? 0;
104
+ totalChunks += res.chunksEmbedded ?? 0;
105
+ console.log(` ✓ ${repo.githubFullName}: ${res.chunksEmbedded ?? 0} chunk(s) from ${res.filesEmbedded ?? 0} file(s)`);
106
+ }
107
+ const parts = [];
108
+ if (grounded > 0)
109
+ parts.push(`grounded ${grounded} repo(s) — ${totalChunks} chunk(s) from ${totalFiles} file(s)`);
110
+ if (unchanged > 0)
111
+ parts.push(`${unchanged} unchanged`);
112
+ console.log(`\nImmersion: ${parts.length ? parts.join(", ") : "nothing to update"}.`);
113
+ }
package/dist/index.js CHANGED
@@ -30,6 +30,11 @@ Usage:
30
30
  npx replen vault me/drone=~/graphs/drone
31
31
  \`npx replen vault --list\` shows configured vaults.
32
32
  (--vault PATH also works on \`npx replen\` + sync.)
33
+ npx replen immerse [on|off] Ground matching on your ACTUAL code, not just
34
+ [status] its description (hosted opt-in; self-host has it
35
+ on by default). \`on\` opts in and sends; bare
36
+ sends for an opted-in account. Vectors-only: your
37
+ code is embedded server-side, then discarded.
33
38
  npx replen atlas Write your knowledge graph as an owned,
34
39
  Obsidian-compatible markdown vault to
35
40
  ~/.replen/atlas/ (projects, capabilities,
@@ -190,6 +195,10 @@ async function main() {
190
195
  await syncDiscoveredProjects({ token: cfg.token, base: cfg.base, explicitRoots });
191
196
  return;
192
197
  }
198
+ if (cmd === "immerse") {
199
+ const { runImmerse } = await import("./immerse.js");
200
+ return runImmerse(argv.slice(1));
201
+ }
193
202
  if (cmd === "run")
194
203
  return runRun(argv);
195
204
  if (cmd === "progress")
@@ -38,6 +38,11 @@ export async function syncDiscoveredProjects({ token, base, explicitRoots = [],
38
38
  name: p.name,
39
39
  tags: p.tags,
40
40
  primaryLanguage: p.primaryLanguage ?? undefined,
41
+ // Absolute local checkout path. Used by Immersion on a self-host install
42
+ // (server == this machine) to ground on the actual source; inert on the
43
+ // hosted server, which can't read it. Always sent — it's identity, not
44
+ // code; nothing is read unless the operator enables Immersion.
45
+ localPath: p.localPath,
41
46
  })),
42
47
  };
43
48
  let res;
@@ -85,7 +90,7 @@ export async function syncDiscoveredProjects({ token, base, explicitRoots = [],
85
90
  * Returns the walked result, the strategy that supplied the roots,
86
91
  * and a flag indicating whether the interactive prompt was used.
87
92
  */
88
- async function resolveAndWalk(explicitRoots) {
93
+ export async function resolveAndWalk(explicitRoots) {
89
94
  // Strategies that take precedence and short-circuit on hit.
90
95
  const ordered = [
91
96
  { source: "flag", roots: () => rootsFromFlag(explicitRoots) },
@@ -305,6 +305,28 @@ Continuing with unfiltered for now."
305
305
  - The session-start hook already surfaced matches in the opening
306
306
  context and the user hasn't asked for more. Don't double-deliver.
307
307
 
308
+ ## Immersion — grounding on the user's actual code
309
+
310
+ By default Replen matches against each repo's *description* (docs, tags,
311
+ extracted capabilities). **Immersion** is an opt-in mode that also grounds
312
+ matching on the *actual source* behind each capability — embedded into vectors;
313
+ the raw code is discarded after embedding, never retained. Self-host installs
314
+ have it on by default; hosted is opt-in (`off` until the user enables it).
315
+
316
+ When to act on it (don't volunteer otherwise):
317
+
318
+ - The user asks to "use my actual code", "ground on the real implementation",
319
+ match more precisely, or asks why a match felt shallow → tell them Immersion
320
+ exists and, if they want it, run `npx replen immerse on` (opts in + sends).
321
+ - Immersion is already on and the user has made material code changes this
322
+ session and wants the next matches to reflect them → run `npx replen immerse`
323
+ to refresh (cheap + a no-op when nothing changed — a content hash gates it).
324
+
325
+ Keep it honest about the trust step: on hosted, enabling Immersion sends the
326
+ grounded files to Replen to be embedded — same posture as the AI agent they're
327
+ already using (transits, embedded, not kept), and strictly less exposure (only
328
+ a vector persists). Never run `immerse on` for them without an explicit yes.
329
+
308
330
  ## Voice guide
309
331
 
310
332
  - **Concrete.** "Replaces `lib/foo.ts:42-180`" beats "could simplify
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replen",
3
- "version": "1.3.1",
3
+ "version": "1.5.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": {