@plebeiantech/lat.md 0.12.3-fork.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 (144) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -0
  3. package/dist/src/cli/check-frontmatter.d.ts +12 -0
  4. package/dist/src/cli/check-frontmatter.js +44 -0
  5. package/dist/src/cli/check-mode.d.ts +6 -0
  6. package/dist/src/cli/check-mode.js +303 -0
  7. package/dist/src/cli/check-status.d.ts +74 -0
  8. package/dist/src/cli/check-status.js +217 -0
  9. package/dist/src/cli/check.d.ts +45 -0
  10. package/dist/src/cli/check.js +777 -0
  11. package/dist/src/cli/checklist-menu.d.ts +12 -0
  12. package/dist/src/cli/checklist-menu.js +111 -0
  13. package/dist/src/cli/comment-guard.d.ts +16 -0
  14. package/dist/src/cli/comment-guard.js +111 -0
  15. package/dist/src/cli/comment-reminder.d.ts +62 -0
  16. package/dist/src/cli/comment-reminder.js +210 -0
  17. package/dist/src/cli/context.d.ts +11 -0
  18. package/dist/src/cli/context.js +58 -0
  19. package/dist/src/cli/expand.d.ts +8 -0
  20. package/dist/src/cli/expand.js +94 -0
  21. package/dist/src/cli/gen-index.d.ts +45 -0
  22. package/dist/src/cli/gen-index.js +235 -0
  23. package/dist/src/cli/gen.d.ts +6 -0
  24. package/dist/src/cli/gen.js +42 -0
  25. package/dist/src/cli/graph.d.ts +7 -0
  26. package/dist/src/cli/graph.js +47 -0
  27. package/dist/src/cli/hook.d.ts +1 -0
  28. package/dist/src/cli/hook.js +432 -0
  29. package/dist/src/cli/index.d.ts +2 -0
  30. package/dist/src/cli/index.js +307 -0
  31. package/dist/src/cli/init.d.ts +9 -0
  32. package/dist/src/cli/init.js +1031 -0
  33. package/dist/src/cli/link-scheme.d.ts +54 -0
  34. package/dist/src/cli/link-scheme.js +101 -0
  35. package/dist/src/cli/locate.d.ts +2 -0
  36. package/dist/src/cli/locate.js +17 -0
  37. package/dist/src/cli/refs.d.ts +22 -0
  38. package/dist/src/cli/refs.js +229 -0
  39. package/dist/src/cli/reindex.d.ts +13 -0
  40. package/dist/src/cli/reindex.js +136 -0
  41. package/dist/src/cli/search.d.ts +36 -0
  42. package/dist/src/cli/search.js +217 -0
  43. package/dist/src/cli/section.d.ts +43 -0
  44. package/dist/src/cli/section.js +252 -0
  45. package/dist/src/cli/select-menu.d.ts +13 -0
  46. package/dist/src/cli/select-menu.js +100 -0
  47. package/dist/src/cli/templates.d.ts +1 -0
  48. package/dist/src/cli/templates.js +15 -0
  49. package/dist/src/cli/ui.d.ts +11 -0
  50. package/dist/src/cli/ui.js +34 -0
  51. package/dist/src/code-refs.d.ts +17 -0
  52. package/dist/src/code-refs.js +277 -0
  53. package/dist/src/config.d.ts +29 -0
  54. package/dist/src/config.js +83 -0
  55. package/dist/src/context.d.ts +23 -0
  56. package/dist/src/context.js +11 -0
  57. package/dist/src/extensions/wiki-link/from-markdown.d.ts +5 -0
  58. package/dist/src/extensions/wiki-link/from-markdown.js +34 -0
  59. package/dist/src/extensions/wiki-link/index.d.ts +4 -0
  60. package/dist/src/extensions/wiki-link/index.js +3 -0
  61. package/dist/src/extensions/wiki-link/syntax.d.ts +14 -0
  62. package/dist/src/extensions/wiki-link/syntax.js +121 -0
  63. package/dist/src/extensions/wiki-link/to-markdown.d.ts +5 -0
  64. package/dist/src/extensions/wiki-link/to-markdown.js +28 -0
  65. package/dist/src/extensions/wiki-link/types.d.ts +34 -0
  66. package/dist/src/extensions/wiki-link/types.js +1 -0
  67. package/dist/src/format.d.ts +8 -0
  68. package/dist/src/format.js +43 -0
  69. package/dist/src/graph-export.d.ts +60 -0
  70. package/dist/src/graph-export.js +309 -0
  71. package/dist/src/init-version.d.ts +10 -0
  72. package/dist/src/init-version.js +49 -0
  73. package/dist/src/knowledge/bd.d.ts +2 -0
  74. package/dist/src/knowledge/bd.js +107 -0
  75. package/dist/src/knowledge/claude-memory.d.ts +13 -0
  76. package/dist/src/knowledge/claude-memory.js +204 -0
  77. package/dist/src/knowledge/cq.d.ts +2 -0
  78. package/dist/src/knowledge/cq.js +74 -0
  79. package/dist/src/knowledge/index.d.ts +49 -0
  80. package/dist/src/knowledge/index.js +271 -0
  81. package/dist/src/knowledge/ranking.d.ts +17 -0
  82. package/dist/src/knowledge/ranking.js +62 -0
  83. package/dist/src/knowledge/session.d.ts +46 -0
  84. package/dist/src/knowledge/session.js +203 -0
  85. package/dist/src/knowledge/types.d.ts +47 -0
  86. package/dist/src/knowledge/types.js +10 -0
  87. package/dist/src/lattice.d.ts +108 -0
  88. package/dist/src/lattice.js +816 -0
  89. package/dist/src/mcp/server.d.ts +1 -0
  90. package/dist/src/mcp/server.js +58 -0
  91. package/dist/src/parser.d.ts +3 -0
  92. package/dist/src/parser.js +18 -0
  93. package/dist/src/search/db.d.ts +16 -0
  94. package/dist/src/search/db.js +54 -0
  95. package/dist/src/search/embedder.d.ts +25 -0
  96. package/dist/src/search/embedder.js +58 -0
  97. package/dist/src/search/index.d.ts +9 -0
  98. package/dist/src/search/index.js +66 -0
  99. package/dist/src/search/search.d.ts +9 -0
  100. package/dist/src/search/search.js +16 -0
  101. package/dist/src/source-parser.d.ts +27 -0
  102. package/dist/src/source-parser.js +916 -0
  103. package/dist/src/untrusted.d.ts +24 -0
  104. package/dist/src/untrusted.js +97 -0
  105. package/dist/src/version.d.ts +7 -0
  106. package/dist/src/version.js +39 -0
  107. package/dist/src/view/client/assets/index-B0n0u03y.css +1 -0
  108. package/dist/src/view/client/assets/index-xm2_px4q.js +23 -0
  109. package/dist/src/view/client/index.html +14 -0
  110. package/dist/src/view/diagnostics.d.ts +5 -0
  111. package/dist/src/view/diagnostics.js +171 -0
  112. package/dist/src/view/git-diff.d.ts +3 -0
  113. package/dist/src/view/git-diff.js +255 -0
  114. package/dist/src/view/git.d.ts +22 -0
  115. package/dist/src/view/git.js +193 -0
  116. package/dist/src/view/highlight.d.ts +2 -0
  117. package/dist/src/view/highlight.js +3767 -0
  118. package/dist/src/view/markdown.d.ts +22 -0
  119. package/dist/src/view/markdown.js +322 -0
  120. package/dist/src/view/protocol.d.ts +89 -0
  121. package/dist/src/view/protocol.js +1 -0
  122. package/dist/src/view/references.d.ts +62 -0
  123. package/dist/src/view/references.js +345 -0
  124. package/dist/src/view/repository.d.ts +12 -0
  125. package/dist/src/view/repository.js +167 -0
  126. package/dist/src/view/search.d.ts +14 -0
  127. package/dist/src/view/search.js +64 -0
  128. package/dist/src/view/server.d.ts +20 -0
  129. package/dist/src/view/server.js +233 -0
  130. package/dist/src/view/store.d.ts +57 -0
  131. package/dist/src/view/store.js +404 -0
  132. package/dist/src/walk.d.ts +18 -0
  133. package/dist/src/walk.js +27 -0
  134. package/package.json +85 -0
  135. package/templates/AGENTS.md +118 -0
  136. package/templates/README +1 -0
  137. package/templates/cursor-rules.md +77 -0
  138. package/templates/init/lat.md +1 -0
  139. package/templates/logo-dark.svg +182 -0
  140. package/templates/logo-light.svg +182 -0
  141. package/templates/logo.txt +7 -0
  142. package/templates/opencode-plugin.ts +180 -0
  143. package/templates/pi-extension.ts +364 -0
  144. package/templates/skill/SKILL.md +189 -0
@@ -0,0 +1,94 @@
1
+ import { join, relative } from 'node:path';
2
+ import { loadAllSections, findSections, } from '../lattice.js';
3
+ import { UNTRUSTED_NOTICE, quoteUntrusted, cleanUntrustedId, } from '../untrusted.js';
4
+ const WIKI_LINK_RE = /\[\[([^\]]+)\]\]/g;
5
+ function formatLocation(section, projectRoot) {
6
+ const relPath = relative(process.cwd(), join(projectRoot, section.filePath));
7
+ return `${cleanUntrustedId(relPath)}:${section.startLine}-${section.endLine}`;
8
+ }
9
+ /**
10
+ * Resolve [[refs]] in text and return the expanded output.
11
+ * Returns null if there are no wiki links, or if resolution fails.
12
+ */
13
+ export async function expandPrompt(ctx, text, preloadedSections) {
14
+ const refs = [...text.matchAll(WIKI_LINK_RE)];
15
+ if (refs.length === 0)
16
+ return null;
17
+ const allSections = preloadedSections ?? (await loadAllSections(ctx.latDir));
18
+ const resolved = new Map();
19
+ const errors = [];
20
+ for (const match of refs) {
21
+ const target = match[1];
22
+ if (resolved.has(target))
23
+ continue;
24
+ const matches = findSections(allSections, target);
25
+ if (matches.length >= 1) {
26
+ resolved.set(target, {
27
+ target,
28
+ best: matches[0],
29
+ alternatives: matches.slice(1),
30
+ });
31
+ }
32
+ else {
33
+ errors.push(`No section found for [[${target}]]`);
34
+ }
35
+ }
36
+ if (errors.length > 0)
37
+ return null;
38
+ // Replace [[refs]] inline
39
+ let output = text.replace(WIKI_LINK_RE, (_match, target) => {
40
+ const ref = resolved.get(target);
41
+ return `[[${cleanUntrustedId(ref.best.section.id)}]]`;
42
+ });
43
+ // Append context block as nested outliner
44
+ output += '\n\n<lat-context>\n';
45
+ output += UNTRUSTED_NOTICE + '\n';
46
+ for (const ref of resolved.values()) {
47
+ const isExact = ref.best.reason === 'exact match' ||
48
+ ref.best.reason.startsWith('file stem expanded');
49
+ const all = isExact ? [ref.best] : [ref.best, ...ref.alternatives];
50
+ if (isExact) {
51
+ output += `* \`[[${ref.target}]]\` is referring to:\n`;
52
+ }
53
+ else {
54
+ output += `* \`[[${ref.target}]]\` might be referring to either of the following:\n`;
55
+ }
56
+ for (const m of all) {
57
+ const reason = isExact ? '' : ` (${cleanUntrustedId(m.reason)})`;
58
+ output += ` * [[${cleanUntrustedId(m.section.id)}]]${reason}\n`;
59
+ output += ` * ${formatLocation(m.section, ctx.projectRoot)}\n`;
60
+ if (m.section.firstParagraph) {
61
+ output += ` * ${quoteUntrusted(m.section.firstParagraph)}\n`;
62
+ }
63
+ }
64
+ }
65
+ output += '</lat-context>\n';
66
+ return output;
67
+ }
68
+ export async function expandCommand(ctx, text) {
69
+ const result = await expandPrompt(ctx, text);
70
+ if (result === null) {
71
+ const refs = [...text.matchAll(WIKI_LINK_RE)];
72
+ if (refs.length === 0) {
73
+ return { output: text };
74
+ }
75
+ // Resolution failed — find which ref is broken
76
+ const allSections = await loadAllSections(ctx.latDir);
77
+ for (const match of refs) {
78
+ const target = match[1];
79
+ const matches = findSections(allSections, target);
80
+ if (matches.length === 0) {
81
+ const s = ctx.styler;
82
+ return {
83
+ output: s.red(`No section found for [[${target}]]`) +
84
+ ' (no exact, substring, or fuzzy matches).\n' +
85
+ s.dim('Ask the user to correct the reference.'),
86
+ isError: true,
87
+ };
88
+ }
89
+ }
90
+ // All refs matched individually but expansion still failed — shouldn't happen
91
+ return { output: text };
92
+ }
93
+ return { output: result };
94
+ }
@@ -0,0 +1,45 @@
1
+ export declare function mdEscapeLabel(value: string): string;
2
+ export declare function mdEscapeDest(value: string): string;
3
+ export type IndexEntrySource = {
4
+ /** Filesystem entry name: "notes.md" for a file, "guides" for a directory. */
5
+ name: string;
6
+ /**
7
+ * Link destination relative to the index file being written: the child's
8
+ * own filename ("notes.md"), or its own index file for a directory
9
+ * ("guides/guides.md"). This is what actually resolves on disk — unlike
10
+ * the wiki-link stem, it keeps the .md extension.
11
+ */
12
+ dest: string;
13
+ /** Absolute path to read the title/summary from (the file itself, or a directory's own index file). */
14
+ readFrom: string;
15
+ };
16
+ /**
17
+ * Render the generated bullet list for a directory index, one entry per
18
+ * child, sorted by title. Each entry links through the wiki-link stem
19
+ * (`entryToStem`-compatible) using a real Markdown link so the title can
20
+ * carry a human-written summary — which is exactly why it must be escaped:
21
+ * the title is repository text landing inside `[...]`.
22
+ */
23
+ export declare function renderIndexEntries(entries: IndexEntrySource[]): Promise<string>;
24
+ /**
25
+ * Compute, for each line, whether it lies inside a fenced code block.
26
+ * Marker and bullet-run detection must skip fenced lines — otherwise a
27
+ * documentation example that shows the marker syntax (or a sample bullet
28
+ * list) inside a ``` block gets mistaken for the real thing and rewritten.
29
+ */
30
+ export declare function fencedLineMask(lines: string[]): boolean[];
31
+ export type SpliceResult = {
32
+ ok: true;
33
+ content: string;
34
+ } | {
35
+ ok: false;
36
+ message: string;
37
+ };
38
+ /**
39
+ * Splice a freshly rendered entry list into an index file's content inside
40
+ * `<!-- lat:index:begin -->` / `<!-- lat:index:end -->` markers, preserving
41
+ * everything outside that region byte for byte. An unmarked file is
42
+ * migrated by wrapping the first contiguous run of local index entry
43
+ * bullets; a malformed marker pair is refused rather than guessed at.
44
+ */
45
+ export declare function spliceIndexContent(existingContent: string | null, dirLabel: string, renderedList: string): SpliceResult;
@@ -0,0 +1,235 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { cleanUntrusted } from '../untrusted.js';
3
+ import { isLatticeLocalDest } from './link-scheme.js';
4
+ // Mirrors the two escaping rules in hooks/generate_docs_index.rb (see
5
+ // lat-t1y.5). Both were found by a working exploit against that generator,
6
+ // and the same exploit applies here: an index entry's title is repository
7
+ // text (an H1 heading or a leading paragraph) placed into a generated
8
+ // Markdown link, so it must be treated as untrusted the moment it is quoted.
9
+ /**
10
+ * Escape a link LABEL. Without this, a title of `Real](https://evil/)
11
+ * [ignore` closes the generated link early and reopens it against an
12
+ * attacker's URL, leaving the real target as the label of a second,
13
+ * harmless-looking link. Backslash-escaping is CommonMark's own answer: each
14
+ * of these renders as the literal character.
15
+ */
16
+ const MD_LABEL_SPECIALS = /([\\[\]()`])/g;
17
+ export function mdEscapeLabel(value) {
18
+ return value.replace(MD_LABEL_SPECIALS, '\\$1');
19
+ }
20
+ /**
21
+ * Escape a link DESTINATION. A destination does not escape the same way a
22
+ * label does: a `)` anywhere ends the destination there, and CommonMark reads
23
+ * a bare `\` in a destination as an escape for the punctuation after it.
24
+ * Percent-encoding survives the round trip; backslash-escaping does not,
25
+ * because a renderer that decodes `%5C` back to `\` does not then treat it as
26
+ * an escape character again.
27
+ *
28
+ * `%` itself must be in the class too, or the encoding is not injective: a
29
+ * file genuinely named `a%28b.md` would be emitted verbatim and then decoded
30
+ * by the renderer to `a(b.md` — a link to a path that does not exist.
31
+ */
32
+ const MD_DEST_SPECIALS = /[()<>\s%\\]/g;
33
+ export function mdEscapeDest(value) {
34
+ return value.replace(MD_DEST_SPECIALS, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0'));
35
+ }
36
+ function fallbackTitle(name) {
37
+ const stem = name.endsWith('.md') ? name.slice(0, -3) : name;
38
+ return stem.replace(/[-_]/g, ' ');
39
+ }
40
+ /**
41
+ * Pull a title (first `# ` heading) and a summary (the paragraph that
42
+ * follows it, or the document's own leading paragraph when it has no
43
+ * heading) out of a document's raw text. Deliberately simple line scanning
44
+ * rather than a full parse — good enough for index generation, and it avoids
45
+ * depending on parseSections' heading-to-id machinery here.
46
+ */
47
+ async function titleAndSummary(readFrom, fallback) {
48
+ let content;
49
+ try {
50
+ content = await readFile(readFrom, 'utf-8');
51
+ }
52
+ catch {
53
+ return { title: fallback, summary: '(no summary yet)' };
54
+ }
55
+ const stripped = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '');
56
+ const lines = stripped.split(/\r?\n/);
57
+ let idx = 0;
58
+ while (idx < lines.length && lines[idx].trim() === '')
59
+ idx++;
60
+ let title = fallback;
61
+ if (idx < lines.length && /^#\s+\S/.test(lines[idx])) {
62
+ title = lines[idx].replace(/^#\s+/, '').trim();
63
+ idx++;
64
+ }
65
+ while (idx < lines.length && lines[idx].trim() === '')
66
+ idx++;
67
+ const paraLines = [];
68
+ while (idx < lines.length &&
69
+ lines[idx].trim() !== '' &&
70
+ !/^#{1,6}\s/.test(lines[idx]) &&
71
+ !/^- /.test(lines[idx])) {
72
+ paraLines.push(lines[idx]);
73
+ idx++;
74
+ }
75
+ const summary = paraLines.join(' ').trim();
76
+ return {
77
+ title: cleanUntrusted(title || fallback, 120),
78
+ summary: summary ? cleanUntrusted(summary, 300) : '(no summary yet)',
79
+ };
80
+ }
81
+ /**
82
+ * Render the generated bullet list for a directory index, one entry per
83
+ * child, sorted by title. Each entry links through the wiki-link stem
84
+ * (`entryToStem`-compatible) using a real Markdown link so the title can
85
+ * carry a human-written summary — which is exactly why it must be escaped:
86
+ * the title is repository text landing inside `[...]`.
87
+ */
88
+ export async function renderIndexEntries(entries) {
89
+ const rendered = await Promise.all(entries.map(async (entry) => {
90
+ const { title, summary } = await titleAndSummary(entry.readFrom, fallbackTitle(entry.name));
91
+ return {
92
+ title,
93
+ line: `- [${mdEscapeLabel(title)}](${mdEscapeDest(entry.dest)}) — ${mdEscapeLabel(summary)}`,
94
+ };
95
+ }));
96
+ rendered.sort((a, b) => a.title.toLowerCase().localeCompare(b.title.toLowerCase()));
97
+ return rendered.map((r) => r.line).join('\n');
98
+ }
99
+ /** HTML comment markers delimiting the region `--fix` is allowed to rewrite. */
100
+ const BEGIN_MARKER = '<!-- lat:index:begin -->';
101
+ const END_MARKER = '<!-- lat:index:end -->';
102
+ // Tolerant of a trailing \r so a CRLF file's marker lines still match —
103
+ // `existingContent` is otherwise split on plain '\n' (see the module doc).
104
+ const BEGIN_RE = /^<!-- lat:index:begin -->\r?$/;
105
+ const END_RE = /^<!-- lat:index:end -->\r?$/;
106
+ /**
107
+ * True when `line` is a "local index entry bullet" per the shared predicate
108
+ * documented in link-scheme.ts: the legacy wiki form, or a Markdown link
109
+ * bullet whose destination names a child of this directory. This MUST agree
110
+ * with `parseIndexEntries` in check.ts — both read the same bullets, and a
111
+ * second, differently-behaved copy of the rule is exactly how they drift.
112
+ */
113
+ function isIndexEntryBulletLine(line) {
114
+ const trimmed = line.replace(/\r$/, '');
115
+ if (/^- \[\[/.test(trimmed))
116
+ return true;
117
+ const m = /^- \[(?:\\.|[^\]])*\]\(([^)]*)\)/.exec(trimmed);
118
+ if (!m)
119
+ return false;
120
+ return isLatticeLocalDest(m[1]);
121
+ }
122
+ /** A fenced code block delimiter line (opening or closing ``` or ~~~). */
123
+ const FENCE_RE = /^ {0,3}(`{3,}|~{3,})/;
124
+ /**
125
+ * Compute, for each line, whether it lies inside a fenced code block.
126
+ * Marker and bullet-run detection must skip fenced lines — otherwise a
127
+ * documentation example that shows the marker syntax (or a sample bullet
128
+ * list) inside a ``` block gets mistaken for the real thing and rewritten.
129
+ */
130
+ export function fencedLineMask(lines) {
131
+ const mask = new Array(lines.length).fill(false);
132
+ let inFence = false;
133
+ for (let i = 0; i < lines.length; i++) {
134
+ const isFenceDelim = FENCE_RE.test(lines[i].replace(/\r$/, ''));
135
+ if (isFenceDelim) {
136
+ // The delimiter line itself counts as "inside" so it can never be
137
+ // mistaken for a marker or entry bullet either.
138
+ mask[i] = true;
139
+ inFence = !inFence;
140
+ }
141
+ else {
142
+ mask[i] = inFence;
143
+ }
144
+ }
145
+ return mask;
146
+ }
147
+ /**
148
+ * Splice a freshly rendered entry list into an index file's content inside
149
+ * `<!-- lat:index:begin -->` / `<!-- lat:index:end -->` markers, preserving
150
+ * everything outside that region byte for byte. An unmarked file is
151
+ * migrated by wrapping the first contiguous run of local index entry
152
+ * bullets; a malformed marker pair is refused rather than guessed at.
153
+ */
154
+ export function spliceIndexContent(existingContent, dirLabel, renderedList) {
155
+ const block = `${BEGIN_MARKER}\n${renderedList}\n${END_MARKER}`;
156
+ if (existingContent === null) {
157
+ return { ok: true, content: `${dirLabel} directory index.\n\n${block}\n` };
158
+ }
159
+ const lines = existingContent.replace(/\n$/, '').split('\n');
160
+ const fenced = fencedLineMask(lines);
161
+ const beginIdx = lines.findIndex((l, i) => !fenced[i] && BEGIN_RE.test(l));
162
+ // The end marker that matters is the first one AFTER begin (per the
163
+ // design contract). Fall back to the first end anywhere so an end marker
164
+ // with no begin at all is still detected and reported.
165
+ let endIdx = -1;
166
+ if (beginIdx !== -1) {
167
+ for (let i = beginIdx + 1; i < lines.length; i++) {
168
+ if (!fenced[i] && END_RE.test(lines[i])) {
169
+ endIdx = i;
170
+ break;
171
+ }
172
+ }
173
+ }
174
+ else {
175
+ endIdx = lines.findIndex((l, i) => !fenced[i] && END_RE.test(l));
176
+ }
177
+ if (beginIdx !== -1 || endIdx !== -1) {
178
+ if (beginIdx === -1) {
179
+ return {
180
+ ok: false,
181
+ message: 'found a "lat:index:end" marker with no matching "lat:index:begin" marker — fix the markers by hand before running --fix again',
182
+ };
183
+ }
184
+ if (endIdx === -1) {
185
+ return {
186
+ ok: false,
187
+ message: 'found a "lat:index:begin" marker with no matching "lat:index:end" marker — fix the markers by hand before running --fix again',
188
+ };
189
+ }
190
+ if (endIdx < beginIdx) {
191
+ return {
192
+ ok: false,
193
+ message: 'found a "lat:index:end" marker before the "lat:index:begin" marker — fix the markers by hand before running --fix again',
194
+ };
195
+ }
196
+ const before = lines.slice(0, beginIdx);
197
+ const after = lines.slice(endIdx + 1);
198
+ const rebuilt = [...before, block, ...after].join('\n');
199
+ return { ok: true, content: `${rebuilt}\n` };
200
+ }
201
+ // No markers at all: migrate. Find the first contiguous run of local index
202
+ // entry bullets and replace exactly that run with the marked block.
203
+ let runStart = -1;
204
+ let runEnd = -1;
205
+ for (let i = 0; i < lines.length; i++) {
206
+ if (!fenced[i] && isIndexEntryBulletLine(lines[i])) {
207
+ if (runStart === -1)
208
+ runStart = i;
209
+ runEnd = i;
210
+ }
211
+ else if (runStart !== -1) {
212
+ break;
213
+ }
214
+ }
215
+ if (runStart !== -1) {
216
+ const before = lines.slice(0, runStart);
217
+ const after = lines.slice(runEnd + 1);
218
+ const rebuilt = [...before, block, ...after].join('\n');
219
+ return { ok: true, content: `${rebuilt}\n` };
220
+ }
221
+ // No entry-bullet run: insert before the first heading, or append at the end.
222
+ const headingIdx = lines.findIndex((l, i) => !fenced[i] && /^#{2,6}\s/.test(l));
223
+ if (headingIdx !== -1) {
224
+ const before = lines.slice(0, headingIdx);
225
+ while (before.length > 0 && before[before.length - 1].trim() === '') {
226
+ before.pop();
227
+ }
228
+ const after = lines.slice(headingIdx);
229
+ const rebuilt = [...before, '', block, '', ...after].join('\n');
230
+ return { ok: true, content: `${rebuilt}\n` };
231
+ }
232
+ const trimmedExisting = lines.join('\n').replace(/\s+$/, '');
233
+ const rebuilt = trimmedExisting.length > 0 ? `${trimmedExisting}\n\n${block}` : block;
234
+ return { ok: true, content: `${rebuilt}\n` };
235
+ }
@@ -0,0 +1,6 @@
1
+ export declare function readAgentsTemplate(): string;
2
+ export declare function readCursorRulesTemplate(): string;
3
+ export declare function readPiExtensionTemplate(): string;
4
+ export declare function readOpenCodePluginTemplate(): string;
5
+ export declare function readSkillTemplate(): string;
6
+ export declare function genCmd(target: string): Promise<void>;
@@ -0,0 +1,42 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { findTemplatesDir } from './templates.js';
4
+ export function readAgentsTemplate() {
5
+ return readFileSync(join(findTemplatesDir(), 'AGENTS.md'), 'utf-8');
6
+ }
7
+ export function readCursorRulesTemplate() {
8
+ return readFileSync(join(findTemplatesDir(), 'cursor-rules.md'), 'utf-8');
9
+ }
10
+ export function readPiExtensionTemplate() {
11
+ return readFileSync(join(findTemplatesDir(), 'pi-extension.ts'), 'utf-8');
12
+ }
13
+ export function readOpenCodePluginTemplate() {
14
+ return readFileSync(join(findTemplatesDir(), 'opencode-plugin.ts'), 'utf-8');
15
+ }
16
+ export function readSkillTemplate() {
17
+ return readFileSync(join(findTemplatesDir(), 'skill', 'SKILL.md'), 'utf-8');
18
+ }
19
+ export async function genCmd(target) {
20
+ const normalized = target.toLowerCase();
21
+ switch (normalized) {
22
+ case 'agents.md':
23
+ case 'claude.md':
24
+ process.stdout.write(readAgentsTemplate());
25
+ break;
26
+ case 'cursor-rules.md':
27
+ process.stdout.write(readCursorRulesTemplate());
28
+ break;
29
+ case 'pi-extension.ts':
30
+ process.stdout.write(readPiExtensionTemplate());
31
+ break;
32
+ case 'opencode-plugin.ts':
33
+ process.stdout.write(readOpenCodePluginTemplate());
34
+ break;
35
+ case 'skill.md':
36
+ process.stdout.write(readSkillTemplate());
37
+ break;
38
+ default:
39
+ console.error(`Unknown target: ${target}. Supported: agents.md, claude.md, cursor-rules.md, pi-extension.ts, opencode-plugin.ts, skill.md`);
40
+ process.exit(1);
41
+ }
42
+ }
@@ -0,0 +1,7 @@
1
+ import type { CmdContext, CmdResult } from '../context.js';
2
+ export type GraphOptions = {
3
+ format?: string;
4
+ at?: string;
5
+ since?: string;
6
+ };
7
+ export declare function graphCommand(ctx: CmdContext, opts: GraphOptions): Promise<CmdResult>;
@@ -0,0 +1,47 @@
1
+ import { buildGraph, diffGraphs, formatGraph, formatGraphDiff, loadRevisionFiles, loadWorkingTreeFiles, } from '../graph-export.js';
2
+ function parseFormat(raw) {
3
+ if (raw === 'mermaid' || raw === 'dot' || raw === 'json')
4
+ return raw;
5
+ if (raw !== undefined) {
6
+ throw new Error(`unknown --format "${raw}" — use one of: json, mermaid, dot`);
7
+ }
8
+ return 'json';
9
+ }
10
+ export async function graphCommand(ctx, opts) {
11
+ let format;
12
+ try {
13
+ format = parseFormat(opts.format);
14
+ }
15
+ catch (err) {
16
+ return { output: err.message, isError: true };
17
+ }
18
+ if (opts.at && opts.since) {
19
+ return {
20
+ output: 'error: `--at` and `--since` cannot be used together',
21
+ isError: true,
22
+ };
23
+ }
24
+ try {
25
+ if (opts.since) {
26
+ const beforeFiles = await loadRevisionFiles(ctx.projectRoot, ctx.latDir, opts.since);
27
+ const afterFiles = await loadWorkingTreeFiles(ctx.latDir, ctx.projectRoot);
28
+ const before = await buildGraph(beforeFiles, ctx.projectRoot, false);
29
+ const after = await buildGraph(afterFiles, ctx.projectRoot, false);
30
+ return { output: formatGraphDiff(diffGraphs(before, after)) };
31
+ }
32
+ if (opts.at) {
33
+ const files = await loadRevisionFiles(ctx.projectRoot, ctx.latDir, opts.at);
34
+ const graph = await buildGraph(files, ctx.projectRoot, false);
35
+ return { output: formatGraph(graph, format) };
36
+ }
37
+ const files = await loadWorkingTreeFiles(ctx.latDir, ctx.projectRoot);
38
+ const graph = await buildGraph(files, ctx.projectRoot, true);
39
+ return { output: formatGraph(graph, format) };
40
+ }
41
+ catch (err) {
42
+ return {
43
+ output: `error: ${err instanceof Error ? err.message : String(err)}`,
44
+ isError: true,
45
+ };
46
+ }
47
+ }
@@ -0,0 +1 @@
1
+ export declare function hookCmd(agent: string, event: string): Promise<void>;