@mikedown/core 0.2.0 → 0.4.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 (42) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +121 -2
  3. package/dist/ai/engines-page.d.ts +137 -0
  4. package/dist/ai/engines-page.js +11 -0
  5. package/dist/ai/engines-page.js.map +1 -0
  6. package/dist/ai/index.d.ts +2722 -0
  7. package/dist/ai/index.js +684 -0
  8. package/dist/ai/index.js.map +1 -0
  9. package/dist/ai-ui-QFYFN73D.js +6397 -0
  10. package/dist/ai-ui-QFYFN73D.js.map +1 -0
  11. package/dist/chunk-APPBNQEY.js +216 -0
  12. package/dist/chunk-APPBNQEY.js.map +1 -0
  13. package/dist/chunk-DWXGWVA3.js +5748 -0
  14. package/dist/chunk-DWXGWVA3.js.map +1 -0
  15. package/dist/chunk-GWCRPPCX.js +18 -0
  16. package/dist/chunk-GWCRPPCX.js.map +1 -0
  17. package/dist/chunk-IWCXWUWW.js +6958 -0
  18. package/dist/chunk-IWCXWUWW.js.map +1 -0
  19. package/dist/chunk-L4FO63DF.js +113 -0
  20. package/dist/chunk-L4FO63DF.js.map +1 -0
  21. package/dist/chunk-N4N54LK2.js +39 -0
  22. package/dist/chunk-N4N54LK2.js.map +1 -0
  23. package/dist/chunk-QWKT57TR.js +62 -0
  24. package/dist/chunk-QWKT57TR.js.map +1 -0
  25. package/dist/chunk-TSP7BAV3.js +22 -0
  26. package/dist/chunk-TSP7BAV3.js.map +1 -0
  27. package/dist/index.d.ts +19 -463
  28. package/dist/index.js +7717 -2103
  29. package/dist/index.js.map +1 -1
  30. package/dist/schema-DVfssxQX.d.ts +100 -0
  31. package/dist/shared/anchoring.js +4 -16
  32. package/dist/shared/anchoring.js.map +1 -1
  33. package/dist/shared/imagePaste.js +3 -57
  34. package/dist/shared/imagePaste.js.map +1 -1
  35. package/dist/shared/sha1.js +3 -57
  36. package/dist/shared/sha1.js.map +1 -1
  37. package/dist/shared/wordCount.js +4 -12
  38. package/dist/shared/wordCount.js.map +1 -1
  39. package/dist/styles.css +4562 -108
  40. package/dist/types-2TZErbJr.d.ts +619 -0
  41. package/dist/types-DO8UBybZ.d.ts +741 -0
  42. package/package.json +11 -1
@@ -0,0 +1,741 @@
1
+ /**
2
+ * AI provider layer contracts: the settings shape (engines, models, routing,
3
+ * features) and the `ai` host group.
4
+ *
5
+ * Everything under `packages/core/src/ai/` is pure: no network, no DOM, no
6
+ * provider SDK. Inference runs in the host (VS Code extension host, the
7
+ * Desktop Rust side, or an npm consumer's own backend); core only resolves
8
+ * which engine and model should serve a request and renders the UI.
9
+ *
10
+ * Open-ended by design: `routing.tasks` and `features` are open maps, and the
11
+ * parser keeps keys it does not understand on every object (top level,
12
+ * engines, models, routes, feature entries), so a newer version's settings
13
+ * survive a save from an older one. Those extra keys ride along at runtime
14
+ * and are not part of these types.
15
+ */
16
+ /**
17
+ * How the host talks to an engine. `openai-compatible` covers oMLX, vLLM,
18
+ * llama.cpp server and the hosted OpenAI-style APIs (OpenRouter, Groq,
19
+ * Gemini, Mistral, DeepSeek, xAI). `vscode-lm` is a model VS Code itself
20
+ * offers (GitHub Copilot, through `vscode.lm`): no address, no key, VS Code
21
+ * host only (see `AI_HOST_ENGINE_KINDS`). A settings file written by a newer
22
+ * version may carry a kind this one does not know; such engines are kept on
23
+ * save but never routed (see `isKnownEngineKind`).
24
+ */
25
+ type AiEngineKind = 'openai-compatible' | 'lmstudio' | 'ollama' | 'anthropic' | 'openai' | 'vscode-lm';
26
+ declare const AI_ENGINE_KINDS: readonly AiEngineKind[];
27
+ /**
28
+ * Kinds only one host can run (no base URL, no key: the host's own model
29
+ * API). A host runs one only when it reports a status for the engine
30
+ * (`listEngines`); elsewhere such an engine is kept and never routed, the
31
+ * same as an unknown kind. The kind picker offers them only to an engine
32
+ * that already has one.
33
+ */
34
+ declare const AI_HOST_ENGINE_KINDS: readonly AiEngineKind[];
35
+ /** `auto` derives local or cloud from the base URL (see `detectLocality`). */
36
+ type AiLocalitySetting = 'auto' | 'local' | 'cloud';
37
+ type AiLocality = 'local' | 'cloud';
38
+ /** Where a model's capability values came from. Re-discovery refreshes `endpoint` and `heuristic`, never `user`. */
39
+ type AiCapabilitiesSource = 'endpoint' | 'heuristic' | 'user';
40
+ interface AiModelCapabilities {
41
+ /** Chat/instruct completion. */
42
+ chat: boolean;
43
+ /** SSE token streaming (assumed true unless the probe says otherwise). */
44
+ stream: boolean;
45
+ /** Image inputs. */
46
+ vision: boolean;
47
+ /** JSON mode or reliable structured output. */
48
+ json: boolean;
49
+ /** Function calling. */
50
+ tools: boolean;
51
+ /** Fill-in-the-middle / raw completion (ghost text). */
52
+ fim: boolean;
53
+ /** Embeddings endpoint. */
54
+ embed: boolean;
55
+ /** Context window in tokens; null when unknown (treated as `DEFAULT_CONTEXT_TOKENS`). */
56
+ contextTokens: number | null;
57
+ }
58
+ /** The boolean capability flags, for chips and requirement checks. */
59
+ type AiCapabilityFlag = Exclude<keyof AiModelCapabilities, 'contextTokens'>;
60
+ declare const AI_CAPABILITY_FLAGS: readonly AiCapabilityFlag[];
61
+ /** Context window assumed when neither the endpoint nor the id says (OPEN-10). */
62
+ declare const DEFAULT_CONTEXT_TOKENS = 8192;
63
+ interface AiModelConfig {
64
+ /** The id the endpoint knows the model by (sent as `model`). Unique within its engine. */
65
+ id: string;
66
+ /** Display name. Absent means show the id. */
67
+ label?: string;
68
+ enabled: boolean;
69
+ capabilities: AiModelCapabilities;
70
+ capabilitiesSource: AiCapabilitiesSource;
71
+ /**
72
+ * Merged into the request body last, so it wins (OPEN-6): thinking controls
73
+ * such as `reasoning_effort`, `thinking_budget`, `chat_template_kwargs`.
74
+ */
75
+ requestOverrides?: Record<string, unknown>;
76
+ /** The last discovery did not list this model. Kept (greyed), never routed. */
77
+ missing?: boolean;
78
+ /** Added by hand. A discovery that does not list it leaves it alone (no `missing`). */
79
+ manual?: boolean;
80
+ }
81
+ interface AiApiKeyFile {
82
+ path: string;
83
+ jsonPath: string;
84
+ }
85
+ interface AiEngineConfig {
86
+ /** Stable, generated once, never shown. */
87
+ id: string;
88
+ name: string;
89
+ kind: AiEngineKind;
90
+ /** e.g. `http://127.0.0.1:8000/v1` or `https://api.anthropic.com`. */
91
+ baseUrl: string;
92
+ enabled: boolean;
93
+ locality: AiLocalitySetting;
94
+ /** Environment variable the host reads when no key is stored. The key itself is never in settings. */
95
+ apiKeyEnv?: string;
96
+ /**
97
+ * A JSON file in the user's home folder the host reads when no key is stored and
98
+ * `apiKeyEnv` is unset. `path` may start with `~/`; `jsonPath` is a dotted path to
99
+ * a string value (`auth.api_key`). The key itself is never in settings.
100
+ */
101
+ apiKeyFile?: AiApiKeyFile;
102
+ /** `auto` lists models from the endpoint and merges them with manual ones; `manual` never asks. */
103
+ discovery: 'auto' | 'manual';
104
+ models: AiModelConfig[];
105
+ }
106
+ interface AiRoute {
107
+ engineId: string;
108
+ modelId: string;
109
+ }
110
+ /**
111
+ * What to do when a task's route is unusable. `none` disables the surface,
112
+ * `default` tries the default route, `anyLocal` then any usable local
113
+ * model, `any` then cloud engines that pass the gate.
114
+ */
115
+ type AiFallbackMode = 'none' | 'default' | 'anyLocal' | 'any';
116
+ declare const AI_FALLBACK_MODES: readonly AiFallbackMode[];
117
+ interface AiRouting {
118
+ /** Null until the user picks one. */
119
+ default: AiRoute | null;
120
+ fallback: AiFallbackMode;
121
+ /** Open map keyed by task id (see `AI_TASKS`). An absent task uses `default`. */
122
+ tasks: Record<string, AiRoute>;
123
+ }
124
+ /** One feature's saved state. Absent fields fall back to the feature registry. */
125
+ interface AiFeatureSetting {
126
+ enabled?: boolean;
127
+ /** Task override, e.g. `{ task: 'vision' }`. */
128
+ task?: string;
129
+ /** `ghostText` only (see `AiGhostTextOptions`; read through `ghostTextOptions`). */
130
+ delayMs?: number;
131
+ maxTokens?: number;
132
+ trigger?: AiGhostTrigger;
133
+ localOnly?: boolean;
134
+ }
135
+ /** When Inline suggestions fire: after an idle pause, or only from the shortcut / menu. */
136
+ type AiGhostTrigger = 'auto' | 'manual';
137
+ /** The `features.ghostText` knobs with defaults applied and ranges clamped (`ghostTextOptions`). */
138
+ interface AiGhostTextOptions {
139
+ /** Idle time after a keystroke before a request fires, in ms. Default 400. */
140
+ delayMs: number;
141
+ /** Output budget in tokens. Default 48. */
142
+ maxTokens: number;
143
+ /** Default `auto`. */
144
+ trigger: AiGhostTrigger;
145
+ /** Reject cloud engines for this feature. Default true. */
146
+ localOnly: boolean;
147
+ }
148
+ /** Open map keyed by feature id (see `AI_FEATURES`). */
149
+ type AiFeatureSettings = Record<string, AiFeatureSetting>;
150
+ /** Everything under `mikedown.ai.*`. Built by `parseAiSettings`, saved by `serializeAiSettings`. */
151
+ interface AiSettings {
152
+ /** Master switch. Default false. */
153
+ enabled: boolean;
154
+ /** Global gate for cloud engines. Default false; per-workspace consent applies on top. */
155
+ allowCloud: boolean;
156
+ engines: AiEngineConfig[];
157
+ routing: AiRouting;
158
+ features: AiFeatureSettings;
159
+ /** CIDR ranges (and `.suffix` host names) that count as local. Default RFC 1918. */
160
+ localNetworks: string[];
161
+ /** The AI button on the toolbar (hosts with AI support only). Default true. */
162
+ showToolbarButton: boolean;
163
+ /** Record what each AI request sent and received, in memory, and show it in the Activity panel. Default false. */
164
+ activityPanel: boolean;
165
+ /**
166
+ * Documentation bases (`mikedown.ai.docBases`). User-level only: a host
167
+ * reads this key from its user settings and never from a workspace, so a
168
+ * repository cannot add or redirect a base. `parseAiSettings` drops invalid
169
+ * entries with a reason (see `parseDocBases`).
170
+ */
171
+ docBases: AiDocBase[];
172
+ /** Advanced options for chatting with the bases (`mikedown.ai.docs`), user-level only like `docBases`. */
173
+ docs: AiDocsOptions;
174
+ }
175
+ /**
176
+ * One documentation base: user-level folders of markdown the chat can
177
+ * navigate and answer from. Roots are absolute, normalized folder paths.
178
+ */
179
+ interface AiDocBase {
180
+ /** Stable, generated once, never shown. The wire and the consent dialog name a base by id; the host maps it to a name from its own settings. */
181
+ id: string;
182
+ name: string;
183
+ /** Absolute normalized folders (`validateDocRoot`). At least one. */
184
+ roots: string[];
185
+ /** Per-base override for the home document's file name, relative to each root (`HOME.md`). Absent: `INDEX.md`, `README.md`, then a map. */
186
+ homeFile?: string;
187
+ /** May pages from this base be sent to a cloud engine. Default false. */
188
+ allowCloud: boolean;
189
+ /** A disabled base is never indexed for or sent with a request. Default true. */
190
+ enabled: boolean;
191
+ }
192
+ type AiDocsNavigateBy = 'section' | 'page';
193
+ declare const AI_DOCS_NAVIGATE_BY: readonly AiDocsNavigateBy[];
194
+ /** The advanced options (`ai.docs`), without the bases. */
195
+ interface AiDocsOptions {
196
+ /** Navigation calls before the answer. Clamped to `AI_DOCS_MAX_HOPS_MIN`..`AI_DOCS_MAX_HOPS_MAX`. Default 3. */
197
+ maxHops: number;
198
+ /** Percent of the route's context the answer's sections may take. Clamped to `AI_DOCS_SOURCE_SHARE_MIN`..`AI_DOCS_SOURCE_SHARE_MAX`. Default 60. */
199
+ sourceShare: number;
200
+ /** Whether the model asks for `page#heading` sections or whole pages. Default `section`. */
201
+ navigateBy: AiDocsNavigateBy;
202
+ }
203
+ /** The options the loop runs with: `ai.docs` plus the bases (`resolveDocsSettings`). */
204
+ interface AiDocsSettings extends AiDocsOptions {
205
+ bases: AiDocBase[];
206
+ }
207
+ declare const AI_DOCS_MAX_HOPS_MIN = 1;
208
+ declare const AI_DOCS_MAX_HOPS_MAX = 5;
209
+ /**
210
+ * The most requests one "Allow once" can admit in a chat turn: a navigation
211
+ * call and its one repair per hop, then the answer. Hosts stop honouring a grant
212
+ * past this, so a webview that never ends its turn cannot keep sending on it.
213
+ * `TURN_GRANT_MAX_USES` in Desktop's `commands/ai.rs` is the same number.
214
+ */
215
+ declare const AI_TURN_GRANT_MAX_USES: number;
216
+ declare const AI_DOCS_MAX_HOPS_DEFAULT = 3;
217
+ declare const AI_DOCS_SOURCE_SHARE_MIN = 30;
218
+ declare const AI_DOCS_SOURCE_SHARE_MAX = 80;
219
+ declare const AI_DOCS_SOURCE_SHARE_DEFAULT = 60;
220
+ declare const AI_DOCS_NAVIGATE_BY_DEFAULT: AiDocsNavigateBy;
221
+ /** Where a root's home document came from (decision 3). `map` means none was found and core builds an in-memory map. */
222
+ type AiDocHomeKind = 'override' | 'index' | 'readme' | 'map';
223
+ /** One root of a base, as the host last saw it. */
224
+ interface AiDocRootStatus {
225
+ /** The root as configured (absolute). */
226
+ root: string;
227
+ /** The label ids use for this root (the same rule on both hosts; `docs/index.ts`). */
228
+ label: string;
229
+ /** The folder exists and is readable. False: "Root not found"; the root is skipped for requests, never deleted. */
230
+ found: boolean;
231
+ /** Which home precedence step won; `map` is the "No home document" state. Absent when the root is not found. */
232
+ home?: AiDocHomeKind;
233
+ /** The home document's path relative to the root, when `home` is not `map`. */
234
+ homePath?: string;
235
+ /** Markdown files indexed under this root. */
236
+ fileCount: number;
237
+ /** Files over the byte cap, skipped. */
238
+ oversizeSkipped: number;
239
+ /** The file cap was reached; later files were not indexed. */
240
+ capReached: boolean;
241
+ /** Files that are not UTF-8 text (a UTF-16 or UTF-32 byte order mark, or NUL bytes), skipped. */
242
+ notTextSkipped?: number;
243
+ /** The folder cap was reached; later folders were not read. */
244
+ dirCapReached?: boolean;
245
+ /** The build's time budget ran out; the index covers what was read before that. */
246
+ timedOut?: boolean;
247
+ }
248
+ /**
249
+ * A base plus its health, as `listBases` reports it. Computed when the
250
+ * manager page opens and on Refresh. "Active" means enabled with at least one
251
+ * root found; only active bases are indexed for or sent to a request.
252
+ */
253
+ interface AiDocBaseStatus {
254
+ base: AiDocBase;
255
+ /** Enabled and at least one root found. */
256
+ active: boolean;
257
+ roots: AiDocRootStatus[];
258
+ /** Sum over roots. */
259
+ fileCount: number;
260
+ oversizeSkipped: number;
261
+ /** Roots not found on disk. */
262
+ rootsMissing: number;
263
+ /** Any root hit the file cap. */
264
+ capReached: boolean;
265
+ /** Files skipped because they are not UTF-8 text, over every root. */
266
+ notTextSkipped?: number;
267
+ /** Any root hit the folder cap. */
268
+ dirCapReached?: boolean;
269
+ /** The build's time budget ran out; the index and the counts cover what was read before that. */
270
+ timedOut?: boolean;
271
+ /** Relative links that leave every root or point at no page. Absent when the host did not compute it. */
272
+ brokenLinks?: number;
273
+ }
274
+ /** A heading in the index (a section starts at each). Bodies are never in the index. */
275
+ interface AiDocsHeading {
276
+ /** The section id: `<page id>#<slug>`. */
277
+ id: string;
278
+ /** 1 to 6. */
279
+ level: number;
280
+ text: string;
281
+ slug: string;
282
+ }
283
+ /** A link as written in a page. Core resolves it (`docs/links.ts`); the host only extracts. */
284
+ interface AiDocsLink {
285
+ /** The target as written: a relative path with an optional `#fragment`, or a wikilink name. */
286
+ target: string;
287
+ /** The link text. */
288
+ text: string;
289
+ kind: 'markdown' | 'wikilink';
290
+ }
291
+ /** One markdown page of a base, without its body. */
292
+ interface AiDocsPage {
293
+ /** `<root label>/<rel path>`, with `/` separators (`docs/index.ts`). The only thing the model names. */
294
+ id: string;
295
+ /** The root's label (the first segment of `id`). */
296
+ root: string;
297
+ /** Path relative to the root, with `/` separators. */
298
+ relPath: string;
299
+ /** Frontmatter title, else the first heading, else the file name. */
300
+ title: string;
301
+ headings: AiDocsHeading[];
302
+ links: AiDocsLink[];
303
+ /** Size of the file in characters (not bytes), for the budget. */
304
+ chars: number;
305
+ }
306
+ /** The home document chosen for a root. */
307
+ interface AiDocsHome {
308
+ /** The root's label. */
309
+ root: string;
310
+ kind: AiDocHomeKind;
311
+ /** The home page's id; null for `map`. */
312
+ pageId: string | null;
313
+ }
314
+ /** A base's index: pages with headings and links, no bodies. Built lazily on the host, in memory. */
315
+ interface AiDocsIndex {
316
+ baseId: string;
317
+ /** Roots that were found, in configured order. */
318
+ roots: Array<{
319
+ label: string;
320
+ }>;
321
+ pages: AiDocsPage[];
322
+ homes: AiDocsHome[];
323
+ /** The file cap cut the walk short. */
324
+ truncated: boolean;
325
+ oversizeSkipped: number;
326
+ /** Unix milliseconds the index was built. */
327
+ builtAt: number;
328
+ }
329
+ /** A section (or whole page) as `readSections` returns it: the only call that carries a body. */
330
+ interface AiDocsSection {
331
+ /** The id that was asked for: `<page id>#<slug>` or a page id. */
332
+ id: string;
333
+ pageId: string;
334
+ /** The page's title. */
335
+ title: string;
336
+ /** Path relative to the root, for the chip and the relative link. */
337
+ relPath: string;
338
+ /** The section's heading text; absent for a whole page. */
339
+ heading?: string;
340
+ /** The markdown, capped at the section byte cap by the host. */
341
+ text: string;
342
+ /** The host cut `text` at the cap. */
343
+ truncated: boolean;
344
+ }
345
+ /** A keyword match: ids and labels only, no text. The loop reads the bodies through `readSections`. */
346
+ interface AiDocsSectionRef {
347
+ /** `<page id>#<slug>`, or a page id for text before the first heading. */
348
+ id: string;
349
+ pageId: string;
350
+ title: string;
351
+ relPath: string;
352
+ heading?: string;
353
+ /** Higher is a better match; the order is `docs/keyword.ts`'s. */
354
+ score: number;
355
+ }
356
+ type AiMessagePart = {
357
+ type: 'text';
358
+ text: string;
359
+ } | {
360
+ type: 'image';
361
+ mime: string;
362
+ bytes: Uint8Array;
363
+ };
364
+ interface AiMessage {
365
+ role: 'system' | 'user' | 'assistant';
366
+ content: string | AiMessagePart[];
367
+ }
368
+ interface AiCompletionRequest {
369
+ requestId: string;
370
+ /** Feature id, for logging and consent copy. */
371
+ feature: string;
372
+ /** Task id, informational for tier-1 hosts. */
373
+ task: string;
374
+ /** Resolved by core; tier-1 hosts may ignore it. */
375
+ route?: AiRoute;
376
+ messages: AiMessage[];
377
+ format?: 'text' | 'json';
378
+ maxTokens?: number;
379
+ temperature?: number;
380
+ /**
381
+ * Per-request body fields (the popover's thinking dial). The host merges
382
+ * them over the model's configured `requestOverrides`, deeply, so the
383
+ * request wins. Tier-1 hosts may ignore them.
384
+ */
385
+ requestOverrides?: Record<string, unknown>;
386
+ /** Set on every request of a chat turn (`AiRoutePin`); absent on every other request. */
387
+ turn?: AiCompletionTurn;
388
+ }
389
+ /**
390
+ * What a request of a chat turn says about that turn. A host uses it to find
391
+ * the "Allow once" it holds for the turn, and to word a dialog it has to raise
392
+ * in the middle of one (`sends`, and `baseIds`, which it maps to names from its
393
+ * own settings). It is a claim, not consent: a host admits the request only
394
+ * against a grant it made itself when the user answered, for this panel or
395
+ * window, this turn, this engine and this set of bases.
396
+ */
397
+ interface AiCompletionTurn {
398
+ id: string;
399
+ sends?: AiSendsScope;
400
+ baseIds?: string[];
401
+ /** The engine's locality when the turn was routed. A host fails the request if it no longer holds. */
402
+ isLocal?: boolean;
403
+ }
404
+ interface AiServedBy {
405
+ engineId?: string;
406
+ /** e.g. "oMLX (Mac Studio)". */
407
+ engineName: string;
408
+ modelId: string;
409
+ isLocal: boolean;
410
+ }
411
+ type AiFinishReason = 'stop' | 'length' | 'cancelled' | 'error';
412
+ interface AiUsage {
413
+ promptTokens?: number;
414
+ completionTokens?: number;
415
+ }
416
+ /**
417
+ * How long a completion took, measured by the host from sending the request.
418
+ * Optional on the wire: an older host sends none.
419
+ */
420
+ interface AiTiming {
421
+ /** To the end of the answer. */
422
+ totalMs: number;
423
+ /** To the first output, answer or reasoning. Absent when the server did not stream. */
424
+ firstTokenMs?: number;
425
+ /** Characters of output (answer plus reasoning), to estimate tokens when the engine reports no usage. */
426
+ outputChars?: number;
427
+ }
428
+ interface AiCompletionResult {
429
+ text: string;
430
+ served: AiServedBy;
431
+ usage?: AiUsage;
432
+ finishReason?: AiFinishReason;
433
+ timing?: AiTiming;
434
+ }
435
+ /** One model as the host last saw it. */
436
+ interface AiEngineStatusModel {
437
+ id: string;
438
+ label?: string;
439
+ capabilities: AiModelCapabilities;
440
+ source: AiCapabilitiesSource;
441
+ missing?: boolean;
442
+ /** Loaded in memory right now (LM Studio, Ollama report it). */
443
+ loaded?: boolean;
444
+ /** What the server says the model takes, in bytes (oMLX, LM Studio, Ollama report it). */
445
+ sizeBytes?: number;
446
+ /** The server says it is a mixture of experts (LM Studio `arch`, oMLX `config_model_type`). */
447
+ moe?: boolean;
448
+ }
449
+ /** Where a host found an engine's key: its secret store, a variable, or a key file. */
450
+ type AiKeySource = 'stored' | 'env' | 'file';
451
+ interface AiEngineStatus {
452
+ engineId: string;
453
+ /** Null = never probed. */
454
+ ok: boolean | null;
455
+ isLocal: boolean;
456
+ hasKey: boolean;
457
+ /**
458
+ * Where the key came from, when `hasKey` and the host knows: its secret store,
459
+ * the engine's `apiKeyEnv` variable, or its `apiKeyFile`. Absent for no key or
460
+ * a host that does not say (the engine form then guesses from the engine's
461
+ * settings).
462
+ */
463
+ keySource?: AiKeySource;
464
+ latencyMs?: number;
465
+ error?: string;
466
+ models: AiEngineStatusModel[];
467
+ }
468
+ /**
469
+ * One local model server as `detectLocalServers` found it: whether its preset's
470
+ * address answered, and how many models it listed when it did.
471
+ */
472
+ interface AiLocalServerProbe {
473
+ presetId: string;
474
+ baseUrl: string;
475
+ ok: boolean;
476
+ /** Models the server listed (embedding models left out where the server says which they are). */
477
+ models?: number;
478
+ latencyMs?: number;
479
+ }
480
+ /**
481
+ * A model API built into the host (VS Code's `vscode.lm`, which GitHub
482
+ * Copilot serves), as `detectHostEngines` found it. Nothing is saved.
483
+ */
484
+ interface AiHostEngineProbe {
485
+ /** The preset to add it from (`vscodelm`). */
486
+ presetId: string;
487
+ kind: AiEngineKind;
488
+ /** At least one chat model is available right now. */
489
+ ok: boolean;
490
+ /** Chat models available. */
491
+ models: number;
492
+ /** Why none is available, when the host knows (no Copilot extension, signed out). */
493
+ error?: string;
494
+ }
495
+ /**
496
+ * What a host can tell about this machine, for the setup wizard's "can it
497
+ * run models locally" hint (`recommendSetupPath`, `modelSizeHint`). A rough
498
+ * call, not a benchmark: VS Code reads no GPU at all.
499
+ */
500
+ interface AiHardwareInfo {
501
+ /** Node's `process.arch` names: `arm64`, `x64`. */
502
+ arch: string;
503
+ /** Node's `process.platform` names: `darwin`, `win32`, `linux`. */
504
+ platform: string;
505
+ totalMemoryBytes: number;
506
+ /** macOS on arm64: the GPU shares `totalMemoryBytes` (unified memory). */
507
+ appleSilicon: boolean;
508
+ /** CPU brand string, when the host read one ("Apple M2 Max"). */
509
+ cpu?: string;
510
+ /** A discrete GPU, when the host could read one (Desktop: NVIDIA through `nvidia-smi`; Apple Silicon reports `apple`). */
511
+ gpu?: {
512
+ vendor: 'nvidia' | 'amd' | 'apple' | 'other';
513
+ vramBytes?: number;
514
+ name?: string;
515
+ };
516
+ }
517
+ type AiErrorCode = 'unreachable' | 'auth' | 'modelNotFound' | 'modelLoadFailed' | 'contextOverflow' | 'rateLimited' | 'cloudBlocked' | 'consentDenied' | 'cancelled' | 'badOutput' | 'unknown';
518
+ declare const AI_ERROR_CODES: readonly AiErrorCode[];
519
+ /** What `MikeDownAiHost` methods reject with. A plain object, so it crosses postMessage and Tauri events. */
520
+ interface AiError {
521
+ code: AiErrorCode;
522
+ message: string;
523
+ /** The engine the failed request went to (or the router turned down), when known. */
524
+ engineId?: string;
525
+ }
526
+ /**
527
+ * The `ai` group on `MikeDownHost`. Absent turns every AI surface off.
528
+ *
529
+ * Tier 1 (`complete` only): core shows the master switch, feature toggles and
530
+ * the indicator from whatever `served` the host reports. Tier 2 (plus the
531
+ * registry methods): core shows the full AI tab and routes.
532
+ */
533
+ interface MikeDownAiHost {
534
+ complete(req: AiCompletionRequest, onToken: (delta: string) => void, signal: AbortSignal): Promise<AiCompletionResult>;
535
+ embed?(texts: string[], route?: AiRoute, signal?: AbortSignal): Promise<number[][]>;
536
+ transcribe?(audio: {
537
+ mime: string;
538
+ bytes: Uint8Array;
539
+ }, signal?: AbortSignal): Promise<string>;
540
+ listEngines?(): Promise<AiEngineStatus[]>;
541
+ /**
542
+ * Look for model servers on this machine at the local presets' default
543
+ * addresses (`localDetectCandidates()`), one GET each, capped at
544
+ * `AI_LOCAL_DETECT_TIMEOUT_MS`. Only on request (the first-run panel, the
545
+ * quick-start cards), never in the background. The host looks each preset
546
+ * up by id itself and sends a key (oMLX's key file) only to the preset's
547
+ * own address. One result per candidate, in order. Nothing is saved.
548
+ */
549
+ detectLocalServers?(candidates: {
550
+ presetId: string;
551
+ baseUrl: string;
552
+ }[], signal: AbortSignal): Promise<AiLocalServerProbe[]>;
553
+ /**
554
+ * Model APIs built into the host that need no address or key (VS Code:
555
+ * `vscode.lm`, GitHub Copilot's models), one result per `AI_HOST_ENGINE_KINDS`
556
+ * kind the host runs. Lists models only; sends no text. Absent: the host
557
+ * has none, and the `host` preset family stays hidden.
558
+ */
559
+ detectHostEngines?(): Promise<AiHostEngineProbe[]>;
560
+ /**
561
+ * This machine's memory, CPU and (where the host can read it) GPU, for the
562
+ * setup wizard's local hint. Static, so hosts may answer from a cache; null
563
+ * when the host cannot tell.
564
+ */
565
+ hardwareInfo?(): Promise<AiHardwareInfo | null>;
566
+ testEngine?(engineId: string): Promise<AiEngineStatus>;
567
+ discoverModels?(engineId: string): Promise<AiEngineStatus>;
568
+ /** Store (or with null, delete) an engine's key. Never part of `saveSettings`. */
569
+ setSecret?(engineId: string, apiKey: string | null): Promise<void>;
570
+ /**
571
+ * False when the host process may not see variables set in a shell profile
572
+ * (MikeDown Desktop opened from the Dock or Start menu), so `apiKeyEnv` is
573
+ * unreliable there and the engine form points at the key file or a pasted
574
+ * key instead. Absent: the host reads them (VS Code resolves the shell
575
+ * environment) or does not say.
576
+ */
577
+ readonly shellEnv?: boolean;
578
+ /**
579
+ * Host UI asking to use a cloud engine in this workspace. Absent means the
580
+ * host decides on its own. `about` names the request that asked, so the
581
+ * dialog can say what is sent (`aiConsentText`).
582
+ */
583
+ requestCloudConsent?(engineId: string, about?: AiConsentAbout): Promise<AiConsentReply>;
584
+ /**
585
+ * `requestCloudConsent` for a chat turn, where `'once'` means the whole turn:
586
+ * the host holds the grant and admits the turn's requests (`req.turn`) on it.
587
+ * Absent: core asks `requestCloudConsent`.
588
+ */
589
+ requestTurnConsent?(engineId: string, about: AiTurnConsentAbout): Promise<AiConsentReply>;
590
+ /**
591
+ * The chat turn `turnId` is over (answered, refused, failed or cancelled):
592
+ * forget any "Allow once" held for it. Fire and forget. A host that holds no
593
+ * grant per turn can leave it out.
594
+ */
595
+ endTurn?(turnId: string): void;
596
+ /**
597
+ * The cloud consents for this workspace or folder, for the Cloud consents
598
+ * list on the AI tab: the stored answers, plus this session's "Don't
599
+ * allow" (`deny`), so it can be forgotten without a restart. "Allow once"
600
+ * leaves nothing to list.
601
+ */
602
+ listConsents?(): Promise<AiConsentRecord[]>;
603
+ /** Forget the answer for an engine (stored or this session's), so the next request asks again. */
604
+ forgetConsent?(engineId: string): Promise<void>;
605
+ }
606
+ /**
607
+ * What `requestCloudConsent` resolves to: `true` allowed for the workspace
608
+ * (remembered), `'once'` allowed for this request only (the next one asks
609
+ * again), or for the whole turn from `requestTurnConsent`, `false` declined for this request (a host may also stop asking for
610
+ * the session; it then answers `false` without a dialog), `'never'` a stored
611
+ * refusal from an older version (the host answers it without asking). A host
612
+ * that only knows allow and decline keeps returning booleans.
613
+ */
614
+ type AiConsentReply = boolean | 'once' | 'never';
615
+ /** One stored cloud consent answer (`MikeDownAiHost.listConsents`). */
616
+ interface AiConsentRecord {
617
+ engineId: string;
618
+ /** `allow` and `never` (older versions) are stored; `deny` is this session's "Don't allow". */
619
+ answer: 'allow' | 'never' | 'deny';
620
+ /** Unix milliseconds. */
621
+ at: number;
622
+ /** What the answer covers: VS Code's workspace, Desktop's folder. */
623
+ scope: 'workspace' | 'folder';
624
+ }
625
+ /**
626
+ * What a request sends to the engine, for the privacy copy (`whatIsSent`):
627
+ * the selection with its neighbouring paragraphs, the whole document, the
628
+ * document with its frontmatter and the workspace's tag names, pages from
629
+ * the documentation bases the user turned on, or the text around the cursor
630
+ * on every pause (Inline suggestions).
631
+ */
632
+ type AiSendsScope = 'selection' | 'document' | 'documentAndTags' | 'docsBase' | 'cursor';
633
+ /**
634
+ * The request behind a consent prompt. `sends` overrides the feature's own
635
+ * scope (whole-document Translate; the docs chat sends `docsBase`).
636
+ */
637
+ interface AiConsentAbout {
638
+ feature?: string;
639
+ sends?: AiSendsScope;
640
+ /**
641
+ * The documentation bases the request would send pages from, by id. The
642
+ * host maps ids to names from its own user-level settings and drops unknown
643
+ * ids, so a name from the webview never reaches its modal.
644
+ */
645
+ baseIds?: string[];
646
+ }
647
+ /** A consent prompt from a chat turn (`AiCompletionTurn.id`): an "Allow once" holds for that turn and no other. */
648
+ interface AiTurnConsentAbout extends AiConsentAbout {
649
+ turnId: string;
650
+ }
651
+ /**
652
+ * The `docs` group on `MikeDownHost`: documentation bases, read through the
653
+ * host. Absent (npm consumers): the chat's scope picker shows "This document"
654
+ * only. Defined next to `MikeDownAiHost` so hosts reach it through
655
+ * `@mikedown/core/ai`; `host.ts` re-exports it.
656
+ *
657
+ * Containment is the host's job and is code, not prompt (S1, S2, S3, S9):
658
+ *
659
+ * - Every method takes a base id and resolves the roots from the host's own
660
+ * user-level configuration. A root that configuration does not list is
661
+ * refused, so a webview message can never name a folder. A disabled base is
662
+ * refused.
663
+ * - `readSections` looks ids up in the host's own index and refuses anything
664
+ * else. No path is ever built from an id's text.
665
+ * - Reads canonicalize the root and the file, refuse anything outside the
666
+ * root (whole path components), non-markdown, symlinks, non-regular files
667
+ * and oversize files.
668
+ * - The model names ids from core's index and nothing else.
669
+ */
670
+ interface MikeDownDocsHost {
671
+ /**
672
+ * Every configured base with its health (disabled ones included). The bases
673
+ * themselves (`enabled`, `allowCloud`, names) are read afresh on every call.
674
+ * A plain call is cheap and must not walk a base that already has a usable
675
+ * index: the chat loop calls it once per turn. `refresh: true` forces a walk
676
+ * and fills the health (file counts, broken links); the manager page passes
677
+ * it when its section opens and on Refresh.
678
+ */
679
+ listBases(opts?: {
680
+ refresh?: boolean;
681
+ }): Promise<AiDocBaseStatus[]>;
682
+ /** The base's index (pages, headings, links, homes; no bodies), built lazily and kept in memory. Rejects for an unknown, disabled or inactive base. */
683
+ index(baseId: string): Promise<AiDocsIndex>;
684
+ /**
685
+ * Bodies for section ids (`<page id>#<slug>`) or page ids, each capped at
686
+ * the section byte cap. Ids the host's index does not know are left out of
687
+ * the result (never an error, never a path); a read that fails a
688
+ * containment check is left out too. Order follows `ids`.
689
+ */
690
+ readSections(baseId: string, ids: string[]): Promise<AiDocsSection[]>;
691
+ /** Case-folded scan over the base's files, headings weighted, best first, at most 20 (`docs/keyword.ts`). Ids and labels only. */
692
+ keywordSearch(baseId: string, query: string): Promise<AiDocsSectionRef[]>;
693
+ /** Tell core when the base's files change (a watcher per root only). Returns the function that stops watching. */
694
+ watch?(baseId: string, onChange: () => void): () => void;
695
+ /**
696
+ * Save the user's bases. The host re-validates with `parseAiSettings` and
697
+ * writes the user-level setting only, never a workspace one. Rejects when
698
+ * nothing valid is left to write over something valid.
699
+ */
700
+ saveBases(bases: AiDocBase[]): Promise<void>;
701
+ /** VS Code: offer the open workspace folders (a picker when there are several) and resolve one's absolute path; null when cancelled or none is open. */
702
+ addWorkspaceFolder?(): Promise<string | null>;
703
+ /** Desktop: the native folder picker; resolves the chosen absolute path, null when cancelled. */
704
+ pickFolder?(): Promise<string | null>;
705
+ }
706
+ /** A model as one discovery call reported it, before merging into settings. */
707
+ interface AiDiscoveredModel {
708
+ id: string;
709
+ label?: string;
710
+ /** Endpoint values; anything absent is filled from the id heuristics on merge. */
711
+ capabilities: Partial<AiModelCapabilities>;
712
+ /** `endpoint` when the server described the model, `heuristic` when only its id was listed. */
713
+ source: 'endpoint' | 'heuristic';
714
+ loaded?: boolean;
715
+ /** Bytes the model takes, as the server reports it (oMLX `estimated_size`, LM Studio `size_bytes`, Ollama `size`). */
716
+ sizeBytes?: number;
717
+ /** The server says it is a mixture of experts: an architecture name with `moe` in it (LM Studio `arch`, oMLX `config_model_type`). */
718
+ moe?: boolean;
719
+ }
720
+ /**
721
+ * Host-side adapter shape. TypeScript in the extension (`src/ai/providers/`),
722
+ * a Rust trait with the same shape on Desktop. Declared here so both hosts
723
+ * share one contract; core never implements it.
724
+ */
725
+ interface ProviderAdapter {
726
+ readonly kind: AiEngineKind;
727
+ /** Discovery plus capability detection. */
728
+ listModels(engine: AiEngineConfig, apiKey: string | null, signal: AbortSignal): Promise<AiDiscoveredModel[]>;
729
+ /** Test connection: reachability, auth, latency. */
730
+ probe(engine: AiEngineConfig, apiKey: string | null, signal: AbortSignal): Promise<{
731
+ ok: boolean;
732
+ latencyMs: number;
733
+ error?: AiError;
734
+ }>;
735
+ /** Streaming completion against one model. */
736
+ complete(engine: AiEngineConfig, apiKey: string | null, model: AiModelConfig, req: AiCompletionRequest, onToken: (delta: string) => void, signal: AbortSignal): Promise<AiCompletionResult>;
737
+ embed?(engine: AiEngineConfig, apiKey: string | null, model: AiModelConfig, texts: string[], signal: AbortSignal): Promise<number[][]>;
738
+ }
739
+
740
+ export { AI_CAPABILITY_FLAGS as H, AI_DOCS_MAX_HOPS_DEFAULT as I, AI_DOCS_MAX_HOPS_MAX as J, AI_DOCS_MAX_HOPS_MIN as K, AI_DOCS_NAVIGATE_BY as L, AI_DOCS_NAVIGATE_BY_DEFAULT as N, AI_DOCS_SOURCE_SHARE_DEFAULT as O, AI_DOCS_SOURCE_SHARE_MAX as P, AI_DOCS_SOURCE_SHARE_MIN as Q, AI_ENGINE_KINDS as R, AI_ERROR_CODES as S, AI_FALLBACK_MODES as T, AI_HOST_ENGINE_KINDS as U, AI_TURN_GRANT_MAX_USES as V, DEFAULT_CONTEXT_TOKENS as ag };
741
+ export type { AiDocHomeKind as $, AiSettings as A, AiDocBaseStatus as B, AiDocsNavigateBy as C, AiConsentRecord as D, AiLocalServerProbe as E, AiHardwareInfo as F, AiHostEngineProbe as G, MikeDownAiHost as M, AiCapabilitiesSource as W, AiCompletionTurn as X, AiConsentAbout as Y, AiConsentReply as Z, AiDocBase as _, AiEngineStatus as a, AiDocRootStatus as a0, AiDocsHeading as a1, AiDocsSettings as a2, AiEngineStatusModel as a3, AiFallbackMode as a4, AiFeatureSetting as a5, AiGhostTextOptions as a6, AiGhostTrigger as a7, AiKeySource as a8, AiLocalitySetting as a9, AiMessagePart as aa, AiRoute as ab, AiRouting as ac, AiServedBy as ad, AiTiming as ae, AiTurnConsentAbout as af, ProviderAdapter as ah, MikeDownDocsHost as b, AiModelCapabilities as c, AiCapabilityFlag as d, AiSendsScope as e, AiFeatureSettings as f, AiLocality as g, AiEngineConfig as h, AiEngineKind as i, AiModelConfig as j, AiDiscoveredModel as k, AiApiKeyFile as l, AiDocsOptions as m, AiFinishReason as n, AiUsage as o, AiErrorCode as p, AiCompletionResult as q, AiCompletionRequest as r, AiMessage as s, AiError as t, AiDocsLink as u, AiDocsIndex as v, AiDocsPage as w, AiDocsHome as x, AiDocsSectionRef as y, AiDocsSection as z };