@hevmind/ask 0.1.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/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # @hevmind/ask
2
+
3
+ hev ask is a heading-anchored search overlay for Astro docs sites. Typing runs
4
+ instant keyword search; pressing `Enter` runs an optional Claude search loop that
5
+ chooses sub-queries and ranks section results.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ pnpm add @hevmind/ask
11
+ ```
12
+
13
+ For the current GitHub-hosted monorepo package before npm publication:
14
+
15
+ ```sh
16
+ pnpm add "git+ssh://git@github.com/hev/ask.git#main&path:/packages/ui"
17
+ ```
18
+
19
+ ## Configure
20
+
21
+ ```js
22
+ // astro.config.mjs
23
+ import { defineConfig } from 'astro/config';
24
+ import hevAsk from '@hevmind/ask';
25
+
26
+ export default defineConfig({
27
+ integrations: [
28
+ hevAsk({
29
+ collections: ['docs'],
30
+ basePath: '/docs/',
31
+ }),
32
+ ],
33
+ });
34
+ ```
35
+
36
+ | Option | Default | Description |
37
+ | --- | --- | --- |
38
+ | `collections` | - | Content collections to index. |
39
+ | `model` | `claude-haiku-4-5` | Runtime search-loop model. |
40
+ | `endpoint` | `/api/ask` | Injected on-demand route. |
41
+ | `basePath` | `/docs/` | Turns a doc slug into its page URL. |
42
+ | `maxResults` | `6` | Max results returned. |
43
+ | `maxIterations` | `4` | Max search-loop rounds. |
44
+ | `chunkHeadingDepth` | `3` | Chunk at `##` through this heading depth. |
45
+ | `candidatePerSearch` | `8` | Chunks returned by each search tool call. |
46
+ | `perDocCap` | `2` | Max chunks per document in one prefilter call. |
47
+ | `digestModel` | `claude-opus-4-8` | Offline digest build model. |
48
+ | `digestPath` | `.hev-ask/digest.json` | Committed digest artifact path. |
49
+ | `digestContentGlobs` | derived from `collections` | Build-time Markdown/MDX corpus globs. |
50
+
51
+ ## Add the overlay
52
+
53
+ ```astro
54
+ ---
55
+ import SearchOverlay from '@hevmind/ask/components/SearchOverlay.astro';
56
+ ---
57
+ <button data-hev-ask-open>Search <kbd>⌘K</kbd></button>
58
+
59
+ <!-- once per page, e.g. at the end of your layout -->
60
+ <SearchOverlay />
61
+ ```
62
+
63
+ Open with `⌘K` / `Ctrl+K`, or `/`. Any element with `data-hev-ask-open` also
64
+ opens it. Typing returns keyword results immediately. Press `Enter` to ask AI,
65
+ or move the selection with arrows/hover and press `Enter` to open a keyword hit.
66
+
67
+ ## Ask digest
68
+
69
+ ```sh
70
+ ask digest build
71
+ ask digest verify
72
+ ```
73
+
74
+ The builder writes `.hev-ask/digest.json`, which should be committed. Builds are
75
+ hash-gated, so unchanged content does not spend another Opus call. `verify`
76
+ builds the site and checks that every chunk anchor exists in `dist`.
77
+
78
+ hev ask uses `github-slugger` to match Astro heading anchors exactly.
79
+
80
+ Recommended CI gates:
81
+
82
+ ```sh
83
+ pnpm test
84
+ pnpm typecheck
85
+ pnpm build
86
+ pnpm digest:verify
87
+ ```
88
+
89
+ ## Publishing
90
+
91
+ This package is intended to publish as `@hevmind/ask`. Before publishing, bump the
92
+ version, run the verification gates, inspect `pnpm --filter @hevmind/ask pack
93
+ --dry-run`, then publish from this package directory with:
94
+
95
+ ```sh
96
+ pnpm publish --access public
97
+ ```
98
+
99
+ After publish, consumers should depend on the npm semver range instead of the
100
+ Git `path:/packages/ui` dependency.
101
+
102
+ Git dependencies are acceptable for local integration while the package is not
103
+ yet published, but they are not the long-term distribution path.
104
+
105
+ ## Server Requirements
106
+
107
+ - Set `ANTHROPIC_API_KEY` for AI search and fresh digest generation.
108
+ - Without a runtime key, `/api/ask` still serves keyword results.
109
+ - The search route is rendered on demand, so the site needs a server adapter in
110
+ production.
111
+
112
+ ## Theming
113
+
114
+ The overlay reads your site's CSS custom properties with dark fallbacks:
115
+ `--paper` (background), `--ink` (text), `--muted`, `--signal` (accent), and
116
+ `--font-mono`. Define these on `:root` to match your brand.
@@ -0,0 +1,110 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const require = createRequire(import.meta.url);
8
+
9
+ export async function runAsk(args, options = {}) {
10
+ const target = resolveAskTarget();
11
+ if (!target) {
12
+ const platform = `${process.platform}/${process.arch}`;
13
+ console.error(
14
+ `[hev-ask] No ask binary is available for ${platform}. ` +
15
+ 'Install a package with @hevmind/ask optional binaries, set HEV_ASK_BINARY, or run from a source checkout with Go installed.',
16
+ );
17
+ return 1;
18
+ }
19
+
20
+ return run(target.command, [...target.args, ...args], {
21
+ cwd: target.cwd,
22
+ env: options.env ?? process.env,
23
+ });
24
+ }
25
+
26
+ function resolveAskTarget() {
27
+ const explicit = process.env.HEV_ASK_BINARY;
28
+ if (explicit) return { command: explicit, args: [] };
29
+
30
+ const packaged = resolvePackagedBinary();
31
+ if (packaged) return { command: packaged, args: [] };
32
+
33
+ const sourceRoot = findSourceRoot();
34
+ if (sourceRoot) return { command: 'go', args: ['run', path.join(sourceRoot, 'cmd', 'ask')], cwd: process.cwd() };
35
+
36
+ return null;
37
+ }
38
+
39
+ function resolvePackagedBinary() {
40
+ const packageName = platformPackageName();
41
+ if (!packageName) return null;
42
+ try {
43
+ const packageJson = require.resolve(`${packageName}/package.json`);
44
+ const candidate = path.join(path.dirname(packageJson), 'bin', executableName());
45
+ return existsSync(candidate) ? candidate : null;
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ function platformPackageName() {
52
+ const platform = process.platform;
53
+ const arch = process.arch;
54
+ if (platform === 'darwin' && arch === 'arm64') return '@hevmind/ask-darwin-arm64';
55
+ if (platform === 'darwin' && arch === 'x64') return '@hevmind/ask-darwin-x64';
56
+ if (platform === 'linux' && arch === 'arm64') return '@hevmind/ask-linux-arm64';
57
+ if (platform === 'linux' && arch === 'x64') return '@hevmind/ask-linux-x64';
58
+ if (platform === 'win32' && arch === 'x64') return '@hevmind/ask-win32-x64';
59
+ return null;
60
+ }
61
+
62
+ function executableName() {
63
+ return process.platform === 'win32' ? 'ask.exe' : 'ask';
64
+ }
65
+
66
+ function findSourceRoot() {
67
+ let current = path.dirname(fileURLToPath(import.meta.url));
68
+ for (let i = 0; i < 8; i += 1) {
69
+ const goMod = path.join(current, 'go.mod');
70
+ const main = path.join(current, 'cmd', 'ask', 'main.go');
71
+ if (existsSync(goMod) && existsSync(main) && isHevAskModule(goMod)) return current;
72
+ const parent = path.dirname(current);
73
+ if (parent === current) break;
74
+ current = parent;
75
+ }
76
+ return null;
77
+ }
78
+
79
+ function isHevAskModule(goMod) {
80
+ try {
81
+ return readFileSync(goMod, 'utf8').includes('module github.com/hev/ask');
82
+ } catch {
83
+ return false;
84
+ }
85
+ }
86
+
87
+ function run(command, args, options) {
88
+ return new Promise((resolve) => {
89
+ const child = spawn(command, args, {
90
+ cwd: options.cwd,
91
+ env: options.env,
92
+ stdio: 'inherit',
93
+ windowsHide: true,
94
+ });
95
+
96
+ child.on('error', (err) => {
97
+ console.error(`[hev-ask] Could not start ${command}: ${err.message}`);
98
+ resolve(1);
99
+ });
100
+
101
+ child.on('exit', (code, signal) => {
102
+ if (signal) {
103
+ process.kill(process.pid, signal);
104
+ resolve(1);
105
+ return;
106
+ }
107
+ resolve(code ?? 1);
108
+ });
109
+ });
110
+ }
package/bin/ask.mjs ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { runAsk } from './ask-launcher.mjs';
3
+
4
+ process.exitCode = await runAsk(process.argv.slice(2));
package/openapi.yaml ADDED
@@ -0,0 +1,363 @@
1
+ openapi: 3.1.0
2
+ info:
3
+ title: hev ask API
4
+ version: 3.0.0
5
+ summary: Search, answer, and digest read API exposed by the @hevmind/ask Astro integration.
6
+ description: |
7
+ `@hevmind/ask` mounts these routes on a consuming Astro site (default base `/api/ask`,
8
+ configurable via the integration's `endpoint` option). Two paths existed in v2:
9
+ keyword + agentic **search** (`POST /api/ask`) and **suggestions** (`GET /api/ask`).
10
+ v3 adds keyless **read** routes over the committed ask digest
11
+ (`/api/ask/glossary`, `/api/ask/sections`, `/api/ask/overview`) so a coding agent —
12
+ via the `ask` CLI, the MCP server, or a generated client — can query the docs
13
+ directly.
14
+
15
+ Degradation: with no `ANTHROPIC_API_KEY` configured on the server, `POST /api/ask`
16
+ falls back to keyword mode (HTTP 200 with a `warning`). The read routes never call a
17
+ model and never require a key.
18
+ license:
19
+ name: MIT
20
+ servers:
21
+ - url: https://askhev.com
22
+ description: The hev ask docs site (dogfoods @hevmind/ask).
23
+ - url: "{origin}"
24
+ description: Any site running the integration.
25
+ variables:
26
+ origin:
27
+ default: http://localhost:4321
28
+ tags:
29
+ - name: search
30
+ description: Keyword and agentic search/answer.
31
+ - name: digest
32
+ description: Keyless reads over the committed ask digest.
33
+
34
+ paths:
35
+ /api/ask:
36
+ get:
37
+ tags: [search]
38
+ operationId: getSuggestions
39
+ summary: Suggested questions and active model
40
+ description: |
41
+ Returns the model-authored example questions baked into the committed graph,
42
+ shown by the overlay on open. Keyless — no model call.
43
+ responses:
44
+ "200":
45
+ description: Suggestions and the configured answer model.
46
+ content:
47
+ application/json:
48
+ schema:
49
+ $ref: "#/components/schemas/SuggestionsResponse"
50
+ post:
51
+ tags: [search]
52
+ operationId: ask
53
+ summary: Search (keyword JSON) or answer (agentic SSE)
54
+ description: |
55
+ With `mode: "keyword"` (or when no API key is configured) returns ranked keyword
56
+ results as JSON. With `mode: "agentic"` and a configured key, returns a
57
+ Server-Sent Events stream of the grounded answer.
58
+
59
+ The SSE stream emits these named events, each with a JSON `data` payload:
60
+ - `sources` — `{ sources: Source[], model, mode: "agentic" }`
61
+ - `search` — `{ query }` (a sub-query the loop issued)
62
+ - `token` — `{ text }` (a chunk of the streamed answer)
63
+ - `done` — `{}`
64
+ - `error` — `{ error }` (failures after the stream has started)
65
+ requestBody:
66
+ required: true
67
+ content:
68
+ application/json:
69
+ schema:
70
+ $ref: "#/components/schemas/AskRequest"
71
+ responses:
72
+ "200":
73
+ description: |
74
+ Keyword results (JSON) or the agentic answer stream (SSE), depending on `mode`
75
+ and server key configuration.
76
+ content:
77
+ application/json:
78
+ schema:
79
+ $ref: "#/components/schemas/KeywordResponse"
80
+ text/event-stream:
81
+ schema:
82
+ type: string
83
+ description: SSE stream; see operation description for event types.
84
+ "400":
85
+ description: Invalid JSON body.
86
+ content:
87
+ application/json:
88
+ schema:
89
+ $ref: "#/components/schemas/Error"
90
+ "500":
91
+ description: Index build failure (e.g. misconfigured collections).
92
+ content:
93
+ application/json:
94
+ schema:
95
+ $ref: "#/components/schemas/Error"
96
+
97
+ /api/ask/glossary:
98
+ get:
99
+ tags: [digest]
100
+ operationId: listGlossary
101
+ summary: List glossary terms
102
+ description: All glossary entries from the committed graph. Keyless.
103
+ responses:
104
+ "200":
105
+ description: The glossary.
106
+ content:
107
+ application/json:
108
+ schema:
109
+ type: object
110
+ required: [terms]
111
+ properties:
112
+ terms:
113
+ type: array
114
+ items: { $ref: "#/components/schemas/GlossaryEntry" }
115
+
116
+ /api/ask/glossary/{term}:
117
+ get:
118
+ tags: [digest]
119
+ operationId: getGlossaryTerm
120
+ summary: Get one glossary entry
121
+ description: Matches case-insensitively on the term or any of its aliases.
122
+ parameters:
123
+ - $ref: "#/components/parameters/Term"
124
+ responses:
125
+ "200":
126
+ description: The matched glossary entry.
127
+ content:
128
+ application/json:
129
+ schema: { $ref: "#/components/schemas/GlossaryEntry" }
130
+ "404":
131
+ description: No term or alias matched.
132
+ content:
133
+ application/json:
134
+ schema: { $ref: "#/components/schemas/Error" }
135
+
136
+ /api/ask/sections:
137
+ get:
138
+ tags: [digest]
139
+ operationId: listSections
140
+ summary: List section nodes
141
+ description: |
142
+ A lightweight listing of every section node in the graph. Use `section get` /
143
+ `GET /api/ask/sections/{id}` for the full node with facts and sources.
144
+ parameters:
145
+ - name: group
146
+ in: query
147
+ required: false
148
+ schema: { type: string }
149
+ description: Filter to sections in this group (e.g. `API`).
150
+ responses:
151
+ "200":
152
+ description: Section summaries.
153
+ content:
154
+ application/json:
155
+ schema:
156
+ type: object
157
+ required: [sections]
158
+ properties:
159
+ sections:
160
+ type: array
161
+ items: { $ref: "#/components/schemas/SectionSummary" }
162
+
163
+ /api/ask/sections/{id}:
164
+ get:
165
+ tags: [digest]
166
+ operationId: getSection
167
+ summary: Get one section node
168
+ description: The full distilled node — summary, verbatim facts, sources, deep link.
169
+ parameters:
170
+ - name: id
171
+ in: path
172
+ required: true
173
+ schema: { type: string }
174
+ description: The section id, e.g. `concepts#the-agentic-loop`.
175
+ responses:
176
+ "200":
177
+ description: The section node.
178
+ content:
179
+ application/json:
180
+ schema: { $ref: "#/components/schemas/DigestNode" }
181
+ "404":
182
+ description: No node with that id.
183
+ content:
184
+ application/json:
185
+ schema: { $ref: "#/components/schemas/Error" }
186
+
187
+ /api/ask/overview:
188
+ get:
189
+ tags: [digest]
190
+ operationId: getOverview
191
+ summary: Grouped map + orientation
192
+ description: |
193
+ The deterministic grouped table of contents (`overview`) and the model-authored
194
+ prose orientation (`context`). The cheapest way for an agent to get its bearings.
195
+ responses:
196
+ "200":
197
+ description: Overview and context.
198
+ content:
199
+ application/json:
200
+ schema:
201
+ type: object
202
+ required: [overview, context]
203
+ properties:
204
+ overview: { type: string }
205
+ context: { type: string }
206
+
207
+ components:
208
+ parameters:
209
+ Term:
210
+ name: term
211
+ in: path
212
+ required: true
213
+ schema: { type: string }
214
+ description: A glossary term or alias (case-insensitive).
215
+
216
+ schemas:
217
+ AskRequest:
218
+ type: object
219
+ required: [query]
220
+ properties:
221
+ query:
222
+ type: string
223
+ description: The user's search text.
224
+ mode:
225
+ type: string
226
+ enum: [keyword, agentic]
227
+ default: keyword
228
+ description: |
229
+ `keyword` returns JSON results. `agentic` streams a grounded answer over SSE
230
+ (falls back to keyword JSON with a `warning` if the server has no API key).
231
+
232
+ KeywordResponse:
233
+ type: object
234
+ required: [results, query, model, mode]
235
+ properties:
236
+ results:
237
+ type: array
238
+ items: { $ref: "#/components/schemas/KeywordResult" }
239
+ query: { type: string }
240
+ model: { type: string }
241
+ mode:
242
+ type: string
243
+ enum: [keyword]
244
+ warning:
245
+ type: string
246
+ description: Present when agentic mode was requested but is unavailable.
247
+
248
+ KeywordResult:
249
+ type: object
250
+ required: [title, url, snippet]
251
+ properties:
252
+ title: { type: string }
253
+ heading: { type: string }
254
+ url:
255
+ type: string
256
+ description: Deep link, e.g. `/docs/concepts#the-agentic-loop`.
257
+ group: { type: string }
258
+ snippet: { type: string }
259
+
260
+ SuggestionsResponse:
261
+ type: object
262
+ required: [suggestions, model]
263
+ properties:
264
+ suggestions:
265
+ type: array
266
+ items: { type: string }
267
+ model: { type: string }
268
+
269
+ Source:
270
+ type: object
271
+ description: A source cited by the agentic answer.
272
+ required: [url]
273
+ properties:
274
+ title: { type: string }
275
+ heading: { type: string }
276
+ group: { type: string }
277
+ url: { type: string }
278
+ terms:
279
+ type: array
280
+ items: { type: string }
281
+
282
+ GlossaryEntry:
283
+ type: object
284
+ required: [term, aliases, definition]
285
+ properties:
286
+ term: { type: string }
287
+ aliases:
288
+ type: array
289
+ items: { type: string }
290
+ definition: { type: string }
291
+
292
+ SectionSummary:
293
+ type: object
294
+ required: [id, title, url]
295
+ properties:
296
+ id:
297
+ type: string
298
+ description: Section id (`slug#anchor`, or `slug` for a page-level section).
299
+ title: { type: string }
300
+ heading:
301
+ type: [string, "null"]
302
+ group:
303
+ type: [string, "null"]
304
+ url: { type: string }
305
+
306
+ Fact:
307
+ type: object
308
+ description: A byte-verbatim literal lifted from the source section.
309
+ required: [kind, literal, chunkId]
310
+ properties:
311
+ kind:
312
+ type: string
313
+ enum: [flag, code, value, default, key]
314
+ literal:
315
+ type: string
316
+ description: Exact source text — never paraphrased.
317
+ chunkId: { type: string }
318
+
319
+ SourceRef:
320
+ type: object
321
+ required: [chunkId, url]
322
+ properties:
323
+ chunkId: { type: string }
324
+ url: { type: string }
325
+ anchor:
326
+ type: [string, "null"]
327
+ description: github-slugger anchor, or null for a page-level section.
328
+
329
+ DigestNode:
330
+ type: object
331
+ required: [id, kind, title, url, summary, facts, sources, mode, terms]
332
+ properties:
333
+ id: { type: string }
334
+ kind:
335
+ type: string
336
+ enum: [section]
337
+ title: { type: string }
338
+ heading:
339
+ type: [string, "null"]
340
+ group:
341
+ type: [string, "null"]
342
+ url: { type: string }
343
+ summary:
344
+ type: string
345
+ description: Model-distilled prose. Exact strings live in `facts`.
346
+ facts:
347
+ type: array
348
+ items: { $ref: "#/components/schemas/Fact" }
349
+ sources:
350
+ type: array
351
+ items: { $ref: "#/components/schemas/SourceRef" }
352
+ mode:
353
+ type: string
354
+ enum: [agent-primary, source-primary]
355
+ terms:
356
+ type: array
357
+ items: { type: string }
358
+
359
+ Error:
360
+ type: object
361
+ required: [error]
362
+ properties:
363
+ error: { type: string }
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@hevmind/ask",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "hev ask: a heading-anchored, agentic search overlay for Astro docs sites.",
6
+ "keywords": [
7
+ "astro",
8
+ "astro-integration",
9
+ "withastro",
10
+ "search",
11
+ "agentic",
12
+ "command-palette",
13
+ "cmdk",
14
+ "claude",
15
+ "anthropic",
16
+ "ai"
17
+ ],
18
+ "license": "MIT",
19
+ "homepage": "https://github.com/hev/ask#readme",
20
+ "sideEffects": false,
21
+ "files": [
22
+ "bin",
23
+ "openapi.yaml",
24
+ "src",
25
+ "skills"
26
+ ],
27
+ "bin": {
28
+ "ask": "./bin/ask.mjs"
29
+ },
30
+ "optionalDependencies": {
31
+ "@hevmind/ask-darwin-arm64": "0.1.0",
32
+ "@hevmind/ask-linux-arm64": "0.1.0",
33
+ "@hevmind/ask-darwin-x64": "0.1.0",
34
+ "@hevmind/ask-linux-x64": "0.1.0",
35
+ "@hevmind/ask-win32-x64": "0.1.0"
36
+ },
37
+ "exports": {
38
+ ".": "./src/index.ts",
39
+ "./endpoint": "./src/endpoint.ts",
40
+ "./components/SearchOverlay.astro": "./src/components/SearchOverlay.astro",
41
+ "./openapi.yaml": "./openapi.yaml",
42
+ "./package.json": "./package.json"
43
+ },
44
+ "dependencies": {
45
+ "github-slugger": "^2.0.0"
46
+ },
47
+ "peerDependencies": {
48
+ "astro": ">=5.0.0"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^22.0.0",
52
+ "astro": "^5.0.0",
53
+ "typescript": "^6.0.3"
54
+ },
55
+ "scripts": {
56
+ "digest:build": "node bin/ask.mjs digest build",
57
+ "digest:verify": "node bin/ask.mjs digest verify",
58
+ "test": "node --test test/*.test.ts",
59
+ "typecheck": "tsc --noEmit"
60
+ }
61
+ }