@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.
- package/README.md +3 -2
- package/bin/wiki-manager +39 -1
- package/bin/wiki-manager.js +62 -3
- package/package.json +8 -7
- package/src/agent/graph.js +5 -10
- package/src/cli/wiki-manager.js +17 -2
- package/src/commands/slash.js +105 -27
- package/src/commands/slash.test.js +68 -0
- package/src/core/cacert.js +66 -0
- package/src/core/compose.js +10 -8
- package/src/core/env.js +13 -3
- package/src/core/mcp.js +1 -1
- package/src/core/modelFetch.js +97 -0
- package/src/core/modelFetch.test.js +38 -0
- package/src/core/startupCheck.js +130 -0
- package/src/core/wikiSetup.js +156 -0
- package/src/core/wikirc.js +80 -1
- package/src/core/wikirc.test.js +111 -0
- package/src/core/workspaces.js +1 -1
- package/src/shell/LeftPane.tsx +54 -28
- package/src/shell/RightPane.tsx +25 -2
- package/src/shell/SetupWizard.tsx +806 -0
- package/src/shell/SlashDialog.tsx +4 -3
- package/src/shell/repl.js +20 -8
- package/src/shell/tui.tsx +85 -13
- package/src/shell/useSession.ts +15 -7
- package/wiki-workspace +116 -23
package/src/core/compose.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
21
|
+
: join(managerStateDir(), '.env');
|
|
12
22
|
}
|
|
13
23
|
|
|
14
24
|
export function managerMcpEndpointsFile() {
|
|
15
|
-
return join(
|
|
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.
|
|
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
|
+
}
|
package/src/core/wikirc.js
CHANGED
|
@@ -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,
|