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.
- package/dist/auto-register.js +3 -0
- package/dist/immerse.js +113 -0
- package/dist/index.js +9 -0
- package/dist/sync-projects.js +6 -1
- package/extras/skills/replen/SKILL.md +22 -0
- package/package.json +1 -1
package/dist/auto-register.js
CHANGED
|
@@ -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,
|
package/dist/immerse.js
ADDED
|
@@ -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")
|
package/dist/sync-projects.js
CHANGED
|
@@ -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
|
+
"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": {
|