@dotdrelle/wiki-manager 0.6.30 → 0.6.34

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.
@@ -3,6 +3,7 @@ import { existsSync, readFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { promisify } from 'node:util';
5
5
  import YAML from 'yaml';
6
+ import { cacertEnv, ensureCacertComposeOverride } from './cacert.js';
6
7
  import { managerEnvFile, readEnvFile } from './env.js';
7
8
  import { managerRoot } from './workspaces.js';
8
9
 
@@ -93,14 +94,14 @@ function projectName(session) {
93
94
  }
94
95
 
95
96
  function composeBaseArgs(session) {
96
- const args = ['compose', '-f', composeFile(), '-p', projectName(session)];
97
+ const compose = composeFile();
98
+ const cacertOverride = ensureCacertComposeOverride(compose);
99
+ const args = ['compose', '-f', compose];
100
+ if (cacertOverride) args.push('-f', cacertOverride);
101
+ args.push('-p', projectName(session));
97
102
  const managerEnvPath = managerEnvFile();
98
- if (existsSync(managerEnvPath)) {
99
- args.push('--env-file', managerEnvPath);
100
- }
101
- if (session.workspaceEnvFile && existsSync(session.workspaceEnvFile)) {
102
- args.push('--env-file', session.workspaceEnvFile);
103
- }
103
+ if (existsSync(managerEnvPath)) args.push('--env-file', managerEnvPath);
104
+ if (session.workspaceEnvFile && existsSync(session.workspaceEnvFile)) args.push('--env-file', session.workspaceEnvFile);
104
105
  return args;
105
106
  }
106
107
 
@@ -109,6 +110,7 @@ function composeEnv(session) {
109
110
  ...process.env,
110
111
  ...readManagerEnv(),
111
112
  ...(session.workspaceEnv ?? {}),
113
+ ...cacertEnv(),
112
114
  WORKSPACE_NAME: session.workspace,
113
115
  WIKI_WORKSPACE_PATH: session.workspacePath,
114
116
  };
@@ -236,7 +238,7 @@ export async function composePs(session) {
236
238
  return runCompose(session, ['ps'], { timeout: 30_000 });
237
239
  }
238
240
 
239
- function parseComposePsJson(output) {
241
+ export function parseComposePsJson(output) {
240
242
  const text = output.trim();
241
243
  if (!text) return [];
242
244
  try {
package/src/core/env.js CHANGED
@@ -1,18 +1,28 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
- import { join, resolve } from 'node:path';
2
+ import { dirname, join, resolve } from 'node:path';
3
3
 
4
4
  export function userManagerDir() {
5
5
  return process.cwd();
6
6
  }
7
7
 
8
+ export function managerStateDir() {
9
+ return process.env.WIKI_MANAGER_ENV_FILE
10
+ ? dirname(resolve(process.env.WIKI_MANAGER_ENV_FILE))
11
+ : userManagerDir();
12
+ }
13
+
14
+ export function managerRuntimeDir() {
15
+ return join(managerStateDir(), '.wiki-manager');
16
+ }
17
+
8
18
  export function managerEnvFile() {
9
19
  return process.env.WIKI_MANAGER_ENV_FILE
10
20
  ? resolve(process.env.WIKI_MANAGER_ENV_FILE)
11
- : join(userManagerDir(), '.env');
21
+ : join(managerStateDir(), '.env');
12
22
  }
13
23
 
14
24
  export function managerMcpEndpointsFile() {
15
- return join(userManagerDir(), 'mcp.endpoints.json');
25
+ return join(managerStateDir(), 'mcp.endpoints.json');
16
26
  }
17
27
 
18
28
  function parseEnvValue(value) {
package/src/core/mcp.js CHANGED
@@ -211,7 +211,7 @@ async function mcpRequest(endpoint, method, params, signal, options = {}) {
211
211
  params: {
212
212
  protocolVersion: '2025-06-18',
213
213
  capabilities: {},
214
- clientInfo: { name: 'wiki-manager', version: '0.6.30' },
214
+ clientInfo: { name: 'wiki-manager', version: '0.6.34' },
215
215
  },
216
216
  }),
217
217
  });
@@ -0,0 +1,97 @@
1
+ const FALLBACK_MODELS = {
2
+ openai: ['gpt-5.4', 'gpt-5.4-mini', 'gpt-4.1', 'gpt-4.1-mini'],
3
+ anthropic: ['claude-sonnet-4-5', 'claude-opus-4-1', 'claude-3-7-sonnet-latest'],
4
+ ollama: ['llama3.2', 'qwen2.5', 'mistral', 'nomic-embed-text'],
5
+ 'openai-compatible': ['gpt-4.1-mini', 'llama3.2'],
6
+ other: ['gpt-4.1-mini', 'llama3.2'],
7
+ };
8
+
9
+ const FALLBACK_EMBEDDINGS = {
10
+ openai: ['text-embedding-3-small', 'text-embedding-3-large'],
11
+ anthropic: ['text-embedding-3-small'],
12
+ ollama: ['nomic-embed-text', 'mxbai-embed-large'],
13
+ 'openai-compatible': ['BAAI/bge-m3', 'text-embedding-3-small', 'nomic-embed-text'],
14
+ other: ['text-embedding-3-small', 'nomic-embed-text'],
15
+ };
16
+
17
+ export function normalizeProvider(provider) {
18
+ const value = String(provider ?? '').toLowerCase();
19
+ if (value.includes('compatible') || value.includes('other')) return 'openai-compatible';
20
+ if (value.includes('anthropic')) return 'anthropic';
21
+ if (value.includes('ollama')) return 'ollama';
22
+ if (value.includes('openai')) return 'openai';
23
+ return 'openai-compatible';
24
+ }
25
+
26
+ function fallbackFor(provider, kind) {
27
+ const normalized = normalizeProvider(provider);
28
+ const source = kind === 'embedding' ? FALLBACK_EMBEDDINGS : FALLBACK_MODELS;
29
+ return source[normalized] ?? source.other;
30
+ }
31
+
32
+ function endpointFor(provider, baseUrl) {
33
+ const normalized = normalizeProvider(provider);
34
+ if (normalized === 'anthropic') return 'https://api.anthropic.com/v1/models';
35
+ const root = String(baseUrl || (normalized === 'ollama' ? 'http://localhost:11434' : 'https://api.openai.com')).replace(/\/+$/g, '');
36
+ return normalized === 'ollama' ? `${root}/api/tags` : `${root}/v1/models`;
37
+ }
38
+
39
+ function headersFor(provider, apiKey) {
40
+ const normalized = normalizeProvider(provider);
41
+ if (normalized === 'ollama') return {};
42
+ if (normalized === 'anthropic') {
43
+ return {
44
+ 'x-api-key': apiKey,
45
+ 'anthropic-version': '2023-06-01',
46
+ };
47
+ }
48
+ return { Authorization: `Bearer ${apiKey}` };
49
+ }
50
+
51
+ function parseModelNames(provider, payload) {
52
+ const normalized = normalizeProvider(provider);
53
+ const items = normalized === 'ollama' ? payload?.models : payload?.data;
54
+ if (!Array.isArray(items)) return [];
55
+ return items
56
+ .map((item) => item?.id ?? item?.name ?? item?.model)
57
+ .filter(Boolean)
58
+ .map(String)
59
+ .sort((a, b) => a.localeCompare(b));
60
+ }
61
+
62
+ export async function fetchModels(provider, baseUrl, apiKey, options = {}) {
63
+ const normalized = normalizeProvider(provider);
64
+ if (normalized === 'anthropic') {
65
+ return { ok: false, models: fallbackFor(normalized, options.kind), source: 'fallback', error: 'Anthropic model listing is not supported' };
66
+ }
67
+ const timeoutMs = options.timeoutMs ?? 10000;
68
+ const controller = new AbortController();
69
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
70
+ try {
71
+ if (normalized !== 'ollama' && !apiKey) {
72
+ throw new Error('API key is required to fetch remote models');
73
+ }
74
+ const response = await fetch(endpointFor(normalized, baseUrl), {
75
+ headers: headersFor(normalized, apiKey),
76
+ signal: controller.signal,
77
+ });
78
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
79
+ const payload = await response.json();
80
+ const models = parseModelNames(normalized, payload);
81
+ if (models.length === 0) throw new Error('No models returned');
82
+ return { ok: true, models, source: 'remote' };
83
+ } catch (err) {
84
+ return {
85
+ ok: false,
86
+ models: fallbackFor(normalized, options.kind),
87
+ source: 'fallback',
88
+ error: err instanceof Error ? err.message : String(err),
89
+ };
90
+ } finally {
91
+ clearTimeout(timer);
92
+ }
93
+ }
94
+
95
+ export function fallbackModels(provider, kind) {
96
+ return fallbackFor(provider, kind);
97
+ }
@@ -0,0 +1,38 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { fallbackModels, fetchModels } from './modelFetch.js';
4
+
5
+ test('fetchModels returns remote OpenAI-compatible model ids', async () => {
6
+ const originalFetch = globalThis.fetch;
7
+ globalThis.fetch = async () => ({
8
+ ok: true,
9
+ json: async () => ({ data: [{ id: 'b-model' }, { id: 'a-model' }] }),
10
+ });
11
+ try {
12
+ const result = await fetchModels('openai', 'http://models.local', 'key', { timeoutMs: 100 });
13
+ assert.deepEqual(result, { ok: true, models: ['a-model', 'b-model'], source: 'remote' });
14
+ } finally {
15
+ globalThis.fetch = originalFetch;
16
+ }
17
+ });
18
+
19
+ test('fetchModels falls back on invalid remote response', async () => {
20
+ const originalFetch = globalThis.fetch;
21
+ globalThis.fetch = async () => ({
22
+ ok: true,
23
+ json: async () => ({ data: [] }),
24
+ });
25
+ try {
26
+ const result = await fetchModels('openai', 'http://models.local', 'key', { timeoutMs: 100 });
27
+ assert.equal(result.ok, false);
28
+ assert.equal(result.source, 'fallback');
29
+ assert.ok(result.models.includes('gpt-5.4-mini'));
30
+ assert.match(result.error, /No models returned/);
31
+ } finally {
32
+ globalThis.fetch = originalFetch;
33
+ }
34
+ });
35
+
36
+ test('fallbackModels leaves custom-model to the wizard append action', () => {
37
+ assert.deepEqual(fallbackModels('openai-compatible'), ['gpt-4.1-mini', 'llama3.2']);
38
+ });
@@ -0,0 +1,130 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ import { parseComposePsJson } from './compose.js';
6
+ import { listWikircProfiles, loadWikircProfile, summarizeWikircConfig } from './wikirc.js';
7
+ import { listWorkspaces, managerRoot, workspacesDir } from './workspaces.js';
8
+
9
+ const execFileAsync = promisify(execFile);
10
+
11
+ async function checkAgents() {
12
+ const composeFile = join(managerRoot(), 'agents.docker-compose.yml');
13
+ if (!existsSync(composeFile)) return null;
14
+ try {
15
+ const { stdout } = await execFileAsync('docker', [
16
+ 'compose',
17
+ '--project-directory',
18
+ managerRoot(),
19
+ '-f',
20
+ composeFile,
21
+ '-p',
22
+ 'wiki-agents',
23
+ 'ps',
24
+ '--format',
25
+ 'json',
26
+ ], {
27
+ cwd: managerRoot(),
28
+ env: {
29
+ ...process.env,
30
+ WORKSPACES_ROOT: workspacesDir(),
31
+ WIKI_WORKSPACES_DIR: workspacesDir(),
32
+ },
33
+ timeout: 5000,
34
+ maxBuffer: 1024 * 1024,
35
+ });
36
+ const entries = parseComposePsJson(stdout);
37
+ if (entries.length === 0) return null;
38
+ const downServices = entries
39
+ .filter((entry) => {
40
+ const state = String(entry.State ?? entry.state ?? entry.Status ?? entry.status ?? '').toLowerCase();
41
+ return !(state.includes('running') || state.includes('up'));
42
+ })
43
+ .map((entry) => entry.Service ?? entry.service ?? entry.Name ?? entry.name)
44
+ .filter(Boolean);
45
+ return downServices.length > 0 ? { kind: 'agents', context: { downServices } } : null;
46
+ } catch (err) {
47
+ if (err?.code === 'ENOENT') {
48
+ return { kind: 'agents', context: { dockerMissing: true } };
49
+ }
50
+ const stderr = String(err?.stderr ?? err?.message ?? '');
51
+ if (
52
+ stderr.includes('Cannot connect to the Docker daemon') ||
53
+ stderr.includes('Is the docker daemon running') ||
54
+ stderr.includes('docker daemon') ||
55
+ stderr.includes('connection refused')
56
+ ) {
57
+ return { kind: 'agents', context: { dockerUnavailable: true } };
58
+ }
59
+ // Timeout or unknown error — don't block startup with ambiguous state.
60
+ return null;
61
+ }
62
+ }
63
+
64
+ function checkWorkspace(workspaces) {
65
+ return workspaces.length === 0 ? { kind: 'workspace' } : null;
66
+ }
67
+
68
+ function checkWikirc(workspace) {
69
+ if (!workspace) return [];
70
+ const baseContext = {
71
+ workspaceName: workspace.name,
72
+ workspacePath: workspace.workspacePath,
73
+ profileName: 'default',
74
+ };
75
+ if (listWikircProfiles(workspace.workspacePath).length === 0) {
76
+ return [
77
+ {
78
+ kind: 'llm',
79
+ context: {
80
+ ...baseContext,
81
+ configError: 'No .wikirc.yaml profile found in the workspace.',
82
+ },
83
+ },
84
+ ];
85
+ }
86
+ try {
87
+ const loaded = loadWikircProfile(workspace.workspacePath, 'default');
88
+ const summary = summarizeWikircConfig(loaded.profile, loaded.config);
89
+ const context = {
90
+ workspaceName: workspace.name,
91
+ workspacePath: workspace.workspacePath,
92
+ profileName: loaded.profile.name,
93
+ };
94
+ const gaps = [];
95
+ if (!summary.hasApiKey || !summary.model) gaps.push({ kind: 'llm', context });
96
+ if (!summary.vectorEnabled) gaps.push({ kind: 'vector', context });
97
+ return gaps;
98
+ } catch {
99
+ return [
100
+ {
101
+ kind: 'llm',
102
+ context: {
103
+ ...baseContext,
104
+ configError: 'Default .wikirc.yaml profile could not be loaded.',
105
+ },
106
+ },
107
+ {
108
+ kind: 'vector',
109
+ context: {
110
+ ...baseContext,
111
+ configError: 'Default .wikirc.yaml profile could not be loaded.',
112
+ },
113
+ },
114
+ ];
115
+ }
116
+ }
117
+
118
+ export async function runChecks() {
119
+ const workspaces = listWorkspaces();
120
+ const agents = await checkAgents();
121
+ const gaps = [];
122
+ if (agents) gaps.push(agents);
123
+ const workspaceGap = checkWorkspace(workspaces);
124
+ if (workspaceGap) {
125
+ gaps.push(workspaceGap);
126
+ return gaps;
127
+ }
128
+ gaps.push(...checkWikirc(workspaces[0] ?? null));
129
+ return gaps;
130
+ }
@@ -0,0 +1,156 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { readFile, rename, rm, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ import { patchWikircProfile } from './wikirc.js';
6
+ import { createWorkspace, findWorkspace, isValidWorkspaceName, listWorkspaces, managerRoot, workspacesDir } from './workspaces.js';
7
+
8
+ const execFileAsync = promisify(execFile);
9
+
10
+ function wrapDockerError(err) {
11
+ if (err?.code === 'ENOENT') {
12
+ return new Error('wiki-workspace script not found. Reinstall wiki-manager.');
13
+ }
14
+ const message = String(err?.message ?? '');
15
+ const output = [err?.stderr, err?.stdout, message].filter(Boolean).map(String).join('\n');
16
+ if (output.includes('docker: command not found') || output.includes('docker: not found')) {
17
+ return new Error('Docker is not installed. Install Docker Desktop (https://docs.docker.com/get-docker/) and restart.');
18
+ }
19
+ if (output.includes('Cannot connect to the Docker daemon') || output.includes('Is the docker daemon running')) {
20
+ return new Error('Docker daemon is not running. Start Docker Desktop and try again.');
21
+ }
22
+ const commandMatch = message.match(/^Command failed:\s+(.+)$/m);
23
+ if (commandMatch) {
24
+ const [, commandLine] = commandMatch;
25
+ const rest = message.split(/\r?\n/).slice(1).join('\n').trim();
26
+ return new Error([
27
+ 'Command failed:',
28
+ commandLine,
29
+ rest,
30
+ ].filter(Boolean).join('\n'));
31
+ }
32
+ return err;
33
+ }
34
+
35
+ export async function startAgents(options = {}) {
36
+ try {
37
+ const { stdout, stderr } = await execFileAsync(join(managerRoot(), 'wiki-workspace'), ['agents', 'up'], {
38
+ cwd: managerRoot(),
39
+ env: {
40
+ ...process.env,
41
+ WIKI_WORKSPACES_DIR: workspacesDir(),
42
+ },
43
+ timeout: options.timeout ?? 180_000,
44
+ maxBuffer: options.maxBuffer ?? 1024 * 1024 * 8,
45
+ });
46
+ return [stdout, stderr].filter(Boolean).join('\n').trim();
47
+ } catch (err) {
48
+ throw wrapDockerError(err);
49
+ }
50
+ }
51
+
52
+ export async function stopAgents(options = {}) {
53
+ try {
54
+ const { stdout, stderr } = await execFileAsync(join(managerRoot(), 'wiki-workspace'), ['agents', 'down'], {
55
+ cwd: managerRoot(),
56
+ env: {
57
+ ...process.env,
58
+ WIKI_WORKSPACES_DIR: workspacesDir(),
59
+ },
60
+ timeout: options.timeout ?? 120_000,
61
+ maxBuffer: options.maxBuffer ?? 1024 * 1024 * 8,
62
+ });
63
+ return [stdout, stderr].filter(Boolean).join('\n').trim();
64
+ } catch (err) {
65
+ throw wrapDockerError(err);
66
+ }
67
+ }
68
+
69
+ export async function createNewWorkspace(name, targetPath) {
70
+ try {
71
+ const output = await createWorkspace(name, targetPath, { timeout: 600_000 });
72
+ const workspace = findWorkspace(name);
73
+ if (workspace) initializeWorkspaceWikirc(workspace);
74
+ return { output, workspace };
75
+ } catch (err) {
76
+ throw wrapDockerError(err);
77
+ }
78
+ }
79
+
80
+ export function initializeWorkspaceWikirc(workspace) {
81
+ const accessKey = workspace?.env?.WIKI_MCP_AUTH_TOKEN;
82
+ if (!workspace?.workspacePath || !accessKey) return null;
83
+ return patchWikircProfile(workspace.workspacePath, 'default', {
84
+ mcp: {
85
+ accessKey,
86
+ },
87
+ });
88
+ }
89
+
90
+ export async function unregisterWorkspace(nameOrWorkspace) {
91
+ const workspace = typeof nameOrWorkspace === 'string' ? findWorkspace(nameOrWorkspace) : nameOrWorkspace;
92
+ if (!workspace) throw new Error(`Workspace not found: ${nameOrWorkspace}`);
93
+ await rm(workspace.registryPath, { recursive: true, force: true });
94
+ return workspace;
95
+ }
96
+
97
+ export async function renameWorkspace(name, nextName) {
98
+ if (!isValidWorkspaceName(nextName)) throw new Error('Invalid workspace name.');
99
+ const all = listWorkspaces();
100
+ const workspace = all.find((w) => w.name === name);
101
+ if (!workspace) throw new Error(`Workspace not found: ${name}`);
102
+ if (all.find((w) => w.name === nextName)) throw new Error(`Workspace already exists: ${nextName}`);
103
+ const nextRegistryPath = join(workspacesDir(), nextName);
104
+ const envText = await readFile(workspace.envFile, 'utf8');
105
+ const nextEnvText = envText.match(/^WORKSPACE_NAME=/m)
106
+ ? envText.replace(/^WORKSPACE_NAME=.*$/m, `WORKSPACE_NAME=${nextName}`)
107
+ : `${envText.trimEnd()}\nWORKSPACE_NAME=${nextName}\n`;
108
+ const tmpEnvFile = join(workspace.registryPath, `.env.${process.pid}.tmp`);
109
+ await writeFile(tmpEnvFile, nextEnvText, 'utf8');
110
+ await rename(tmpEnvFile, workspace.envFile);
111
+ await rename(workspace.registryPath, nextRegistryPath);
112
+ return { previousName: name, name: nextName, registryPath: nextRegistryPath };
113
+ }
114
+
115
+ export async function deleteWorkspaceAndFiles(name, workspacePath) {
116
+ const workspace = await unregisterWorkspace(name);
117
+ const target = workspacePath || workspace.workspacePath;
118
+ await rm(target, { recursive: true, force: true });
119
+ return { workspace, deletedPath: target };
120
+ }
121
+
122
+ export function writeLanguageConfig(workspacePath, profileName, language) {
123
+ return patchWikircProfile(workspacePath, profileName || 'default', { language });
124
+ }
125
+
126
+ export function writeLlmConfig(workspacePath, profileName, config) {
127
+ const patches = {
128
+ llm: {
129
+ provider: config.provider,
130
+ ...(config.baseUrl ? { baseUrl: config.baseUrl } : {}),
131
+ ...(config.apiKey ? { apiKey: config.apiKey } : {}),
132
+ model: config.model,
133
+ },
134
+ };
135
+ return patchWikircProfile(workspacePath, profileName || 'default', patches);
136
+ }
137
+
138
+ export function writeVectorConfig(workspacePath, profileName, config) {
139
+ const patches = {
140
+ retrieval: {
141
+ vector: {
142
+ enabled: true,
143
+ ...(config.baseUrl ? { baseUrl: config.baseUrl } : {}),
144
+ ...(config.apiKey ? { apiKey: config.apiKey } : {}),
145
+ timeoutMs: config.timeoutMs ?? 600_000,
146
+ embeddingModel: config.embeddingModel,
147
+ rerankEnabled: Boolean(config.rerankEnabled),
148
+ ...(config.rerankerModel ? { rerankerModel: config.rerankerModel } : {}),
149
+ topK: config.topK ?? 120,
150
+ rerankTopK: config.rerankTopK ?? 80,
151
+ maxResults: config.maxResults ?? 6,
152
+ },
153
+ },
154
+ };
155
+ return patchWikircProfile(workspacePath, profileName || 'default', patches);
156
+ }
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync, readdirSync } from 'node:fs';
1
+ import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
2
2
  import { basename, join } from 'node:path';
3
3
  import YAML from 'yaml';
4
4
 
@@ -50,6 +50,85 @@ export function loadWikircProfile(workspacePath, profileName = 'default') {
50
50
  return { profile, config };
51
51
  }
52
52
 
53
+ function isPlainObject(value) {
54
+ return value && typeof value === 'object' && !Array.isArray(value);
55
+ }
56
+
57
+ function setYamlValue(map, key, value) {
58
+ if (isPlainObject(value)) {
59
+ let child = map.get(key, true);
60
+ if (!YAML.isMap(child)) {
61
+ child = new YAML.YAMLMap();
62
+ map.set(key, child);
63
+ }
64
+ mergeYamlMap(child, value);
65
+ return;
66
+ }
67
+ map.set(key, value);
68
+ }
69
+
70
+ function mergeYamlMap(map, patches) {
71
+ for (const [key, value] of Object.entries(patches ?? {})) {
72
+ setYamlValue(map, key, value);
73
+ }
74
+ }
75
+
76
+ function stripCommentedVectorKeys(raw, keys) {
77
+ const keySet = new Set(keys.filter(Boolean));
78
+ if (keySet.size === 0) return raw;
79
+
80
+ let inRetrieval = false;
81
+ let retrievalIndent = -1;
82
+ let inVector = false;
83
+ let vectorIndent = -1;
84
+
85
+ return raw.split(/\r?\n/).filter((line) => {
86
+ const nonComment = line.match(/^(\s*)([A-Za-z0-9_-]+):(?:\s|$)/);
87
+ if (nonComment) {
88
+ const indent = nonComment[1].length;
89
+ const key = nonComment[2];
90
+ if (inVector && indent <= vectorIndent) inVector = false;
91
+ if (inRetrieval && indent <= retrievalIndent) inRetrieval = false;
92
+ if (!inRetrieval && key === 'retrieval') {
93
+ inRetrieval = true;
94
+ retrievalIndent = indent;
95
+ } else if (inRetrieval && !inVector && indent > retrievalIndent && key === 'vector') {
96
+ inVector = true;
97
+ vectorIndent = indent;
98
+ }
99
+ }
100
+
101
+ if (inVector) {
102
+ const commentedKey = line.match(/^\s*#\s*([A-Za-z0-9_-]+):/);
103
+ if (commentedKey && keySet.has(commentedKey[1])) return false;
104
+ }
105
+ return true;
106
+ }).join('\n');
107
+ }
108
+
109
+ export function patchWikircProfile(workspacePath, profileName = 'default', patches = {}) {
110
+ const profile = resolveWikircProfile(workspacePath, profileName);
111
+ const vectorPatch = patches?.retrieval?.vector;
112
+ const commentedVectorKeysToStrip = [
113
+ vectorPatch?.baseUrl ? 'baseUrl' : null,
114
+ vectorPatch?.apiKey ? 'apiKey' : null,
115
+ ];
116
+ const raw = stripCommentedVectorKeys(readFileSync(profile.path, 'utf8'), commentedVectorKeysToStrip);
117
+ const doc = YAML.parseDocument(raw, {
118
+ schema: 'core',
119
+ keepSourceTokens: true,
120
+ });
121
+ if (doc.errors.length > 0) {
122
+ throw new Error(`wikirc YAML invalide: ${doc.errors[0].message}`);
123
+ }
124
+ if (!YAML.isMap(doc.contents)) {
125
+ throw new Error('wikirc YAML invalide: objet attendu a la racine');
126
+ }
127
+ mergeYamlMap(doc.contents, patches);
128
+ writeFileSync(profile.path, doc.toString(), 'utf8');
129
+ return { profile, patches };
130
+ }
131
+
53
132
  export function summarizeWikircConfig(profile, config) {
54
133
  return {
55
134
  profile: profile.name,