@oooooooooo/ch0de 1.9.57 → 1.9.59

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
@@ -3840,7 +3840,12 @@ async function runAgentLoop(prompt, sessionId, force, callOptions, aiCall, onToo
3840
3840
  mutating.push(executable[mi].args.file);
3841
3841
  }
3842
3842
  }
3843
- if (mutating.length) takeSnapshot('agent turn ' + steps, mutating, sessionId);
3843
+ if (mutating.length) {
3844
+ // Auto-commit checkpoint before mutating agent turn so work can be rolled back.
3845
+ try { run('git add -A', { silent: true, cwd: ROOT }); } catch (e) {}
3846
+ try { run('git commit --allow-empty -m "chode checkpoint: agent turn ' + steps + '"', { silent: true, cwd: ROOT }); } catch (e) {}
3847
+ takeSnapshot('agent turn ' + steps, mutating, sessionId);
3848
+ }
3844
3849
  var toolCtx = { yolo:callOptions.yolo===true,onChildAuthors:ids=>{for(const id of ids)draftAuthors.add(String(id).toLowerCase());},taskGroup,mode:callOptions.mode,acceptanceTest:callOptions.acceptanceTest,sessionId: sessionId, actions: actions, aiCall: aiCall, onTool: onTool, onAsk: onAsk, planTargets: effTargets, overwriteTargets: effOverwrites, scopeTargets: callOptions.scopeTargets || null, inspectedUrls: taskContext.inspectedUrls };
3845
3850
  var acceptanceChangedThisTurn=false;
3846
3851
  async function execOne(call) {
@@ -7298,6 +7303,40 @@ function cmdServe(argv) {
7298
7303
  return 'ses_' + crypto.randomBytes(8).toString('hex');
7299
7304
  }
7300
7305
 
7306
+ function verifyProviderKey(baseUrl, key) {
7307
+ return new Promise(function(resolve){
7308
+ try {
7309
+ var u = new URL(baseUrl + '/models');
7310
+ var req = http.request(u, {
7311
+ method: 'GET',
7312
+ headers: {'Authorization': 'Bearer ' + key, 'Accept': 'application/json'},
7313
+ timeout: 8000
7314
+ }, function(resp){
7315
+ var working = resp.statusCode >= 200 && resp.statusCode < 400;
7316
+ resp.resume();
7317
+ resolve(working);
7318
+ });
7319
+ req.on('error', function(){ resolve(false); });
7320
+ req.on('timeout', function(){ req.destroy(); resolve(false); });
7321
+ req.end();
7322
+ } catch(e) { resolve(false); }
7323
+ });
7324
+ }
7325
+
7326
+ function providerInfo(pid) {
7327
+ var info = null;
7328
+ try {
7329
+ for (var id in PROVIDERS) { if (id === pid) { info = PROVIDERS[id]; break; } }
7330
+ } catch(e) {}
7331
+ if (info) return info;
7332
+ try {
7333
+ var dir = require('./provider-directory');
7334
+ var listed = dir.list();
7335
+ for (var i = 0; i < listed.length; i++) { if (listed[i].id === pid) { info = listed[i]; break; } }
7336
+ } catch(e) {}
7337
+ return info;
7338
+ }
7339
+
7301
7340
  function loadServeConfig() {
7302
7341
  try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')); } catch(e) { return { freeOnly: true, defaultProvider: 'auto', providers: {}, connections: {} }; }
7303
7342
  }
@@ -7434,9 +7473,11 @@ function cmdServe(argv) {
7434
7473
  if (pathname === '/api/providers' && req.method === 'GET') {
7435
7474
  var out = [];
7436
7475
  try {
7476
+ var cfg = loadServeConfig();
7477
+ var keys = cfg.providerKeys || {};
7437
7478
  for (var id in PROVIDERS) {
7438
7479
  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});
7480
+ 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
7481
  }
7441
7482
  var dir = require('./provider-directory');
7442
7483
  var listed = dir.list();
@@ -7444,11 +7485,59 @@ function cmdServe(argv) {
7444
7485
  out.forEach(function(x){ids[x.id]=true;});
7445
7486
  listed.forEach(function(p){
7446
7487
  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});
7488
+ 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
7489
  }
7449
7490
  });
7450
7491
  } catch(e) {}
7451
7492
  json(res, 200, {ok: true, providers: out});
7493
+ return;
7494
+ }
7495
+
7496
+ var keyM = pathname.match(/^\/api\/provider\/([^\/]+)\/key(\/test)?$/);
7497
+ if (keyM) {
7498
+ var pid = keyM[1];
7499
+ var isTest = !!keyM[2];
7500
+ if (req.method === 'GET' && !isTest) {
7501
+ var cfg2 = loadServeConfig();
7502
+ var keys2 = cfg2.providerKeys || {};
7503
+ json(res, 200, {ok: true, hasKey: !!keys2[pid]});
7504
+ return;
7505
+ }
7506
+ if (req.method === 'POST' && !isTest) {
7507
+ readBody(req).then(function(body){
7508
+ var parsed;
7509
+ try { parsed = JSON.parse(body || '{}'); } catch(e) { json(res, 400, {ok:false, error:'invalid json'}); return; }
7510
+ var key = String(parsed.key || '');
7511
+ var cfg3 = loadServeConfig();
7512
+ if (!cfg3.providerKeys) cfg3.providerKeys = {};
7513
+ cfg3.providerKeys[pid] = key;
7514
+ saveServeConfig(cfg3);
7515
+ var pinfo = providerInfo(pid);
7516
+ if (pinfo && pinfo.baseUrl) {
7517
+ verifyProviderKey(pinfo.baseUrl, key).then(function(working){
7518
+ json(res, 200, {ok: true, working: working});
7519
+ }).catch(function(){ json(res, 200, {ok: true, working: false}); });
7520
+ } else {
7521
+ json(res, 200, {ok: true, working: false});
7522
+ }
7523
+ }).catch(function(e){ json(res, 500, {ok:false, error: e.message}); });
7524
+ return;
7525
+ }
7526
+ if (req.method === 'GET' && isTest) {
7527
+ var cfg4 = loadServeConfig();
7528
+ var keys4 = cfg4.providerKeys || {};
7529
+ var key = String(keys4[pid] || '');
7530
+ if (!key) { json(res, 200, {ok: true, working: false, hasKey: false}); return; }
7531
+ var pinfo2 = providerInfo(pid);
7532
+ if (pinfo2 && pinfo2.baseUrl) {
7533
+ verifyProviderKey(pinfo2.baseUrl, key).then(function(working){
7534
+ json(res, 200, {ok: true, working: working, hasKey: true});
7535
+ }).catch(function(){ json(res, 200, {ok: true, working: false, hasKey: true}); });
7536
+ } else {
7537
+ json(res, 200, {ok: true, working: false, hasKey: true});
7538
+ }
7539
+ return;
7540
+ }
7452
7541
  }
7453
7542
 
7454
7543
  res.writeHead(404, {'Content-Type': 'text/plain'});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oooooooooo/ch0de",
3
- "version": "1.9.57",
3
+ "version": "1.9.59",
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": {
@@ -151,7 +151,7 @@
151
151
  },
152
152
  "dependencies": {
153
153
  "@modelcontextprotocol/sdk": "1.32.2",
154
- "@oooooooooo/ch0de": "^1.9.56",
154
+ "@oooooooooo/ch0de": "^1.9.58",
155
155
  "lean": "^0.1.1"
156
156
  },
157
157
  "devDependencies": {
@@ -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() {