@danieltmn/tamnora 0.12.0 → 0.13.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.
@@ -1056,6 +1056,167 @@ test('skills: indice lazy por defecto (sin body)', async () => {
1056
1056
  }
1057
1057
  });
1058
1058
 
1059
+ test('manages project memory from the console', async () => {
1060
+ const cwd = await mkdtemp(path.join(tmpdir(), 'tamnora-mem-'));
1061
+ const { instance, base } = await startServer({ cwd });
1062
+ const json = { 'Content-Type': 'application/json' };
1063
+ try {
1064
+ let res = await fetch(`${base}/api/memory/arquitectura`, {
1065
+ method: 'PUT',
1066
+ headers: json,
1067
+ body: JSON.stringify({ title: 'Arquitectura', body: 'Usa SQLite.' }),
1068
+ });
1069
+ assert.equal(res.status, 200);
1070
+ let data = await res.json();
1071
+ assert.equal(data.topics.length, 1);
1072
+ assert.match(data.index, /arquitectura\.md/);
1073
+
1074
+ const detail = await (await fetch(`${base}/api/memory/arquitectura`)).json();
1075
+ assert.match(detail.topic.body, /Usa SQLite/);
1076
+
1077
+ await fetch(`${base}/api/memory`, {
1078
+ method: 'PUT',
1079
+ headers: json,
1080
+ body: JSON.stringify({ index: '# Memoria\n\n- propio' }),
1081
+ });
1082
+ data = await (await fetch(`${base}/api/memory`)).json();
1083
+ assert.match(data.index, /propio/);
1084
+
1085
+ res = await fetch(`${base}/api/memory/arquitectura`, { method: 'DELETE' });
1086
+ assert.equal(res.status, 200);
1087
+ data = await res.json();
1088
+ assert.equal(data.topics.length, 0);
1089
+ } finally {
1090
+ await instance.close();
1091
+ }
1092
+ });
1093
+
1094
+ test('project memory is injected into the system prompt', async () => {
1095
+ const cwd = await mkdtemp(path.join(tmpdir(), 'tamnora-mem-prompt-'));
1096
+ await mkdir(path.join(cwd, '.tamnora', 'memory'), { recursive: true });
1097
+ await writeFile(path.join(cwd, '.tamnora', 'memory', 'MEMORY.md'), '# Memoria\n\n- [Dato](dato.md) — clave\n');
1098
+ let captured = '';
1099
+ const provider = {
1100
+ id: 'capture',
1101
+ async streamChat(_config, { messages }) {
1102
+ captured = messages?.[0]?.content ?? '';
1103
+ return { content: 'ok', reasoning: '', toolCalls: [] };
1104
+ },
1105
+ };
1106
+ const { instance, base } = await startServer({ cwd, resolveProvider: () => ({ provider, config: {} }) });
1107
+ const json = { 'Content-Type': 'application/json' };
1108
+ try {
1109
+ const created = await (
1110
+ await fetch(`${base}/api/sessions`, { method: 'POST', headers: json, body: JSON.stringify({ title: 't', model: 'm', cwd }) })
1111
+ ).json();
1112
+ await (
1113
+ await fetch(`${base}/api/chat`, {
1114
+ method: 'POST',
1115
+ headers: json,
1116
+ body: JSON.stringify({ sessionId: created.session.id, model: 'm', input: 'hola' }),
1117
+ })
1118
+ ).text();
1119
+ assert.match(captured, /Memoria del proyecto/);
1120
+ assert.match(captured, /dato\.md/);
1121
+ } finally {
1122
+ await instance.close();
1123
+ }
1124
+ });
1125
+
1126
+ test('the memory tool writes project memory', async () => {
1127
+ const cwd = await mkdtemp(path.join(tmpdir(), 'tamnora-mem-tool-'));
1128
+ await writeFile(path.join(cwd, 'tamnora.json'), JSON.stringify({ permissions: { memory: 'allow' } }));
1129
+ const provider = {
1130
+ id: 'memtool',
1131
+ async streamChat(_config, { messages }) {
1132
+ if (messages.some((m) => m.role === 'tool' && m.name === 'memory')) {
1133
+ return { content: 'listo', reasoning: '', toolCalls: [] };
1134
+ }
1135
+ return {
1136
+ content: '',
1137
+ reasoning: '',
1138
+ toolCalls: [
1139
+ {
1140
+ id: '1',
1141
+ type: 'function',
1142
+ function: { name: 'memory', arguments: JSON.stringify({ action: 'write', topic: 'nota', content: 'Hola memoria.' }) },
1143
+ },
1144
+ ],
1145
+ };
1146
+ },
1147
+ };
1148
+ const { instance, base } = await startServer({ cwd, resolveProvider: () => ({ provider, config: {} }) });
1149
+ const json = { 'Content-Type': 'application/json' };
1150
+ try {
1151
+ const created = await (
1152
+ await fetch(`${base}/api/sessions`, { method: 'POST', headers: json, body: JSON.stringify({ title: 't', model: 'm', cwd }) })
1153
+ ).json();
1154
+ await (
1155
+ await fetch(`${base}/api/chat`, {
1156
+ method: 'POST',
1157
+ headers: json,
1158
+ body: JSON.stringify({ sessionId: created.session.id, model: 'm', input: 'recordá esto' }),
1159
+ })
1160
+ ).text();
1161
+ const file = await readFile(path.join(cwd, '.tamnora', 'memory', 'nota.md'), 'utf8');
1162
+ assert.match(file, /Hola memoria/);
1163
+ } finally {
1164
+ await instance.close();
1165
+ }
1166
+ });
1167
+
1168
+ test('rules by path are injected (transiently) when a tool touches a matching file', async () => {
1169
+ const cwd = await mkdtemp(path.join(tmpdir(), 'tamnora-rules-inject-'));
1170
+ await mkdir(path.join(cwd, '.tamnora', 'rules'), { recursive: true });
1171
+ await writeFile(path.join(cwd, '.tamnora', 'rules', 'estilo.md'), '---\npaths: src/**\n---\nMARCADOR_ESTILO: usá 2 espacios.');
1172
+ await mkdir(path.join(cwd, 'src'), { recursive: true });
1173
+ await writeFile(path.join(cwd, 'src', 'index.js'), 'export const x = 1;\n');
1174
+ let captured = null;
1175
+ const provider = {
1176
+ id: 'rules',
1177
+ async streamChat(_config, { messages }) {
1178
+ if (messages.some((m) => m.role === 'tool' && m.name === 'read')) {
1179
+ captured = messages;
1180
+ return { content: 'ok', reasoning: '', toolCalls: [] };
1181
+ }
1182
+ return {
1183
+ content: '',
1184
+ reasoning: '',
1185
+ toolCalls: [
1186
+ { id: 'r1', type: 'function', function: { name: 'read', arguments: JSON.stringify({ path: 'src/index.js' }) } },
1187
+ ],
1188
+ };
1189
+ },
1190
+ };
1191
+ const { instance, base } = await startServer({ cwd, resolveProvider: () => ({ provider, config: {} }) });
1192
+ const json = { 'Content-Type': 'application/json' };
1193
+ try {
1194
+ const created = await (
1195
+ await fetch(`${base}/api/sessions`, { method: 'POST', headers: json, body: JSON.stringify({ title: 't', model: 'm', cwd }) })
1196
+ ).json();
1197
+ await (
1198
+ await fetch(`${base}/api/chat`, {
1199
+ method: 'POST',
1200
+ headers: json,
1201
+ body: JSON.stringify({ sessionId: created.session.id, model: 'm', input: 'mirá src/index.js' }),
1202
+ })
1203
+ ).text();
1204
+ assert.ok(captured, 'el provider no recibió una segunda ronda');
1205
+ assert.ok(
1206
+ captured.some((m) => m.role === 'system' && /MARCADOR_ESTILO/.test(m.content)),
1207
+ 'no se inyectó la regla por path'
1208
+ );
1209
+ // No se persiste: el historial guardado no incluye la regla efímera.
1210
+ const session = await (await fetch(`${base}/api/sessions/${created.session.id}`)).json();
1211
+ assert.ok(
1212
+ session.messages.every((m) => !(m.role === 'system' && /MARCADOR_ESTILO/.test(String(m.content ?? '')))),
1213
+ 'la regla efímera se persistió en el historial'
1214
+ );
1215
+ } finally {
1216
+ await instance.close();
1217
+ }
1218
+ });
1219
+
1059
1220
  test('skills: modo eager inyecta el body en el system prompt', async () => {
1060
1221
  let captured = '';
1061
1222
  const captureProvider = {
@@ -5,6 +5,12 @@
5
5
  "maxRounds": 0,
6
6
  "maxTokens": 0,
7
7
  "maxDurationMs": 0,
8
+ "maxOutputTokens": 0,
9
+ "promptCaching": false,
10
+ "skills": { "mode": "lazy" },
11
+ "compact": { "enabled": true, "threshold": 0.75, "keepLast": 8 },
12
+ "rules": [".tamnora/rules/*.md"],
13
+ "memory": { "enabled": true },
8
14
  "tools": [],
9
15
  "permissions": {},
10
16
  "mcp": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danieltmn/tamnora",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Harness local para IAs: agente CLI-first con TUI, consola web y evaluacion. Node puro, sin dependencias.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.25.0",