kryptheon 0.1.31 → 0.1.32

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.
@@ -103,10 +103,24 @@
103
103
  busy = on;
104
104
  Array.prototype.forEach.call(document.querySelectorAll('[data-action]'), function (b) { b.disabled = on; });
105
105
  }
106
+ // The clipboard, and when a browser refuses it, the older way: the text
107
+ // put in a box off the screen, selected and copied.
106
108
  function copy(text, done) {
107
- navigator.clipboard.writeText(text).then(function () { toast(done); }, function () {
108
- toast('Could not copy - select the text instead');
109
- });
109
+ function older() {
110
+ var box = el('textarea');
111
+ box.value = text;
112
+ box.setAttribute('readonly', '');
113
+ box.style.position = 'fixed';
114
+ box.style.left = '-9999px';
115
+ document.body.appendChild(box);
116
+ box.select();
117
+ var ok = false;
118
+ try { ok = document.execCommand('copy'); } catch (e) { ok = false; }
119
+ document.body.removeChild(box);
120
+ toast(ok ? done : 'Could not copy - select the text instead');
121
+ }
122
+ if (!navigator.clipboard || !navigator.clipboard.writeText) return older();
123
+ navigator.clipboard.writeText(text).then(function () { toast(done); }, older);
110
124
  }
111
125
  function button(cls, iconName, text, onClick) {
112
126
  var b = el('button', 'btn' + (cls ? ' ' + cls : ''));
@@ -155,33 +169,70 @@
155
169
  // lines are what the command printed, already scrubbed of any secret.
156
170
  var job = null;
157
171
  var lastRecheck = null;
158
-
172
+ // Whether the printed lines are shown: null follows the job - shown while it
173
+ // runs or when it failed, folded away once it finished well - until the
174
+ // person opens or folds them.
175
+ var linesOpen = null;
176
+ // Whether the one prompt for all findings is shown in full.
177
+ var allPromptOpen = false;
178
+
179
+ // The panel sits in the corner of the window, over the page; the page gets
180
+ // room at its foot so nothing stays hidden under it.
159
181
  function renderJob() {
160
182
  var box = byId('job');
161
183
  clear(box);
162
- if (!job) { box.hidden = true; return; }
184
+ var main = document.querySelector('main');
185
+ var root = document.documentElement;
186
+ if (!job) { box.hidden = true; linesOpen = null; main.style.paddingBottom = ''; root.style.removeProperty('--job-room'); return; }
163
187
  box.hidden = false;
164
- box.className = 'job' + (job.running ? ' on' : job.result && job.result.ok ? ' ok' : ' bad');
188
+ var ok = !!(job.result && job.result.ok);
189
+ box.className = 'job' + (job.running ? ' on' : ok ? ' ok' : ' bad');
165
190
  var head = el('div', 'job-head');
166
191
  if (job.running) head.appendChild(el('span', 'spin'));
167
192
  head.appendChild(el('b', null, job.title));
168
- head.appendChild(el('span', 'tag ' + (job.running ? 'sun' : job.result && job.result.ok ? 'leaf' : 'ember'),
169
- job.running ? 'running' : job.result && job.result.ok ? 'finished' : 'did not finish'));
193
+ head.appendChild(el('span', 'tag ' + (job.running ? 'sun' : ok ? 'leaf' : 'ember'),
194
+ job.running ? 'running' : ok ? 'finished' : 'did not finish'));
170
195
  if (!job.running) {
171
- var close = el('button', 'job-close', 'Hide');
196
+ var close = el('button', 'job-close', 'Close');
172
197
  close.type = 'button';
173
198
  close.addEventListener('click', function () { job = null; renderJob(); });
174
199
  head.appendChild(close);
175
200
  }
176
201
  box.appendChild(head);
177
- var lines = el('pre', 'job-lines', (job.lines || []).filter(function (l, i, all) { return l || (i && all[i - 1]); }).slice(-60).join('\n') || 'Starting...');
178
- box.appendChild(lines);
202
+ var open = linesOpen === null ? job.running || !ok : linesOpen;
203
+ var lines = null;
204
+ if (open) {
205
+ lines = el('pre', 'job-lines', (job.lines || []).filter(function (l, i, all) { return l || (i && all[i - 1]); }).slice(-60).join('\n') || 'Starting...');
206
+ box.appendChild(lines);
207
+ }
179
208
  if (!job.running && job.result) {
180
209
  var r = job.result;
181
- box.appendChild(el('div', 'job-result', r.ok ? 'Result: ' + (r.summary || 'done') : 'It did not finish: ' + (r.why || 'see the lines above')));
210
+ box.appendChild(el('div', 'job-result', ok ? 'Result: ' + (r.summary || 'done') : 'It did not finish: ' + (r.why || 'see what it printed')));
211
+ }
212
+ if (!job.running) {
213
+ var acts = el('div', 'job-acts');
214
+ // What it found is on Findings; a recording is on Runs. Setting the
215
+ // nightly check up, or removing it, finds nothing to go and see.
216
+ var setUp = job.kind === 'db' && / (un)?install$/.test(job.command || '');
217
+ var next = !ok || setUp ? null : job.kind === 'record' ? { id: 'runs', label: 'See runs' } : { id: 'findings', label: 'See findings' };
218
+ if (next && currentView() !== next.id) {
219
+ var goBtn = el('button', 'go', next.label + ' →');
220
+ goBtn.type = 'button';
221
+ goBtn.addEventListener('click', function () { go(next.id); });
222
+ acts.appendChild(goBtn);
223
+ }
224
+ var fold = el('button', null, open ? 'Hide what it printed' : 'Show what it printed');
225
+ fold.type = 'button';
226
+ fold.setAttribute('aria-expanded', open ? 'true' : 'false');
227
+ fold.addEventListener('click', function () { linesOpen = !open; renderJob(); });
228
+ acts.appendChild(fold);
229
+ box.appendChild(acts);
182
230
  }
183
231
  if (job.command) box.appendChild(el('div', 'fallback', 'The same in a terminal: ' + job.command));
184
- lines.scrollTop = lines.scrollHeight;
232
+ if (lines) lines.scrollTop = lines.scrollHeight;
233
+ main.style.paddingBottom = (box.offsetHeight + 48) + 'px';
234
+ // A message pops up above the panel, never over it.
235
+ root.style.setProperty('--job-room', (box.offsetHeight + 12) + 'px');
185
236
  }
186
237
 
187
238
  function getJob() {
@@ -194,6 +245,7 @@
194
245
  setBusy(true);
195
246
  return post(path, body).then(function (started) {
196
247
  job = started.job;
248
+ linesOpen = null;
197
249
  renderJob();
198
250
  return follow();
199
251
  }).then(function (done) {
@@ -255,6 +307,8 @@
255
307
  byId('viewTitle').textContent = view.title;
256
308
  document.title = (id === 'dashboard' ? '' : view.title + ' - ') + 'Kryptheon';
257
309
  if (state) renderSide();
310
+ // Its "See findings" goes once Findings is the page on screen.
311
+ if (job && !job.running) renderJob();
258
312
  window.scrollTo(0, 0);
259
313
  }
260
314
  window.addEventListener('hashchange', showView);
@@ -528,7 +582,7 @@
528
582
  if (!connection) { toast('Paste your connection string in "Connect your database" first.'); dbInput.focus(); return; }
529
583
  runJob('/api/db', { action: 'night', connection: connection }).then(function (done) {
530
584
  var r = done && done.result;
531
- toast(r && r.ok ? 'Read back - see the lines above and Findings' : 'Nothing to read back' + (r && r.why ? ' - ' + r.why : ''));
585
+ toast(r && r.ok ? 'Read back - see Findings' : 'Nothing to read back' + (r && r.why ? ' - ' + r.why : ''));
532
586
  }, function () {});
533
587
  });
534
588
  see.setAttribute('data-action', 'db');
@@ -724,6 +778,28 @@
724
778
  if (lastRecheck.newProblems.length) gone.appendChild(el('span', 'tag ember', plural(lastRecheck.newProblems.length, 'problem') + ' newly broken - marked below'));
725
779
  body.appendChild(gone);
726
780
  }
781
+ // Every fix in one paste, with the rules that keep the AI from breaking
782
+ // something else - whatever the filter shows, it is all of them.
783
+ if (state.allFixPrompt) {
784
+ var many = state.findings.filter(function (f) { return f.fixPrompt; }).length;
785
+ var all = el('div', 'fix-all');
786
+ var words = el('div', 'fix-all-words');
787
+ words.appendChild(el('b', null, 'Fix all ' + many + ' at once'));
788
+ words.appendChild(el('span', null, 'One prompt with every fix, and rules that stop your AI from changing anything else. Then Re-check each one.'));
789
+ all.appendChild(words);
790
+ all.appendChild(button('sun', 'copy', 'Copy one prompt for all', function () {
791
+ copy(state.allFixPrompt, 'One prompt for all ' + many + ' copied - paste it into your AI tool');
792
+ }));
793
+ // To read before pasting, or to select by hand. It stays open across
794
+ // the page's own refresh every few seconds.
795
+ var whole = el('details', 'fix-all-text');
796
+ whole.open = allPromptOpen;
797
+ whole.addEventListener('toggle', function () { allPromptOpen = whole.open; });
798
+ whole.appendChild(el('summary', null, 'See the prompt'));
799
+ whole.appendChild(el('pre', null, state.allFixPrompt));
800
+ all.appendChild(whole);
801
+ body.appendChild(all);
802
+ }
727
803
  if (!shown.length) {
728
804
  body.appendChild(el('p', 'empty', state.findings.length ? 'Nothing matches.' : 'Nothing found by the checks that are on and have run. That is not the same as nothing wrong - see which checks are not built yet.'));
729
805
  return;
@@ -1012,6 +1088,7 @@
1012
1088
  return getJob().then(function (now) {
1013
1089
  if (!now || !now.running) return;
1014
1090
  job = now;
1091
+ linesOpen = null;
1015
1092
  setBusy(true);
1016
1093
  renderJob();
1017
1094
  return follow().then(function () { setBusy(false); return load(); });
@@ -265,14 +265,22 @@
265
265
  @media (max-width: 600px) { .files div { grid-template-columns: minmax(0, 1fr); gap: 2px; } }
266
266
 
267
267
  /* --- a run in progress, and what it said ---------------------------------- */
268
- .job { margin: 0 0 28px; padding: 16px 18px 18px; color: var(--on-dark); background: var(--ink); border-left: 3px solid var(--orange); }
268
+ /* Pinned to the corner of the window, so it is seen from wherever the button
269
+ was pressed - never at the top of a page scrolled away from. */
270
+ .job { position: fixed; right: 24px; bottom: 24px; z-index: 9; display: flex; flex-direction: column; width: min(520px, calc(100vw - 48px)); max-height: calc(100vh - 48px); padding: 16px 18px 18px; color: var(--on-dark); background: var(--ink); border-left: 3px solid var(--orange); box-shadow: 0 18px 50px rgba(11, 14, 20, 0.35); animation: job-in 0.35s var(--ease); }
271
+ .job[hidden] { display: none; }
269
272
  .job.ok { border-left-color: var(--on-dark); }
273
+ @keyframes job-in { from { opacity: 0; transform: translateY(16px); } }
274
+ @media (max-width: 600px) { .job { right: 16px; bottom: 16px; width: calc(100vw - 32px); } }
270
275
  .job-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
271
276
  .job-head b { font-weight: 800; font-stretch: 74%; font-size: 1.1rem; text-transform: uppercase; overflow-wrap: anywhere; }
272
277
  .job-head .tag { color: var(--on-dark); border-color: var(--line-dark); }
273
278
  .job-close { margin-left: auto; padding: 4px 10px; font: 10px var(--mono); letter-spacing: 0.12em; text-transform: uppercase; border: 1px solid var(--line-dark); }
274
- .job-lines { max-height: 260px; margin: 12px 0 0; overflow: auto; font: 12px/1.6 var(--mono); white-space: pre-wrap; overflow-wrap: anywhere; opacity: 0.85; }
279
+ .job-lines { min-height: 0; max-height: 220px; margin: 12px 0 0; overflow: auto; font: 12px/1.6 var(--mono); white-space: pre-wrap; overflow-wrap: anywhere; opacity: 0.85; }
275
280
  .job-result { margin-top: 12px; font-weight: 700; }
281
+ .job-acts { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 12px; }
282
+ .job-acts button { padding: 6px 12px; font: 10px var(--mono); letter-spacing: 0.12em; text-transform: uppercase; color: var(--on-dark); border: 1px solid var(--line-dark); }
283
+ .job-acts button.go { color: var(--ink); background: var(--on-dark); border-color: var(--on-dark); }
276
284
  .job .fallback { color: rgba(233, 237, 242, 0.6); }
277
285
 
278
286
  /* --- boxes to type into, the words around them, and the yes -------------- */
@@ -293,9 +301,15 @@
293
301
  .fallback { font: 10.5px/1.4 var(--mono); color: var(--muted); overflow-wrap: anywhere; }
294
302
  .tag.when { color: var(--on-light); border-style: dotted; }
295
303
  .f-quick { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 10px; padding: 0 8px 16px 20px; margin-top: -6px; }
304
+ .fix-all { display: flex; flex-wrap: wrap; align-items: center; gap: 14px 20px; padding: 16px 8px 16px 20px; border-bottom: 1px solid var(--line); box-shadow: inset 3px 0 0 var(--orange); }
305
+ .fix-all-words { flex: 1 1 260px; display: grid; gap: 4px; font-size: 14px; }
306
+ .fix-all-words b { font-weight: 800; font-stretch: 74%; font-size: 1.05rem; text-transform: uppercase; }
307
+ .fix-all-text { flex: 1 1 100%; min-width: 0; }
308
+ .fix-all-text summary { width: max-content; font: 11px var(--mono); letter-spacing: 0.12em; text-transform: uppercase; cursor: pointer; opacity: 0.75; }
309
+ .fix-all-text pre { max-height: 320px; margin: 10px 0 0; padding: 14px 16px; overflow: auto; font: 12px/1.6 var(--mono); white-space: pre-wrap; overflow-wrap: anywhere; border: 1px solid var(--line); }
296
310
  .rechecked { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 14px 8px 14px 20px; font-size: 14px; border-bottom: 1px solid var(--line); box-shadow: inset 3px 0 0 var(--on-light); }
297
311
 
298
- .toast { position: fixed; left: 50%; bottom: 24px; z-index: 10; max-width: calc(100vw - 32px); padding: 14px 20px; font-size: 14px; color: var(--on-dark); background: var(--ink); border-left: 2px solid var(--orange); opacity: 0; transform: translate(-50%, 20px); transition: opacity 0.35s var(--ease), transform 0.35s var(--ease); pointer-events: none; }
312
+ .toast { position: fixed; left: 50%; bottom: calc(24px + var(--job-room, 0px)); z-index: 10; max-width: calc(100vw - 32px); padding: 14px 20px; font-size: 14px; color: var(--on-dark); background: var(--ink); border-left: 2px solid var(--orange); opacity: 0; transform: translate(-50%, 20px); transition: opacity 0.35s var(--ease), transform 0.35s var(--ease); pointer-events: none; }
299
313
  .toast.show { opacity: 1; transform: translate(-50%, 0); }
300
314
 
301
315
  @media (prefers-reduced-motion: reduce) {
@@ -284,6 +284,44 @@ function nextAction(findings, steps) {
284
284
  return { kind: 'quiet', title: 'Nothing open' };
285
285
  }
286
286
 
287
+ /**
288
+ * One prompt that fixes every open finding at once, for pasting into an AI
289
+ * tool, or '' when fewer than two findings have a prompt (one is its own).
290
+ * Each finding's own prompt goes in as it is, in the order the page shows
291
+ * them, most severe first. Around them are the rules that keep a fix from
292
+ * breaking something else: only what is listed, the smallest change, one at
293
+ * a time with the app checked between, nothing weakened to make a finding go
294
+ * away, ask rather than guess. The re-checks come once, at the end.
295
+ *
296
+ * items: [{ id, severity, headline, where, prompt }] - prompt without its
297
+ * re-check line.
298
+ */
299
+ function combinedPrompt(items) {
300
+ const withPrompt = items.filter((f) => f.prompt);
301
+ if (withPrompt.length < 2) return '';
302
+ const n = withPrompt.length;
303
+ const lines = [
304
+ 'Kryptheon checked this project and found ' + n + ' problems. Fix all ' + n + ', carefully - not breaking anything matters more than speed.',
305
+ '',
306
+ 'Rules for every fix:',
307
+ '1. Fix only the problems listed below. Do not refactor, rename, reformat or "improve" anything else, and do not touch a file a fix does not need.',
308
+ '2. Make the smallest change that fixes each one. Every feature must keep working exactly as it does now for the people meant to use it.',
309
+ '3. Never delete or weaken a check, test, validation or security rule to make a problem go away, and never delete or rewrite existing data.',
310
+ '4. Do them one at a time, in the order below. After each one, make sure the app still builds and its existing tests still pass before starting the next.',
311
+ '5. If a fix needs a decision only I can make, or you are not sure a change is safe, stop and ask me instead of guessing.',
312
+ '6. When you are done, list every file you changed and why, one line each.',
313
+ ];
314
+ withPrompt.forEach((f, i) => {
315
+ lines.push('', '======== Problem ' + (i + 1) + ' of ' + n + ' - ' + f.severity + ': ' + f.headline + (f.where ? ' (' + f.where + ')' : ''), '', f.prompt.trim());
316
+ });
317
+ lines.push('', '======== When all ' + n + ' are fixed',
318
+ '',
319
+ 'Prove each fix: run these one at a time (or press Re-check on each finding in the Kryptheon dashboard). A fix counts only when it says FIXED, and a re-check also says if anything else newly broke - if it does, fix that before anything else.',
320
+ '');
321
+ withPrompt.forEach((f) => lines.push(' npx kryptheon recheck ' + f.id));
322
+ return lines.join('\n');
323
+ }
324
+
287
325
  /**
288
326
  * One sentence about the nightly run inside the database, or null when
289
327
  * kryptheon-night has never been asked. Its four states are four different
@@ -433,9 +471,11 @@ function buildState(root, env) {
433
471
  // prove the fix. Added here, where the loop lives, so every check's prompt
434
472
  // ends the same way and the detection code itself stays untouched.
435
473
  const attempts = readHistory(paths.fixes, 500);
474
+ const forAll = [];
436
475
  for (const f of findings) {
437
476
  f.id = changes.findingId(f);
438
477
  f.attempts = attempts.filter((a) => a.id === f.id).map((a) => ({ at: a.at, verdict: a.verdict, why: a.why || '', newProblems: (a.newProblems || []).length }));
478
+ forAll.push({ id: f.id, severity: f.severity, headline: f.headline, where: f.where, prompt: f.fixPrompt || '' });
439
479
  if (f.fixPrompt) {
440
480
  f.fixPrompt += '\n\nWhen you have fixed it, prove it: run npx kryptheon recheck ' + f.id +
441
481
  ' (or press Re-check on this finding in the Kryptheon dashboard). It is fixed only when that says FIXED.';
@@ -497,6 +537,7 @@ function buildState(root, env) {
497
537
  proven: proven,
498
538
  checks: checks,
499
539
  findings: findings,
540
+ allFixPrompt: combinedPrompt(forAll),
500
541
  runs: history.slice().reverse().map((r) => ({ runAt: r.runAt, status: r.status, passed: r.passed, failed: r.failed, durationMs: r.durationMs })),
501
542
  recordings: recordings,
502
543
  steps: steps,
@@ -858,6 +899,7 @@ module.exports = {
858
899
  databaseVerdict: databaseVerdict,
859
900
  startSteps: startSteps,
860
901
  nextAction: nextAction,
902
+ combinedPrompt: combinedPrompt,
861
903
  readConfig: readConfig,
862
904
  setCheck: setCheck,
863
905
  createServer: createServer,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kryptheon",
3
- "version": "0.1.31",
3
+ "version": "0.1.32",
4
4
  "description": "Record what you do in your app and get plain-language reports when it breaks.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",