minnimemory 1.0.0-beta.1

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.
Files changed (59) hide show
  1. package/LICENSE +39 -0
  2. package/README.md +824 -0
  3. package/dist/bench.d.ts +98 -0
  4. package/dist/bench.js +142 -0
  5. package/dist/benchReport.d.ts +12 -0
  6. package/dist/benchReport.js +128 -0
  7. package/dist/bounds.d.ts +40 -0
  8. package/dist/bounds.js +44 -0
  9. package/dist/cli.d.ts +15 -0
  10. package/dist/cli.js +503 -0
  11. package/dist/compile.d.ts +187 -0
  12. package/dist/compile.js +516 -0
  13. package/dist/discover.d.ts +125 -0
  14. package/dist/discover.js +520 -0
  15. package/dist/doctor.d.ts +9 -0
  16. package/dist/doctor.js +67 -0
  17. package/dist/episodic.d.ts +47 -0
  18. package/dist/episodic.js +130 -0
  19. package/dist/hook.d.ts +45 -0
  20. package/dist/hook.js +104 -0
  21. package/dist/index.d.ts +18 -0
  22. package/dist/index.js +18 -0
  23. package/dist/init.d.ts +125 -0
  24. package/dist/init.js +475 -0
  25. package/dist/instructions.d.ts +60 -0
  26. package/dist/instructions.js +270 -0
  27. package/dist/mcp.d.ts +109 -0
  28. package/dist/mcp.js +252 -0
  29. package/dist/mcpServer.d.ts +136 -0
  30. package/dist/mcpServer.js +997 -0
  31. package/dist/paths.d.ts +25 -0
  32. package/dist/paths.js +47 -0
  33. package/dist/recall.d.ts +113 -0
  34. package/dist/recall.js +256 -0
  35. package/dist/recallDir.d.ts +50 -0
  36. package/dist/recallDir.js +187 -0
  37. package/dist/reorganize.d.ts +62 -0
  38. package/dist/reorganize.js +216 -0
  39. package/dist/report.d.ts +16 -0
  40. package/dist/report.js +204 -0
  41. package/dist/router.d.ts +141 -0
  42. package/dist/router.js +314 -0
  43. package/dist/rules.d.ts +32 -0
  44. package/dist/rules.js +651 -0
  45. package/dist/scan.d.ts +110 -0
  46. package/dist/scan.js +173 -0
  47. package/dist/text.d.ts +158 -0
  48. package/dist/text.js +395 -0
  49. package/dist/tokenizer.d.ts +26 -0
  50. package/dist/tokenizer.js +69 -0
  51. package/dist/types.d.ts +156 -0
  52. package/dist/types.js +17 -0
  53. package/dist/version.d.ts +7 -0
  54. package/dist/version.js +7 -0
  55. package/dist/writeProtocol.d.ts +19 -0
  56. package/dist/writeProtocol.js +45 -0
  57. package/examples/CLAUDE.md +75 -0
  58. package/examples/README.md +7 -0
  59. package/package.json +52 -0
package/dist/scan.d.ts ADDED
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Structured, whole-directory inventory of a memory setup for an agent to reorganize.
3
+ *
4
+ * This is the deterministic half of "AI-scanned reorganization" (project_minnimemory memory,
5
+ * 2026-09-03): scan.ts computes facts - frontmatter, section boundaries, volatility evidence,
6
+ * candidate routing keywords, cross-file duplicate blocks - the same primitives doctor/init
7
+ * already use. It never classifies, merges, or judges; it hands a calling agent everything it
8
+ * would otherwise have to re-derive by reading every file by hand. The agent (not this module,
9
+ * not an API call - see the locked "no LLM in init/doctor" decision) does the O1-O5/CoALA
10
+ * judgment and produces a ReorganizePlan for reorganize.ts to apply.
11
+ */
12
+ import { type MemoryKind } from "./episodic.js";
13
+ import type { WorkspaceShape } from "./types.js";
14
+ export interface Frontmatter {
15
+ name?: string;
16
+ description?: string;
17
+ /** metadata.type - the auto-memory category (user/feedback/project/reference) when present */
18
+ type?: string;
19
+ }
20
+ export interface SectionScan {
21
+ heading: string;
22
+ level: number;
23
+ startLine: number;
24
+ endLine: number;
25
+ tokens: number;
26
+ volatile: boolean;
27
+ volatilityReasons: string[];
28
+ /** O2: semantic, episodic or procedural, decided per section */
29
+ kind: MemoryKind;
30
+ keywords: string[];
31
+ }
32
+ export interface FileScan {
33
+ rel: string;
34
+ tokens: number;
35
+ isList: boolean;
36
+ alwaysLoaded: boolean;
37
+ frontmatter?: Frontmatter;
38
+ sections: SectionScan[];
39
+ }
40
+ export interface DuplicateScan {
41
+ /** short preview, not the full block - keep the scan itself cheap to hold in context */
42
+ preview: string;
43
+ tokensWastedPerExtraCopy: number;
44
+ occurrences: {
45
+ file: string;
46
+ startLine: number;
47
+ endLine: number;
48
+ }[];
49
+ }
50
+ export interface MemoryScan {
51
+ root: string;
52
+ shape: WorkspaceShape;
53
+ /** `@path` imports outside the root that were not followed; see discover.resolveImports */
54
+ skippedImports?: string[];
55
+ totalTokens: number;
56
+ alwaysLoadedTokens: number;
57
+ files: FileScan[];
58
+ duplicates: DuplicateScan[];
59
+ }
60
+ export interface FileScanSummary {
61
+ rel: string;
62
+ tokens: number;
63
+ isList: boolean;
64
+ alwaysLoaded: boolean;
65
+ frontmatter?: Frontmatter;
66
+ sectionCount: number;
67
+ volatileSectionCount: number;
68
+ kinds: Record<MemoryKind, number>;
69
+ }
70
+ export interface MemoryScanSummary {
71
+ root: string;
72
+ shape: WorkspaceShape;
73
+ skippedImports?: string[];
74
+ totalTokens: number;
75
+ alwaysLoadedTokens: number;
76
+ files: FileScanSummary[];
77
+ duplicates: {
78
+ groups: number;
79
+ tokensWasted: number;
80
+ };
81
+ }
82
+ /**
83
+ * The rollup a caller reaches for first: counts and per-file totals, not every section's
84
+ * heading, keywords and volatility reasons (the `full` shape below). 2026-09-13 audit round 2,
85
+ * D4 - `scan`'s default output on a real memory directory of any size was mostly detail nobody
86
+ * asked for yet; `detail: "full"` still returns the original shape when a caller actually wants
87
+ * every section.
88
+ */
89
+ export declare function summarize(scan: MemoryScan): MemoryScanSummary;
90
+ /**
91
+ * Restrict a scan to exactly the named files (matched against `rel`), recomputing the token
92
+ * totals over just that subset so they describe what is actually returned. A name that matches
93
+ * nothing is reported back in `unknownFiles` rather than silently dropped. `duplicates` are left
94
+ * as computed over the whole workspace: a block duplicated between a kept file and a filtered-out
95
+ * one is still worth knowing about, and pretending the other copy does not exist would hide
96
+ * exactly the fact this field exists to surface.
97
+ */
98
+ export declare function filterScanFiles(scan: MemoryScan, files: string[] | undefined): {
99
+ scan: MemoryScan;
100
+ unknownFiles: string[];
101
+ };
102
+ /**
103
+ * Scan any target discover() can resolve - a bare memory directory, a Claude Code auto-memory
104
+ * folder (directly or via a project root), a host-file repo - into a full structured inventory.
105
+ * Read-only, no writes, safe to call as often as needed while drafting a plan.
106
+ */
107
+ export declare function scanMemoryDir(target: string, options?: {
108
+ followExternalImports?: boolean;
109
+ includeAutoMemory?: boolean;
110
+ }): MemoryScan;
package/dist/scan.js ADDED
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Structured, whole-directory inventory of a memory setup for an agent to reorganize.
3
+ *
4
+ * This is the deterministic half of "AI-scanned reorganization" (project_minnimemory memory,
5
+ * 2026-09-03): scan.ts computes facts - frontmatter, section boundaries, volatility evidence,
6
+ * candidate routing keywords, cross-file duplicate blocks - the same primitives doctor/init
7
+ * already use. It never classifies, merges, or judges; it hands a calling agent everything it
8
+ * would otherwise have to re-derive by reading every file by hand. The agent (not this module,
9
+ * not an API call - see the locked "no LLM in init/doctor" decision) does the O1-O5/CoALA
10
+ * judgment and produces a ReorganizePlan for reorganize.ts to apply.
11
+ */
12
+ import { discover } from "./discover.js";
13
+ import { classifyKind } from "./episodic.js";
14
+ import { findDuplicateBlocks } from "./rules.js";
15
+ import { keywords as extractKeywords, sectionVolatilityReasons, sections, splitFrontmatter } from "./text.js";
16
+ import { estimateTokens } from "./tokenizer.js";
17
+ /**
18
+ * The rollup a caller reaches for first: counts and per-file totals, not every section's
19
+ * heading, keywords and volatility reasons (the `full` shape below). 2026-09-13 audit round 2,
20
+ * D4 - `scan`'s default output on a real memory directory of any size was mostly detail nobody
21
+ * asked for yet; `detail: "full"` still returns the original shape when a caller actually wants
22
+ * every section.
23
+ */
24
+ export function summarize(scan) {
25
+ const files = scan.files.map((f) => {
26
+ const kinds = { semantic: 0, episodic: 0, procedural: 0 };
27
+ let volatileSectionCount = 0;
28
+ for (const s of f.sections) {
29
+ kinds[s.kind]++;
30
+ if (s.volatile)
31
+ volatileSectionCount++;
32
+ }
33
+ return {
34
+ rel: f.rel,
35
+ tokens: f.tokens,
36
+ isList: f.isList,
37
+ alwaysLoaded: f.alwaysLoaded,
38
+ ...(f.frontmatter ? { frontmatter: f.frontmatter } : {}),
39
+ sectionCount: f.sections.length,
40
+ volatileSectionCount,
41
+ kinds,
42
+ };
43
+ });
44
+ const tokensWasted = scan.duplicates.reduce((n, d) => n + d.tokensWastedPerExtraCopy * Math.max(0, d.occurrences.length - 1), 0);
45
+ return {
46
+ root: scan.root,
47
+ shape: scan.shape,
48
+ ...(scan.skippedImports?.length ? { skippedImports: scan.skippedImports } : {}),
49
+ totalTokens: scan.totalTokens,
50
+ alwaysLoadedTokens: scan.alwaysLoadedTokens,
51
+ files,
52
+ duplicates: { groups: scan.duplicates.length, tokensWasted },
53
+ };
54
+ }
55
+ /**
56
+ * Restrict a scan to exactly the named files (matched against `rel`), recomputing the token
57
+ * totals over just that subset so they describe what is actually returned. A name that matches
58
+ * nothing is reported back in `unknownFiles` rather than silently dropped. `duplicates` are left
59
+ * as computed over the whole workspace: a block duplicated between a kept file and a filtered-out
60
+ * one is still worth knowing about, and pretending the other copy does not exist would hide
61
+ * exactly the fact this field exists to surface.
62
+ */
63
+ export function filterScanFiles(scan, files) {
64
+ if (!files || files.length === 0)
65
+ return { scan, unknownFiles: [] };
66
+ const wanted = new Set(files);
67
+ const kept = scan.files.filter((f) => wanted.has(f.rel));
68
+ const keptRels = new Set(kept.map((f) => f.rel));
69
+ const unknownFiles = files.filter((f) => !keptRels.has(f));
70
+ const totalTokens = kept.reduce((n, f) => n + f.tokens, 0);
71
+ const alwaysLoadedTokens = kept.filter((f) => f.alwaysLoaded).reduce((n, f) => n + f.tokens, 0);
72
+ return {
73
+ scan: { ...scan, files: kept, totalTokens, alwaysLoadedTokens },
74
+ unknownFiles,
75
+ };
76
+ }
77
+ /**
78
+ * A minimal, deliberately non-general YAML reader for exactly the frontmatter shape this
79
+ * project's memory files use: flat `key: value` lines plus one level of nesting under
80
+ * `metadata:`. Not a YAML parser - a real one is a dependency this offline-first tool does not
81
+ * need for three known keys.
82
+ */
83
+ function parseFrontmatter(lines) {
84
+ if (lines.length === 0)
85
+ return undefined;
86
+ const body = lines.slice(1, -1); // drop the two `---` fence lines
87
+ const out = {};
88
+ let inMetadata = false;
89
+ const unquote = (v) => v.trim().replace(/^["']|["']$/g, "");
90
+ for (const raw of body) {
91
+ const indented = /^\s+/.test(raw) && raw.trim() !== "";
92
+ if (!indented)
93
+ inMetadata = /^metadata\s*:/.test(raw.trim());
94
+ const m = /^\s*([\w.-]+)\s*:\s*(.*)$/.exec(raw);
95
+ if (!m)
96
+ continue;
97
+ const key = m[1] ?? "";
98
+ const value = unquote(m[2] ?? "");
99
+ if (!indented && key === "name" && value)
100
+ out.name = value;
101
+ else if (!indented && key === "description" && value)
102
+ out.description = value;
103
+ else if (indented && inMetadata && key === "type" && value)
104
+ out.type = value;
105
+ }
106
+ return Object.keys(out).length > 0 ? out : undefined;
107
+ }
108
+ function scanFile(file, isList) {
109
+ const { frontmatter: fmLines, bodyStartLine } = splitFrontmatter(file.lines);
110
+ const bodyLines = file.lines.slice(bodyStartLine - 1);
111
+ const sectionScans = sections(bodyLines).map((s) => {
112
+ const sectionLines = bodyLines.slice(s.startLine - 1, s.endLine);
113
+ const reasons = sectionVolatilityReasons(sectionLines);
114
+ const bodyOnly = sectionLines.slice(1).join("\n");
115
+ return {
116
+ heading: s.heading,
117
+ level: s.level,
118
+ // offset back into the whole-file line numbering, since sections() above only saw bodyLines
119
+ startLine: s.startLine + bodyStartLine - 1,
120
+ endLine: s.endLine + bodyStartLine - 1,
121
+ tokens: estimateTokens(sectionLines.join("\n")),
122
+ volatile: reasons.length > 0,
123
+ volatilityReasons: reasons,
124
+ kind: classifyKind(s.heading, sectionLines.slice(1)),
125
+ keywords: extractKeywords(s.heading, bodyOnly),
126
+ };
127
+ });
128
+ return {
129
+ rel: file.rel,
130
+ tokens: file.tokens,
131
+ isList,
132
+ alwaysLoaded: file.alwaysLoaded,
133
+ frontmatter: parseFrontmatter(fmLines),
134
+ sections: sectionScans,
135
+ };
136
+ }
137
+ const PREVIEW_LEN = 160;
138
+ /**
139
+ * Scan any target discover() can resolve - a bare memory directory, a Claude Code auto-memory
140
+ * folder (directly or via a project root), a host-file repo - into a full structured inventory.
141
+ * Read-only, no writes, safe to call as often as needed while drafting a plan.
142
+ */
143
+ export function scanMemoryDir(target, options = {}) {
144
+ const ws = discover(target, {
145
+ followExternalImports: options.followExternalImports === true,
146
+ includeAutoMemory: options.includeAutoMemory !== false,
147
+ });
148
+ const files = ws.files.map((f) => scanFile(f, f === ws.onDemandList || f === ws.autoMemoryList));
149
+ const duplicates = findDuplicateBlocks(ws.files).map(({ occurrences, wastedTokens }) => {
150
+ const first = occurrences[0];
151
+ const text = first ? first.block.text : "";
152
+ return {
153
+ preview: text.length > PREVIEW_LEN ? `${text.slice(0, PREVIEW_LEN)}…` : text,
154
+ tokensWastedPerExtraCopy: occurrences.length > 1 ? Math.round(wastedTokens / (occurrences.length - 1)) : 0,
155
+ occurrences: occurrences.map((o) => ({
156
+ file: o.file.rel,
157
+ startLine: o.block.startLine,
158
+ endLine: o.block.endLine,
159
+ })),
160
+ };
161
+ });
162
+ const totalTokens = ws.files.reduce((n, f) => n + f.tokens, 0);
163
+ const alwaysLoadedTokens = ws.files.filter((f) => f.alwaysLoaded).reduce((n, f) => n + f.tokens, 0);
164
+ return {
165
+ root: ws.root,
166
+ shape: ws.shape,
167
+ ...(ws.skippedImports?.length ? { skippedImports: ws.skippedImports } : {}),
168
+ totalTokens,
169
+ alwaysLoadedTokens,
170
+ files,
171
+ duplicates,
172
+ };
173
+ }
package/dist/text.d.ts ADDED
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Text primitives shared by the rule engine and the compiler.
3
+ */
4
+ export interface Section {
5
+ /** heading text with the hashes stripped */
6
+ heading: string;
7
+ /** 1 for #, 2 for ##, and so on. 0 for the synthetic preamble section. */
8
+ level: number;
9
+ /** 1-indexed, inclusive */
10
+ startLine: number;
11
+ endLine: number;
12
+ }
13
+ export interface Block {
14
+ text: string;
15
+ startLine: number;
16
+ endLine: number;
17
+ }
18
+ /**
19
+ * Split lines into heading-delimited sections. Lines inside fenced code blocks are never
20
+ * headings: a shell comment like `# Machine-readable output` in a bash example is not a title.
21
+ * Found by the 2026-09-02 sweep, where one real CLAUDE.md gained a phantom H1 mid-file and
22
+ * half its content was duplicated into AlwaysOnMemory as "preamble".
23
+ */
24
+ export declare function sections(lines: string[]): Section[];
25
+ /** Split lines into blank-line-delimited blocks. */
26
+ export declare function blocks(lines: string[]): Block[];
27
+ /** Normalise a block so cosmetic differences do not hide a duplicate. */
28
+ export declare function normalise(text: string): string;
29
+ /** Filesystem-safe, stable slug for a heading. */
30
+ export declare function slugify(heading: string): string;
31
+ /**
32
+ * Volatility: does this content change often, so that editing it would invalidate the cached
33
+ * always-loaded prefix?
34
+ *
35
+ * This is scored on evidence, not fired by a single word. The 2026-09-02 sweep over 23 real
36
+ * CLAUDE.md/AGENTS.md files from public repos showed that one-word matching flagged stable
37
+ * sections constantly: a version number inside a URL, "the next step" in a coding standard,
38
+ * "regenerate the changelog" in commit guidance, "pending activity" in a debugging note. Those
39
+ * are prose, not status. What actually marks status is a status heading, dated log entries,
40
+ * checklists, or several status phrases together.
41
+ *
42
+ * Scoring (a section is volatile at VOLATILITY_THRESHOLD or above):
43
+ * status heading ("Current status", "Changelog", "Roadmap", "Open items"...) +2
44
+ * list item that starts with a date (a log entry) +1 each
45
+ * strong status phrase ("as of 2026", "in progress", "blocked on") +1 each
46
+ * date anywhere else in a line +0.5 each
47
+ * weak status word ("currently", "pending", "todo", "roadmap" in prose) +0.5 each
48
+ * checkbox item +0.25 each, at most +1
49
+ * version string +0.25 each
50
+ * Inline code spans and URLs are removed from a line before matching, so a version in a link
51
+ * or a `TODO` in a code sample never counts.
52
+ */
53
+ export declare const VOLATILITY_THRESHOLD = 2;
54
+ /**
55
+ * Interface rule O7, the granularity clause: a section above this many tokens carries
56
+ * subheadings so that retrieval has a unit smaller than the whole. `doctor` flags a flat
57
+ * section over it (MM009); `init` splits a section over it at its subheadings into separate
58
+ * OnDemandMemory files, so the agent-driven route (an agent reading a whole file the
59
+ * OnDemandMemory list named) gets the same granularity `recall` gets from in-file sections.
60
+ */
61
+ export declare const GRANULARITY_TOKENS = 800;
62
+ /**
63
+ * A bullet (list marker or numbered item) whose first few words carry a date: a changelog or
64
+ * session-log entry. The single shared definition (2026-09-13 audit round 2, D7a) - episodic.ts,
65
+ * router.ts and rules.ts's MM009 each grew their own slightly different copy (one required a full
66
+ * day, one skipped numbered lists, one had an explicit checkbox branch this pattern's lazy
67
+ * 0-24-character window already covers). This is the widest of the three: numbered lists
68
+ * (`1.`/`1)`), a month-only date, and up to 24 characters (a checkbox, a bold marker, ...) before
69
+ * the date all count.
70
+ */
71
+ export declare const RE_DATED_BULLET: RegExp;
72
+ /**
73
+ * Is this body of lines "episodic": mostly a list of dated entries (a changelog, a session log)?
74
+ * At least 3 dated bullets, and at least half of all bullets dated. The one rule every episodic-
75
+ * detection call site in the codebase now shares, so "how many dated entries make a section a
76
+ * changelog" has one answer instead of three.
77
+ */
78
+ export declare function isEpisodicBody(lines: string[]): boolean;
79
+ export interface Volatility {
80
+ score: number;
81
+ /** distinct kinds of evidence found, in a fixed order, for messages */
82
+ reasons: string[];
83
+ }
84
+ /**
85
+ * Score a run of lines. Pass the heading line as `heading` when the lines are a section body;
86
+ * pass an empty string for a headingless block. A heading with no body is never volatile: a
87
+ * title cannot be edited often when there is nothing under it to edit.
88
+ */
89
+ export declare function volatility(heading: string, bodyLines: string[]): Volatility;
90
+ /** Reasons a headingless block is volatile; empty when it is not. */
91
+ export declare function volatilityReasons(text: string): string[];
92
+ /** Reasons a heading-led section is volatile; empty when it is not. */
93
+ export declare function sectionVolatilityReasons(sectionLines: string[]): string[];
94
+ /** Delimiters around the generated regions of a stub: the OnDemandMemory list and the instruction block. */
95
+ export declare const LIST_START = "<!-- minnimemory:ondemand-list -->";
96
+ export declare const LIST_END = "<!-- /minnimemory:ondemand-list -->";
97
+ export declare const INSTRUCTIONS_START = "<!-- minnimemory:instructions -->";
98
+ export declare const INSTRUCTIONS_END = "<!-- /minnimemory:instructions -->";
99
+ /**
100
+ * Locate one generated region. Only a single well-formed pair counts: the first opening marker
101
+ * that is followed by a closing marker. An unclosed opener, a second pair, or a closer with no
102
+ * opener is ignored, so a marker pasted into prose cannot hide the rest of a file from the
103
+ * rules (security audit 2026-09-02, finding 5). Returns 0-based inclusive line indices.
104
+ */
105
+ export declare function generatedRegion(lines: string[], start: string, end: string): {
106
+ from: number;
107
+ to: number;
108
+ } | undefined;
109
+ /**
110
+ * Blank out the generated regions, line for line so line numbers are preserved. The
111
+ * OnDemandMemory list is a routing manifest: its trigger words ("changelog", "open items") are
112
+ * labels for OnDemandMemory files, not status, and it only changes when a file is added or
113
+ * removed. The instruction block is fixed text. Auditing either as prose would make `doctor`
114
+ * flag `init`'s own output.
115
+ */
116
+ export declare function stripGeneratedRegions(lines: string[]): string[];
117
+ /** Kept for callers that only know about the OnDemandMemory list region. */
118
+ export declare const stripGeneratedList: typeof stripGeneratedRegions;
119
+ /**
120
+ * The inverse of `stripGeneratedRegions`: blank every line outside one marked region, keeping
121
+ * the region's own lines verbatim at their real position. Line numbers stay meaningful for a
122
+ * region carved out of a larger always-loaded file (compile.ts's merged AlwaysOnMemory.md holds
123
+ * always-on prose and the OnDemandMemory list in one file; this is how a caller gets just the
124
+ * list's lines back without losing the line numbers a finding reports against the real file).
125
+ */
126
+ export declare function regionOnly(lines: string[], start: string, end: string): string[];
127
+ /**
128
+ * Leading YAML frontmatter: a `---` line first, closed by the next `---` line. Returned as the
129
+ * verbatim lines plus the 1-indexed line number where the document body begins. Frontmatter has
130
+ * to stay first in a file to remain frontmatter, so the compiler carries it through untouched
131
+ * instead of treating it as preamble prose.
132
+ */
133
+ export declare function splitFrontmatter(lines: string[]): {
134
+ frontmatter: string[];
135
+ bodyStartLine: number;
136
+ };
137
+ export declare const STOPWORDS: Set<string>;
138
+ /** A code token's raw frequency inside a fenced block, relative to a comment or prose word. */
139
+ export declare const CODE_WEIGHT = 0.2;
140
+ /**
141
+ * Split body text into (fragment, weight) pieces for keyword extraction: prose and `#` comment
142
+ * text keep weight 1, the command/flag/tool-name text around a comment inside a fenced code
143
+ * block is down-weighted, so a repeated tool name doesn't drown out a rare-but-specific comment
144
+ * word under raw frequency. Found live (2026-09-09): an OnDemandMemory file whose only mentions of "UAC" and
145
+ * "window" were in comments lost to "npm"/"venv"/"pester" appearing many times across unrelated
146
+ * commands sharing one section, so a question phrased the way a user actually asks it ("run
147
+ * without a UAC prompt") matched no trigger at all. Shared by `keywords()` below and
148
+ * `router.ts`'s `selectTriggers()`, the two places that rank candidate routing words this way.
149
+ */
150
+ export declare function weightedFragments(body: string): {
151
+ text: string;
152
+ weight: number;
153
+ }[];
154
+ /**
155
+ * Pull candidate routing keywords out of a piece of text.
156
+ * Frequency-ranked, stopword-filtered, and biased toward terms in the heading.
157
+ */
158
+ export declare function keywords(heading: string, body: string, limit?: number): string[];