@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.
- package/CHANGELOG.md +19 -0
- package/README.md +121 -2
- package/dist/ai/engines-page.d.ts +137 -0
- package/dist/ai/engines-page.js +11 -0
- package/dist/ai/engines-page.js.map +1 -0
- package/dist/ai/index.d.ts +2722 -0
- package/dist/ai/index.js +684 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/ai-ui-QFYFN73D.js +6397 -0
- package/dist/ai-ui-QFYFN73D.js.map +1 -0
- package/dist/chunk-APPBNQEY.js +216 -0
- package/dist/chunk-APPBNQEY.js.map +1 -0
- package/dist/chunk-DWXGWVA3.js +5748 -0
- package/dist/chunk-DWXGWVA3.js.map +1 -0
- package/dist/chunk-GWCRPPCX.js +18 -0
- package/dist/chunk-GWCRPPCX.js.map +1 -0
- package/dist/chunk-IWCXWUWW.js +6958 -0
- package/dist/chunk-IWCXWUWW.js.map +1 -0
- package/dist/chunk-L4FO63DF.js +113 -0
- package/dist/chunk-L4FO63DF.js.map +1 -0
- package/dist/chunk-N4N54LK2.js +39 -0
- package/dist/chunk-N4N54LK2.js.map +1 -0
- package/dist/chunk-QWKT57TR.js +62 -0
- package/dist/chunk-QWKT57TR.js.map +1 -0
- package/dist/chunk-TSP7BAV3.js +22 -0
- package/dist/chunk-TSP7BAV3.js.map +1 -0
- package/dist/index.d.ts +19 -463
- package/dist/index.js +7717 -2103
- package/dist/index.js.map +1 -1
- package/dist/schema-DVfssxQX.d.ts +100 -0
- package/dist/shared/anchoring.js +4 -16
- package/dist/shared/anchoring.js.map +1 -1
- package/dist/shared/imagePaste.js +3 -57
- package/dist/shared/imagePaste.js.map +1 -1
- package/dist/shared/sha1.js +3 -57
- package/dist/shared/sha1.js.map +1 -1
- package/dist/shared/wordCount.js +4 -12
- package/dist/shared/wordCount.js.map +1 -1
- package/dist/styles.css +4562 -108
- package/dist/types-2TZErbJr.d.ts +619 -0
- package/dist/types-DO8UBybZ.d.ts +741 -0
- package/package.json +11 -1
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// src/ai/types.ts
|
|
2
|
+
var AI_ENGINE_KINDS = ["openai-compatible", "lmstudio", "ollama", "anthropic", "openai", "vscode-lm"];
|
|
3
|
+
var AI_HOST_ENGINE_KINDS = ["vscode-lm"];
|
|
4
|
+
var AI_CAPABILITY_FLAGS = ["chat", "stream", "vision", "json", "tools", "fim", "embed"];
|
|
5
|
+
var DEFAULT_CONTEXT_TOKENS = 8192;
|
|
6
|
+
var AI_FALLBACK_MODES = ["none", "default", "anyLocal", "any"];
|
|
7
|
+
var AI_DOCS_NAVIGATE_BY = ["section", "page"];
|
|
8
|
+
var AI_DOCS_MAX_HOPS_MIN = 1;
|
|
9
|
+
var AI_DOCS_MAX_HOPS_MAX = 5;
|
|
10
|
+
var AI_TURN_GRANT_MAX_USES = AI_DOCS_MAX_HOPS_MAX * 2 + 1;
|
|
11
|
+
var AI_DOCS_MAX_HOPS_DEFAULT = 3;
|
|
12
|
+
var AI_DOCS_SOURCE_SHARE_MIN = 30;
|
|
13
|
+
var AI_DOCS_SOURCE_SHARE_MAX = 80;
|
|
14
|
+
var AI_DOCS_SOURCE_SHARE_DEFAULT = 60;
|
|
15
|
+
var AI_DOCS_NAVIGATE_BY_DEFAULT = "section";
|
|
16
|
+
var AI_ERROR_CODES = [
|
|
17
|
+
"unreachable",
|
|
18
|
+
"auth",
|
|
19
|
+
"modelNotFound",
|
|
20
|
+
"modelLoadFailed",
|
|
21
|
+
"contextOverflow",
|
|
22
|
+
"rateLimited",
|
|
23
|
+
"cloudBlocked",
|
|
24
|
+
"consentDenied",
|
|
25
|
+
"cancelled",
|
|
26
|
+
"badOutput",
|
|
27
|
+
"unknown"
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// src/ai/locality.ts
|
|
31
|
+
var DEFAULT_LOCAL_NETWORKS = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"];
|
|
32
|
+
var ALWAYS_LOCAL_IPV4 = ["127.0.0.0/8", "169.254.0.0/16", "0.0.0.0/32"];
|
|
33
|
+
var LOCAL_SUFFIXES = [".localhost", ".local", ".lan", ".home.arpa", ".internal"];
|
|
34
|
+
function hostOfUrl(url) {
|
|
35
|
+
const trimmed = url.trim();
|
|
36
|
+
if (!trimmed) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const withoutScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "") : trimmed;
|
|
40
|
+
const authority = withoutScheme.split(/[/?#]/, 1)[0];
|
|
41
|
+
const hostPort = authority.includes("@") ? authority.slice(authority.lastIndexOf("@") + 1) : authority;
|
|
42
|
+
if (!hostPort) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
if (hostPort.startsWith("[")) {
|
|
46
|
+
const end = hostPort.indexOf("]");
|
|
47
|
+
return end > 1 ? hostPort.slice(1, end).toLowerCase() : null;
|
|
48
|
+
}
|
|
49
|
+
if ((hostPort.match(/:/g) ?? []).length > 1) {
|
|
50
|
+
return hostPort.toLowerCase();
|
|
51
|
+
}
|
|
52
|
+
const host = hostPort.split(":", 1)[0].toLowerCase().replace(/\.$/, "");
|
|
53
|
+
return host || null;
|
|
54
|
+
}
|
|
55
|
+
function parseIpv4(host) {
|
|
56
|
+
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
57
|
+
if (!m) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const parts = m.slice(1).map(Number);
|
|
61
|
+
if (parts.some((p) => p > 255)) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return (parts[0] << 24 >>> 0) + (parts[1] << 16) + (parts[2] << 8) + parts[3];
|
|
65
|
+
}
|
|
66
|
+
function inIpv4Cidr(ip, cidr) {
|
|
67
|
+
const [base, bitsText] = cidr.split("/");
|
|
68
|
+
const baseIp = parseIpv4(base.trim());
|
|
69
|
+
const bits = bitsText === void 0 ? 32 : Number(bitsText);
|
|
70
|
+
if (baseIp === null || !Number.isInteger(bits) || bits < 0 || bits > 32) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
if (bits === 0) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
const mask = 4294967295 << 32 - bits >>> 0;
|
|
77
|
+
return (ip & mask) >>> 0 === (baseIp & mask) >>> 0;
|
|
78
|
+
}
|
|
79
|
+
function isLocalIpv6(host) {
|
|
80
|
+
if (!host.includes(":")) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
if (host === "::1" || host === "::") {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
const first = host.split(":")[0];
|
|
87
|
+
if (!first) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
const word = parseInt(first, 16);
|
|
91
|
+
if (Number.isNaN(word)) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return (word & 65472) === 65152 || (word & 65024) === 64512;
|
|
95
|
+
}
|
|
96
|
+
function detectLocality(baseUrl, localNetworks = DEFAULT_LOCAL_NETWORKS) {
|
|
97
|
+
const host = hostOfUrl(baseUrl);
|
|
98
|
+
if (!host) {
|
|
99
|
+
return "cloud";
|
|
100
|
+
}
|
|
101
|
+
if (host === "localhost" || LOCAL_SUFFIXES.some((s) => host.endsWith(s))) {
|
|
102
|
+
return "local";
|
|
103
|
+
}
|
|
104
|
+
const mapped = /^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/.exec(host);
|
|
105
|
+
const ip = parseIpv4(mapped ? mapped[1] : host);
|
|
106
|
+
if (ip !== null) {
|
|
107
|
+
if (ALWAYS_LOCAL_IPV4.some((c) => inIpv4Cidr(ip, c))) {
|
|
108
|
+
return "local";
|
|
109
|
+
}
|
|
110
|
+
return localNetworks.some((entry) => /^\d/.test(entry.trim()) && inIpv4Cidr(ip, entry.trim())) ? "local" : "cloud";
|
|
111
|
+
}
|
|
112
|
+
if (isLocalIpv6(host)) {
|
|
113
|
+
return "local";
|
|
114
|
+
}
|
|
115
|
+
for (const raw of localNetworks) {
|
|
116
|
+
const entry = raw.trim().toLowerCase();
|
|
117
|
+
if (!entry || /^\d/.test(entry)) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (entry.startsWith(".") ? host.endsWith(entry) : host === entry || host.endsWith(`.${entry}`)) {
|
|
121
|
+
return "local";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return "cloud";
|
|
125
|
+
}
|
|
126
|
+
function engineLocality(engine, localNetworks) {
|
|
127
|
+
if (engine.kind === "vscode-lm") {
|
|
128
|
+
return "cloud";
|
|
129
|
+
}
|
|
130
|
+
if (engine.locality === "local" || engine.locality === "cloud") {
|
|
131
|
+
return engine.locality;
|
|
132
|
+
}
|
|
133
|
+
return detectLocality(engine.baseUrl, localNetworks);
|
|
134
|
+
}
|
|
135
|
+
function localityMismatch(engine, localNetworks) {
|
|
136
|
+
if (engine.locality === "auto" || engine.kind === "vscode-lm") {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
return engine.locality !== detectLocality(engine.baseUrl, localNetworks);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/ai/defaults.ts
|
|
143
|
+
function defaultAiDocsOptions() {
|
|
144
|
+
return { maxHops: AI_DOCS_MAX_HOPS_DEFAULT, sourceShare: AI_DOCS_SOURCE_SHARE_DEFAULT, navigateBy: AI_DOCS_NAVIGATE_BY_DEFAULT };
|
|
145
|
+
}
|
|
146
|
+
function defaultAiSettings() {
|
|
147
|
+
return {
|
|
148
|
+
enabled: false,
|
|
149
|
+
allowCloud: false,
|
|
150
|
+
engines: [],
|
|
151
|
+
routing: { default: null, fallback: "anyLocal", tasks: {} },
|
|
152
|
+
features: {},
|
|
153
|
+
localNetworks: [...DEFAULT_LOCAL_NETWORKS],
|
|
154
|
+
showToolbarButton: true,
|
|
155
|
+
activityPanel: false,
|
|
156
|
+
docBases: [],
|
|
157
|
+
docs: defaultAiDocsOptions()
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/shared/tagSyntax.ts
|
|
162
|
+
var SEGMENT = "[A-Za-z0-9_-]+";
|
|
163
|
+
var TAG_SOURCE = `(?<![\\w/#&])#(${SEGMENT}(?:/${SEGMENT})*)`;
|
|
164
|
+
function inlineTagRegex() {
|
|
165
|
+
return new RegExp(TAG_SOURCE, "g");
|
|
166
|
+
}
|
|
167
|
+
function isValidTag(tag) {
|
|
168
|
+
return /[A-Za-z]/.test(tag);
|
|
169
|
+
}
|
|
170
|
+
function normalizeTag(raw) {
|
|
171
|
+
const t = raw.trim().replace(/^#+/, "").toLowerCase();
|
|
172
|
+
if (!t || !isValidTag(t)) return null;
|
|
173
|
+
return t;
|
|
174
|
+
}
|
|
175
|
+
function looksLikeHexColor(tag) {
|
|
176
|
+
if (!/^[0-9a-f]+$/i.test(tag)) return false;
|
|
177
|
+
return /[0-9]/.test(tag) || tag.length === 6 || tag.length === 8;
|
|
178
|
+
}
|
|
179
|
+
function findInlineTags(text) {
|
|
180
|
+
const re = inlineTagRegex();
|
|
181
|
+
const out = [];
|
|
182
|
+
let m;
|
|
183
|
+
while ((m = re.exec(text)) !== null) {
|
|
184
|
+
if (!isValidTag(m[1]) || looksLikeHexColor(m[1])) continue;
|
|
185
|
+
out.push({ tag: m[1], index: m.index, length: m[0].length });
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export {
|
|
191
|
+
normalizeTag,
|
|
192
|
+
findInlineTags,
|
|
193
|
+
AI_ENGINE_KINDS,
|
|
194
|
+
AI_HOST_ENGINE_KINDS,
|
|
195
|
+
AI_CAPABILITY_FLAGS,
|
|
196
|
+
DEFAULT_CONTEXT_TOKENS,
|
|
197
|
+
AI_FALLBACK_MODES,
|
|
198
|
+
AI_DOCS_NAVIGATE_BY,
|
|
199
|
+
AI_DOCS_MAX_HOPS_MIN,
|
|
200
|
+
AI_DOCS_MAX_HOPS_MAX,
|
|
201
|
+
AI_TURN_GRANT_MAX_USES,
|
|
202
|
+
AI_DOCS_MAX_HOPS_DEFAULT,
|
|
203
|
+
AI_DOCS_SOURCE_SHARE_MIN,
|
|
204
|
+
AI_DOCS_SOURCE_SHARE_MAX,
|
|
205
|
+
AI_DOCS_SOURCE_SHARE_DEFAULT,
|
|
206
|
+
AI_DOCS_NAVIGATE_BY_DEFAULT,
|
|
207
|
+
AI_ERROR_CODES,
|
|
208
|
+
DEFAULT_LOCAL_NETWORKS,
|
|
209
|
+
hostOfUrl,
|
|
210
|
+
detectLocality,
|
|
211
|
+
engineLocality,
|
|
212
|
+
localityMismatch,
|
|
213
|
+
defaultAiDocsOptions,
|
|
214
|
+
defaultAiSettings
|
|
215
|
+
};
|
|
216
|
+
//# sourceMappingURL=chunk-APPBNQEY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ai/types.ts","../src/ai/locality.ts","../src/ai/defaults.ts","../src/shared/tagSyntax.ts"],"sourcesContent":["/**\n * AI provider layer contracts: the settings shape (engines, models, routing,\n * features) and the `ai` host group.\n *\n * Everything under `packages/core/src/ai/` is pure: no network, no DOM, no\n * provider SDK. Inference runs in the host (VS Code extension host, the\n * Desktop Rust side, or an npm consumer's own backend); core only resolves\n * which engine and model should serve a request and renders the UI.\n *\n * Open-ended by design: `routing.tasks` and `features` are open maps, and the\n * parser keeps keys it does not understand on every object (top level,\n * engines, models, routes, feature entries), so a newer version's settings\n * survive a save from an older one. Those extra keys ride along at runtime\n * and are not part of these types.\n */\n\n// ── Engines and models ──────────────────────────────────────────────────────\n\n/**\n * How the host talks to an engine. `openai-compatible` covers oMLX, vLLM,\n * llama.cpp server and the hosted OpenAI-style APIs (OpenRouter, Groq,\n * Gemini, Mistral, DeepSeek, xAI). `vscode-lm` is a model VS Code itself\n * offers (GitHub Copilot, through `vscode.lm`): no address, no key, VS Code\n * host only (see `AI_HOST_ENGINE_KINDS`). A settings file written by a newer\n * version may carry a kind this one does not know; such engines are kept on\n * save but never routed (see `isKnownEngineKind`).\n */\nexport type AiEngineKind = 'openai-compatible' | 'lmstudio' | 'ollama' | 'anthropic' | 'openai' | 'vscode-lm';\n\nexport const AI_ENGINE_KINDS: readonly AiEngineKind[] = ['openai-compatible', 'lmstudio', 'ollama', 'anthropic', 'openai', 'vscode-lm'];\n\n/**\n * Kinds only one host can run (no base URL, no key: the host's own model\n * API). A host runs one only when it reports a status for the engine\n * (`listEngines`); elsewhere such an engine is kept and never routed, the\n * same as an unknown kind. The kind picker offers them only to an engine\n * that already has one.\n */\nexport const AI_HOST_ENGINE_KINDS: readonly AiEngineKind[] = ['vscode-lm'];\n\n/** `auto` derives local or cloud from the base URL (see `detectLocality`). */\nexport type AiLocalitySetting = 'auto' | 'local' | 'cloud';\nexport type AiLocality = 'local' | 'cloud';\n\n/** Where a model's capability values came from. Re-discovery refreshes `endpoint` and `heuristic`, never `user`. */\nexport type AiCapabilitiesSource = 'endpoint' | 'heuristic' | 'user';\n\nexport interface AiModelCapabilities {\n /** Chat/instruct completion. */\n chat: boolean;\n /** SSE token streaming (assumed true unless the probe says otherwise). */\n stream: boolean;\n /** Image inputs. */\n vision: boolean;\n /** JSON mode or reliable structured output. */\n json: boolean;\n /** Function calling. */\n tools: boolean;\n /** Fill-in-the-middle / raw completion (ghost text). */\n fim: boolean;\n /** Embeddings endpoint. */\n embed: boolean;\n /** Context window in tokens; null when unknown (treated as `DEFAULT_CONTEXT_TOKENS`). */\n contextTokens: number | null;\n}\n\n/** The boolean capability flags, for chips and requirement checks. */\nexport type AiCapabilityFlag = Exclude<keyof AiModelCapabilities, 'contextTokens'>;\n\nexport const AI_CAPABILITY_FLAGS: readonly AiCapabilityFlag[] = ['chat', 'stream', 'vision', 'json', 'tools', 'fim', 'embed'];\n\n/** Context window assumed when neither the endpoint nor the id says (OPEN-10). */\nexport const DEFAULT_CONTEXT_TOKENS = 8192;\n\nexport interface AiModelConfig {\n /** The id the endpoint knows the model by (sent as `model`). Unique within its engine. */\n id: string;\n /** Display name. Absent means show the id. */\n label?: string;\n enabled: boolean;\n capabilities: AiModelCapabilities;\n capabilitiesSource: AiCapabilitiesSource;\n /**\n * Merged into the request body last, so it wins (OPEN-6): thinking controls\n * such as `reasoning_effort`, `thinking_budget`, `chat_template_kwargs`.\n */\n requestOverrides?: Record<string, unknown>;\n /** The last discovery did not list this model. Kept (greyed), never routed. */\n missing?: boolean;\n /** Added by hand. A discovery that does not list it leaves it alone (no `missing`). */\n manual?: boolean;\n}\n\nexport interface AiApiKeyFile {\n path: string;\n jsonPath: string;\n}\n\nexport interface AiEngineConfig {\n /** Stable, generated once, never shown. */\n id: string;\n name: string;\n kind: AiEngineKind;\n /** e.g. `http://127.0.0.1:8000/v1` or `https://api.anthropic.com`. */\n baseUrl: string;\n enabled: boolean;\n locality: AiLocalitySetting;\n /** Environment variable the host reads when no key is stored. The key itself is never in settings. */\n apiKeyEnv?: string;\n /**\n * A JSON file in the user's home folder the host reads when no key is stored and\n * `apiKeyEnv` is unset. `path` may start with `~/`; `jsonPath` is a dotted path to\n * a string value (`auth.api_key`). The key itself is never in settings.\n */\n apiKeyFile?: AiApiKeyFile;\n /** `auto` lists models from the endpoint and merges them with manual ones; `manual` never asks. */\n discovery: 'auto' | 'manual';\n models: AiModelConfig[];\n}\n\n// ── Routing and features ────────────────────────────────────────────────────\n\nexport interface AiRoute {\n engineId: string;\n modelId: string;\n}\n\n/**\n * What to do when a task's route is unusable. `none` disables the surface,\n * `default` tries the default route, `anyLocal` then any usable local\n * model, `any` then cloud engines that pass the gate.\n */\nexport type AiFallbackMode = 'none' | 'default' | 'anyLocal' | 'any';\n\nexport const AI_FALLBACK_MODES: readonly AiFallbackMode[] = ['none', 'default', 'anyLocal', 'any'];\n\nexport interface AiRouting {\n /** Null until the user picks one. */\n default: AiRoute | null;\n fallback: AiFallbackMode;\n /** Open map keyed by task id (see `AI_TASKS`). An absent task uses `default`. */\n tasks: Record<string, AiRoute>;\n}\n\n/** One feature's saved state. Absent fields fall back to the feature registry. */\nexport interface AiFeatureSetting {\n enabled?: boolean;\n /** Task override, e.g. `{ task: 'vision' }`. */\n task?: string;\n /** `ghostText` only (see `AiGhostTextOptions`; read through `ghostTextOptions`). */\n delayMs?: number;\n maxTokens?: number;\n trigger?: AiGhostTrigger;\n localOnly?: boolean;\n}\n\n/** When Inline suggestions fire: after an idle pause, or only from the shortcut / menu. */\nexport type AiGhostTrigger = 'auto' | 'manual';\n\n/** The `features.ghostText` knobs with defaults applied and ranges clamped (`ghostTextOptions`). */\nexport interface AiGhostTextOptions {\n /** Idle time after a keystroke before a request fires, in ms. Default 400. */\n delayMs: number;\n /** Output budget in tokens. Default 48. */\n maxTokens: number;\n /** Default `auto`. */\n trigger: AiGhostTrigger;\n /** Reject cloud engines for this feature. Default true. */\n localOnly: boolean;\n}\n\n/** Open map keyed by feature id (see `AI_FEATURES`). */\nexport type AiFeatureSettings = Record<string, AiFeatureSetting>;\n\n/** Everything under `mikedown.ai.*`. Built by `parseAiSettings`, saved by `serializeAiSettings`. */\nexport interface AiSettings {\n /** Master switch. Default false. */\n enabled: boolean;\n /** Global gate for cloud engines. Default false; per-workspace consent applies on top. */\n allowCloud: boolean;\n engines: AiEngineConfig[];\n routing: AiRouting;\n features: AiFeatureSettings;\n /** CIDR ranges (and `.suffix` host names) that count as local. Default RFC 1918. */\n localNetworks: string[];\n /** The AI button on the toolbar (hosts with AI support only). Default true. */\n showToolbarButton: boolean;\n /** Record what each AI request sent and received, in memory, and show it in the Activity panel. Default false. */\n activityPanel: boolean;\n /**\n * Documentation bases (`mikedown.ai.docBases`). User-level only: a host\n * reads this key from its user settings and never from a workspace, so a\n * repository cannot add or redirect a base. `parseAiSettings` drops invalid\n * entries with a reason (see `parseDocBases`).\n */\n docBases: AiDocBase[];\n /** Advanced options for chatting with the bases (`mikedown.ai.docs`), user-level only like `docBases`. */\n docs: AiDocsOptions;\n}\n\n// ── Documentation bases ─────────────────────────────────────────────────────\n\n/**\n * One documentation base: user-level folders of markdown the chat can\n * navigate and answer from. Roots are absolute, normalized folder paths.\n */\nexport interface AiDocBase {\n /** 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. */\n id: string;\n name: string;\n /** Absolute normalized folders (`validateDocRoot`). At least one. */\n roots: string[];\n /** Per-base override for the home document's file name, relative to each root (`HOME.md`). Absent: `INDEX.md`, `README.md`, then a map. */\n homeFile?: string;\n /** May pages from this base be sent to a cloud engine. Default false. */\n allowCloud: boolean;\n /** A disabled base is never indexed for or sent with a request. Default true. */\n enabled: boolean;\n}\n\nexport type AiDocsNavigateBy = 'section' | 'page';\n\nexport const AI_DOCS_NAVIGATE_BY: readonly AiDocsNavigateBy[] = ['section', 'page'];\n\n/** The advanced options (`ai.docs`), without the bases. */\nexport interface AiDocsOptions {\n /** Navigation calls before the answer. Clamped to `AI_DOCS_MAX_HOPS_MIN`..`AI_DOCS_MAX_HOPS_MAX`. Default 3. */\n maxHops: number;\n /** 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. */\n sourceShare: number;\n /** Whether the model asks for `page#heading` sections or whole pages. Default `section`. */\n navigateBy: AiDocsNavigateBy;\n}\n\n/** The options the loop runs with: `ai.docs` plus the bases (`resolveDocsSettings`). */\nexport interface AiDocsSettings extends AiDocsOptions {\n bases: AiDocBase[];\n}\n\nexport const AI_DOCS_MAX_HOPS_MIN = 1;\nexport const AI_DOCS_MAX_HOPS_MAX = 5;\n/**\n * The most requests one \"Allow once\" can admit in a chat turn: a navigation\n * call and its one repair per hop, then the answer. Hosts stop honouring a grant\n * past this, so a webview that never ends its turn cannot keep sending on it.\n * `TURN_GRANT_MAX_USES` in Desktop's `commands/ai.rs` is the same number.\n */\nexport const AI_TURN_GRANT_MAX_USES = AI_DOCS_MAX_HOPS_MAX * 2 + 1;\nexport const AI_DOCS_MAX_HOPS_DEFAULT = 3;\nexport const AI_DOCS_SOURCE_SHARE_MIN = 30;\nexport const AI_DOCS_SOURCE_SHARE_MAX = 80;\nexport const AI_DOCS_SOURCE_SHARE_DEFAULT = 60;\nexport const AI_DOCS_NAVIGATE_BY_DEFAULT: AiDocsNavigateBy = 'section';\n\n// ── Documentation bases: what a host reports ────────────────────────────────\n\n/** Where a root's home document came from (decision 3). `map` means none was found and core builds an in-memory map. */\nexport type AiDocHomeKind = 'override' | 'index' | 'readme' | 'map';\n\n/** One root of a base, as the host last saw it. */\nexport interface AiDocRootStatus {\n /** The root as configured (absolute). */\n root: string;\n /** The label ids use for this root (the same rule on both hosts; `docs/index.ts`). */\n label: string;\n /** The folder exists and is readable. False: \"Root not found\"; the root is skipped for requests, never deleted. */\n found: boolean;\n /** Which home precedence step won; `map` is the \"No home document\" state. Absent when the root is not found. */\n home?: AiDocHomeKind;\n /** The home document's path relative to the root, when `home` is not `map`. */\n homePath?: string;\n /** Markdown files indexed under this root. */\n fileCount: number;\n /** Files over the byte cap, skipped. */\n oversizeSkipped: number;\n /** The file cap was reached; later files were not indexed. */\n capReached: boolean;\n /** Files that are not UTF-8 text (a UTF-16 or UTF-32 byte order mark, or NUL bytes), skipped. */\n notTextSkipped?: number;\n /** The folder cap was reached; later folders were not read. */\n dirCapReached?: boolean;\n /** The build's time budget ran out; the index covers what was read before that. */\n timedOut?: boolean;\n}\n\n/**\n * A base plus its health, as `listBases` reports it. Computed when the\n * manager page opens and on Refresh. \"Active\" means enabled with at least one\n * root found; only active bases are indexed for or sent to a request.\n */\nexport interface AiDocBaseStatus {\n base: AiDocBase;\n /** Enabled and at least one root found. */\n active: boolean;\n roots: AiDocRootStatus[];\n /** Sum over roots. */\n fileCount: number;\n oversizeSkipped: number;\n /** Roots not found on disk. */\n rootsMissing: number;\n /** Any root hit the file cap. */\n capReached: boolean;\n /** Files skipped because they are not UTF-8 text, over every root. */\n notTextSkipped?: number;\n /** Any root hit the folder cap. */\n dirCapReached?: boolean;\n /** The build's time budget ran out; the index and the counts cover what was read before that. */\n timedOut?: boolean;\n /** Relative links that leave every root or point at no page. Absent when the host did not compute it. */\n brokenLinks?: number;\n}\n\n/** A heading in the index (a section starts at each). Bodies are never in the index. */\nexport interface AiDocsHeading {\n /** The section id: `<page id>#<slug>`. */\n id: string;\n /** 1 to 6. */\n level: number;\n text: string;\n slug: string;\n}\n\n/** A link as written in a page. Core resolves it (`docs/links.ts`); the host only extracts. */\nexport interface AiDocsLink {\n /** The target as written: a relative path with an optional `#fragment`, or a wikilink name. */\n target: string;\n /** The link text. */\n text: string;\n kind: 'markdown' | 'wikilink';\n}\n\n/** One markdown page of a base, without its body. */\nexport interface AiDocsPage {\n /** `<root label>/<rel path>`, with `/` separators (`docs/index.ts`). The only thing the model names. */\n id: string;\n /** The root's label (the first segment of `id`). */\n root: string;\n /** Path relative to the root, with `/` separators. */\n relPath: string;\n /** Frontmatter title, else the first heading, else the file name. */\n title: string;\n headings: AiDocsHeading[];\n links: AiDocsLink[];\n /** Size of the file in characters (not bytes), for the budget. */\n chars: number;\n}\n\n/** The home document chosen for a root. */\nexport interface AiDocsHome {\n /** The root's label. */\n root: string;\n kind: AiDocHomeKind;\n /** The home page's id; null for `map`. */\n pageId: string | null;\n}\n\n/** A base's index: pages with headings and links, no bodies. Built lazily on the host, in memory. */\nexport interface AiDocsIndex {\n baseId: string;\n /** Roots that were found, in configured order. */\n roots: Array<{ label: string }>;\n pages: AiDocsPage[];\n homes: AiDocsHome[];\n /** The file cap cut the walk short. */\n truncated: boolean;\n oversizeSkipped: number;\n /** Unix milliseconds the index was built. */\n builtAt: number;\n}\n\n/** A section (or whole page) as `readSections` returns it: the only call that carries a body. */\nexport interface AiDocsSection {\n /** The id that was asked for: `<page id>#<slug>` or a page id. */\n id: string;\n pageId: string;\n /** The page's title. */\n title: string;\n /** Path relative to the root, for the chip and the relative link. */\n relPath: string;\n /** The section's heading text; absent for a whole page. */\n heading?: string;\n /** The markdown, capped at the section byte cap by the host. */\n text: string;\n /** The host cut `text` at the cap. */\n truncated: boolean;\n}\n\n/** A keyword match: ids and labels only, no text. The loop reads the bodies through `readSections`. */\nexport interface AiDocsSectionRef {\n /** `<page id>#<slug>`, or a page id for text before the first heading. */\n id: string;\n pageId: string;\n title: string;\n relPath: string;\n heading?: string;\n /** Higher is a better match; the order is `docs/keyword.ts`'s. */\n score: number;\n}\n\n// ── Host group ──────────────────────────────────────────────────────────────\n\nexport type AiMessagePart =\n | { type: 'text'; text: string }\n | { type: 'image'; mime: string; bytes: Uint8Array };\n\nexport interface AiMessage {\n role: 'system' | 'user' | 'assistant';\n content: string | AiMessagePart[];\n}\n\nexport interface AiCompletionRequest {\n requestId: string;\n /** Feature id, for logging and consent copy. */\n feature: string;\n /** Task id, informational for tier-1 hosts. */\n task: string;\n /** Resolved by core; tier-1 hosts may ignore it. */\n route?: AiRoute;\n messages: AiMessage[];\n format?: 'text' | 'json';\n maxTokens?: number;\n temperature?: number;\n /**\n * Per-request body fields (the popover's thinking dial). The host merges\n * them over the model's configured `requestOverrides`, deeply, so the\n * request wins. Tier-1 hosts may ignore them.\n */\n requestOverrides?: Record<string, unknown>;\n /** Set on every request of a chat turn (`AiRoutePin`); absent on every other request. */\n turn?: AiCompletionTurn;\n}\n\n/**\n * What a request of a chat turn says about that turn. A host uses it to find\n * the \"Allow once\" it holds for the turn, and to word a dialog it has to raise\n * in the middle of one (`sends`, and `baseIds`, which it maps to names from its\n * own settings). It is a claim, not consent: a host admits the request only\n * against a grant it made itself when the user answered, for this panel or\n * window, this turn, this engine and this set of bases.\n */\nexport interface AiCompletionTurn {\n id: string;\n sends?: AiSendsScope;\n baseIds?: string[];\n /** The engine's locality when the turn was routed. A host fails the request if it no longer holds. */\n isLocal?: boolean;\n}\n\nexport interface AiServedBy {\n engineId?: string;\n /** e.g. \"oMLX (Mac Studio)\". */\n engineName: string;\n modelId: string;\n isLocal: boolean;\n}\n\nexport type AiFinishReason = 'stop' | 'length' | 'cancelled' | 'error';\n\nexport interface AiUsage {\n promptTokens?: number;\n completionTokens?: number;\n}\n\n/**\n * How long a completion took, measured by the host from sending the request.\n * Optional on the wire: an older host sends none.\n */\nexport interface AiTiming {\n /** To the end of the answer. */\n totalMs: number;\n /** To the first output, answer or reasoning. Absent when the server did not stream. */\n firstTokenMs?: number;\n /** Characters of output (answer plus reasoning), to estimate tokens when the engine reports no usage. */\n outputChars?: number;\n}\n\nexport interface AiCompletionResult {\n text: string;\n served: AiServedBy;\n usage?: AiUsage;\n finishReason?: AiFinishReason;\n timing?: AiTiming;\n}\n\n/** One model as the host last saw it. */\nexport interface AiEngineStatusModel {\n id: string;\n label?: string;\n capabilities: AiModelCapabilities;\n source: AiCapabilitiesSource;\n missing?: boolean;\n /** Loaded in memory right now (LM Studio, Ollama report it). */\n loaded?: boolean;\n /** What the server says the model takes, in bytes (oMLX, LM Studio, Ollama report it). */\n sizeBytes?: number;\n /** The server says it is a mixture of experts (LM Studio `arch`, oMLX `config_model_type`). */\n moe?: boolean;\n}\n\n/** Where a host found an engine's key: its secret store, a variable, or a key file. */\nexport type AiKeySource = 'stored' | 'env' | 'file';\n\nexport interface AiEngineStatus {\n engineId: string;\n /** Null = never probed. */\n ok: boolean | null;\n isLocal: boolean;\n hasKey: boolean;\n /**\n * Where the key came from, when `hasKey` and the host knows: its secret store,\n * the engine's `apiKeyEnv` variable, or its `apiKeyFile`. Absent for no key or\n * a host that does not say (the engine form then guesses from the engine's\n * settings).\n */\n keySource?: AiKeySource;\n latencyMs?: number;\n error?: string;\n models: AiEngineStatusModel[];\n}\n\n/**\n * One local model server as `detectLocalServers` found it: whether its preset's\n * address answered, and how many models it listed when it did.\n */\nexport interface AiLocalServerProbe {\n presetId: string;\n baseUrl: string;\n ok: boolean;\n /** Models the server listed (embedding models left out where the server says which they are). */\n models?: number;\n latencyMs?: number;\n}\n\n/**\n * A model API built into the host (VS Code's `vscode.lm`, which GitHub\n * Copilot serves), as `detectHostEngines` found it. Nothing is saved.\n */\nexport interface AiHostEngineProbe {\n /** The preset to add it from (`vscodelm`). */\n presetId: string;\n kind: AiEngineKind;\n /** At least one chat model is available right now. */\n ok: boolean;\n /** Chat models available. */\n models: number;\n /** Why none is available, when the host knows (no Copilot extension, signed out). */\n error?: string;\n}\n\n/**\n * What a host can tell about this machine, for the setup wizard's \"can it\n * run models locally\" hint (`recommendSetupPath`, `modelSizeHint`). A rough\n * call, not a benchmark: VS Code reads no GPU at all.\n */\nexport interface AiHardwareInfo {\n /** Node's `process.arch` names: `arm64`, `x64`. */\n arch: string;\n /** Node's `process.platform` names: `darwin`, `win32`, `linux`. */\n platform: string;\n totalMemoryBytes: number;\n /** macOS on arm64: the GPU shares `totalMemoryBytes` (unified memory). */\n appleSilicon: boolean;\n /** CPU brand string, when the host read one (\"Apple M2 Max\"). */\n cpu?: string;\n /** A discrete GPU, when the host could read one (Desktop: NVIDIA through `nvidia-smi`; Apple Silicon reports `apple`). */\n gpu?: { vendor: 'nvidia' | 'amd' | 'apple' | 'other'; vramBytes?: number; name?: string };\n}\n\nexport type AiErrorCode =\n | 'unreachable'\n | 'auth'\n | 'modelNotFound'\n | 'modelLoadFailed'\n | 'contextOverflow'\n | 'rateLimited'\n | 'cloudBlocked'\n | 'consentDenied'\n | 'cancelled'\n | 'badOutput'\n | 'unknown';\n\nexport const AI_ERROR_CODES: readonly AiErrorCode[] = [\n 'unreachable',\n 'auth',\n 'modelNotFound',\n 'modelLoadFailed',\n 'contextOverflow',\n 'rateLimited',\n 'cloudBlocked',\n 'consentDenied',\n 'cancelled',\n 'badOutput',\n 'unknown',\n];\n\n/** What `MikeDownAiHost` methods reject with. A plain object, so it crosses postMessage and Tauri events. */\nexport interface AiError {\n code: AiErrorCode;\n message: string;\n /** The engine the failed request went to (or the router turned down), when known. */\n engineId?: string;\n}\n\n/**\n * The `ai` group on `MikeDownHost`. Absent turns every AI surface off.\n *\n * Tier 1 (`complete` only): core shows the master switch, feature toggles and\n * the indicator from whatever `served` the host reports. Tier 2 (plus the\n * registry methods): core shows the full AI tab and routes.\n */\nexport interface MikeDownAiHost {\n // Tier 1: enough for every feature.\n complete(req: AiCompletionRequest, onToken: (delta: string) => void, signal: AbortSignal): Promise<AiCompletionResult>;\n embed?(texts: string[], route?: AiRoute, signal?: AbortSignal): Promise<number[][]>;\n transcribe?(audio: { mime: string; bytes: Uint8Array }, signal?: AbortSignal): Promise<string>;\n\n // Tier 2: turns on the engine registry UI and core routing.\n listEngines?(): Promise<AiEngineStatus[]>;\n /**\n * Look for model servers on this machine at the local presets' default\n * addresses (`localDetectCandidates()`), one GET each, capped at\n * `AI_LOCAL_DETECT_TIMEOUT_MS`. Only on request (the first-run panel, the\n * quick-start cards), never in the background. The host looks each preset\n * up by id itself and sends a key (oMLX's key file) only to the preset's\n * own address. One result per candidate, in order. Nothing is saved.\n */\n detectLocalServers?(candidates: { presetId: string; baseUrl: string }[], signal: AbortSignal): Promise<AiLocalServerProbe[]>;\n /**\n * Model APIs built into the host that need no address or key (VS Code:\n * `vscode.lm`, GitHub Copilot's models), one result per `AI_HOST_ENGINE_KINDS`\n * kind the host runs. Lists models only; sends no text. Absent: the host\n * has none, and the `host` preset family stays hidden.\n */\n detectHostEngines?(): Promise<AiHostEngineProbe[]>;\n /**\n * This machine's memory, CPU and (where the host can read it) GPU, for the\n * setup wizard's local hint. Static, so hosts may answer from a cache; null\n * when the host cannot tell.\n */\n hardwareInfo?(): Promise<AiHardwareInfo | null>;\n testEngine?(engineId: string): Promise<AiEngineStatus>;\n discoverModels?(engineId: string): Promise<AiEngineStatus>;\n /** Store (or with null, delete) an engine's key. Never part of `saveSettings`. */\n setSecret?(engineId: string, apiKey: string | null): Promise<void>;\n /**\n * False when the host process may not see variables set in a shell profile\n * (MikeDown Desktop opened from the Dock or Start menu), so `apiKeyEnv` is\n * unreliable there and the engine form points at the key file or a pasted\n * key instead. Absent: the host reads them (VS Code resolves the shell\n * environment) or does not say.\n */\n readonly shellEnv?: boolean;\n /**\n * Host UI asking to use a cloud engine in this workspace. Absent means the\n * host decides on its own. `about` names the request that asked, so the\n * dialog can say what is sent (`aiConsentText`).\n */\n requestCloudConsent?(engineId: string, about?: AiConsentAbout): Promise<AiConsentReply>;\n /**\n * `requestCloudConsent` for a chat turn, where `'once'` means the whole turn:\n * the host holds the grant and admits the turn's requests (`req.turn`) on it.\n * Absent: core asks `requestCloudConsent`.\n */\n requestTurnConsent?(engineId: string, about: AiTurnConsentAbout): Promise<AiConsentReply>;\n /**\n * The chat turn `turnId` is over (answered, refused, failed or cancelled):\n * forget any \"Allow once\" held for it. Fire and forget. A host that holds no\n * grant per turn can leave it out.\n */\n endTurn?(turnId: string): void;\n /**\n * The cloud consents for this workspace or folder, for the Cloud consents\n * list on the AI tab: the stored answers, plus this session's \"Don't\n * allow\" (`deny`), so it can be forgotten without a restart. \"Allow once\"\n * leaves nothing to list.\n */\n listConsents?(): Promise<AiConsentRecord[]>;\n /** Forget the answer for an engine (stored or this session's), so the next request asks again. */\n forgetConsent?(engineId: string): Promise<void>;\n}\n\n/**\n * What `requestCloudConsent` resolves to: `true` allowed for the workspace\n * (remembered), `'once'` allowed for this request only (the next one asks\n * again), or for the whole turn from `requestTurnConsent`, `false` declined for this request (a host may also stop asking for\n * the session; it then answers `false` without a dialog), `'never'` a stored\n * refusal from an older version (the host answers it without asking). A host\n * that only knows allow and decline keeps returning booleans.\n */\nexport type AiConsentReply = boolean | 'once' | 'never';\n\n/** One stored cloud consent answer (`MikeDownAiHost.listConsents`). */\nexport interface AiConsentRecord {\n engineId: string;\n /** `allow` and `never` (older versions) are stored; `deny` is this session's \"Don't allow\". */\n answer: 'allow' | 'never' | 'deny';\n /** Unix milliseconds. */\n at: number;\n /** What the answer covers: VS Code's workspace, Desktop's folder. */\n scope: 'workspace' | 'folder';\n}\n\n/**\n * What a request sends to the engine, for the privacy copy (`whatIsSent`):\n * the selection with its neighbouring paragraphs, the whole document, the\n * document with its frontmatter and the workspace's tag names, pages from\n * the documentation bases the user turned on, or the text around the cursor\n * on every pause (Inline suggestions).\n */\nexport type AiSendsScope = 'selection' | 'document' | 'documentAndTags' | 'docsBase' | 'cursor';\n\n/**\n * The request behind a consent prompt. `sends` overrides the feature's own\n * scope (whole-document Translate; the docs chat sends `docsBase`).\n */\nexport interface AiConsentAbout {\n feature?: string;\n sends?: AiSendsScope;\n /**\n * The documentation bases the request would send pages from, by id. The\n * host maps ids to names from its own user-level settings and drops unknown\n * ids, so a name from the webview never reaches its modal.\n */\n baseIds?: string[];\n}\n\n/** A consent prompt from a chat turn (`AiCompletionTurn.id`): an \"Allow once\" holds for that turn and no other. */\nexport interface AiTurnConsentAbout extends AiConsentAbout {\n turnId: string;\n}\n\n// ── Docs host group ─────────────────────────────────────────────────────────\n\n/**\n * The `docs` group on `MikeDownHost`: documentation bases, read through the\n * host. Absent (npm consumers): the chat's scope picker shows \"This document\"\n * only. Defined next to `MikeDownAiHost` so hosts reach it through\n * `@mikedown/core/ai`; `host.ts` re-exports it.\n *\n * Containment is the host's job and is code, not prompt (S1, S2, S3, S9):\n *\n * - Every method takes a base id and resolves the roots from the host's own\n * user-level configuration. A root that configuration does not list is\n * refused, so a webview message can never name a folder. A disabled base is\n * refused.\n * - `readSections` looks ids up in the host's own index and refuses anything\n * else. No path is ever built from an id's text.\n * - Reads canonicalize the root and the file, refuse anything outside the\n * root (whole path components), non-markdown, symlinks, non-regular files\n * and oversize files.\n * - The model names ids from core's index and nothing else.\n */\nexport interface MikeDownDocsHost {\n /**\n * Every configured base with its health (disabled ones included). The bases\n * themselves (`enabled`, `allowCloud`, names) are read afresh on every call.\n * A plain call is cheap and must not walk a base that already has a usable\n * index: the chat loop calls it once per turn. `refresh: true` forces a walk\n * and fills the health (file counts, broken links); the manager page passes\n * it when its section opens and on Refresh.\n */\n listBases(opts?: { refresh?: boolean }): Promise<AiDocBaseStatus[]>;\n /** The base's index (pages, headings, links, homes; no bodies), built lazily and kept in memory. Rejects for an unknown, disabled or inactive base. */\n index(baseId: string): Promise<AiDocsIndex>;\n /**\n * Bodies for section ids (`<page id>#<slug>`) or page ids, each capped at\n * the section byte cap. Ids the host's index does not know are left out of\n * the result (never an error, never a path); a read that fails a\n * containment check is left out too. Order follows `ids`.\n */\n readSections(baseId: string, ids: string[]): Promise<AiDocsSection[]>;\n /** Case-folded scan over the base's files, headings weighted, best first, at most 20 (`docs/keyword.ts`). Ids and labels only. */\n keywordSearch(baseId: string, query: string): Promise<AiDocsSectionRef[]>;\n /** Tell core when the base's files change (a watcher per root only). Returns the function that stops watching. */\n watch?(baseId: string, onChange: () => void): () => void;\n /**\n * Save the user's bases. The host re-validates with `parseAiSettings` and\n * writes the user-level setting only, never a workspace one. Rejects when\n * nothing valid is left to write over something valid.\n */\n saveBases(bases: AiDocBase[]): Promise<void>;\n /** 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. */\n addWorkspaceFolder?(): Promise<string | null>;\n /** Desktop: the native folder picker; resolves the chosen absolute path, null when cancelled. */\n pickFolder?(): Promise<string | null>;\n}\n\n// ── Provider adapters (host side) ───────────────────────────────────────────\n\n/** A model as one discovery call reported it, before merging into settings. */\nexport interface AiDiscoveredModel {\n id: string;\n label?: string;\n /** Endpoint values; anything absent is filled from the id heuristics on merge. */\n capabilities: Partial<AiModelCapabilities>;\n /** `endpoint` when the server described the model, `heuristic` when only its id was listed. */\n source: 'endpoint' | 'heuristic';\n loaded?: boolean;\n /** Bytes the model takes, as the server reports it (oMLX `estimated_size`, LM Studio `size_bytes`, Ollama `size`). */\n sizeBytes?: number;\n /** The server says it is a mixture of experts: an architecture name with `moe` in it (LM Studio `arch`, oMLX `config_model_type`). */\n moe?: boolean;\n}\n\n/**\n * Host-side adapter shape. TypeScript in the extension (`src/ai/providers/`),\n * a Rust trait with the same shape on Desktop. Declared here so both hosts\n * share one contract; core never implements it.\n */\nexport interface ProviderAdapter {\n readonly kind: AiEngineKind;\n /** Discovery plus capability detection. */\n listModels(engine: AiEngineConfig, apiKey: string | null, signal: AbortSignal): Promise<AiDiscoveredModel[]>;\n /** Test connection: reachability, auth, latency. */\n probe(engine: AiEngineConfig, apiKey: string | null, signal: AbortSignal): Promise<{ ok: boolean; latencyMs: number; error?: AiError }>;\n /** Streaming completion against one model. */\n complete(\n engine: AiEngineConfig,\n apiKey: string | null,\n model: AiModelConfig,\n req: AiCompletionRequest,\n onToken: (delta: string) => void,\n signal: AbortSignal,\n ): Promise<AiCompletionResult>;\n embed?(engine: AiEngineConfig, apiKey: string | null, model: AiModelConfig, texts: string[], signal: AbortSignal): Promise<number[][]>;\n}\n","/**\n * Local or cloud, from an engine's base URL. Loopback, link-local, IPv6\n * unique-local, `localhost`, `.local`, `.lan`, `.home.arpa` and `.internal`\n * are always local; `localNetworks` adds CIDR ranges (RFC 1918 by default, a\n * WireGuard range such as `10.0.98.0/24`) and `.suffix` host names. Anything\n * else, including a URL that does not parse, is cloud.\n */\n\nimport type { AiEngineConfig, AiLocality } from './types';\n\n/** Default `mikedown.ai.localNetworks`: RFC 1918. */\nexport const DEFAULT_LOCAL_NETWORKS: readonly string[] = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'];\n\nconst ALWAYS_LOCAL_IPV4 = ['127.0.0.0/8', '169.254.0.0/16', '0.0.0.0/32'];\nconst LOCAL_SUFFIXES = ['.localhost', '.local', '.lan', '.home.arpa', '.internal'];\n\n/** Lower-cased host of a URL (no port, no brackets), or null. Accepts a bare `host:port`. */\nexport function hostOfUrl(url: string): string | null {\n const trimmed = url.trim();\n if (!trimmed) {return null;}\n const withoutScheme = /^[a-z][a-z0-9+.-]*:\\/\\//i.test(trimmed) ? trimmed.replace(/^[a-z][a-z0-9+.-]*:\\/\\//i, '') : trimmed;\n const authority = withoutScheme.split(/[/?#]/, 1)[0];\n const hostPort = authority.includes('@') ? authority.slice(authority.lastIndexOf('@') + 1) : authority;\n if (!hostPort) {return null;}\n if (hostPort.startsWith('[')) {\n const end = hostPort.indexOf(']');\n return end > 1 ? hostPort.slice(1, end).toLowerCase() : null;\n }\n // More than one colon and no brackets: a bare IPv6 address.\n if ((hostPort.match(/:/g) ?? []).length > 1) {return hostPort.toLowerCase();}\n const host = hostPort.split(':', 1)[0].toLowerCase().replace(/\\.$/, '');\n return host || null;\n}\n\nfunction parseIpv4(host: string): number | null {\n const m = /^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/.exec(host);\n if (!m) {return null;}\n const parts = m.slice(1).map(Number);\n if (parts.some((p) => p > 255)) {return null;}\n return ((parts[0] << 24) >>> 0) + (parts[1] << 16) + (parts[2] << 8) + parts[3];\n}\n\nfunction inIpv4Cidr(ip: number, cidr: string): boolean {\n const [base, bitsText] = cidr.split('/');\n const baseIp = parseIpv4(base.trim());\n const bits = bitsText === undefined ? 32 : Number(bitsText);\n if (baseIp === null || !Number.isInteger(bits) || bits < 0 || bits > 32) {return false;}\n if (bits === 0) {return true;}\n const mask = (0xffffffff << (32 - bits)) >>> 0;\n return ((ip & mask) >>> 0) === ((baseIp & mask) >>> 0);\n}\n\nfunction isLocalIpv6(host: string): boolean {\n if (!host.includes(':')) {return false;}\n if (host === '::1' || host === '::') {return true;}\n // IPv4-mapped (::ffff:192.168.1.2) is judged by its IPv4 part elsewhere.\n const first = host.split(':')[0];\n if (!first) {return false;}\n const word = parseInt(first, 16);\n if (Number.isNaN(word)) {return false;}\n // fe80::/10 link-local, fc00::/7 unique-local.\n return (word & 0xffc0) === 0xfe80 || (word & 0xfe00) === 0xfc00;\n}\n\n/**\n * `local` or `cloud` for a base URL. `localNetworks` entries are IPv4 CIDR\n * ranges (`10.0.98.0/24`, a bare address counts as /32) or host-name\n * suffixes (`.ts.net`, `studio.example.com`).\n */\nexport function detectLocality(baseUrl: string, localNetworks: readonly string[] = DEFAULT_LOCAL_NETWORKS): AiLocality {\n const host = hostOfUrl(baseUrl);\n if (!host) {return 'cloud';}\n if (host === 'localhost' || LOCAL_SUFFIXES.some((s) => host.endsWith(s))) {return 'local';}\n\n const mapped = /^::ffff:(\\d{1,3}(?:\\.\\d{1,3}){3})$/.exec(host);\n const ip = parseIpv4(mapped ? mapped[1] : host);\n if (ip !== null) {\n if (ALWAYS_LOCAL_IPV4.some((c) => inIpv4Cidr(ip, c))) {return 'local';}\n return localNetworks.some((entry) => /^\\d/.test(entry.trim()) && inIpv4Cidr(ip, entry.trim())) ? 'local' : 'cloud';\n }\n if (isLocalIpv6(host)) {return 'local';}\n\n for (const raw of localNetworks) {\n const entry = raw.trim().toLowerCase();\n if (!entry || /^\\d/.test(entry)) {continue;}\n if (entry.startsWith('.') ? host.endsWith(entry) : host === entry || host.endsWith(`.${entry}`)) {return 'local';}\n }\n return 'cloud';\n}\n\n/**\n * The engine's effective locality: its pin, or `detectLocality` for `auto`.\n * A `vscode-lm` engine is always cloud, whatever its pin: it has no address,\n * and the text goes to GitHub's Copilot service.\n */\nexport function engineLocality(engine: Pick<AiEngineConfig, 'baseUrl' | 'locality'> & { kind?: string }, localNetworks?: readonly string[]): AiLocality {\n if (engine.kind === 'vscode-lm') {return 'cloud';}\n if (engine.locality === 'local' || engine.locality === 'cloud') {return engine.locality;}\n return detectLocality(engine.baseUrl, localNetworks);\n}\n\n/**\n * True when a pinned locality disagrees with the URL (cloud pinned on a\n * private address, or local pinned on a public one). The indicator shows a\n * warning glyph for it.\n */\nexport function localityMismatch(engine: Pick<AiEngineConfig, 'baseUrl' | 'locality'> & { kind?: string }, localNetworks?: readonly string[]): boolean {\n if (engine.locality === 'auto' || engine.kind === 'vscode-lm') {return false;}\n return engine.locality !== detectLocality(engine.baseUrl, localNetworks);\n}\n","/**\n * The settings before the host sends any. Its own module so the editor can\n * start from them without loading the rest of the AI layer (see `ai-lite.ts`).\n */\n\nimport { DEFAULT_LOCAL_NETWORKS } from './locality';\nimport { AI_DOCS_MAX_HOPS_DEFAULT, AI_DOCS_NAVIGATE_BY_DEFAULT, AI_DOCS_SOURCE_SHARE_DEFAULT } from './types';\nimport type { AiDocsOptions, AiSettings } from './types';\n\n/** The advanced docs options before the user changes any. */\nexport function defaultAiDocsOptions(): AiDocsOptions {\n return { maxHops: AI_DOCS_MAX_HOPS_DEFAULT, sourceShare: AI_DOCS_SOURCE_SHARE_DEFAULT, navigateBy: AI_DOCS_NAVIGATE_BY_DEFAULT };\n}\n\nexport function defaultAiSettings(): AiSettings {\n return {\n enabled: false,\n allowCloud: false,\n engines: [],\n routing: { default: null, fallback: 'anyLocal', tasks: {} },\n features: {},\n localNetworks: [...DEFAULT_LOCAL_NETWORKS],\n showToolbarButton: true,\n activityPanel: false,\n docBases: [],\n docs: defaultAiDocsOptions(),\n };\n}\n","// Shared tag syntax — pure, no DOM or vscode imports so both the extension\n// host and the webview bundle can consume it (same cross-bundle pattern as\n// `frontmatterYaml.ts` / `imageDisplayPath.ts`).\n//\n// A tag is `#` followed by one or more `/`-separated segments of\n// [A-Za-z0-9_-]. It must NOT be preceded by a word char, `/`, `#`, or `&`\n// (so it won't fire inside URLs, `##` sequences, or HTML entities) and must\n// contain at least one letter (so pure numbers like `#1234` aren't tags).\n// Inline tags that look like hex colors (`#2563eb`, `#ffffff`, `#f0f`) are\n// skipped too; frontmatter `tags:` entries are explicit, so they aren't.\n// Nested tags use slashes: `#project/active`.\n\nconst SEGMENT = '[A-Za-z0-9_-]+';\nconst TAG_SOURCE = `(?<![\\\\w/#&])#(${SEGMENT}(?:/${SEGMENT})*)`;\n\n/** Fresh global regex each call (global regexes carry mutable lastIndex). */\nexport function inlineTagRegex(): RegExp {\n return new RegExp(TAG_SOURCE, 'g');\n}\n\n/** A tag is only valid if it contains at least one letter. */\nexport function isValidTag(tag: string): boolean {\n return /[A-Za-z]/.test(tag);\n}\n\n/** Normalize a tag for indexing/matching: strip a leading `#`, lowercase. */\nexport function normalizeTag(raw: string): string | null {\n const t = raw.trim().replace(/^#+/, '').toLowerCase();\n if (!t || !isValidTag(t)) return null;\n return t;\n}\n\n/** Hex-color-like tokens: any all-hex run containing a digit (`#0f`,\n * `#2563eb`), or 6/8 hex letters (`#ffffff`). All-letter words such as\n * `#bad`, `#cafe`, `#face` still count as tags. */\nexport function looksLikeHexColor(tag: string): boolean {\n if (!/^[0-9a-f]+$/i.test(tag)) return false;\n return /[0-9]/.test(tag) || tag.length === 6 || tag.length === 8;\n}\n\nexport interface InlineTagMatch {\n /** The tag without its leading `#`, original case. */\n tag: string;\n /** Offset of the `#` within the scanned string. */\n index: number;\n /** Length of the full `#tag` token. */\n length: number;\n}\n\n/** Find every inline `#tag` token in a single string. */\nexport function findInlineTags(text: string): InlineTagMatch[] {\n const re = inlineTagRegex();\n const out: InlineTagMatch[] = [];\n let m: RegExpExecArray | null;\n while ((m = re.exec(text)) !== null) {\n if (!isValidTag(m[1]) || looksLikeHexColor(m[1])) continue;\n out.push({ tag: m[1], index: m.index, length: m[0].length });\n }\n return out;\n}\n"],"mappings":"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","names":[]}
|