@danieltmn/tamnora 0.1.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 +48 -0
- package/LICENSE +21 -0
- package/README.md +130 -0
- package/SECURITY.md +31 -0
- package/harness/packages/cli/bin/tamnora.mjs +11 -0
- package/harness/packages/cli/package.json +10 -0
- package/harness/packages/cli/src/cli.js +90 -0
- package/harness/packages/cli/src/commands/autostart.js +121 -0
- package/harness/packages/cli/src/commands/bridge.js +274 -0
- package/harness/packages/cli/src/commands/doctor.js +73 -0
- package/harness/packages/cli/src/commands/eval.js +183 -0
- package/harness/packages/cli/src/commands/init.js +31 -0
- package/harness/packages/cli/src/commands/mcp.js +101 -0
- package/harness/packages/cli/src/commands/repl.js +172 -0
- package/harness/packages/cli/src/commands/run.js +165 -0
- package/harness/packages/cli/src/commands/serve.js +1 -0
- package/harness/packages/cli/src/commands/server.js +345 -0
- package/harness/packages/cli/src/commands/tui.js +110 -0
- package/harness/packages/cli/src/commands/update.js +76 -0
- package/harness/packages/cli/src/commands/user.js +135 -0
- package/harness/packages/cli/src/lib/core.js +2 -0
- package/harness/packages/cli/src/lib/daemon.js +153 -0
- package/harness/packages/cli/src/lib/qr.js +203 -0
- package/harness/packages/cli/src/lib/runtime.js +110 -0
- package/harness/packages/cli/src/lib/tunnel.js +113 -0
- package/harness/packages/cli/src/lib/util.js +24 -0
- package/harness/packages/cli/src/tui/app.js +302 -0
- package/harness/packages/cli/src/tui/keys.js +78 -0
- package/harness/packages/cli/src/tui/layout.js +130 -0
- package/harness/packages/cli/src/tui/screen.js +13 -0
- package/harness/packages/cli/src/ui/color.js +15 -0
- package/harness/packages/cli/test/release.test.js +34 -0
- package/harness/packages/cli/test/tui.test.js +99 -0
- package/harness/packages/cli/test/tunnel.test.js +20 -0
- package/harness/packages/core/package.json +14 -0
- package/harness/packages/core/src/agent/context.js +66 -0
- package/harness/packages/core/src/agent/modes.js +26 -0
- package/harness/packages/core/src/agent/runAgent.js +257 -0
- package/harness/packages/core/src/config/app.js +49 -0
- package/harness/packages/core/src/config/home.js +98 -0
- package/harness/packages/core/src/config/identity.js +41 -0
- package/harness/packages/core/src/config/index.js +69 -0
- package/harness/packages/core/src/config/providers.js +24 -0
- package/harness/packages/core/src/env.js +19 -0
- package/harness/packages/core/src/eval/dataset.js +47 -0
- package/harness/packages/core/src/eval/judge.js +39 -0
- package/harness/packages/core/src/eval/report.js +48 -0
- package/harness/packages/core/src/eval/runTask.js +71 -0
- package/harness/packages/core/src/eval/runner.js +150 -0
- package/harness/packages/core/src/eval/scorers.js +217 -0
- package/harness/packages/core/src/eval/task.js +59 -0
- package/harness/packages/core/src/index.js +72 -0
- package/harness/packages/core/src/mcp/common.js +82 -0
- package/harness/packages/core/src/mcp/http.js +149 -0
- package/harness/packages/core/src/mcp/index.js +167 -0
- package/harness/packages/core/src/mcp/stdio.js +218 -0
- package/harness/packages/core/src/providers/index.js +119 -0
- package/harness/packages/core/src/providers/ollama.js +76 -0
- package/harness/packages/core/src/providers/openaiCompatible.js +57 -0
- package/harness/packages/core/src/providers/openrouter.js +57 -0
- package/harness/packages/core/src/providers/presets.js +19 -0
- package/harness/packages/core/src/providers/resilience.js +74 -0
- package/harness/packages/core/src/providers/stream.js +200 -0
- package/harness/packages/core/src/providers/utils.js +66 -0
- package/harness/packages/core/src/storage/sqlite.js +453 -0
- package/harness/packages/core/src/tools/diff.js +90 -0
- package/harness/packages/core/src/tools/executors.js +277 -0
- package/harness/packages/core/src/tools/glob.js +67 -0
- package/harness/packages/core/src/tools/index.js +28 -0
- package/harness/packages/core/src/tools/paths.js +29 -0
- package/harness/packages/core/src/tools/permissions.js +69 -0
- package/harness/packages/core/src/tools/registry.js +44 -0
- package/harness/packages/core/src/tools/schema.js +177 -0
- package/harness/packages/core/test/agent.test.js +237 -0
- package/harness/packages/core/test/config.test.js +33 -0
- package/harness/packages/core/test/context.test.js +44 -0
- package/harness/packages/core/test/diff.test.js +20 -0
- package/harness/packages/core/test/eval.test.js +144 -0
- package/harness/packages/core/test/git-tools.test.js +44 -0
- package/harness/packages/core/test/home.test.js +62 -0
- package/harness/packages/core/test/mcp.test.js +158 -0
- package/harness/packages/core/test/providers.test.js +80 -0
- package/harness/packages/core/test/resilience.test.js +61 -0
- package/harness/packages/core/test/storage.test.js +55 -0
- package/harness/packages/core/test/stream.test.js +91 -0
- package/harness/packages/core/test/tools.test.js +96 -0
- package/harness/packages/server/bin/tamnora-server.mjs +11 -0
- package/harness/packages/server/package.json +10 -0
- package/harness/packages/server/public/app.js +2188 -0
- package/harness/packages/server/public/img/fondo-agentes.webp +0 -0
- package/harness/packages/server/public/index.html +368 -0
- package/harness/packages/server/public/landing/index.html +523 -0
- package/harness/packages/server/public/landing/landing.js +89 -0
- package/harness/packages/server/public/markdown.js +174 -0
- package/harness/packages/server/public/styles.css +923 -0
- package/harness/packages/server/src/auth.js +146 -0
- package/harness/packages/server/src/cli.js +61 -0
- package/harness/packages/server/src/index.js +1 -0
- package/harness/packages/server/src/lib/approvals.js +51 -0
- package/harness/packages/server/src/lib/core.js +2 -0
- package/harness/packages/server/src/lib/http.js +74 -0
- package/harness/packages/server/src/lib/modelCache.js +32 -0
- package/harness/packages/server/src/lib/prompt.js +48 -0
- package/harness/packages/server/src/lib/workspace.js +74 -0
- package/harness/packages/server/src/routes/auth.js +56 -0
- package/harness/packages/server/src/routes/chat.js +222 -0
- package/harness/packages/server/src/routes/eval.js +100 -0
- package/harness/packages/server/src/routes/files.js +117 -0
- package/harness/packages/server/src/routes/folders.js +80 -0
- package/harness/packages/server/src/routes/mcp.js +50 -0
- package/harness/packages/server/src/routes/models.js +26 -0
- package/harness/packages/server/src/routes/providers.js +135 -0
- package/harness/packages/server/src/routes/sessions.js +157 -0
- package/harness/packages/server/src/server.js +249 -0
- package/harness/packages/server/src/static.js +67 -0
- package/harness/packages/server/test/markdown.test.js +36 -0
- package/harness/packages/server/test/modelCache.test.js +72 -0
- package/harness/packages/server/test/server.test.js +809 -0
- package/harness/scripts/example-dataset.jsonl +4 -0
- package/harness/scripts/mcp-echo-server.mjs +108 -0
- package/harness/scripts/mock-provider.mjs +134 -0
- package/harness/tamnora.example.json +18 -0
- package/package.json +70 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import readline from 'node:readline';
|
|
3
|
+
import {
|
|
4
|
+
loadConfig,
|
|
5
|
+
loadInstructions,
|
|
6
|
+
Store,
|
|
7
|
+
createTools,
|
|
8
|
+
buildMcpRegistry,
|
|
9
|
+
} from '../lib/core.js';
|
|
10
|
+
import { parse, databasePath, num } from '../lib/util.js';
|
|
11
|
+
import { buildSystemPrompt, runTurn, createPrinter, resolveCliProvider } from '../lib/runtime.js';
|
|
12
|
+
import { color } from '../ui/color.js';
|
|
13
|
+
|
|
14
|
+
const OPTIONS = {
|
|
15
|
+
provider: { type: 'string' },
|
|
16
|
+
model: { type: 'string' },
|
|
17
|
+
dir: { type: 'string' },
|
|
18
|
+
'max-steps': { type: 'string' },
|
|
19
|
+
tools: { type: 'string' },
|
|
20
|
+
'no-tools': { type: 'boolean' },
|
|
21
|
+
yes: { type: 'boolean', short: 'y' },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export async function replCommand(argv) {
|
|
25
|
+
const { values } = parse(argv, OPTIONS);
|
|
26
|
+
const cwd = values.dir ? path.resolve(values.dir) : process.cwd();
|
|
27
|
+
const config = await loadConfig(cwd);
|
|
28
|
+
|
|
29
|
+
let systemPrompt = buildSystemPrompt(await loadInstructions(cwd, config.instructions));
|
|
30
|
+
const maxRounds = num(values['max-steps'], config.maxRounds);
|
|
31
|
+
const store = new Store(databasePath(cwd));
|
|
32
|
+
const resolved = resolveCliProvider(store, config, { provider: values.provider, model: values.model });
|
|
33
|
+
let providerId = resolved.providerId;
|
|
34
|
+
let model = resolved.model;
|
|
35
|
+
const baseUrl = resolved.baseUrl;
|
|
36
|
+
const apiKey = resolved.apiKey;
|
|
37
|
+
if (!model) {
|
|
38
|
+
console.error(color.red('Falta el modelo (usá --model, Ajustes o "model" en tamnora.json).'));
|
|
39
|
+
store.close();
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
const sessionId = store.createSession({ title: 'REPL', provider: providerId, model, cwd });
|
|
43
|
+
let history = [];
|
|
44
|
+
let busy = false;
|
|
45
|
+
|
|
46
|
+
const rl = readline.createInterface({
|
|
47
|
+
input: process.stdin,
|
|
48
|
+
output: process.stdout,
|
|
49
|
+
prompt: color.cyan('tamnora> '),
|
|
50
|
+
historySize: 200,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Interactive permission prompt for 'ask' tools.
|
|
54
|
+
const onAsk = (name, args) =>
|
|
55
|
+
new Promise((resolve) => {
|
|
56
|
+
const detail = args?.command ?? args?.path ?? '';
|
|
57
|
+
rl.question(
|
|
58
|
+
color.yellow(`\npermitir ${name}${detail ? ` (${detail})` : ''}? [y/N] `),
|
|
59
|
+
(answer) => resolve(/^y(es)?$/i.test(answer.trim()))
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
let mcp = null;
|
|
64
|
+
if (Array.isArray(config.mcp) && config.mcp.length) {
|
|
65
|
+
try {
|
|
66
|
+
mcp = await buildMcpRegistry(config.mcp);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
process.stdout.write(color.yellow(`MCP: ${err.message}\n`));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const enabled = values['no-tools']
|
|
72
|
+
? []
|
|
73
|
+
: values.tools
|
|
74
|
+
? String(values.tools).split(',').map((s) => s.trim()).filter(Boolean)
|
|
75
|
+
: Array.isArray(config.tools) && config.tools.length
|
|
76
|
+
? config.tools
|
|
77
|
+
: undefined;
|
|
78
|
+
const { definitions: toolDefs, execute } = createTools({
|
|
79
|
+
root: cwd,
|
|
80
|
+
enabled,
|
|
81
|
+
permissions: config.permissions,
|
|
82
|
+
yes: Boolean(values.yes),
|
|
83
|
+
onAsk,
|
|
84
|
+
mcp,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
process.stdout.write(color.dim(`Tamnora REPL · ${providerId}/${model}\n`));
|
|
88
|
+
process.stdout.write(color.dim(`workspace ${cwd}\n`));
|
|
89
|
+
process.stdout.write(color.dim(`session ${sessionId} · /help para comandos · Ctrl+C para salir\n\n`));
|
|
90
|
+
rl.prompt();
|
|
91
|
+
|
|
92
|
+
rl.on('line', async (line) => {
|
|
93
|
+
if (busy) return;
|
|
94
|
+
const text = line.trim();
|
|
95
|
+
if (!text) {
|
|
96
|
+
rl.prompt();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (text.startsWith('/')) {
|
|
101
|
+
const [rawCmd, ...rest] = text.slice(1).split(/\s+/);
|
|
102
|
+
const cmd = rawCmd.toLowerCase();
|
|
103
|
+
const arg = rest.join(' ').trim();
|
|
104
|
+
if (cmd === 'exit' || cmd === 'quit') {
|
|
105
|
+
rl.close();
|
|
106
|
+
return;
|
|
107
|
+
} else if (cmd === 'help') {
|
|
108
|
+
process.stdout.write(
|
|
109
|
+
`${color.dim(
|
|
110
|
+
'/model <id> /provider <id> /system <texto> /clear /session /exit'
|
|
111
|
+
)}\n`
|
|
112
|
+
);
|
|
113
|
+
} else if (cmd === 'model') {
|
|
114
|
+
if (arg) model = arg;
|
|
115
|
+
process.stdout.write(color.dim(`modelo: ${model}\n`));
|
|
116
|
+
} else if (cmd === 'provider') {
|
|
117
|
+
if (arg) providerId = arg;
|
|
118
|
+
process.stdout.write(color.dim(`proveedor: ${providerId}\n`));
|
|
119
|
+
} else if (cmd === 'system') {
|
|
120
|
+
if (arg) systemPrompt = arg;
|
|
121
|
+
process.stdout.write(color.dim(`system prompt (${systemPrompt.length} chars)\n`));
|
|
122
|
+
} else if (cmd === 'clear') {
|
|
123
|
+
history = [];
|
|
124
|
+
process.stdout.write(color.dim('historial limpiado\n'));
|
|
125
|
+
} else if (cmd === 'session') {
|
|
126
|
+
process.stdout.write(color.dim(`session ${sessionId}\n`));
|
|
127
|
+
} else {
|
|
128
|
+
process.stdout.write(color.red(`comando desconocido: /${cmd}\n`));
|
|
129
|
+
}
|
|
130
|
+
rl.prompt();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
busy = true;
|
|
135
|
+
const printer = createPrinter();
|
|
136
|
+
process.stdout.write(color.magenta('assistant: '));
|
|
137
|
+
try {
|
|
138
|
+
const { newMessages } = await runTurn({
|
|
139
|
+
providerId,
|
|
140
|
+
model,
|
|
141
|
+
baseUrl,
|
|
142
|
+
apiKey,
|
|
143
|
+
systemPrompt,
|
|
144
|
+
history,
|
|
145
|
+
input: text,
|
|
146
|
+
maxRounds,
|
|
147
|
+
tools: toolDefs,
|
|
148
|
+
execute,
|
|
149
|
+
onEvent: printer.onEvent,
|
|
150
|
+
});
|
|
151
|
+
printer.finish();
|
|
152
|
+
history = history.concat(newMessages);
|
|
153
|
+
store.addMessages(sessionId, newMessages);
|
|
154
|
+
store.touchSession(sessionId, { provider: providerId, model });
|
|
155
|
+
} catch (err) {
|
|
156
|
+
process.stdout.write('\n');
|
|
157
|
+
console.error(color.red(`Error: ${err.message}`));
|
|
158
|
+
} finally {
|
|
159
|
+
busy = false;
|
|
160
|
+
rl.prompt();
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return new Promise((resolve) => {
|
|
165
|
+
rl.on('close', () => {
|
|
166
|
+
store.close();
|
|
167
|
+
mcp?.close?.();
|
|
168
|
+
process.stdout.write('\n');
|
|
169
|
+
resolve(0);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { loadConfig, loadInstructions, Store, createTools, buildMcpRegistry } from '../lib/core.js';
|
|
3
|
+
import { parse, databasePath, num } from '../lib/util.js';
|
|
4
|
+
import { buildSystemPrompt, runTurn, createPrinter, resolveCliProvider } from '../lib/runtime.js';
|
|
5
|
+
import { color } from '../ui/color.js';
|
|
6
|
+
|
|
7
|
+
const OPTIONS = {
|
|
8
|
+
provider: { type: 'string' },
|
|
9
|
+
model: { type: 'string' },
|
|
10
|
+
dir: { type: 'string' },
|
|
11
|
+
session: { type: 'string' },
|
|
12
|
+
continue: { type: 'boolean', short: 'c' },
|
|
13
|
+
json: { type: 'boolean' },
|
|
14
|
+
'max-steps': { type: 'string' },
|
|
15
|
+
system: { type: 'string' },
|
|
16
|
+
tools: { type: 'string' },
|
|
17
|
+
'no-tools': { type: 'boolean' },
|
|
18
|
+
yes: { type: 'boolean', short: 'y' },
|
|
19
|
+
help: { type: 'boolean', short: 'h' },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export async function runCommand(argv) {
|
|
23
|
+
const { values, positionals } = parse(argv, OPTIONS);
|
|
24
|
+
if (values.help) {
|
|
25
|
+
printHelp();
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const prompt = positionals.join(' ').trim();
|
|
30
|
+
if (!prompt) {
|
|
31
|
+
console.error(color.red('Falta el prompt. Uso: tamnora run "<prompt>"'));
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const cwd = values.dir ? path.resolve(values.dir) : process.cwd();
|
|
36
|
+
const config = await loadConfig(cwd);
|
|
37
|
+
const instructions = await loadInstructions(cwd, config.instructions);
|
|
38
|
+
const store = new Store(databasePath(cwd));
|
|
39
|
+
const { providerId, model, baseUrl, apiKey } = resolveCliProvider(store, config, {
|
|
40
|
+
provider: values.provider,
|
|
41
|
+
model: values.model,
|
|
42
|
+
});
|
|
43
|
+
if (!model) {
|
|
44
|
+
console.error(color.red('Falta el modelo (usá --model, Ajustes o "model" en tamnora.json).'));
|
|
45
|
+
store.close();
|
|
46
|
+
return 1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Local, workspace-scoped tools (combined with any configured MCP servers).
|
|
50
|
+
let mcp = null;
|
|
51
|
+
if (Array.isArray(config.mcp) && config.mcp.length) {
|
|
52
|
+
try {
|
|
53
|
+
mcp = await buildMcpRegistry(config.mcp);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
process.stderr.write(color.yellow(`MCP: ${err.message}\n`));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const enabled = values['no-tools']
|
|
59
|
+
? []
|
|
60
|
+
: values.tools
|
|
61
|
+
? String(values.tools).split(',').map((s) => s.trim()).filter(Boolean)
|
|
62
|
+
: Array.isArray(config.tools) && config.tools.length
|
|
63
|
+
? config.tools
|
|
64
|
+
: undefined;
|
|
65
|
+
const { definitions: toolDefs, execute } = createTools({
|
|
66
|
+
root: cwd,
|
|
67
|
+
enabled,
|
|
68
|
+
permissions: config.permissions,
|
|
69
|
+
yes: Boolean(values.yes),
|
|
70
|
+
mcp,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
let sessionId = values.session;
|
|
74
|
+
let history = [];
|
|
75
|
+
if (sessionId) {
|
|
76
|
+
if (!store.getSession(sessionId)) {
|
|
77
|
+
console.error(color.red(`Sesión no encontrada: ${sessionId}`));
|
|
78
|
+
store.close();
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
history = store.getMessages(sessionId);
|
|
82
|
+
} else if (values.continue) {
|
|
83
|
+
const latest = store.latestSession();
|
|
84
|
+
if (latest) {
|
|
85
|
+
sessionId = latest.id;
|
|
86
|
+
history = store.getMessages(sessionId);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (!sessionId) {
|
|
90
|
+
sessionId = store.createSession({ title: prompt.slice(0, 60), provider: providerId, model, cwd });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const printer = createPrinter({ json: values.json });
|
|
94
|
+
const controller = new AbortController();
|
|
95
|
+
const onSigint = () => controller.abort();
|
|
96
|
+
process.once('SIGINT', onSigint);
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const { result, newMessages } = await runTurn({
|
|
100
|
+
providerId,
|
|
101
|
+
model,
|
|
102
|
+
baseUrl,
|
|
103
|
+
apiKey,
|
|
104
|
+
systemPrompt: buildSystemPrompt(instructions, values.system),
|
|
105
|
+
history,
|
|
106
|
+
input: prompt,
|
|
107
|
+
maxRounds: num(values['max-steps'], config.maxRounds),
|
|
108
|
+
signal: controller.signal,
|
|
109
|
+
tools: toolDefs,
|
|
110
|
+
execute,
|
|
111
|
+
onEvent: printer.onEvent,
|
|
112
|
+
});
|
|
113
|
+
printer.finish();
|
|
114
|
+
store.addMessages(sessionId, newMessages);
|
|
115
|
+
store.touchSession(sessionId, { provider: providerId, model });
|
|
116
|
+
|
|
117
|
+
if (values.json) {
|
|
118
|
+
process.stdout.write(
|
|
119
|
+
`${JSON.stringify(
|
|
120
|
+
{
|
|
121
|
+
output: result.content,
|
|
122
|
+
stopReason: result.stopReason,
|
|
123
|
+
rounds: result.rounds,
|
|
124
|
+
sessionId,
|
|
125
|
+
provider: providerId,
|
|
126
|
+
model,
|
|
127
|
+
},
|
|
128
|
+
null,
|
|
129
|
+
2
|
|
130
|
+
)}\n`
|
|
131
|
+
);
|
|
132
|
+
} else {
|
|
133
|
+
process.stderr.write(color.dim(`\nsession ${sessionId}\n`));
|
|
134
|
+
}
|
|
135
|
+
return 0;
|
|
136
|
+
} catch (err) {
|
|
137
|
+
if (err.name === 'AbortError') {
|
|
138
|
+
process.stderr.write('\n');
|
|
139
|
+
return 130;
|
|
140
|
+
}
|
|
141
|
+
console.error(color.red(`\nError: ${err.message}`));
|
|
142
|
+
return 1;
|
|
143
|
+
} finally {
|
|
144
|
+
process.removeListener('SIGINT', onSigint);
|
|
145
|
+
mcp?.close?.();
|
|
146
|
+
store.close();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function printHelp() {
|
|
151
|
+
console.log(`Uso: tamnora run "<prompt>" [opciones]
|
|
152
|
+
|
|
153
|
+
Opciones:
|
|
154
|
+
--provider <id> Proveedor (openrouter, deepseek, groq, ollama, ...)
|
|
155
|
+
--model <id> Modelo a usar
|
|
156
|
+
--dir <path> Directorio del proyecto (tamnora.json / AGENTS.md)
|
|
157
|
+
--session <id> Continuar una sesión existente
|
|
158
|
+
-c, --continue Continuar la última sesión
|
|
159
|
+
--system <texto> Instrucciones extra de sistema
|
|
160
|
+
--max-steps <n> Máximo de rondas de herramientas (default: 8)
|
|
161
|
+
--tools <lista> Tools habilitadas (coma), ej. read,list,grep
|
|
162
|
+
--no-tools Deshabilitar todas las tools
|
|
163
|
+
-y, --yes Aprobar automáticamente los permisos "ask"
|
|
164
|
+
--json Salida JSON (sin streaming)`);
|
|
165
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { serveCommand } from '../../../server/src/cli.js';
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { appendFileSync, writeFileSync, readFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
setHome,
|
|
6
|
+
homeDir,
|
|
7
|
+
ensureHome,
|
|
8
|
+
homeExists,
|
|
9
|
+
readApp,
|
|
10
|
+
writeApp,
|
|
11
|
+
ensureApp,
|
|
12
|
+
databasePath,
|
|
13
|
+
pidPath,
|
|
14
|
+
serverLogPath,
|
|
15
|
+
} from '../../../core/src/index.js';
|
|
16
|
+
import { createTamnoraServer } from '../../../server/src/index.js';
|
|
17
|
+
import { parse, num } from '../lib/util.js';
|
|
18
|
+
import {
|
|
19
|
+
killPid,
|
|
20
|
+
pidAlive,
|
|
21
|
+
readRuntime,
|
|
22
|
+
writeRuntime,
|
|
23
|
+
clearRuntime,
|
|
24
|
+
stopRunningServer,
|
|
25
|
+
portHint,
|
|
26
|
+
tailLines,
|
|
27
|
+
} from '../lib/daemon.js';
|
|
28
|
+
|
|
29
|
+
const OPTIONS = {
|
|
30
|
+
init: { type: 'boolean' },
|
|
31
|
+
start: { type: 'boolean' },
|
|
32
|
+
stop: { type: 'boolean' },
|
|
33
|
+
reload: { type: 'boolean' },
|
|
34
|
+
status: { type: 'boolean' },
|
|
35
|
+
logs: { type: 'boolean' },
|
|
36
|
+
follow: { type: 'boolean', short: 'f' },
|
|
37
|
+
stream: { type: 'boolean' },
|
|
38
|
+
foreground: { type: 'boolean' },
|
|
39
|
+
'no-tunnel': { type: 'boolean' },
|
|
40
|
+
port: { type: 'string' },
|
|
41
|
+
host: { type: 'string' },
|
|
42
|
+
dir: { type: 'string' },
|
|
43
|
+
home: { type: 'string' },
|
|
44
|
+
force: { type: 'boolean' },
|
|
45
|
+
yes: { type: 'boolean', short: 'y' },
|
|
46
|
+
help: { type: 'boolean', short: 'h' },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function applyHome(values) {
|
|
50
|
+
const override = values.home ? path.resolve(values.home) : process.env.TAMNORA_HOME || null;
|
|
51
|
+
if (values.home) {
|
|
52
|
+
setHome(values.home);
|
|
53
|
+
process.env.TAMNORA_HOME = override;
|
|
54
|
+
}
|
|
55
|
+
return override;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function log(line) {
|
|
59
|
+
const text = `[${new Date().toISOString()}] ${line}`;
|
|
60
|
+
try {
|
|
61
|
+
ensureHome();
|
|
62
|
+
appendFileSync(serverLogPath(), `${text}\n`);
|
|
63
|
+
} catch {
|
|
64
|
+
/* sin log no detenemos nada */
|
|
65
|
+
}
|
|
66
|
+
console.log(text);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function serverCommand(argv) {
|
|
70
|
+
const { values } = parse(argv, OPTIONS);
|
|
71
|
+
if (values.help) {
|
|
72
|
+
printHelp();
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
applyHome(values);
|
|
76
|
+
|
|
77
|
+
if (values.init) return cmdInit(values);
|
|
78
|
+
if (values.stop) return cmdStop();
|
|
79
|
+
if (values.status) return cmdStatus();
|
|
80
|
+
if (values.logs) return cmdLogs(values);
|
|
81
|
+
if (values.reload) return cmdReload(values);
|
|
82
|
+
return cmdStart(values);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function cmdInit(values) {
|
|
86
|
+
ensureHome();
|
|
87
|
+
const existed = homeExists();
|
|
88
|
+
if (existed && !values.force) {
|
|
89
|
+
console.log(`Ya existe configuracion en ${homeDir()}`);
|
|
90
|
+
console.log('Usa --force para reconfigurar (se pisa app.json).');
|
|
91
|
+
} else {
|
|
92
|
+
if (existed && stopRunningServer()) {
|
|
93
|
+
console.log('Habia un server corriendo; lo detuve para reconfigurar.');
|
|
94
|
+
}
|
|
95
|
+
const app = readApp();
|
|
96
|
+
if (values.port) app.port = num(values.port, app.port);
|
|
97
|
+
if (values.host) app.host = values.host;
|
|
98
|
+
if (values['no-tunnel']) app.tunnel = { ...app.tunnel, provider: 'none' };
|
|
99
|
+
writeApp(app);
|
|
100
|
+
ensureApp();
|
|
101
|
+
console.log(`Casa Tamnora creada en ${homeDir()}`);
|
|
102
|
+
}
|
|
103
|
+
return cmdStart(values);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function cmdStart(values) {
|
|
107
|
+
ensureHome();
|
|
108
|
+
const app = ensureApp();
|
|
109
|
+
const port = num(values.port, app.port || 4517);
|
|
110
|
+
const host = values.host || app.host || '127.0.0.1';
|
|
111
|
+
if (app.port !== port || app.host !== host) {
|
|
112
|
+
app.port = port;
|
|
113
|
+
app.host = host;
|
|
114
|
+
writeApp(app);
|
|
115
|
+
}
|
|
116
|
+
const cwd = values.dir ? path.resolve(values.dir) : process.cwd();
|
|
117
|
+
|
|
118
|
+
const isChild = Boolean(process.env.TAMNORA_DETACHED);
|
|
119
|
+
const foreground = Boolean(values.stream || values.foreground) || isChild;
|
|
120
|
+
|
|
121
|
+
if (!isChild) {
|
|
122
|
+
const cur = readRuntime();
|
|
123
|
+
if (cur && pidAlive(cur.pid)) {
|
|
124
|
+
console.log(`Tamnora ya esta corriendo (pid ${cur.pid}).`);
|
|
125
|
+
printStatus(cur);
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!foreground) {
|
|
131
|
+
const bin = process.argv[1];
|
|
132
|
+
const args = [bin, 'server', '--stream', '--port', String(port), '--host', host];
|
|
133
|
+
if (values.dir) args.push('--dir', cwd);
|
|
134
|
+
if (process.env.TAMNORA_HOME) args.push('--home', process.env.TAMNORA_HOME);
|
|
135
|
+
if (values.yes) args.push('--yes');
|
|
136
|
+
const child = spawn(process.execPath, args, {
|
|
137
|
+
detached: true,
|
|
138
|
+
stdio: 'ignore',
|
|
139
|
+
windowsHide: true,
|
|
140
|
+
env: { ...process.env, TAMNORA_DETACHED: '1' },
|
|
141
|
+
});
|
|
142
|
+
child.unref();
|
|
143
|
+
let dead = false;
|
|
144
|
+
child.on('exit', () => {
|
|
145
|
+
dead = true;
|
|
146
|
+
});
|
|
147
|
+
console.log(`Tamnora arrancando en segundo plano (pid ${child.pid})...`);
|
|
148
|
+
const rt = await waitForRuntime(child.pid, () => dead, 30000);
|
|
149
|
+
const tail = rt ? '' : tailLines(serverLogPath(), 8);
|
|
150
|
+
if (tail) {
|
|
151
|
+
console.log('');
|
|
152
|
+
console.log(`Ultimas lineas de ${serverLogPath()}:`);
|
|
153
|
+
console.log(tail);
|
|
154
|
+
}
|
|
155
|
+
const current = rt || readRuntime();
|
|
156
|
+
if (current) printStatus(current);
|
|
157
|
+
else console.log('No pude confirmar el arranque. Revisa: tamnora server --logs');
|
|
158
|
+
return current ? 0 : 1;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return runForeground({ values, port, host, cwd });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function runForeground({ values, port, host, cwd }) {
|
|
165
|
+
let instance;
|
|
166
|
+
try {
|
|
167
|
+
instance = await createTamnoraServer({ cwd, port, host, dbPath: databasePath(cwd), yes: Boolean(values.yes) });
|
|
168
|
+
} catch (err) {
|
|
169
|
+
log(`ERROR: ${err.message}`);
|
|
170
|
+
return 1;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let info;
|
|
174
|
+
try {
|
|
175
|
+
info = await instance.listen({ port, host });
|
|
176
|
+
} catch (err) {
|
|
177
|
+
if (err && err.code === 'EADDRINUSE') {
|
|
178
|
+
log(`ERROR: puerto ${port} ocupado (¿ya corre Tamnora?). Usa --port <otro> o \`tamnora server --stop\`.`);
|
|
179
|
+
const hint = portHint(port);
|
|
180
|
+
if (hint) log(` lo esta usando: ${hint}`);
|
|
181
|
+
return 1;
|
|
182
|
+
}
|
|
183
|
+
log(`ERROR: ${err.message}`);
|
|
184
|
+
return 1;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const runtime = {
|
|
188
|
+
pid: process.pid,
|
|
189
|
+
port: info.port,
|
|
190
|
+
host: info.host,
|
|
191
|
+
url: info.url,
|
|
192
|
+
cwd,
|
|
193
|
+
provider: instance.config.provider,
|
|
194
|
+
model: instance.config.model || '',
|
|
195
|
+
tunnel: 'none',
|
|
196
|
+
startedAt: new Date().toISOString(),
|
|
197
|
+
children: [],
|
|
198
|
+
};
|
|
199
|
+
writeRuntime(runtime);
|
|
200
|
+
try {
|
|
201
|
+
writeFileSync(pidPath(), String(process.pid));
|
|
202
|
+
} catch {
|
|
203
|
+
/* nada */
|
|
204
|
+
}
|
|
205
|
+
log(`Tamnora escuchando en ${info.url} (workspace ${cwd})`);
|
|
206
|
+
if (!instance.ctx.authEnabled) {
|
|
207
|
+
log('aviso: sin usuario configurado; el server no pide login. Crea uno: tamnora user --name <n> --password <p>');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return new Promise((resolve) => {
|
|
211
|
+
let closing = false;
|
|
212
|
+
const shutdown = async () => {
|
|
213
|
+
if (closing) return;
|
|
214
|
+
closing = true;
|
|
215
|
+
log('Cerrando Tamnora...');
|
|
216
|
+
try {
|
|
217
|
+
await instance.close();
|
|
218
|
+
} catch {
|
|
219
|
+
/* nada */
|
|
220
|
+
}
|
|
221
|
+
clearRuntime();
|
|
222
|
+
resolve(0);
|
|
223
|
+
};
|
|
224
|
+
process.once('SIGINT', shutdown);
|
|
225
|
+
process.once('SIGTERM', shutdown);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function cmdStop() {
|
|
230
|
+
const rt = readRuntime();
|
|
231
|
+
const pid = rt && rt.pid ? rt.pid : readPid();
|
|
232
|
+
const stopped = [];
|
|
233
|
+
if (pid && pidAlive(pid)) {
|
|
234
|
+
killPid(pid);
|
|
235
|
+
if (rt && Array.isArray(rt.children)) {
|
|
236
|
+
for (const c of rt.children) killPid(c);
|
|
237
|
+
}
|
|
238
|
+
stopped.push(`server ${pid}`);
|
|
239
|
+
}
|
|
240
|
+
clearRuntime();
|
|
241
|
+
if (!stopped.length) {
|
|
242
|
+
console.log('Tamnora no esta corriendo.');
|
|
243
|
+
return 0;
|
|
244
|
+
}
|
|
245
|
+
console.log(`Tamnora detenido (${stopped.join(', ')}).`);
|
|
246
|
+
return 0;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function cmdReload(values) {
|
|
250
|
+
await cmdStop();
|
|
251
|
+
for (let i = 0; i < 25 && pidAlive(readPid()); i++) {
|
|
252
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
253
|
+
}
|
|
254
|
+
return cmdStart(values);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function cmdStatus() {
|
|
258
|
+
const rt = readRuntime();
|
|
259
|
+
if (!rt || !pidAlive(rt.pid)) {
|
|
260
|
+
console.log('Tamnora detenido.');
|
|
261
|
+
return 0;
|
|
262
|
+
}
|
|
263
|
+
printStatus(rt);
|
|
264
|
+
return 0;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function cmdLogs(values) {
|
|
268
|
+
const file = serverLogPath();
|
|
269
|
+
const initial = tailLines(file, 40);
|
|
270
|
+
console.log(initial || `(sin logs en ${file})`);
|
|
271
|
+
if (!values.follow) return 0;
|
|
272
|
+
let offset = initial.length;
|
|
273
|
+
return new Promise((resolve) => {
|
|
274
|
+
const timer = setInterval(() => {
|
|
275
|
+
let text = '';
|
|
276
|
+
try {
|
|
277
|
+
text = readFileSync(file, 'utf8');
|
|
278
|
+
} catch {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (text.length > offset) {
|
|
282
|
+
process.stdout.write(text.slice(offset));
|
|
283
|
+
offset = text.length;
|
|
284
|
+
}
|
|
285
|
+
}, 1000);
|
|
286
|
+
const stop = () => {
|
|
287
|
+
clearInterval(timer);
|
|
288
|
+
resolve(0);
|
|
289
|
+
};
|
|
290
|
+
process.once('SIGINT', stop);
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function printStatus(rt) {
|
|
295
|
+
console.log(`Tamnora corriendo (pid ${rt.pid})`);
|
|
296
|
+
console.log(` url : ${rt.url}`);
|
|
297
|
+
console.log(` workspace: ${rt.cwd}`);
|
|
298
|
+
console.log(` modelo : ${rt.provider}${rt.model ? `/${rt.model}` : ''}`);
|
|
299
|
+
console.log(` casa : ${homeDir()}`);
|
|
300
|
+
console.log(` logs : ${serverLogPath()}`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function readPid() {
|
|
304
|
+
try {
|
|
305
|
+
return parseInt(readFileSync(pidPath(), 'utf8'), 10);
|
|
306
|
+
} catch {
|
|
307
|
+
return 0;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function waitForRuntime(childPid, isDead, timeoutMs) {
|
|
312
|
+
return new Promise((resolve) => {
|
|
313
|
+
const start = Date.now();
|
|
314
|
+
const tick = () => {
|
|
315
|
+
if (isDead()) return resolve(null);
|
|
316
|
+
const rt = readRuntime();
|
|
317
|
+
if (rt && rt.pid && pidAlive(rt.pid) && (!childPid || rt.pid === childPid)) return resolve(rt);
|
|
318
|
+
if (Date.now() - start > timeoutMs) return resolve(null);
|
|
319
|
+
setTimeout(tick, 400);
|
|
320
|
+
};
|
|
321
|
+
tick();
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function printHelp() {
|
|
326
|
+
console.log(`Uso: tamnora server [accion] [opciones]
|
|
327
|
+
|
|
328
|
+
Arranca la consola del harness como servicio local (segundo plano por defecto).
|
|
329
|
+
|
|
330
|
+
Acciones:
|
|
331
|
+
--init Crea/valida la casa y arranca
|
|
332
|
+
--start Arranca en segundo plano (default)
|
|
333
|
+
--reload Reinicia (aplica config/codigo)
|
|
334
|
+
--stop Detiene
|
|
335
|
+
--status Estado del server
|
|
336
|
+
--logs [--follow] Ultimas lineas del log
|
|
337
|
+
--stream Corre en primer plano (debug)
|
|
338
|
+
|
|
339
|
+
Opciones:
|
|
340
|
+
--port <n> Puerto (default: el de app.json, 4517)
|
|
341
|
+
--host <addr> Interfaz (default: 127.0.0.1)
|
|
342
|
+
--dir <path> Workspace del server
|
|
343
|
+
--home <path> Casa de Tamnora (default: TAMNORA_HOME o datos del sistema)
|
|
344
|
+
-y, --yes Auto-aprobar tools con permiso "ask"`);
|
|
345
|
+
}
|