tmpa-cli 1.0.10 → 1.0.12

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.
Files changed (2) hide show
  1. package/bin/index.js +202 -82
  2. package/package.json +1 -1
package/bin/index.js CHANGED
@@ -6,7 +6,15 @@ const path = require('path');
6
6
  const os = require('os');
7
7
  const { execSync } = require('child_process');
8
8
 
9
+ const TMPA_DIR = path.join(os.homedir(), '.tmpa');
10
+ const SKILLS_DIR = path.join(TMPA_DIR, 'skills');
11
+ const MCP_DIR = path.join(TMPA_DIR, 'mcp');
9
12
  const CONFIG_FILE = path.join(os.homedir(), '.tmpa_config.json');
13
+ const REGISTRY_FILE = path.join(TMPA_DIR, 'registry.json');
14
+
15
+ if (!fs.existsSync(TMPA_DIR)) fs.mkdirSync(TMPA_DIR, { recursive: true });
16
+ if (!fs.existsSync(SKILLS_DIR)) fs.mkdirSync(SKILLS_DIR, { recursive: true });
17
+ if (!fs.existsSync(MCP_DIR)) fs.mkdirSync(MCP_DIR, { recursive: true });
10
18
 
11
19
  const C = {
12
20
  reset: '\x1b[0m',
@@ -34,11 +42,7 @@ const PROVIDERS = {
34
42
 
35
43
  function loadConfig() {
36
44
  if (fs.existsSync(CONFIG_FILE)) {
37
- try {
38
- return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
39
- } catch (e) {
40
- return {};
41
- }
45
+ try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } catch (e) { return {}; }
42
46
  }
43
47
  return {};
44
48
  }
@@ -47,6 +51,21 @@ function saveConfig(config) {
47
51
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
48
52
  }
49
53
 
54
+ function loadRegistry() {
55
+ if (fs.existsSync(REGISTRY_FILE)) {
56
+ try { return JSON.parse(fs.readFileSync(REGISTRY_FILE, 'utf8')); } catch (e) { return { skills: {}, mcp: {} }; }
57
+ }
58
+ return { skills: {}, mcp: {} };
59
+ }
60
+
61
+ function saveRegistry(registry) {
62
+ fs.writeFileSync(REGISTRY_FILE, JSON.stringify(registry, null, 2));
63
+ }
64
+
65
+ let rl = readline.createInterface({ input: process.stdin, output: process.stdout });
66
+ let config = loadConfig();
67
+ let registry = loadRegistry();
68
+
50
69
  function showBanner() {
51
70
  console.clear();
52
71
  console.log(`
@@ -58,18 +77,11 @@ ${C.c4} ██║ ██║ ╚═╝ ██║██║ ██║ █
58
77
  ${C.c4} ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝${C.reset}
59
78
  ${C.darkGray}────────────────────────────────────────────────────────────${C.reset}
60
79
  ${C.reset}The Multi Platform AI ${C.green}[Interactive Mode]${C.reset}
61
- ${C.gray}/config : Set API | /models : Interactive Model Selector | /uninstall : Remove | /exit : Exit${C.reset}
80
+ ${C.gray}/config | /models | /skill | /mcp | /connect | /scan | /exit${C.reset}
62
81
  ${C.darkGray}────────────────────────────────────────────────────────────${C.reset}
63
82
  `);
64
83
  }
65
84
 
66
- let rl = readline.createInterface({
67
- input: process.stdin,
68
- output: process.stdout
69
- });
70
-
71
- let config = loadConfig();
72
-
73
85
  function askConfig(callback) {
74
86
  console.log(`\n${C.cyan}[+] Choose AI Provider:${C.reset}`);
75
87
  console.log(` ${C.yellow}1.${C.reset} OpenRouter`);
@@ -109,81 +121,79 @@ function askConfig(callback) {
109
121
  });
110
122
  }
111
123
 
112
- function renderInteractiveMenu(models, selectedIndex, pageOffset, pageSize) {
113
- console.clear();
114
- console.log(`${C.cyan}=== Select Model for ${config.provider} ===${C.reset}`);
115
- console.log(`${C.gray}Use Up/Down Arrow keys to navigate, press ENTER to select.${C.reset}`);
116
- console.log(`${C.darkGray}────────────────────────────────────────────────────────────${C.reset}`);
117
-
118
- const visibleModels = models.slice(pageOffset, pageOffset + pageSize);
119
-
120
- visibleModels.forEach((m, idx) => {
121
- const realIndex = pageOffset + idx;
122
- const isSelected = realIndex === selectedIndex;
123
- const bullet = isSelected ? `${C.green}●${C.reset}` : `${C.gray}○${C.reset}`;
124
- const modelText = isSelected ? `${C.green}${C.c1}${m.id}${C.reset}` : `${C.gray}${m.id}${C.reset}`;
125
- console.log(` ${bullet} ${modelText}`);
126
- });
124
+ function selectModelCLI(allModels) {
125
+ let filteredModels = [...allModels];
126
+ let currentPage = 0;
127
+ const pageSize = 12;
127
128
 
128
- console.log(`${C.darkGray}────────────────────────────────────────────────────────────${C.reset}`);
129
- console.log(`${C.yellow}Item ${selectedIndex + 1} of ${models.length}${C.reset} ${C.darkGray}| Current Active: ${config.model}${C.reset}\n`);
130
- }
129
+ function displayList() {
130
+ console.clear();
131
+ const totalPages = Math.ceil(filteredModels.length / pageSize) || 1;
132
+ if (currentPage >= totalPages) currentPage = totalPages - 1;
133
+ if (currentPage < 0) currentPage = 0;
131
134
 
132
- async function interactiveModelSelect(models) {
133
- let selectedIndex = 0;
134
- let pageOffset = 0;
135
- const pageSize = 10; // Menampilkan 10 item per halaman agar rapi
135
+ const startIdx = currentPage * pageSize;
136
+ const pageItems = filteredModels.slice(startIdx, startIdx + pageSize);
136
137
 
137
- // Matikan readline sementara untuk mengaktifkan keypress listener
138
- rl.close();
139
- readline.emitKeypressEvents(process.stdin);
140
- if (process.stdin.isTTY) process.stdin.setRawMode(true);
138
+ console.log(`${C.cyan}=== Models Selector for ${config.provider} (${filteredModels.length} models) ===${C.reset}`);
139
+ console.log(`${C.gray}Current Active: ${C.green}${config.model || 'None'}${C.reset}`);
140
+ console.log(`${C.darkGray}────────────────────────────────────────────────────────────${C.reset}`);
141
141
 
142
- renderInteractiveMenu(models, selectedIndex, pageOffset, pageSize);
142
+ if (pageItems.length === 0) {
143
+ console.log(`${C.red} No models found matching filter.${C.reset}`);
144
+ } else {
145
+ pageItems.forEach((m, idx) => {
146
+ const itemNum = startIdx + idx + 1;
147
+ const isCurrent = m.id === config.model;
148
+ const bullet = isCurrent ? `${C.green}●${C.reset}` : `${C.gray}○${C.reset}`;
149
+ console.log(` ${bullet} ${C.yellow}${itemNum.toString().padStart(3, ' ')}.${C.reset} ${m.id}`);
150
+ });
151
+ }
143
152
 
144
- return new Promise((resolve) => {
145
- const onKeypress = (str, key) => {
146
- if (key.name === 'up') {
147
- if (selectedIndex > 0) {
148
- selectedIndex--;
149
- if (selectedIndex < pageOffset) pageOffset--;
150
- }
151
- renderInteractiveMenu(models, selectedIndex, pageOffset, pageSize);
152
- } else if (key.name === 'down') {
153
- if (selectedIndex < models.length - 1) {
154
- selectedIndex++;
155
- if (selectedIndex >= pageOffset + pageSize) pageOffset++;
153
+ console.log(`${C.darkGray}────────────────────────────────────────────────────────────${C.reset}`);
154
+ console.log(`${C.cyan}Page ${currentPage + 1}/${totalPages}${C.reset} | ${C.gray}Commands: [n]ext | [p]rev | [f]ilter <keyword> | [q]uit${C.reset}`);
155
+
156
+ rl.question(`\n${C.yellow}[>] Enter Number / Model ID / Command:${C.reset} `, (answer) => {
157
+ const input = answer.trim();
158
+
159
+ if (input.toLowerCase() === 'n') {
160
+ if (currentPage < totalPages - 1) currentPage++;
161
+ displayList();
162
+ } else if (input.toLowerCase() === 'p') {
163
+ if (currentPage > 0) currentPage--;
164
+ displayList();
165
+ } else if (input.toLowerCase() === 'q' || input.toLowerCase() === 'exit') {
166
+ console.log(`${C.gray}Model selection cancelled.${C.reset}\n`);
167
+ startPrompt();
168
+ } else if (input.toLowerCase().startsWith('f ')) {
169
+ const query = input.slice(2).trim().toLowerCase();
170
+ filteredModels = allModels.filter(m => m.id.toLowerCase().includes(query));
171
+ currentPage = 0;
172
+ displayList();
173
+ } else if (!isNaN(input) && input !== '') {
174
+ const num = parseInt(input);
175
+ if (num >= 1 && num <= filteredModels.length) {
176
+ const selected = filteredModels[num - 1].id;
177
+ config.model = selected;
178
+ saveConfig(config);
179
+ console.log(`\n${C.green}[+] Model changed to: ${config.model}${C.reset}\n`);
180
+ startPrompt();
181
+ } else {
182
+ console.log(`${C.red}[x] Invalid number!${C.reset}`);
183
+ setTimeout(displayList, 1000);
156
184
  }
157
- renderInteractiveMenu(models, selectedIndex, pageOffset, pageSize);
158
- } else if (key.name === 'return' || key.name === 'enter') {
159
- cleanup();
160
- const chosen = models[selectedIndex].id;
161
- config.model = chosen;
185
+ } else if (input.length > 0) {
186
+ config.model = input;
162
187
  saveConfig(config);
163
- console.log(`\n${C.green}[+] Successfully switched to model: ${config.model}${C.reset}\n`);
164
- resolve();
165
- } else if (key.ctrl && key.name === 'c') {
166
- cleanup();
167
- process.exit(0);
168
- } else if (key.name === 'escape') {
169
- cleanup();
170
- console.log(`\n${C.gray}Model selection cancelled.${C.reset}\n`);
171
- resolve();
188
+ console.log(`\n${C.green}[+] Custom Model ID set to: ${config.model}${C.reset}\n`);
189
+ startPrompt();
190
+ } else {
191
+ startPrompt();
172
192
  }
173
- };
174
-
175
- function cleanup() {
176
- process.stdin.removeListener('keypress', onKeypress);
177
- if (process.stdin.isTTY) process.stdin.setRawMode(false);
178
- // Restart readline interface untuk percakapan selanjutnya
179
- rl = readline.createInterface({
180
- input: process.stdin,
181
- output: process.stdout
182
- });
183
- }
193
+ });
194
+ }
184
195
 
185
- process.stdin.on('keypress', onKeypress);
186
- });
196
+ displayList();
187
197
  }
188
198
 
189
199
  async function fetchAvailableModels() {
@@ -214,8 +224,7 @@ async function fetchAvailableModels() {
214
224
  const data = await response.json();
215
225
 
216
226
  if (data.data && Array.isArray(data.data) && data.data.length > 0) {
217
- await interactiveModelSelect(data.data);
218
- startPrompt();
227
+ selectModelCLI(data.data);
219
228
  } else {
220
229
  console.log(`${C.red}[x] Could not retrieve models list automatically from ${config.provider}.${C.reset}`);
221
230
  startPrompt();
@@ -300,6 +309,106 @@ function handleUninstall() {
300
309
  });
301
310
  }
302
311
 
312
+ function listSkills() {
313
+ console.log(`\n${C.cyan}=== Registered Skills ===${C.reset}`);
314
+ const keys = Object.keys(registry.skills || {});
315
+ if (keys.length === 0) {
316
+ console.log(`${C.gray}Belum ada skill yang terhubung. Gunakan ${C.yellow}/connect skill <path>${C.gray} untuk menghubungkan.${C.reset}\n`);
317
+ return;
318
+ }
319
+ keys.forEach((name, i) => {
320
+ const item = registry.skills[name];
321
+ console.log(` ${C.yellow}${i + 1}.${C.reset} ${C.green}${name}${C.reset} -> ${C.gray}${item.path}${C.reset} [${item.status || 'Active'}]`);
322
+ });
323
+ console.log('');
324
+ }
325
+
326
+ function listMCP() {
327
+ console.log(`\n${C.cyan}=== Registered MCP (Model Context Protocol) Servers ===${C.reset}`);
328
+ const keys = Object.keys(registry.mcp || {});
329
+ if (keys.length === 0) {
330
+ console.log(`${C.gray}Belum ada MCP server terhubung. Gunakan ${C.yellow}/connect mcp <target>${C.gray} untuk menghubungkan.${C.reset}\n`);
331
+ return;
332
+ }
333
+ keys.forEach((name, i) => {
334
+ const item = registry.mcp[name];
335
+ console.log(` ${C.yellow}${i + 1}.${C.reset} ${C.green}${name}${C.reset} -> ${C.gray}${item.target}${C.reset} [${item.type || 'local'}]`);
336
+ });
337
+ console.log('');
338
+ }
339
+
340
+ function connectResource(inputArgs) {
341
+ const parts = inputArgs.trim().split(/\s+/);
342
+ const type = parts[0]?.toLowerCase();
343
+ const targetPath = parts.slice(1).join(' ');
344
+
345
+ if (!type || !targetPath) {
346
+ console.log(`${C.red}[x] Format salah! Gunakan:${C.reset}`);
347
+ console.log(` ${C.yellow}/connect skill <filepath_atau_folder>${C.reset}`);
348
+ console.log(` ${C.yellow}/connect mcp <filepath_atau_url>${C.reset}\n`);
349
+ return;
350
+ }
351
+
352
+ const resolvedPath = path.resolve(targetPath);
353
+
354
+ if (type === 'skill') {
355
+ const skillName = path.basename(resolvedPath, path.extname(resolvedPath));
356
+ registry.skills = registry.skills || {};
357
+ registry.skills[skillName] = {
358
+ path: resolvedPath,
359
+ connectedAt: new Date().toISOString(),
360
+ status: 'Active'
361
+ };
362
+ saveRegistry(registry);
363
+ console.log(`${C.green}[+] Skill "${skillName}" berhasil dihubungkan dari: ${resolvedPath}${C.reset}\n`);
364
+ } else if (type === 'mcp') {
365
+ const mcpName = path.basename(resolvedPath, path.extname(resolvedPath));
366
+ registry.mcp = registry.mcp || {};
367
+ registry.mcp[mcpName] = {
368
+ target: targetPath,
369
+ type: targetPath.startsWith('http') ? 'remote' : 'local',
370
+ connectedAt: new Date().toISOString(),
371
+ status: 'Connected'
372
+ };
373
+ saveRegistry(registry);
374
+ console.log(`${C.green}[+] MCP Server "${mcpName}" berhasil dihubungkan!${C.reset}\n`);
375
+ } else {
376
+ console.log(`${C.red}[x] Tipe tidak dikenal. Gunakan "skill" atau "mcp".${C.reset}\n`);
377
+ }
378
+ }
379
+
380
+ function scanResources() {
381
+ console.log(`${C.yellow}[...] Memindai folder ~/.tmpa/skills dan ~/.tmpa/mcp...${C.reset}`);
382
+ registry = loadRegistry();
383
+
384
+ if (fs.existsSync(SKILLS_DIR)) {
385
+ const files = fs.readdirSync(SKILLS_DIR);
386
+ files.forEach(file => {
387
+ const fullPath = path.join(SKILLS_DIR, file);
388
+ const name = path.basename(file, path.extname(file));
389
+ if (!registry.skills[name]) {
390
+ registry.skills[name] = { path: fullPath, connectedAt: new Date().toISOString(), status: 'Active' };
391
+ console.log(`${C.green}[+] Auto-detected skill: ${name}${C.reset}`);
392
+ }
393
+ });
394
+ }
395
+
396
+ if (fs.existsSync(MCP_DIR)) {
397
+ const files = fs.readdirSync(MCP_DIR);
398
+ files.forEach(file => {
399
+ const fullPath = path.join(MCP_DIR, file);
400
+ const name = path.basename(file, path.extname(file));
401
+ if (!registry.mcp[name]) {
402
+ registry.mcp[name] = { target: fullPath, type: 'local', connectedAt: new Date().toISOString(), status: 'Connected' };
403
+ console.log(`${C.green}[+] Auto-detected MCP: ${name}${C.reset}`);
404
+ }
405
+ });
406
+ }
407
+
408
+ saveRegistry(registry);
409
+ console.log(`${C.green}[+] Pemindaian selesai.${C.reset}\n`);
410
+ }
411
+
303
412
  function startPrompt() {
304
413
  rl.question(`${C.c1}TMPA >${C.reset} `, (input) => {
305
414
  const cmd = input.trim();
@@ -314,6 +423,18 @@ function startPrompt() {
314
423
  askConfig(() => startPrompt());
315
424
  } else if (cmd === '/models') {
316
425
  fetchAvailableModels();
426
+ } else if (cmd === '/skill') {
427
+ listSkills();
428
+ startPrompt();
429
+ } else if (cmd === '/mcp') {
430
+ listMCP();
431
+ startPrompt();
432
+ } else if (cmd.startsWith('/connect')) {
433
+ connectResource(cmd.replace('/connect', ''));
434
+ startPrompt();
435
+ } else if (cmd === '/scan') {
436
+ scanResources();
437
+ startPrompt();
317
438
  } else if (cmd === '/uninstall') {
318
439
  handleUninstall();
319
440
  } else if (cmd === '') {
@@ -324,7 +445,6 @@ function startPrompt() {
324
445
  });
325
446
  }
326
447
 
327
- // Program Execution
328
448
  showBanner();
329
449
  if (!config.apiKey) {
330
450
  askConfig(() => startPrompt());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmpa-cli",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "bin": {