@danieltmn/tamnora 0.21.1 → 0.22.0
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/CHANGELOG.md +15 -0
- package/README.md +10 -6
- package/SECURITY.md +13 -0
- package/harness/packages/core/src/agent/runAgent.js +9 -5
- package/harness/packages/core/src/browser/index.js +56 -2
- package/harness/packages/core/src/hooks/index.js +9 -5
- package/harness/packages/core/src/index.js +1 -0
- package/harness/packages/core/src/mcp/stdio.js +15 -0
- package/harness/packages/core/src/net/hosts.js +181 -0
- package/harness/packages/core/src/plugins/index.js +6 -4
- package/harness/packages/core/src/redact.js +2 -0
- package/harness/packages/core/src/tools/executors.js +54 -53
- package/harness/packages/core/src/tools/index.js +1 -1
- package/harness/packages/core/src/tools/paths.js +35 -5
- package/harness/packages/core/src/tools/permissions.js +41 -2
- package/harness/packages/core/src/tools/registry.js +8 -3
- package/harness/packages/core/src/tunnel.js +15 -5
- package/harness/packages/core/test/hooks.test.js +6 -3
- package/harness/packages/core/test/hosts.test.js +50 -0
- package/harness/packages/core/test/plugins.test.js +5 -3
- package/harness/packages/core/test/tools.test.js +98 -2
- package/harness/packages/server/public/app.js +343 -6729
- package/harness/packages/server/public/chat.js +1893 -0
- package/harness/packages/server/public/chrome.js +812 -0
- package/harness/packages/server/public/commands.js +403 -0
- package/harness/packages/server/public/core.js +209 -0
- package/harness/packages/server/public/explorer.js +625 -0
- package/harness/packages/server/public/index.html +19 -19
- package/harness/packages/server/public/sessions.js +274 -0
- package/harness/packages/server/public/settings.js +1076 -0
- package/harness/packages/server/public/styles.css +29 -2
- package/harness/packages/server/public/system.js +495 -0
- package/harness/packages/server/public/voice.js +1058 -0
- package/harness/packages/server/src/auth.js +12 -10
- package/harness/packages/server/src/lib/agentRun.js +6 -1
- package/harness/packages/server/src/lib/channels/telegram.js +1 -0
- package/harness/packages/server/src/lib/presence.js +12 -1
- package/harness/packages/server/src/lib/procs.js +30 -3
- package/harness/packages/server/src/lib/scheduler.js +11 -2
- package/harness/packages/server/src/lib/tasks.js +20 -3
- package/harness/packages/server/src/lib/workspace.js +90 -2
- package/harness/packages/server/src/routes/auth.js +8 -4
- package/harness/packages/server/src/routes/chat.js +15 -4
- package/harness/packages/server/src/routes/preview.js +10 -2
- package/harness/packages/server/src/routes/procs.js +2 -6
- package/harness/packages/server/src/routes/providers.js +12 -9
- package/harness/packages/server/src/server.js +28 -2
- package/harness/packages/server/test/chat.test.js +586 -0
- package/harness/packages/server/test/helpers.js +107 -0
- package/harness/packages/server/test/ops.test.js +511 -0
- package/harness/packages/server/test/procs-manager.test.js +43 -0
- package/harness/packages/server/test/runs.test.js +816 -0
- package/harness/packages/server/test/security.test.js +112 -9
- package/harness/packages/server/test/sessions.test.js +437 -0
- package/harness/packages/server/test/settings.test.js +465 -0
- package/harness/tamnora.example.json +1 -1
- package/package.json +1 -1
- package/harness/packages/server/test/server.test.js +0 -2878
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtemp, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { completionFooter } from '../src/lib/channels/telegram.js';
|
|
7
|
+
import { startServer } from './helpers.js';
|
|
8
|
+
|
|
9
|
+
test('completionFooter resume el cierre del turno', () => {
|
|
10
|
+
assert.equal(completionFooter('end_turn', 1, 0, 0), '✓ terminado · 1 ronda');
|
|
11
|
+
assert.equal(completionFooter('end_turn', 3, 5, 1200), '✓ terminado · 3 rondas · 5 herramientas · 1.2s');
|
|
12
|
+
assert.match(completionFooter('max_rounds', 2, 2, 300), /^⚠ corte de seguridad · 2 rondas · 2 herramientas · 300ms$/);
|
|
13
|
+
assert.match(completionFooter('no_progress', 2, 2, 300), /^⚠ corte de seguridad/);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('configures Telegram from the console (masked token)', async () => {
|
|
17
|
+
const { instance, base } = await startServer({ telegram: false });
|
|
18
|
+
const json = { 'Content-Type': 'application/json' };
|
|
19
|
+
try {
|
|
20
|
+
let data = await (await fetch(`${base}/api/channels`)).json();
|
|
21
|
+
assert.equal(data.telegram.tokenSet, false);
|
|
22
|
+
assert.equal(data.telegram.transcribeModel, 'whisper-1');
|
|
23
|
+
|
|
24
|
+
let res = await fetch(`${base}/api/channels/telegram`, {
|
|
25
|
+
method: 'PUT',
|
|
26
|
+
headers: json,
|
|
27
|
+
body: JSON.stringify({ allowedChatIds: '123, 456', transcribeModel: '' }),
|
|
28
|
+
});
|
|
29
|
+
data = await res.json();
|
|
30
|
+
assert.deepEqual(data.telegram.allowedChatIds, ['123', '456']);
|
|
31
|
+
assert.equal(data.telegram.transcribeModel, 'whisper-1');
|
|
32
|
+
|
|
33
|
+
res = await fetch(`${base}/api/channels/telegram`, {
|
|
34
|
+
method: 'PUT',
|
|
35
|
+
headers: json,
|
|
36
|
+
body: JSON.stringify({ token: '123456:ABCDEFghij', transcribeProvider: 'openai' }),
|
|
37
|
+
});
|
|
38
|
+
data = await res.json();
|
|
39
|
+
assert.equal(data.telegram.tokenSet, true);
|
|
40
|
+
assert.equal(data.telegram.transcribeProvider, 'openai');
|
|
41
|
+
assert.match(data.telegram.tokenHint, /ghij$/);
|
|
42
|
+
assert.doesNotMatch(JSON.stringify(data), /ABCDEF/);
|
|
43
|
+
|
|
44
|
+
// Mandar la mascara no pisa el token real.
|
|
45
|
+
res = await fetch(`${base}/api/channels/telegram`, {
|
|
46
|
+
method: 'PUT',
|
|
47
|
+
headers: json,
|
|
48
|
+
body: JSON.stringify({ token: data.telegram.tokenHint, allowedChatIds: ['1'] }),
|
|
49
|
+
});
|
|
50
|
+
data = await res.json();
|
|
51
|
+
assert.equal(data.telegram.tokenSet, true);
|
|
52
|
+
assert.match(data.telegram.tokenHint, /ghij$/);
|
|
53
|
+
} finally {
|
|
54
|
+
await instance.close();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('manages MCP servers from the console', async () => {
|
|
59
|
+
const { instance, base } = await startServer();
|
|
60
|
+
const json = { 'Content-Type': 'application/json' };
|
|
61
|
+
try {
|
|
62
|
+
let data = await (await fetch(`${base}/api/mcp`)).json();
|
|
63
|
+
assert.deepEqual(data.servers, []);
|
|
64
|
+
|
|
65
|
+
let res = await fetch(`${base}/api/mcp/servers/echo`, {
|
|
66
|
+
method: 'PUT',
|
|
67
|
+
headers: json,
|
|
68
|
+
body: JSON.stringify({ name: 'Echo', transport: 'stdio', command: 'node', args: ['a.mjs'], enabled: true }),
|
|
69
|
+
});
|
|
70
|
+
assert.equal(res.status, 200);
|
|
71
|
+
data = await res.json();
|
|
72
|
+
const echo = data.servers.find((s) => s.id === 'echo');
|
|
73
|
+
assert.equal(echo.source, 'store');
|
|
74
|
+
assert.deepEqual(echo.args, ['a.mjs']);
|
|
75
|
+
|
|
76
|
+
const bad = await fetch(`${base}/api/mcp/servers/bad`, {
|
|
77
|
+
method: 'PUT',
|
|
78
|
+
headers: json,
|
|
79
|
+
body: JSON.stringify({ transport: 'http' }),
|
|
80
|
+
});
|
|
81
|
+
assert.equal(bad.status, 400);
|
|
82
|
+
|
|
83
|
+
res = await fetch(`${base}/api/mcp/servers/echo`, { method: 'DELETE' });
|
|
84
|
+
assert.equal(res.status, 200);
|
|
85
|
+
data = await res.json();
|
|
86
|
+
assert.equal(data.servers.length, 0);
|
|
87
|
+
} finally {
|
|
88
|
+
await instance.close();
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('sirve el catálogo de MCP/skills y los instala deshabilitados', async () => {
|
|
93
|
+
const { instance, base } = await startServer();
|
|
94
|
+
const json = { 'Content-Type': 'application/json' };
|
|
95
|
+
try {
|
|
96
|
+
const catalog = await (await fetch(`${base}/api/mcp/catalog`)).json();
|
|
97
|
+
assert.ok(catalog.catalog.some((e) => e.id === 'playwright' && e.installed === false));
|
|
98
|
+
|
|
99
|
+
const add = await fetch(`${base}/api/mcp/import`, {
|
|
100
|
+
method: 'POST',
|
|
101
|
+
headers: json,
|
|
102
|
+
body: JSON.stringify({ catalog: 'playwright' }),
|
|
103
|
+
});
|
|
104
|
+
assert.equal(add.status, 200);
|
|
105
|
+
const added = await add.json();
|
|
106
|
+
assert.equal(added.imported, 'playwright');
|
|
107
|
+
assert.equal(added.servers.find((s) => s.id === 'playwright').enabled, false);
|
|
108
|
+
|
|
109
|
+
const again = await (await fetch(`${base}/api/mcp/catalog`)).json();
|
|
110
|
+
assert.equal(again.catalog.find((e) => e.id === 'playwright').installed, true);
|
|
111
|
+
|
|
112
|
+
const skills = await (await fetch(`${base}/api/skills/catalog`)).json();
|
|
113
|
+
assert.ok(skills.catalog.some((e) => e.id === 'code-review' && e.installed === false));
|
|
114
|
+
const skillAdd = await fetch(`${base}/api/skills`, {
|
|
115
|
+
method: 'POST',
|
|
116
|
+
headers: json,
|
|
117
|
+
body: JSON.stringify({ id: 'code-review' }),
|
|
118
|
+
});
|
|
119
|
+
assert.equal(skillAdd.status, 200);
|
|
120
|
+
const skillData = await skillAdd.json();
|
|
121
|
+
assert.equal(skillData.installed, 'code-review');
|
|
122
|
+
assert.equal(skillData.skills.find((s) => s.id === 'code-review').enabled, false);
|
|
123
|
+
|
|
124
|
+
const unknown = await fetch(`${base}/api/mcp/import`, {
|
|
125
|
+
method: 'POST',
|
|
126
|
+
headers: json,
|
|
127
|
+
body: JSON.stringify({ catalog: 'nope' }),
|
|
128
|
+
});
|
|
129
|
+
assert.equal(unknown.status, 404);
|
|
130
|
+
const unknownSkill = await fetch(`${base}/api/skills`, {
|
|
131
|
+
method: 'POST',
|
|
132
|
+
headers: json,
|
|
133
|
+
body: JSON.stringify({ id: 'nope' }),
|
|
134
|
+
});
|
|
135
|
+
assert.equal(unknownSkill.status, 404);
|
|
136
|
+
} finally {
|
|
137
|
+
await instance.close();
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('los buscadores devuelven vacío sin query', async () => {
|
|
142
|
+
const { instance, base } = await startServer();
|
|
143
|
+
try {
|
|
144
|
+
const mcp = await (await fetch(`${base}/api/mcp/search`)).json();
|
|
145
|
+
assert.deepEqual(mcp.results, []);
|
|
146
|
+
const skills = await (await fetch(`${base}/api/skills/search`)).json();
|
|
147
|
+
assert.deepEqual(skills.results, []);
|
|
148
|
+
} finally {
|
|
149
|
+
await instance.close();
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('MCP: el env se enmascara y no se pisa al guardar sin cambios', async () => {
|
|
154
|
+
const { instance, base } = await startServer();
|
|
155
|
+
const json = { 'Content-Type': 'application/json' };
|
|
156
|
+
try {
|
|
157
|
+
await fetch(`${base}/api/mcp/servers/db`, {
|
|
158
|
+
method: 'PUT',
|
|
159
|
+
headers: json,
|
|
160
|
+
body: JSON.stringify({
|
|
161
|
+
name: 'DB',
|
|
162
|
+
transport: 'stdio',
|
|
163
|
+
command: 'node',
|
|
164
|
+
args: ['db.mjs'],
|
|
165
|
+
env: { MYSQL_HOST: 'localhost', MYSQL_PASSWORD: 'supersecret' },
|
|
166
|
+
}),
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
let data = await (await fetch(`${base}/api/mcp`)).json();
|
|
170
|
+
const db = data.servers.find((s) => s.id === 'db');
|
|
171
|
+
assert.equal(db.env.MYSQL_HOST, 'localhost');
|
|
172
|
+
assert.equal(db.env.MYSQL_PASSWORD, '••••cret');
|
|
173
|
+
|
|
174
|
+
// Un toggle manda de vuelta el env enmascarado: no debe pisar el secreto.
|
|
175
|
+
await fetch(`${base}/api/mcp/servers/db`, {
|
|
176
|
+
method: 'PUT',
|
|
177
|
+
headers: json,
|
|
178
|
+
body: JSON.stringify({
|
|
179
|
+
name: db.name,
|
|
180
|
+
transport: 'stdio',
|
|
181
|
+
command: db.command,
|
|
182
|
+
args: db.args,
|
|
183
|
+
env: db.env,
|
|
184
|
+
enabled: false,
|
|
185
|
+
}),
|
|
186
|
+
});
|
|
187
|
+
const stored = instance.store.listMcpServers().find((s) => s.id === 'db');
|
|
188
|
+
assert.equal(stored.config.env.MYSQL_PASSWORD, 'supersecret');
|
|
189
|
+
assert.equal(stored.enabled, false);
|
|
190
|
+
} finally {
|
|
191
|
+
await instance.close();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('guarda los límites del agente desde Ajustes', async () => {
|
|
196
|
+
const { instance, base } = await startServer();
|
|
197
|
+
const json = { 'Content-Type': 'application/json' };
|
|
198
|
+
try {
|
|
199
|
+
const res = await fetch(`${base}/api/settings`, {
|
|
200
|
+
method: 'PUT',
|
|
201
|
+
headers: json,
|
|
202
|
+
body: JSON.stringify({ maxRounds: 20, maxTokens: 50000, maxDurationMs: 600000 }),
|
|
203
|
+
});
|
|
204
|
+
const data = await res.json();
|
|
205
|
+
assert.equal(data.settings.maxRounds, 20);
|
|
206
|
+
assert.equal(data.settings.maxTokens, 50000);
|
|
207
|
+
assert.equal(data.settings.maxDurationMs, 600000);
|
|
208
|
+
|
|
209
|
+
const again = await (await fetch(`${base}/api/settings`)).json();
|
|
210
|
+
assert.equal(again.settings.maxRounds, 20);
|
|
211
|
+
} finally {
|
|
212
|
+
await instance.close();
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('guarda el modo de carga y las tools fijas de MCP', async () => {
|
|
217
|
+
const { instance, base } = await startServer();
|
|
218
|
+
const json = { 'Content-Type': 'application/json' };
|
|
219
|
+
try {
|
|
220
|
+
const res = await fetch(`${base}/api/settings`, {
|
|
221
|
+
method: 'PUT',
|
|
222
|
+
headers: json,
|
|
223
|
+
body: JSON.stringify({ mcpMode: 'lazy', mcpPinned: 'mcp__postgres__query, postgres' }),
|
|
224
|
+
});
|
|
225
|
+
const data = await res.json();
|
|
226
|
+
assert.equal(data.settings.mcpMode, 'lazy');
|
|
227
|
+
assert.deepEqual(data.settings.mcpPinned, ['mcp__postgres__query', 'postgres']);
|
|
228
|
+
|
|
229
|
+
const mcp = await (await fetch(`${base}/api/mcp`)).json();
|
|
230
|
+
assert.equal(mcp.mode, 'lazy');
|
|
231
|
+
assert.deepEqual(mcp.pinned, ['mcp__postgres__query', 'postgres']);
|
|
232
|
+
assert.equal(mcp.threshold, 12);
|
|
233
|
+
assert.equal(mcp.toolCount, null);
|
|
234
|
+
|
|
235
|
+
const bad = await fetch(`${base}/api/settings`, {
|
|
236
|
+
method: 'PUT',
|
|
237
|
+
headers: json,
|
|
238
|
+
body: JSON.stringify({ mcpMode: 'nope' }),
|
|
239
|
+
});
|
|
240
|
+
assert.equal((await bad.json()).settings.mcpMode, 'auto');
|
|
241
|
+
} finally {
|
|
242
|
+
await instance.close();
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('imports an MCP server from a JSON snippet', async () => {
|
|
247
|
+
const { instance, base } = await startServer();
|
|
248
|
+
const json = { 'Content-Type': 'application/json' };
|
|
249
|
+
try {
|
|
250
|
+
const res = await fetch(`${base}/api/mcp/import`, {
|
|
251
|
+
method: 'POST',
|
|
252
|
+
headers: json,
|
|
253
|
+
body: JSON.stringify({ json: '{"mcpServers":{"demo":{"command":"npx","args":["-y","pkg"]}}}' }),
|
|
254
|
+
});
|
|
255
|
+
assert.equal(res.status, 200);
|
|
256
|
+
const data = await res.json();
|
|
257
|
+
assert.equal(data.imported, 'demo');
|
|
258
|
+
assert.ok(data.servers.some((s) => s.id === 'demo' && s.transport === 'stdio'));
|
|
259
|
+
} finally {
|
|
260
|
+
await instance.close();
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test('rejects unknown import refs (skills and mcpservers.org)', async () => {
|
|
265
|
+
const { instance, base } = await startServer();
|
|
266
|
+
const json = { 'Content-Type': 'application/json' };
|
|
267
|
+
try {
|
|
268
|
+
const badSkill = await fetch(`${base}/api/skills/import`, {
|
|
269
|
+
method: 'POST',
|
|
270
|
+
headers: json,
|
|
271
|
+
body: JSON.stringify({ ref: 'nope' }),
|
|
272
|
+
});
|
|
273
|
+
assert.equal(badSkill.status, 400);
|
|
274
|
+
|
|
275
|
+
const blocked = await fetch(`${base}/api/mcp/import`, {
|
|
276
|
+
method: 'POST',
|
|
277
|
+
headers: json,
|
|
278
|
+
body: JSON.stringify({ url: 'https://mcpservers.org/es/official' }),
|
|
279
|
+
});
|
|
280
|
+
assert.equal(blocked.status, 400);
|
|
281
|
+
} finally {
|
|
282
|
+
await instance.close();
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test('manages skills from the console', async () => {
|
|
287
|
+
const { instance, base } = await startServer();
|
|
288
|
+
const json = { 'Content-Type': 'application/json' };
|
|
289
|
+
const put = (id, body) => fetch(`${base}/api/skills/${id}`, { method: 'PUT', headers: json, body: JSON.stringify(body) });
|
|
290
|
+
try {
|
|
291
|
+
let res = await put('demo', { name: 'Demo', description: 'para pruebas', body: 'Hacé X.' });
|
|
292
|
+
assert.equal(res.status, 200);
|
|
293
|
+
let data = await res.json();
|
|
294
|
+
const demo = data.skills.find((s) => s.id === 'demo');
|
|
295
|
+
assert.equal(demo.enabled, true);
|
|
296
|
+
assert.equal(demo.removable, true);
|
|
297
|
+
|
|
298
|
+
const detail = await (await fetch(`${base}/api/skills/demo`)).json();
|
|
299
|
+
assert.match(detail.skill.body, /Hacé X/);
|
|
300
|
+
|
|
301
|
+
await fetch(`${base}/api/skills/demo/toggle`, { method: 'POST' });
|
|
302
|
+
data = await (await fetch(`${base}/api/skills`)).json();
|
|
303
|
+
assert.equal(data.skills.find((s) => s.id === 'demo').enabled, false);
|
|
304
|
+
|
|
305
|
+
res = await fetch(`${base}/api/skills/demo`, { method: 'DELETE' });
|
|
306
|
+
assert.equal(res.status, 200);
|
|
307
|
+
data = await (await fetch(`${base}/api/skills`)).json();
|
|
308
|
+
assert.equal(data.skills.length, 0);
|
|
309
|
+
|
|
310
|
+
res = await put('vacia', { body: ' ' });
|
|
311
|
+
assert.equal(res.status, 400);
|
|
312
|
+
} finally {
|
|
313
|
+
await instance.close();
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test('skills: indice lazy por defecto (sin body)', async () => {
|
|
318
|
+
let captured = '';
|
|
319
|
+
const captureProvider = {
|
|
320
|
+
id: 'capture',
|
|
321
|
+
async streamChat(_config, { messages }) {
|
|
322
|
+
captured = messages?.[0]?.content ?? '';
|
|
323
|
+
return { content: 'ok', reasoning: '', toolCalls: [] };
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
const { instance, base } = await startServer({ resolveProvider: () => ({ provider: captureProvider, config: {} }) });
|
|
327
|
+
const json = { 'Content-Type': 'application/json' };
|
|
328
|
+
const chat = (sessionId) =>
|
|
329
|
+
fetch(`${base}/api/chat`, { method: 'POST', headers: json, body: JSON.stringify({ sessionId, model: 'm', input: 'hola' }) });
|
|
330
|
+
const newSession = async () => {
|
|
331
|
+
const created = await (
|
|
332
|
+
await fetch(`${base}/api/sessions`, { method: 'POST', headers: json, body: JSON.stringify({ title: 't', model: 'm' }) })
|
|
333
|
+
).json();
|
|
334
|
+
return created.session.id;
|
|
335
|
+
};
|
|
336
|
+
try {
|
|
337
|
+
await fetch(`${base}/api/skills/demo`, {
|
|
338
|
+
method: 'PUT',
|
|
339
|
+
headers: json,
|
|
340
|
+
body: JSON.stringify({ name: 'Demo', body: 'Regla secreta de la skill.' }),
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// Lazy: se inyecta el indice (id), no el cuerpo.
|
|
344
|
+
await (await chat(await newSession())).text();
|
|
345
|
+
assert.match(captured, /demo/);
|
|
346
|
+
assert.doesNotMatch(captured, /Regla secreta de la skill/);
|
|
347
|
+
|
|
348
|
+
// Deshabilitada: no aparece ni en el indice.
|
|
349
|
+
await fetch(`${base}/api/skills/demo/toggle`, { method: 'POST' });
|
|
350
|
+
await (await chat(await newSession())).text();
|
|
351
|
+
assert.doesNotMatch(captured, /demo/);
|
|
352
|
+
} finally {
|
|
353
|
+
await instance.close();
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test('manages project memory from the console', async () => {
|
|
358
|
+
const cwd = await mkdtemp(path.join(tmpdir(), 'tamnora-mem-'));
|
|
359
|
+
const { instance, base } = await startServer({ cwd });
|
|
360
|
+
const json = { 'Content-Type': 'application/json' };
|
|
361
|
+
try {
|
|
362
|
+
let res = await fetch(`${base}/api/memory/arquitectura`, {
|
|
363
|
+
method: 'PUT',
|
|
364
|
+
headers: json,
|
|
365
|
+
body: JSON.stringify({ title: 'Arquitectura', body: 'Usa SQLite.' }),
|
|
366
|
+
});
|
|
367
|
+
assert.equal(res.status, 200);
|
|
368
|
+
let data = await res.json();
|
|
369
|
+
assert.equal(data.topics.length, 1);
|
|
370
|
+
assert.match(data.index, /arquitectura\.md/);
|
|
371
|
+
|
|
372
|
+
const detail = await (await fetch(`${base}/api/memory/arquitectura`)).json();
|
|
373
|
+
assert.match(detail.topic.body, /Usa SQLite/);
|
|
374
|
+
|
|
375
|
+
await fetch(`${base}/api/memory`, {
|
|
376
|
+
method: 'PUT',
|
|
377
|
+
headers: json,
|
|
378
|
+
body: JSON.stringify({ index: '# Memoria\n\n- propio' }),
|
|
379
|
+
});
|
|
380
|
+
data = await (await fetch(`${base}/api/memory`)).json();
|
|
381
|
+
assert.match(data.index, /propio/);
|
|
382
|
+
|
|
383
|
+
res = await fetch(`${base}/api/memory/arquitectura`, { method: 'DELETE' });
|
|
384
|
+
assert.equal(res.status, 200);
|
|
385
|
+
data = await res.json();
|
|
386
|
+
assert.equal(data.topics.length, 0);
|
|
387
|
+
} finally {
|
|
388
|
+
await instance.close();
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test('skills: modo eager inyecta el body en el system prompt', async () => {
|
|
393
|
+
let captured = '';
|
|
394
|
+
const captureProvider = {
|
|
395
|
+
id: 'capture',
|
|
396
|
+
async streamChat(_config, { messages }) {
|
|
397
|
+
captured = messages?.[0]?.content ?? '';
|
|
398
|
+
return { content: 'ok', reasoning: '', toolCalls: [] };
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
const cwd = await mkdtemp(path.join(tmpdir(), 'tamnora-srv-'));
|
|
402
|
+
await writeFile(path.join(cwd, 'tamnora.json'), JSON.stringify({ skills: { mode: 'eager' } }));
|
|
403
|
+
const { instance, base } = await startServer({ cwd, resolveProvider: () => ({ provider: captureProvider, config: {} }) });
|
|
404
|
+
const json = { 'Content-Type': 'application/json' };
|
|
405
|
+
try {
|
|
406
|
+
await fetch(`${base}/api/skills/demo`, {
|
|
407
|
+
method: 'PUT',
|
|
408
|
+
headers: json,
|
|
409
|
+
body: JSON.stringify({ name: 'Demo', body: 'Regla secreta de la skill.' }),
|
|
410
|
+
});
|
|
411
|
+
const created = await (
|
|
412
|
+
await fetch(`${base}/api/sessions`, { method: 'POST', headers: json, body: JSON.stringify({ title: 't', model: 'm' }) })
|
|
413
|
+
).json();
|
|
414
|
+
await fetch(`${base}/api/chat`, {
|
|
415
|
+
method: 'POST',
|
|
416
|
+
headers: json,
|
|
417
|
+
body: JSON.stringify({ sessionId: created.session.id, model: 'm', input: 'hola' }),
|
|
418
|
+
}).then((r) => r.text());
|
|
419
|
+
assert.match(captured, /Regla secreta de la skill/);
|
|
420
|
+
} finally {
|
|
421
|
+
await instance.close();
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test('manages providers and active settings', async () => {
|
|
426
|
+
const { instance, base } = await startServer();
|
|
427
|
+
const json = { 'Content-Type': 'application/json' };
|
|
428
|
+
try {
|
|
429
|
+
const put = await fetch(`${base}/api/providers/deepseek`, {
|
|
430
|
+
method: 'PUT',
|
|
431
|
+
headers: json,
|
|
432
|
+
body: JSON.stringify({
|
|
433
|
+
label: 'DeepSeek',
|
|
434
|
+
baseUrl: 'https://api.deepseek.com/v1',
|
|
435
|
+
apiKey: 'sk-secret-1234',
|
|
436
|
+
models: ['deepseek-chat'],
|
|
437
|
+
}),
|
|
438
|
+
});
|
|
439
|
+
const data = await put.json();
|
|
440
|
+
const ds = data.providers.find((p) => p.id === 'deepseek');
|
|
441
|
+
assert.equal(ds.hasKey, true);
|
|
442
|
+
assert.equal(ds.apiKeyHint.endsWith('1234'), true);
|
|
443
|
+
assert.deepEqual(ds.models, ['deepseek-chat']);
|
|
444
|
+
|
|
445
|
+
await fetch(`${base}/api/settings`, {
|
|
446
|
+
method: 'PUT',
|
|
447
|
+
headers: json,
|
|
448
|
+
body: JSON.stringify({ provider: 'deepseek', model: 'deepseek-chat' }),
|
|
449
|
+
});
|
|
450
|
+
const settings = await (await fetch(`${base}/api/settings`)).json();
|
|
451
|
+
assert.equal(settings.settings.provider, 'deepseek');
|
|
452
|
+
assert.equal(settings.settings.model, 'deepseek-chat');
|
|
453
|
+
|
|
454
|
+
const list = await (await fetch(`${base}/api/providers`)).json();
|
|
455
|
+
assert.equal(list.active.provider, 'deepseek');
|
|
456
|
+
|
|
457
|
+
const deleted = await fetch(`${base}/api/providers/deepseek`, { method: 'DELETE' });
|
|
458
|
+
assert.equal(deleted.status, 200);
|
|
459
|
+
const after = await (await fetch(`${base}/api/providers`)).json();
|
|
460
|
+
assert.equal(after.providers.find((p) => p.id === 'deepseek').hasKey, false);
|
|
461
|
+
} finally {
|
|
462
|
+
await instance.close();
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"enabled": true,
|
|
54
54
|
"allow": ["npm", "node", "npx", "pnpm", "yarn", "bun", "php", "python", "composer"]
|
|
55
55
|
},
|
|
56
|
-
"preview": { "provider": "tailscale", "tailscalePort": 8443, "fallback":
|
|
56
|
+
"preview": { "provider": "tailscale", "tailscalePort": 8443, "fallback": false },
|
|
57
57
|
"telegram": { "token": "", "allowedChatIds": [], "transcribeModel": "whisper-1" },
|
|
58
58
|
"agents": { "maxRounds": 6 }
|
|
59
59
|
}
|
package/package.json
CHANGED