greprag 5.80.0 → 5.82.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/capture-manifest.js +2 -1
- package/dist/codex-fast-hook.js +6 -0
- package/dist/codex-steering.js +1 -1
- package/dist/commands/app-model.js +0 -1
- package/dist/commands/arm-reminder.js +9 -7
- package/dist/commands/collision-check.js +7 -6
- package/dist/commands/corpus/client.js +13 -3
- package/dist/commands/delivery-reminder.js +35 -14
- package/dist/commands/deploy-gate.js +55 -0
- package/dist/commands/deploy-lock.js +100 -0
- package/dist/commands/deploy-record.js +145 -0
- package/dist/commands/deploy-verify.js +111 -0
- package/dist/commands/inbox-primer-reminder.js +5 -5
- package/dist/commands/inbox-watch.js +2 -4
- package/dist/commands/init.js +82 -0
- package/dist/commands/load.js +40 -0
- package/dist/commands/loadout-reminder.js +1 -1
- package/dist/commands/merge-guard.js +419 -0
- package/dist/commands/merge-lock.js +176 -0
- package/dist/commands/parity-reminder.js +53 -0
- package/dist/commands/persona-reminder.js +11 -0
- package/dist/commands/persona.js +50 -0
- package/dist/commands/procedure.js +77 -6
- package/dist/commands/reminder-registry.js +21 -5
- package/dist/commands/repodoc.js +433 -0
- package/dist/commands/search.js +149 -0
- package/dist/commands/skillgain.js +33 -25
- package/dist/delivery-lifecycle.js +16 -1
- package/dist/deploy-gate.js +355 -0
- package/dist/deploy-locks.js +339 -0
- package/dist/deploy-verify.js +209 -0
- package/dist/env-redaction.js +157 -0
- package/dist/harness-limits.js +17 -0
- package/dist/hook-runtime.js +11 -1
- package/dist/hook.js +170 -88
- package/dist/index.js +593 -567
- package/dist/inline-atom-episode.js +15 -7
- package/dist/inline-atom.js +8 -2
- package/dist/native-skill-adoption.js +11 -0
- package/dist/native-skill-mirror.js +8 -1
- package/dist/node-identity.bundle.js +1166 -0
- package/dist/opencode-plugin.bundle.js +307 -119
- package/dist/procedure-enabled.js +55 -0
- package/dist/procedure-runtime.js +6 -0
- package/dist/procedure-scope.js +190 -0
- package/dist/procedure-watch.js +29 -16
- package/dist/procedure.js +111 -5
- package/dist/project-anchor.js +1 -14
- package/dist/reminder-injector.js +11 -10
- package/dist/repodoc-client.js +296 -0
- package/dist/session-id.js +7 -8
- package/dist/skill-landing.js +57 -2
- package/dist/skill-mirror-client.js +14 -0
- package/dist/skill-mirror-files.js +18 -0
- package/package.json +2 -2
- package/scripts/bundle-node-identity.mjs +47 -0
- package/skill/templates/chip-spawn.md +7 -1
- package/skill/templates/delivery.md +105 -0
- package/skill/templates/prompt-audit.md +196 -0
- package/skill/templates/skill-change.md +25 -2
- package/dist/assistant-doctrine.js +0 -85
- package/dist/commands/assistant-reminder.js +0 -19
- package/dist/commands/assistant.js +0 -95
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/** `greprag repodoc` — give a repository's own markdown a stable identity that
|
|
3
|
+
* survives editing.
|
|
4
|
+
*
|
|
5
|
+
* The problem this solves: greprag's corpus matcher pairs a stored chunk with a
|
|
6
|
+
* new one only when they are byte-identical, which is right for a source that
|
|
7
|
+
* is re-fetched whole and mostly grows at the end. Repository documents are not
|
|
8
|
+
* that. An ADR gains a dated entry, a skill has a paragraph reworded, a design
|
|
9
|
+
* doc has a section split in two — and under the corpus matcher every one of
|
|
10
|
+
* those looks like a death and a birth. The chunk re-enriches at model cost and
|
|
11
|
+
* anything pointing at it dangles.
|
|
12
|
+
*
|
|
13
|
+
* So each document keeps a node graph beside it, under `.greprag/node-identity/`,
|
|
14
|
+
* mirroring the tree. `adr/thing.md` gets `.greprag/node-identity/adr/thing.md.json`.
|
|
15
|
+
* The graph is committed with the repo, so identity travels across machines and
|
|
16
|
+
* through git. The document itself is never written to — its own head stays
|
|
17
|
+
* clean, which matters because agents read these files constantly and a
|
|
18
|
+
* fifty-entry array at the top of every skill would land in context every time.
|
|
19
|
+
*
|
|
20
|
+
* The matching engine is @greprag/core's node-identity, built into
|
|
21
|
+
* dist/node-identity.bundle.js on every CLI build (see
|
|
22
|
+
* scripts/bundle-node-identity.mjs) because the CLI cannot import core.
|
|
23
|
+
*
|
|
24
|
+
* Reporting is the default and writing is opt-in. This is the first thing
|
|
25
|
+
* greprag writes into a user's own repository, so seeing the change precedes
|
|
26
|
+
* making it.
|
|
27
|
+
*
|
|
28
|
+
* adr: adr/node-identity-port.md · docs/node-identity-port.md
|
|
29
|
+
*/
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.runRepoDoc = runRepoDoc;
|
|
32
|
+
const child_process_1 = require("child_process");
|
|
33
|
+
const fs_1 = require("fs");
|
|
34
|
+
const path_1 = require("path");
|
|
35
|
+
const client_1 = require("./corpus/client");
|
|
36
|
+
const repodoc_client_1 = require("../repodoc-client");
|
|
37
|
+
/** Loaded lazily so `--help` works even if the bundle is missing from a partial
|
|
38
|
+
* build, and so the failure names the real cause instead of a module error. */
|
|
39
|
+
function engine() {
|
|
40
|
+
const path = (0, path_1.join)(__dirname, '..', 'node-identity.bundle.js');
|
|
41
|
+
if (!(0, fs_1.existsSync)(path)) {
|
|
42
|
+
throw new Error('node-identity.bundle.js is missing — run `npm run build` in packages/cli ' +
|
|
43
|
+
'(scripts/bundle-node-identity.mjs produces it from @greprag/core).');
|
|
44
|
+
}
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
46
|
+
return require(path);
|
|
47
|
+
}
|
|
48
|
+
// ---------- repo + file discovery -------------------------------------------
|
|
49
|
+
/** Walk up for the `.git` that marks the repository root. Everything is
|
|
50
|
+
* recorded relative to it so a graph means the same thing on any machine. */
|
|
51
|
+
function findRepoRoot(start) {
|
|
52
|
+
let dir = (0, path_1.resolve)(start);
|
|
53
|
+
for (;;) {
|
|
54
|
+
if ((0, fs_1.existsSync)((0, path_1.join)(dir, '.git')))
|
|
55
|
+
return dir;
|
|
56
|
+
const parent = (0, path_1.dirname)(dir);
|
|
57
|
+
if (parent === dir)
|
|
58
|
+
return null;
|
|
59
|
+
dir = parent;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** The repository's name, which decides which store its documents land in.
|
|
63
|
+
*
|
|
64
|
+
* Taken from the COMMON git directory, not the working tree, so a worktree and
|
|
65
|
+
* the checkout it was cut from share one store. Cutting a worktree must not
|
|
66
|
+
* fork a repository's identity into a second, empty store named after a branch. */
|
|
67
|
+
function repoName(repoRoot) {
|
|
68
|
+
try {
|
|
69
|
+
const common = (0, child_process_1.execFileSync)('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], {
|
|
70
|
+
cwd: repoRoot, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
71
|
+
}).trim();
|
|
72
|
+
if (common)
|
|
73
|
+
return (0, path_1.basename)((0, path_1.dirname)(common));
|
|
74
|
+
}
|
|
75
|
+
catch { /* not a git checkout, or git is absent — fall through */ }
|
|
76
|
+
return (0, path_1.basename)(repoRoot);
|
|
77
|
+
}
|
|
78
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.greprag']);
|
|
79
|
+
/** Every INDEXABLE markdown file under a path, or the path itself if it is one.
|
|
80
|
+
*
|
|
81
|
+
* Eligibility is `isRepoDocEligible`, the same predicate the Stop hook uses to
|
|
82
|
+
* decide what it will keep rebuilt. Anything this command indexes must be
|
|
83
|
+
* something the hook will maintain — a document indexed once and never
|
|
84
|
+
* refreshed is frozen at whatever it said that day, which is worse than not
|
|
85
|
+
* indexing it. That is why the filter lives in one module and not two. */
|
|
86
|
+
function collectMarkdown(target, repoRoot) {
|
|
87
|
+
if (!(0, fs_1.existsSync)(target))
|
|
88
|
+
return [];
|
|
89
|
+
const eligible = (abs) => {
|
|
90
|
+
const rel = (0, path_1.relative)(repoRoot, abs).split(path_1.sep).join('/');
|
|
91
|
+
if (!rel || rel.startsWith('..'))
|
|
92
|
+
return false;
|
|
93
|
+
if (!(0, repodoc_client_1.isRepoDocEligible)(rel))
|
|
94
|
+
return false;
|
|
95
|
+
try {
|
|
96
|
+
return !(0, repodoc_client_1.isGeneratedAdapter)((0, fs_1.readFileSync)(abs, 'utf-8'));
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
if ((0, fs_1.statSync)(target).isFile())
|
|
103
|
+
return eligible(target) ? [target] : [];
|
|
104
|
+
const out = [];
|
|
105
|
+
const walk = (dir) => {
|
|
106
|
+
for (const entry of (0, fs_1.readdirSync)(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
107
|
+
if (entry.isDirectory()) {
|
|
108
|
+
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith('.'))
|
|
109
|
+
continue;
|
|
110
|
+
walk((0, path_1.join)(dir, entry.name));
|
|
111
|
+
}
|
|
112
|
+
else if (entry.name.endsWith('.md') && eligible((0, path_1.join)(dir, entry.name))) {
|
|
113
|
+
out.push((0, path_1.join)(dir, entry.name));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
walk(target);
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
function reconcileFile(eng, repoRoot, absPath, write, keepBlocks = false) {
|
|
121
|
+
const relPath = (0, path_1.relative)(repoRoot, absPath).split(path_1.sep).join('/');
|
|
122
|
+
const sidecarRel = eng.sidecarPathFor(relPath);
|
|
123
|
+
const base = {
|
|
124
|
+
relPath, sidecarPath: sidecarRel || '', blocks: 0, preserved: 0, fresh: 0,
|
|
125
|
+
orphaned: 0, firstSight: false, changed: false, skipped: false,
|
|
126
|
+
};
|
|
127
|
+
if (!sidecarRel) {
|
|
128
|
+
return { ...base, skipped: true, error: 'path escapes the repository root' };
|
|
129
|
+
}
|
|
130
|
+
const body = (0, fs_1.readFileSync)(absPath, 'utf-8');
|
|
131
|
+
const sidecarAbs = (0, path_1.join)(repoRoot, sidecarRel);
|
|
132
|
+
let stored = null;
|
|
133
|
+
let storedText = null;
|
|
134
|
+
if ((0, fs_1.existsSync)(sidecarAbs)) {
|
|
135
|
+
storedText = (0, fs_1.readFileSync)(sidecarAbs, 'utf-8');
|
|
136
|
+
stored = eng.parseNodeGraph(storedText);
|
|
137
|
+
if (stored === null) {
|
|
138
|
+
// Unreadable or from a version we do not know. Re-bootstrapping loses id
|
|
139
|
+
// continuity for this one document but never corrupts it, which is the
|
|
140
|
+
// right trade — see adr/node-identity-port.md.
|
|
141
|
+
base.error = 'stored graph unreadable, re-bootstrapping';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const firstSight = stored === null;
|
|
145
|
+
const result = eng.reconcile(stored, eng.markdownToBlocks(body), { path: relPath, body });
|
|
146
|
+
const s = result.match.summary;
|
|
147
|
+
const outcome = {
|
|
148
|
+
...base,
|
|
149
|
+
blocks: s.totalBlocks,
|
|
150
|
+
preserved: s.preserved,
|
|
151
|
+
fresh: s.fresh,
|
|
152
|
+
orphaned: s.orphanedCount,
|
|
153
|
+
firstSight,
|
|
154
|
+
changed: result.changed,
|
|
155
|
+
...(keepBlocks ? { resolved: result.blocks } : {}),
|
|
156
|
+
};
|
|
157
|
+
if (write && result.changed) {
|
|
158
|
+
(0, fs_1.mkdirSync)((0, path_1.dirname)(sidecarAbs), { recursive: true });
|
|
159
|
+
(0, fs_1.writeFileSync)(sidecarAbs, eng.serializeNodeGraph(result.graph), 'utf-8');
|
|
160
|
+
}
|
|
161
|
+
return outcome;
|
|
162
|
+
}
|
|
163
|
+
// ---------- reporting --------------------------------------------------------
|
|
164
|
+
function pad(s, n) {
|
|
165
|
+
return s.length >= n ? s : s + ' '.repeat(n - s.length);
|
|
166
|
+
}
|
|
167
|
+
function reportIndex(outcomes, write) {
|
|
168
|
+
const width = Math.min(60, Math.max(20, ...outcomes.map(o => o.relPath.length)));
|
|
169
|
+
const interesting = outcomes.filter(o => o.changed || o.skipped || o.error);
|
|
170
|
+
const quiet = outcomes.length - interesting.length;
|
|
171
|
+
for (const o of interesting) {
|
|
172
|
+
if (o.skipped) {
|
|
173
|
+
console.log(` skip ${pad(o.relPath, width)} ${o.error}`);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const detail = o.firstSight
|
|
177
|
+
? `${o.blocks} blocks, first sight`
|
|
178
|
+
: `${o.blocks} blocks · kept ${o.preserved} · new ${o.fresh} · gone ${o.orphaned}`;
|
|
179
|
+
console.log(` ${write ? 'wrote' : 'would'} ${pad(o.relPath, width)} ${detail}${o.error ? ` (${o.error})` : ''}`);
|
|
180
|
+
}
|
|
181
|
+
if (quiet > 0) {
|
|
182
|
+
console.log(` ${quiet} unchanged (no graph written)`);
|
|
183
|
+
}
|
|
184
|
+
const totals = outcomes.reduce((acc, o) => ({
|
|
185
|
+
files: acc.files + (o.skipped ? 0 : 1),
|
|
186
|
+
blocks: acc.blocks + o.blocks,
|
|
187
|
+
preserved: acc.preserved + o.preserved,
|
|
188
|
+
fresh: acc.fresh + o.fresh,
|
|
189
|
+
orphaned: acc.orphaned + o.orphaned,
|
|
190
|
+
changed: acc.changed + (o.changed ? 1 : 0),
|
|
191
|
+
}), { files: 0, blocks: 0, preserved: 0, fresh: 0, orphaned: 0, changed: 0 });
|
|
192
|
+
console.log('');
|
|
193
|
+
console.log(` ${totals.files} files · ${totals.blocks} blocks · ` +
|
|
194
|
+
`kept ${totals.preserved} · new ${totals.fresh} · gone ${totals.orphaned}`);
|
|
195
|
+
if (!write) {
|
|
196
|
+
console.log(` ${totals.changed} graph${totals.changed === 1 ? '' : 's'} would be written under .greprag/node-identity/`);
|
|
197
|
+
console.log(' nothing was written — pass --write to persist');
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
console.log(` ${totals.changed} graph${totals.changed === 1 ? '' : 's'} written under .greprag/node-identity/`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// ---------- push --------------------------------------------------------------
|
|
204
|
+
/** One request carries at most this many documents; the server caps at the same
|
|
205
|
+
* number. A repository of any size is therefore several requests. */
|
|
206
|
+
const DOCS_PER_REQUEST = 100;
|
|
207
|
+
/** Ship resolved blocks to the repodoc store.
|
|
208
|
+
*
|
|
209
|
+
* The ids travel with the blocks. That is the whole point: every other ingest
|
|
210
|
+
* path re-derives identity server-side with the byte-identical matcher, which
|
|
211
|
+
* would throw away what the graph in this repository already settled. */
|
|
212
|
+
async function pushDocuments(outcomes, project, all) {
|
|
213
|
+
const cfg = (0, client_1.getConfig)();
|
|
214
|
+
if (!cfg.apiKey)
|
|
215
|
+
(0, client_1.abortNotConfigured)();
|
|
216
|
+
const candidates = outcomes.filter(o => !o.skipped && o.resolved && o.resolved.length > 0);
|
|
217
|
+
if (candidates.length === 0) {
|
|
218
|
+
console.log(' nothing to push');
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
// Ship what actually needs shipping. A document whose graph did not move and
|
|
222
|
+
// whose chunks the store already holds would be deleted and re-inserted
|
|
223
|
+
// byte-for-byte — pushing a whole repository would rewrite every row on every
|
|
224
|
+
// run. `changed` covers edits; the store listing covers a document that was
|
|
225
|
+
// reconciled on an earlier run but never made it into the store (an
|
|
226
|
+
// interrupted push, or a graph written by `index` before the first push).
|
|
227
|
+
let shippable = candidates;
|
|
228
|
+
let skipped = 0;
|
|
229
|
+
if (!all) {
|
|
230
|
+
let stored = new Set();
|
|
231
|
+
try {
|
|
232
|
+
const listing = await (0, client_1.apiGet)(`${cfg.apiUrl}/v1/repodoc/${encodeURIComponent(project)}`, cfg.apiKey);
|
|
233
|
+
stored = new Set(listing.documents.map(d => d.path));
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
// Cannot tell what the store holds — ship everything rather than guess.
|
|
237
|
+
stored = new Set();
|
|
238
|
+
}
|
|
239
|
+
shippable = candidates.filter(o => o.changed || !stored.has(o.relPath));
|
|
240
|
+
skipped = candidates.length - shippable.length;
|
|
241
|
+
}
|
|
242
|
+
if (shippable.length === 0) {
|
|
243
|
+
console.log(` nothing to push — all ${candidates.length} documents are already stored and unchanged`);
|
|
244
|
+
return { storeName: '', inserted: 0, replaced: 0, documents: 0, skipped };
|
|
245
|
+
}
|
|
246
|
+
let storeName = '';
|
|
247
|
+
let inserted = 0;
|
|
248
|
+
let replaced = 0;
|
|
249
|
+
for (let start = 0; start < shippable.length; start += DOCS_PER_REQUEST) {
|
|
250
|
+
const batch = shippable.slice(start, start + DOCS_PER_REQUEST);
|
|
251
|
+
const res = await (0, client_1.apiCall)(`${cfg.apiUrl}/v1/repodoc/${encodeURIComponent(project)}`, cfg.apiKey, {
|
|
252
|
+
documents: batch.map(o => ({
|
|
253
|
+
path: o.relPath,
|
|
254
|
+
blocks: (o.resolved || []).map(b => ({
|
|
255
|
+
id: b.id,
|
|
256
|
+
position: b.position,
|
|
257
|
+
type: b.type,
|
|
258
|
+
text: b.text,
|
|
259
|
+
parentPosition: b.parentPosition,
|
|
260
|
+
ordinalInParent: b.ordinalInParent,
|
|
261
|
+
...(b.level !== undefined ? { level: b.level } : {}),
|
|
262
|
+
...(b.language !== undefined ? { language: b.language } : {}),
|
|
263
|
+
headingPath: b.headingPath,
|
|
264
|
+
})),
|
|
265
|
+
})),
|
|
266
|
+
});
|
|
267
|
+
storeName = res.storeName;
|
|
268
|
+
inserted += res.totals.inserted;
|
|
269
|
+
replaced += res.totals.replaced;
|
|
270
|
+
console.log(` pushed ${Math.min(start + batch.length, shippable.length)}/${shippable.length} documents`);
|
|
271
|
+
}
|
|
272
|
+
return { storeName, inserted, replaced, documents: shippable.length, skipped };
|
|
273
|
+
}
|
|
274
|
+
function reportStatus(outcomes) {
|
|
275
|
+
const tracked = outcomes.filter(o => !o.firstSight && !o.skipped);
|
|
276
|
+
const untracked = outcomes.filter(o => o.firstSight && !o.skipped);
|
|
277
|
+
const stale = tracked.filter(o => o.changed);
|
|
278
|
+
console.log(` tracked ${tracked.length}`);
|
|
279
|
+
console.log(` untracked ${untracked.length}${untracked.length ? ' (run `greprag repodoc index <path> --write`)' : ''}`);
|
|
280
|
+
console.log(` stale ${stale.length}${stale.length ? ' (edited since their graph was written)' : ''}`);
|
|
281
|
+
for (const o of stale.slice(0, 20)) {
|
|
282
|
+
console.log(` ${o.relPath} kept ${o.preserved} · new ${o.fresh} · gone ${o.orphaned}`);
|
|
283
|
+
}
|
|
284
|
+
if (stale.length > 20)
|
|
285
|
+
console.log(` … and ${stale.length - 20} more`);
|
|
286
|
+
}
|
|
287
|
+
// ---------- entry ------------------------------------------------------------
|
|
288
|
+
const HELP = `
|
|
289
|
+
greprag repodoc — stable identity for a repository's own markdown
|
|
290
|
+
|
|
291
|
+
Each document keeps a node graph under .greprag/node-identity/, mirroring the
|
|
292
|
+
tree, committed alongside the repo. Blocks keep their ids through edits,
|
|
293
|
+
splits, merges, moves, retypes and paste-backs, so re-indexing an edited
|
|
294
|
+
document costs only the genuinely new content. The documents themselves are
|
|
295
|
+
never modified.
|
|
296
|
+
|
|
297
|
+
Usage
|
|
298
|
+
greprag repodoc index <path>... [--write] reconcile documents against their graphs
|
|
299
|
+
greprag repodoc push <path>... reconcile, write the graphs, and store the chunks
|
|
300
|
+
greprag repodoc status <path>... what is tracked, untracked, or stale
|
|
301
|
+
greprag repodoc list what the store currently holds
|
|
302
|
+
|
|
303
|
+
Search what you have pushed with the ordinary corpus search:
|
|
304
|
+
greprag corpus search "<query>" --kind repodoc
|
|
305
|
+
|
|
306
|
+
Options
|
|
307
|
+
--write persist the graphs. index only; push always writes them,
|
|
308
|
+
because chunks whose graph was not saved would be re-minted
|
|
309
|
+
with different ids on the next push.
|
|
310
|
+
--root <dir> repository root. Defaults to the nearest ancestor with .git.
|
|
311
|
+
--project <name> store name. Defaults to the repository's own name, taken
|
|
312
|
+
from the common git directory so a worktree shares the
|
|
313
|
+
checkout's store rather than forking a new one.
|
|
314
|
+
--all push every document, not only the ones whose graph moved or
|
|
315
|
+
that the store does not hold yet. Rarely needed.
|
|
316
|
+
|
|
317
|
+
Examples
|
|
318
|
+
greprag repodoc index adr see what indexing the ADRs would do
|
|
319
|
+
greprag repodoc index adr --write write their graphs
|
|
320
|
+
greprag repodoc push adr write the graphs and store the chunks
|
|
321
|
+
greprag repodoc status adr docs what has drifted since last time
|
|
322
|
+
`;
|
|
323
|
+
async function runRepoDoc(args) {
|
|
324
|
+
const fail = (message) => {
|
|
325
|
+
console.error(message);
|
|
326
|
+
process.exitCode = 1;
|
|
327
|
+
};
|
|
328
|
+
const sub = args[0];
|
|
329
|
+
if (!sub || sub === '--help' || sub === '-h' || sub === 'help') {
|
|
330
|
+
console.log(HELP.trim());
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (sub !== 'index' && sub !== 'status' && sub !== 'push' && sub !== 'list') {
|
|
334
|
+
return fail(`unknown subcommand "${sub}". Try: greprag repodoc --help`);
|
|
335
|
+
}
|
|
336
|
+
const rest = args.slice(1);
|
|
337
|
+
// push always writes the graphs. Storing chunks whose graph was not saved
|
|
338
|
+
// would make the next push re-bootstrap and mint different ids for the same
|
|
339
|
+
// prose — the exact churn this system exists to prevent.
|
|
340
|
+
const write = rest.includes('--write') || sub === 'push';
|
|
341
|
+
const rootFlag = rest.indexOf('--root');
|
|
342
|
+
const rootArg = rootFlag >= 0 ? rest[rootFlag + 1] : undefined;
|
|
343
|
+
const projectFlag = rest.indexOf('--project');
|
|
344
|
+
const projectArg = projectFlag >= 0 ? rest[projectFlag + 1] : undefined;
|
|
345
|
+
const valueFlagIndexes = new Set([rootFlag, projectFlag].filter(i => i >= 0).map(i => i + 1));
|
|
346
|
+
const targets = rest.filter((a, i) => !a.startsWith('--') && !valueFlagIndexes.has(i));
|
|
347
|
+
if (sub === 'list') {
|
|
348
|
+
const cfg = (0, client_1.getConfig)();
|
|
349
|
+
if (!cfg.apiKey)
|
|
350
|
+
(0, client_1.abortNotConfigured)();
|
|
351
|
+
const root = rootArg ? (0, path_1.resolve)(rootArg) : findRepoRoot(process.cwd());
|
|
352
|
+
const project = projectArg || (root ? repoName(root) : (0, path_1.basename)(process.cwd()));
|
|
353
|
+
try {
|
|
354
|
+
const res = await (0, client_1.apiGet)(`${cfg.apiUrl}/v1/repodoc/${encodeURIComponent(project)}`, cfg.apiKey);
|
|
355
|
+
console.log(`\n${res.storeName} — ${res.totals.documents} documents · ${res.totals.nodes} chunks · ${res.totals.words} words\n`);
|
|
356
|
+
for (const d of res.documents) {
|
|
357
|
+
console.log(` ${pad(d.path, 52)} ${String(d.nodes).padStart(5)} chunks`);
|
|
358
|
+
}
|
|
359
|
+
console.log('');
|
|
360
|
+
}
|
|
361
|
+
catch (err) {
|
|
362
|
+
return fail(String(err instanceof Error ? err.message : err));
|
|
363
|
+
}
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (targets.length === 0) {
|
|
367
|
+
return fail('name at least one file or directory. Try: greprag repodoc --help');
|
|
368
|
+
}
|
|
369
|
+
const repoRoot = rootArg ? (0, path_1.resolve)(rootArg) : findRepoRoot((0, path_1.resolve)(targets[0]));
|
|
370
|
+
if (!repoRoot) {
|
|
371
|
+
return fail('could not find a repository root (no .git above the target). Pass --root.');
|
|
372
|
+
}
|
|
373
|
+
const files = [];
|
|
374
|
+
for (const t of targets) {
|
|
375
|
+
const found = collectMarkdown((0, path_1.resolve)(t), repoRoot);
|
|
376
|
+
if (found.length === 0)
|
|
377
|
+
console.error(` nothing indexable under ${t}`);
|
|
378
|
+
files.push(...found);
|
|
379
|
+
}
|
|
380
|
+
if (files.length === 0)
|
|
381
|
+
return fail('nothing indexable found under any target.');
|
|
382
|
+
let eng;
|
|
383
|
+
try {
|
|
384
|
+
eng = engine();
|
|
385
|
+
}
|
|
386
|
+
catch (err) {
|
|
387
|
+
return fail(String(err instanceof Error ? err.message : err));
|
|
388
|
+
}
|
|
389
|
+
console.log(`\nrepodoc ${sub} — ${files.length} document${files.length === 1 ? '' : 's'} under ${repoRoot}\n`);
|
|
390
|
+
const outcomes = [];
|
|
391
|
+
for (const file of files) {
|
|
392
|
+
try {
|
|
393
|
+
// `status` never writes, whatever flags were passed. `push` keeps the
|
|
394
|
+
// resolved blocks so it can ship them without walking anything twice.
|
|
395
|
+
outcomes.push(reconcileFile(eng, repoRoot, file, sub !== 'status' && write, sub === 'push'));
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
outcomes.push({
|
|
399
|
+
relPath: (0, path_1.relative)(repoRoot, file).split(path_1.sep).join('/'),
|
|
400
|
+
sidecarPath: '', blocks: 0, preserved: 0, fresh: 0, orphaned: 0,
|
|
401
|
+
firstSight: false, changed: false, skipped: true,
|
|
402
|
+
error: String(err instanceof Error ? err.message : err),
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (sub === 'status') {
|
|
407
|
+
reportStatus(outcomes);
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
reportIndex(outcomes, write);
|
|
411
|
+
}
|
|
412
|
+
if (sub === 'push') {
|
|
413
|
+
const project = projectArg || repoName(repoRoot);
|
|
414
|
+
console.log('');
|
|
415
|
+
try {
|
|
416
|
+
const pushed = await pushDocuments(outcomes, project, rest.includes('--all'));
|
|
417
|
+
if (pushed && pushed.documents > 0) {
|
|
418
|
+
console.log('');
|
|
419
|
+
console.log(` ${pushed.documents} documents → ${pushed.storeName}: ` +
|
|
420
|
+
`${pushed.inserted} chunks stored, ${pushed.replaced} replaced` +
|
|
421
|
+
(pushed.skipped ? `, ${pushed.skipped} unchanged and skipped` : ''));
|
|
422
|
+
console.log(` search them: greprag corpus search "<query>" --kind repodoc`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
catch (err) {
|
|
426
|
+
console.log('');
|
|
427
|
+
return fail(` push failed: ${String(err instanceof Error ? err.message : err)}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
console.log('');
|
|
431
|
+
if (outcomes.some(o => o.skipped))
|
|
432
|
+
process.exitCode = 1;
|
|
433
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/** greprag search — one lexical search across everything greprag holds.
|
|
3
|
+
*
|
|
4
|
+
* The engine for this already existed, reachable only as
|
|
5
|
+
* `greprag corpus search "<q>" --all`. Nothing about it was corpus-specific,
|
|
6
|
+
* and nobody would guess to look there, so it went unused. This is the front
|
|
7
|
+
* door: no flags needed, and the default scope is every store holding
|
|
8
|
+
* retrievable prose — reference banks, repo markdown, skills, episodic memory.
|
|
9
|
+
*
|
|
10
|
+
* Queues and registries are excluded by the server (SEARCHABLE_KINDS in
|
|
11
|
+
* @greprag/core). Name a kind explicitly to reach one.
|
|
12
|
+
*
|
|
13
|
+
* adr: adr/searchable-store-kinds.md
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.runSearch = runSearch;
|
|
17
|
+
const client_1 = require("./corpus/client");
|
|
18
|
+
const VALUE_FLAGS = new Set(['--limit', '--kind', '--store', '--tag', '--section', '--rerank-model']);
|
|
19
|
+
const USAGE = `Usage: greprag search "<query>" [options]
|
|
20
|
+
|
|
21
|
+
Searches everything: reference banks, repo markdown, skills, episodic memory.
|
|
22
|
+
|
|
23
|
+
--limit N results to return (default 8)
|
|
24
|
+
--kind K only stores of this kind (repodoc, docs, memory, skill, ...)
|
|
25
|
+
--store REF only this store; repeatable
|
|
26
|
+
--tag T only stores carrying this tag
|
|
27
|
+
--section "H" only chunks under this heading
|
|
28
|
+
--json machine-readable output
|
|
29
|
+
--rerank opt-in LLM relevance rerank (costs money; off by default)
|
|
30
|
+
|
|
31
|
+
greprag search "how do deploy locks work"
|
|
32
|
+
greprag search "secure enclave" --kind repodoc
|
|
33
|
+
greprag search "watcher arm check" --kind memory --limit 15`;
|
|
34
|
+
/** Where a hit came from, in words rather than store-naming conventions. The
|
|
35
|
+
* point of one search over everything is knowing which body answered. */
|
|
36
|
+
function sourceLabel(storeName, kind) {
|
|
37
|
+
if (kind === 'repodoc')
|
|
38
|
+
return storeName.replace(/^repodoc:/, 'repo ');
|
|
39
|
+
if (kind === 'memory')
|
|
40
|
+
return 'memory';
|
|
41
|
+
if (kind === 'skill')
|
|
42
|
+
return storeName.replace(/^skill:(global:|project:[^:]+:)?/, 'skill ');
|
|
43
|
+
if (storeName.startsWith('docmirror:'))
|
|
44
|
+
return 'project docs';
|
|
45
|
+
return storeName;
|
|
46
|
+
}
|
|
47
|
+
const scoreTag = (n) => n.confidence != null && Number.isFinite(n.confidence)
|
|
48
|
+
? `conf=${n.confidence.toFixed(2)}`
|
|
49
|
+
: `score=${n.score.toFixed(3)}`;
|
|
50
|
+
async function runSearch(args) {
|
|
51
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
|
|
52
|
+
console.log(USAGE);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const cfg = (0, client_1.getConfig)();
|
|
56
|
+
if (!cfg.apiKey)
|
|
57
|
+
(0, client_1.abortNotConfigured)();
|
|
58
|
+
const json = args.includes('--json');
|
|
59
|
+
const rerank = args.includes('--rerank');
|
|
60
|
+
const rerankModel = (0, client_1.getFlag)(args, '--rerank-model');
|
|
61
|
+
const limit = parseInt((0, client_1.getFlag)(args, '--limit') || '8', 10);
|
|
62
|
+
const kind = (0, client_1.getFlag)(args, '--kind');
|
|
63
|
+
const tag = (0, client_1.getFlag)(args, '--tag');
|
|
64
|
+
const section = (0, client_1.getFlag)(args, '--section');
|
|
65
|
+
const storeRefs = (0, client_1.getFlags)(args, '--store');
|
|
66
|
+
const queryText = (0, client_1.extractPositionals)(args, VALUE_FLAGS)[0];
|
|
67
|
+
if (!queryText) {
|
|
68
|
+
console.error(USAGE);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
if (!Number.isFinite(limit) || limit < 1) {
|
|
72
|
+
console.error('--limit must be a positive number.');
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
// Scope. Nothing named = every searchable store, decided server-side so the
|
|
76
|
+
// CLI never has to carry its own copy of which kinds hold prose.
|
|
77
|
+
let storeIds;
|
|
78
|
+
if (storeRefs.length > 0) {
|
|
79
|
+
storeIds = [];
|
|
80
|
+
for (const ref of storeRefs)
|
|
81
|
+
storeIds.push((await (0, client_1.resolveStore)(ref, cfg.apiUrl, cfg.apiKey)).id);
|
|
82
|
+
}
|
|
83
|
+
else if (kind || tag) {
|
|
84
|
+
const params = new URLSearchParams();
|
|
85
|
+
if (kind)
|
|
86
|
+
params.set('kind', kind);
|
|
87
|
+
if (tag)
|
|
88
|
+
params.set('tag', tag);
|
|
89
|
+
const list = await (0, client_1.apiGet)(`${cfg.apiUrl}/v1/stores?${params.toString()}`, cfg.apiKey);
|
|
90
|
+
storeIds = list.stores.map(s => s.id);
|
|
91
|
+
if (storeIds.length === 0) {
|
|
92
|
+
console.error(`Nothing to search${kind ? ` with kind "${kind}"` : ''}${tag ? ` tagged "${tag}"` : ''}.`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const body = {
|
|
97
|
+
queries: [queryText], limit, adjacencyWindow: 3, adjacencyAlpha: 0.3,
|
|
98
|
+
};
|
|
99
|
+
if (storeIds)
|
|
100
|
+
body.storeIds = storeIds;
|
|
101
|
+
if (section)
|
|
102
|
+
body.filters = { section };
|
|
103
|
+
if (rerank) {
|
|
104
|
+
body.rerank = true;
|
|
105
|
+
if (rerankModel)
|
|
106
|
+
body.rerankModel = rerankModel;
|
|
107
|
+
}
|
|
108
|
+
const t0 = Date.now();
|
|
109
|
+
const result = await (0, client_1.apiCall)(`${cfg.apiUrl}/v1/search`, cfg.apiKey, body);
|
|
110
|
+
if (!result.ok || !result.nodes)
|
|
111
|
+
throw new Error(result.error || 'search failed');
|
|
112
|
+
const ms = Date.now() - t0;
|
|
113
|
+
// Store names for labeling. One listing call covers every hit.
|
|
114
|
+
const list = await (0, client_1.apiGet)(`${cfg.apiUrl}/v1/stores`, cfg.apiKey);
|
|
115
|
+
const byId = new Map(list.stores.map(s => [s.id, s]));
|
|
116
|
+
if (json) {
|
|
117
|
+
console.log(JSON.stringify({
|
|
118
|
+
ok: true, query: queryText, latencyMs: ms, reranked: !!result.reranked,
|
|
119
|
+
hits: result.nodes.map(n => {
|
|
120
|
+
const s = byId.get(n.storeId);
|
|
121
|
+
return {
|
|
122
|
+
nodeId: n.nodeId, storeId: n.storeId,
|
|
123
|
+
store: s?.name ?? n.storeId, kind: s?.kind ?? null,
|
|
124
|
+
source: sourceLabel(s?.name ?? n.storeId, s?.kind ?? ''),
|
|
125
|
+
score: n.score, confidence: n.confidence ?? null,
|
|
126
|
+
headingPath: n.headingPath ?? [], content: n.content,
|
|
127
|
+
};
|
|
128
|
+
}),
|
|
129
|
+
}, null, 2));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const scope = storeIds ? `${storeIds.length} store${storeIds.length === 1 ? '' : 's'}` : 'everything';
|
|
133
|
+
const rerankTag = result.reranked ? ' · reranked' : (rerank ? ' · rerank unavailable, lexical only' : '');
|
|
134
|
+
console.log(`\nSearch (${scope}) <- "${queryText}"${rerankTag}`);
|
|
135
|
+
console.log(` ${result.nodes.length} hit${result.nodes.length === 1 ? '' : 's'} in ${ms}ms\n`);
|
|
136
|
+
if (result.nodes.length === 0) {
|
|
137
|
+
console.log(' Nothing matched. Every word has to appear in the same chunk —');
|
|
138
|
+
console.log(' try fewer words, or quote a phrase.\n');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
for (const n of result.nodes) {
|
|
142
|
+
const s = byId.get(n.storeId);
|
|
143
|
+
const source = sourceLabel(s?.name ?? n.storeId.slice(0, 8), s?.kind ?? '');
|
|
144
|
+
const heading = n.headingPath?.length ? ` · ${n.headingPath.join(' / ')}` : '';
|
|
145
|
+
const snippet = n.content.length > 240 ? n.content.slice(0, 237) + '...' : n.content;
|
|
146
|
+
console.log(` [${source} · ${n.nodeId}] ${scoreTag(n)}${heading}`);
|
|
147
|
+
console.log(` ${snippet.replace(/\n/g, ' ')}\n`);
|
|
148
|
+
}
|
|
149
|
+
}
|