tinywebui 0.1.4 → 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 -53
- package/package.json +25 -8
- package/public/app.js +40 -34
- package/public/chat/composer.js +1 -1
- package/public/chat/sidebar.js +8 -5
- package/public/index.html +88 -53
- 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 +84 -21
- 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 -136
- 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 -454
- 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 +90 -72
- 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 -111
- 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 +426 -415
- 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/routes/settings.js
CHANGED
|
@@ -1,132 +1,137 @@
|
|
|
1
|
-
import { json, readJson } from '../http.js';
|
|
1
|
+
import { json, readJson, oneOf } from '../http.js';
|
|
2
2
|
import { isClosed, enabledEntries, publicEntry, findConnector } from '../config/models.js';
|
|
3
3
|
import { modelsFor, SECRET_PATCH_KEYS } from '../access/policy.js';
|
|
4
4
|
import { audit } from '../audit.js';
|
|
5
5
|
import { setOverride } from '../config/approval.js';
|
|
6
6
|
import { actor } from '../access/auth_gate.js';
|
|
7
7
|
import { FEATURE } from '../../public/shared/features.js';
|
|
8
|
-
|
|
9
8
|
export const toolList = (hub) => hub.tools.map((t) => ({
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
name: t.function.name,
|
|
10
|
+
description: t.function.description,
|
|
11
|
+
parameters: t.function.parameters
|
|
13
12
|
}));
|
|
14
|
-
|
|
15
13
|
export function settingsRoutes({ config, configFor, hub, saveConfig, toolsView }) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
14
|
+
// The provider's model list, cached per endpoint for a few minutes:
|
|
15
|
+
// OpenRouter's list is large and the picker is opened far more often than
|
|
16
|
+
// the catalogue changes.
|
|
17
|
+
let modelCache = null;
|
|
18
|
+
const listModels = async () => {
|
|
19
|
+
const cfg = config();
|
|
20
|
+
const key = `${cfg.baseUrl}|${Boolean(cfg.apiKey)}`;
|
|
21
|
+
if (modelCache?.key === key && Date.now() - modelCache.at < 5 * 60_000)
|
|
22
|
+
return modelCache.value;
|
|
23
|
+
let value;
|
|
24
|
+
try {
|
|
25
|
+
const r = await fetch(`${cfg.baseUrl}/models`, {
|
|
26
|
+
headers: cfg.apiKey ? { authorization: `Bearer ${cfg.apiKey}` } : {},
|
|
27
|
+
signal: AbortSignal.timeout(8000)
|
|
28
|
+
});
|
|
29
|
+
if (!r.ok)
|
|
30
|
+
throw new Error(`${r.status} from ${cfg.baseUrl}/models`);
|
|
31
|
+
const body = await r.json();
|
|
32
|
+
const rows = Array.isArray(body?.data) ? body.data : Array.isArray(body?.models) ? body.models : [];
|
|
33
|
+
const models = rows
|
|
34
|
+
.map((m) => ({ id: m.id || m.name, name: m.name && m.name !== m.id ? m.name : null }))
|
|
35
|
+
.filter((m) => typeof m.id === 'string' && m.id)
|
|
36
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
37
|
+
value = { supported: models.length > 0, models };
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
value = { supported: false, models: [], error: err.message };
|
|
41
|
+
}
|
|
42
|
+
modelCache = { key, at: Date.now(), value };
|
|
43
|
+
return value;
|
|
44
|
+
};
|
|
45
|
+
return [
|
|
46
|
+
{ method: 'GET', path: /^\/api\/config$/, feature: null, handle: ({ res, auth }) => json(res, 200, {
|
|
47
|
+
...configFor(auth),
|
|
48
|
+
tools: toolList(hub()),
|
|
49
|
+
mcpErrors: hub().errors
|
|
50
|
+
}) },
|
|
51
|
+
{ method: 'POST', path: /^\/api\/config$/, feature: FEATURE.SETTINGS, handle: async ({ req, res, auth }) => {
|
|
52
|
+
const patch = await readJson(req);
|
|
53
|
+
// Credentials and routing: the sole user or an admin, not any role that
|
|
54
|
+
// was merely granted the settings feature.
|
|
55
|
+
const secret = Object.keys(patch).filter((k) => SECRET_PATCH_KEYS.has(k));
|
|
56
|
+
if (secret.length && !auth.isAdmin)
|
|
57
|
+
return json(res, 403, { error: `only an admin can change: ${secret.join(', ')}` });
|
|
58
|
+
saveConfig(patch, actor(auth));
|
|
59
|
+
audit('admin.config_changed', { by: auth.userId ?? null });
|
|
60
|
+
return json(res, 200, configFor(auth));
|
|
61
|
+
} },
|
|
62
|
+
// The provider's own model list, for the composer's model picker. Not
|
|
63
|
+
// every OpenAI-compatible endpoint serves /models, so a failure is an
|
|
64
|
+
// answer too: the picker falls back to typing an id.
|
|
65
|
+
{ method: 'GET', path: /^\/api\/models$/, feature: FEATURE.MODEL_PICKER, handle: async ({ res, auth }) => {
|
|
66
|
+
const cfg = config();
|
|
67
|
+
const allowed = modelsFor(cfg, auth.role);
|
|
68
|
+
// A catalog is the whole list: its labels, never the provider's ids.
|
|
69
|
+
if (isClosed(cfg)) {
|
|
70
|
+
const entries = enabledEntries(cfg).filter((e) => allowed === '*' || allowed.includes(e.id));
|
|
71
|
+
return json(res, 200, { supported: true, restricted: true, catalog: true, models: entries.map(publicEntry) });
|
|
72
|
+
}
|
|
73
|
+
const all = await listModels();
|
|
74
|
+
if (allowed === '*')
|
|
75
|
+
return json(res, 200, all);
|
|
76
|
+
// A fixed catalog: only those ids, named from the provider list when
|
|
77
|
+
// it knows them, listed even when it does not.
|
|
78
|
+
const byId = new Map(all.models.map((m) => [m.id, m]));
|
|
79
|
+
return json(res, 200, { supported: true, restricted: true, models: allowed.map((id) => byId.get(id) || { id, name: null }) });
|
|
80
|
+
} },
|
|
81
|
+
// "Test connection" for the connector editor. A key typed into the form is
|
|
82
|
+
// used as is; a blank one means the stored key of that connector ('default'
|
|
83
|
+
// is the top-level baseUrl/apiKey), since the page never holds keys.
|
|
84
|
+
{ method: 'POST', path: /^\/api\/connectors\/test$/, feature: FEATURE.SETTINGS, handle: async ({ req, res, auth }) => {
|
|
85
|
+
if (!auth.isAdmin)
|
|
86
|
+
return json(res, 403, { error: 'only an admin can test connectors' });
|
|
87
|
+
const body = await readJson(req);
|
|
88
|
+
const cfg = config();
|
|
89
|
+
const text = (v) => (typeof v === 'string' ? v : '');
|
|
90
|
+
const stored = text(body.id) && body.id !== 'default' ? findConnector(cfg, text(body.id)) : { baseUrl: cfg.baseUrl, apiKey: cfg.apiKey };
|
|
91
|
+
const baseUrl = String(text(body.baseUrl) || stored?.baseUrl || '').replace(/\/+$/, '');
|
|
92
|
+
const apiKey = text(body.apiKey) || stored?.apiKey || '';
|
|
93
|
+
if (!/^https?:\/\//.test(baseUrl))
|
|
94
|
+
return json(res, 400, { error: 'baseUrl must be an http(s) URL' });
|
|
95
|
+
try {
|
|
96
|
+
const r = await fetch(`${baseUrl}/models`, {
|
|
97
|
+
headers: apiKey ? { authorization: `Bearer ${apiKey}` } : {},
|
|
98
|
+
signal: AbortSignal.timeout(8000)
|
|
99
|
+
});
|
|
100
|
+
if (r.status === 401 || r.status === 403)
|
|
101
|
+
return json(res, 200, { ok: false, error: `${r.status}: the endpoint rejected the key` });
|
|
102
|
+
// Not every OpenAI-compatible server lists models; reachable is the point.
|
|
103
|
+
const list = r.ok ? await r.json().catch(() => null) : null;
|
|
104
|
+
const rows = Array.isArray(list?.data) ? list.data : Array.isArray(list?.models) ? list.models : null;
|
|
105
|
+
return json(res, 200, { ok: r.status < 500, status: r.status, models: rows ? rows.length : null });
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
return json(res, 200, { ok: false, error: err.message });
|
|
109
|
+
}
|
|
110
|
+
} },
|
|
111
|
+
// The grouped view behind the tools panel: built-ins, and every MCP
|
|
112
|
+
// server's tools under it with that server's connection health.
|
|
113
|
+
{ method: 'GET', path: /^\/api\/tools$/, feature: FEATURE.TOOLS, handle: ({ res }) => json(res, 200, toolsView()) },
|
|
114
|
+
// Per-tool approval override from the settings panel: 'ask', 'auto', or
|
|
115
|
+
// 'default' to fall back to the global toolApproval mode.
|
|
116
|
+
{ method: 'POST', path: /^\/api\/tools\/approval$/, feature: FEATURE.TOOLS, handle: async ({ req, res, auth }) => {
|
|
117
|
+
const { name, policy } = await readJson(req);
|
|
118
|
+
if (typeof name !== 'string' || !name || !oneOf(policy, ['ask', 'auto', 'default'])) {
|
|
119
|
+
return json(res, 400, { error: 'name and policy (ask | auto | default) are required' });
|
|
120
|
+
}
|
|
121
|
+
saveConfig(setOverride(config(), name, policy), actor(auth));
|
|
122
|
+
return json(res, 200, toolsView());
|
|
123
|
+
} },
|
|
124
|
+
{ method: 'POST', path: /^\/api\/tools\/toggle$/, feature: FEATURE.TOOLS, handle: async ({ req, res, auth }) => {
|
|
125
|
+
const { name, disabled } = await readJson(req);
|
|
126
|
+
if (typeof name !== 'string' || !name)
|
|
127
|
+
return json(res, 400, { error: 'name is required' });
|
|
128
|
+
const set = new Set(config().disabledTools || []);
|
|
129
|
+
if (disabled)
|
|
130
|
+
set.add(name);
|
|
131
|
+
else
|
|
132
|
+
set.delete(name);
|
|
133
|
+
saveConfig({ disabledTools: [...set] }, actor(auth));
|
|
134
|
+
return json(res, 200, toolsView());
|
|
135
|
+
} }
|
|
136
|
+
];
|
|
132
137
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|