@oooooooooo/ch0de 1.9.57 → 1.9.58

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/chode.js CHANGED
@@ -7298,6 +7298,40 @@ function cmdServe(argv) {
7298
7298
  return 'ses_' + crypto.randomBytes(8).toString('hex');
7299
7299
  }
7300
7300
 
7301
+ function verifyProviderKey(baseUrl, key) {
7302
+ return new Promise(function(resolve){
7303
+ try {
7304
+ var u = new URL(baseUrl + '/models');
7305
+ var req = http.request(u, {
7306
+ method: 'GET',
7307
+ headers: {'Authorization': 'Bearer ' + key, 'Accept': 'application/json'},
7308
+ timeout: 8000
7309
+ }, function(resp){
7310
+ var working = resp.statusCode >= 200 && resp.statusCode < 400;
7311
+ resp.resume();
7312
+ resolve(working);
7313
+ });
7314
+ req.on('error', function(){ resolve(false); });
7315
+ req.on('timeout', function(){ req.destroy(); resolve(false); });
7316
+ req.end();
7317
+ } catch(e) { resolve(false); }
7318
+ });
7319
+ }
7320
+
7321
+ function providerInfo(pid) {
7322
+ var info = null;
7323
+ try {
7324
+ for (var id in PROVIDERS) { if (id === pid) { info = PROVIDERS[id]; break; } }
7325
+ } catch(e) {}
7326
+ if (info) return info;
7327
+ try {
7328
+ var dir = require('./provider-directory');
7329
+ var listed = dir.list();
7330
+ for (var i = 0; i < listed.length; i++) { if (listed[i].id === pid) { info = listed[i]; break; } }
7331
+ } catch(e) {}
7332
+ return info;
7333
+ }
7334
+
7301
7335
  function loadServeConfig() {
7302
7336
  try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')); } catch(e) { return { freeOnly: true, defaultProvider: 'auto', providers: {}, connections: {} }; }
7303
7337
  }
@@ -7434,9 +7468,11 @@ function cmdServe(argv) {
7434
7468
  if (pathname === '/api/providers' && req.method === 'GET') {
7435
7469
  var out = [];
7436
7470
  try {
7471
+ var cfg = loadServeConfig();
7472
+ var keys = cfg.providerKeys || {};
7437
7473
  for (var id in PROVIDERS) {
7438
7474
  var p = PROVIDERS[id];
7439
- out.push({id: id, name: p.name || id, requiresKey: !!p.requiresKey, free: require('./provider-capacity').free ? require('./provider-capacity').free(p) : true, category: p.category || '', builtIn: true});
7475
+ out.push({id: id, name: p.name || id, requiresKey: !!p.requiresKey, free: require('./provider-capacity').free ? require('./provider-capacity').free(p) : true, category: p.category || '', builtIn: true, hasKey: !!keys[id]});
7440
7476
  }
7441
7477
  var dir = require('./provider-directory');
7442
7478
  var listed = dir.list();
@@ -7444,13 +7480,60 @@ function cmdServe(argv) {
7444
7480
  out.forEach(function(x){ids[x.id]=true;});
7445
7481
  listed.forEach(function(p){
7446
7482
  if (!ids[p.id]) {
7447
- out.push({id: p.id, name: p.name, requiresKey: !!p.env, free: !p.env, category: 'directory', builtIn: false, baseUrl: p.baseUrl, models: p.models.length, doc: p.doc, verification: p.verification});
7483
+ out.push({id: p.id, name: p.name, requiresKey: !!p.env, free: !p.env, category: 'directory', builtIn: false, baseUrl: p.baseUrl, models: p.models.length, doc: p.doc, verification: p.verification, hasKey: !!keys[p.id]});
7448
7484
  }
7449
7485
  });
7450
7486
  } catch(e) {}
7451
7487
  json(res, 200, {ok: true, providers: out});
7452
7488
  }
7453
7489
 
7490
+ var keyM = pathname.match(/^\/api\/provider\/([^\/]+)\/key(\/test)?$/);
7491
+ if (keyM) {
7492
+ var pid = keyM[1];
7493
+ var isTest = !!keyM[2];
7494
+ if (req.method === 'GET' && !isTest) {
7495
+ var cfg2 = loadServeConfig();
7496
+ var keys2 = cfg2.providerKeys || {};
7497
+ json(res, 200, {ok: true, hasKey: !!keys2[pid]});
7498
+ return;
7499
+ }
7500
+ if (req.method === 'POST' && !isTest) {
7501
+ readBody(req).then(function(body){
7502
+ var parsed;
7503
+ try { parsed = JSON.parse(body || '{}'); } catch(e) { json(res, 400, {ok:false, error:'invalid json'}); return; }
7504
+ var key = String(parsed.key || '');
7505
+ var cfg3 = loadServeConfig();
7506
+ if (!cfg3.providerKeys) cfg3.providerKeys = {};
7507
+ cfg3.providerKeys[pid] = key;
7508
+ saveServeConfig(cfg3);
7509
+ var pinfo = providerInfo(pid);
7510
+ if (pinfo && pinfo.baseUrl) {
7511
+ verifyProviderKey(pinfo.baseUrl, key).then(function(working){
7512
+ json(res, 200, {ok: true, working: working});
7513
+ }).catch(function(){ json(res, 200, {ok: true, working: false}); });
7514
+ } else {
7515
+ json(res, 200, {ok: true, working: false});
7516
+ }
7517
+ }).catch(function(e){ json(res, 500, {ok:false, error: e.message}); });
7518
+ return;
7519
+ }
7520
+ if (req.method === 'GET' && isTest) {
7521
+ var cfg4 = loadServeConfig();
7522
+ var keys4 = cfg4.providerKeys || {};
7523
+ var key = String(keys4[pid] || '');
7524
+ if (!key) { json(res, 200, {ok: true, working: false, hasKey: false}); return; }
7525
+ var pinfo2 = providerInfo(pid);
7526
+ if (pinfo2 && pinfo2.baseUrl) {
7527
+ verifyProviderKey(pinfo2.baseUrl, key).then(function(working){
7528
+ json(res, 200, {ok: true, working: working, hasKey: true});
7529
+ }).catch(function(){ json(res, 200, {ok: true, working: false, hasKey: true}); });
7530
+ } else {
7531
+ json(res, 200, {ok: true, working: false, hasKey: true});
7532
+ }
7533
+ return;
7534
+ }
7535
+ }
7536
+
7454
7537
  res.writeHead(404, {'Content-Type': 'text/plain'});
7455
7538
  res.end('Not found');
7456
7539
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oooooooooo/ch0de",
3
- "version": "1.9.57",
3
+ "version": "1.9.58",
4
4
  "description": "The self-healing AI coding harness. Free tiers. HEMO auto-provisioning. Zero lock-in. Canonical brand: ch0de.",
5
5
  "main": "chode.js",
6
6
  "bin": {
@@ -58,8 +58,9 @@ body { background: #000; color: #e0e0e0; font-family: "IBM Plex Mono", monospace
58
58
  .setting-select, .setting-input { background: #000; border: 1px solid #22dd5533; color: #e0e0e0; padding: 6px 10px; border-radius: 6px; font-family: inherit; font-size: 12px; outline: none; }
59
59
  .setting-select:focus, .setting-input:focus { border-color: #22dd55; }
60
60
  .setting-checkbox { width: 18px; height: 18px; accent-color: #22dd55; background: #000; border: 1px solid #22dd5533; cursor: pointer; }
61
- .provider-row { display: flex; align-items: center; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #141414; font-size: 12px; }
61
+ .provider-row { display: flex; align-items: center; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #141414; font-size: 12px; cursor: pointer; }
62
62
  .provider-row:last-child { border-bottom: none; }
63
+ .provider-row:hover { background: #141414; }
63
64
  #providersList { max-height: 340px; overflow-y: auto; border: 1px solid #141414; border-radius: 6px; padding: 10px; background: #000; margin-top: 8px; }
64
65
  .provider-info { display: flex; align-items: center; gap: 10px; flex: 1; }
65
66
  .provider-name { font-weight: bold; color: #e0e0e0; }
@@ -67,9 +68,27 @@ body { background: #000; color: #e0e0e0; font-family: "IBM Plex Mono", monospace
67
68
  .provider-status { font-size: 11px; padding: 2px 8px; border-radius: 4px; }
68
69
  .provider-status.free { background: #1a1a1a; color: #22dd55; border: 1px solid #22dd5533; }
69
70
  .provider-status.keyed { background: #1a1a1a; color: #ff9900; border: 1px solid #ff990033; }
71
+ .provider-status.haskey { background: #1a1a1a; color: #22dd55; border: 1px solid #22dd5533; }
72
+ .provider-status.working { background: #1a1a1a; color: #22dd55; border: 1px solid #22dd55; }
73
+ .provider-status.badkey { background: #1a1a1a; color: #ff4444; border: 1px solid #ff444433; }
70
74
  .btn-save { background: #22dd55; color: #000; border: none; border-radius: 6px; padding: 8px 16px; font-family: inherit; font-weight: bold; cursor: pointer; margin-top: 20px; }
71
75
  .btn-save:hover { background: #3dffa0; }
72
76
  .btn-save:disabled { background: #1a1a1a; color: #666; cursor: not-allowed; }
77
+
78
+ /* Key input modal */
79
+ .key-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 100; display: none; }
80
+ .key-modal { position: fixed; top: 20%; left: 25%; right: 25%; background: #0a0a0a; border: 1px solid #22dd5533; border-radius: 12px; z-index: 110; padding: 24px; display: none; box-shadow: 0 4px 20px rgba(0,0,0,0.4); }
81
+ .key-modal.open, .key-modal-overlay.open { display: block; }
82
+ .key-modal-title { color: #22dd55; font-weight: bold; font-size: 1.1rem; margin-bottom: 16px; }
83
+ .key-modal-field { margin-bottom: 16px; }
84
+ .key-modal-label { display: block; color: #888; font-size: 11px; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; }
85
+ .key-modal-input { width: 100%; background: #000; border: 1px solid #22dd5533; color: #e0e0e0; padding: 10px 14px; border-radius: 6px; font-family: inherit; font-size: 13px; outline: none; }
86
+ .key-modal-input:focus { border-color: #22dd55; }
87
+ .key-modal-actions { display: flex; gap: 10px; margin-top: 20px; }
88
+ .key-status { margin-top: 12px; padding: 8px 12px; border-radius: 6px; font-size: 12px; display: none; }
89
+ .key-status.ok { background: #141414; color: #22dd55; border: 1px solid #22dd5533; display: block; }
90
+ .key-status.err { background: #141414; color: #ff4444; border: 1px solid #ff444433; display: block; }
91
+ .key-status.saving { background: #141414; color: #ff9900; border: 1px solid #ff990033; display: block; }
73
92
  </style>
74
93
  </head>
75
94
  <body>
@@ -175,6 +194,19 @@ body { background: #000; color: #e0e0e0; font-family: "IBM Plex Mono", monospace
175
194
  </div>
176
195
  </div>
177
196
  </div>
197
+ <div class="key-modal-overlay" id="keyModalOverlay"></div>
198
+ <div class="key-modal" id="keyModal">
199
+ <div class="key-modal-title" id="keyModalTitle">API Key</div>
200
+ <div class="key-modal-field">
201
+ <span class="key-modal-label" id="keyModalLabel">API Key</span>
202
+ <input type="password" id="keyModalInput" class="key-modal-input" placeholder="Enter API key">
203
+ </div>
204
+ <div class="key-modal-actions">
205
+ <button class="btn-save" id="keyCancelBtn" style="background:#141414;color:#e0e0e0;border:1px solid #22dd5533;">Cancel</button>
206
+ <button class="btn-save" id="keySubmitBtn">Save & Test</button>
207
+ </div>
208
+ <div class="key-status" id="keyStatus"></div>
209
+ </div>
178
210
  <script>
179
211
  const API = "http://127.0.0.1:" + (window.location.port || "4096");
180
212
  let currentSessionId = null;
@@ -309,8 +341,10 @@ async function loadProviders() {
309
341
  select.appendChild(opt);
310
342
  var row = document.createElement("div");
311
343
  row.className = "provider-row";
312
- var statusCls = p.requiresKey ? "keyed" : "free";
313
- var statusText = p.requiresKey ? "needs API key" : "free";
344
+ var statusCls, statusText;
345
+ if (!p.requiresKey) { statusCls = "free"; statusText = "free"; }
346
+ else if (p.hasKey) { statusCls = "working"; statusText = "key saved"; }
347
+ else { statusCls = "keyed"; statusText = "needs API key"; }
314
348
  var extra = p.builtIn ? "" : ' <span style="font-size:10px;color:#888;margin-left:6px;">(' + (p.models||0) + ' models)</span>';
315
349
  row.innerHTML =
316
350
  '<div class="provider-info">' +
@@ -318,6 +352,7 @@ async function loadProviders() {
318
352
  '<div class="provider-id">' + escapeHtml(p.id) + (p.baseUrl ? ' · ' + escapeHtml(String(p.baseUrl).slice(0,40)) : '') + '</div>' +
319
353
  '</div>' +
320
354
  '<div class="provider-status ' + statusCls + '">' + statusText + '</div>';
355
+ row.onclick = function() { openKeyModal(p); };
321
356
  list.appendChild(row);
322
357
  });
323
358
  } catch(e) {}
@@ -366,6 +401,80 @@ function closeSettings() {
366
401
  settingsOpen = false;
367
402
  }
368
403
 
404
+ var keyModalProvider = null;
405
+ function openKeyModal(p) {
406
+ keyModalProvider = p;
407
+ document.getElementById("keyModalTitle").textContent = "API Key · " + (p.name || p.id);
408
+ document.getElementById("keyModalLabel").textContent = p.env ? ("API Key (" + p.env + ")") : "API Key";
409
+ document.getElementById("keyModalInput").value = "";
410
+ document.getElementById("keyStatus").style.display = "none";
411
+ document.getElementById("keyModalOverlay").classList.add("open");
412
+ document.getElementById("keyModal").classList.add("open");
413
+ document.getElementById("keyModalInput").focus();
414
+ }
415
+ function closeKeyModal() {
416
+ document.getElementById("keyModalOverlay").classList.remove("open");
417
+ document.getElementById("keyModal").classList.remove("open");
418
+ keyModalProvider = null;
419
+ }
420
+ async function submitKey() {
421
+ if (!keyModalProvider) return;
422
+ var input = document.getElementById("keyModalInput");
423
+ var key = input.value.trim();
424
+ if (!key) return;
425
+ var status = document.getElementById("keyStatus");
426
+ var btn = document.getElementById("keySubmitBtn");
427
+ status.textContent = "Saving and testing key…";
428
+ status.className = "key-status saving";
429
+ status.style.display = "block";
430
+ btn.disabled = true;
431
+ try {
432
+ const res = await fetch(API + "/api/provider/" + encodeURIComponent(keyModalProvider.id) + "/key", {
433
+ method: "POST",
434
+ headers: { "Content-Type": "application/json" },
435
+ body: JSON.stringify({ key: key })
436
+ });
437
+ const data = await res.json();
438
+ if (data.ok && data.working) {
439
+ status.textContent = "Key is working ✓";
440
+ status.className = "key-status ok";
441
+ // Refresh provider list to show updated status
442
+ await loadProviders();
443
+ setTimeout(closeKeyModal, 800);
444
+ } else {
445
+ status.textContent = "Key saved but could not be verified";
446
+ status.className = "key-status err";
447
+ }
448
+ } catch(e) {
449
+ status.textContent = "Error: " + e.message;
450
+ status.className = "key-status err";
451
+ } finally {
452
+ btn.disabled = false;
453
+ }
454
+ }
455
+ async function testKey() {
456
+ if (!keyModalProvider) return;
457
+ var status = document.getElementById("keyStatus");
458
+ status.textContent = "Testing key…";
459
+ status.className = "key-status saving";
460
+ status.style.display = "block";
461
+ try {
462
+ const res = await fetch(API + "/api/provider/" + encodeURIComponent(keyModalProvider.id) + "/key/test");
463
+ const data = await res.json();
464
+ if (data.ok && data.working) {
465
+ status.textContent = "Key is working ✓";
466
+ status.className = "key-status ok";
467
+ await loadProviders();
468
+ } else {
469
+ status.textContent = "Key verification failed";
470
+ status.className = "key-status err";
471
+ }
472
+ } catch(e) {
473
+ status.textContent = "Error: " + e.message;
474
+ status.className = "key-status err";
475
+ }
476
+ }
477
+
369
478
  document.getElementById("newSessionBtn").onclick = createSession;
370
479
  document.getElementById("sendBtn").onclick = sendMessage;
371
480
  document.getElementById("messageInput").addEventListener("keypress", function(e) {
@@ -375,6 +484,12 @@ document.getElementById("settingsBtn").onclick = openSettings;
375
484
  document.getElementById("settingsClose").onclick = closeSettings;
376
485
  document.getElementById("settingsOverlay").onclick = function(e) { if (e.target === this) closeSettings(); };
377
486
  document.getElementById("settingsSave").onclick = function() { saveSettings(); closeSettings(); };
487
+ document.getElementById("keyCancelBtn").onclick = closeKeyModal;
488
+ document.getElementById("keySubmitBtn").onclick = submitKey;
489
+ document.getElementById("keyModalOverlay").onclick = function(e) { if (e.target === this) closeKeyModal(); };
490
+ document.getElementById("keyModalInput").addEventListener("keypress", function(e) {
491
+ if (e.key === "Enter") submitKey();
492
+ });
378
493
 
379
494
  document.querySelectorAll(".settings-tab").forEach(function(tab) {
380
495
  tab.onclick = function() {