kentutai-app 1.0.9 → 1.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.
@@ -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
+ };