@danieltmn/tamnora 0.17.0 → 0.18.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 +38 -0
- package/README.md +37 -3
- package/SECURITY.md +8 -0
- package/harness/packages/cli/src/commands/mcp.js +31 -4
- package/harness/packages/cli/src/commands/repl.js +7 -2
- package/harness/packages/cli/src/commands/run.js +5 -2
- package/harness/packages/cli/src/commands/tui.js +9 -2
- package/harness/packages/cli/src/lib/runtime.js +23 -1
- package/harness/packages/cli/test/tunnel.test.js +13 -1
- package/harness/packages/core/src/agent/modes.js +3 -0
- package/harness/packages/core/src/config/index.js +3 -1
- package/harness/packages/core/src/index.js +15 -1
- package/harness/packages/core/src/mcp/catalog.js +79 -0
- package/harness/packages/core/src/mcp/index.js +14 -2
- package/harness/packages/core/src/mcp/registry.js +29 -1
- package/harness/packages/core/src/redact.js +25 -0
- package/harness/packages/core/src/skills/catalog.js +78 -0
- package/harness/packages/core/src/skills/remote.js +51 -0
- package/harness/packages/core/src/tools/executors.js +93 -2
- package/harness/packages/core/src/tools/index.js +27 -6
- package/harness/packages/core/src/tools/labels.js +2 -0
- package/harness/packages/core/src/tools/mcp.js +91 -0
- package/harness/packages/core/src/tools/permissions.js +53 -8
- package/harness/packages/core/src/tools/registry.js +14 -7
- package/harness/packages/core/src/tools/schema.js +86 -0
- package/harness/packages/core/src/tts/edge.js +28 -7
- package/harness/packages/core/src/tunnel.js +51 -5
- package/harness/packages/core/test/compact.test.js +2 -0
- package/harness/packages/core/test/labels.test.js +1 -0
- package/harness/packages/core/test/mcp-registry.test.js +37 -1
- package/harness/packages/core/test/mcp-routing.test.js +115 -0
- package/harness/packages/core/test/redact.test.js +22 -0
- package/harness/packages/core/test/skills.test.js +11 -0
- package/harness/packages/core/test/tools.test.js +97 -3
- package/harness/packages/server/public/app.js +501 -23
- package/harness/packages/server/public/index.html +41 -2
- package/harness/packages/server/public/styles.css +99 -3
- package/harness/packages/server/src/lib/agentTools.js +45 -3
- package/harness/packages/server/src/lib/channels/telegram.js +2 -0
- package/harness/packages/server/src/lib/prompt.js +4 -1
- package/harness/packages/server/src/lib/questions.js +72 -0
- package/harness/packages/server/src/lib/scheduler.js +12 -1
- package/harness/packages/server/src/lib/tasks.js +3 -1
- package/harness/packages/server/src/routes/chat.js +31 -2
- package/harness/packages/server/src/routes/mcp.js +54 -2
- package/harness/packages/server/src/routes/preview.js +49 -28
- package/harness/packages/server/src/routes/providers.js +12 -0
- package/harness/packages/server/src/routes/sessions.js +3 -1
- package/harness/packages/server/src/routes/skills.js +48 -1
- package/harness/packages/server/src/routes/state.js +8 -1
- package/harness/packages/server/src/server.js +37 -1
- package/harness/packages/server/test/preview.test.js +111 -0
- package/harness/packages/server/test/server.test.js +186 -19
- package/harness/packages/server/test/telegram-features.test.js +2 -0
- package/harness/packages/server/test/telegram-voz.test.js +2 -0
- package/harness/packages/server/test/telegram.test.js +2 -0
- package/harness/tamnora.example.json +3 -1
- package/package.json +1 -1
|
@@ -943,13 +943,74 @@ test('manages MCP servers from the console', async () => {
|
|
|
943
943
|
});
|
|
944
944
|
assert.equal(bad.status, 400);
|
|
945
945
|
|
|
946
|
-
res = await fetch(`${base}/api/mcp/servers/echo`, { method: 'DELETE' });
|
|
947
|
-
assert.equal(res.status, 200);
|
|
948
|
-
data = await res.json();
|
|
949
|
-
assert.equal(data.servers.length, 0);
|
|
950
|
-
} finally {
|
|
951
|
-
await instance.close();
|
|
952
|
-
}
|
|
946
|
+
res = await fetch(`${base}/api/mcp/servers/echo`, { method: 'DELETE' });
|
|
947
|
+
assert.equal(res.status, 200);
|
|
948
|
+
data = await res.json();
|
|
949
|
+
assert.equal(data.servers.length, 0);
|
|
950
|
+
} finally {
|
|
951
|
+
await instance.close();
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
test('sirve el catálogo de MCP/skills y los instala deshabilitados', async () => {
|
|
956
|
+
const { instance, base } = await startServer();
|
|
957
|
+
const json = { 'Content-Type': 'application/json' };
|
|
958
|
+
try {
|
|
959
|
+
const catalog = await (await fetch(`${base}/api/mcp/catalog`)).json();
|
|
960
|
+
assert.ok(catalog.catalog.some((e) => e.id === 'playwright' && e.installed === false));
|
|
961
|
+
|
|
962
|
+
const add = await fetch(`${base}/api/mcp/import`, {
|
|
963
|
+
method: 'POST',
|
|
964
|
+
headers: json,
|
|
965
|
+
body: JSON.stringify({ catalog: 'playwright' }),
|
|
966
|
+
});
|
|
967
|
+
assert.equal(add.status, 200);
|
|
968
|
+
const added = await add.json();
|
|
969
|
+
assert.equal(added.imported, 'playwright');
|
|
970
|
+
assert.equal(added.servers.find((s) => s.id === 'playwright').enabled, false);
|
|
971
|
+
|
|
972
|
+
const again = await (await fetch(`${base}/api/mcp/catalog`)).json();
|
|
973
|
+
assert.equal(again.catalog.find((e) => e.id === 'playwright').installed, true);
|
|
974
|
+
|
|
975
|
+
const skills = await (await fetch(`${base}/api/skills/catalog`)).json();
|
|
976
|
+
assert.ok(skills.catalog.some((e) => e.id === 'code-review' && e.installed === false));
|
|
977
|
+
const skillAdd = await fetch(`${base}/api/skills`, {
|
|
978
|
+
method: 'POST',
|
|
979
|
+
headers: json,
|
|
980
|
+
body: JSON.stringify({ id: 'code-review' }),
|
|
981
|
+
});
|
|
982
|
+
assert.equal(skillAdd.status, 200);
|
|
983
|
+
const skillData = await skillAdd.json();
|
|
984
|
+
assert.equal(skillData.installed, 'code-review');
|
|
985
|
+
assert.equal(skillData.skills.find((s) => s.id === 'code-review').enabled, false);
|
|
986
|
+
|
|
987
|
+
const unknown = await fetch(`${base}/api/mcp/import`, {
|
|
988
|
+
method: 'POST',
|
|
989
|
+
headers: json,
|
|
990
|
+
body: JSON.stringify({ catalog: 'nope' }),
|
|
991
|
+
});
|
|
992
|
+
assert.equal(unknown.status, 404);
|
|
993
|
+
const unknownSkill = await fetch(`${base}/api/skills`, {
|
|
994
|
+
method: 'POST',
|
|
995
|
+
headers: json,
|
|
996
|
+
body: JSON.stringify({ id: 'nope' }),
|
|
997
|
+
});
|
|
998
|
+
assert.equal(unknownSkill.status, 404);
|
|
999
|
+
} finally {
|
|
1000
|
+
await instance.close();
|
|
1001
|
+
}
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
test('los buscadores devuelven vacío sin query', async () => {
|
|
1005
|
+
const { instance, base } = await startServer();
|
|
1006
|
+
try {
|
|
1007
|
+
const mcp = await (await fetch(`${base}/api/mcp/search`)).json();
|
|
1008
|
+
assert.deepEqual(mcp.results, []);
|
|
1009
|
+
const skills = await (await fetch(`${base}/api/skills/search`)).json();
|
|
1010
|
+
assert.deepEqual(skills.results, []);
|
|
1011
|
+
} finally {
|
|
1012
|
+
await instance.close();
|
|
1013
|
+
}
|
|
953
1014
|
});
|
|
954
1015
|
|
|
955
1016
|
test('MCP: el env se enmascara y no se pisa al guardar sin cambios', async () => {
|
|
@@ -1015,6 +1076,36 @@ test('guarda los límites del agente desde Ajustes', async () => {
|
|
|
1015
1076
|
}
|
|
1016
1077
|
});
|
|
1017
1078
|
|
|
1079
|
+
test('guarda el modo de carga y las tools fijas de MCP', async () => {
|
|
1080
|
+
const { instance, base } = await startServer();
|
|
1081
|
+
const json = { 'Content-Type': 'application/json' };
|
|
1082
|
+
try {
|
|
1083
|
+
const res = await fetch(`${base}/api/settings`, {
|
|
1084
|
+
method: 'PUT',
|
|
1085
|
+
headers: json,
|
|
1086
|
+
body: JSON.stringify({ mcpMode: 'lazy', mcpPinned: 'mcp__postgres__query, postgres' }),
|
|
1087
|
+
});
|
|
1088
|
+
const data = await res.json();
|
|
1089
|
+
assert.equal(data.settings.mcpMode, 'lazy');
|
|
1090
|
+
assert.deepEqual(data.settings.mcpPinned, ['mcp__postgres__query', 'postgres']);
|
|
1091
|
+
|
|
1092
|
+
const mcp = await (await fetch(`${base}/api/mcp`)).json();
|
|
1093
|
+
assert.equal(mcp.mode, 'lazy');
|
|
1094
|
+
assert.deepEqual(mcp.pinned, ['mcp__postgres__query', 'postgres']);
|
|
1095
|
+
assert.equal(mcp.threshold, 12);
|
|
1096
|
+
assert.equal(mcp.toolCount, null);
|
|
1097
|
+
|
|
1098
|
+
const bad = await fetch(`${base}/api/settings`, {
|
|
1099
|
+
method: 'PUT',
|
|
1100
|
+
headers: json,
|
|
1101
|
+
body: JSON.stringify({ mcpMode: 'nope' }),
|
|
1102
|
+
});
|
|
1103
|
+
assert.equal((await bad.json()).settings.mcpMode, 'auto');
|
|
1104
|
+
} finally {
|
|
1105
|
+
await instance.close();
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1018
1109
|
test('imports an MCP server from a JSON snippet', async () => {
|
|
1019
1110
|
const { instance, base } = await startServer();
|
|
1020
1111
|
const json = { 'Content-Type': 'application/json' };
|
|
@@ -2066,18 +2157,89 @@ test('asks for approval before running a gated tool and runs it once approved',
|
|
|
2066
2157
|
}
|
|
2067
2158
|
});
|
|
2068
2159
|
|
|
2069
|
-
test('rejects unknown permission requests', async () => {
|
|
2070
|
-
const { instance, base } = await startServer();
|
|
2071
|
-
try {
|
|
2072
|
-
const res = await fetch(`${base}/api/permissions/does-not-exist`, {
|
|
2073
|
-
method: 'POST',
|
|
2074
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2075
|
-
body: JSON.stringify({ allow: true }),
|
|
2076
|
-
});
|
|
2077
|
-
assert.equal(res.status, 404);
|
|
2078
|
-
} finally {
|
|
2079
|
-
await instance.close();
|
|
2080
|
-
}
|
|
2160
|
+
test('rejects unknown permission requests', async () => {
|
|
2161
|
+
const { instance, base } = await startServer();
|
|
2162
|
+
try {
|
|
2163
|
+
const res = await fetch(`${base}/api/permissions/does-not-exist`, {
|
|
2164
|
+
method: 'POST',
|
|
2165
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2166
|
+
body: JSON.stringify({ allow: true }),
|
|
2167
|
+
});
|
|
2168
|
+
assert.equal(res.status, 404);
|
|
2169
|
+
} finally {
|
|
2170
|
+
await instance.close();
|
|
2171
|
+
}
|
|
2172
|
+
});
|
|
2173
|
+
|
|
2174
|
+
test('ask_user muestra un formulario y recibe las respuestas', async () => {
|
|
2175
|
+
const cwd = await mkdtemp(path.join(tmpdir(), 'tamnora-askuser-'));
|
|
2176
|
+
let calls = 0;
|
|
2177
|
+
let captured = null;
|
|
2178
|
+
const provider = {
|
|
2179
|
+
id: 'formy',
|
|
2180
|
+
async streamChat(_config, opts) {
|
|
2181
|
+
calls += 1;
|
|
2182
|
+
if (calls === 1) {
|
|
2183
|
+
return {
|
|
2184
|
+
content: '',
|
|
2185
|
+
reasoning: '',
|
|
2186
|
+
toolCalls: [
|
|
2187
|
+
{
|
|
2188
|
+
id: 'q1',
|
|
2189
|
+
function: {
|
|
2190
|
+
name: 'ask_user',
|
|
2191
|
+
arguments: JSON.stringify({
|
|
2192
|
+
questions: [
|
|
2193
|
+
{ id: 'nombre', label: '¿Tu nombre?' },
|
|
2194
|
+
{ id: 'color', label: 'Color', type: 'select', options: ['rojo', 'verde'] },
|
|
2195
|
+
],
|
|
2196
|
+
}),
|
|
2197
|
+
},
|
|
2198
|
+
},
|
|
2199
|
+
],
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2202
|
+
captured = opts.messages;
|
|
2203
|
+
return { content: 'Gracias', reasoning: '', toolCalls: [] };
|
|
2204
|
+
},
|
|
2205
|
+
};
|
|
2206
|
+
const { instance, base } = await startServer({ cwd, resolveProvider: () => ({ provider, config: {} }) });
|
|
2207
|
+
try {
|
|
2208
|
+
const res = await fetch(`${base}/api/chat`, {
|
|
2209
|
+
method: 'POST',
|
|
2210
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2211
|
+
body: JSON.stringify({ input: 'preguntame', model: 'm' }),
|
|
2212
|
+
});
|
|
2213
|
+
const reader = res.body.getReader();
|
|
2214
|
+
const decoder = new TextDecoder();
|
|
2215
|
+
let buffer = '';
|
|
2216
|
+
let askId = null;
|
|
2217
|
+
let done = false;
|
|
2218
|
+
while (!done) {
|
|
2219
|
+
const { value } = await reader.read();
|
|
2220
|
+
if (!value) break;
|
|
2221
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2222
|
+
if (!askId && buffer.includes('event: question')) {
|
|
2223
|
+
const match = buffer.match(/"askId":"([0-9a-f-]+)"/);
|
|
2224
|
+
if (match) {
|
|
2225
|
+
askId = match[1];
|
|
2226
|
+
const answer = await fetch(`${base}/api/questions/${askId}`, {
|
|
2227
|
+
method: 'POST',
|
|
2228
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2229
|
+
body: JSON.stringify({ answers: { nombre: 'Ana', color: 'verde' } }),
|
|
2230
|
+
});
|
|
2231
|
+
assert.equal(answer.status, 200);
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
if (buffer.includes('event: done')) done = true;
|
|
2235
|
+
}
|
|
2236
|
+
assert.ok(askId, 'emitió el evento question');
|
|
2237
|
+
const toolMsg = (captured ?? []).find((m) => m.role === 'tool');
|
|
2238
|
+
assert.match(String(toolMsg?.content ?? ''), /Ana/);
|
|
2239
|
+
assert.match(String(toolMsg?.content ?? ''), /verde/);
|
|
2240
|
+
} finally {
|
|
2241
|
+
await instance.close();
|
|
2242
|
+
}
|
|
2081
2243
|
});
|
|
2082
2244
|
|
|
2083
2245
|
async function readEvents(res, onFrame) {
|
|
@@ -2321,6 +2483,7 @@ test('plan mode exposes only read-only tools and adds guidance', async () => {
|
|
|
2321
2483
|
})
|
|
2322
2484
|
).text();
|
|
2323
2485
|
assert.deepEqual(captured.tools, [
|
|
2486
|
+
'ask_user',
|
|
2324
2487
|
'console_errors',
|
|
2325
2488
|
'fetch_url',
|
|
2326
2489
|
'git_diff',
|
|
@@ -2331,9 +2494,13 @@ test('plan mode exposes only read-only tools and adds guidance', async () => {
|
|
|
2331
2494
|
'list',
|
|
2332
2495
|
'list_mcp',
|
|
2333
2496
|
'list_skills',
|
|
2497
|
+
'offer_choices',
|
|
2334
2498
|
'preview_url',
|
|
2335
2499
|
'read',
|
|
2336
2500
|
'screenshot',
|
|
2501
|
+
'search_mcp',
|
|
2502
|
+
'search_skills',
|
|
2503
|
+
'show_plan',
|
|
2337
2504
|
'skill',
|
|
2338
2505
|
'speak',
|
|
2339
2506
|
'task',
|
|
@@ -79,6 +79,8 @@ function makeCtx(cwd, { app = {}, presence } = {}) {
|
|
|
79
79
|
effectiveProviderId: () => 'fake',
|
|
80
80
|
effectiveModel: () => 'm',
|
|
81
81
|
effectiveMode: () => 'agent',
|
|
82
|
+
effectiveMcpMode: () => 'auto',
|
|
83
|
+
effectiveMcpPinned: () => [],
|
|
82
84
|
providerSettings: () => ({ baseUrl: 'http://127.0.0.1:1', apiKey: '' }),
|
|
83
85
|
apiKeyFor: () => '',
|
|
84
86
|
customProviderDefs: () => [],
|
|
@@ -81,6 +81,8 @@ function makeCtx(cwd, app = {}) {
|
|
|
81
81
|
effectiveProviderId: () => 'fake',
|
|
82
82
|
effectiveModel: () => 'm',
|
|
83
83
|
effectiveMode: () => 'agent',
|
|
84
|
+
effectiveMcpMode: () => 'auto',
|
|
85
|
+
effectiveMcpPinned: () => [],
|
|
84
86
|
providerSettings: () => ({ baseUrl: 'http://127.0.0.1:1', apiKey: '' }),
|
|
85
87
|
apiKeyFor: () => '',
|
|
86
88
|
customProviderDefs: () => [],
|
|
@@ -48,6 +48,8 @@ function makeCtx(cwd) {
|
|
|
48
48
|
effectiveProviderId: () => 'fake',
|
|
49
49
|
effectiveModel: () => 'm',
|
|
50
50
|
effectiveMode: () => 'agent',
|
|
51
|
+
effectiveMcpMode: () => 'auto',
|
|
52
|
+
effectiveMcpPinned: () => [],
|
|
51
53
|
providerSettings: () => ({ baseUrl: 'http://127.0.0.1:1', apiKey: '' }),
|
|
52
54
|
apiKeyFor: () => '',
|
|
53
55
|
customProviderDefs: () => [],
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
"plugins": [],
|
|
16
16
|
"tools": [],
|
|
17
17
|
"permissions": {},
|
|
18
|
+
"mcpMode": "auto",
|
|
19
|
+
"mcpPinned": [],
|
|
18
20
|
"mcp": [
|
|
19
21
|
{
|
|
20
22
|
"id": "echo",
|
|
@@ -51,7 +53,7 @@
|
|
|
51
53
|
"enabled": true,
|
|
52
54
|
"allow": ["npm", "node", "npx", "pnpm", "yarn", "bun", "php", "python", "composer"]
|
|
53
55
|
},
|
|
54
|
-
"preview": { "provider": "
|
|
56
|
+
"preview": { "provider": "tailscale", "tailscalePort": 8443, "fallback": true },
|
|
55
57
|
"telegram": { "token": "", "allowedChatIds": [], "transcribeModel": "whisper-1" },
|
|
56
58
|
"agents": { "maxRounds": 6 }
|
|
57
59
|
}
|
package/package.json
CHANGED