tinywebui 0.1.3 → 0.1.5
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 +20 -13
- package/bin/tinywebui.js +69 -63
- package/package.json +25 -8
- package/public/app.js +40 -14
- package/public/chat/composer.js +1 -1
- package/public/chat/sidebar.js +8 -5
- package/public/index.html +88 -49
- package/public/panels/admin.js +40 -1
- package/public/panels/model-editor.js +336 -0
- package/public/panels/panels.js +37 -12
- package/public/panels/settings-home.js +125 -0
- package/public/panels/settings.js +124 -36
- package/public/shared/attachment_note.js +7 -1
- package/public/shared/events.js +1 -0
- package/public/shared/features.js +1 -0
- package/public/shared/origins.js +1 -0
- package/public/shared/usage.js +16 -1
- package/public/styles.css +83 -20
- package/src/access/auth.js +155 -155
- package/src/access/auth_gate.js +39 -38
- package/src/access/email.js +17 -0
- package/src/access/policy.js +123 -138
- package/src/audit.js +1 -1
- package/src/automations/automation.js +135 -123
- package/src/automations/scheduler.js +138 -127
- package/src/chat/compact.js +149 -156
- package/src/chat/llm.js +380 -382
- package/src/chat/runs.js +162 -151
- package/src/chat/tool_executor.js +229 -218
- package/src/cli.js +535 -433
- package/src/config/approval.js +35 -29
- package/src/config/config.js +546 -451
- package/src/config/data_dir.js +24 -0
- package/src/config/models.js +191 -115
- package/src/config/refs.js +32 -0
- package/src/config/secrets.js +153 -0
- package/src/files/documents.js +57 -68
- package/src/files/images.js +30 -31
- package/src/harness/agent.js +60 -60
- package/src/harness/attribution.js +66 -58
- package/src/harness/checkpoint.js +67 -62
- package/src/harness/history.js +28 -23
- package/src/harness/prompt.js +40 -57
- package/src/harness/provider.js +284 -283
- package/src/harness/types.js +1 -0
- package/src/http.js +98 -66
- package/src/log.js +11 -12
- package/src/mcp.js +398 -374
- package/src/retrieval/embedding.js +228 -240
- package/src/retrieval/retrieval.js +182 -173
- package/src/routes/admin.js +178 -138
- package/src/routes/auth.js +184 -100
- package/src/routes/automations.js +99 -82
- package/src/routes/chats.js +240 -218
- package/src/routes/documents.js +130 -120
- package/src/routes/library.js +43 -46
- package/src/routes/mcp.js +49 -49
- package/src/routes/settings.js +127 -122
- package/src/routes/types.js +1 -0
- package/src/server.js +428 -392
- package/src/store/automations.js +106 -106
- package/src/store/chats.js +217 -242
- package/src/store/documents.js +181 -185
- package/src/store/embeddings.js +52 -54
- package/src/store/index.js +95 -49
- package/src/store/messages.js +55 -80
- package/src/store/schema.js +128 -107
- package/src/store/scope.js +18 -17
- package/src/store/search.js +25 -27
- package/src/store/turns.js +53 -53
- package/src/store/usage.js +343 -269
- package/src/store/users.js +162 -87
- package/src/store/wire.js +50 -41
- package/src/tools/ask_tool.js +68 -68
- package/src/tools/chat_search_tool.js +38 -44
- package/src/tools/context_tool.js +131 -138
- package/src/tools/document_tool.js +73 -78
- package/src/tools/mcp_tool.js +274 -232
- package/src/tools/task_tool.js +53 -50
- package/src/tools/types.js +1 -0
- package/public/panels/connectors.js +0 -184
- package/src/set-password.js +0 -43
package/src/tools/task_tool.js
CHANGED
|
@@ -1,63 +1,66 @@
|
|
|
1
|
-
/** A small, chat-scoped checklist that the model can maintain during a task. */
|
|
2
1
|
export const MANAGE_TASKS = 'manage_tasks';
|
|
3
|
-
|
|
4
2
|
export function taskToolDef() {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
3
|
+
return {
|
|
4
|
+
type: 'function',
|
|
5
|
+
function: {
|
|
6
|
+
name: MANAGE_TASKS,
|
|
7
|
+
description: 'Keep a visible checklist for this chat. Create tasks when a request has multiple steps. Mark a task in_progress when you start it and completed as soon as it is done; before your final answer, make sure no finished task is still open. Update several tasks at once with ids. This is a checklist, not a scheduled automation.',
|
|
8
|
+
parameters: {
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: {
|
|
11
|
+
action: { type: 'string', enum: ['add', 'update', 'list'] },
|
|
12
|
+
title: { type: 'string', description: 'Short task title for add.' },
|
|
13
|
+
id: { type: 'string', description: 'Task ID returned by add or list, for update.' },
|
|
14
|
+
ids: { type: 'array', items: { type: 'string' }, description: 'Several task IDs to set to the same status, for update.' },
|
|
15
|
+
status: { type: 'string', enum: ['pending', 'in_progress', 'completed'], description: 'New status for update.' }
|
|
16
|
+
},
|
|
17
|
+
required: ['action']
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
};
|
|
23
21
|
}
|
|
24
|
-
|
|
25
22
|
export function callManageTasks(args, { store, chatId, onChange } = {}) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
23
|
+
if (!store || !chatId)
|
|
24
|
+
return 'Error: this tool requires a chat.';
|
|
25
|
+
const { action } = args || {};
|
|
26
|
+
if (action === 'list')
|
|
27
|
+
return JSON.stringify({ tasks: store.chats.listTasks(chatId) });
|
|
28
|
+
if (action === 'add') {
|
|
29
|
+
const title = typeof args.title === 'string' ? args.title.trim() : '';
|
|
30
|
+
if (!title || title.length > 200)
|
|
31
|
+
return 'Error: title must be 1–200 characters.';
|
|
32
|
+
if (store.chats.listTasks(chatId).length >= 50)
|
|
33
|
+
return 'Error: this chat has reached the 50-task limit.';
|
|
34
|
+
const task = store.chats.addTask(chatId, title);
|
|
35
|
+
onChange?.(store.chats.listTasks(chatId));
|
|
36
|
+
return JSON.stringify({ task });
|
|
37
|
+
}
|
|
38
|
+
if (action === 'update') {
|
|
39
|
+
const ids = Array.isArray(args.ids) ? args.ids : [args.id];
|
|
40
|
+
if (!ids.length || ids.some((id) => typeof id !== 'string' || !id)
|
|
41
|
+
|| !['pending', 'in_progress', 'completed'].includes(args.status)) {
|
|
42
|
+
return 'Error: update requires a valid task id (or ids) and status.';
|
|
43
|
+
}
|
|
44
|
+
const tasks = ids.map((id) => store.chats.updateTask(chatId, id, args.status));
|
|
45
|
+
const missing = ids.filter((_, i) => !tasks[i]);
|
|
46
|
+
if (missing.length === ids.length)
|
|
47
|
+
return 'Error: task not found in this chat.';
|
|
48
|
+
onChange?.(store.chats.listTasks(chatId));
|
|
49
|
+
if (!Array.isArray(args.ids))
|
|
50
|
+
return JSON.stringify({ task: tasks[0] });
|
|
51
|
+
return JSON.stringify({ tasks: tasks.filter(Boolean), ...(missing.length ? { not_found: missing } : {}) });
|
|
42
52
|
}
|
|
43
|
-
|
|
44
|
-
const missing = ids.filter((_, i) => !tasks[i]);
|
|
45
|
-
if (missing.length === ids.length) return 'Error: task not found in this chat.';
|
|
46
|
-
onChange?.(store.chats.listTasks(chatId));
|
|
47
|
-
if (!Array.isArray(args.ids)) return JSON.stringify({ task: tasks[0] });
|
|
48
|
-
return JSON.stringify({ tasks: tasks.filter(Boolean), ...(missing.length ? { not_found: missing } : {}) });
|
|
49
|
-
}
|
|
50
|
-
return 'Error: action must be add, update, or list.';
|
|
53
|
+
return 'Error: action must be add, update, or list.';
|
|
51
54
|
}
|
|
52
|
-
|
|
53
55
|
/**
|
|
54
56
|
* The open checklist, for the budget footer. The model otherwise sees tasks
|
|
55
57
|
* only in the tool results that created them, which scroll out of attention
|
|
56
58
|
* within a turn and are gone from view entirely on the next one.
|
|
57
59
|
*/
|
|
58
60
|
export function openTasksNote(tasks) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
const open = (tasks || []).filter((t) => t.status !== 'completed');
|
|
62
|
+
if (!open.length)
|
|
63
|
+
return '';
|
|
64
|
+
const lines = open.map((t) => `- ${t.id} [${t.status}] ${t.title}`);
|
|
65
|
+
return `\n[Open tasks -- update them with ${MANAGE_TASKS} as you finish each one:\n${lines.join('\n')}]`;
|
|
63
66
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The admin editors for connectors (endpoint + key) and the model catalog.
|
|
3
|
-
* Keys are write-only: a row shows whether one is set (and its last four
|
|
4
|
-
* characters), and a blank key field means "keep it". Everything is read back
|
|
5
|
-
* from the DOM on save, so the rows are the draft until then.
|
|
6
|
-
*/
|
|
7
|
-
import { $, el } from '../core/dom.js';
|
|
8
|
-
import { api } from '../core/api.js';
|
|
9
|
-
|
|
10
|
-
const TAGS = ['vision', 'reasoning', 'tools'];
|
|
11
|
-
// Per-model settings with no field of their own; edited as one JSON object.
|
|
12
|
-
const OVERRIDE_KEYS = ['systemPrompt', 'temperature', 'maxTokens', 'maxToolRounds', 'cacheMode', 'cacheTtl', 'extraBody', 'reasoningReplay'];
|
|
13
|
-
|
|
14
|
-
function field(label, input, note) {
|
|
15
|
-
const wrap = el('div');
|
|
16
|
-
const l = el('label');
|
|
17
|
-
l.textContent = label;
|
|
18
|
-
wrap.append(l, input);
|
|
19
|
-
if (note) wrap.appendChild(Object.assign(el('small'), { textContent: note }));
|
|
20
|
-
return wrap;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const textInput = (value, placeholder) => Object.assign(el('input'), { value: value ?? '', placeholder: placeholder || '', spellcheck: false });
|
|
24
|
-
|
|
25
|
-
function removeButton(row) {
|
|
26
|
-
const b = Object.assign(el('button'), { type: 'button', textContent: 'Remove' });
|
|
27
|
-
b.onclick = () => row.remove();
|
|
28
|
-
return b;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/* ---------- connectors ---------- */
|
|
32
|
-
|
|
33
|
-
function connectorRow(c = {}) {
|
|
34
|
-
const row = el('div', 'cfg-row');
|
|
35
|
-
row.dataset.kind = 'connector';
|
|
36
|
-
const id = textInput(c.id, 'local');
|
|
37
|
-
// An id is what models name; renaming a saved one would orphan them.
|
|
38
|
-
if (c.id) id.readOnly = true;
|
|
39
|
-
const label = textInput(c.label, 'Local models');
|
|
40
|
-
const url = textInput(c.baseUrl, 'http://localhost:11434/v1');
|
|
41
|
-
const key = Object.assign(textInput('', c.hasApiKey ? `set (…${c.keyHint || ''})` : 'none'), { type: 'password', autocomplete: 'off' });
|
|
42
|
-
const clear = Object.assign(el('input'), { type: 'checkbox' });
|
|
43
|
-
const grid = el('div', 'set-grid');
|
|
44
|
-
grid.append(
|
|
45
|
-
field('Id', id, 'What a model names in its connector field.'),
|
|
46
|
-
field('Label', label),
|
|
47
|
-
Object.assign(field('API URL', url), { className: 'span-2' }),
|
|
48
|
-
Object.assign(field('API key', key, 'Write-only. Blank keeps the current key.'), { className: 'span-2' })
|
|
49
|
-
);
|
|
50
|
-
const test = Object.assign(el('button'), { type: 'button', textContent: 'Test connection' });
|
|
51
|
-
const msg = el('span', 'usage');
|
|
52
|
-
test.onclick = async () => {
|
|
53
|
-
msg.textContent = 'testing…';
|
|
54
|
-
try {
|
|
55
|
-
const out = await api.post('/api/connectors/test', { id: id.value.trim(), baseUrl: url.value.trim(), apiKey: key.value });
|
|
56
|
-
msg.textContent = out.ok ? `reachable${out.models != null ? ` · ${out.models} models` : ''}` : out.error || `status ${out.status}`;
|
|
57
|
-
} catch (err) { msg.textContent = err.message; }
|
|
58
|
-
};
|
|
59
|
-
const clearLabel = el('label');
|
|
60
|
-
clearLabel.append(clear, ' remove stored key');
|
|
61
|
-
const actions = el('div', 'cfg-actions');
|
|
62
|
-
actions.append(test, msg, ...(c.hasApiKey ? [clearLabel] : []), removeButton(row));
|
|
63
|
-
row.append(grid, actions);
|
|
64
|
-
row.read = () => ({
|
|
65
|
-
id: id.value.trim(),
|
|
66
|
-
...(label.value.trim() ? { label: label.value.trim() } : {}),
|
|
67
|
-
baseUrl: url.value.trim(),
|
|
68
|
-
// undefined keeps the stored key on the server; null clears it.
|
|
69
|
-
...(key.value ? { apiKey: key.value } : clear.checked ? { apiKey: null } : {})
|
|
70
|
-
});
|
|
71
|
-
return row;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/* ---------- model catalog ---------- */
|
|
75
|
-
|
|
76
|
-
function modelRow(m = {}, connectorIds = []) {
|
|
77
|
-
const row = el('div', 'cfg-row');
|
|
78
|
-
row.dataset.kind = 'model';
|
|
79
|
-
const id = textInput(m.id, 'fast');
|
|
80
|
-
const label = textInput(m.label, 'Quick');
|
|
81
|
-
const description = textInput(m.description, 'Everyday questions');
|
|
82
|
-
const model = textInput(m.model, 'vendor/model-id');
|
|
83
|
-
const connector = el('select');
|
|
84
|
-
connector.className = 'model-connector';
|
|
85
|
-
const none = Object.assign(el('option'), { value: '', textContent: 'Default endpoint' });
|
|
86
|
-
connector.appendChild(none);
|
|
87
|
-
for (const cid of connectorIds) connector.appendChild(Object.assign(el('option'), { value: cid, textContent: cid }));
|
|
88
|
-
if (m.connector && !connectorIds.includes(m.connector)) connector.appendChild(Object.assign(el('option'), { value: m.connector, textContent: m.connector }));
|
|
89
|
-
connector.value = m.connector || '';
|
|
90
|
-
const enabled = Object.assign(el('input'), { type: 'checkbox', checked: m.enabled !== false });
|
|
91
|
-
const tags = el('div', 'cfg-tags');
|
|
92
|
-
const tagBoxes = TAGS.map((t) => {
|
|
93
|
-
const cb = Object.assign(el('input'), { type: 'checkbox', checked: (m.tags || []).includes(t) });
|
|
94
|
-
const l = el('label');
|
|
95
|
-
l.append(cb, ` ${t}`);
|
|
96
|
-
tags.appendChild(l);
|
|
97
|
-
return [t, cb];
|
|
98
|
-
});
|
|
99
|
-
const overrides = Object.assign(el('textarea'), { rows: 3, spellcheck: false, placeholder: '{ "temperature": 0.3 }' });
|
|
100
|
-
const kept = Object.fromEntries(OVERRIDE_KEYS.filter((k) => m[k] !== undefined).map((k) => [k, m[k]]));
|
|
101
|
-
overrides.value = Object.keys(kept).length ? JSON.stringify(kept, null, 2) : '';
|
|
102
|
-
const enabledLabel = el('label');
|
|
103
|
-
enabledLabel.append(enabled, ' enabled');
|
|
104
|
-
|
|
105
|
-
const grid = el('div', 'set-grid');
|
|
106
|
-
grid.append(
|
|
107
|
-
field('Id', id, 'Stable handle: roles and preferences name this.'),
|
|
108
|
-
field('Label', label, 'What people see.'),
|
|
109
|
-
Object.assign(field('Description', description), { className: 'span-2' }),
|
|
110
|
-
field('Provider model id', model, 'Blank: the id above.'),
|
|
111
|
-
field('Connector', connector),
|
|
112
|
-
Object.assign(field('Capabilities', tags), { className: 'span-2' }),
|
|
113
|
-
Object.assign(field('Overrides (JSON)', overrides, `Optional: ${OVERRIDE_KEYS.join(', ')}.`), { className: 'span-2' })
|
|
114
|
-
);
|
|
115
|
-
const actions = el('div', 'cfg-actions');
|
|
116
|
-
actions.append(enabledLabel, removeButton(row));
|
|
117
|
-
row.append(grid, actions);
|
|
118
|
-
row.read = () => {
|
|
119
|
-
let extra = {};
|
|
120
|
-
if (overrides.value.trim()) {
|
|
121
|
-
try { extra = JSON.parse(overrides.value); } catch (err) { throw new Error(`model "${id.value.trim()}" overrides: ${err.message}`); }
|
|
122
|
-
const bad = Object.keys(extra).filter((k) => !OVERRIDE_KEYS.includes(k));
|
|
123
|
-
if (bad.length || !extra || typeof extra !== 'object' || Array.isArray(extra)) throw new Error(`model "${id.value.trim()}" overrides must be an object of: ${OVERRIDE_KEYS.join(', ')}`);
|
|
124
|
-
}
|
|
125
|
-
const chosen = tagBoxes.filter(([, cb]) => cb.checked).map(([t]) => t);
|
|
126
|
-
return {
|
|
127
|
-
id: id.value.trim(),
|
|
128
|
-
...(label.value.trim() ? { label: label.value.trim() } : {}),
|
|
129
|
-
...(description.value.trim() ? { description: description.value.trim() } : {}),
|
|
130
|
-
...(model.value.trim() ? { model: model.value.trim() } : {}),
|
|
131
|
-
...(connector.value ? { connector: connector.value } : {}),
|
|
132
|
-
...(chosen.length ? { tags: chosen } : {}),
|
|
133
|
-
...(enabled.checked ? {} : { enabled: false }),
|
|
134
|
-
...extra
|
|
135
|
-
};
|
|
136
|
-
};
|
|
137
|
-
return row;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const rows = (box) => [...box.children];
|
|
141
|
-
const connectorIds = () => rows($('connectorsBox')).map((r) => r.read().id).filter(Boolean);
|
|
142
|
-
|
|
143
|
-
/** Rebuilds both editors from /api/config. */
|
|
144
|
-
export function renderConnectors(cfg) {
|
|
145
|
-
$('connectorsBox').replaceChildren(...(cfg.connectors || []).map((c) => connectorRow(c)));
|
|
146
|
-
const ids = (cfg.connectors || []).map((c) => c.id);
|
|
147
|
-
$('modelsBox').replaceChildren(...(cfg.models || []).map((m) => modelRow(m, ids)));
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** What the editors currently hold, as the config patch to save. Throws on bad JSON. */
|
|
151
|
-
export function collectConnectors() {
|
|
152
|
-
return {
|
|
153
|
-
connectors: rows($('connectorsBox')).map((r) => r.read()),
|
|
154
|
-
models: rows($('modelsBox')).map((r) => r.read())
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/** Locks or unlocks every control in the three admin editors. */
|
|
159
|
-
export function lockConnectors(locked) {
|
|
160
|
-
for (const id of ['connectorsBox', 'modelsBox']) for (const n of $(id).querySelectorAll('input,select,textarea,button')) n.disabled = locked;
|
|
161
|
-
$('addConnector').disabled = locked;
|
|
162
|
-
$('addModel').disabled = locked;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export function initConnectors() {
|
|
166
|
-
$('addConnector').onclick = () => {
|
|
167
|
-
$('connectorsBox').appendChild(connectorRow());
|
|
168
|
-
// New connector ids become choices in every model row.
|
|
169
|
-
refreshConnectorChoices();
|
|
170
|
-
};
|
|
171
|
-
$('addModel').onclick = () => $('modelsBox').appendChild(modelRow({}, connectorIds()));
|
|
172
|
-
// Ids are typed after the row exists, so the model rows re-read them on focus.
|
|
173
|
-
$('modelsBox').addEventListener('focusin', (e) => { if (e.target.classList?.contains('model-connector')) refreshConnectorChoices(); });
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function refreshConnectorChoices() {
|
|
177
|
-
const ids = connectorIds();
|
|
178
|
-
for (const sel of $('modelsBox').querySelectorAll('.model-connector')) {
|
|
179
|
-
const current = sel.value;
|
|
180
|
-
sel.replaceChildren(Object.assign(el('option'), { value: '', textContent: 'Default endpoint' }),
|
|
181
|
-
...[...new Set([...ids, ...(current && !ids.includes(current) ? [current] : [])])].map((cid) => Object.assign(el('option'), { value: cid, textContent: cid })));
|
|
182
|
-
sel.value = current;
|
|
183
|
-
}
|
|
184
|
-
}
|
package/src/set-password.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, existsSync, chmodSync } from 'node:fs';
|
|
2
|
-
import { createInterface } from 'node:readline';
|
|
3
|
-
import { createConfigSource } from './config/config.js';
|
|
4
|
-
import { hashPassword } from './access/auth.js';
|
|
5
|
-
import { MIN_PASSWORD_LENGTH } from './access/policy.js';
|
|
6
|
-
|
|
7
|
-
/** Reads one line without echoing it when attached to a terminal. */
|
|
8
|
-
function ask(prompt) {
|
|
9
|
-
return new Promise((resolve) => {
|
|
10
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: process.stdin.isTTY });
|
|
11
|
-
if (process.stdin.isTTY) rl._writeToOutput = (s) => { if (s.startsWith(prompt)) rl.output.write(prompt); };
|
|
12
|
-
rl.question(prompt, (answer) => { rl.close(); if (process.stdin.isTTY) process.stdout.write('\n'); resolve(answer); });
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* `tinywebui set-password`: stores an scrypt hash of a new password in the
|
|
18
|
-
* config file and switches authMode to 'single'. Existing sessions are
|
|
19
|
-
* signed with sessionSecret, which is rotated so they all end here too.
|
|
20
|
-
*/
|
|
21
|
-
export async function setPassword(opts = {}) {
|
|
22
|
-
if (opts.config && Object.hasOwn(opts.config, 'authMode') && opts.config.authMode !== 'single') {
|
|
23
|
-
throw new Error('authMode is set in code; change the JS config to use password login');
|
|
24
|
-
}
|
|
25
|
-
if (opts.config && Object.hasOwn(opts.config, 'authPassword')) {
|
|
26
|
-
throw new Error('authPassword is set in code; change the JS config to update the password');
|
|
27
|
-
}
|
|
28
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
29
|
-
throw new Error('set-password needs an interactive terminal; for headless startup set TINYWEBUI_PASSWORD');
|
|
30
|
-
}
|
|
31
|
-
const file = createConfigSource(opts).path();
|
|
32
|
-
if (!file) throw new Error('no config file to write to');
|
|
33
|
-
const pw = await ask('New password: ');
|
|
34
|
-
if (pw.length < MIN_PASSWORD_LENGTH) throw new Error(`use at least ${MIN_PASSWORD_LENGTH} characters`);
|
|
35
|
-
if ((await ask('Repeat password: ')) !== pw) throw new Error('passwords do not match');
|
|
36
|
-
const current = existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : {};
|
|
37
|
-
current.authMode = 'single';
|
|
38
|
-
current.authPassword = hashPassword(pw);
|
|
39
|
-
delete current.sessionSecret; // regenerated on next start: every old session ends
|
|
40
|
-
if (existsSync(file) && process.platform !== 'win32') chmodSync(file, 0o600);
|
|
41
|
-
writeFileSync(file, JSON.stringify(current, null, 2) + '\n', { mode: 0o600 });
|
|
42
|
-
console.log(`[tinywebui] password set in ${file}`);
|
|
43
|
-
}
|