taskmonkey-cli 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/.tmrc ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "server": "https://gpt.meco-media.com",
3
+ "tenant": "bloomify",
4
+ "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJtZWNvZ3B0Iiwic3ViIjoiYzAyZWYyMjItMDU1MC00OTgwLTg0ODYtNzIzOTJmZDI2ZjY5IiwiZW1haWwiOiJtYXJjdXMuZ29lZGVAbWVjby1tZWRpYS5jb20iLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3NzUxNTY4NDksImV4cCI6MTc3NTI0MzI0OX0.bpfHNBOuq1un8Psc89Xevn-Ibo4YVAYNNwDRYe6fb4A",
5
+ "refresh_token": "5c9eb7bacec249d2aacb57402f3f5fedee2faf2796d721be6c02970653e9186b",
6
+ "tenant_path": "."
7
+ }
package/bin/tm.js ADDED
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { login } from '../src/commands/login.js';
5
+ import { testTool } from '../src/commands/test-tool.js';
6
+ import { sync } from '../src/commands/sync.js';
7
+ import { pull } from '../src/commands/pull.js';
8
+ import { watch } from '../src/commands/watch.js';
9
+ import { logs } from '../src/commands/logs.js';
10
+ import { chat } from '../src/commands/chat.js';
11
+
12
+ const program = new Command();
13
+
14
+ program
15
+ .name('tm')
16
+ .description('TaskMonkey CLI — Remote dev tools for tenant config')
17
+ .version('0.1.0');
18
+
19
+ program
20
+ .command('login')
21
+ .description('Login and select tenant')
22
+ .option('-s, --server <url>', 'Server URL', 'https://gpt.meco-media.com')
23
+ .action(login);
24
+
25
+ program
26
+ .command('test-tool <toolName> [args...]')
27
+ .description('Run a tenant tool with arguments (key=value)')
28
+ .option('--dry-run', 'Simulation mode')
29
+ .option('--json', 'JSON output only')
30
+ .action(testTool);
31
+
32
+ program
33
+ .command('sync')
34
+ .description('Upload local config files to server')
35
+ .action(sync);
36
+
37
+ program
38
+ .command('pull')
39
+ .description('Download config files from server')
40
+ .action(pull);
41
+
42
+ program
43
+ .command('watch')
44
+ .description('Watch for file changes and auto-sync')
45
+ .action(watch);
46
+
47
+ program
48
+ .command('logs')
49
+ .description('Stream server logs')
50
+ .option('-n, --lines <number>', 'Initial lines to show', '50')
51
+ .action(logs);
52
+
53
+ program
54
+ .command('chat')
55
+ .description('Interactive chat REPL')
56
+ .option('-t, --task <slug>', 'Monkey task slug (e.g. inventur)')
57
+ .option('-p, --public', 'Public chat (no auth, no tools)')
58
+ .action(chat);
59
+
60
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "taskmonkey-cli",
3
+ "version": "0.1.0",
4
+ "description": "TaskMonkey CLI — Remote dev tools for tenant config editing and tool testing",
5
+ "bin": {
6
+ "tm": "./bin/tm.js",
7
+ "taskmonkey": "./bin/tm.js"
8
+ },
9
+ "type": "module",
10
+ "scripts": {
11
+ "start": "node bin/tm.js"
12
+ },
13
+ "dependencies": {
14
+ "chalk": "^5.3.0",
15
+ "chokidar": "^3.6.0",
16
+ "commander": "^12.0.0",
17
+ "eventsource": "^2.0.2",
18
+ "inquirer": "^9.2.0",
19
+ "ora": "^7.0.0"
20
+ },
21
+ "engines": {
22
+ "node": ">=18"
23
+ }
24
+ }
package/skills/chat.md ADDED
@@ -0,0 +1,18 @@
1
+ ---
2
+ description: Interaktiven Chat im Terminal starten
3
+ ---
4
+
5
+ Starte einen interaktiven Chat im Terminal. Der User kann optional einen Task angeben.
6
+
7
+ ```bash
8
+ # Unified Chat (alle Tools)
9
+ tm chat
10
+
11
+ # Spezifischer Monkey Task
12
+ tm chat --task inventur
13
+
14
+ # Öffentlicher Chat (keine Auth, keine internen Tools)
15
+ tm chat --public
16
+ ```
17
+
18
+ Frage den User welchen Modus er möchte, falls nicht angegeben.
package/skills/logs.md ADDED
@@ -0,0 +1,12 @@
1
+ ---
2
+ description: Server-Logs live streamen
3
+ ---
4
+
5
+ Streame die Server-Logs in Echtzeit. Nützlich zum Debuggen nach Config-Änderungen.
6
+
7
+ ```bash
8
+ tm logs
9
+ ```
10
+
11
+ Die Logs werden farbig angezeigt (Fehler=rot, Warnungen=gelb, Erfolg=grün).
12
+ Ctrl+C zum Beenden.
package/skills/pull.md ADDED
@@ -0,0 +1,11 @@
1
+ ---
2
+ description: Config-Dateien vom Server herunterladen
3
+ ---
4
+
5
+ Führe `tm pull` aus um die aktuellen Config-Dateien vom Server herunterzuladen.
6
+
7
+ ```bash
8
+ tm pull
9
+ ```
10
+
11
+ Zeige dem User anschließend welche Dateien heruntergeladen wurden.
package/skills/sync.md ADDED
@@ -0,0 +1,11 @@
1
+ ---
2
+ description: Lokale Config-Dateien zum Server hochladen
3
+ ---
4
+
5
+ Führe `tm sync` aus um die lokalen Config-Dateien zum Server zu synchronisieren. Der Server prüft die PHP-Syntax und leert den Cache automatisch.
6
+
7
+ ```bash
8
+ tm sync
9
+ ```
10
+
11
+ Zeige dem User das Ergebnis. Bei Fehlern (PHP Syntax etc.) die betroffenen Dateien nennen.
@@ -0,0 +1,24 @@
1
+ ---
2
+ description: Ein Tool auf dem Server testen
3
+ ---
4
+
5
+ Führe ein Tool auf dem Server aus. Der User gibt den Tool-Namen und optionale Argumente an.
6
+
7
+ Wenn der User nur `/test-tool` ohne Argumente sagt:
8
+ 1. Frage nach dem Tool-Namen
9
+ 2. Frage nach den Argumenten (key=value Format)
10
+
11
+ Dann ausführen:
12
+ ```bash
13
+ tm test-tool <toolName> [key=value ...]
14
+ ```
15
+
16
+ Beispiele:
17
+ ```bash
18
+ tm test-tool searchItems searchSku=bodo
19
+ tm test-tool getStockBySku skus='["X0047"]'
20
+ tm test-tool matchDeliveryItems --dry-run
21
+ ```
22
+
23
+ Für `--dry-run` (Simulation ohne echte Buchung) das Flag anhängen.
24
+ Zeige dem User das Ergebnis übersichtlich formatiert.
@@ -0,0 +1,173 @@
1
+ import { createInterface } from 'readline';
2
+ import chalk from 'chalk';
3
+ import { loadConfig } from '../config.js';
4
+
5
+ const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
6
+
7
+ export async function chat(options) {
8
+ const config = loadConfig();
9
+ if (!config) {
10
+ console.error(chalk.red('Not logged in. Run `tm login` first.'));
11
+ process.exit(1);
12
+ }
13
+
14
+ const task = options.task || null;
15
+ const isPublic = options.public || false;
16
+ let chatId;
17
+ if (task) chatId = `task_${task}_cli_${Date.now()}`;
18
+ else if (isPublic) chatId = `public_cli_${Date.now()}`;
19
+ else chatId = `unified_cli_${Date.now()}`;
20
+
21
+ const label = task ? `(${task})` : isPublic ? '(public)' : '(unified)';
22
+ console.log(chalk.cyan('💬 Chat'), chalk.gray(label));
23
+ console.log(chalk.gray(' Empty line to quit.\n'));
24
+
25
+ await sendMessage('', config, chatId);
26
+
27
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
28
+
29
+ const prompt = () => {
30
+ rl.question(chalk.green('\n> '), async (input) => {
31
+ if (input.trim() === '') {
32
+ rl.close();
33
+ process.exit(0);
34
+ }
35
+ await sendMessage(input.trim(), config, chatId);
36
+ prompt();
37
+ });
38
+ };
39
+
40
+ prompt();
41
+ }
42
+
43
+ function trunc(obj, max = 140) {
44
+ const s = typeof obj === 'string' ? obj : JSON.stringify(obj);
45
+ return s.length <= max ? s : s.substring(0, max) + '…';
46
+ }
47
+
48
+ async function sendMessage(message, config, chatId) {
49
+ const params = new URLSearchParams({ tenant: config.tenant, chat_id: chatId, message });
50
+ const url = `${config.server}/chat/stream?${params}`;
51
+
52
+ let si = null, sf = 0, st = 'Thinking';
53
+ const spin = (text) => {
54
+ st = text || st;
55
+ if (!si) si = setInterval(() => {
56
+ process.stdout.write(`\r ${chalk.cyan(SPINNER[sf++ % SPINNER.length])} ${chalk.gray(st)} `);
57
+ }, 80);
58
+ };
59
+ const stop = () => {
60
+ if (si) { clearInterval(si); si = null; process.stdout.write('\r' + ' '.repeat(50) + '\r'); }
61
+ };
62
+
63
+ try {
64
+ const headers = { 'Accept': 'text/event-stream' };
65
+ if (config.token && !chatId.startsWith('public_')) {
66
+ headers['Authorization'] = `Bearer ${config.token}`;
67
+ }
68
+
69
+ spin('Thinking');
70
+ const response = await fetch(url, { headers });
71
+
72
+ if (!response.ok) {
73
+ stop();
74
+ console.error(chalk.red(`HTTP ${response.status}`));
75
+ return;
76
+ }
77
+
78
+ const reader = response.body.getReader();
79
+ const decoder = new TextDecoder();
80
+ let buf = '', evt = 'message', msg = '';
81
+
82
+ while (true) {
83
+ const { done, value } = await reader.read();
84
+ if (done) break;
85
+
86
+ buf += decoder.decode(value, { stream: true });
87
+ const lines = buf.split('\n');
88
+ buf = lines.pop();
89
+
90
+ for (const line of lines) {
91
+ if (line.startsWith('event: ')) {
92
+ evt = line.substring(7).trim();
93
+ } else if (line.startsWith('data: ')) {
94
+ const raw = line.substring(6);
95
+ if (raw === '[DONE]') continue;
96
+
97
+ try {
98
+ const d = JSON.parse(raw);
99
+
100
+ switch (evt) {
101
+ case 'message': {
102
+ const c = d.content || d.message || '';
103
+ if (c) { stop(); msg += stripHtml(c); }
104
+ break;
105
+ }
106
+ case 'status': {
107
+ const s = d.status || d.message || '';
108
+ if (s) spin(s);
109
+ break;
110
+ }
111
+ case 'tool_start': {
112
+ stop();
113
+ const name = d.tool || d.name || '?';
114
+ const args = d.args || d.arguments || null;
115
+ let argStr = '';
116
+ if (args && Object.keys(args).length > 0) {
117
+ argStr = ' ' + chalk.gray(trunc(args, 100));
118
+ }
119
+ console.log(chalk.yellow(` ▶ ${name}`) + argStr);
120
+ spin(name);
121
+ break;
122
+ }
123
+ case 'tool_complete': {
124
+ stop();
125
+ const name = d.tool || d.name || '?';
126
+ const ok = d.success !== false;
127
+ console.log(ok ? chalk.green(` ✓ ${name}`) : chalk.red(` ✗ ${name}`));
128
+ spin('Thinking');
129
+ break;
130
+ }
131
+ case 'suggestions': {
132
+ const sug = d.suggestions;
133
+ if (sug?.length) msg += '\n' + sug.map(s => chalk.bgGray.white(` ${s} `)).join(' ');
134
+ break;
135
+ }
136
+ case 'error':
137
+ stop();
138
+ console.error(chalk.red(` ✗ ${d.message || JSON.stringify(d)}`));
139
+ break;
140
+ }
141
+ } catch {}
142
+ }
143
+ }
144
+ }
145
+
146
+ stop();
147
+ if (msg) console.log(compact(msg));
148
+
149
+ } catch (err) {
150
+ stop();
151
+ console.error(chalk.red(err.message));
152
+ }
153
+ }
154
+
155
+ function stripHtml(h) {
156
+ return h
157
+ .replace(/<br\s*\/?>/gi, '\n')
158
+ .replace(/<\/p>\s*<p[^>]*>/gi, '\n')
159
+ .replace(/<\/?(?:p|div|h[1-6])[^>]*>/gi, '')
160
+ .replace(/<li[^>]*>/gi, '\n • ')
161
+ .replace(/<\/li>/gi, '')
162
+ .replace(/<\/?(?:ul|ol)[^>]*>/gi, '')
163
+ .replace(/<strong>(.*?)<\/strong>/gi, (_, t) => chalk.bold(t))
164
+ .replace(/<em>(.*?)<\/em>/gi, (_, t) => chalk.italic(t))
165
+ .replace(/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/gi, (_, u, t) => `${t} ${chalk.gray(`(${u})`)}`)
166
+ .replace(/<[^>]+>/g, '')
167
+ .replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>')
168
+ .replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&nbsp;/g, ' ');
169
+ }
170
+
171
+ function compact(t) {
172
+ return t.replace(/\n{2,}/g, '\n').replace(/^\n+/, '').replace(/\n+$/, '');
173
+ }
@@ -0,0 +1,50 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+ import { initConfig } from '../config.js';
4
+
5
+ export async function login(options) {
6
+ const server = options.server || 'https://gpt.meco-media.com';
7
+
8
+ const { email, password } = await inquirer.prompt([
9
+ { type: 'input', name: 'email', message: 'E-Mail:' },
10
+ { type: 'password', name: 'password', message: 'Passwort:', mask: '*' },
11
+ ]);
12
+
13
+ const res = await fetch(`${server}/api/users/login`, {
14
+ method: 'POST',
15
+ headers: { 'Content-Type': 'application/json' },
16
+ body: JSON.stringify({ email, password }),
17
+ });
18
+
19
+ const data = await res.json();
20
+
21
+ if (!res.ok || data.success === false) {
22
+ console.error(chalk.red('Login fehlgeschlagen:'), data.message || 'Unbekannter Fehler');
23
+ process.exit(1);
24
+ }
25
+
26
+ const tenants = data.tenants || [];
27
+
28
+ if (tenants.length === 0) {
29
+ console.error(chalk.red('Keine Tenants verfügbar'));
30
+ process.exit(1);
31
+ }
32
+
33
+ let tenant;
34
+ if (tenants.length === 1) {
35
+ tenant = tenants[0].code;
36
+ console.log(chalk.gray(`Tenant: ${tenant}`));
37
+ } else {
38
+ const { selectedTenant } = await inquirer.prompt([{
39
+ type: 'list',
40
+ name: 'selectedTenant',
41
+ message: 'Tenant wählen:',
42
+ choices: tenants.map(t => ({ name: `${t.name} (${t.code})`, value: t.code })),
43
+ }]);
44
+ tenant = selectedTenant;
45
+ }
46
+
47
+ const configPath = initConfig(server, tenant, data.token, data.refresh_token);
48
+ console.log(chalk.green('✓'), `Eingeloggt als ${data.user.fullname || email}`);
49
+ console.log(chalk.gray(` Config: ${configPath}`));
50
+ }
@@ -0,0 +1,63 @@
1
+ import chalk from 'chalk';
2
+ import EventSource from 'eventsource';
3
+ import { createClient } from '../lib/api.js';
4
+ import { loadConfig } from '../config.js';
5
+
6
+ export async function logs(options) {
7
+ const config = loadConfig();
8
+ if (!config) {
9
+ console.error(chalk.red('Not logged in. Run `tm login` first.'));
10
+ process.exit(1);
11
+ }
12
+
13
+ const lines = options.lines || 50;
14
+ const url = `${config.server}/api/logs/stream?tenant=${config.tenant}&lines=${lines}`;
15
+
16
+ console.log(chalk.cyan('📋 Streaming logs'), chalk.gray(`(${config.tenant})`));
17
+ console.log(chalk.gray(' Ctrl+C to stop\n'));
18
+
19
+ const es = new EventSource(url, {
20
+ headers: { 'Authorization': `Bearer ${config.token}` },
21
+ });
22
+
23
+ es.onmessage = (e) => {
24
+ try {
25
+ const data = JSON.parse(e.data);
26
+ const line = data.line || '';
27
+ const type = data.type || 'info';
28
+
29
+ switch (type) {
30
+ case 'error':
31
+ console.log(chalk.red(line));
32
+ break;
33
+ case 'warning':
34
+ console.log(chalk.yellow(line));
35
+ break;
36
+ case 'success':
37
+ console.log(chalk.green(line));
38
+ break;
39
+ default:
40
+ console.log(chalk.gray(line));
41
+ }
42
+ } catch {
43
+ console.log(chalk.gray(e.data));
44
+ }
45
+ };
46
+
47
+ es.addEventListener('init_complete', () => {
48
+ console.log(chalk.gray('--- live ---\n'));
49
+ });
50
+
51
+ es.onerror = (err) => {
52
+ if (es.readyState === EventSource.CLOSED) {
53
+ console.log(chalk.gray('\nConnection closed.'));
54
+ process.exit(0);
55
+ }
56
+ };
57
+
58
+ process.on('SIGINT', () => {
59
+ es.close();
60
+ console.log(chalk.gray('\nStopped.'));
61
+ process.exit(0);
62
+ });
63
+ }
@@ -0,0 +1,165 @@
1
+ import { mkdirSync, writeFileSync, existsSync, readdirSync, readFileSync } from 'fs';
2
+ import { join, dirname, resolve } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import chalk from 'chalk';
5
+ import ora from 'ora';
6
+ import { loadConfig } from '../config.js';
7
+ import { createClient } from '../lib/api.js';
8
+
9
+ export async function pull() {
10
+ const config = loadConfig();
11
+ if (!config) {
12
+ console.error(chalk.red('Not logged in. Run `tm login` first.'));
13
+ process.exit(1);
14
+ }
15
+
16
+ const client = createClient();
17
+ const tenantDir = join(config._configDir, config.tenant_path || '.');
18
+
19
+ const spinner = ora('Downloading config...').start();
20
+
21
+ try {
22
+ // Download all file contents
23
+ const contents = await client.get('/api/tenant/pull');
24
+
25
+ // Also download docs
26
+ const docs = await client.get('/api/tenant/docs').catch(() => null);
27
+
28
+ spinner.stop();
29
+
30
+ let written = 0;
31
+ for (const [relativePath, content] of Object.entries(contents.files || {})) {
32
+ const fullPath = join(tenantDir, relativePath);
33
+ mkdirSync(dirname(fullPath), { recursive: true });
34
+ writeFileSync(fullPath, content);
35
+ console.log(chalk.gray(` ${relativePath}`));
36
+ written++;
37
+ }
38
+
39
+ // Write docs if available
40
+ if (docs?.files) {
41
+ const docsDir = join(config._configDir, 'docs');
42
+ mkdirSync(docsDir, { recursive: true });
43
+ for (const [name, content] of Object.entries(docs.files)) {
44
+ writeFileSync(join(docsDir, name), content);
45
+ console.log(chalk.gray(` docs/${name}`));
46
+ written++;
47
+ }
48
+ }
49
+
50
+ // Create CLAUDE.md if it doesn't exist
51
+ const claudeMdPath = join(config._configDir, 'CLAUDE.md');
52
+ if (!existsSync(claudeMdPath)) {
53
+ writeFileSync(claudeMdPath, generateClaudeMd(config.tenant));
54
+ console.log(chalk.cyan(` CLAUDE.md (created)`));
55
+ written++;
56
+ }
57
+
58
+ // Install Claude Code skills to .claude/commands/
59
+ const skillsInstalled = installSkills(config._configDir);
60
+ written += skillsInstalled;
61
+
62
+ console.log(chalk.green('✓'), `${written} files downloaded`);
63
+ } catch (err) {
64
+ spinner.fail(err.message);
65
+ process.exit(1);
66
+ }
67
+ }
68
+
69
+ function installSkills(targetDir) {
70
+ const commandsDir = join(targetDir, '.claude', 'commands');
71
+ mkdirSync(commandsDir, { recursive: true });
72
+
73
+ // Find skills directory relative to this file
74
+ const __filename = fileURLToPath(import.meta.url);
75
+ const skillsDir = resolve(dirname(__filename), '..', '..', 'skills');
76
+
77
+ if (!existsSync(skillsDir)) return 0;
78
+
79
+ let count = 0;
80
+ const files = readdirSync(skillsDir).filter(f => f.endsWith('.md'));
81
+ for (const file of files) {
82
+ const content = readFileSync(join(skillsDir, file), 'utf8');
83
+ writeFileSync(join(commandsDir, file), content);
84
+ console.log(chalk.cyan(` .claude/commands/${file}`));
85
+ count++;
86
+ }
87
+ return count;
88
+ }
89
+
90
+ function generateClaudeMd(tenant) {
91
+ return `# TaskMonkey Tenant: ${tenant}
92
+
93
+ ## Was ist das?
94
+
95
+ Dieses Verzeichnis enthält die Konfiguration für den Tenant **${tenant}** der TaskMonkey-Plattform.
96
+ Du kannst hier Prompts, Tools und Einstellungen bearbeiten und sie mit dem \`tm\` CLI auf den Server synchronisieren.
97
+
98
+ ## Verzeichnisstruktur
99
+
100
+ \`\`\`
101
+ .tmrc # Login-Daten (nicht committen!)
102
+ CLAUDE.md # Diese Datei
103
+ docs/ # Dokumentation (TenantConfig.md etc.)
104
+ monkey_tasks/ # Monkey Task Definitionen (Name, Tools, Prompts)
105
+ prompts/ # Allgemeine Prompts (System, First Message)
106
+ tools/ # Tool-Konfigurationen
107
+ jtl/ # JTL-spezifische Tools
108
+ lieferschein/ # Lieferschein/Wareneingang Tools
109
+ google_sheets/ # Google Sheets Tools
110
+ support/ # Support Tools
111
+ scan.php # Barcode/QR Scanner Konfiguration
112
+ apis.php # API-Verbindungen (JTL etc.)
113
+ \`\`\`
114
+
115
+ ## CLI Befehle
116
+
117
+ \`\`\`bash
118
+ tm login # Einloggen und Tenant wählen
119
+ tm pull # Config-Dateien vom Server herunterladen
120
+ tm sync # Lokale Dateien zum Server hochladen
121
+ tm watch # Auto-Sync bei Dateiänderung
122
+ tm test-tool <name> [key=value ...] # Tool auf dem Server testen
123
+ tm test-tool <name> --dry-run # Tool im Simulationsmodus testen
124
+ tm logs # Server-Logs live streamen
125
+ \`\`\`
126
+
127
+ ## Workflow
128
+
129
+ 1. **Datei bearbeiten** — z.B. einen Prompt in \`monkey_tasks/inventur.php\` ändern
130
+ 2. **Synchronisieren** — \`tm sync\` oder \`tm watch\` für Auto-Sync
131
+ 3. **Testen** — Im Chat ausprobieren oder \`tm test-tool\` für direktes Tool-Testing
132
+ 4. **Logs prüfen** — \`tm logs\` bei Problemen
133
+
134
+ ## Config-Format
135
+
136
+ Jede .php Datei gibt ein Array zurück mit Dot-Notation Keys:
137
+
138
+ \`\`\`php
139
+ <?php
140
+ return [
141
+ 'monkey_tasks.inventur' => [
142
+ 'name' => 'Inventur',
143
+ 'description' => 'Lagerbestand erfassen',
144
+ 'icon' => '✅',
145
+ 'tools' => ['searchItems', 'updateStock', ...],
146
+ ],
147
+ 'monkey_tasks.inventur.prompt' => <<<'PROMPT'
148
+ Du bist der Inventur-Assistent...
149
+ PROMPT,
150
+ ];
151
+ \`\`\`
152
+
153
+ Siehe \`docs/TenantConfig.md\` für die vollständige Dokumentation aller Config-Keys.
154
+
155
+ ## Wichtige Regeln
156
+
157
+ - **Nur .php Dateien** werden synchronisiert
158
+ - **PHP-Syntax wird geprüft** vor dem Schreiben auf dem Server
159
+ - **\`.tmrc\` nie committen** — enthält Login-Token
160
+ - **Prompts** definieren das Verhalten der KI im Chat
161
+ - **Tools** definieren welche Aktionen die KI ausführen kann
162
+ - **\`internal: true\`** bei Tools bedeutet: nur für Monkey Tasks, nicht für öffentliche Chats
163
+ `;
164
+ }
165
+
@@ -0,0 +1,71 @@
1
+ import { readdirSync, readFileSync, statSync } from 'fs';
2
+ import { join, relative } from 'path';
3
+ import chalk from 'chalk';
4
+ import ora from 'ora';
5
+ import { loadConfig } from '../config.js';
6
+ import { createClient } from '../lib/api.js';
7
+
8
+ function collectFiles(dir, base = dir) {
9
+ const files = {};
10
+ const entries = readdirSync(dir, { withFileTypes: true });
11
+
12
+ for (const entry of entries) {
13
+ const fullPath = join(dir, entry.name);
14
+ if (entry.isDirectory()) {
15
+ Object.assign(files, collectFiles(fullPath, base));
16
+ } else if (entry.isFile() && entry.name.endsWith('.php')) {
17
+ const relPath = relative(base, fullPath);
18
+ files[relPath] = readFileSync(fullPath, 'utf8');
19
+ }
20
+ }
21
+
22
+ return files;
23
+ }
24
+
25
+ export async function sync() {
26
+ const config = loadConfig();
27
+ if (!config) {
28
+ console.error(chalk.red('Not logged in. Run `tm login` first.'));
29
+ process.exit(1);
30
+ }
31
+
32
+ const tenantDir = join(config._configDir, config.tenant_path || '.');
33
+
34
+ const spinner = ora('Collecting files...').start();
35
+
36
+ const files = collectFiles(tenantDir);
37
+ const fileCount = Object.keys(files).length;
38
+
39
+ if (fileCount === 0) {
40
+ spinner.warn('No .php files found');
41
+ return;
42
+ }
43
+
44
+ spinner.text = `Syncing ${fileCount} files...`;
45
+
46
+ try {
47
+ const client = createClient();
48
+ const result = await client.post('/api/tenant/sync', { files });
49
+
50
+ spinner.stop();
51
+
52
+ if (result.success) {
53
+ console.log(chalk.green('✓'), `${result.written} files synced`);
54
+ for (const file of result.files || []) {
55
+ console.log(chalk.gray(` ${file}`));
56
+ }
57
+ } else {
58
+ console.log(chalk.yellow('⚠'), `${result.written || 0} files synced, ${result.errors?.length || 0} errors`);
59
+ }
60
+
61
+ if (result.errors?.length > 0) {
62
+ console.log(chalk.red('\nErrors:'));
63
+ for (const err of result.errors) {
64
+ console.log(chalk.red(` ${err.path}: ${err.error}`));
65
+ }
66
+ }
67
+ } catch (err) {
68
+ spinner.fail(err.message);
69
+ process.exit(1);
70
+ }
71
+ }
@@ -0,0 +1,67 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { createClient } from '../lib/api.js';
4
+
5
+ export async function testTool(toolName, args, options) {
6
+ const client = createClient();
7
+
8
+ // Parse key=value args
9
+ const toolArgs = {};
10
+ for (const arg of args) {
11
+ const eq = arg.indexOf('=');
12
+ if (eq > 0) {
13
+ const key = arg.substring(0, eq);
14
+ let val = arg.substring(eq + 1);
15
+ // Auto-convert types
16
+ if (val === 'true') val = true;
17
+ else if (val === 'false') val = false;
18
+ else if (val === 'null') val = null;
19
+ else if (/^\d+$/.test(val)) val = parseInt(val, 10);
20
+ else if (/^\d+\.\d+$/.test(val)) val = parseFloat(val);
21
+ toolArgs[key] = val;
22
+ }
23
+ }
24
+
25
+ const spinner = ora(`Running ${toolName}...`).start();
26
+
27
+ try {
28
+ const result = await client.post('/api/tools/run', {
29
+ tool: toolName,
30
+ args: toolArgs,
31
+ dry_run: options.dryRun || false,
32
+ });
33
+
34
+ spinner.stop();
35
+
36
+ if (options.json) {
37
+ console.log(JSON.stringify(result, null, 2));
38
+ return;
39
+ }
40
+
41
+ // Formatted output
42
+ const success = result.success;
43
+ const icon = success ? chalk.green('✓') : chalk.red('✗');
44
+ console.log(`${icon} ${toolName} (${result.duration_ms}ms)`);
45
+
46
+ // Show log entries if present
47
+ const log = result.result?.log || result.result?._logger?.log || [];
48
+ if (log.length > 0) {
49
+ console.log(chalk.gray('─'.repeat(40)));
50
+ for (const line of log) {
51
+ console.log(chalk.gray(' ' + line));
52
+ }
53
+ }
54
+
55
+ // Show result (without log/logger)
56
+ const cleanResult = { ...result.result };
57
+ delete cleanResult.log;
58
+ delete cleanResult._logger;
59
+
60
+ console.log(chalk.gray('─'.repeat(40)));
61
+ console.log(JSON.stringify(cleanResult, null, 2));
62
+
63
+ } catch (err) {
64
+ spinner.fail(err.message);
65
+ process.exit(1);
66
+ }
67
+ }
@@ -0,0 +1,46 @@
1
+ import chokidar from 'chokidar';
2
+ import chalk from 'chalk';
3
+ import { loadConfig } from '../config.js';
4
+ import { sync } from './sync.js';
5
+ import { join } from 'path';
6
+
7
+ export async function watch() {
8
+ const config = loadConfig();
9
+ if (!config) {
10
+ console.error(chalk.red('Not logged in. Run `tm login` first.'));
11
+ process.exit(1);
12
+ }
13
+
14
+ const tenantDir = join(config._configDir, config.tenant_path || '.');
15
+
16
+ console.log(chalk.cyan('👀 Watching'), tenantDir);
17
+ console.log(chalk.gray(' Ctrl+C to stop\n'));
18
+
19
+ let debounceTimer = null;
20
+
21
+ const watcher = chokidar.watch(join(tenantDir, '**/*.php'), {
22
+ ignoreInitial: true,
23
+ awaitWriteFinish: { stabilityThreshold: 300 },
24
+ });
25
+
26
+ watcher.on('all', (event, path) => {
27
+ console.log(chalk.gray(` ${event}: ${path.replace(tenantDir + '/', '')}`));
28
+
29
+ // Debounce: wait 500ms after last change
30
+ if (debounceTimer) clearTimeout(debounceTimer);
31
+ debounceTimer = setTimeout(async () => {
32
+ try {
33
+ await sync();
34
+ } catch (err) {
35
+ console.error(chalk.red(` Sync error: ${err.message}`));
36
+ }
37
+ }, 500);
38
+ });
39
+
40
+ // Keep process alive
41
+ process.on('SIGINT', () => {
42
+ watcher.close();
43
+ console.log(chalk.gray('\nStopped.'));
44
+ process.exit(0);
45
+ });
46
+ }
package/src/config.js ADDED
@@ -0,0 +1,50 @@
1
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ const CONFIG_FILE = '.tmrc';
5
+
6
+ export function findConfig(startDir = process.cwd()) {
7
+ let dir = startDir;
8
+ while (dir !== '/') {
9
+ const configPath = join(dir, CONFIG_FILE);
10
+ if (existsSync(configPath)) {
11
+ return { path: configPath, dir };
12
+ }
13
+ dir = join(dir, '..');
14
+ }
15
+ return null;
16
+ }
17
+
18
+ export function loadConfig() {
19
+ const found = findConfig();
20
+ if (!found) return null;
21
+
22
+ try {
23
+ const raw = readFileSync(found.path, 'utf8');
24
+ const config = JSON.parse(raw);
25
+ config._configDir = found.dir;
26
+ config._configPath = found.path;
27
+ return config;
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ export function saveConfig(data) {
34
+ const path = data._configPath || join(process.cwd(), CONFIG_FILE);
35
+ const { _configDir, _configPath, ...clean } = data;
36
+ writeFileSync(path, JSON.stringify(clean, null, 2) + '\n');
37
+ }
38
+
39
+ export function initConfig(server, tenant, token, refreshToken) {
40
+ const data = {
41
+ server,
42
+ tenant,
43
+ token,
44
+ refresh_token: refreshToken,
45
+ tenant_path: '.',
46
+ };
47
+ const path = join(process.cwd(), CONFIG_FILE);
48
+ writeFileSync(path, JSON.stringify(data, null, 2) + '\n');
49
+ return path;
50
+ }
package/src/lib/api.js ADDED
@@ -0,0 +1,101 @@
1
+ import { loadConfig, saveConfig } from '../config.js';
2
+
3
+ export class ApiClient {
4
+ constructor(config) {
5
+ this.server = config.server;
6
+ this.tenant = config.tenant;
7
+ this.token = config.token;
8
+ this.refreshToken = config.refresh_token;
9
+ this._config = config;
10
+ }
11
+
12
+ async get(path, params = {}) {
13
+ const url = new URL(path, this.server);
14
+ url.searchParams.set('tenant', this.tenant);
15
+ for (const [k, v] of Object.entries(params)) {
16
+ url.searchParams.set(k, v);
17
+ }
18
+
19
+ let res = await fetch(url.toString(), { headers: this._headers() });
20
+
21
+ if (res.status === 401) {
22
+ const refreshed = await this._refresh();
23
+ if (refreshed) {
24
+ res = await fetch(url.toString(), { headers: this._headers() });
25
+ }
26
+ }
27
+
28
+ if (!res.ok) {
29
+ const body = await res.text();
30
+ throw new Error(`HTTP ${res.status}: ${body}`);
31
+ }
32
+
33
+ return res.json();
34
+ }
35
+
36
+ async post(path, body = {}) {
37
+ const url = new URL(path, this.server);
38
+ url.searchParams.set('tenant', this.tenant);
39
+
40
+ let res = await fetch(url.toString(), {
41
+ method: 'POST',
42
+ headers: this._headers(),
43
+ body: JSON.stringify(body),
44
+ });
45
+
46
+ if (res.status === 401) {
47
+ const refreshed = await this._refresh();
48
+ if (refreshed) {
49
+ res = await fetch(url.toString(), {
50
+ method: 'POST',
51
+ headers: this._headers(),
52
+ body: JSON.stringify(body),
53
+ });
54
+ }
55
+ }
56
+
57
+ if (!res.ok) {
58
+ const text = await res.text();
59
+ throw new Error(`HTTP ${res.status}: ${text}`);
60
+ }
61
+
62
+ return res.json();
63
+ }
64
+
65
+ async _refresh() {
66
+ if (!this.refreshToken) return false;
67
+
68
+ try {
69
+ const url = new URL('/api/users/refresh', this.server);
70
+ const res = await fetch(url.toString(), {
71
+ method: 'POST',
72
+ headers: { 'Content-Type': 'application/json' },
73
+ body: JSON.stringify({ refresh_token: this.refreshToken }),
74
+ });
75
+
76
+ if (res.ok) {
77
+ const data = await res.json();
78
+ this.token = data.token;
79
+ this._config.token = data.token;
80
+ saveConfig(this._config);
81
+ return true;
82
+ }
83
+ } catch {}
84
+ return false;
85
+ }
86
+
87
+ _headers() {
88
+ return {
89
+ 'Authorization': `Bearer ${this.token}`,
90
+ 'Content-Type': 'application/json',
91
+ };
92
+ }
93
+ }
94
+
95
+ export function createClient() {
96
+ const config = loadConfig();
97
+ if (!config) {
98
+ throw new Error('Not logged in. Run `tm login` first.');
99
+ }
100
+ return new ApiClient(config);
101
+ }