@justyork/repo-mind 0.8.1 → 0.9.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.
Files changed (92) hide show
  1. package/README.md +29 -2
  2. package/dist/ask/ask.d.ts +7 -0
  3. package/dist/ask/ask.js +68 -0
  4. package/dist/ask/citations.d.ts +6 -0
  5. package/dist/ask/citations.js +40 -0
  6. package/dist/ask/config.d.ts +9 -0
  7. package/dist/ask/config.js +19 -0
  8. package/dist/ask/llm.d.ts +8 -0
  9. package/dist/ask/llm.js +110 -0
  10. package/dist/ask/prompt.d.ts +5 -0
  11. package/dist/ask/prompt.js +39 -0
  12. package/dist/ask/retrieve.d.ts +4 -0
  13. package/dist/ask/retrieve.js +38 -0
  14. package/dist/ask/search.d.ts +7 -0
  15. package/dist/ask/search.js +232 -0
  16. package/dist/ask/small-talk.d.ts +2 -0
  17. package/dist/ask/small-talk.js +35 -0
  18. package/dist/ask/suggestions.d.ts +12 -0
  19. package/dist/ask/suggestions.js +145 -0
  20. package/dist/ask/types.d.ts +27 -0
  21. package/dist/ask/types.js +1 -0
  22. package/dist/cli.js +6 -1
  23. package/dist/commands/export.d.ts +2 -0
  24. package/dist/commands/export.js +6 -4
  25. package/dist/commands/ui.js +6 -0
  26. package/dist/env/load-repomind-env.d.ts +10 -0
  27. package/dist/env/load-repomind-env.js +96 -0
  28. package/dist/ui/api-handlers.d.ts +1 -0
  29. package/dist/ui/api-handlers.js +84 -0
  30. package/dist/ui/draft-api.js +2 -2
  31. package/dist/ui/server.js +6 -1
  32. package/package.json +2 -1
  33. package/ui/dist/assets/{arc-BitGDA7a.js → arc-BN7_EkMD.js} +1 -1
  34. package/ui/dist/assets/{architectureDiagram-3BPJPVTR-BmY2OtU5.js → architectureDiagram-3BPJPVTR-B8enQ17Y.js} +1 -1
  35. package/ui/dist/assets/{blockDiagram-GPEHLZMM-btai_20t.js → blockDiagram-GPEHLZMM-BnmnlgNm.js} +1 -1
  36. package/ui/dist/assets/{c4Diagram-AAUBKEIU-CN-pXtFe.js → c4Diagram-AAUBKEIU-DoP4UJAJ.js} +1 -1
  37. package/ui/dist/assets/channel-DqyPrNMh.js +1 -0
  38. package/ui/dist/assets/{chunk-2J33WTMH-CUgwCKKP.js → chunk-2J33WTMH-Bls2Xdkl.js} +1 -1
  39. package/ui/dist/assets/{chunk-4BX2VUAB-ZXi72_qf.js → chunk-4BX2VUAB-DCIvYWrM.js} +1 -1
  40. package/ui/dist/assets/{chunk-55IACEB6-BvoNhEcH.js → chunk-55IACEB6-BZCrHrSY.js} +1 -1
  41. package/ui/dist/assets/{chunk-727SXJPM-CPVv-LcP.js → chunk-727SXJPM-CyTz7y8e.js} +1 -1
  42. package/ui/dist/assets/{chunk-AQP2D5EJ-15HMmfKe.js → chunk-AQP2D5EJ-kOonrdhC.js} +1 -1
  43. package/ui/dist/assets/{chunk-FMBD7UC4-CTJu1mOO.js → chunk-FMBD7UC4-BVCWYiIY.js} +1 -1
  44. package/ui/dist/assets/{chunk-ND2GUHAM-BaS37xMv.js → chunk-ND2GUHAM-CiV-9tNZ.js} +1 -1
  45. package/ui/dist/assets/{chunk-QZHKN3VN-CzNZR3D9.js → chunk-QZHKN3VN-DAimc2AE.js} +1 -1
  46. package/ui/dist/assets/classDiagram-4FO5ZUOK-CwifX_Y1.js +1 -0
  47. package/ui/dist/assets/classDiagram-v2-Q7XG4LA2-CwifX_Y1.js +1 -0
  48. package/ui/dist/assets/{cose-bilkent-S5V4N54A-mpC0WnuY.js → cose-bilkent-S5V4N54A-B4yubD-y.js} +1 -1
  49. package/ui/dist/assets/{dagre-BM42HDAG-DVbZMTFr.js → dagre-BM42HDAG-EoElLLGl.js} +1 -1
  50. package/ui/dist/assets/{diagram-2AECGRRQ-BvYsIaeC.js → diagram-2AECGRRQ-Df9wv121.js} +1 -1
  51. package/ui/dist/assets/{diagram-5GNKFQAL-mM_4t2hT.js → diagram-5GNKFQAL-Ck5dW8ww.js} +1 -1
  52. package/ui/dist/assets/{diagram-KO2AKTUF-u5t0FZtc.js → diagram-KO2AKTUF-DQqRKGqm.js} +1 -1
  53. package/ui/dist/assets/{diagram-LMA3HP47-Ngnghl4t.js → diagram-LMA3HP47-Dua6RRw8.js} +1 -1
  54. package/ui/dist/assets/{diagram-OG6HWLK6-DxCLvBmh.js → diagram-OG6HWLK6-B3Ogt3zv.js} +1 -1
  55. package/ui/dist/assets/{erDiagram-TEJ5UH35-Cblf5xKO.js → erDiagram-TEJ5UH35-BYdkXgPm.js} +1 -1
  56. package/ui/dist/assets/{flowDiagram-I6XJVG4X-3aVBpTd3.js → flowDiagram-I6XJVG4X-DxwAsXVj.js} +1 -1
  57. package/ui/dist/assets/{ganttDiagram-6RSMTGT7-CCk8vbkn.js → ganttDiagram-6RSMTGT7-CkvGosG3.js} +1 -1
  58. package/ui/dist/assets/{gitGraphDiagram-PVQCEYII-DLUIS6md.js → gitGraphDiagram-PVQCEYII-xeD8b-c2.js} +1 -1
  59. package/ui/dist/assets/{graph-DwaX8nDF.js → graph-D1eDjihg.js} +1 -1
  60. package/ui/dist/assets/{infoDiagram-5YYISTIA-C6umdFYV.js → infoDiagram-5YYISTIA-CZX2wTOb.js} +1 -1
  61. package/ui/dist/assets/{ishikawaDiagram-YF4QCWOH-H8wERm8_.js → ishikawaDiagram-YF4QCWOH-B97H92r3.js} +1 -1
  62. package/ui/dist/assets/{journeyDiagram-JHISSGLW-DnTRQ6Mn.js → journeyDiagram-JHISSGLW-Djcx9Sr4.js} +1 -1
  63. package/ui/dist/assets/{kanban-definition-UN3LZRKU-DrVEsZSL.js → kanban-definition-UN3LZRKU-DGmylaB2.js} +1 -1
  64. package/ui/dist/assets/main-B3H8-mmv.js +365 -0
  65. package/ui/dist/assets/{mermaid.core-BtOBEW5j.js → mermaid.core-CkbXj8L9.js} +4 -4
  66. package/ui/dist/assets/{mindmap-definition-RKZ34NQL-BFEjh9BY.js → mindmap-definition-RKZ34NQL-BxiFVhcQ.js} +1 -1
  67. package/ui/dist/assets/{pieDiagram-4H26LBE5-KLu3myN4.js → pieDiagram-4H26LBE5-CIAMX7m1.js} +1 -1
  68. package/ui/dist/assets/{quadrantDiagram-W4KKPZXB-DwSP1nfl.js → quadrantDiagram-W4KKPZXB-BjcX-J56.js} +1 -1
  69. package/ui/dist/assets/{requirementDiagram-4Y6WPE33-CTe59AWj.js → requirementDiagram-4Y6WPE33-DDYZmURd.js} +1 -1
  70. package/ui/dist/assets/{sankeyDiagram-5OEKKPKP-DBG4pZal.js → sankeyDiagram-5OEKKPKP-Dp3WtUXa.js} +1 -1
  71. package/ui/dist/assets/{sequenceDiagram-3UESZ5HK-BV9E_Iae.js → sequenceDiagram-3UESZ5HK-DQALrsRY.js} +1 -1
  72. package/ui/dist/assets/{stateDiagram-AJRCARHV-BWnAUqpA.js → stateDiagram-AJRCARHV-Bi2QeigX.js} +1 -1
  73. package/ui/dist/assets/stateDiagram-v2-BHNVJYJU-pbZREyTr.js +1 -0
  74. package/ui/dist/assets/theme-CROa1dkI.js +21 -0
  75. package/ui/dist/assets/theme-CiCs9Mn_.css +1 -0
  76. package/ui/dist/assets/{timeline-definition-PNZ67QCA-DDoGl25M.js → timeline-definition-PNZ67QCA-C8K5LzbR.js} +1 -1
  77. package/ui/dist/assets/{vennDiagram-CIIHVFJN-DVRPIJjW.js → vennDiagram-CIIHVFJN-Bf0WkuCL.js} +1 -1
  78. package/ui/dist/assets/visual-editor-B6GUowO4.js +177 -0
  79. package/ui/dist/assets/{wardley-L42UT6IY-Li08EMdE.js → wardley-L42UT6IY-R45K8ohg.js} +1 -1
  80. package/ui/dist/assets/{wardleyDiagram-YWT4CUSO-BA2E9m1f.js → wardleyDiagram-YWT4CUSO-CZNd6CHT.js} +1 -1
  81. package/ui/dist/assets/{xychartDiagram-2RQKCTM6-DKHRvUg5.js → xychartDiagram-2RQKCTM6-ClCu9u0t.js} +1 -1
  82. package/ui/dist/graph.html +14 -3
  83. package/ui/dist/index.html +14 -3
  84. package/ui/dist/theme-boot.js +16 -0
  85. package/ui/dist/assets/channel-D1plp4bS.js +0 -1
  86. package/ui/dist/assets/classDiagram-4FO5ZUOK-BsDWU9KJ.js +0 -1
  87. package/ui/dist/assets/classDiagram-v2-Q7XG4LA2-BsDWU9KJ.js +0 -1
  88. package/ui/dist/assets/main-C0Y60ppL.js +0 -269
  89. package/ui/dist/assets/stateDiagram-v2-BHNVJYJU-yNXWOXzs.js +0 -1
  90. package/ui/dist/assets/theme-CfkLgQFf.js +0 -21
  91. package/ui/dist/assets/theme-DJYNWCef.css +0 -1
  92. package/ui/dist/assets/visual-editor-COzWiJ8C.js +0 -220
package/README.md CHANGED
@@ -45,7 +45,7 @@ Then ask your agent a project question — it should call `search_docs` / `get_d
45
45
  | `repo-mind check` | Validate frontmatter schema and `related:` links |
46
46
  | `repo-mind prepare` | Add frontmatter to legacy markdown (`--all` for batch) |
47
47
  | `repo-mind sync-links` | Convert markdown links to wikilinks; sync `related:` |
48
- | `repo-mind export` | Write `agents.md` to repo root |
48
+ | `repo-mind export` | Write `agents-export.md` flat dump to repo root |
49
49
  | `repo-mind publish` | Publish active drafts to `docs/`; `--pr` opens a GitHub pull request |
50
50
  | `repo-mind mcp` | Start the MCP stdio server |
51
51
  | `repo-mind ui` | Confluence-style workspace over `docs/` (127.0.0.1:3847) |
@@ -64,17 +64,44 @@ Confluence-style workspace over **`docs/`**:
64
64
  npm run build
65
65
  repo-mind ui # http://127.0.0.1:3847
66
66
  repo-mind ui --port 4000 --cwd /path/to/project
67
+
68
+ # Local development (uses this repo's dist/, includes latest .env loader):
69
+ npm run ui
70
+ node dist/cli.js ui
67
71
  ```
68
72
 
73
+ `repo-mind ui` loads `REPOMIND_*` variables from the nearest `.env` (starting at cwd and the docs project root). If you installed `@justyork/repo-mind` globally, run `npm link` in this repo after `npm run build` so `repo-mind ui` uses the local build.
74
+
69
75
  - **Catalog tree** — docs by domain (`Product`, `Technical`, …) and type
70
76
  - **Prepare** — recursively find markdown without frontmatter; add schema in one click
71
77
  - **Drafts + Publish** — SQLite drafts, publish to `docs/*.md`
72
- - **Health** — schema check, publish queue, export `agents.md`
78
+ - **Health** — schema check, publish queue, export `agents-export.md`
73
79
  - **Graph** — full-page view at `/graph.html`
80
+ - **Ask** — documentation assistant (BYOK): floating chat button, natural-language Q&A with source links
74
81
  - **Theme** — light (default) / dark toggle
75
82
 
76
83
  Binds **127.0.0.1** only. MCP reads published files in `docs/` only (not SQLite drafts).
77
84
 
85
+ ### Ask assistant (BYOK)
86
+
87
+ Click the **chat icon** in the bottom-right corner to open the documentation assistant. Answers are grounded in published `docs/` via the same retrieval path as MCP (`search_docs` → `get_doc`). Each answer includes links to source pages (`?slug=`).
88
+
89
+ Configure your provider API key in **Ask → Settings** (stored in browser `localStorage` only) or set a team default:
90
+
91
+ Or set a team default in project `.env` (loaded automatically by `repo-mind ui`):
92
+
93
+ ```bash
94
+ REPOMIND_ASK_API_KEY=sk-...
95
+ REPOMIND_ASK_PROVIDER=openai
96
+ REPOMIND_UI_PASSWORD=your-password
97
+ ```
98
+
99
+ | Variable | Description |
100
+ |----------|-------------|
101
+ | `REPOMIND_ASK_API_KEY` | Optional server default API key |
102
+ | `REPOMIND_ASK_PROVIDER` | `openai` (default) or `anthropic` |
103
+ | `REPOMIND_ASK_MODEL` | Override model (default: `gpt-4o-mini` / `claude-3-5-haiku-latest`) |
104
+
78
105
  ## MCP tools
79
106
 
80
107
  - `list_docs` — filter by type, status, tag
@@ -0,0 +1,7 @@
1
+ import type { DocIndex } from '../index/doc-index.js';
2
+ import type { AskRequest, AskResponse, CompleteAskFn } from './types.js';
3
+ export interface AskDocsOptions {
4
+ completeFn?: CompleteAskFn;
5
+ }
6
+ /** Answers a documentation question using retrieval + optional LLM synthesis. */
7
+ export declare function askDocs(index: DocIndex, input: AskRequest, options?: AskDocsOptions): Promise<AskResponse>;
@@ -0,0 +1,68 @@
1
+ import { resolveAskApiKey } from './config.js';
2
+ import { ensureCitations } from './citations.js';
3
+ import { completeAsk, defaultModelForProvider, resolveAskProvider, } from './llm.js';
4
+ import { buildAskSystemPrompt, buildAskUserPrompt } from './prompt.js';
5
+ import { retrieveAskContext } from './retrieve.js';
6
+ import { buildAskGreetingReply, buildAskNotFoundReply } from './suggestions.js';
7
+ import { isAskSmallTalk } from './small-talk.js';
8
+ const MAX_HISTORY_TURNS = 6;
9
+ function resolveProvider(requestProvider) {
10
+ if (requestProvider) {
11
+ return requestProvider;
12
+ }
13
+ return resolveAskProvider(process.env.REPOMIND_ASK_PROVIDER);
14
+ }
15
+ function resolveModel(provider, requestModel) {
16
+ const trimmed = requestModel?.trim();
17
+ if (trimmed) {
18
+ return trimmed;
19
+ }
20
+ const fromEnv = process.env.REPOMIND_ASK_MODEL?.trim();
21
+ if (fromEnv) {
22
+ return fromEnv;
23
+ }
24
+ return defaultModelForProvider(provider);
25
+ }
26
+ function buildAskMessages(question, sources, history) {
27
+ const messages = [{ role: 'system', content: buildAskSystemPrompt(sources.length > 0) }];
28
+ for (const turn of (history ?? []).slice(-MAX_HISTORY_TURNS)) {
29
+ messages.push({ role: turn.role, content: turn.content });
30
+ }
31
+ messages.push({ role: 'user', content: buildAskUserPrompt(question, sources) });
32
+ return messages;
33
+ }
34
+ /** Answers a documentation question using retrieval + optional LLM synthesis. */
35
+ export async function askDocs(index, input, options = {}) {
36
+ const question = input.question?.trim() ?? '';
37
+ if (!question) {
38
+ throw new Error('question is required');
39
+ }
40
+ const apiKey = resolveAskApiKey(input.apiKey);
41
+ if (!apiKey) {
42
+ throw new Error('API key is required — set it in Ask settings or REPOMIND_ASK_API_KEY');
43
+ }
44
+ const sources = retrieveAskContext(index, question);
45
+ if (sources.length === 0 && isAskSmallTalk(question)) {
46
+ return {
47
+ answer: buildAskGreetingReply(index, question),
48
+ sources: [],
49
+ notFound: false,
50
+ };
51
+ }
52
+ if (sources.length === 0) {
53
+ return {
54
+ answer: buildAskNotFoundReply(index, question),
55
+ sources: [],
56
+ notFound: true,
57
+ };
58
+ }
59
+ const provider = resolveProvider(input.provider);
60
+ const model = resolveModel(provider, input.model);
61
+ const completeFn = options.completeFn ?? completeAsk;
62
+ const rawAnswer = await completeFn(provider, apiKey, model, buildAskMessages(question, sources, input.history));
63
+ return {
64
+ answer: sources.length > 0 ? ensureCitations(rawAnswer, sources) : rawAnswer,
65
+ sources,
66
+ notFound: sources.length === 0,
67
+ };
68
+ }
@@ -0,0 +1,6 @@
1
+ import type { AskSource } from './types.js';
2
+ /** Extracts slug values from markdown links using ?slug= query params. */
3
+ export declare function extractCitedSlugs(markdown: string): string[];
4
+ export declare function hasCitationForSources(answer: string, sources: AskSource[]): boolean;
5
+ export declare function appendSourcesBlock(answer: string, sources: AskSource[]): string;
6
+ export declare function ensureCitations(answer: string, sources: AskSource[]): string;
@@ -0,0 +1,40 @@
1
+ const SLUG_LINK_PATTERN = /\?slug=([^)\s"&]+)/gi;
2
+ /** Extracts slug values from markdown links using ?slug= query params. */
3
+ export function extractCitedSlugs(markdown) {
4
+ const slugs = new Set();
5
+ for (const match of markdown.matchAll(SLUG_LINK_PATTERN)) {
6
+ const raw = match[1];
7
+ if (!raw) {
8
+ continue;
9
+ }
10
+ try {
11
+ slugs.add(decodeURIComponent(raw));
12
+ }
13
+ catch {
14
+ slugs.add(raw);
15
+ }
16
+ }
17
+ return [...slugs];
18
+ }
19
+ export function hasCitationForSources(answer, sources) {
20
+ if (sources.length === 0) {
21
+ return true;
22
+ }
23
+ const cited = new Set(extractCitedSlugs(answer));
24
+ return sources.some((source) => cited.has(source.slug));
25
+ }
26
+ export function appendSourcesBlock(answer, sources) {
27
+ if (sources.length === 0) {
28
+ return answer;
29
+ }
30
+ const trimmed = answer.trimEnd();
31
+ const links = sources.map((source) => `- [${source.title}](?slug=${encodeURIComponent(source.slug)})`);
32
+ const block = ['', '## Sources', '', ...links].join('\n');
33
+ return `${trimmed}${block}`;
34
+ }
35
+ export function ensureCitations(answer, sources) {
36
+ if (hasCitationForSources(answer, sources)) {
37
+ return answer;
38
+ }
39
+ return appendSourcesBlock(answer, sources);
40
+ }
@@ -0,0 +1,9 @@
1
+ import type { LlmProvider } from './types.js';
2
+ export interface AskServerConfig {
3
+ serverConfigured: boolean;
4
+ provider: LlmProvider;
5
+ model: string;
6
+ }
7
+ /** Public ask config for the UI (never exposes the API key). */
8
+ export declare function getAskServerConfig(): AskServerConfig;
9
+ export declare function resolveAskApiKey(requestKey: string | undefined): string | null;
@@ -0,0 +1,19 @@
1
+ import { defaultModelForProvider, resolveAskProvider } from './llm.js';
2
+ /** Public ask config for the UI (never exposes the API key). */
3
+ export function getAskServerConfig() {
4
+ const provider = resolveAskProvider(process.env.REPOMIND_ASK_PROVIDER);
5
+ const envModel = process.env.REPOMIND_ASK_MODEL?.trim();
6
+ return {
7
+ serverConfigured: Boolean(process.env.REPOMIND_ASK_API_KEY?.trim()),
8
+ provider,
9
+ model: envModel || defaultModelForProvider(provider),
10
+ };
11
+ }
12
+ export function resolveAskApiKey(requestKey) {
13
+ const fromEnv = process.env.REPOMIND_ASK_API_KEY?.trim();
14
+ if (fromEnv) {
15
+ return fromEnv;
16
+ }
17
+ const fromRequest = requestKey?.trim();
18
+ return fromRequest || null;
19
+ }
@@ -0,0 +1,8 @@
1
+ import type { CompleteAskFn, LlmMessage, LlmProvider } from './types.js';
2
+ export declare const DEFAULT_OPENAI_MODEL = "gpt-4o-mini";
3
+ export declare const DEFAULT_ANTHROPIC_MODEL = "claude-3-5-haiku-latest";
4
+ export declare function defaultModelForProvider(provider: LlmProvider): string;
5
+ export declare function resolveAskProvider(value: string | undefined): LlmProvider;
6
+ export declare function callOpenAi(apiKey: string, model: string, messages: LlmMessage[]): Promise<string>;
7
+ export declare function callAnthropic(apiKey: string, model: string, messages: LlmMessage[]): Promise<string>;
8
+ export declare const completeAsk: CompleteAskFn;
@@ -0,0 +1,110 @@
1
+ const OPENAI_URL = 'https://api.openai.com/v1/chat/completions';
2
+ const ANTHROPIC_URL = 'https://api.anthropic.com/v1/messages';
3
+ const ANTHROPIC_VERSION = '2023-06-01';
4
+ export const DEFAULT_OPENAI_MODEL = 'gpt-4o-mini';
5
+ export const DEFAULT_ANTHROPIC_MODEL = 'claude-3-5-haiku-latest';
6
+ export function defaultModelForProvider(provider) {
7
+ switch (provider) {
8
+ case 'openai':
9
+ return DEFAULT_OPENAI_MODEL;
10
+ case 'anthropic':
11
+ return DEFAULT_ANTHROPIC_MODEL;
12
+ default: {
13
+ const _exhaustive = provider;
14
+ return _exhaustive;
15
+ }
16
+ }
17
+ }
18
+ export function resolveAskProvider(value) {
19
+ if (value === 'anthropic') {
20
+ return 'anthropic';
21
+ }
22
+ return 'openai';
23
+ }
24
+ async function readApiError(res) {
25
+ try {
26
+ const body = (await res.json());
27
+ if (typeof body.error === 'string') {
28
+ return body.error;
29
+ }
30
+ if (body.error && typeof body.error.message === 'string') {
31
+ return body.error.message;
32
+ }
33
+ }
34
+ catch {
35
+ // ignore parse errors
36
+ }
37
+ return `HTTP ${res.status}`;
38
+ }
39
+ export async function callOpenAi(apiKey, model, messages) {
40
+ const res = await fetch(OPENAI_URL, {
41
+ method: 'POST',
42
+ headers: {
43
+ Authorization: `Bearer ${apiKey}`,
44
+ 'Content-Type': 'application/json',
45
+ },
46
+ body: JSON.stringify({
47
+ model,
48
+ messages,
49
+ temperature: 0.4,
50
+ }),
51
+ });
52
+ if (!res.ok) {
53
+ throw new Error(`OpenAI API error: ${await readApiError(res)}`);
54
+ }
55
+ const data = (await res.json());
56
+ const content = data.choices?.[0]?.message?.content?.trim();
57
+ if (!content) {
58
+ throw new Error('OpenAI API returned an empty response');
59
+ }
60
+ return content;
61
+ }
62
+ export async function callAnthropic(apiKey, model, messages) {
63
+ const system = messages.find((message) => message.role === 'system')?.content ?? '';
64
+ const chatMessages = messages
65
+ .filter((message) => message.role !== 'system')
66
+ .map((message) => ({
67
+ role: message.role === 'assistant' ? 'assistant' : 'user',
68
+ content: message.content,
69
+ }));
70
+ const res = await fetch(ANTHROPIC_URL, {
71
+ method: 'POST',
72
+ headers: {
73
+ 'x-api-key': apiKey,
74
+ 'anthropic-version': ANTHROPIC_VERSION,
75
+ 'Content-Type': 'application/json',
76
+ },
77
+ body: JSON.stringify({
78
+ model,
79
+ max_tokens: 2048,
80
+ system,
81
+ messages: chatMessages,
82
+ temperature: 0.4,
83
+ }),
84
+ });
85
+ if (!res.ok) {
86
+ throw new Error(`Anthropic API error: ${await readApiError(res)}`);
87
+ }
88
+ const data = (await res.json());
89
+ const text = data.content
90
+ ?.filter((block) => block.type === 'text')
91
+ .map((block) => block.text ?? '')
92
+ .join('')
93
+ .trim();
94
+ if (!text) {
95
+ throw new Error('Anthropic API returned an empty response');
96
+ }
97
+ return text;
98
+ }
99
+ export const completeAsk = async (provider, apiKey, model, messages) => {
100
+ switch (provider) {
101
+ case 'openai':
102
+ return callOpenAi(apiKey, model, messages);
103
+ case 'anthropic':
104
+ return callAnthropic(apiKey, model, messages);
105
+ default: {
106
+ const _exhaustive = provider;
107
+ return _exhaustive;
108
+ }
109
+ }
110
+ };
@@ -0,0 +1,5 @@
1
+ import type { AskSource } from './types.js';
2
+ export declare const NOT_FOUND_ANSWER_RU = "\u041D\u0435 \u043D\u0430\u0448\u0451\u043B \u0432 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0430\u0446\u0438\u0438.";
3
+ export declare const NOT_FOUND_ANSWER_EN = "Not found in the documentation.";
4
+ export declare function buildAskSystemPrompt(hasSources: boolean): string;
5
+ export declare function buildAskUserPrompt(question: string, sources: AskSource[]): string;
@@ -0,0 +1,39 @@
1
+ export const NOT_FOUND_ANSWER_RU = 'Не нашёл в документации.';
2
+ export const NOT_FOUND_ANSWER_EN = 'Not found in the documentation.';
3
+ export function buildAskSystemPrompt(hasSources) {
4
+ const lines = [
5
+ 'You are a helpful documentation assistant for RepoMind.',
6
+ 'Answer in a clear, conversational tone.',
7
+ 'Reply in the same language as the user question.',
8
+ ];
9
+ if (hasSources) {
10
+ lines.push('Use ONLY the provided source excerpts to answer documentation questions.', 'Synthesize a helpful explanation for the user — do not dump raw excerpts or bullet lists of sources without explanation.', 'If the sources do not contain enough information, say clearly that you could not find it in the documentation.', 'Always cite sources inline using markdown links: [Page Title](?slug=slug-name).', 'Use the exact slug from each source block.', 'Do not invent facts, APIs, or pages that are not in the sources.');
11
+ }
12
+ else {
13
+ lines.push('No documentation excerpts were retrieved for this message.', 'If the user is greeting you or making small talk, reply briefly and explain that you can answer questions about the project documentation in docs/.', 'If they asked a documentation question, say you could not find relevant pages and suggest more specific keywords.', 'Do not invent documentation content.');
14
+ }
15
+ return lines.join(' ');
16
+ }
17
+ export function buildAskUserPrompt(question, sources) {
18
+ if (sources.length === 0) {
19
+ return [
20
+ '## Sources',
21
+ '',
22
+ '_No matching documentation pages were retrieved._',
23
+ '',
24
+ '## Question',
25
+ '',
26
+ question.trim(),
27
+ ].join('\n');
28
+ }
29
+ const blocks = sources.map((source) => `### ${source.title} (slug: ${source.slug})\n\n${source.excerpt}`);
30
+ return [
31
+ '## Sources',
32
+ '',
33
+ blocks.join('\n\n---\n\n'),
34
+ '',
35
+ '## Question',
36
+ '',
37
+ question.trim(),
38
+ ].join('\n');
39
+ }
@@ -0,0 +1,4 @@
1
+ import type { DocIndex } from '../index/doc-index.js';
2
+ import type { AskSource } from './types.js';
3
+ /** Retrieves top matching published docs for an ask question. */
4
+ export declare function retrieveAskContext(index: DocIndex, question: string): AskSource[];
@@ -0,0 +1,38 @@
1
+ import { getDoc } from '../tools/get-doc.js';
2
+ import { searchDocsForAsk } from './search.js';
3
+ const TOP_N = 5;
4
+ const EXCERPT_CHARS = 1200;
5
+ function excerpt(body, maxChars = EXCERPT_CHARS) {
6
+ const trimmed = body.trim();
7
+ if (trimmed.length <= maxChars) {
8
+ return trimmed;
9
+ }
10
+ return `${trimmed.slice(0, maxChars)}…`;
11
+ }
12
+ function titleFromDoc(slug, frontmatter) {
13
+ if (typeof frontmatter?.title === 'string' && frontmatter.title.trim()) {
14
+ return frontmatter.title.trim();
15
+ }
16
+ return slug;
17
+ }
18
+ /** Retrieves top matching published docs for an ask question. */
19
+ export function retrieveAskContext(index, question) {
20
+ const query = question.trim();
21
+ if (!query) {
22
+ return [];
23
+ }
24
+ const hits = searchDocsForAsk(index, query);
25
+ const sources = [];
26
+ for (const hit of hits.slice(0, TOP_N)) {
27
+ const doc = getDoc(index, hit.slug);
28
+ if (!doc.found || !doc.body) {
29
+ continue;
30
+ }
31
+ sources.push({
32
+ slug: hit.slug,
33
+ title: titleFromDoc(hit.slug, doc.frontmatter),
34
+ excerpt: excerpt(doc.body),
35
+ });
36
+ }
37
+ return sources;
38
+ }
@@ -0,0 +1,7 @@
1
+ import type { DocIndex } from '../index/doc-index.js';
2
+ import { type SearchDocsResult } from '../tools/search-docs.js';
3
+ /** Extracts meaningful search terms from a natural-language question. */
4
+ export declare function extractAskSearchTerms(question: string): string[];
5
+ export declare function searchDocsWithOrTerms(index: DocIndex, terms: string[]): SearchDocsResult[];
6
+ /** Relaxed search tuned for natural-language ask questions. */
7
+ export declare function searchDocsForAsk(index: DocIndex, question: string): SearchDocsResult[];
@@ -0,0 +1,232 @@
1
+ import { searchDocs } from '../tools/search-docs.js';
2
+ const STOP_WORDS = new Set([
3
+ 'a',
4
+ 'an',
5
+ 'the',
6
+ 'is',
7
+ 'are',
8
+ 'was',
9
+ 'were',
10
+ 'be',
11
+ 'been',
12
+ 'being',
13
+ 'what',
14
+ 'how',
15
+ 'why',
16
+ 'when',
17
+ 'where',
18
+ 'who',
19
+ 'which',
20
+ 'do',
21
+ 'does',
22
+ 'did',
23
+ 'can',
24
+ 'could',
25
+ 'should',
26
+ 'would',
27
+ 'will',
28
+ 'to',
29
+ 'of',
30
+ 'in',
31
+ 'on',
32
+ 'at',
33
+ 'for',
34
+ 'and',
35
+ 'or',
36
+ 'but',
37
+ 'not',
38
+ 'with',
39
+ 'about',
40
+ 'from',
41
+ 'into',
42
+ 'me',
43
+ 'my',
44
+ 'i',
45
+ 'you',
46
+ 'your',
47
+ 'we',
48
+ 'our',
49
+ 'it',
50
+ 'its',
51
+ 'this',
52
+ 'that',
53
+ 'these',
54
+ 'those',
55
+ 'there',
56
+ 'tell',
57
+ 'please',
58
+ 'explain',
59
+ 'describe',
60
+ 'как',
61
+ 'что',
62
+ 'где',
63
+ 'когда',
64
+ 'почему',
65
+ 'кто',
66
+ 'какой',
67
+ 'какая',
68
+ 'какие',
69
+ 'какое',
70
+ 'каков',
71
+ 'какова',
72
+ 'каково',
73
+ 'это',
74
+ 'этот',
75
+ 'эта',
76
+ 'эти',
77
+ 'тот',
78
+ 'та',
79
+ 'те',
80
+ 'в',
81
+ 'во',
82
+ 'на',
83
+ 'с',
84
+ 'со',
85
+ 'по',
86
+ 'для',
87
+ 'из',
88
+ 'у',
89
+ 'о',
90
+ 'об',
91
+ 'от',
92
+ 'до',
93
+ 'при',
94
+ 'про',
95
+ 'над',
96
+ 'под',
97
+ 'и',
98
+ 'или',
99
+ 'не',
100
+ 'ни',
101
+ 'ли',
102
+ 'же',
103
+ 'бы',
104
+ 'мне',
105
+ 'меня',
106
+ 'мой',
107
+ 'моя',
108
+ 'мои',
109
+ 'твой',
110
+ 'твоя',
111
+ 'твои',
112
+ 'наш',
113
+ 'наша',
114
+ 'наши',
115
+ 'расскажи',
116
+ 'рассказать',
117
+ 'объясни',
118
+ 'объяснить',
119
+ 'покажи',
120
+ 'показать',
121
+ 'найди',
122
+ 'найти',
123
+ 'есть',
124
+ 'быть',
125
+ 'был',
126
+ 'была',
127
+ 'были',
128
+ ]);
129
+ const TERM_PATTERN = /[a-z0-9\u0400-\u04ff]+/gi;
130
+ function uniqueTerms(terms) {
131
+ const seen = new Set();
132
+ const result = [];
133
+ for (const term of terms) {
134
+ const normalized = term.toLowerCase();
135
+ if (!normalized || seen.has(normalized)) {
136
+ continue;
137
+ }
138
+ seen.add(normalized);
139
+ result.push(normalized);
140
+ }
141
+ return result;
142
+ }
143
+ /** Extracts meaningful search terms from a natural-language question. */
144
+ export function extractAskSearchTerms(question) {
145
+ const raw = question.match(TERM_PATTERN) ?? [];
146
+ const meaningful = raw.filter((term) => term.length >= 3 && !STOP_WORDS.has(term.toLowerCase()));
147
+ if (meaningful.length > 0) {
148
+ return uniqueTerms(meaningful);
149
+ }
150
+ return uniqueTerms(raw.filter((term) => term.length >= 2 && !STOP_WORDS.has(term.toLowerCase())));
151
+ }
152
+ function scoreDocForTerms(doc, terms) {
153
+ const titleLower = doc.title.toLowerCase();
154
+ const bodyLower = doc.body.toLowerCase();
155
+ const tagsLower = doc.tags.map((tag) => tag.toLowerCase());
156
+ let score = 0;
157
+ let matches = 0;
158
+ for (const term of terms) {
159
+ let termScore = 0;
160
+ if (tagsLower.some((tag) => tag.includes(term))) {
161
+ termScore = 3;
162
+ }
163
+ if (titleLower.includes(term)) {
164
+ termScore = Math.max(termScore, 2);
165
+ }
166
+ if (bodyLower.includes(term)) {
167
+ termScore = Math.max(termScore, 1);
168
+ }
169
+ if (termScore > 0) {
170
+ matches += 1;
171
+ score += termScore;
172
+ }
173
+ }
174
+ if (matches === 0) {
175
+ return null;
176
+ }
177
+ if (matches === terms.length) {
178
+ score += 2;
179
+ }
180
+ return score;
181
+ }
182
+ export function searchDocsWithOrTerms(index, terms) {
183
+ if (terms.length === 0) {
184
+ return [];
185
+ }
186
+ const docs = index.refresh();
187
+ const results = [];
188
+ for (const doc of docs) {
189
+ const score = scoreDocForTerms(doc, terms);
190
+ if (score === null) {
191
+ continue;
192
+ }
193
+ const snippetSource = terms.find((term) => doc.body.toLowerCase().includes(term)) ?? terms[0];
194
+ const lowerBody = doc.body.toLowerCase();
195
+ const indexOf = lowerBody.indexOf(snippetSource);
196
+ const snippet = indexOf >= 0
197
+ ? doc.body
198
+ .slice(Math.max(0, indexOf - 40), Math.min(doc.body.length, indexOf + snippetSource.length + 40))
199
+ .replace(/\s+/g, ' ')
200
+ .trim()
201
+ : doc.body.slice(0, 120).replace(/\s+/g, ' ').trim();
202
+ results.push({
203
+ slug: doc.slug,
204
+ title: doc.title,
205
+ snippet,
206
+ score,
207
+ });
208
+ }
209
+ return results
210
+ .sort((a, b) => b.score - a.score || a.slug.localeCompare(b.slug))
211
+ .slice(0, 20);
212
+ }
213
+ /** Relaxed search tuned for natural-language ask questions. */
214
+ export function searchDocsForAsk(index, question) {
215
+ const trimmed = question.trim();
216
+ if (!trimmed) {
217
+ return [];
218
+ }
219
+ const strict = searchDocs(index, { query: trimmed });
220
+ if (strict.length > 0) {
221
+ return strict;
222
+ }
223
+ const terms = extractAskSearchTerms(trimmed);
224
+ if (terms.length === 0) {
225
+ return [];
226
+ }
227
+ const andQuery = searchDocs(index, { query: terms.join(' ') });
228
+ if (andQuery.length > 0) {
229
+ return andQuery;
230
+ }
231
+ return searchDocsWithOrTerms(index, terms);
232
+ }
@@ -0,0 +1,2 @@
1
+ /** Detects short greetings or small talk that should not trigger doc-not-found. */
2
+ export declare function isAskSmallTalk(question: string): boolean;