@dotdrelle/wiki-manager 0.6.31 → 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.js +1 -1
- 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 +104 -27
- package/src/commands/slash.test.js +68 -0
- package/src/core/compose.js +1 -1
- 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 +3 -3
|
@@ -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,
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
import YAML from 'yaml';
|
|
7
|
+
import { patchWikircProfile } from './wikirc.js';
|
|
8
|
+
import { writeVectorConfig } from './wikiSetup.js';
|
|
9
|
+
|
|
10
|
+
test('patchWikircProfile merges keys and preserves existing values', () => {
|
|
11
|
+
const root = mkdtempSync(join(tmpdir(), 'wikirc-patch-'));
|
|
12
|
+
const file = join(root, '.wikirc.yaml');
|
|
13
|
+
writeFileSync(file, [
|
|
14
|
+
'# workspace config',
|
|
15
|
+
'language: en-US',
|
|
16
|
+
'llm:',
|
|
17
|
+
' provider: openai',
|
|
18
|
+
' temperature: 0.2',
|
|
19
|
+
'',
|
|
20
|
+
].join('\n'), 'utf8');
|
|
21
|
+
|
|
22
|
+
patchWikircProfile(root, 'default', {
|
|
23
|
+
llm: {
|
|
24
|
+
model: 'gpt-5.4-mini',
|
|
25
|
+
apiKey: 'secret',
|
|
26
|
+
},
|
|
27
|
+
retrieval: {
|
|
28
|
+
vector: {
|
|
29
|
+
enabled: true,
|
|
30
|
+
embeddingModel: 'text-embedding-3-small',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const raw = readFileSync(file, 'utf8');
|
|
36
|
+
const parsed = YAML.parse(raw);
|
|
37
|
+
assert.match(raw, /# workspace config/);
|
|
38
|
+
assert.equal(parsed.language, 'en-US');
|
|
39
|
+
assert.equal(parsed.llm.provider, 'openai');
|
|
40
|
+
assert.equal(parsed.llm.temperature, 0.2);
|
|
41
|
+
assert.equal(parsed.llm.model, 'gpt-5.4-mini');
|
|
42
|
+
assert.equal(parsed.retrieval.vector.enabled, true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('writeVectorConfig writes llm-wiki vector and rerank keys', () => {
|
|
46
|
+
const root = mkdtempSync(join(tmpdir(), 'wikirc-vector-'));
|
|
47
|
+
writeFileSync(join(root, '.wikirc.yaml'), [
|
|
48
|
+
'language: en',
|
|
49
|
+
'llm:',
|
|
50
|
+
' provider: openai-compatible',
|
|
51
|
+
' baseUrl: http://localhost:8000/v1',
|
|
52
|
+
' apiKey: llm-key',
|
|
53
|
+
' model: chat-model',
|
|
54
|
+
'retrieval:',
|
|
55
|
+
' vector:',
|
|
56
|
+
' enabled: false',
|
|
57
|
+
'',
|
|
58
|
+
].join('\n'), 'utf8');
|
|
59
|
+
|
|
60
|
+
writeVectorConfig(root, 'default', {
|
|
61
|
+
baseUrl: 'http://localhost:7997/v1',
|
|
62
|
+
apiKey: 'vector-key',
|
|
63
|
+
embeddingModel: 'BAAI/bge-m3',
|
|
64
|
+
rerankEnabled: true,
|
|
65
|
+
rerankerModel: 'BAAI/bge-reranker-v2-m3',
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const parsed = YAML.parse(readFileSync(join(root, '.wikirc.yaml'), 'utf8'));
|
|
69
|
+
assert.equal(parsed.retrieval.vector.enabled, true);
|
|
70
|
+
assert.equal(parsed.retrieval.vector.baseUrl, 'http://localhost:7997/v1');
|
|
71
|
+
assert.equal(parsed.retrieval.vector.apiKey, 'vector-key');
|
|
72
|
+
assert.equal(parsed.retrieval.vector.embeddingModel, 'BAAI/bge-m3');
|
|
73
|
+
assert.equal(parsed.retrieval.vector.rerankEnabled, true);
|
|
74
|
+
assert.equal(parsed.retrieval.vector.rerankerModel, 'BAAI/bge-reranker-v2-m3');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('writeVectorConfig removes commented vector placeholders it replaces', () => {
|
|
78
|
+
const root = mkdtempSync(join(tmpdir(), 'wikirc-vector-comments-'));
|
|
79
|
+
writeFileSync(join(root, '.wikirc.yaml'), [
|
|
80
|
+
'language: en',
|
|
81
|
+
'llm:',
|
|
82
|
+
' provider: openai-compatible',
|
|
83
|
+
' baseUrl: http://localhost:8000/v1',
|
|
84
|
+
' apiKey: llm-key',
|
|
85
|
+
' model: chat-model',
|
|
86
|
+
'retrieval:',
|
|
87
|
+
' vector:',
|
|
88
|
+
' enabled: false',
|
|
89
|
+
' # Defaults to llm.baseUrl.',
|
|
90
|
+
' # baseUrl: http://127.0.0.1:7997/v1',
|
|
91
|
+
' # apiKey: your-vector-key',
|
|
92
|
+
' embeddingModel: BAAI/bge-m3',
|
|
93
|
+
' rerankerModel: BAAI/bge-reranker-v2-m3',
|
|
94
|
+
'',
|
|
95
|
+
].join('\n'), 'utf8');
|
|
96
|
+
|
|
97
|
+
writeVectorConfig(root, 'default', {
|
|
98
|
+
baseUrl: 'http://localhost:7997/v1',
|
|
99
|
+
apiKey: 'vector-key',
|
|
100
|
+
embeddingModel: 'BAAI/bge-m3',
|
|
101
|
+
rerankEnabled: true,
|
|
102
|
+
rerankerModel: 'custom-reranker',
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const raw = readFileSync(join(root, '.wikirc.yaml'), 'utf8');
|
|
106
|
+
const parsed = YAML.parse(raw);
|
|
107
|
+
assert.doesNotMatch(raw, /^\s*#\s*baseUrl:/m);
|
|
108
|
+
assert.doesNotMatch(raw, /^\s*#\s*apiKey:/m);
|
|
109
|
+
assert.equal(parsed.retrieval.vector.baseUrl, 'http://localhost:7997/v1');
|
|
110
|
+
assert.equal(parsed.retrieval.vector.apiKey, 'vector-key');
|
|
111
|
+
});
|
package/src/core/workspaces.js
CHANGED
|
@@ -49,7 +49,7 @@ export function findWorkspace(name) {
|
|
|
49
49
|
return listWorkspaces().find((workspace) => workspace.name === name);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
function isValidWorkspaceName(name) {
|
|
52
|
+
export function isValidWorkspaceName(name) {
|
|
53
53
|
return (
|
|
54
54
|
typeof name === 'string' &&
|
|
55
55
|
/^[a-zA-Z0-9][a-zA-Z0-9_.-]*[a-zA-Z0-9]$|^[a-zA-Z0-9]$/.test(name) &&
|
package/src/shell/LeftPane.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
|
-
import { For, createEffect, createMemo, createSignal } from 'solid-js';
|
|
2
|
+
import { For, createEffect, createMemo, createSignal, onCleanup } from 'solid-js';
|
|
3
3
|
import { colorForRenderedLine, helpCommandParts, keyValueParts, renderPlainMarkdown } from './renderer';
|
|
4
4
|
|
|
5
5
|
const LEGACY_DONNA_ROLE = 'do' + 't';
|
|
@@ -59,12 +59,12 @@ const STATUS_COLUMN_GAP = 2;
|
|
|
59
59
|
type HelpCard = { title: string; text: string; example: string };
|
|
60
60
|
|
|
61
61
|
const HELP_CARDS: HelpCard[] = [
|
|
62
|
+
{ title: '/use', text: 'Load one workspace.', example: 'Ex: /use <workspace>' },
|
|
62
63
|
{ title: '/new', text: 'Create/configure a workspace.', example: 'Ex: /new <name> [path]' },
|
|
63
|
-
{ title: '
|
|
64
|
-
{ title: '
|
|
65
|
-
{ title: '
|
|
66
|
-
{ title: '/
|
|
67
|
-
{ title: 'llm call action', text: 'Ask to LLM.', example: 'Ex: ask to LLM to run an action' },
|
|
64
|
+
{ title: '/config', text: 'Modify LLM configuration.', example: 'Ex: /config edit <profile>' },
|
|
65
|
+
{ title: '/mcp', text: 'View MCP.', example: 'Ex: /mcp status' },
|
|
66
|
+
{ title: 'configure CME', text: 'Add type, URL, PAT, email.', example: 'Ask to donna to set credentials' },
|
|
67
|
+
{ title: '/status', text: 'Check config.', example: 'Ex: /status' },
|
|
68
68
|
];
|
|
69
69
|
|
|
70
70
|
function HelpCardPanel(props: { card: HelpCard; width: number }) {
|
|
@@ -364,6 +364,7 @@ function conversationLines(messages: Array<{ role: string; content: string }>, c
|
|
|
364
364
|
if (isStatusOutput(message)) {
|
|
365
365
|
return [
|
|
366
366
|
{ segments: messageHeaderSegments(message.role, columns), copyContent: raw },
|
|
367
|
+
{ segments: [{ text: ' ', color: '#D6DEE8' }] },
|
|
367
368
|
...raw.split('\n').map((line) => {
|
|
368
369
|
const { left, right } = statusColumns(line || ' ');
|
|
369
370
|
return { status: true, statusLeft: left, statusRight: right, segments: [] };
|
|
@@ -379,6 +380,7 @@ function conversationLines(messages: Array<{ role: string; content: string }>, c
|
|
|
379
380
|
}
|
|
380
381
|
return [
|
|
381
382
|
{ segments: messageHeaderSegments(message.role, columns), copyContent: raw },
|
|
383
|
+
{ segments: [{ text: ' ', color: '#D6DEE8' }] },
|
|
382
384
|
...renderMarkdownLines(lines, message.role, columns),
|
|
383
385
|
{ segments: [{ text: ' ', color: '#D6DEE8' }] },
|
|
384
386
|
];
|
|
@@ -397,7 +399,7 @@ export function ConversationView(props: {
|
|
|
397
399
|
const allLines = createMemo(() => conversationLines(props.messages, props.columns));
|
|
398
400
|
const visibleLines = () => {
|
|
399
401
|
const lines = allLines();
|
|
400
|
-
const rows = Math.max(1, props.rows -
|
|
402
|
+
const rows = Math.max(1, props.rows - 2);
|
|
401
403
|
const maxScroll = Math.max(0, lines.length - rows);
|
|
402
404
|
const scroll = Math.min(props.scroll, maxScroll);
|
|
403
405
|
const end = lines.length - scroll;
|
|
@@ -406,7 +408,7 @@ export function ConversationView(props: {
|
|
|
406
408
|
};
|
|
407
409
|
const scrollHint = () => {
|
|
408
410
|
const lines = allLines();
|
|
409
|
-
const rows = Math.max(1, props.rows -
|
|
411
|
+
const rows = Math.max(1, props.rows - 2);
|
|
410
412
|
const maxScroll = Math.max(0, lines.length - rows);
|
|
411
413
|
const scroll = Math.min(props.scroll, maxScroll);
|
|
412
414
|
if (maxScroll === 0) return '';
|
|
@@ -483,6 +485,7 @@ export function ConversationView(props: {
|
|
|
483
485
|
)
|
|
484
486
|
)}
|
|
485
487
|
</For>
|
|
488
|
+
<text height={1} />
|
|
486
489
|
</box>
|
|
487
490
|
);
|
|
488
491
|
}
|
|
@@ -504,28 +507,34 @@ export function ChatInput(props: {
|
|
|
504
507
|
const minRows = 1;
|
|
505
508
|
const maxRows = 5;
|
|
506
509
|
const [textareaRows, setTextareaRows] = createSignal(minRows);
|
|
510
|
+
let disposed = false;
|
|
507
511
|
const idleColor = () => props.chatMode ? '#22C55E' : '#06B6D4';
|
|
508
512
|
const promptText = () => props.busy ? `${props.spinnerFrame} ` : props.prompt;
|
|
509
513
|
const boxHeight = () => textareaRows() + 2;
|
|
514
|
+
const inputColumns = () => Math.max(8, props.width - promptText().length - 6);
|
|
510
515
|
const textareaColumns = () => {
|
|
511
516
|
const measured = Number(textareaRef?.width ?? 0);
|
|
512
|
-
|
|
513
|
-
return Math.max(8,
|
|
517
|
+
const bounded = Math.min(inputColumns(), Number.isFinite(measured) && measured > 0 ? measured : inputColumns());
|
|
518
|
+
return Math.max(8, bounded - 1);
|
|
514
519
|
};
|
|
515
520
|
const estimatedVisualRows = (value: string) => {
|
|
516
521
|
const columns = textareaColumns();
|
|
517
522
|
return Math.max(1, value.split('\n').reduce((rows, line) => rows + Math.max(1, Math.ceil(line.length / columns)), 0));
|
|
518
523
|
};
|
|
519
524
|
const measuredVisualRows = (value: string) => {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
525
|
+
try {
|
|
526
|
+
const virtualRows = Number(textareaRef?.virtualLineCount ?? 0);
|
|
527
|
+
const logicalRows = Number(textareaRef?.lineCount ?? 0);
|
|
528
|
+
const scrollRows = Number(textareaRef?.scrollHeight ?? 0);
|
|
529
|
+
return Math.max(
|
|
530
|
+
estimatedVisualRows(value),
|
|
531
|
+
Number.isFinite(virtualRows) ? virtualRows : 0,
|
|
532
|
+
Number.isFinite(logicalRows) ? logicalRows : 0,
|
|
533
|
+
Number.isFinite(scrollRows) ? scrollRows : 0,
|
|
534
|
+
);
|
|
535
|
+
} catch {
|
|
536
|
+
return estimatedVisualRows(value);
|
|
537
|
+
}
|
|
529
538
|
};
|
|
530
539
|
const applyHeight = (rows: number) => {
|
|
531
540
|
const height = rows + 2;
|
|
@@ -533,11 +542,25 @@ export function ChatInput(props: {
|
|
|
533
542
|
if (containerRef) containerRef.height = height;
|
|
534
543
|
props.onHeightChange(height);
|
|
535
544
|
};
|
|
536
|
-
const
|
|
537
|
-
|
|
545
|
+
const safePlainText = () => {
|
|
546
|
+
try {
|
|
547
|
+
return String(textareaRef?.plainText ?? props.value ?? '');
|
|
548
|
+
} catch {
|
|
549
|
+
return String(props.value ?? '');
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
const updateRows = (value?: string) => {
|
|
553
|
+
if (disposed) return;
|
|
554
|
+
const text = value ?? safePlainText();
|
|
555
|
+
const rows = Math.min(maxRows, Math.max(minRows, measuredVisualRows(text)));
|
|
538
556
|
setTextareaRows(rows);
|
|
539
557
|
applyHeight(rows);
|
|
540
558
|
};
|
|
559
|
+
const queueUpdateRows = (value?: string) => {
|
|
560
|
+
queueMicrotask(() => {
|
|
561
|
+
if (!disposed) updateRows(value);
|
|
562
|
+
});
|
|
563
|
+
};
|
|
541
564
|
const syncTextareaValue = (value: string) => {
|
|
542
565
|
const current = String(textareaRef?.plainText ?? '');
|
|
543
566
|
if (!textareaRef || current === value) return;
|
|
@@ -549,16 +572,16 @@ export function ChatInput(props: {
|
|
|
549
572
|
// Some renderable states reject cursor movement while layout is settling.
|
|
550
573
|
}
|
|
551
574
|
updateRows(value);
|
|
552
|
-
|
|
575
|
+
queueUpdateRows(value);
|
|
553
576
|
};
|
|
554
577
|
const handleContentChange = () => {
|
|
555
|
-
const value =
|
|
578
|
+
const value = safePlainText();
|
|
556
579
|
props.onInput(value);
|
|
557
580
|
updateRows(value);
|
|
558
|
-
|
|
581
|
+
queueUpdateRows(value);
|
|
559
582
|
};
|
|
560
583
|
const submitCurrentValue = () => {
|
|
561
|
-
props.onSubmit(
|
|
584
|
+
props.onSubmit(safePlainText());
|
|
562
585
|
};
|
|
563
586
|
|
|
564
587
|
createEffect(() => {
|
|
@@ -573,7 +596,10 @@ export function ChatInput(props: {
|
|
|
573
596
|
props.width;
|
|
574
597
|
props.prompt;
|
|
575
598
|
props.spinnerFrame;
|
|
576
|
-
|
|
599
|
+
queueUpdateRows();
|
|
600
|
+
});
|
|
601
|
+
onCleanup(() => {
|
|
602
|
+
disposed = true;
|
|
577
603
|
});
|
|
578
604
|
|
|
579
605
|
return (
|
|
@@ -590,7 +616,7 @@ export function ChatInput(props: {
|
|
|
590
616
|
<text height={1} fg={props.busy ? '#FBBF24' : idleColor()}>{promptText()}</text>
|
|
591
617
|
<textarea
|
|
592
618
|
ref={textareaRef}
|
|
593
|
-
|
|
619
|
+
width={inputColumns()}
|
|
594
620
|
height={textareaRows()}
|
|
595
621
|
focused={props.focused && !props.busy}
|
|
596
622
|
initialValue={props.value}
|
|
@@ -599,7 +625,7 @@ export function ChatInput(props: {
|
|
|
599
625
|
keyBindings={[
|
|
600
626
|
{ name: 'return', action: 'submit' },
|
|
601
627
|
{ name: 'kpenter', action: 'submit' },
|
|
602
|
-
{ name: 'linefeed', action: '
|
|
628
|
+
{ name: 'linefeed', action: 'submit' },
|
|
603
629
|
{ name: 'return', shift: true, action: 'newline' },
|
|
604
630
|
{ name: 'kpenter', shift: true, action: 'newline' },
|
|
605
631
|
{ name: 'linefeed', shift: true, action: 'newline' },
|