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.
Files changed (82) hide show
  1. package/README.md +20 -13
  2. package/bin/tinywebui.js +69 -63
  3. package/package.json +25 -8
  4. package/public/app.js +40 -14
  5. package/public/chat/composer.js +1 -1
  6. package/public/chat/sidebar.js +8 -5
  7. package/public/index.html +88 -49
  8. package/public/panels/admin.js +40 -1
  9. package/public/panels/model-editor.js +336 -0
  10. package/public/panels/panels.js +37 -12
  11. package/public/panels/settings-home.js +125 -0
  12. package/public/panels/settings.js +124 -36
  13. package/public/shared/attachment_note.js +7 -1
  14. package/public/shared/events.js +1 -0
  15. package/public/shared/features.js +1 -0
  16. package/public/shared/origins.js +1 -0
  17. package/public/shared/usage.js +16 -1
  18. package/public/styles.css +83 -20
  19. package/src/access/auth.js +155 -155
  20. package/src/access/auth_gate.js +39 -38
  21. package/src/access/email.js +17 -0
  22. package/src/access/policy.js +123 -138
  23. package/src/audit.js +1 -1
  24. package/src/automations/automation.js +135 -123
  25. package/src/automations/scheduler.js +138 -127
  26. package/src/chat/compact.js +149 -156
  27. package/src/chat/llm.js +380 -382
  28. package/src/chat/runs.js +162 -151
  29. package/src/chat/tool_executor.js +229 -218
  30. package/src/cli.js +535 -433
  31. package/src/config/approval.js +35 -29
  32. package/src/config/config.js +546 -451
  33. package/src/config/data_dir.js +24 -0
  34. package/src/config/models.js +191 -115
  35. package/src/config/refs.js +32 -0
  36. package/src/config/secrets.js +153 -0
  37. package/src/files/documents.js +57 -68
  38. package/src/files/images.js +30 -31
  39. package/src/harness/agent.js +60 -60
  40. package/src/harness/attribution.js +66 -58
  41. package/src/harness/checkpoint.js +67 -62
  42. package/src/harness/history.js +28 -23
  43. package/src/harness/prompt.js +40 -57
  44. package/src/harness/provider.js +284 -283
  45. package/src/harness/types.js +1 -0
  46. package/src/http.js +98 -66
  47. package/src/log.js +11 -12
  48. package/src/mcp.js +398 -374
  49. package/src/retrieval/embedding.js +228 -240
  50. package/src/retrieval/retrieval.js +182 -173
  51. package/src/routes/admin.js +178 -138
  52. package/src/routes/auth.js +184 -100
  53. package/src/routes/automations.js +99 -82
  54. package/src/routes/chats.js +240 -218
  55. package/src/routes/documents.js +130 -120
  56. package/src/routes/library.js +43 -46
  57. package/src/routes/mcp.js +49 -49
  58. package/src/routes/settings.js +127 -122
  59. package/src/routes/types.js +1 -0
  60. package/src/server.js +428 -392
  61. package/src/store/automations.js +106 -106
  62. package/src/store/chats.js +217 -242
  63. package/src/store/documents.js +181 -185
  64. package/src/store/embeddings.js +52 -54
  65. package/src/store/index.js +95 -49
  66. package/src/store/messages.js +55 -80
  67. package/src/store/schema.js +128 -107
  68. package/src/store/scope.js +18 -17
  69. package/src/store/search.js +25 -27
  70. package/src/store/turns.js +53 -53
  71. package/src/store/usage.js +343 -269
  72. package/src/store/users.js +162 -87
  73. package/src/store/wire.js +50 -41
  74. package/src/tools/ask_tool.js +68 -68
  75. package/src/tools/chat_search_tool.js +38 -44
  76. package/src/tools/context_tool.js +131 -138
  77. package/src/tools/document_tool.js +73 -78
  78. package/src/tools/mcp_tool.js +274 -232
  79. package/src/tools/task_tool.js +53 -50
  80. package/src/tools/types.js +1 -0
  81. package/public/panels/connectors.js +0 -184
  82. package/src/set-password.js +0 -43
@@ -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
- name: t.function.name,
11
- description: t.function.description,
12
- parameters: t.function.parameters
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
- // The provider's model list, cached per endpoint for a few minutes:
17
- // OpenRouter's list is large and the picker is opened far more often than
18
- // the catalogue changes.
19
- let modelCache = null;
20
- const listModels = async () => {
21
- const cfg = config();
22
- const key = `${cfg.baseUrl}|${Boolean(cfg.apiKey)}`;
23
- if (modelCache?.key === key && Date.now() - modelCache.at < 5 * 60_000) return modelCache.value;
24
- let value;
25
- try {
26
- const r = await fetch(`${cfg.baseUrl}/models`, {
27
- headers: cfg.apiKey ? { authorization: `Bearer ${cfg.apiKey}` } : {},
28
- signal: AbortSignal.timeout(8000)
29
- });
30
- if (!r.ok) 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
- } catch (err) {
39
- value = { supported: false, models: [], error: err.message };
40
- }
41
- modelCache = { key, at: Date.now(), value };
42
- return value;
43
- };
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
-
52
- { method: 'POST', path: /^\/api\/config$/, feature: FEATURE.SETTINGS, handle: async ({ req, res, auth }) => {
53
- const patch = await readJson(req);
54
- // Credentials and routing: the sole user or an admin, not any role that
55
- // was merely granted the settings feature.
56
- const secret = Object.keys(patch).filter((k) => SECRET_PATCH_KEYS.has(k));
57
- if (secret.length && !auth.isAdmin) 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
-
63
- // The provider's own model list, for the composer's model picker. Not
64
- // every OpenAI-compatible endpoint serves /models, so a failure is an
65
- // answer too: the picker falls back to typing an id.
66
- { method: 'GET', path: /^\/api\/models$/, feature: FEATURE.MODEL_PICKER, handle: async ({ res, auth }) => {
67
- const cfg = config();
68
- const allowed = modelsFor(cfg, auth.role);
69
- // A catalog is the whole list: its labels, never the provider's ids.
70
- if (isClosed(cfg)) {
71
- const entries = enabledEntries(cfg).filter((e) => allowed === '*' || allowed.includes(e.id));
72
- return json(res, 200, { supported: true, restricted: true, catalog: true, models: entries.map(publicEntry) });
73
- }
74
- const all = await listModels();
75
- if (allowed === '*') 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
-
82
- // "Test connection" for the connector editor. A key typed into the form is
83
- // used as is; a blank one means the stored key of that connector ('default'
84
- // is the top-level baseUrl/apiKey), since the page never holds keys.
85
- { method: 'POST', path: /^\/api\/connectors\/test$/, feature: FEATURE.SETTINGS, handle: async ({ req, res, auth }) => {
86
- if (!auth.isAdmin) return json(res, 403, { error: 'only an admin can test connectors' });
87
- const body = await readJson(req);
88
- const cfg = config();
89
- const stored = body.id && body.id !== 'default' ? findConnector(cfg, body.id) : { baseUrl: cfg.baseUrl, apiKey: cfg.apiKey };
90
- const baseUrl = String(body.baseUrl || stored?.baseUrl || '').replace(/\/+$/, '');
91
- const apiKey = body.apiKey || stored?.apiKey || '';
92
- if (!/^https?:\/\//.test(baseUrl)) return json(res, 400, { error: 'baseUrl must be an http(s) URL' });
93
- try {
94
- const r = await fetch(`${baseUrl}/models`, {
95
- headers: apiKey ? { authorization: `Bearer ${apiKey}` } : {},
96
- signal: AbortSignal.timeout(8000)
97
- });
98
- if (r.status === 401 || r.status === 403) return json(res, 200, { ok: false, error: `${r.status}: the endpoint rejected the key` });
99
- // Not every OpenAI-compatible server lists models; reachable is the point.
100
- const list = r.ok ? await r.json().catch(() => null) : null;
101
- const rows = Array.isArray(list?.data) ? list.data : Array.isArray(list?.models) ? list.models : null;
102
- return json(res, 200, { ok: r.status < 500, status: r.status, models: rows ? rows.length : null });
103
- } catch (err) {
104
- return json(res, 200, { ok: false, error: err.message });
105
- }
106
- } },
107
-
108
- // The grouped view behind the tools panel: built-ins, and every MCP
109
- // server's tools under it with that server's connection health.
110
- { method: 'GET', path: /^\/api\/tools$/, feature: FEATURE.TOOLS, handle: ({ res }) => json(res, 200, toolsView()) },
111
-
112
- // Per-tool approval override from the settings panel: 'ask', 'auto', or
113
- // 'default' to fall back to the global toolApproval mode.
114
- { method: 'POST', path: /^\/api\/tools\/approval$/, feature: FEATURE.TOOLS, handle: async ({ req, res, auth }) => {
115
- const { name, policy } = await readJson(req);
116
- if (!name || !['ask', 'auto', 'default'].includes(policy)) {
117
- return json(res, 400, { error: 'name and policy (ask | auto | default) are required' });
118
- }
119
- saveConfig(setOverride(config(), name, policy), actor(auth));
120
- return json(res, 200, toolsView());
121
- } },
122
-
123
- { method: 'POST', path: /^\/api\/tools\/toggle$/, feature: FEATURE.TOOLS, handle: async ({ req, res, auth }) => {
124
- const { name, disabled } = await readJson(req);
125
- if (!name) return json(res, 400, { error: 'name is required' });
126
- const set = new Set(config().disabledTools || []);
127
- if (disabled) set.add(name); else set.delete(name);
128
- saveConfig({ disabledTools: [...set] }, actor(auth));
129
- return json(res, 200, toolsView());
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 {};