@danieltmn/tamnora 0.9.3 → 0.9.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/CHANGELOG.md +30 -0
- package/README.md +51 -16
- package/harness/packages/cli/src/commands/doctor.js +14 -0
- package/harness/packages/cli/src/commands/update.js +91 -21
- package/harness/packages/core/src/agent/modes.js +14 -1
- package/harness/packages/core/src/agent/runAgent.js +23 -2
- package/harness/packages/core/src/browser/index.js +289 -0
- package/harness/packages/core/src/config/home.js +5 -0
- package/harness/packages/core/src/index.js +9 -2
- package/harness/packages/core/src/tools/executors.js +96 -0
- package/harness/packages/core/src/tools/labels.js +4 -0
- package/harness/packages/core/src/tools/permissions.js +4 -0
- package/harness/packages/core/src/tools/schema.js +47 -0
- package/harness/packages/core/src/version.js +81 -7
- package/harness/packages/core/test/agent.test.js +30 -1
- package/harness/packages/core/test/browser.test.js +18 -0
- package/harness/packages/core/test/tools.test.js +16 -1
- package/harness/packages/core/test/version.test.js +34 -0
- package/harness/packages/server/public/app.js +244 -8
- package/harness/packages/server/public/index.html +57 -3
- package/harness/packages/server/public/landing/index.html +14 -1
- package/harness/packages/server/public/markdown.js +54 -0
- package/harness/packages/server/public/reconcile.js +3 -1
- package/harness/packages/server/public/styles.css +48 -0
- package/harness/packages/server/src/lib/advisor.js +177 -0
- package/harness/packages/server/src/lib/agentRun.js +4 -3
- package/harness/packages/server/src/lib/channels/telegram.js +130 -25
- package/harness/packages/server/src/lib/ttsSettings.js +2 -2
- package/harness/packages/server/src/lib/update.js +82 -0
- package/harness/packages/server/src/routes/advisor.js +76 -0
- package/harness/packages/server/src/routes/chat.js +30 -0
- package/harness/packages/server/src/routes/files.js +25 -1
- package/harness/packages/server/src/routes/tts.js +1 -1
- package/harness/packages/server/src/routes/update.js +43 -0
- package/harness/packages/server/src/server.js +26 -2
- package/harness/packages/server/test/advisor.test.js +205 -0
- package/harness/packages/server/test/markdown.test.js +43 -1
- package/harness/packages/server/test/server.test.js +50 -14
- package/harness/packages/server/test/telegram-features.test.js +25 -4
- package/harness/packages/server/test/update.test.js +94 -0
- package/harness/scripts/self-update.mjs +123 -0
- package/package.json +1 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtemp } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { createTamnoraServer } from '../src/index.js';
|
|
7
|
+
import { setHome } from '../../core/src/index.js';
|
|
8
|
+
import { clearUpdateCache } from '../src/lib/update.js';
|
|
9
|
+
|
|
10
|
+
async function startServer() {
|
|
11
|
+
const cwd = await mkdtemp(path.join(tmpdir(), 'tamnora-update-'));
|
|
12
|
+
setHome(cwd);
|
|
13
|
+
const instance = await createTamnoraServer({
|
|
14
|
+
cwd,
|
|
15
|
+
dbPath: path.join(cwd, 'tamnora.db'),
|
|
16
|
+
loadMcp: false,
|
|
17
|
+
resolveProvider: () => ({ provider: { id: 'fake' }, config: {} }),
|
|
18
|
+
});
|
|
19
|
+
const info = await instance.listen({ port: 0 });
|
|
20
|
+
return { instance, base: info.url };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** `fetch` que solo intercepta el registro npm; el resto va al server real. */
|
|
24
|
+
function fakeRegistry(version, original) {
|
|
25
|
+
return async (url, opts) => {
|
|
26
|
+
if (String(url).includes('registry.npmjs.org')) {
|
|
27
|
+
return { ok: true, json: async () => ({ 'dist-tags': { latest: version } }) };
|
|
28
|
+
}
|
|
29
|
+
return original(url, opts);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
test('GET /api/update informa cuando hay version nueva', async () => {
|
|
34
|
+
clearUpdateCache();
|
|
35
|
+
const { instance, base } = await startServer();
|
|
36
|
+
const original = global.fetch;
|
|
37
|
+
global.fetch = fakeRegistry('99.0.0', original);
|
|
38
|
+
try {
|
|
39
|
+
const data = await (await fetch(`${base}/api/update`)).json();
|
|
40
|
+
assert.equal(data.latest, '99.0.0');
|
|
41
|
+
assert.equal(data.updateAvailable, true);
|
|
42
|
+
assert.match(data.current, /^\d+\.\d+\.\d+/);
|
|
43
|
+
} finally {
|
|
44
|
+
global.fetch = original;
|
|
45
|
+
await instance.close();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('GET /api/update no rompe si no hay red', async () => {
|
|
50
|
+
clearUpdateCache();
|
|
51
|
+
const { instance, base } = await startServer();
|
|
52
|
+
const original = global.fetch;
|
|
53
|
+
global.fetch = async (url, opts) => {
|
|
54
|
+
if (String(url).includes('registry.npmjs.org')) throw new Error('offline');
|
|
55
|
+
return original(url, opts);
|
|
56
|
+
};
|
|
57
|
+
try {
|
|
58
|
+
const data = await (await fetch(`${base}/api/update`)).json();
|
|
59
|
+
assert.equal(data.updateAvailable, false);
|
|
60
|
+
assert.equal(data.latest, '');
|
|
61
|
+
assert.ok(data.error);
|
|
62
|
+
} finally {
|
|
63
|
+
global.fetch = original;
|
|
64
|
+
await instance.close();
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('POST /api/update dispara el actualizador y responde 202', async () => {
|
|
69
|
+
clearUpdateCache();
|
|
70
|
+
const { instance, base } = await startServer();
|
|
71
|
+
const original = global.fetch;
|
|
72
|
+
global.fetch = fakeRegistry('99.0.0', original);
|
|
73
|
+
let called = null;
|
|
74
|
+
instance.ctx.spawnUpdater = (options) => {
|
|
75
|
+
called = options;
|
|
76
|
+
return { ok: true, pid: 1234 };
|
|
77
|
+
};
|
|
78
|
+
instance.ctx.shutdown = async () => {};
|
|
79
|
+
try {
|
|
80
|
+
const res = await fetch(`${base}/api/update`, { method: 'POST' });
|
|
81
|
+
assert.equal(res.status, 202);
|
|
82
|
+
const data = await res.json();
|
|
83
|
+
assert.equal(data.updating, true);
|
|
84
|
+
assert.match(data.from, /^\d+\.\d+\.\d+/);
|
|
85
|
+
assert.equal(data.to, '99.0.0');
|
|
86
|
+
assert.ok(called);
|
|
87
|
+
assert.equal(called.target, '99.0.0');
|
|
88
|
+
assert.equal(called.pid, process.pid);
|
|
89
|
+
assert.equal(typeof called.cwd, 'string');
|
|
90
|
+
} finally {
|
|
91
|
+
global.fetch = original;
|
|
92
|
+
await instance.close();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Auto-actualizacion disparada desde el server (POST /api/update).
|
|
3
|
+
//
|
|
4
|
+
// Corre detached: espera a que el server viejo muera (en Windows eso libera los
|
|
5
|
+
// archivos que npm necesita reemplazar), instala la ultima version y vuelve a
|
|
6
|
+
// levantar el server con el codigo nuevo. Todo se loguea en el log del server
|
|
7
|
+
// para poder diagnosticar desde Ajustes o `tamnora server --logs`.
|
|
8
|
+
|
|
9
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
10
|
+
import { appendFileSync, existsSync } from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
|
|
13
|
+
const PACKAGE = '@danieltmn/tamnora';
|
|
14
|
+
const IS_WIN = process.platform === 'win32';
|
|
15
|
+
const NPM = IS_WIN ? 'npm.cmd' : 'npm';
|
|
16
|
+
|
|
17
|
+
function arg(name) {
|
|
18
|
+
const i = process.argv.indexOf(`--${name}`);
|
|
19
|
+
return i >= 0 ? process.argv[i + 1] : '';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const pid = Number(arg('pid')) || 0;
|
|
23
|
+
const port = arg('port');
|
|
24
|
+
const host = arg('host');
|
|
25
|
+
const cwd = arg('cwd');
|
|
26
|
+
const target = arg('target') || 'latest';
|
|
27
|
+
const home = arg('home') || process.env.TAMNORA_HOME || '';
|
|
28
|
+
|
|
29
|
+
function defaultHome() {
|
|
30
|
+
if (process.env.APPDATA) return path.join(process.env.APPDATA, 'tamnora');
|
|
31
|
+
if (process.platform === 'darwin') {
|
|
32
|
+
return path.join(process.env.HOME || '', 'Library', 'Application Support', 'tamnora');
|
|
33
|
+
}
|
|
34
|
+
return path.join(process.env.HOME || process.env.USERPROFILE || '', '.config', 'tamnora');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function log(line) {
|
|
38
|
+
const text = `[${new Date().toISOString()}] self-update: ${line}`;
|
|
39
|
+
try {
|
|
40
|
+
appendFileSync(path.join(home || defaultHome(), 'logs', 'server.log'), `${text}\n`);
|
|
41
|
+
} catch {
|
|
42
|
+
/* sin log seguimos: la instalacion es lo importante */
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
process.stdout.write(`${text}\n`);
|
|
46
|
+
} catch {
|
|
47
|
+
/* stdout cerrado */
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
52
|
+
|
|
53
|
+
function alive(p) {
|
|
54
|
+
if (!p) return false;
|
|
55
|
+
try {
|
|
56
|
+
process.kill(p, 0);
|
|
57
|
+
return true;
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function waitDead(p, ms = 15000) {
|
|
64
|
+
const start = Date.now();
|
|
65
|
+
while (alive(p)) {
|
|
66
|
+
if (Date.now() - start > ms) {
|
|
67
|
+
try {
|
|
68
|
+
process.kill(p);
|
|
69
|
+
} catch {
|
|
70
|
+
/* ya murio */
|
|
71
|
+
}
|
|
72
|
+
await sleep(1500);
|
|
73
|
+
return !alive(p);
|
|
74
|
+
}
|
|
75
|
+
await sleep(300);
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function runNpm(args) {
|
|
81
|
+
return spawnSync(NPM, args, { encoding: 'utf8', windowsHide: true, shell: IS_WIN, timeout: 180000 });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function main() {
|
|
85
|
+
log(`esperando que muera el server (pid ${pid})`);
|
|
86
|
+
await waitDead(pid);
|
|
87
|
+
await sleep(800);
|
|
88
|
+
|
|
89
|
+
log(`instalando ${PACKAGE}@${target}`);
|
|
90
|
+
const install = runNpm(['i', '-g', `${PACKAGE}@${target}`]);
|
|
91
|
+
if (install.status !== 0) {
|
|
92
|
+
log(`FALLO la instalacion: ${(install.stderr || install.stdout || '').trim()}`);
|
|
93
|
+
log('Podes reintentar a mano: npm i -g ' + `${PACKAGE}@${target}`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const rootOut = runNpm(['root', '-g']).stdout?.trim();
|
|
98
|
+
const pkgDir = rootOut ? path.join(rootOut, ...PACKAGE.split('/')) : '';
|
|
99
|
+
const bin = pkgDir ? path.join(pkgDir, 'harness', 'packages', 'cli', 'bin', 'tamnora.mjs') : '';
|
|
100
|
+
if (!bin || !existsSync(bin)) {
|
|
101
|
+
log(`instale pero no encontre el binario nuevo en ${pkgDir || '(npm root -g vacio)'}`);
|
|
102
|
+
log('Levantalo a mano: tamnora server --start');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const args = [bin, 'server', '--stream', '--no-open'];
|
|
107
|
+
if (port) args.push('--port', port);
|
|
108
|
+
if (host) args.push('--host', host);
|
|
109
|
+
if (cwd) args.push('--dir', cwd);
|
|
110
|
+
log(`reiniciando: ${args.join(' ')}`);
|
|
111
|
+
const env = { ...process.env, TAMNORA_DETACHED: '1' };
|
|
112
|
+
if (home) env.TAMNORA_HOME = home;
|
|
113
|
+
const child = spawn(process.execPath, args, {
|
|
114
|
+
detached: true,
|
|
115
|
+
stdio: 'ignore',
|
|
116
|
+
windowsHide: true,
|
|
117
|
+
env,
|
|
118
|
+
});
|
|
119
|
+
child.unref();
|
|
120
|
+
log(`server reiniciado (pid ${child.pid}).`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
main().catch((error) => log(`error: ${error?.stack ?? error}`));
|
package/package.json
CHANGED