kentutai-app 1.0.9 → 1.1.1

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/cli/cli.js CHANGED
@@ -7,7 +7,6 @@ const os = require("os");
7
7
 
8
8
  const pkg = require("./package.json");
9
9
 
10
- // ─── Configuration ────────────────────────────────────────────────────
11
10
  const DEFAULT_PORT = 20123;
12
11
  const DEFAULT_HOST = "0.0.0.0";
13
12
 
@@ -17,7 +16,6 @@ let noBrowser = false;
17
16
  let showLog = false;
18
17
  let trayMode = false;
19
18
 
20
- // Parse arguments
21
19
  const args = process.argv.slice(2);
22
20
  for (let i = 0; i < args.length; i++) {
23
21
  if (args[i] === "--port" || args[i] === "-p") {
@@ -52,7 +50,6 @@ Options:
52
50
  }
53
51
  }
54
52
 
55
- // ─── Standalone server path ───────────────────────────────────────────
56
53
  const standaloneDir = path.join(__dirname, "app");
57
54
  const serverPath = path.join(standaloneDir, "server.js");
58
55
 
@@ -62,8 +59,6 @@ if (!fs.existsSync(serverPath)) {
62
59
  process.exit(1);
63
60
  }
64
61
 
65
- // ─── Helpers ──────────────────────────────────────────────────────────
66
-
67
62
  function openBrowser(url) {
68
63
  const cmd = process.platform === "darwin" ? "open"
69
64
  : process.platform === "win32" ? "start"
@@ -94,8 +89,6 @@ function killProcessOnPort(port) {
94
89
  });
95
90
  }
96
91
 
97
- // ─── Start Server ─────────────────────────────────────────────────────
98
-
99
92
  function startServer() {
100
93
  const displayHost = host === DEFAULT_HOST ? "localhost" : host;
101
94
  const url = `http://${displayHost}:${port}/dashboard`;
@@ -118,7 +111,6 @@ function startServer() {
118
111
  process.exit(code || 0);
119
112
  });
120
113
 
121
- // Cleanup on exit
122
114
  const cleanup = () => {
123
115
  try { server.kill(); } catch {}
124
116
  };
@@ -149,14 +141,11 @@ async function checkForUpdate() {
149
141
  } catch { return null; }
150
142
  }
151
143
 
152
- // ─── Main ─────────────────────────────────────────────────────────────
153
-
154
144
  async function main() {
155
145
  await killProcessOnPort(port);
156
146
 
157
147
  const { server, url } = startServer();
158
148
 
159
- // Tray mode: no interactive menu
160
149
  if (trayMode) {
161
150
  console.log(`\n ${pkg.name} v${pkg.version}`);
162
151
  console.log(` Server: ${url}`);
@@ -164,14 +153,11 @@ async function main() {
164
153
  return;
165
154
  }
166
155
 
167
- // Wait for server to be ready
168
156
  const { selectMenu, pause } = require("./src/cli/utils/input");
169
157
  const { clearScreen } = require("./src/cli/utils/display");
170
158
 
171
- // Wait a bit for server startup
172
159
  await new Promise(r => setTimeout(r, 3000));
173
160
 
174
- // If --no-browser, just show info and wait
175
161
  if (noBrowser) {
176
162
  console.log(`\n ${pkg.name} v${pkg.version}`);
177
163
  console.log(` Server: ${url}`);
@@ -208,12 +194,10 @@ async function main() {
208
194
  `\ud83d\ude80 Server: ${serverUrl}`
209
195
  );
210
196
 
211
- // Handle choice
212
197
  const adjustedChoice = latestVersion ? choice : choice + 1;
213
198
 
214
199
  if (adjustedChoice === 0 && latestVersion) {
215
200
  console.log("\n Updating...");
216
- const { execSync } = require("child_process");
217
201
  try {
218
202
  execSync(`npm install -g ${pkg.name}`, { stdio: "inherit" });
219
203
  console.log("\n Update complete! Please restart the application.");
@@ -251,39 +235,6 @@ async function main() {
251
235
  }
252
236
  }
253
237
  }
254
- } else if (adjustedChoice === 1 || (adjustedChoice === 0 && !latestVersion)) {
255
- // Web UI
256
- openBrowser(url);
257
- await pause("\nPress Enter to go back to menu...");
258
- } else if (adjustedChoice === 2) {
259
- // Terminal UI
260
- try {
261
- const { startTerminalUI } = require("./src/cli/terminalUI");
262
- await startTerminalUI(port);
263
- } catch (err) {
264
- console.error("\n Terminal UI error:", err.message);
265
- await pause("\nPress Enter to continue...");
266
- }
267
- } else if (adjustedChoice === 3) {
268
- // Hide to Tray
269
- clearScreen();
270
- console.log(`\n \ud83d\udd14 ${pkg.name} is running in background`);
271
- console.log(` Server: ${serverUrl}`);
272
- console.log(` PID: ${server.pid}`);
273
- console.log(`\n Press Ctrl+C to stop.\n`);
274
- // Keep process alive, remove stdin listeners
275
- process.stdin.removeAllListeners("keypress");
276
- if (process.stdin.isTTY) try { process.stdin.setRawMode(false); } catch {}
277
- return;
278
- } else {
279
- // Exit
280
- console.log("\nExiting...");
281
- server.kill();
282
- setTimeout(() => process.exit(0), 100);
283
- return;
284
- }
285
- }
286
- }
287
238
 
288
239
  main().catch((err) => {
289
240
  console.error("Fatal:", err.message);
@@ -0,0 +1,547 @@
1
+ const http = require("http");
2
+ const https = require("https");
3
+ const crypto = require("crypto");
4
+ const fs = require("node:fs");
5
+ const path = require("node:path");
6
+ const os = require("node:os");
7
+ const { machineIdSync } = require("node-machine-id");
8
+
9
+ // Default configuration
10
+ const DEFAULT_CONFIG = {
11
+ host: "localhost",
12
+ port: 20123,
13
+ protocol: "http:",
14
+ };
15
+
16
+ const CLI_TOKEN_HEADER = "x-9r-cli-token";
17
+ const CLI_TOKEN_SALT = "kt-cli-auth";
18
+ const APP_NAME = "kentutai";
19
+
20
+ function getDataDir() {
21
+ if (process.env.DATA_DIR) return process.env.DATA_DIR;
22
+ if (process.platform === "win32") {
23
+ return path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), APP_NAME);
24
+ }
25
+ return path.join(os.homedir(), `.${APP_NAME}`);
26
+ }
27
+
28
+ const MACHINE_ID_FILE = path.join(getDataDir(), "machine-id");
29
+ const AUTH_DIR = path.join(getDataDir(), "auth");
30
+ const CLI_SECRET_FILE = path.join(AUTH_DIR, "cli-secret");
31
+
32
+ let config = { ...DEFAULT_CONFIG };
33
+ let cachedCliToken = null;
34
+ let cachedCliSecret = null;
35
+
36
+ // Read raw machineId from shared file (written by server) → guarantees token match
37
+ function loadRawMachineId() {
38
+ try {
39
+ const raw = fs.readFileSync(MACHINE_ID_FILE, "utf8").trim();
40
+ if (raw) return raw;
41
+ } catch {}
42
+ try { return machineIdSync(); } catch { return ""; }
43
+ }
44
+
45
+ // Random secret shared with server via file → token unpredictable from machineId alone.
46
+ function loadCliSecret() {
47
+ if (cachedCliSecret) return cachedCliSecret;
48
+ try {
49
+ cachedCliSecret = fs.readFileSync(CLI_SECRET_FILE, "utf8").trim();
50
+ if (cachedCliSecret) return cachedCliSecret;
51
+ } catch {}
52
+ cachedCliSecret = crypto.randomBytes(32).toString("hex");
53
+ try {
54
+ fs.mkdirSync(AUTH_DIR, { recursive: true });
55
+ fs.writeFileSync(CLI_SECRET_FILE, cachedCliSecret, { mode: 0o600 });
56
+ } catch {}
57
+ return cachedCliSecret;
58
+ }
59
+
60
+ function getCliToken() {
61
+ if (cachedCliToken !== null) return cachedCliToken;
62
+ const raw = loadRawMachineId();
63
+ const secret = loadCliSecret();
64
+ cachedCliToken = raw ? crypto.createHash("sha256").update(raw + CLI_TOKEN_SALT + secret).digest("hex").substring(0, 16) : "";
65
+ return cachedCliToken;
66
+ }
67
+
68
+ /**
69
+ * Configure API client
70
+ * @param {Object} options - Configuration options
71
+ * @param {string} options.host - API host
72
+ * @param {number} options.port - API port
73
+ * @param {string} options.protocol - Protocol (http: or https:)
74
+ */
75
+ function configure(options = {}) {
76
+ config = { ...config, ...options };
77
+ }
78
+
79
+ /**
80
+ * Make HTTP request to API
81
+ * @param {string} method - HTTP method
82
+ * @param {string} path - API path
83
+ * @param {Object} body - Request body (optional)
84
+ * @returns {Promise<Object>} Response with { success, data/error }
85
+ */
86
+ function makeRequest(method, path, body = null) {
87
+ return new Promise((resolve) => {
88
+ const httpModule = config.protocol === "https:" ? https : http;
89
+
90
+ const options = {
91
+ hostname: config.host,
92
+ port: config.port,
93
+ path: path,
94
+ method: method,
95
+ headers: {
96
+ "Content-Type": "application/json",
97
+ [CLI_TOKEN_HEADER]: getCliToken(),
98
+ },
99
+ };
100
+
101
+ // Add Content-Length for POST/PUT requests
102
+ if (body && (method === "POST" || method === "PUT" || method === "PATCH")) {
103
+ const bodyString = JSON.stringify(body);
104
+ options.headers["Content-Length"] = Buffer.byteLength(bodyString);
105
+ }
106
+
107
+ const req = httpModule.request(options, (res) => {
108
+ let data = "";
109
+
110
+ res.on("data", (chunk) => {
111
+ data += chunk;
112
+ });
113
+
114
+ res.on("end", () => {
115
+ try {
116
+ const parsed = data ? JSON.parse(data) : {};
117
+
118
+ // Check if response indicates error
119
+ if (res.statusCode >= 400 || parsed.error) {
120
+ resolve({
121
+ success: false,
122
+ error: parsed.error || `HTTP ${res.statusCode}`,
123
+ statusCode: res.statusCode,
124
+ });
125
+ } else {
126
+ resolve({
127
+ success: true,
128
+ data: parsed,
129
+ statusCode: res.statusCode,
130
+ });
131
+ }
132
+ } catch (err) {
133
+ resolve({
134
+ success: false,
135
+ error: `Failed to parse response: ${err.message}`,
136
+ });
137
+ }
138
+ });
139
+ });
140
+
141
+ req.on("error", (err) => {
142
+ resolve({
143
+ success: false,
144
+ error: `Network error: ${err.message}`,
145
+ });
146
+ });
147
+
148
+ req.on("timeout", () => {
149
+ req.destroy();
150
+ resolve({
151
+ success: false,
152
+ error: "Request timeout",
153
+ });
154
+ });
155
+
156
+ // Set timeout (30 seconds)
157
+ req.setTimeout(30000);
158
+
159
+ // Write body if present
160
+ if (body && (method === "POST" || method === "PUT" || method === "PATCH")) {
161
+ req.write(JSON.stringify(body));
162
+ }
163
+
164
+ req.end();
165
+ });
166
+ }
167
+
168
+ // ============================================================================
169
+ // PROVIDERS API
170
+ // ============================================================================
171
+
172
+ /**
173
+ * Get all providers
174
+ * @returns {Promise<Object>} { success, data: { connections } }
175
+ */
176
+ async function getProviders() {
177
+ return makeRequest("GET", "/api/providers");
178
+ }
179
+
180
+ /**
181
+ * Get provider by ID
182
+ * @param {string} id - Provider ID
183
+ * @returns {Promise<Object>} { success, data: { connection } }
184
+ */
185
+ async function getProviderById(id) {
186
+ return makeRequest("GET", `/api/providers/${id}`);
187
+ }
188
+
189
+ /**
190
+ * Test provider connection
191
+ * @param {string} id - Provider ID
192
+ * @returns {Promise<Object>} { success, data: { valid, error } }
193
+ */
194
+ async function testProvider(id) {
195
+ return makeRequest("POST", `/api/providers/${id}/test`);
196
+ }
197
+
198
+ /**
199
+ * Delete provider
200
+ * @param {string} id - Provider ID
201
+ * @returns {Promise<Object>} { success, data: { message } }
202
+ */
203
+ async function deleteProvider(id) {
204
+ return makeRequest("DELETE", `/api/providers/${id}`);
205
+ }
206
+
207
+ /**
208
+ * Get provider models
209
+ * @param {string} id - Provider ID
210
+ * @returns {Promise<Object>} { success, data: { provider, connectionId, models } }
211
+ */
212
+ async function getProviderModels(id) {
213
+ return makeRequest("GET", `/api/providers/${id}/models`);
214
+ }
215
+
216
+ // ============================================================================
217
+ // OAUTH API
218
+ // ============================================================================
219
+
220
+ /**
221
+ * Get OAuth authorization URL
222
+ * @param {string} provider - Provider ID
223
+ * @returns {Promise<Object>} { success, data: { authUrl, codeVerifier, state, redirectUri } }
224
+ */
225
+ async function getOAuthAuthUrl(provider) {
226
+ // Codex requires fixed port 1455 and path /auth/callback
227
+ const redirectUri = provider === "codex"
228
+ ? "http://localhost:1455/auth/callback"
229
+ : "http://localhost:20123/callback";
230
+ return makeRequest("GET", `/api/oauth/${provider}/authorize?redirect_uri=${encodeURIComponent(redirectUri)}`);
231
+ }
232
+
233
+ /**
234
+ * Exchange OAuth authorization code for token
235
+ * @param {string} provider - Provider ID
236
+ * @param {Object} data - { code, redirectUri, codeVerifier, state }
237
+ * @returns {Promise<Object>} { success, data }
238
+ */
239
+ async function exchangeOAuthCode(provider, data) {
240
+ return makeRequest("POST", `/api/oauth/${provider}/exchange`, data);
241
+ }
242
+
243
+ /**
244
+ * Get OAuth device code
245
+ * @param {string} provider - Provider ID
246
+ * @returns {Promise<Object>} { success, data: { device_code, user_code, verification_uri, verification_uri_complete, codeVerifier, extraData } }
247
+ */
248
+ async function getOAuthDeviceCode(provider) {
249
+ return makeRequest("GET", `/api/oauth/${provider}/device-code`);
250
+ }
251
+
252
+ /**
253
+ * Poll OAuth token using device code
254
+ * @param {string} provider - Provider ID
255
+ * @param {Object} data - { deviceCode, codeVerifier, extraData }
256
+ * @returns {Promise<Object>} { success, data: { pending } }
257
+ */
258
+ async function pollOAuthToken(provider, data) {
259
+ return makeRequest("POST", `/api/oauth/${provider}/poll`, data);
260
+ }
261
+
262
+ /**
263
+ * Create API key provider connection
264
+ * @param {Object} data - { provider, name, apiKey }
265
+ * @returns {Promise<Object>} { success, data }
266
+ */
267
+ async function createApiKeyProvider(data) {
268
+ return makeRequest("POST", "/api/providers", data);
269
+ }
270
+
271
+ /**
272
+ * Update provider connection
273
+ * @param {string} id - Connection ID
274
+ * @param {Object} data - { name, priority, defaultModel, isActive }
275
+ * @returns {Promise<Object>} { success, data: { connection } }
276
+ */
277
+ async function updateConnection(id, data) {
278
+ return makeRequest("PUT", `/api/providers/${id}`, data);
279
+ }
280
+
281
+ // ============================================================================
282
+ // API KEYS API
283
+ // ============================================================================
284
+
285
+ /**
286
+ * Get all API keys
287
+ * @returns {Promise<Object>} { success, data: { keys } }
288
+ */
289
+ async function getApiKeys() {
290
+ return makeRequest("GET", "/api/keys");
291
+ }
292
+
293
+ /**
294
+ * Create new API key
295
+ * @param {string} name - Key name
296
+ * @returns {Promise<Object>} { success, data: { key, name, id, machineId } }
297
+ */
298
+ async function createApiKey(name) {
299
+ return makeRequest("POST", "/api/keys", { name });
300
+ }
301
+
302
+ /**
303
+ * Delete API key
304
+ * @param {string} id - Key ID
305
+ * @returns {Promise<Object>} { success, data: { success } }
306
+ */
307
+ async function deleteApiKey(id) {
308
+ return makeRequest("DELETE", `/api/keys/${id}`);
309
+ }
310
+
311
+ // ============================================================================
312
+ // COMBOS API
313
+ // ============================================================================
314
+
315
+ /**
316
+ * Get all combos
317
+ * @returns {Promise<Object>} { success, data: { combos } }
318
+ */
319
+ async function getCombos() {
320
+ return makeRequest("GET", "/api/combos");
321
+ }
322
+
323
+ /**
324
+ * Get combo by ID
325
+ * @param {string} id - Combo ID
326
+ * @returns {Promise<Object>} { success, data: combo }
327
+ */
328
+ async function getComboById(id) {
329
+ return makeRequest("GET", `/api/combos/${id}`);
330
+ }
331
+
332
+ /**
333
+ * Create new combo
334
+ * @param {Object} data - Combo data { name, models }
335
+ * @returns {Promise<Object>} { success, data: combo }
336
+ */
337
+ async function createCombo(data) {
338
+ return makeRequest("POST", "/api/combos", data);
339
+ }
340
+
341
+ /**
342
+ * Update combo
343
+ * @param {string} id - Combo ID
344
+ * @param {Object} data - Update data { name?, models? }
345
+ * @returns {Promise<Object>} { success, data: combo }
346
+ */
347
+ async function updateCombo(id, data) {
348
+ return makeRequest("PUT", `/api/combos/${id}`, data);
349
+ }
350
+
351
+ /**
352
+ * Delete combo
353
+ * @param {string} id - Combo ID
354
+ * @returns {Promise<Object>} { success, data: { success } }
355
+ */
356
+ async function deleteCombo(id) {
357
+ return makeRequest("DELETE", `/api/combos/${id}`);
358
+ }
359
+
360
+ // ============================================================================
361
+ // CLI TOOLS API
362
+ // ============================================================================
363
+
364
+ /**
365
+ * Get CLI tool settings
366
+ * @param {string} tool - Tool name: claude | codex | droid | openclaw
367
+ * @returns {Promise<Object>} { success, data: { installed, has9Router, ... } }
368
+ */
369
+ async function getCliToolSettings(tool) {
370
+ return makeRequest("GET", `/api/cli-tools/${tool}-settings`);
371
+ }
372
+
373
+ /**
374
+ * Apply CLI tool settings (POST)
375
+ * @param {string} tool - Tool name: claude | codex | droid | openclaw
376
+ * @param {Object} body - Payload depends on tool
377
+ * @returns {Promise<Object>} { success, data }
378
+ */
379
+ async function applyCliToolSettings(tool, body) {
380
+ return makeRequest("POST", `/api/cli-tools/${tool}-settings`, body);
381
+ }
382
+
383
+ /**
384
+ * Reset CLI tool settings (DELETE)
385
+ * @param {string} tool - Tool name: claude | codex | droid | openclaw
386
+ * @returns {Promise<Object>} { success, data }
387
+ */
388
+ async function resetCliToolSettings(tool) {
389
+ return makeRequest("DELETE", `/api/cli-tools/${tool}-settings`);
390
+ }
391
+
392
+ // ============================================================================
393
+ // SETTINGS API
394
+ // ============================================================================
395
+
396
+ /**
397
+ * Get settings
398
+ * @returns {Promise<Object>} { success, data: settings }
399
+ */
400
+ async function getSettings() {
401
+ return makeRequest("GET", "/api/settings");
402
+ }
403
+
404
+ /**
405
+ * Update settings
406
+ * @param {Object} data - Settings data
407
+ * @returns {Promise<Object>} { success, data: settings }
408
+ */
409
+ async function updateSettings(data) {
410
+ return makeRequest("PATCH", "/api/settings", data);
411
+ }
412
+
413
+ // ============================================================================
414
+ // MODELS API
415
+ // ============================================================================
416
+
417
+ /**
418
+ * Get all models (internal API)
419
+ * @returns {Promise<Object>} { success, data: { models } }
420
+ */
421
+ async function getModels() {
422
+ return makeRequest("GET", "/api/models");
423
+ }
424
+
425
+ /**
426
+ * Get available models from active providers + combos (OpenAI compatible)
427
+ * @returns {Promise<Object>} { success, data: { object, data: [...models] } }
428
+ */
429
+ async function getAvailableModels() {
430
+ return makeRequest("GET", "/v1/models");
431
+ }
432
+
433
+ // ============================================================================
434
+ // PROVIDER NODES API (custom providers)
435
+ // ============================================================================
436
+
437
+ async function getProviderNodes() {
438
+ return makeRequest("GET", "/api/provider-nodes");
439
+ }
440
+
441
+ async function createProviderNode(data) {
442
+ return makeRequest("POST", "/api/provider-nodes", data);
443
+ }
444
+
445
+ async function updateProviderNode(id, data) {
446
+ return makeRequest("PUT", `/api/provider-nodes/${id}`, data);
447
+ }
448
+
449
+ async function deleteProviderNode(id) {
450
+ return makeRequest("DELETE", `/api/provider-nodes/${id}`);
451
+ }
452
+
453
+ async function validateProviderNode(data) {
454
+ return makeRequest("POST", "/api/provider-nodes/validate", data);
455
+ }
456
+
457
+ // ============================================================================
458
+ // TUNNEL API
459
+ // ============================================================================
460
+
461
+ /**
462
+ * Get tunnel status
463
+ * @returns {Promise<Object>} { success, data: { enabled, tunnelUrl, shortId, running } }
464
+ */
465
+ async function getTunnelStatus() {
466
+ return makeRequest("GET", "/api/tunnel/status");
467
+ }
468
+
469
+ /**
470
+ * Enable tunnel
471
+ * @returns {Promise<Object>} { success, data: { tunnelUrl, shortId } }
472
+ */
473
+ async function enableTunnel() {
474
+ return makeRequest("POST", "/api/tunnel/enable");
475
+ }
476
+
477
+ /**
478
+ * Disable tunnel
479
+ * @returns {Promise<Object>} { success, data: { success } }
480
+ */
481
+ async function disableTunnel() {
482
+ return makeRequest("POST", "/api/tunnel/disable");
483
+ }
484
+
485
+ // ============================================================================
486
+ // EXPORTS
487
+ // ============================================================================
488
+
489
+ module.exports = {
490
+ configure,
491
+
492
+ // Providers
493
+ getProviders,
494
+ getProviderById,
495
+ testProvider,
496
+ deleteProvider,
497
+ getProviderModels,
498
+
499
+ // Connection aliases
500
+ testConnection: testProvider,
501
+ deleteConnection: deleteProvider,
502
+ updateConnection,
503
+
504
+ // OAuth
505
+ getOAuthAuthUrl,
506
+ exchangeOAuthCode,
507
+ getOAuthDeviceCode,
508
+ pollOAuthToken,
509
+ createApiKeyProvider,
510
+
511
+ // API Keys
512
+ getApiKeys,
513
+ createApiKey,
514
+ deleteApiKey,
515
+
516
+ // Combos
517
+ getCombos,
518
+ getComboById,
519
+ createCombo,
520
+ updateCombo,
521
+ deleteCombo,
522
+
523
+ // CLI Tools
524
+ getCliToolSettings,
525
+ applyCliToolSettings,
526
+ resetCliToolSettings,
527
+
528
+ // Settings
529
+ getSettings,
530
+ updateSettings,
531
+
532
+ // Tunnel
533
+ getTunnelStatus,
534
+ enableTunnel,
535
+ disableTunnel,
536
+
537
+ // Models
538
+ getModels,
539
+ getAvailableModels,
540
+
541
+ // Provider Nodes (custom providers)
542
+ getProviderNodes,
543
+ createProviderNode,
544
+ updateProviderNode,
545
+ deleteProviderNode,
546
+ validateProviderNode,
547
+ };