@dotdrelle/wiki-manager 0.15.29 → 0.15.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/.env.example +5 -2
- package/agents.docker-compose.yml +10 -0
- package/bin/wiki-manager.js +1 -1
- package/docker-compose.override.example.yml +1 -0
- package/package.json +2 -2
- package/src/agent/graph.test.js +17 -4
- package/src/cli/runtimeStartup.test.js +14 -0
- package/src/cli/wiki-manager.js +79 -13
- package/src/cli/wiki-manager.test.js +157 -0
- package/src/commands/slash.js +9 -0
- package/src/core/buildInfo.json +2 -2
- package/src/core/mcp.js +113 -38
- package/src/core/mcp.test.js +181 -20
- package/src/core/modelFetch.js +229 -41
- package/src/core/modelFetch.test.js +77 -2
- package/src/core/wikiSetup.js +32 -2
- package/src/core/wikiWorkspace.test.js +16 -0
- package/src/core/wikirc.test.js +59 -5
- package/src/orchestrator/agentRegistry.js +35 -7
- package/src/orchestrator/agentRegistry.test.js +73 -0
- package/src/orchestrator/dispatcher.js +6 -1
- package/src/orchestrator/dispatcher.test.js +24 -1
- package/src/runtime/auth.test.js +1 -65
- package/src/runtime/donna-contract.test.js +2 -1
- package/src/runtime/lifecycle.js +0 -33
- package/src/runtime/runner.test.js +10 -1
- package/src/runtime/supervisor.test.js +11 -1
- package/src/shell/SetupWizard.tsx +282 -31
- package/src/shell/setupWizardPlaceholders.test.js +1 -1
- package/src/shell/setupWizardSuggestions.test.js +24 -0
- package/src/shell/tui.tsx +10 -18
- package/wiki-workspace +70 -0
|
@@ -3,7 +3,15 @@ import { execFileSync } from 'node:child_process';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { useKeyboard, usePaste } from '@opentui/solid';
|
|
5
5
|
import { createEffect, createMemo, createSignal, For, Show } from 'solid-js';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
defaultBaseUrl,
|
|
8
|
+
fallbackModels,
|
|
9
|
+
fetchGatewayCatalog,
|
|
10
|
+
fetchModels,
|
|
11
|
+
normalizeEngine,
|
|
12
|
+
normalizeProvider,
|
|
13
|
+
requiresBaseUrl,
|
|
14
|
+
} from '../core/modelFetch.js';
|
|
7
15
|
import { checkInternetConnectivity } from '../core/startupCheck.js';
|
|
8
16
|
import {
|
|
9
17
|
createNewWorkspace,
|
|
@@ -24,11 +32,40 @@ type Step =
|
|
|
24
32
|
| { kind: 'menu'; title: string; items: Array<{ label: string; value: string; muted?: boolean }> }
|
|
25
33
|
| { kind: 'confirm'; title: string; message: string; yesLabel: string; noLabel: string }
|
|
26
34
|
| { kind: 'select'; title: string; label: string; options: string[]; note?: string }
|
|
27
|
-
| {
|
|
35
|
+
| {
|
|
36
|
+
kind: 'text';
|
|
37
|
+
title: string;
|
|
38
|
+
label: string;
|
|
39
|
+
note?: string;
|
|
40
|
+
placeholder?: string;
|
|
41
|
+
prefill?: string;
|
|
42
|
+
secret?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Catalogue découvert. Purement indicatif : le champ texte fait foi, ce
|
|
45
|
+
* qui garde l'étape utilisable quand l'endpoint est injoignable ou quand
|
|
46
|
+
* le modèle voulu n'y figure pas.
|
|
47
|
+
*/
|
|
48
|
+
suggestions?: string[];
|
|
49
|
+
}
|
|
28
50
|
| { kind: 'done' };
|
|
29
51
|
type LogEntry = { icon: string; label: string; detail?: string };
|
|
30
52
|
|
|
31
|
-
|
|
53
|
+
// Deux axes, deux questions. `provider` dit où l'on tape, `engine` dit
|
|
54
|
+
// comment se comporte le serveur en face. Les fusionner était précisément ce
|
|
55
|
+
// qui empêchait de décrire une gateway.
|
|
56
|
+
const PROVIDERS = [
|
|
57
|
+
'Direct server (OpenAI-compatible)',
|
|
58
|
+
'AI gateway (LiteLLM, Bifrost, Portkey…)',
|
|
59
|
+
];
|
|
60
|
+
const ENGINE_OPTIONS = [
|
|
61
|
+
'OpenAI',
|
|
62
|
+
'Anthropic',
|
|
63
|
+
'Ollama (local)',
|
|
64
|
+
'vLLM (local)',
|
|
65
|
+
'MLX (local)',
|
|
66
|
+
'Albert',
|
|
67
|
+
'Other (generic OpenAI-compatible)',
|
|
68
|
+
];
|
|
32
69
|
// The scaffolded .wikirc.yaml ships fake endpoints and secrets so the file
|
|
33
70
|
// documents its own shape (`https://mon-provider.example.com/v1`,
|
|
34
71
|
// `http://infinity.local:7997/v1`, `YOUR_LLM_API_KEY`…). Preloading them into
|
|
@@ -43,12 +80,10 @@ function configuredValue(value: unknown) {
|
|
|
43
80
|
}
|
|
44
81
|
const MAIN_MENU = ['Agents', 'Workspaces', 'LLM configuration', 'Vector search', '---', 'Close'];
|
|
45
82
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (provider === 'openai-compatible') return 'http://localhost:8000';
|
|
51
|
-
return '';
|
|
83
|
+
// Les défauts et la question « faut-il demander une baseUrl ? » vivent dans
|
|
84
|
+
// core/modelFetch.js, source unique partagée avec la découverte.
|
|
85
|
+
function exampleBaseUrl(provider: string, engine: string) {
|
|
86
|
+
return defaultBaseUrl(provider, engine);
|
|
52
87
|
}
|
|
53
88
|
|
|
54
89
|
function currentWorkspaceContext(session: any, fallback?: any) {
|
|
@@ -239,17 +274,30 @@ export function SetupWizard(props: {
|
|
|
239
274
|
kind: 'select',
|
|
240
275
|
title: 'LLM configuration',
|
|
241
276
|
label: context?.configError
|
|
242
|
-
? `${context.configError} Select
|
|
243
|
-
: `No LLM configured${context?.workspaceName ? ` for ${context.workspaceName}` : ''}.
|
|
277
|
+
? `${context.configError} Select how requests are routed after creating or fixing the config:`
|
|
278
|
+
: `No LLM configured${context?.workspaceName ? ` for ${context.workspaceName}` : ''}. How are requests routed?`,
|
|
244
279
|
options: PROVIDERS,
|
|
280
|
+
note: 'A gateway is external infrastructure you deploy yourself; llm-wiki only reads its model catalog.',
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
if (currentRoute === 'llm-engine') {
|
|
284
|
+
return {
|
|
285
|
+
kind: 'select',
|
|
286
|
+
title: 'LLM configuration',
|
|
287
|
+
label: 'Which server is answering?',
|
|
288
|
+
options: ENGINE_OPTIONS,
|
|
289
|
+
note: 'Drives request-shaping workarounds and the `wiki doctor` calibration.',
|
|
245
290
|
};
|
|
246
291
|
}
|
|
247
292
|
if (currentRoute === 'llm-baseurl') {
|
|
248
|
-
const
|
|
293
|
+
const isGateway = llm().provider === 'ai-gateway';
|
|
294
|
+
const example = exampleBaseUrl(llm().provider, llm().engine);
|
|
249
295
|
return {
|
|
250
296
|
kind: 'text',
|
|
251
297
|
title: 'LLM configuration',
|
|
252
|
-
label:
|
|
298
|
+
label: isGateway
|
|
299
|
+
? 'Gateway base URL (example: http://gateway:4000/v1)'
|
|
300
|
+
: `Base URL${example ? ` (example: ${example})` : ''}`,
|
|
253
301
|
prefill: llm().baseUrl || '',
|
|
254
302
|
};
|
|
255
303
|
}
|
|
@@ -257,13 +305,15 @@ export function SetupWizard(props: {
|
|
|
257
305
|
return { kind: 'text', title: 'LLM configuration', label: 'API key (required)', secret: true };
|
|
258
306
|
}
|
|
259
307
|
if (currentRoute === 'llm-model') {
|
|
260
|
-
const
|
|
308
|
+
const discovered = catalog()?.chat ?? [];
|
|
309
|
+
const example = discovered[0] || fallbackModels(llm().engine)[0] || 'provider-agentic-model';
|
|
261
310
|
return {
|
|
262
311
|
kind: 'text',
|
|
263
312
|
title: 'LLM configuration',
|
|
264
313
|
label: `Model (example: ${example})`,
|
|
265
|
-
note: 'Required: an agentic model with tool/function calling support.',
|
|
266
|
-
prefill: llm().model ||
|
|
314
|
+
note: catalogNote('Required: an agentic model with tool/function calling support.'),
|
|
315
|
+
prefill: llm().model || example,
|
|
316
|
+
suggestions: discovered,
|
|
267
317
|
};
|
|
268
318
|
}
|
|
269
319
|
if (currentRoute === 'vector-confirm') {
|
|
@@ -274,19 +324,50 @@ export function SetupWizard(props: {
|
|
|
274
324
|
return { kind: 'text', title: 'Vector search', label: 'Embeddings/rerank base URL', prefill: baseUrl, placeholder: baseUrl };
|
|
275
325
|
}
|
|
276
326
|
if (currentRoute === 'vector-apikey') {
|
|
277
|
-
|
|
278
|
-
|
|
327
|
+
// L'héritage n'est proposé que tant que l'URL n'a pas divergé : sinon la
|
|
328
|
+
// clé du LLM — celle de la gateway, qui ouvre tous les providers —
|
|
329
|
+
// partirait vers un autre hôte.
|
|
330
|
+
const diverged = vectorBaseUrlDiverged();
|
|
331
|
+
const hint = !diverged && llm().apiKey ? '(leave empty to reuse LLM key)' : undefined;
|
|
332
|
+
return {
|
|
333
|
+
kind: 'text',
|
|
334
|
+
title: 'Vector search',
|
|
335
|
+
label: diverged ? 'Vector API key (required: different host)' : 'Vector API key',
|
|
336
|
+
placeholder: hint,
|
|
337
|
+
secret: true,
|
|
338
|
+
};
|
|
279
339
|
}
|
|
280
340
|
if (currentRoute === 'vector-model') {
|
|
281
|
-
const
|
|
282
|
-
|
|
341
|
+
const discovered = catalog()?.embedding ?? [];
|
|
342
|
+
const defaultEmbedding =
|
|
343
|
+
vector().embeddingModel ||
|
|
344
|
+
discovered[0] ||
|
|
345
|
+
fallbackModels(llm().engine, 'embedding')[0] ||
|
|
346
|
+
'';
|
|
347
|
+
return {
|
|
348
|
+
kind: 'text',
|
|
349
|
+
title: 'Vector search',
|
|
350
|
+
label: 'Embedding model',
|
|
351
|
+
note: catalogNote('Embeddings endpoint model.'),
|
|
352
|
+
prefill: defaultEmbedding,
|
|
353
|
+
suggestions: discovered,
|
|
354
|
+
};
|
|
283
355
|
}
|
|
284
356
|
if (currentRoute === 'vector-rerank') {
|
|
285
357
|
return { kind: 'confirm', title: 'Vector search', message: 'Enable reranking?', yesLabel: 'Enable', noLabel: 'Skip' };
|
|
286
358
|
}
|
|
287
359
|
if (currentRoute === 'vector-rerank-model') {
|
|
288
|
-
const
|
|
289
|
-
|
|
360
|
+
const discovered = catalog()?.rerank ?? [];
|
|
361
|
+
const defaultReranker =
|
|
362
|
+
vector().rerankerModel || discovered[0] || 'BAAI/bge-reranker-v2-m3';
|
|
363
|
+
return {
|
|
364
|
+
kind: 'text',
|
|
365
|
+
title: 'Vector search',
|
|
366
|
+
label: 'Rerank model',
|
|
367
|
+
note: catalogNote('Leave reranking disabled if no rerank model is available.'),
|
|
368
|
+
prefill: defaultReranker,
|
|
369
|
+
suggestions: discovered,
|
|
370
|
+
};
|
|
290
371
|
}
|
|
291
372
|
if (currentRoute === 'unregister-confirm') {
|
|
292
373
|
const workspace = targetWorkspace();
|
|
@@ -315,12 +396,93 @@ export function SetupWizard(props: {
|
|
|
315
396
|
setInput((s as any).prefill ?? '');
|
|
316
397
|
const items = (s as any).items ?? (s as any).options?.map((label: string) => ({ label })) ?? [{ label: 'x' }];
|
|
317
398
|
let preferred = -1;
|
|
399
|
+
// La question de routage est reposée à chaque passage — c'est plus simple
|
|
400
|
+
// et plus honnête que de la mémoriser dans un champ dédié. On se contente
|
|
401
|
+
// de présélectionner ce que le wikirc déclare déjà.
|
|
318
402
|
if (route() === 'llm-provider' && llm().provider) {
|
|
319
|
-
preferred = PROVIDERS.findIndex(
|
|
403
|
+
preferred = PROVIDERS.findIndex(
|
|
404
|
+
(p) => normalizeProvider(p) === normalizeProvider(llm().provider),
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
if (route() === 'llm-engine' && llm().engine) {
|
|
408
|
+
preferred = ENGINE_OPTIONS.findIndex(
|
|
409
|
+
(e) => normalizeEngine(e) === normalizeEngine(llm().engine),
|
|
410
|
+
);
|
|
320
411
|
}
|
|
321
412
|
setSelected(preferred >= 0 ? preferred : firstSelectableIndex(items));
|
|
322
413
|
});
|
|
323
414
|
|
|
415
|
+
/**
|
|
416
|
+
* Catalogue découvert auprès du serveur ou de la gateway. Il ne sert qu'à
|
|
417
|
+
* préremplir : le champ texte reste la vérité, ce qui garde le wizard
|
|
418
|
+
* utilisable quand l'endpoint est injoignable ou quand le modèle voulu n'y
|
|
419
|
+
* figure pas.
|
|
420
|
+
*/
|
|
421
|
+
const [catalog, setCatalog] = createSignal<any>(null);
|
|
422
|
+
const [catalogError, setCatalogError] = createSignal<string | null>(null);
|
|
423
|
+
|
|
424
|
+
function catalogNote(base: string) {
|
|
425
|
+
const error = catalogError();
|
|
426
|
+
if (error) return `${base} (model discovery unavailable: ${error})`;
|
|
427
|
+
const found = catalog();
|
|
428
|
+
if (!found) return base;
|
|
429
|
+
if (!found.typed) {
|
|
430
|
+
return `${base} (gateway has no /model/info: the three lists are unfiltered)`;
|
|
431
|
+
}
|
|
432
|
+
return base;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function discoverModels(target?: {
|
|
436
|
+
provider?: string;
|
|
437
|
+
engine?: string;
|
|
438
|
+
baseUrl?: string;
|
|
439
|
+
apiKey?: string;
|
|
440
|
+
}) {
|
|
441
|
+
setCatalog(null);
|
|
442
|
+
setCatalogError(null);
|
|
443
|
+
const source = target ?? llm();
|
|
444
|
+
const { provider, engine, baseUrl, apiKey } = source as any;
|
|
445
|
+
if (!baseUrl) return;
|
|
446
|
+
try {
|
|
447
|
+
if (normalizeProvider(provider) === 'ai-gateway') {
|
|
448
|
+
const found = await fetchGatewayCatalog(baseUrl, apiKey);
|
|
449
|
+
if (!found.ok) {
|
|
450
|
+
setCatalogError(found.error ?? 'gateway unreachable');
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
setCatalog(found);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const chat = await fetchModels(provider, baseUrl, apiKey, { engine });
|
|
457
|
+
const embedding = await fetchModels(provider, baseUrl, apiKey, {
|
|
458
|
+
engine,
|
|
459
|
+
kind: 'embedding',
|
|
460
|
+
});
|
|
461
|
+
if (!chat.ok && !embedding.ok) {
|
|
462
|
+
setCatalogError(chat.error ?? 'server unreachable');
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
// Un serveur direct ne type pas ses modèles : les mêmes entrées
|
|
466
|
+
// alimentent les trois questions.
|
|
467
|
+
setCatalog({
|
|
468
|
+
typed: false,
|
|
469
|
+
chat: chat.models,
|
|
470
|
+
embedding: embedding.models,
|
|
471
|
+
rerank: embedding.models,
|
|
472
|
+
});
|
|
473
|
+
} catch (err) {
|
|
474
|
+
setCatalogError(err instanceof Error ? err.message : String(err));
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** Vrai quand l'URL vecteur ne pointe plus le même hôte que le LLM. */
|
|
479
|
+
function vectorBaseUrlDiverged() {
|
|
480
|
+
const vectorUrl = vector().baseUrl;
|
|
481
|
+
const llmUrl = llm().baseUrl;
|
|
482
|
+
if (!vectorUrl || !llmUrl) return false;
|
|
483
|
+
return vectorUrl.replace(/\/+$/, '') !== llmUrl.replace(/\/+$/, '');
|
|
484
|
+
}
|
|
485
|
+
|
|
324
486
|
function preloadWikirc(context: any) {
|
|
325
487
|
const workspacePath = context?.workspacePath;
|
|
326
488
|
if (!workspacePath) return;
|
|
@@ -330,6 +492,15 @@ export function SetupWizard(props: {
|
|
|
330
492
|
if (config?.llm?.provider) {
|
|
331
493
|
setLlm({
|
|
332
494
|
provider: normalizeProvider(config.llm.provider),
|
|
495
|
+
// Un wikirc pré-0.16 porte le moteur dans `provider`
|
|
496
|
+
// (`ollama`, `anthropic`, `openai`). Sans cette déduction, l'étape
|
|
497
|
+
// moteur ne présélectionne rien et propose OpenAI en tête — au
|
|
498
|
+
// risque d'écraser une configuration qui marchait.
|
|
499
|
+
engine: config.llm.engine
|
|
500
|
+
? normalizeEngine(config.llm.engine)
|
|
501
|
+
: normalizeProvider(config.llm.provider) === 'ai-gateway'
|
|
502
|
+
? null
|
|
503
|
+
: normalizeEngine(config.llm.provider),
|
|
333
504
|
baseUrl: configuredValue(config.llm.baseUrl),
|
|
334
505
|
apiKey: configuredValue(config.llm.apiKey),
|
|
335
506
|
model: configuredValue(config.llm.model),
|
|
@@ -500,14 +671,28 @@ export function SetupWizard(props: {
|
|
|
500
671
|
}
|
|
501
672
|
if (currentRoute === 'llm-provider') {
|
|
502
673
|
const provider = normalizeProvider(value);
|
|
674
|
+
setLlm((old: any) => ({
|
|
675
|
+
...old,
|
|
676
|
+
provider,
|
|
677
|
+
// Derrière une gateway il n'y a pas de moteur : l'endpoint est opaque
|
|
678
|
+
// et chaque modèle peut en avoir un différent.
|
|
679
|
+
engine: provider === 'ai-gateway' ? null : old.engine,
|
|
680
|
+
baseUrl: old.provider === provider ? old.baseUrl : '',
|
|
681
|
+
}));
|
|
682
|
+
// La gateway exige toujours une baseUrl, et n'a pas de moteur à choisir.
|
|
683
|
+
if (provider === 'ai-gateway') return navigate('llm-baseurl');
|
|
684
|
+
return navigate('llm-engine');
|
|
685
|
+
}
|
|
686
|
+
if (currentRoute === 'llm-engine') {
|
|
687
|
+
const engine = normalizeEngine(value);
|
|
503
688
|
setLlm((old: any) => {
|
|
504
|
-
const asksForBaseUrl = provider
|
|
505
|
-
const baseUrl = (old.
|
|
689
|
+
const asksForBaseUrl = requiresBaseUrl(old.provider, engine);
|
|
690
|
+
const baseUrl = (old.engine === engine && old.baseUrl)
|
|
506
691
|
? old.baseUrl
|
|
507
|
-
: asksForBaseUrl ? '' : defaultBaseUrl(provider);
|
|
508
|
-
return { ...old,
|
|
692
|
+
: asksForBaseUrl ? '' : defaultBaseUrl(old.provider, engine);
|
|
693
|
+
return { ...old, engine, baseUrl, ...(engine === 'ollama' && !old.apiKey ? { apiKey: 'ollama' } : {}) };
|
|
509
694
|
});
|
|
510
|
-
if (provider
|
|
695
|
+
if (requiresBaseUrl(llm().provider, engine)) return navigate('llm-baseurl');
|
|
511
696
|
return navigate('llm-apikey');
|
|
512
697
|
}
|
|
513
698
|
if (currentRoute === 'unregister-confirm') {
|
|
@@ -569,7 +754,7 @@ export function SetupWizard(props: {
|
|
|
569
754
|
setVector((old: any) => ({
|
|
570
755
|
...old,
|
|
571
756
|
provider: old.provider || llm().provider,
|
|
572
|
-
baseUrl: old.baseUrl || llm().baseUrl || defaultBaseUrl(llm().provider),
|
|
757
|
+
baseUrl: old.baseUrl || llm().baseUrl || defaultBaseUrl(llm().provider, llm().engine),
|
|
573
758
|
}));
|
|
574
759
|
return navigate('vector-baseurl');
|
|
575
760
|
}
|
|
@@ -612,24 +797,46 @@ export function SetupWizard(props: {
|
|
|
612
797
|
if (currentRoute === 'llm-baseurl') {
|
|
613
798
|
if (!value) return setError('Base URL is required.');
|
|
614
799
|
setLlm((old: any) => ({ ...old, baseUrl: value }));
|
|
615
|
-
|
|
800
|
+
// Ollama n'exige pas de clé : on peut découvrir tout de suite.
|
|
801
|
+
if (llm().engine === 'ollama') {
|
|
802
|
+
await discoverModels();
|
|
803
|
+
return navigate('llm-model');
|
|
804
|
+
}
|
|
616
805
|
return navigate('llm-apikey');
|
|
617
806
|
}
|
|
618
807
|
if (currentRoute === 'llm-apikey') {
|
|
619
808
|
if (!value) return setError('API key is required.');
|
|
620
809
|
setLlm((old: any) => ({ ...old, apiKey: value }));
|
|
810
|
+
await discoverModels();
|
|
621
811
|
return navigate('llm-model');
|
|
622
812
|
}
|
|
623
813
|
if (currentRoute === 'vector-baseurl') {
|
|
624
814
|
const baseUrl = value || vector().baseUrl || llm().baseUrl;
|
|
625
815
|
if (!baseUrl) return setError('Embeddings/rerank base URL is required.');
|
|
626
|
-
setVector((old: any) => ({
|
|
816
|
+
setVector((old: any) => ({
|
|
817
|
+
...old,
|
|
818
|
+
provider: llm().provider,
|
|
819
|
+
engine: llm().engine,
|
|
820
|
+
baseUrl,
|
|
821
|
+
}));
|
|
627
822
|
return navigate('vector-apikey');
|
|
628
823
|
}
|
|
629
824
|
if (currentRoute === 'vector-apikey') {
|
|
825
|
+
if (vectorBaseUrlDiverged() && !value) {
|
|
826
|
+
return setError(
|
|
827
|
+
'API key is required: the vector base URL differs from the LLM one, so the LLM key is not reused.',
|
|
828
|
+
);
|
|
829
|
+
}
|
|
630
830
|
const apiKey = value || llm().apiKey || undefined;
|
|
631
831
|
if (!apiKey) return setError('API key is required (or set LLM key first).');
|
|
632
832
|
setVector((old: any) => ({ ...old, apiKey }));
|
|
833
|
+
// Le catalogue affiché aux étapes embeddings et rerank doit venir de
|
|
834
|
+
// l'endpoint vecteur, pas du LLM : ce sont deux serveurs distincts dès
|
|
835
|
+
// que l'URL diverge, et proposer les modèles de chat de l'un pour les
|
|
836
|
+
// embeddings de l'autre n'a aucun sens.
|
|
837
|
+
if (vectorBaseUrlDiverged()) {
|
|
838
|
+
await discoverModels({ ...vector(), apiKey });
|
|
839
|
+
}
|
|
633
840
|
return navigate('vector-model');
|
|
634
841
|
}
|
|
635
842
|
if (currentRoute === 'llm-model') {
|
|
@@ -754,6 +961,31 @@ export function SetupWizard(props: {
|
|
|
754
961
|
return value || (s.secret ? (s.placeholder ?? '') : '');
|
|
755
962
|
};
|
|
756
963
|
const inputHasValue = () => step().kind === 'text' && input().length > 0;
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* Suggestions filtrées par ce qui est tapé.
|
|
967
|
+
*
|
|
968
|
+
* Une gateway correctement remplie expose plusieurs centaines de modèles :
|
|
969
|
+
* une liste brute est inutilisable, et un select classique interdirait de
|
|
970
|
+
* saisir un modèle absent du catalogue. On garde donc le champ texte comme
|
|
971
|
+
* seule vérité et on n'affiche qu'un rappel filtré — ce qui règle d'un coup
|
|
972
|
+
* les trois cas : liste énorme, modèle absent, endpoint injoignable.
|
|
973
|
+
*/
|
|
974
|
+
const SUGGESTION_ROWS = 4;
|
|
975
|
+
const filteredSuggestions = createMemo(() => {
|
|
976
|
+
const current = step() as any;
|
|
977
|
+
const all: string[] = current?.suggestions ?? [];
|
|
978
|
+
if (all.length === 0) return { rows: [] as string[], total: 0, matched: 0 };
|
|
979
|
+
const needle = input().trim().toLowerCase();
|
|
980
|
+
const matches = needle
|
|
981
|
+
? all.filter((item) => item.toLowerCase().includes(needle))
|
|
982
|
+
: all;
|
|
983
|
+
return {
|
|
984
|
+
rows: matches.slice(0, SUGGESTION_ROWS),
|
|
985
|
+
total: all.length,
|
|
986
|
+
matched: matches.length,
|
|
987
|
+
};
|
|
988
|
+
});
|
|
757
989
|
const lineWidth = () => Math.max(10, dialogWidth() - 10);
|
|
758
990
|
const displayLine1 = () => displayValue().slice(0, lineWidth());
|
|
759
991
|
const displayLine2 = () => displayValue().slice(lineWidth(), lineWidth() * 2);
|
|
@@ -839,6 +1071,25 @@ export function SetupWizard(props: {
|
|
|
839
1071
|
</box>
|
|
840
1072
|
</box>
|
|
841
1073
|
</Show>
|
|
1074
|
+
<Show when={step().kind === 'text' && filteredSuggestions().total > 0}>
|
|
1075
|
+
<text height={1} fg="#7F8C8D">
|
|
1076
|
+
{filteredSuggestions().matched === 0
|
|
1077
|
+
? `no match among ${filteredSuggestions().total} discovered model(s) — the typed value is used as-is`
|
|
1078
|
+
: `${filteredSuggestions().matched}/${filteredSuggestions().total} discovered model(s) — type to filter, the typed value wins`}
|
|
1079
|
+
</text>
|
|
1080
|
+
<For each={filteredSuggestions().rows}>
|
|
1081
|
+
{(suggestion) => (
|
|
1082
|
+
<text height={1} fg={suggestion === input().trim() ? '#8BD5CA' : '#9CA3AF'}>
|
|
1083
|
+
{` ${suggestion === input().trim() ? '*' : '·'} ${suggestion}`}
|
|
1084
|
+
</text>
|
|
1085
|
+
)}
|
|
1086
|
+
</For>
|
|
1087
|
+
<Show when={filteredSuggestions().matched > SUGGESTION_ROWS}>
|
|
1088
|
+
<text height={1} fg="#7F8C8D">
|
|
1089
|
+
{` … ${filteredSuggestions().matched - SUGGESTION_ROWS} more`}
|
|
1090
|
+
</text>
|
|
1091
|
+
</Show>
|
|
1092
|
+
</Show>
|
|
842
1093
|
<Show when={step().kind !== 'text'}>
|
|
843
1094
|
<For each={currentItems()}>
|
|
844
1095
|
{(item, index) => (
|
|
@@ -48,7 +48,7 @@ test('preloading a profile filters placeholders out of every prefilled field', (
|
|
|
48
48
|
// With the vector URL filtered to null, this is what makes the embeddings
|
|
49
49
|
// step offer the base URL the operator just typed.
|
|
50
50
|
assert.match(source, /const baseUrl = vector\(\)\.baseUrl \|\| llm\(\)\.baseUrl;/);
|
|
51
|
-
assert.match(source, /baseUrl: old\.baseUrl \|\| llm\(\)\.baseUrl \|\| defaultBaseUrl\(llm\(\)\.provider\)/);
|
|
51
|
+
assert.match(source, /baseUrl: old\.baseUrl \|\| llm\(\)\.baseUrl \|\| defaultBaseUrl\(llm\(\)\.provider, llm\(\)\.engine\)/);
|
|
52
52
|
});
|
|
53
53
|
|
|
54
54
|
test('a real configuration is never mistaken for a placeholder', () => {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
|
|
5
|
+
const source = readFileSync(new URL('./SetupWizard.tsx', import.meta.url), 'utf8');
|
|
6
|
+
|
|
7
|
+
test('the model steps feed the discovered catalog into the filter', () => {
|
|
8
|
+
// Trois étapes doivent porter des suggestions : modèle LLM, embeddings,
|
|
9
|
+
// reranker. Sans elles, le filtre n'a rien a filtrer.
|
|
10
|
+
const occurrences = source.match(/suggestions: discovered/g) ?? [];
|
|
11
|
+
assert.equal(occurrences.length, 3);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('the typed value always wins over the suggestion list', () => {
|
|
15
|
+
// Garde-fou : la liste ne doit jamais devenir un select. Si ce message
|
|
16
|
+
// disparait, c'est que la saisie libre a ete perdue.
|
|
17
|
+
assert.match(source, /the typed value wins/);
|
|
18
|
+
assert.match(source, /the typed value is used as-is/);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('suggestions are filtered by the current input, not shown raw', () => {
|
|
22
|
+
assert.match(source, /all\.filter\(\(item\) => item\.toLowerCase\(\)\.includes\(needle\)\)/);
|
|
23
|
+
assert.match(source, /matches\.slice\(0, SUGGESTION_ROWS\)/);
|
|
24
|
+
});
|
package/src/shell/tui.tsx
CHANGED
|
@@ -150,30 +150,22 @@ function App(props: {
|
|
|
150
150
|
const [preflightBusy, setPreflightBusy] = createSignal(false);
|
|
151
151
|
let ctrlCTimer: ReturnType<typeof setTimeout> | null = null;
|
|
152
152
|
let exiting = false;
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
// screen — 0.12.9 shipped that and killed the runtime mid-session.
|
|
153
|
+
// The runtime is shared infrastructure: `llm-wiki serve` can still be using
|
|
154
|
+
// it after this shell closes. Exiting the UI only destroys the renderer;
|
|
155
|
+
// explicit lifecycle commands own runtime shutdown.
|
|
157
156
|
const exitShell = () => {
|
|
158
157
|
if (exiting) return;
|
|
159
158
|
exiting = true;
|
|
160
159
|
setExitStatus('Fermeture enclenchée…');
|
|
161
|
-
// Register the complete shutdown before destroying the renderer: onDestroy
|
|
162
|
-
// resolves runOpenTuiShell, which must still await this cleanup task.
|
|
163
160
|
const task = Promise.resolve().then(async () => {
|
|
164
161
|
renderer.destroy();
|
|
165
|
-
console.log('[wiki-manager]
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
} catch {
|
|
173
|
-
// Best effort: never block the exit on runtime cleanup.
|
|
174
|
-
}
|
|
175
|
-
for (const message of messages) console.log(`[wiki-manager] ${message}`);
|
|
176
|
-
console.log('[wiki-manager] fermeture terminée — shell clos proprement.');
|
|
162
|
+
console.log('[wiki-manager] shell closed; shared runtime left running.');
|
|
163
|
+
// MCP clients, image-refresh probes, and reconnect timers may still own
|
|
164
|
+
// event-loop handles. They belong to this shell process, not to the
|
|
165
|
+
// detached shared runtime. Once the renderer is gone there is no useful
|
|
166
|
+
// foreground work left, so terminate explicitly instead of leaving the
|
|
167
|
+
// terminal pending indefinitely.
|
|
168
|
+
process.exit(0);
|
|
177
169
|
});
|
|
178
170
|
if (props.exitRef) props.exitRef.current = task;
|
|
179
171
|
};
|
package/wiki-workspace
CHANGED
|
@@ -84,6 +84,7 @@ Usage:
|
|
|
84
84
|
wiki-workspace [--cacert <path>] <command> ...
|
|
85
85
|
wiki-workspace config <workspace> [path]
|
|
86
86
|
wiki-workspace up <workspace>
|
|
87
|
+
wiki-workspace refresh
|
|
87
88
|
wiki-workspace wiki <workspace> <command> [args...]
|
|
88
89
|
wiki-workspace list
|
|
89
90
|
|
|
@@ -91,6 +92,7 @@ Commands:
|
|
|
91
92
|
config <workspace> [path] Create workspace folder, auto-select ports, run wiki init
|
|
92
93
|
up <workspace> [--open] Start workspace stack: wiki + production
|
|
93
94
|
list List configured workspaces
|
|
95
|
+
refresh Stop runtime/stacks and remove their Compose images
|
|
94
96
|
|
|
95
97
|
agents up [--pull] Start all external agents (cme, documents)
|
|
96
98
|
agents down Stop all external agents
|
|
@@ -260,6 +262,31 @@ agents_compose() {
|
|
|
260
262
|
local compose_env_args=()
|
|
261
263
|
[[ -f "$MANAGER_ENV_FILE" ]] && compose_env_args+=(--env-file "$MANAGER_ENV_FILE")
|
|
262
264
|
|
|
265
|
+
# Local-iteration build only: the packaged connectors service carries a
|
|
266
|
+
# `build:` block pointing at the sibling agent-connectors repo, whose OAuth
|
|
267
|
+
# credentials live in a gitignored .env.build.local. Compose resolves the
|
|
268
|
+
# value-less build args from *this* process environment, not from
|
|
269
|
+
# --env-file, so the variables must be exported here or the image ships with
|
|
270
|
+
# empty WIKILLM_GOOGLE_OAUTH_* and every Google connector stays unusable.
|
|
271
|
+
local connectors_build_env="$ROOT_DIR/../agent-external/agent-connectors/.env.build.local"
|
|
272
|
+
if [[ -f "$connectors_build_env" ]]; then
|
|
273
|
+
local build_key build_value
|
|
274
|
+
while IFS='=' read -r build_key build_value; do
|
|
275
|
+
case "$build_key" in
|
|
276
|
+
WIKILLM_GOOGLE_OAUTH_CLIENT_ID|WIKILLM_GOOGLE_OAUTH_CLIENT_SECRET) ;;
|
|
277
|
+
*) continue ;;
|
|
278
|
+
esac
|
|
279
|
+
[[ -n "${!build_key:-}" ]] || export "$build_key=$build_value"
|
|
280
|
+
done < <(grep -v '^[[:space:]]*#' "$connectors_build_env" || true)
|
|
281
|
+
fi
|
|
282
|
+
|
|
283
|
+
require_connectors_build_credentials() {
|
|
284
|
+
local build_key
|
|
285
|
+
for build_key in WIKILLM_GOOGLE_OAUTH_CLIENT_ID WIKILLM_GOOGLE_OAUTH_CLIENT_SECRET; do
|
|
286
|
+
[[ -n "${!build_key:-}" ]] || die "cannot build connectors: $build_key is missing from $connectors_build_env"
|
|
287
|
+
done
|
|
288
|
+
}
|
|
289
|
+
|
|
263
290
|
_agents_dc() {
|
|
264
291
|
local cacert_args=()
|
|
265
292
|
local profile_args=()
|
|
@@ -288,6 +315,9 @@ agents_compose() {
|
|
|
288
315
|
[[ $# -eq 0 ]] || die "agents up accepts only --pull"
|
|
289
316
|
ensure_agent_tokens
|
|
290
317
|
ensure_endpoints_file
|
|
318
|
+
if connectors_enabled && [[ -f "$ROOT_DIR/../agent-external/agent-connectors/Dockerfile" ]]; then
|
|
319
|
+
require_connectors_build_credentials
|
|
320
|
+
fi
|
|
291
321
|
# The .env may have just been created above — recompute the env-file
|
|
292
322
|
# flag so freshly generated tokens reach the containers on first run.
|
|
293
323
|
compose_env_args=()
|
|
@@ -314,6 +344,10 @@ agents_compose() {
|
|
|
314
344
|
[[ $# -eq 0 ]] || die "agents down does not take arguments"
|
|
315
345
|
_agents_dc down
|
|
316
346
|
;;
|
|
347
|
+
purge)
|
|
348
|
+
[[ $# -eq 0 ]] || die "agents purge does not take arguments"
|
|
349
|
+
_agents_dc down --rmi all --remove-orphans
|
|
350
|
+
;;
|
|
317
351
|
logs)
|
|
318
352
|
local log_args=()
|
|
319
353
|
read_lines_into_array log_args logs_args "$@"
|
|
@@ -687,6 +721,19 @@ runtime_command() {
|
|
|
687
721
|
;;
|
|
688
722
|
down|stop)
|
|
689
723
|
[[ $# -eq 0 ]] || die "runtime down does not take arguments"
|
|
724
|
+
token="$(ensure_manager_runtime_token)"
|
|
725
|
+
if runtime_health_check; then
|
|
726
|
+
RUNTIME_URL="$(runtime_health_url)/shutdown" RUNTIME_TOKEN="$token" "$node_bin" --input-type=module -e \
|
|
727
|
+
"const h=process.env.RUNTIME_TOKEN?{Authorization:'Bearer '+process.env.RUNTIME_TOKEN}:{}; const r=await fetch(process.env.RUNTIME_URL,{method:'POST',headers:h}); process.exit(r.ok?0:1);"
|
|
728
|
+
local wait_count=0
|
|
729
|
+
while runtime_health_check && (( wait_count < 40 )); do
|
|
730
|
+
sleep 0.1
|
|
731
|
+
(( wait_count++ )) || true
|
|
732
|
+
done
|
|
733
|
+
rm -f "$pid_file"
|
|
734
|
+
printf 'agent-runtime stopped\n'
|
|
735
|
+
return
|
|
736
|
+
fi
|
|
690
737
|
if ! runtime_pid_running; then
|
|
691
738
|
printf 'agent-runtime is not running from %s\n' "$pid_file"
|
|
692
739
|
rm -f "$pid_file"
|
|
@@ -731,6 +778,23 @@ runtime_command() {
|
|
|
731
778
|
esac
|
|
732
779
|
}
|
|
733
780
|
|
|
781
|
+
refresh_project() {
|
|
782
|
+
printf 'Stopping shared runtime...\n'
|
|
783
|
+
runtime_command down
|
|
784
|
+
|
|
785
|
+
local entry workspace
|
|
786
|
+
for entry in "$WORKSPACES_DIR"/*; do
|
|
787
|
+
[[ -f "$entry/.env" ]] || continue
|
|
788
|
+
workspace="$(basename "$entry")"
|
|
789
|
+
printf 'Removing workspace stack and images: %s\n' "$workspace"
|
|
790
|
+
compose_for_workspace "$workspace" down --rmi all --remove-orphans
|
|
791
|
+
done
|
|
792
|
+
|
|
793
|
+
printf 'Removing external-agent stack and images...\n'
|
|
794
|
+
agents_compose purge
|
|
795
|
+
printf 'Refresh complete. Persistent workspace and agent data were preserved.\n'
|
|
796
|
+
}
|
|
797
|
+
|
|
734
798
|
ensure_runtime_up() {
|
|
735
799
|
if [[ "${WIKI_MANAGER_RUNTIME_AUTOSTART:-1}" == "0" ]]; then
|
|
736
800
|
return
|
|
@@ -1010,6 +1074,7 @@ cacert_compose_args() {
|
|
|
1010
1074
|
run_wiki() {
|
|
1011
1075
|
local workspace="$1"
|
|
1012
1076
|
shift
|
|
1077
|
+
need_workspace_env "$workspace"
|
|
1013
1078
|
local ws_path
|
|
1014
1079
|
ws_path="$(normalize_path "$(workspace_value "$workspace" WIKI_WORKSPACE_PATH)")"
|
|
1015
1080
|
ensure_workspace_dirs "$ws_path"
|
|
@@ -1214,6 +1279,11 @@ main() {
|
|
|
1214
1279
|
exit 0
|
|
1215
1280
|
fi
|
|
1216
1281
|
|
|
1282
|
+
if [[ $# -eq 1 && "$1" == "refresh" ]]; then
|
|
1283
|
+
refresh_project
|
|
1284
|
+
exit 0
|
|
1285
|
+
fi
|
|
1286
|
+
|
|
1217
1287
|
# config <workspace> [path]
|
|
1218
1288
|
if [[ $# -ge 2 && "$1" == "config" ]]; then
|
|
1219
1289
|
local cfg_workspace="$2"
|