@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
|
@@ -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' },
|
package/src/shell/RightPane.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
|
-
import { Index, Show } from 'solid-js';
|
|
2
|
+
import { createMemo, Index, Show } from 'solid-js';
|
|
3
3
|
|
|
4
4
|
type PlanStep = { step: number; description: string; status: string };
|
|
5
5
|
type QueueItem = {
|
|
@@ -12,6 +12,7 @@ type QueueItem = {
|
|
|
12
12
|
reason?: string;
|
|
13
13
|
};
|
|
14
14
|
type QueueInfo = { active: number; current: number; frozen: number };
|
|
15
|
+
type LogLineParts = { time: string | null; message: string };
|
|
15
16
|
|
|
16
17
|
const ACTIVITY_SLOTS = Array.from({ length: 6 }, (_, index) => index);
|
|
17
18
|
const LOG_SLOTS = Array.from({ length: 24 }, (_, index) => index);
|
|
@@ -51,6 +52,12 @@ function fit(value: string, width: number) {
|
|
|
51
52
|
return value.slice(0, max - 1) + '…';
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
function logLineParts(line: string): LogLineParts {
|
|
56
|
+
const match = line.match(/^((?:\d{1,2}:\d{2}(?::\d{2})?(?:\s?[AP]M)?|\d{4}-\d{2}-\d{2}[T ][0-9:.]+Z?))\s+(.+)$/i);
|
|
57
|
+
if (!match) return { time: null, message: line };
|
|
58
|
+
return { time: match[1], message: match[2] };
|
|
59
|
+
}
|
|
60
|
+
|
|
54
61
|
function activityColor(status: string) {
|
|
55
62
|
const value = String(status ?? '').toLowerCase();
|
|
56
63
|
if (['done', 'complete', 'completed', 'success'].includes(value)) return '#8BD5CA';
|
|
@@ -190,7 +197,23 @@ export function LogPanel(props: { logs: string[]; width: number }) {
|
|
|
190
197
|
<text width={lineWidth()} fg="#D6DEE8" content="Logs / Trace" />
|
|
191
198
|
<box flexGrow={1} flexDirection="column" overflow="hidden">
|
|
192
199
|
<Index each={LOG_SLOTS}>
|
|
193
|
-
{(slot) =>
|
|
200
|
+
{(slot) => {
|
|
201
|
+
const line = () => logLineAt(slot());
|
|
202
|
+
const parts = createMemo(() => logLineParts(line()));
|
|
203
|
+
const timeWidth = () => parts().time ? Math.min(parts().time!.length + 1, lineWidth()) : 0;
|
|
204
|
+
const messageWidth = () => Math.max(1, lineWidth() - timeWidth());
|
|
205
|
+
return (
|
|
206
|
+
<Show
|
|
207
|
+
when={parts().time}
|
|
208
|
+
fallback={<text width={lineWidth()} fg="#AAB7C4" content={line()} />}
|
|
209
|
+
>
|
|
210
|
+
<box height={1} flexDirection="row" overflow="hidden">
|
|
211
|
+
<text width={timeWidth()} fg="#89B4FA" content={fit(`${parts().time} `, timeWidth())} />
|
|
212
|
+
<text width={messageWidth()} fg="#AAB7C4" content={fit(parts().message, messageWidth())} />
|
|
213
|
+
</box>
|
|
214
|
+
</Show>
|
|
215
|
+
);
|
|
216
|
+
}}
|
|
194
217
|
</Index>
|
|
195
218
|
</box>
|
|
196
219
|
</box>
|